8.2 ניצולת SM, pipe ו issue efficiency פתרון
פתרון - ניצולת - SM, pipe ו-issue efficiency¶
הערה: כל המספרים בפלטים למטה הם דוגמאות ממכונת H100 SXM עם CUDA 12 ו-Nsight Compute 2024.x, וישתנו אצלכם לפי הכרטיס, גרסת הדרייבר ומצב המערכת. מה שלא ישתנה הוא המבנה, סדרי הגודל והמגמות. חישובי-העיפרון (הצפי של 1/132, חוק Little) מדויקים ואינם תלויים בחומרה. כל הפקודות מניחות ש-profile.cu קומפל עם nvcc -O2 -arch=sm_90a -o profile profile.cu כפי שב-הכנה (החליפו ל-sm_75 על T4, ואז 132 ה-SM הופכים ל-40).
פתרון תרגיל 1 - ניצולת GPU מול ניצולת SM¶
בזמן שהריצה ./profile compute 1 256 40000 פעילה, nvidia-smi dmon -s u מראה:
ואילו ncu על אותה הlaunch בדיוק נותן את האמת:
הצפי בעיפרון: block יחיד רץ על SM אחד מתוך 132, ולכן ניצולת ה-SM = 1/132 = 0.0076 = 0.76% - בדיוק המספר שהתקבל. הפער מ-nvidia-smi הוא פי ~130.
התיקון - הצפת כל ה-SM עם grid גדול:
ncu -s 1 -c 1 \
--metrics sm__cycles_active.avg.pct_of_peak_sustained_elapsed \
./profile compute 1056 256 40000
מ-0.76% ל-99.3% - קפיצה של פי 130 בעבודה השימושית, בלי לגעת בשורת חישוב אחת, רק בתצורת הlaunch.
למה זה עבד: nvidia-smi מודד ניצולת GPU/kernel - מדד גס שאומר רק "רץ kernel". block יחיד מספיק כדי לפנן אותו ל-100%. ניצולת ה-SM האמיתית מודדת ממוצע על כל 132 ה-SM, ולכן היא רואה ש-131 מהם בטלים. הסיומת elapsed היא הקריטית כאן: היא כוללת את המחזורים הבטלים במכנה, ולכן חושפת את הבטלה - לו השתמשנו ב-active היינו מחריגים בדיוק את מה שרצינו למדוד.
איך להכליל: כל kernel עם grid קטן מדי מ-nvidia-smi נראה בריא ובעצם משאיר את רוב המכונה בטלה. הכלל: ודאו שיש לפחות 2 x מספר_ה-SM blocks (רצוי הרבה יותר) כדי לתת לכל SM עבודה ולתזמן ממה לבחור. nvidia-smi הוא בדיקת החיים הראשונה, אף פעם לא האבחנה הסופית.
פתרון תרגיל 2 - איזה pipe שולט¶
המדידות על שני ה-kernels מסתדרות כך:
./profile compute 1056 256 4096
sm__inst_executed_pipe_fma...active 82.40 %
sm__inst_executed_pipe_tensor...active 0.00 %
sm__inst_executed_pipe_lsu...active 1.10 %
./profile mem 1056 256
sm__inst_executed_pipe_fma...active 4.30 %
sm__inst_executed_pipe_tensor...active 0.00 %
sm__inst_executed_pipe_lsu...active 88.70 %
בטבלה:
| kernel | fma | tensor | lsu | הpipe השולט |
|---|---|---|---|---|
| compute | 82.4% | 0% | 1.1% | fma (CUDA Cores) |
| mem | 4.3% | 0% | 88.7% | lsu (Load/Store) |
הpipe ה-tensor הוא 0% בשניהם כי אף אחד מה-kernels לא מנפיק הוראת HMMA/WMMA. שרשרת ה-x = x*a + b מתקמפלת ל-FFMA על ה-CUDA Cores, לא על ה-Tensor Cores; אלה מונעים רק מ-API של wmma/mma או מספריות כמו cuBLAS/cuDNN (פרק 6). Tensor Cores בשקט אינם אשמה - הם פשוט לא רלוונטיים לעומס הזה.
למה זה עבד: לכל kernel יש "טביעת אצבע" של pipes. compute_fma הוא לולאת חשבון טהורה, ולכן pipe ה-fma עולה גבוה וה-lsu כמעט אפס (רק load אחד בכניסה ו-store אחד ביציאה, מבוזרים על אלפי איטרציות). mem_stream הוא ההפך - load ו-store לכל thread כמעט בלי חשבון, ולכן ה-lsu רווי. הpipe הדומיננטי הוא הרמז הראשון לסיווג Roofline: fma דומיננטי מרמז על compute-bound, lsu דומיננטי על memory-bound (שיעור 7.2).
איך להכליל: הריצו את שלושת מדדי ה-pipe על כל kernel חדש כדי לזהות מיד היכן העבודה מתרכזת. אבל תמיד יחד עם ניצולת ה-SM (תרגיל 1): ניצולת pipe ממוצעת רק על מחזורים פעילים, אז 82% fma על kernel שרוב הזמן רץ על SM יחיד היא מספר חסר-משמעות עד שמוודאים שה-SM עצמם רוויים.
פתרון תרגיל 3 - יעילות הנפקה ו-occupancy¶
מדידת שלושת הצעדים (מ-warp יחיד לכל SM עד occupancy גבוה):
./profile compute 132 32 20000 (1 warp / block, ~1 block / SM)
smsp__issue_active...active 26.10 % <- issue efficiency
sm__warps_active...active 1.55 % <- achieved occupancy
./profile compute 132 128 20000 (4 warps / block)
smsp__issue_active...active 74.80 %
sm__warps_active...active 6.20 %
./profile compute 528 256 20000 (8 warps / block, many blocks)
smsp__issue_active...active 97.30 %
sm__warps_active...active 49.10 %
בטבלה:
| תצורה | achieved occupancy | issue efficiency |
|---|---|---|
| 132 x 32 | 1.55% | 26.1% |
| 132 x 128 | 6.2% | 74.8% |
| 528 x 256 | 49.1% | 97.3% |
הקשר ברור ומונוטוני: ככל שה-occupancy עולה, יעילות ההנפקה עולה איתה - עד שהיא נרוויה קרוב ל-100%. חוק Little בעיפרון: latency של FFMA ~4 מחזורים, throughput רצוי = הנפקה למחזור לכל scheduler, ולכן צריך בערך 4 x 1 = 4 warps כשירים לכל scheduler. ל-H100 יש 4 schedulers ל-SM, כלומר בערך 4 x 4 = 16 warps תושבים ל-SM כדי להסתיר את השרשרת - וזה בדיוק סדר-הגודל שבו יעילות ההנפקה מזנקת בטבלה (בין הצעד השני לשלישי).
למה זה עבד: השרשרת x = x*a + b תלויה - כל FFMA ממתין לתוצאת קודמו. עם warp יחיד לכל scheduler, אחרי כל הנפקה ה-warp נתקע ~4 מחזורים ואין ממי אחר להנפיק, אז המתזמן יושב בטל ויעילות ההנפקה צונחת ל-26%. הוספת warps ממלאת את החורים: בכל מחזור warp אחר כשיר, המתזמן מנפיק ברציפות, והיעילות עולה. זה חוק Little משיעור 7.4 בפעולה - occupancy היא הסיבה, issue efficiency הוא הסימפטום.
איך להכליל: כשאתם רואים issue efficiency נמוך (smsp__issue_active נמוך), החשוד המיידי הוא לא מספיק warps כשירים - כלומר occupancy נמוכה מדי או שרשראות תלויות ארוכות. העלו TLP (יותר warps) או שברו את התלות (יותר ILP, משתני-צבירה מרובים). זהירות מהכיוון ההפוך: מרגע שיש מספיק warps להסתרה, occupancy נוסף לא מוסיף (שיעור 8.1) - היעד הוא יעילות הנפקה גבוהה, לא occupancy מקסימלי.
פתרון תרגיל 4 - מסלול הדיבוג המלא¶
המדידה על ה-kernel החולה:
./profile compute 1 256 40000
sm__cycles_active...elapsed 0.76 % <- layer 2: SM utilization
sm__inst_executed_pipe_fma...active 79.90 % <- layer 3: pipe (misleading!)
smsp__issue_active...active 71.40 % <- layer 4: issue
פסקת האבחנה: nvidia-smi הראה 100% ניצולת GPU (שכבה 1 עברה), אז ירדנו לשכבה 2 - וניצולת ה-SM היא 0.76% בלבד. כאן עוצרים בעץ ההחלטה. הצוואר הוא שה-grid מריץ block יחיד, כך ש-131 מתוך 132 ה-SM בטלים. אסור להיתפות ל-fma של 79.9%: הוא ממוצע רק על ה-SM היחיד שעובד ומחריג את 131 הבטלים (מלכודת "המחזורים הפעילים"), ולכן הוא נראה בריא בזמן שהמכונה כמעט כולה מובטלת. השורה הבאה שנשנה היא תצורת הlaunch - להגדיל את ה-grid.
התיקון והמדידה החוזרת:
ncu -s 1 -c 1 --metrics \
sm__cycles_active.avg.pct_of_peak_sustained_elapsed,\
sm__inst_executed_pipe_fma.avg.pct_of_peak_sustained_active,\
smsp__issue_active.avg.pct_of_peak_sustained_active \
./profile compute 1056 256 40000
sm__cycles_active...elapsed 99.30 % <- layer 2 fixed
sm__inst_executed_pipe_fma...active 83.10 %
smsp__issue_active...active 95.80 %
השכבה שהשתפרה היא שכבה 2 - ניצולת ה-SM זינקה מ-0.76% ל-99.3%. עכשיו, כשכל ה-SM עובדים, מספר ה-fma סוף-סוף אמין (83%), ואפשר לרדת הלאה ולשאול אם 83% זה הגבול (שיעור 7.1, roofline).
למה זה עבד: העץ אילץ אותנו לתקן את השכבה הגסה קודם. לו קפצנו ישר לשכבה 3 היינו "מתקנים" kernel שכבר הראה fma של 80% - ומבזבזים זמן על 1/132 מהמכונה. רק אחרי שהרווינו את ה-SM המספרים העמוקים יותר נהיו בעלי משמעות.
איך להכליל: זה התהליך לכל kernel, תמיד באותו סדר - GPU util (nvidia-smi) -> SM util -> pipe util -> issue efficiency. אל תרדו שכבה עד שהשכבה שמעליה גבוהה כמה שהיא יכולה. מדד עדין תמיד ממוצע רק על מה שהשכבה הגסה השאירה פעיל, ולכן קריאתו לפני שהשכבה הגסה תוקנה מטעה בהכרח.
פתרון תרגיל 5 (בונוס) - למה nvidia-smi ו-ncu לא מסכימים¶
בזמן ש-./profile compute 1 256 2000000 & רץ ברקע, nvidia-smi --query-gpu=utilization.gpu --format=csv -l 1 מדווח:
ואילו ncu על אותו kernel נותן sm__cycles_active...elapsed = 0.76%.
ההסבר: השניים מודדים דברים שונים לחלוטין. nvidia-smi מדווח ניצולת GPU/kernel - מדד גס, כמעט בינארי, שדוגם "האם רץ kernel כלשהו בחלון הדגימה". block יחיד מספיק כדי שהתשובה תהיה "כן" בכל דגימה, ולכן הוא מראה ~100%. הוא לא מבחין בין SM אחד עובד ל-132 עובדים - זה מחוץ לרזולוציה שלו. ncu לעומתו סופר מחזורי-שעון ברמת כל SM וממצע על כל 132 ה-SM, ולכן הוא רואה ש-131 מהם בטלים ומדווח 0.76%. לכן nvidia-smi הוא כלי ניטור מערכת (שיעור 5.4) מצוין לבדיקת החיים "האם ה-GPU עובד או בטל" - הצעד הראשון בדיבוג - אבל הוא עיוור לניצול הפנימי, ולכן לעולם לא הצעד האחרון. את זה עושה profiler.
למה זה עבד: הפער הוא ההדגמה החיה של שתי הקומות העליונות בהיררכיה - הקומה הגסה (nvidia-smi) משקרת ב-100%, והקומה השנייה (ncu) חושפת את האמת ב-0.76%. אותו kernel, שני מספרים שרחוקים פי 130, ושניהם "נכונים" - כי הם מודדים שאלות שונות.
איך להכליל: אל תסמכו על nvidia-smi כדי לקבוע אם kernel יעיל. השתמשו בו כדי לענות "האם ה-GPU בטל?" (אם כן - הבעיה בhost/תזמון, שיעור 7.4), ורק אם הוא מראה עומס, רדו ל-ncu כדי למדוד את הניצולת האמיתית שכבה-שכבה.