8.3 Warp divergence ו scoreboard stalls תרגול
תרגול - Warp divergence ו-scoreboard stalls¶
בתרגול הזה תראו במו עיניכם את שתי המכשלות מהשיעור - התפצלות ה-warp ועצירות ה-scoreboard - חיות ב-SASS וב-Nsight Compute. תתחילו מ-kernel מתפצל: תהדרו אותו, תשלפו את ה-SASS ותזהו בו את שלישיית ה-FSETP / FADD / @P0 FMUL ואת נקודת ההתכנסות, ואז תמדדו ב-ncu את ה-branch efficiency ואת מספר ה-threads הפעילים לפקודה. אחר כך תוכיחו שבדיקת-הגבולות if (idx < n) היא כמעט חינמית (יעילות ~100%). בהמשך תבודדו את שני סוגי ה-scoreboard stall: long scoreboard ב-kernel memory-bound, ו-short scoreboard ב-kernel עתיר-טרנסצנדנטלים. לבסוף תלמדו לקרוא את סימון המחסום [B..:W..:S..] על זוג load-then-use אמיתי, ובבונוס תיתקלו במלכודת מיחזור ה-scoreboards. עבדו לפי הסדר. הריצו הכל על GPU של NVIDIA; אם אין לכם, השתמשו ב-Google Colab (T4 חינמי) או במכונת ענן כפי שראינו בשיעור 0.2. ה-H100 הוא כרטיס הייחוס (החליפו sm_90a בארכיטקטורה שלכם - למשל sm_75 ל-T4).
הכנה¶
צרו קובץ divergence.cu עם מקרו הבדיקה, ה-kernel המתפצל, וממלא נתונים אקראיים:
#include <cstdio>
#include <cstdlib>
#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)
__global__ void divergent_kernel(float* data, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
if (data[idx] > 0.5f) {
data[idx] = data[idx] * 4.0f; // Block A
} else {
data[idx] = data[idx] + 2.0f; // Block B
}
data[idx] = data[idx] * data[idx];
}
}
הcompilation: nvcc -O2 -arch=sm_90a -o divergence divergence.cu.
תרגיל 1 - לקרוא את ה-SASS של ההתפצלות¶
- הדרו את
divergence.cuושלפו את ה-SASS של ה-kernel:cuobjdump --dump-sass ./divergence(או הדרו ל-cubin עםnvcc -cubin -arch=sm_90a divergence.cu -o divergence.cubinואזnvdisasm -c divergence.cubin). - אתרו את פקודת ה-
FSETP- איזה predicate היא קובעת, ומה התנאי (השוואה לאיזה קבוע)? - אתרו את פקודת ה-
FADDואת פקודת ה-FMULהמותנית (@P0). איזו מהן מבוצעת עבור כל ה-threads, ואיזו רק עבור חלקם? קשרו כל אחת ל-Block A / Block B בקוד המקור. - זהו את פקודת ההתכנסות - הפקודה שאחריה כל ה-threads שוב מריצים יחד (הריבוע
data[idx]*data[idx]). - הסבירו במשפט: מדוע ה-FADD מבוצעת גם עבור threads שערכם גדול מ-0.5, למרות שהתוצאה שלה נדרסת מיד?
רמז: חפשו בפלט את המחרוזות FSETP, FADD, FMUL. הפקודה המותנית מתחילה ב-@P0. הפקודה הלא-מותנית (בלי @) היא זו שרצה לכולם. זכרו את הפילוסופיה מההרצאה: הcompiler מעדיף לבזבז FADD זול מאשר להוסיף predication נוסף.
תרגיל 2 - למדוד branch efficiency והתפצלות ב-ncu¶
- הוסיפו
mainשמקצה מערך שלN = 1<<20floats, ממלא אותו בערכים אקראיים ב-[0,1) (עםrand()/RAND_MAX), מעתיק ל-device, ומשיק אתdivergent_kernel<<<(N+255)/256, 256>>>(d_data, N). עטפו כל קריאת CUDA ב-CUDA_CHECK. - הריצו את ה-branch efficiency:
ncu --metrics smsp__sass_average_branch_targets_threads_uniform.pct ./divergence. - הריצו את מדד ה-threads הפעילים לפקודה:
ncu --metrics smsp__thread_inst_executed_per_inst_executed.ratio ./divergence. כמה threads פעילים בממוצע מתוך 32? - הסבירו את שני המספרים. מדוע ה-branch efficiency יוצאת סביב 50% ולא 0% (רמז: יש שני ענפים ב-kernel, לא אחד)? מדוע ה-active threads יוצאים סביב 16?
רמז: הנתונים אקראיים אחידים, ולכן התנאי הפנימי data > 0.5 מפצל כל warp כמעט תמיד לחצי-חצי. אבל בדיקת-הגבולות החיצונית idx < n אחידה כמעט תמיד. ה-branch efficiency היא ממוצע על שני הענפים. ה-.ratio של active threads הוא מספר בין 0 ל-32.
תרגיל 3 - להוכיח שבדיקת-הגבולות כמעט חינמית¶
- כתבו
bounds_only.cu: kernel שכל מה שהוא עושה הוא בדיקת-גבולות וכתיבה אחידה, בלי תנאי תלוי-נתונים:
- השיקו אותו עם
nשאיננו כפולה של 32, כדי להבטיח שיהיה warp אחד שרוכב על הגבול - למשלn = 1000003,blockDim = 256. - מדדו:
ncu --metrics smsp__sass_average_branch_targets_threads_uniform.pct ./bounds_only. - חשבו ידנית את ה-branch efficiency הצפויה: כמה warps רצים בסך הכל, כמה מתוכם מתפצלים, ומה היחס? השוו למספר שמדדתם.
- נסחו את המסקנה: האם "יש לי if ב-kernel" פירושו בהכרח התפצלות?
רמז: gridDim = ceil(1000003/256) = 3907, כלומר 3907 x 256 = 1,000,192 threads = 31,256 warps. רק ה-warp שרוכב על idx = 1,000,003 מתפצל. היעילות היא (31256-1)/31256. אתם אמורים לקבל ~99.99%.
תרגיל 4 - long scoreboard מול short scoreboard¶
- כתבו שני kernels בקובץ
stalls.cu: mem_bound: memory-bound טהור -out[i] = in[i]על מערך גדול (הרבהLDG/STG, מעט חישוב).transcendental: עתיר-SFU - כל thread מריץ לולאה של הרבהsinf/expf/sqrtfעל ערך יחיד באוגר, עם גישת זיכרון מינימלית. למשל:
- פרפלו כל אחד עם פירוק סיבות העצירה:
ncu --section WarpStateStats ./stalls. (או הריצוncu --set fullוקִראו את הטבלה "Warp State Statistics".) - עבור
mem_bound- מהי סיבת העצירה הדומיננטית? עבורtranscendental- מהי? - שלפו את ה-SASS של
transcendental(cuobjdump --dump-sass) ואתרו את פקודות ה-MUFU(למשלMUFU.SIN,MUFU.EX2,MUFU.RSQ). קשרו אותן ל-short scoreboard.
רמז: LDG/STG יוצאים מה-SM אל ה-HBM ולכן גורמים ל-Stall Long Scoreboard; MUFU.* הן פעולות SFU תוך-SM עם latency משתנה ולכן גורמות ל-Stall Short Scoreboard. sinf/expf/sqrtf מתהדרות ל-MUFU.SIN/MUFU.EX2/MUFU.RSQ בתוספת חישובי range-reduction.
תרגיל 5 - לקרוא את סימון המחסום על זוג load-then-use¶
- כתבו kernel קטן שטוען שני ערכים משני מצביעים בלתי-תלויים ואז מחבר/מכפיל אותם:
- שלפו את ה-SASS עם סימון המחסום:
nvcc -cubin -arch=sm_90a stalls.cu -o k.cubin && nvdisasm -c k.cubin(אוcuobjdump --dump-sass ./stalls). - אתרו את שתי פקודות ה-
LDGואת פקודת השימוש (IMAD/IMUL/STG). לכל אחת, קִראו את השדות בסוגריים[B..:R..:W..:..:S..]. - ענו: איזה scoreboard כל
LDGמסמן (שדה ה-W)? על איזה scoreboard פקודת השימוש חוסמת (שדה ה-B)? הסבירו כיצד המחסום מבטיח שהכפל לא יונפק לפני ששני ה-loads חזרו. - אם שני ה-
LDGהשתמשו באותו scoreboard בשדה ה-W - הסבירו מה המשמעות לגבי המתנת פקודת השימוש.
רמז: הפורמט הוא [B<barrier>:R<read>:W<write>:<yield>:S<stall>]. W2 פירושו "מסמן scoreboard 2"; B--2--- פירושו "המתן ל-scoreboard 2 שיתנקה". שדה ה-S הוא מספר מחזורי-העצירה הסטטי. אם שני ה-loads מסמנים את אותו scoreboard, חסימה עליו ממתינה לשניהם.
תרגיל 6 (בונוס) - מלכודת מיחזור ה-scoreboards¶
- הריצו
ncu --set fullעל ה-kerneltranscendentalמתרגיל 4 והסתכלו בסיבות העצירה. אם ה-kernel מערבב גם MUFU (short) וגם LDG (long), האם ncu מפריד ביניהן נקי? - שלפו את ה-SASS ובדקו: כמה scoreboards שונים באמת בשימוש (הסתכלו על שדות ה-W השונים)? זכרו שיש רק 6.
- בנו מקרה שבו load (long) ופעולת MUFU (short) חולקים בטעות את אותו scoreboard, והסבירו מדוע ncu עלול לתייג את העצירה בסיווג הלא-נכון.
- נסחו כלל אצבע: מתי סומכים על תווית ה-short/long של ncu, ומתי חוזרים ל-SASS כמקור-האמת?
רמז: ה-glossary מזהיר במפורש שמיחזור scoreboards עלול לגרום ל-ncu לבלבל short ו-long כשהם משתמשים באותו scoreboard. עם 6 scoreboards בלבד ל-warp, הcompiler ממחזר אותם בקוד עמוס. כשהמספרים מפתיעים - cuobjdump --dump-sass, אתרו את הפקודה החמה, וקִראו ישירות את שדה ה-B ואת הפקודה שסימנה את אותו scoreboard בשדה ה-W.