לדלג לתוכן

7.2 Compute bound, memory bound ו arithmetic intensity תרגול

תרגול - Compute-bound, memory-bound ו-arithmetic intensity

בתרגול הזה תהפכו את מודל ה-Roofline לכלי עבודה מספרי. תתחילו בחישוב ידני של arithmetic intensity לשלושה גרעינים ותסווגו כל אחד מול ה-ridge point של H100; אחר כך תנתחו את דוגמת ה-LLM של 1 TB - תחשבו intertoken latency ותמצאו את גודל ה-batch הדרוש כדי להתקרב ל-compute-bound; לאחר מכן תסבירו בעזרת טבלת ה-ridge point למה FP8/FP4 מקשים להישאר compute-bound; ולבסוף תציעו טרנספורמציה שמעלה arithmetic intensity לגרעין memory-bound, ותאמתו הכל אמפירית עם ncu. חלק מהתרגילים הם עיפרון-ונייר, וחלק דורשים GPU (H100 הוא כרטיס הייחוס, אך כל כרטיס NVIDIA יעבוד, כולל T4 חינמי ב-Google Colab). עבדו לפי הסדר - כל תרגיל נשען על הקודם.

הכנה

לצד המחשבון, כדאי שיהיה מותקן CUDA Toolkit לתרגילים האמפיריים. בררו את הכרטיס ואת רוחב הפס התיאורטי שלו:

nvidia-smi --query-gpu=name,memory.total --format=csv
nvidia-smi -q | grep -A2 -i "Max.*Clocks\|Memory"

מספרי הייחוס שנשתמש בהם לאורך התרגול (H100 SXM, מתוך ההרצאה): arithmetic bandwidth של כ-989 TFLOP/s ב-BF16 ו-1979 TFLOP/s ב-FP8; memory bandwidth של 3.35 TB/s; ומכאן ridge point של 295 (BF16) ו-592 (FP8) FLOPs לכל בית. שמרו את הטבלה מההרצאה בהישג יד.

תרגיל 1 - חישוב וסיווג של arithmetic intensity

כאן אין קוד - רק עיפרון, נייר ומחשבון. עבדו על שלושה גרעינים והשוו כל אחד ל-ridge point של H100 ב-BF16 (295 FLOPs/byte).

  1. SAXPY (y = a*x + y, וקטורים באורך N, דיוק float של 4 בתים): כתבו את מספר ה-FLOPs, את מספר הבתים שזזים (זכרו: קריאת x, קריאת y, כתיבת y), חשבו את ה-arithmetic intensity, וסווגו - compute-bound או memory-bound?
  2. פעולה איבר-איבר מהצורה z = x + y (חיבור וקטורים, שני קלטים ופלט אחד, float): חזרו על אותו תהליך. שימו לב במה המכנה שונה מ-SAXPY.
  3. SGEMM (C = A@B + C, מטריצות מרובעות N x N, float): כתבו את ה-FLOPs (2N^3), את הבתים (16N^2, ארבע מטריצות), חשבו את הarithmetic intensity כפונקציה של N, ומצאו את ה-N המינימלי שבו הגרעין נעשה compute-bound על H100.

רמז: לכל גרעין, ה-arithmetic intensity הוא FLOPs / bytes. ב-SAXPY וב-חיבור הווקטורים ה-N יתבטל ותקבלו מספר קבוע - השוו אותו ל-295. ב-SGEMM ה-N לא יתבטל; קבלתם N/8, ואז פתרו את אי-השוויון N/8 >= 295. שימו לב שחיבור הווקטורים (z = x + y) מזיז 12 בתים לכל איבר אבל מבצע רק FLOP אחד - arithmetic intensity נמוכה אף מ-SAXPY.

תרגיל 2 - intertoken latency ו-batch לדוגמת ה-LLM

עיפרון ונייר. עבדו על מודל LLM של 500 מיליארד פרמטרים בדיוק 16 ביט (2 בתים לפרמטר), בדיוק כמו בהרצאה. הפעם, במקום ה-GPU ההיפותטי, השתמשו בכרטיס H100 אמיתי: memory bandwidth של 3.35 TB/s ו-arithmetic bandwidth של 989 TFLOP/s ב-BF16.

  1. חשבו את הגודל הכולל של המודל בבתים.
  2. חשבו את ה-intertoken latency המינימלי ל-decode של batch יחיד: כמה זמן לוקח לטעון את כל המשקלים פעם אחת ב-3.35 TB/s? זה הזמן לכל token.
  3. חשבו את ה-arithmetic intensity של decode ל-batch יחיד (רמז: כ-2 FLOPs לפרמטר במונה, כל המשקלים במכנה), וסווגו אותו מול ה-ridge point של H100 (295).
  4. מצאו את גודל ה-batch שבו הגרעין מתקרב ל-compute-bound: מכיוון שהarithmetic intensity גדלה לינארית ב-batch, איזה B מביא את הarithmetic intensity ל-ridge point?
  5. הסבירו בשורה: מה קורה ל-throughput כשעולים מ-batch 1 ל-batch שמצאתם, וכמה latency נוסף זה עולה?

רמז: גודל = 500·10^9 × 2 בתים. זמן טעינה = גודל חלקי רוחב הפס. הarithmetic intensity ל-batch יחיד = (2 × 500·10^9) FLOP / (10^12 בתים). מכיוון שהarithmetic intensity ל-batch של B היא B פעמים זו של batch יחיד, השוו B × (arithmetic intensity יחיד) = 295. ה-throughput גדל פי B בזמן שה-latency לכל forward pass נשאר זהה - זהו ה"batching בחינם".

תרגיל 3 - מדוע FP8/FP4 מקשים להישאר compute-bound

עיפרון ונייר, בעזרת טבלת ה-ridge point מההרצאה.

  1. חשבו מחדש את שלושת ערכי ה-ridge point של B200 מהטבלה מתוך רוחבי הפס: BF16 (2250 TFLOP/s), FP8 (4500 TFLOP/s), FP4 (9000 TFLOP/s), כולם מול memory bandwidth של 8 TB/s. ודאו שקיבלתם 281, 562, 1125.
  2. הסבירו במילים: כשיורדים מ-BF16 ל-FP8 ל-FP4, מה קורה ל-arithmetic bandwidth ומה קורה ל-memory bandwidth? למה היחס (ה-ridge point) קופץ?
  3. קחו גרעין SGEMM עם arithmetic intensity של N/8. מצאו את ה-N המינימלי להיות compute-bound על B200 בכל אחד משלושת הדיוקים. מה זה אומר על גודל המטריצה שצריך כדי למצות את החומרה ב-FP4 לעומת BF16?
  4. נסחו את הפרדוקס בשורה אחת: דיוק נמוך נותן יותר כוח חישוב, אבל דורש יותר arithmetic intensity כדי לנצל אותו. למה זו לא סתירה?

רמז: ridge point = arithmetic BW / memory BW. ב-FP8 ה-arithmetic BW מוכפל מול BF16 בעוד ה-memory BW זהה (אותו HBM3e של 8 TB/s), ולכן הרכס מוכפל. עבור SGEMM פתרו N/8 >= ridge, כלומר N >= 8 × ridge: ל-BF16 תקבלו N >= 2248, ל-FP4 N >= 9000. המטריצה שצריך ב-FP4 גדולה בהרבה - הדיוק הנמוך "רעב" יותר לarithmetic intensity.

תרגיל 4 - להעלות arithmetic intensity לגרעין memory-bound

בחרו גרעין memory-bound והציעו טרנספורמציה קונקרטית שמעלה את ה-arithmetic intensity שלו ומזיזה אותו ימינה על ה-Roofline.

  1. קחו כ"קורבן" שרשרת של פעולות איבר-איבר, למשל d = relu(a*x + b) המחושבת כשלושה גרעינים נפרדים: t1 = a*x, t2 = t1 + b, d = relu(t2). חשבו את ה-arithmetic intensity של המימוש הזה: כמה בתים נעים אם כל גרעין קורא את הקלט שלו וכותב פלט ל-global memory, וכמה FLOPs?
  2. הציעו את הטרנספורמציה: kernel fusion - איחוד שלושת הגרעינים לאחד שקורא x פעם אחת, מבצע את שלושת השלבים באוגרים, וכותב d פעם אחת. חשבו מחדש את הבתים ואת הarithmetic intensity.
  3. תַארו טרנספורמציה שנייה מההרצאה - דחיסה (compression) או gradient checkpointing - והסבירו איזו עבודת זיכרון היא חוסכת ובאיזו עבודת חישוב היא משלמת.
  4. הסבירו למה ההעלאה הזו "כמעט חינם" כשהגרעין המקורי היה memory-bound, ומתי היא מפסיקה להשתלם.

רמז: המימוש הלא-מאוחד מזיז את הביניים t1 ו-t2 הלוך ושוב ל-global memory - הרבה בתים על מעט FLOPs. ה-fusion שומר את הביניים באוגרים ומעלה את היחס FLOPs/byte. ההעלאה משתלמת כל עוד אתם משמאל ל-ridge point (החישוב היה בטל בין כה); היא מפסיקה להשתלם כשהגעתם לרכס והחישוב עצמו נעשה צוואר הבקבוק.

תרגיל 5 - אימות אמפירי עם ncu

הפכו את הניתוח התיאורטי לתצפית. כתבו שני גרעינים - אחד memory-bound בעליל ואחד compute-bound - ותנו ל-Nsight Compute לאשר את הסיווג.

  1. צרו intensity.cu עם שני גרעינים: saxpy (y[i] = a*x[i] + y[i], arithmetic intensity 1/6) ו-GEMM דרך cuBLAS (arithmetic intensity גבוהה על מטריצות גדולות). קמפלו:
nvcc -arch=sm_90a -o intensity intensity.cu -lcublas
  1. הריצו כל אחד תחת ניתוח ה-Roofline האוטומטי של ncu, או לפחות בקשו את מטריקות רוחב הפס וניצול הpipelines:
ncu --set full ./intensity
  1. אתרו בפלט של SAXPY את אחוז ניצול הזיכרון (dram__throughput) ואת אחוז ניצול pipeline החישוב, ואשרו שהזיכרון קרוב לרוויה בעוד החישוב נמוך - זהו memory-bound. עשו את ההפך ל-GEMM.

רמז: המטריקות המפתח הן dram__throughput.avg.pct_of_peak_sustained_elapsed (אחוז מרוחב הפס לזיכרון) מול sm__pipe_tensor_cycles_active.avg.pct_of_peak_sustained_active או sm__throughput... (ניצול pipelines החישוב). ב-SAXPY הראשון גבוה והשני נמוך; ב-GEMM ההפך. ncu --set full מפיק גם דיאגרמת Roofline מלאה עם הנקודה שלכם מסומנת - בדיוק הגרף מ-7.1.

תרגיל 6 (בונוס) - למדוד arithmetic intensity ישירות ב-ncu

אל תסתפקו בחישוב ידני - תנו לפרופיילר לחשב את ה-arithmetic intensity שלכם ולמקם אתכם על ה-Roofline.

  1. הריצו את גרעין ה-SAXPY שלכם ובקשו את שתי המטריקות שמרכיבות את הarithmetic intensity: סך פעולות הנקודה-צפה שבוצעו, וסך הבתים שזזו מ-DRAM ואליו.
ncu --metrics \
  smsp__sass_thread_inst_executed_op_fadd_pred_on.sum,\
smsp__sass_thread_inst_executed_op_fmul_pred_on.sum,\
smsp__sass_thread_inst_executed_op_ffma_pred_on.sum,\
dram__bytes.sum ./intensity
  1. חשבו FLOPs מהמונים (זכרו: ffma נספרת כ-2 FLOP - כפל וחיבור), חלקו בסך הבתים מ-dram__bytes.sum, וקבלו את ה-arithmetic intensity הנמדד.
  2. השוו לערך התיאורטי 1/6 שחישבתם בתרגיל 1. הסבירו כל פער (למשל השפעת caches, יישור, גודל הבעיה).

רמז: FLOPs = fadd + fmul + 2*ffma (כי כל ffma היא MAC = כפל + חיבור). חלקו ב-dram__bytes.sum. אם הווקטור קטן וחלקים ממנו נשארים בcache L2, ה-dram__bytes.sum יהיה נמוך מהצפוי וה-arithmetic intensity הנמדדת תעלה מעל 1/6 - הגדילו את הווקטור עד שהוא חורג בבירור מ-L2 כדי להתקרב לתיאוריה.