8.2 ניצולת SM, pipe ו issue efficiency תרגול
תרגול - ניצולת - SM, pipe ו-issue efficiency¶
בתרגול הזה תמדדו את כל ארבע שכבות היררכיית הניצולת במו ידיכם, על אותו קובץ מקור אחד. תתחילו מהתופעה המכוננת - kernel של block יחיד שמראה 100% ניצולת GPU ב-nvidia-smi אבל מתחת ל-1% ניצולת SM ב-ncu - ותתקנו אותה בשינוי תצורת הlaunch בלבד. משם תרדו שכבה: תפרופלו kernel חישובי ותקראו את ניצולת pipes ה-fma, ה-tensor וה-lsu כדי לראות איזה pipe שולט. אחר-כך תיצרו ביד kernel עם יעילות הנפקה נמוכה - שרשרת FFMA תלויה עם מעט warps - ותקשרו את זה ל-occupancy ולחוק Little משיעור 7.4. לבסוף תלכו את כל מסלול הדיבוג top-down על kernel אחד ותכתבו אבחנה. עבדו לפי הסדר - כל תרגיל בונה על קודמו. הריצו הכל על GPU אמיתי עם Nsight Compute מותקן; אם אין לכם, השתמשו ב-Google Colab (T4 חינמי) או במכונת ענן כפי שראינו בשיעור 0.2. ה-H100 הוא כרטיס הייחוס (132 SM), אבל כל תרגיל ירוץ על כל GPU של NVIDIA - רק המספרים המוחלטים ישתנו (על T4 החליפו 132 ב-40 SM).
הכנה¶
צרו קובץ profile.cu. הוא מכיל שני kernels - אחד חישובי (שרשרת FFMA, שולט בpipe ה-fma) ואחד זיכרוני (העתקה, שולט ב-lsu) - ו-main שבוחר kernel ותצורת launch מהפרמטרים בשורת-הפקודה. כך כל התרגילים משתמשים באותו בינארי ורק משנים ארגומנטים.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cuda_runtime.h>
#define CUDA_CHECK(call) \
do { \
cudaError_t err_ = (call); \
if (err_ != cudaSuccess) { \
fprintf(stderr, "CUDA error at %s:%d\n '%s'\n -> %s\n", \
__FILE__, __LINE__, #call, cudaGetErrorString(err_)); \
exit(EXIT_FAILURE); \
} \
} while (0)
// pure compute load: dependent FFMA chain. dominates the fma pipe.
// the chain is intentionally dependent - so latency hiding (occupancy) affects issue efficiency.
__global__ void compute_fma(const float* in, float* out, int n, int iters) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
float x = in[idx];
const float a = 1.0000001f, b = 0.9999999f;
#pragma unroll 1
for (int i = 0; i < iters; i++) x = x * a + b; // dependent FFMA
out[idx] = x;
}
}
// memory load: one load, one store, almost no arithmetic. dominates the lsu.
__global__ void mem_stream(const float* in, float* out, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) out[idx] = in[idx] * 2.0f;
}
int main(int argc, char** argv) {
const char* mode = argc > 1 ? argv[1] : "compute";
int blocks = argc > 2 ? atoi(argv[2]) : 132 * 8; // default: flood all the SMs
int threads = argc > 3 ? atoi(argv[3]) : 256;
int iters = argc > 4 ? atoi(argv[4]) : 4096;
const int N = 132 * 8 * 256; // fixed size, enough for every configuration
float *in, *out;
CUDA_CHECK(cudaMalloc(&in, (size_t)N * sizeof(float)));
CUDA_CHECK(cudaMalloc(&out, (size_t)N * sizeof(float)));
CUDA_CHECK(cudaMemset(in, 0, (size_t)N * sizeof(float)));
// warmup (ncu will skip it with -s 1)
if (!strcmp(mode, "mem")) mem_stream<<<blocks, threads>>>(in, out, N);
else compute_fma<<<blocks, threads>>>(in, out, N, iters);
CUDA_CHECK(cudaGetLastError());
CUDA_CHECK(cudaDeviceSynchronize());
cudaEvent_t s, e;
CUDA_CHECK(cudaEventCreate(&s));
CUDA_CHECK(cudaEventCreate(&e));
CUDA_CHECK(cudaEventRecord(s));
if (!strcmp(mode, "mem")) mem_stream<<<blocks, threads>>>(in, out, N);
else compute_fma<<<blocks, threads>>>(in, out, N, iters);
CUDA_CHECK(cudaEventRecord(e));
CUDA_CHECK(cudaEventSynchronize(e));
float ms = 0.0f;
CUDA_CHECK(cudaEventElapsedTime(&ms, s, e));
printf("mode=%s blocks=%d threads=%d iters=%d -> %.3f ms\n",
mode, blocks, threads, iters, ms);
CUDA_CHECK(cudaFree(in));
CUDA_CHECK(cudaFree(out));
return 0;
}
הקומפילציה (החליפו sm_90a בארכיטקטורה שלכם - למשל sm_75 ל-T4):
לאורך התרגול נשתמש ב-ncu כדי לפרופל רק את הlaunch השנייה (המדודה) ולדלג על החימום, עם -s 1 -c 1 (דלג 1, ספור 1). ריצת ncu דורשת הרשאות; על מכונת ענן/Colab ייתכן שתצטרכו sudo או דגל --target-processes all.
תרגיל 1 - ניצולת GPU מול ניצולת SM¶
- הריצו ידנית
./profile compute 1 256 40000(block יחיד, iters גדול כדי שירוץ מספיק זמן). במקביל, בטרמינל שני, הריצוnvidia-smi dmon -s uוהביטו בעמודת ה-sm %. - עכשיו מדדו את ניצולת ה-SM האמיתית עם
ncu:
- רשמו את שני המספרים זה-לצד-זה. חשבו ידנית מה צפוי: block יחיד רץ על SM אחד מתוך 132 - כמה זה באחוזים?
- תקנו: הריצו שוב עם grid שמציף את כל ה-SM -
./profile compute 1056 256 40000(1056 = 132 x 8) - ומדדו שוב את ניצולת ה-SM. השוו.
רמז: block לעולם לא מתפצל בין SM, לכן block יחיד = SM אחד עובד, 131 בטלים. הצפי הוא 1/132 = 0.76%. nvidia-smi יראה sm % גבוה כי הוא רק שואל "היה SM פעיל בחלון הדגימה", לא "כמה SM היו פעילים". שימו לב לסיומת elapsed במדד - היא כוללת את המחזורים הבטלים במכנה, ולכן חושפת את ה-SM המובטלים (בניגוד ל-active שהיינו משתמשים בו ל-pipe).
תרגיל 2 - איזה pipe שולט - pipe utilization¶
- פרופלו את ה-kernel החישובי וקראו את שלושת pipes הביצוע במדידה אחת:
- חזרו על אותה מדידה בדיוק על ה-kernel הזיכרוני:
./profile mem 1056 256. - סדרו את שש התוצאות בטבלה 2x3 (compute מול mem, על פני fma/tensor/lsu). איזה pipe שולט בכל kernel?
- הסבירו במשפט: מדוע
pipe_tensorהוא 0% בשני המקרים, למרות שה-H100 מלא ב-Tensor Cores?
רמז: compute_fma הוא לולאת x = x*a + b - כל איטרציה היא הוראת FFMA אחת על ה-CUDA Cores, ולכן pipe ה-fma יגיע גבוה וה-lsu יהיה זניח (load אחד בהתחלה, store אחד בסוף). mem_stream הפוך - load ו-store לכל thread כמעט בלי חשבון, ולכן ה-lsu שולט. ה-tensor נשאר 0 כי אף אחד מהשניים לא מנפיק הוראת HMMA/WMMA - pipe ה-Tensor מונע רק מ-wmma/mma או מספריות כמו cuBLAS (שיעור 6.1). זכרו: ניצולת pipe ממוצעת רק על מחזורים פעילים, אז קראו אותה יחד עם ניצולת ה-SM מתרגיל 1.
תרגיל 3 - יעילות הנפקה נמוכה ו-occupancy¶
- הריצו את ה-kernel החישובי עם מעט warps מאוד - warp אחד לכל block, block אחד בערך לכל SM:
ncu -s 1 -c 1 --metrics \ smsp__issue_active.avg.pct_of_peak_sustained_active,\ sm__warps_active.avg.pct_of_peak_sustained_active \ ./profile compute 132 32 20000
המדד הראשון הוא יעילות ההנפקה (Issue Slot Utilization); השני הוא ה-occupancy שהושג בפועל (Achieved Occupancy). - עכשיו הגדילו בהדרגה את מספר ה-warps התושבים והריצו שוב אחרי כל שינוי -
./profile compute 132 128 20000, ואז./profile compute 528 256 20000. רשמו את שני המדדים בכל צעד. - שרטטו טבלה: occupancy מול issue efficiency לאורך שלושת הצעדים. מה הקשר ביניהם?
- חוק Little בעיפרון: השרשרת
x = x*a + bהיא FFMA תלוי, ו-latency של FFMA הוא בערך 4 מחזורים. כמה warps כשירים לכל scheduler צריך בערך כדי להסתיר את זה ולמלא כל מחזור? (רמז: זו בדיוק המדידה שראיתם).
רמז: כשיש warp יחיד לכל scheduler, אחרי כל הנפקת FFMA ה-warp נתקע ~4 מחזורים עד שהתוצאה מוכנה (תלות), ואין warp אחר להנפיק ממנו - אז המתזמן יושב בטל ויעילות ההנפקה צונחת. כשמוסיפים warps, בכל מחזור יש warp כשיר אחר, החורים מתמלאים, ויעילות ההנפקה עולה. זה בדיוק חוק Little משיעור 7.4: כדי להסתיר latency של 4 מחזורים ב-throughput של הנפקה למחזור צריך בערך 4 warps כשירים לכל scheduler. שימו לב: occupancy נמוכה היא הסיבה, יעילות הנפקה נמוכה היא הסימפטום הנמדד.
תרגיל 4 - מסלול הדיבוג המלא - top-down¶
- בחרו kernel אחד "חולה" - הריצו
./profile compute 1 256 40000. - הלכו את כל ארבע השכבות עליו, בסדר, במדידה אחת:
- כתבו פסקת אבחנה קצרה (3-4 משפטים) שעונה: איפה בעץ ההחלטה מהשיעור אתם עוצרים, מהו הצוואר, ומה השורה הבאה שתשנו בקוד. הסבירו במפורש מדוע אסור להיתפות למספר ה-
fmaהגבוה. - יישמו את התיקון שגזרתם, מדדו שוב, והראו איזו שכבה השתפרה.
רמז: המדד הראשון (ניצולת SM) יהיה זעיר (~0.76%), ולכן עוצרים כבר בשכבה 2 של העץ - אין טעם אפילו להסתכל על ה-pipe וה-issue. ה-fma עלול להראות מספר יפה כי הוא ממוצע רק על ה-SM היחיד שעובד ומחריג את 131 הבטלים - זו בדיוק מלכודת "המחזורים הפעילים" מההרצאה. השורה שתשנו היא תצורת הlaunch: <<<1, 256>>> -> <<<1056, 256>>>.
תרגיל 5 (בונוס) - למה nvidia-smi ו-ncu לא מסכימים¶
- הריצו את ה-kernel של block יחיד בלולאה ארוכה ברקע כך שירוץ כמה שניות רצוף (למשל עטפו את הlaunch בלולאת host, או הגדילו את
itersל-2000000 והריצו./profile compute 1 256 2000000 &). - בזמן שהוא רץ, קראו
nvidia-smi --query-gpu=utilization.gpu --format=csv -l 1(דגימה לשנייה). - השוו את המספר הזה למדד
sm__cycles_active...elapsedשלncuעל אותו kernel. - כתבו הסבר: מדוע
nvidia-smiמראה ~100% בעוד ניצולת ה-SM האמיתית מתחת ל-1%? מהו בדיוק ההבדל במה שכל אחד מהם מודד, ולמה זה הופך אתnvidia-smiלצעד ראשון בדיבוג אבל לא לצעד אחרון?
רמז: nvidia-smi מדווח ניצולת GPU/kernel - מדד גס, כמעט בינארי, שאומר רק "היה kernel פעיל בחלון הדגימה". הוא לא מבחין בין SM אחד עובד ל-132 עובדים. ncu מודד את ניצולת ה-SM האמיתית ברמת מחזור-שעון ממוצעת על כל 132 ה-SM, ולכן רואה את 131 הבטלים. זה בדיוק הפער מההרצאה: הקומה העליונה משקרת, הקומה השנייה חושפת.