8.4 Coalescing ו bank conflicts תרגול
תרגול - Coalescing ו-bank conflicts¶
בתרגול הזה תרגישו בידיים את שתי אופטימיזציות דפוס-הגישה הגדולות. תבנו את מדד ה-stride ותשחזרו את קריסת ה-coalescing מ-206 GB/s ל-~11 GB/s; תתקנו kernel לא מאוחד על ידי היפוך תפקידי thread ו-loop, ותמדדו את הרווח; תכתבו transpose נאיבי עם התנגשות בנקים 32-way, תאשרו אותה עם ncu, תרפדו ל-[32][33] ותמדדו את הזינוק; ולבסוף תסבירו במדויק מדוע אופטימיזציה אחת שייכת ל-global memory והשנייה ל-shared memory. עבדו לפי הסדר - כל תרגיל בונה על הקודם. אפשר לעבוד על כל כרטיס NVIDIA מקומי או בחינם ב-Google Colab. אם אתם ב-Colab על T4, החליפו -arch=sm_90 ב--arch=sm_75, הוסיפו ! לפני כל פקודת shell, והשתמשו ברוחב הפס של ה-T4 (peak ~320 GB/s של GDDR6) כמכנה במקום ה-HBM3 של ה-H100 - העיקרון זהה, המספרים שונים.
הכנה - כלים ומאקרו¶
לכל התרגילים נשתמש במאקרו בדיקת השגיאות הסטנדרטי; שימו אותו בראש כל קובץ .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)
קמפלו תמיד עם ארכיטקטורה ואופטימיזציה, למשל nvcc -arch=sm_90 -O3 file.cu -o file. לפרופיילינג צריך את ncu (Nsight Compute), שמגיע עם ה-CUDA Toolkit.
תרגיל 1 - בניית מדד ה-stride ושחזור קריסת ה-coalescing¶
נבנה את ה-benchmark שמודד רוחב פס מושג כפונקציה של ה-stride, ונשחזר את הטבלה מהשיעור.
- צרו
strided.cuעם ה-kernelstrided_read_kernelמהשיעור (קריאה עם stride, הצטברות ל-acc, כתיבה ל-outכדי למנוע מחיקה). - ב-
main, הקצו מערך שלN = 64Mfloats (64ull << 20), אתחלו אותו, והריצו חימום (warmup) אחד לפני המדידה. - לכל
strideבקבוצה{1, 2, 4, 8, 16, 32, 64, 128}: מדדו את הזמן עםcudaEventעל פני כמה איטרציות, חשבו רוחב פס מושג, והדפיסו שורה. - ספירת הבייטים: ה-kernel קורא בערך
N / strideאלמנטים (הלולאה חסומה ב-j < Nבצעדי stride). לכן הבייטים השימושיים הם(N / stride) * sizeof(float). חלקו בזמן ל-GB/s. - הריצו והשוו לטבלה בשיעור. האם קיבלתם את הצורה: חצייה בכל הכפלה עד stride 8, ואז השטחה מ-stride 16?
רמז: הבייטים השימושיים הם (double)N / stride * sizeof(float). השתמשו ב-grid = 4096, block = 256. אם אתם על H100 המספרים המוחלטים יהיו גבוהים בהרבה מ-T4 (stride 1 קרוב ל-2800-3000 GB/s), אבל הצורה של הקריסה זהה. חממו לפני המדידה ומדדו על פני 20-50 איטרציות.
תרגיל 2 - אישור ה-coalescing עם ncu (sectors per request)¶
נאשר שהקריסה היא באמת מ-coalescing ולא ממשהו אחר, דרך מדד ה-sectors-per-request.
- הריצו את
strided(עם stride קבוע 1) תחת ncu עם המדדl1tex__average_t_sectors_per_request_pipe_lsu_mem_global_op_ld.ratio. - הריצו שוב עם stride 32. השוו את המדד בין שתי ההרצות.
- הסבירו: כמה sectors לבקשה מצפים לקבל בגישה מאוחדת (stride 1), וכמה בגישה מפוזרת (stride 32)? קשרו למספר 4 (128B = 4 sectors של 32B) ולמקסימום 32.
- איזה מהשניים - ה-sectors-per-request או ה-GB/s מתרגיל 1 - הוא מדד "ישיר" יותר של coalescing, ולמה?
רמז: קריאה מאוחדת של warp מביאה 128B = 4 sectors של 32B לבקשה. קריאה שבה כל thread נופל ב-sector אחר תביא עד 32. כדי לקבע את ה-stride, קמפלו גרסה עם stride קשיח, או קבלו אותו כארגומנט והריצו ./strided 1 ו-./strided 32.
תרגיל 3 - תיקון kernel לא מאוחד ומדידת הרווח¶
נכתוב kernel לסיכום שורות מטריצה בשתי גרסאות - לא מאוחדת ומאוחדת - ונמדוד את ההבדל.
- צרו
rowsum.cuעם מטריצהM x N(למשלM = 8192, N = 8192) שמורה row-major ב-global memory. - ממשו את
rowsum_bad: כל thread לוקח שורה שלמה (for col: acc += A[row*N+col]). זכרו שזה נותן ל-threads שכנים כתובות במרחקN*4בייט. - ממשו את
rowsum_good: block אחד לשורה, ו-threadIdx.xרץ על העמודות בצעדיblockDim.x, כך ש-threads שכנים קוראים כתובות רצופות. (לצמצום בתוך ה-block אפשר להשתמש ב-atomicAdd ל-s[row]או בצמצום shared פשוט; ההתמקדות היא בדפוס הקריאה מ-global.) - מדדו את שתי הגרסאות עם
cudaEventוהדפיסו את היחס (speedup). - הריצו את שתיהן תחת ncu עם
l1tex__average_t_sectors_per_request_pipe_lsu_mem_global_op_ld.ratio. איזו גרסה מראה ~4 ואיזו מראה ~32? כיצד זה מסביר את ה-speedup?
רמז: הפי-כמה-שיפור צריך לנחות בטווח פי 8 עד פי 20 - בדיוק היחס בין stride 1 ל-stride גדול בטבלה. הכלל: ה-threadIdx.x חייב לרוץ על הממד הרציף בזיכרון (העמודה, ב-row-major). אמתו את שתי הגרסאות שמחזירות אותה תוצאה מספרית לפני שמשווים זמנים.
תרגיל 4 - transpose נאיבי והתנגשות בנקים 32-way¶
נכתוב transpose דרך shared memory ונאשר את ההתנגשות 32-way.
- צרו
transpose.cuעםtransposeNaiveמהשיעור:__shared__ float tile[32][32], קריאה מאוחדת מ-global אלtile[threadIdx.y][threadIdx.x],__syncthreads(), וכתיבה מ-tile[threadIdx.x][threadIdx.y]ל-global. - הריצו על מטריצה
4096 x 4096, ואמתו נכונות מול transpose על ה-CPU (או מול העתקהout[j][i] = in[i][j]). - חשבו ביד את הבנק שכל thread ב-warp נוגע בו בקריאה
tile[threadIdx.x][threadIdx.y]. הראו שכולם נופלים על בנקthreadIdx.y(קבוע ל-warp) -> התנגשות 32-way. - הריצו תחת ncu עם
l1tex__data_bank_conflicts_pipe_lsu_mem_shared_op_ld.sum. כמה התנגשויות אתם רואים? מדדו גם את הזמן עםcudaEvent.
רמז: הכתובת (במילים) של tile[threadIdx.x][threadIdx.y] היא threadIdx.x * 32 + threadIdx.y, והבנק הוא הביטוי mod 32. שימו לב ש-(threadIdx.x * 32) mod 32 = 0 תמיד, ולכן נשאר רק threadIdx.y. השתמשו ב-block של dim3(32, 8) או dim3(32, 32).
תרגיל 5 - ריפוד ל-[32][33] ומדידת השיפור¶
נתקן את ההתנגשות בשינוי של תו אחד, ונמדוד.
- שנו את ההצהרה ל-
__shared__ float tile[32][33](עמודת ריפוד אחת). אל תשנו שום דבר אחר בקוד. - חשבו ביד מחדש את הבנק לקריאה
tile[threadIdx.x][threadIdx.y]עם stride שורה 33. הראו שהוא הופך ל-(threadIdx.x + threadIdx.y) mod 32, שעובר על כל 32 הבנקים -> אפס התנגשויות. - הריצו תחת ncu עם אותו מדד
l1tex__data_bank_conflicts_pipe_lsu_mem_shared_op_ld.sum. אמתו שהמונה צנח כמעט לאפס. - מדדו את הזמן והשוו ל-
transposeNaiveמתרגיל 4. מה ה-speedup? כמה shared memory נוסף "עלה" הריפוד (באחוזים)?
רמז: 33 = 32 + 1, ולכן (threadIdx.x * 33) mod 32 = (threadIdx.x * 32 + threadIdx.x) mod 32 = threadIdx.x. הריפוד עולה 33/32 = ~3% מקום נוסף ב-shared וקונה עד פי 32 בקריאה. אמתו שהתוצאה המספרית עדיין נכונה אחרי הריפוד - הריפוד לא אמור לשנות את הפלט, רק את הביצועים.
תרגיל 6 - שידור מול התנגשות (broadcast)¶
נחדד את התנאי "כתובות שונות" של התנגשות בנקים.
- כתבו kernel קטן שבו כל ה-warp קורא
data[0](אותה כתובת בדיוק) לתוך משתנה מקומי. חשבו: כל ה-threads באותו בנק (בנק 0), אבל אותה כתובת. האם זו התנגשות? - הריצו אותו תחת ncu עם מדד ה-bank conflicts. כמה התנגשויות? הסבירו את התוצאה במונח "שידור (broadcast)".
- עכשיו שנו ל-
data[threadIdx.x % 1]... לא, שנו ל-data[(threadIdx.x % 2) * 32]- כלומר חצי מה-threads קוראים מילה 0 וחצי מילה 32, שתיהן בבנק 0 אך בכתובות שונות. איזו דרגת התנגשות מצפים (2-way? 32-way?)? אמתו עם ncu. - סכמו בטבלה קטנה את שלושת המצבים: בנקים שונים, אותו בנק+אותה כתובת, אותו בנק+כתובות שונות - ומה הקנס בכל אחד.
רמז: שידור קורה רק כשהכתובת זהה; אז אין קנס. במקרה (threadIdx.x % 2) * 32 יש שתי כתובות שונות (0 ו-32) שתיהן בבנק 0, אז זו התנגשות 2-way - אבל שימו לב שהחומרה עשויה לשדר בתוך כל קבוצת-אותה-כתובת, כך שהקנס הוא כמספר הכתובות המובחנות בבנק, לא כמספר ה-threads.
תרגיל 7 (בונוס) - למה אחת ל-global והשנייה ל-shared¶
תרגיל הסבר שמחבר את הכל.
- הסבירו בפסקה מדוע coalescing לא רלוונטי כלל ל-shared memory. רמז: מה מקור ה-128B transaction, והאם ל-shared memory יש bursts של DRAM?
- הסבירו מדוע bank conflicts לא רלוונטיים כלל ל-global memory. רמז: היכן יושבים 32 הבנקים, ומה מגבה את ה-global memory?
- בנו טבלת 2x2: (coalescing, bank conflict) מול (global, shared), וסמנו איפה כל אחד חי ואיפה הוא חסר משמעות.
- תרחיש: kernel שמעתיק מ-global אל shared, מעבד ב-shared, וכותב חזרה ל-global. באיזה שלב אתם דואגים ל-coalescing ובאיזה שלב ל-bank conflicts?
רמז: ה-128B burst הוא תכונה של ה-DRAM שמגבה את ה-global memory; ל-shared (SRAM על השבב) אין bursts, יש בנקים. ההפך: הבנקים הם ארגון פיזי של ה-SRAM המשותף; ל-global אין בנקים. בתרחיש: coalescing בשלבי הקריאה/כתיבה ל-global, bank conflicts בשלב העיבוד ב-shared.