לדלג לתוכן

1.2 ה SM לעומק תרגול

תרגול - ה-SM לעומק

בתרגול הזה תחקרו את ה-SM משני כיוונים: מלמעלה, בעזרת תוכנית שקוראת את מאפייני החומרה בזמן ריצה, ומלמטה, בעזרת חישובים ידניים והבנה מושגית של מה שראינו בהרצאה. חלק מהתרגילים הם עיוניים (הסבר/חישוב על נייר), וחלקם דורשים GPU של NVIDIA. אם אין לכם כרטיס, הריצו את החלקים המעשיים ב-Google Colab (T4 חינמי) או במכונת ענן, כפי שראינו בשיעור 0.2. ה-H100 הוא כרטיס הייחוס שלנו, אבל התוכניות כאן ירוצו על כל GPU של NVIDIA - פשוט תקבלו מספרים אחרים, וזו כל הנקודה. עבדו לפי הסדר: כל תרגיל בונה על הקודם.

הכנה

צרו קובץ smquery.cu והעתיקו אליו את תוכנית ה-deviceQuery המלאה מההרצאה (הכוללת את מקרו CUDA_CHECK ואת הקריאה ל-cudaGetDeviceProperties). הקומפילציה לאורך התרגול (החליפו את sm_90a בארכיטקטורה של הכרטיס שלכם - למשל sm_75 ל-T4):

nvcc -O2 -arch=sm_90a -o smquery smquery.cu

אם אינכם יודעים את הארכיטקטורה, בררו קודם עם:

nvidia-smi --query-gpu=name,compute_cap --format=csv

תרגיל 1 - קריאת המאפיינים של ה-SM שלכם

  1. קמפלו והריצו את smquery.
  2. רשמו מהפלט: שם הכרטיס, ה-compute capability, מספר ה-SMs, מספר ה-warps המקסימלי ל-SM, ומספר האוגרים ל-SM.
  3. הצליבו את ה-compute capability עם טבלת המשפחות בהרצאה - לאיזו משפחת ארכיטקטורה שייך הכרטיס שלכם (Pascal / Volta / Turing / Ampere / Ada / Hopper / Blackwell)?
  4. הצליבו את מספר ה-compute capability עם הפלט של nvidia-smi --query-gpu=compute_cap. הם צריכים להתאים.

רמז: השדה p.major וה-p.minor יחד נותנים את ה-compute capability (למשל 9 ו-0 עבור H100 = 9.0). מספר ה-warps ל-SM הוא maxThreadsPerMultiProcessor / warpSize. אם nvidia-smi מדווח 9.0 וה-API מדפיס 9.0 - התאמתם. אם יש לכם כמה כרטיסים, cudaGetDeviceProperties(&p, 0) קורא רק את מספר 0.

תרגיל 2 - ה-SM מול ה"ליבה": מדוע ה-SM הוא המעבד

זהו תרגיל עיוני (על נייר). ראיתם בהרצאה את הטענה שה-SM, ולא ליבת CUDA, הוא המקבילה של ליבת CPU.

  1. נסחו במילים שלכם את ההבחנה בין "pipe" לבין "מעבד". מה בדיוק חסר לליבת CUDA בודדת כדי להיות מעבד?
  2. מנו את שלושת המרכיבים ההכרחיים של מעבד שקיימים ב-SM, והסבירו בקצרה מה כל אחד תורם.
  3. חבר מפרסם GPU עם "16,896 CUDA Cores" ומסיק שהוא יכול להריץ 16,896 תוכניות עצמאיות בו-זמנית. הסבירו לו במדויק מדוע המסקנה שגויה, ומהו המספר הנכון שמתאר "כמה יחידות עצמאיות" יש בכרטיס.

רמז: הpipe יודע רק "המר נתון" - אין לו מונה-פקודות, אין לו מתזמן, ואין לו זיכרון פרטי מעבר לאוגרים שמוזנים אליו. שלושת המרכיבים הם קובץ האוגרים, הליבות (יחידות החישוב), ומתזמני ה-warp. "היחידות העצמאיות" הן ה-SMs (132 ב-H100), לא הליבות.

תרגיל 3 - חישוב parallel מול concurrent לכרטיס שלכם

זהו תרגיל חישובי. השתמשו במספרים שקראתם בתרגיל 1.

  1. חשבו את מספר הthreads המקביליים באמת (parallel) של הכרטיס שלכם: (מספר תת-מחיצות = 4) x 32 x (מספר SMs). (מספר תת-המחיצות הוא 4 בכל הארכיטקטורות המודרניות; הוא שווה גם למספר מתזמני ה-warp ל-SM.)
  2. חשבו את מספר הthreads המקביליים-לכאורה (concurrent / תושבים): maxThreadsPerMultiProcessor x (מספר SMs).
  3. חשבו את היחס בין השניים. עבור H100 היחס הוא 2048/128 = 16. מה קיבלתם?
  4. הסבירו במילים שלכם: מהו התפקיד של הפער בין שני המספרים בהסתרת ה-latency? מה היה קורה אם ה-SM היה יכול להחזיק בדיוק כמה threads שהוא מנפיק בכל מחזור (יחס 1:1)?

רמז: החישוב ל-H100: parallel = 4 x 32 x 132 = 16,896; concurrent = 2048 x 132 = 270,336. הפער הוא מאגר ההסתרה: כשה-warps הרצים נתקעים בהמתנה לזיכרון, המתזמן שולף warp מוכן אחר מהמאגר. ביחס 1:1 לא היה מאגר, וכל המתנה לזיכרון הייתה משביתה את ה-SM לחלוטין - אין למי להחליף.

תרגיל 4 - מדוע "אין חיזוי הסתעפויות" מקובל ב-GPU אבל לא ב-CPU

זהו תרגיל עיוני. ה-SM מוותר על חיזוי הסתעפויות (branch prediction) - מנגנון שליבת CPU משקיעה בו סיליקון רב.

  1. הסבירו מה חיזוי הסתעפויות עושה ב-CPU, ומה המחיר של ניחוש שגוי (רמזו: ריקון הpipeline - pipeline flush).
  2. הסבירו מדוע ל-CPU, שמריץ מעט threads, אין ברירה אלא לנחש: מה הוא יעשה עם הpipeline בזמן שהתנאי של ה-if עדיין מחושב, אם לא ינחש?
  3. הסבירו כיצד ה-GPU פותר את אותה בעיה בלי לנחש: מה עושה מתזמן ה-warp כשה-warp הנוכחי מגיע להסתעפות שהתנאי שלה עדיין לא מוכן?
  4. נסחו את המשפט המסכם: איזה משאב יש ל-GPU בשפע ול-CPU אין, וזה מה שהופך את הוויתור על הניבוי למקובל דווקא ב-GPU.

רמז: ל-CPU יש מעט threads, ולכן כשthread תקוע אין לו למי להחליף - הוא חייב לנחש כדי לא לבזבז את הpipeline, גם במחיר של ריקון יקר בטעות. ל-GPU יש עשרות warps תושבים, ולכן במקום לנחש הוא פשוט מחליף ל-warp מוכן בעלות של מחזור אחד. המשאב שיש ל-GPU בשפע הוא מקביליות (warps תושבים), וזה מה שמייתר את הניחוש.

תרגיל 5 - תאימות SASS בין גרסאות ראשיות

זהו תרגיל מעשי שמדגים מדוע "תאימות בין גרסאות ראשיות אינה מובטחת". הריצו על כרטיס Hopper (H100, 9.0), או התאימו את המספרים לכרטיס שלכם (בחרו גרסה ראשית ישנה מזו של הכרטיס).

  1. קחו kernel פשוט כלשהו (למשל vecadd.cu מהתרגול הקודם, או כתבו kernel טריוויאלי בן שורה).
  2. קמפלו אותו כך שיטמיע רק SASS של Volta (גרסה ראשית 7), בלי PTX:
nvcc -gencode arch=compute_70,code=sm_70 -o app_v70 vecadd.cu
  1. הריצו את ./app_v70 על ה-H100. רשמו את השגיאה המדויקת שמקרו ה-CUDA_CHECK תופס.
  2. עכשיו קמפלו כך שיטמיע PTX של Volta (ייצוג ביניים נייד), בלי SASS ילידי:
nvcc -gencode arch=compute_70,code=compute_70 -o app_ptx vecadd.cu
  1. הריצו את ./app_ptx על ה-H100. הפעם היא צריכה לעבוד. הסבירו לעצמכם את ההבדל בין שתי ההרצות.

רמז: SASS של גרסה ראשית 7 (Volta) אינו תואם ל-SM של גרסה ראשית 9 (Hopper), ולכן app_v70 נכשל עם no kernel image is available for execution on the device. לעומת זאת, PTX הוא נייד: כשאין SASS מתאים, ה-driver מבצע לו קומפילציה בזמן ריצה (JIT) ל-SASS של Hopper, ולכן app_ptx רץ. זהו מנגנון התאימות קדימה. אם אתם על כרטיס שאינו Hopper, החליפו את המספרים - העיקר לקמפל לגרסה ראשית ישנה מזו של הכרטיס.

תרגיל 6 (בונוס) - לחץ אוגרים וגבול ה-warps התושבים

זהו תרגיל חישובי שקושר בין קובץ האוגרים לבין ה-occupancy. ל-H100 יש 65,536 אוגרים בני 32 סיביות לכל SM, ותקרה של 64 warps תושבים.

  1. kernel משתמש ב-R אוגרים לכל thread. כתבו נוסחה למספר הthreads המקסימלי שיכולים להיות תושבים ב-SM מבחינת אוגרים בלבד: רצפה(65536 / R).
  2. חשבו עבור R = 32, R = 64, R = 128, R = 255 (המקסימום). כמה warps תושבים מתקבלים בכל מקרה (חלקו ב-32)?
  3. באיזה ערך של R מגיעים בדיוק ל-64 ה-warps המלאים (התפוסה המקסימלית)? מעל איזה R מתחילים לאבד warps תושבים?
  4. גלו כמה אוגרים ה-kernel שלכם באמת משתמש: קמפלו עם nvcc -arch=sm_90a -Xptxas -v -c vecadd.cu וקראו את השורה Used N registers. הציבו את ה-N בנוסחה.

רמז: 65536 / (32 threads ל-warp) = 2048 threads מקסימום; חלקו את זה גם בתקרת ה-64 warps. עבור R = 32: 65536/32 = 2048 threads = 64 warps (מלא). עבור R = 64: 65536/64 = 1024 threads = 32 warps (חצי). מעל R = 32 (בקירוב) מתחילים לאבד תפוסה. הדגל -Xptxas -v מדפיס את השימוש באוגרים בזמן קומפילציה - זו הדרך למדוד R בפועל. את המסגרת המלאה של occupancy נלמד בפרק הביצועים.