לדלג לתוכן

7.3 רוחבי פס ו peak rate תרגול

תרגול - רוחבי פס ו-peak rate

בתרגול הזה תיתנו לגגות של ה-Roofline מספרים אמיתיים, ואז תמדדו כמה אתם מתקרבים אליהם בפועל. תחשבו את קצב השיא של הכרטיס שלכם ביד מהמפרט (ותיזהרו מהמלכודת של FMA=2), תכתבו kernel שמעתיק זיכרון device-to-device ותמדדו את רוחב הפס המושג כאחוז מהשיא של ה-HBM, תחשבו נקודות מפנה לשני דיוקים ותקשרו לטבלה של שיעור 7.2, ולבסוף תסבירו - ותוכיחו עם profiler - למה הביצוע בפועל תמיד נמוך מהשיא. עבדו לפי הסדר; כל תרגיל בונה על הקודם. אפשר לעבוד על כל כרטיס NVIDIA מקומי או בחינם ב-Google Colab. אם אתם ב-Colab על T4, החליפו -arch=sm_90 ב--arch=sm_75, הוסיפו ! לפני כל פקודת shell, והשתמשו במפרט של ה-T4 (40 SM, 64 ליבות FP32 ל-SM, ~1.59 GHz, רוחב פס GDDR6 320 GB/s) במקום של ה-H100 - העיקרון זהה, המספרים שונים.

הכנה - קובץ המדידה

לאורך התרגול נשתמש בקובץ מדידה אחד, נרחיב אותו תרגיל אחרי תרגיל. צרו אותו עכשיו עם מאקרו בדיקת השגיאות:

// bwpeak.cu
#include <cstdio>
#include <cstdlib>
#include <cuda_runtime.h>

#define CUDA_CHECK(call)                                                   \
    do {                                                                   \
        cudaError_t err_ = (call);                                         \
        if (err_ != cudaSuccess) {                                         \
            fprintf(stderr, "CUDA error %s at %s:%d\n",                    \
                    cudaGetErrorString(err_), __FILE__, __LINE__);         \
            exit(EXIT_FAILURE);                                            \
        }                                                                  \
    } while (0)

int main() {
    // filled in in exercises 2 and 5
    return 0;
}

קמפלו תמיד עם ציון הארכיטקטורה ואופטימיזציה, למשל nvcc -arch=sm_90 -O3 bwpeak.cu -o bwpeak.

תרגיל 1 - חישוב קצב השיא מהמפרט

לפני שמודדים, מחשבים את התקרה. בתרגיל הזה אין קוד לחישוב עצמו - נייר, עיפרון, ומחשבון - אבל שלפו את המפרט מהכרטיס.

  1. הריצו תוכנית קצרה שקוראת ל-cudaGetDeviceProperties ומדפיסה את multiProcessorCount ואת clockRate (או שלפו את התדר מ-nvidia-smi -q -d CLOCK). לכל SM ב-H100 יש 128 ליבות FP32 (ידיעה סטנדרטית; לא כל השדות ב-cudaDeviceProp חושפים זאת ישירות).
  2. חשבו ביד את קצב השיא של FP32 (non-Tensor) לפי הנוסחה: SM x (ליבות FP32 ל-SM) x (FLOP לליבה למחזור) x תדר. השתמשו ב-132 SM, 128 ליבות, ותדר 1.98 GHz. שימו לב: כמה FLOP יש ב-FMA אחת?
  3. חשבו כעת את קצב השיא של BF16 על ה-Tensor Cores. NVIDIA מפרסמת ל-H100 SXM את המספר 989 TFLOP/s ב-BF16. עבדו הפוך: מכמה FLOP למחזור זה נובע, כמה FLOP ל-SM למחזור, וכמה FMA ל-SM למחזור זה (זכרו לחלק ב-2). השוו את מספר ה-FMA ל-SM למחזור בין ה-FP32 CUDA Core לבין ה-BF16 Tensor Core - פי כמה?
  4. חשבו: אם שכחתם ש-FMA = 2 FLOP, איזה מספר הייתם מקבלים ל-FP32, ובכמה אחוז הייתם טועים?

רמז: FMA (Fused Multiply-Add) מבצעת כפל וחיבור בהוראה אחת, ולכן נספרת כ-2 פעולות ממשיות. קצב שיא FP32 = 132 x 128 x 2 x 1.98e9. לחישוב ההפוך של BF16: התחילו מ-989e12, חלקו בתדר לקבל FLOP/מחזור, חלקו ב-132 לקבל FLOP/SM/מחזור, וחלקו ב-2 לקבל FMA/SM/מחזור.

תרגיל 2 - מדידת רוחב פס מושג של העתקה device-to-device

עכשיו נמדוד את גג הזיכרון בפועל. נכתוב kernel memory-bound מובהק - העתקה טהורה - ונראה כמה אחוז מ-3.35 TB/s אנחנו גורפים.

  1. הוסיפו ל-bwpeak.cu kernel copyKernel שמעתיק out[i] = in[i] עם grid-stride loop על מערך של N = 256M float (1 GiB לכל מערך).
  2. הקצו שני מערכים ב-device עם cudaMalloc, אתחלו את הקלט, והריצו חימום (warmup) אחד לפני המדידה.
  3. מדדו את הזמן עם cudaEvent על פני לפחות 50 איטרציות, וחשבו רוחב פס: כמה בייטים זזו בכל העתקה? (רמז מובנה: כל אלמנט נקרא פעם ונכתב פעם.)
  4. הדפיסו GB/s ואת האחוז מ-3350 GB/s. איזה אחוז קיבלתם? האם הוא בטווח 85%-92%?
  5. שנו את גודל ה-block וה-grid (למשל block של 128 מול 512, grid קטן מול גדול) והריצו שוב. כמה רגיש רוחב הפס לתצורת ההפעלה?

רמז: הבייטים שזזו הם 2 x N x sizeof(float) - קריאה אחת וכתיבה אחת לכל אלמנט. שכחת ה-2 תיתן חצי מהמספר. חלקו את הבייטים בזמן בשניות וב-1e9 ל-GB/s. תמיד חממו לפני המדידה כדי לא לספור overhead הפעלה ראשונה, ומדדו על פני הרבה איטרציות.

תרגיל 3 - נקודת המפנה לשני דיוקים

נחבר את שני רוחבי הפס לנקודת מפנה, ונקשר לטבלה של שיעור 7.2.

  1. חשבו את נקודת המפנה של H100 BF16 TC: 989 TFLOP/s / 3.35 TB/s. שמרו על היחידות (FLOP/s חלקי בייט/s = FLOP/בייט).
  2. חשבו את נקודת המפנה של H100 FP8 TC: 1979 TFLOP/s / 3.35 TB/s. השוו לערך בטבלה של 7.2.
  3. הסבירו בפסקה: מדוע נקודת המפנה של FP8 גבוהה בערך פי 2 מזו של BF16, למרות שרוחב פס הזיכרון זהה? מה זה אומר על כמה עבודה לכל בית צריך kernel כדי להישאר compute-bound ב-FP8?
  4. kernel של הכפלת וקטור בסקלר (y[i] = a * x[i]) קורא 4 בייט וכותב 4 בייט לכל פעולה ממשית אחת - arithmetic intensity של ~0.125 FLOP/בייט. השוו ל-295 ול-591: משמאל או מימין לנקודת המפנה? memory-bound או compute-bound?
  5. בונוס-חישוב: חשבו את נקודת המפנה גם ל-A100 BF16 (312/2) ול-B200 FP4 (9000/8). סדרו את כל ארבע (A100 BF16, H100 BF16, H100 FP8, B200 FP4) בסדר עולה והסבירו את המגמה.

רמז: נקודת מפנה = רוחב פס אריתמטי / רוחב פס זיכרון. 989/3.35 ~= 295; 1979/3.35 ~= 591. ה-FP8 מכפיל את רוחב הפס האריתמטי בלי לשנות את רוחב פס הזיכרון, ולכן מכפיל את נקודת המפנה. הarithmetic intensity של 0.125 קטנה בהרבה מ-295 - עמוק בצד ה-memory-bound.

תרגיל 4 - למה הביצוע בפועל תמיד מתחת לשיא

זהו תרגיל הסבר עם אימות ב-profiler.

  1. כתבו בפסקה, במילים שלכם, שלוש סיבות מדוע kernel אמיתי לא מגיע לקצב השיא. קשרו לשלושת החשודים שהגלוסרי מונה: לחץ אוגרים, מגבלת רוחב פס זיכרון, ומחסומי סנכרון (barriers).
  2. עבור ה-copyKernel מתרגיל 2: האם הוא compute-bound או memory-bound? באיזה משני הגגות הוא "נוגע", ולמה גג החישוב שלו יהיה כמעט אפס?
  3. הריצו את ה-copyKernel תחת ncu עם המדדים gpu__dram_throughput.avg.pct_of_peak_sustained_elapsed ו-sm__throughput.avg.pct_of_peak_sustained_elapsed. איזה מהשניים גבוה ואיזה נמוך? כיצד זה מאשר את התשובה שלכם מסעיף 2?
  4. הסבירו: מדוע גם ה-copyKernel שהוא "אופטימלי" מגיע רק ל-~90% ולא ל-100% מרוחב הפס? מה גוזל את ה-10% האחרונים?

רמז: kernel memory-bound "נוגע" בגג הזיכרון (ה-DRAM throughput גבוה) אבל בקושי בגג החישוב (ה-SM throughput נמוך) - וההפך ל-kernel compute-bound. ה-10% האחרונים אובדים לרענון DRAM, מעברי שורות, ו-overhead התחלה/סיום. השיא הוא תיאורטי בהנחת אפס עצירות.

תרגיל 5 - מדידת FLOP/s מושג ואחוז מהשיא

מדדנו את גג הזיכרון; עכשיו נמדוד את גג החישוב. נכתוב kernel עתיר-FMA טהור ונשווה ל-66.9 TFLOPS.

  1. הוסיפו ל-bwpeak.cu kernel שכל thread מריץ בו לולאה ארוכה של פעולות FMA תלויות-חלקית על ערכים באוגרים בלבד (בלי גישות זיכרון בלולאה הפנימית), למשל מספר משתני מצטבר שכל אחד מקבל acc = acc * x + y; שוב ושוב. השתמשו במספיק threads כדי להרוות את הכרטיס (למשל grid = 132 * 4, block = 256).
  2. ספרו כמה FLOP ה-kernel כולו ביצע: (מספר threads) x (איטרציות ל-thread) x (FMA לאיטרציה) x 2.
  3. מדדו את הזמן עם cudaEvent, חשבו FLOP/s, וחלקו ב-66.9e12. איזה אחוז מהשיא קיבלתם?
  4. הריצו את אותו kernel תחת ncu עם sm__throughput.avg.pct_of_peak_sustained_elapsed והשוו לאחוז שחישבתם ידנית. האם ה-DRAM throughput כאן נמוך (כצפוי מ-kernel compute-bound)?

רמז: כדי שהcompiler לא ימחק את הלולאה, כל איטרציה חייבת להיות תלויה בקודמת, והתוצאה הסופית חייבת להיכתב ל-global memory. השתמשו בכמה שרשראות FMA מקבילות (למשל 4-8 משתני acc) כדי לתת לחומרה מספיק עבודה בלתי-תלויה להסתיר latency ולהתקרב לשיא. הכפילו את מספר ה-FMA ב-2 ל-FLOP.

תרגיל 6 (בונוס) - מיפוי ה-Roofline של שני ה-kernels

עכשיו נשים את שני ה-kernels על ה-Roofline באותו גרף.

  1. עבור ה-copyKernel (תרגיל 2) ועבור ה-kernel עתיר-FMA (תרגיל 5), רשמו זוג מספרים: (arithmetic intensity, ביצוע מושג ב-FLOP/s). ל-copyKernel הarithmetic intensity היא בערך 0 (הוא לא מחשב); ל-kernel החישובי היא גבוהה מאוד (בקושי נוגע בזיכרון).
  2. ציירו את ה-Roofline של H100 (גג חישוב 66.9 TFLOPS ל-FP32 CUDA Core, שיפוע 3.35 TB/s) וסמנו עליו את שתי הנקודות. איזו נקודה יושבת על הגג המשופע ואיזו על הגג האופקי?
  3. הריצו את שניהם תחת ncu עם שני מדדי ה-pct_of_peak_sustained_elapsed (DRAM ו-SM) ובנו טבלה של 2x2: kernel מול (DRAM%, SM%). הראו שהטבלה מזהה בדיוק איזה kernel memory-bound ואיזה compute-bound, בלי לצייר בכלל.

רמז: kernel שנוגע בגג המשופע יראה DRAM% גבוה ו-SM% נמוך; kernel שנוגע בגג האופקי יראה את ההפך. זו בדיוק אבחנת ה-Roofline שנשתמש בה בכל פרויקט מכאן והלאה כדי להחליט מה לייעל.