לדלג לתוכן

8.2 - ניצולת - SM, pipe ו-issue efficiency - הרצאה

בשיעור 8.1 הכרנו את ה-occupancy (תפוסה) - היחס בין ה-warps הפעילים על ה-SM לבין המקסימום - וראינו שהיא אמצעי להסתרת latency, לא מטרה בפני עצמה. אבל occupancy לבדה לא אומרת לנו כמה עבודה שימושית ה-GPU באמת מוציא מהחומרה. בשביל זה צריך משפחה שנייה של מדדים: מדדי הניצולת (utilization) - כמה מהזמן, וכמה מקצב-השיא, החומרה באמת מנצלת. בשיעור הזה נבנה את היררכיית מדדי הניצולת ונלמד לדבג ביצועים בשיטת top-down: מהמדד הכי גס שאפשר לקרוא במבט אחד על nvidia-smi, ועד למדד הכי עדין שנמדד ברמת מחזור-שעון בודד במתזמן ה-warp. סדר הדיבוג הזה קריטי: מדד גס שנראה טוב יכול להסתיר בזבוז עצום ברמה עדינה יותר, ולכן תמיד יורדים במדרג צעד-צעד. את הכלים עצמם - Nsight Compute (ncu) ו-Nsight Systems (nsys) - נכיר לעומק בפרק 9 (שיעור 9.1); כאן נשתמש בהם ככלי מדידה ונתמקד במה מודדים ואיך קוראים את המספרים. ה-H100 (עם 132 ה-SM שלו) יהיה כרטיס הייחוס לאורך כל הדרך.

היררכיית מדדי הניצולת - the utilization hierarchy

ה-glossary מציב ארבעה מדדי ניצולת, מהגס לעדין, וממליץ לדבג אותם בדיוק בסדר הזה. הרעיון פשוט: כל שכבה מכסה שכבה עמוקה יותר, ואין טעם לתקן דבר עדין כל עוד השכבה שמעליו שבורה. אם ה-SM שלכם עומדים בטל חצי מהזמן, אין שום משמעות לשאלה כמה יעיל pipe ה-FMA שלכם ב-cycles שהם כן עובדים.

   coarse to fine - debugging order (top-down):

   ┌─────────────────────────────────────────────────────────────┐
   │ 1. GPU / kernel utilization   (nvidia-smi)                    │  coarse
   │    "is the GPU running any kernel right now?"                 │
   ├─────────────────────────────────────────────────────────────┤
   │ 2. SM utilization            (sm__cycles_active...)           │
   │    "how many of the 132 SMs are truly executing instructions?"│
   ├─────────────────────────────────────────────────────────────┤
   │ 3. pipe utilization          (sm__inst_executed_pipe_*...)    │
   │    "inside a working SM - how much of peak does each          │
   │     execution pipe deliver?"                                  │
   ├─────────────────────────────────────────────────────────────┤
   │ 4. issue efficiency          (smsp__issue_active...)          │  fine
   │    "in how many cycles did the scheduler have an eligible     │
   │     warp to issue?"                                           │
   └─────────────────────────────────────────────────────────────┘
        golden rule: fix top to bottom. don't drop a layer until
        the layer above it is as high as it can be.

שימו לב שכל שכבה שואלת שאלה מוכלת בשאלה שמעליה. שכבה 1 שואלת אם ה-GPU עסוק בכלל. שכבה 2 מתנה על "עסוק" ושואלת כמה מה-SM עסוקים. שכבה 3 מתנה על "SM עסוק" ושואלת כמה מהיחידות בתוכו עסוקות. שכבה 4 מתנה על "יחידה עסוקה" ושואלת כמה מהמחזורים באמת הונפקה הוראה. ככל שיורדים, המכנה מצטמצם וה"עדשה" מתחדדת. נעבור עכשיו שכבה-שכבה.

ניצולת ה-GPU - הקומה העליונה - nvidia-smi

הקומה הגסה ביותר היא ניצולת ה-GPU (או ניצולת ה-kernel), וזה המספר שכולם מכירים - העמודה שקופצת ל-100% ב-nvidia-smi. הגדרתו: חלק הזמן שבו kernel כלשהו רץ איפשהו על ה-GPU. זהו מדד בינארי כמעט - או שיש kernel פעיל, או שאין. הוא לא יודע דבר על כמה מהחומרה ה-kernel הזה מנצל.

אפשר לקרוא אותו בשתי צורות. בצילום-בזק:

nvidia-smi --query-gpu=utilization.gpu,utilization.memory --format=csv
# utilization.gpu [%], utilization.memory [%]
# 100 %, 3 %

או במעקב רציף (מדגם לשנייה):

nvidia-smi dmon -s u
# gpu    sm   mem   enc   dec
# Idx     %     %     %     %
#   0   100     3     0     0

וזו בדיוק המלכודת: ה-sm % של nvidia-smi הוא לא ניצולת ה-SM האמיתית שנדבר עליה מיד. הוא מדגם דגימה גסה של "האם היה SM פעיל בחלון הדגימה", ולא כמה SM היו פעילים בו-זמנית. nvidia-smi הוא כלי ניטור מערכת (כפי שראינו בשיעור 5.4), לא profiler. הוא מצוין כדי לענות "האם ה-GPU עובד או בטל", והוא הצעד הראשון בדיבוג - אבל אם הוא מראה 100% זו רק ההתחלה, לא הסוף.

ניצולת ה-SM - הלב של הסיפור - SM utilization

עכשיו יורדים שכבה. ניצולת ה-SM (SM utilization) מוגדרת כאחוז הזמן שבו ה-SM מריצים הוראות. ה-glossary מנסח זאת יפה: זה "בן-דוד עדין יותר" של ניצולת ה-kernel של nvidia-smi. במקום לשאול "האם ה-GPU פעיל", היא שואלת "כמה מ-132 ה-SM של ה-H100 באמת מריצים הוראות, ובאיזה חלק מהזמן".

ההבחנה הזו היא כנראה השיעור החשוב ביותר בכל הפרק, והדרך הטובה להפנים אותה היא דרך הדוגמה הקנונית של ה-glossary: kernel עם block אחד בלבד.

// kernel that occupies a single block - a classic performance mistake
myKernel<<<1, 256>>>(...);   // grid of size 1: one block, one SM

בלוק אחד רץ, לפי המודל, על SM אחד בלבד (בלוק לעולם לא מתפצל בין SM). כל עוד ה-kernel הזה רץ, nvidia-smi יראה בשמחה 100% ניצולת GPU - יש kernel פעיל. אבל בפועל SM אחד עובד ו-131 עומדים בטל. ניצולת ה-SM האמיתית:

   SM utilization = active SMs / total SMs

   one block  ->  1 SM working out of 132
   =  1 / 132  =  0.0076  =  0.76%

   nvidia-smi will show 100%.   SM utilization is 0.76%.
   a 130x gap that the coarse metric simply doesn't see.

ה-glossary מנסח זאת מדויק: kernel כזה "ישיג 100% ניצולת GPU בזמן שהוא פעיל, אבל ניצולת ה-SM תהיה לכל היותר אחד חלקי מספר ה-SM - מתחת ל-1% ב-H100". זהו בדיוק המקרה שבו הקומה העליונה משקרת לכם, והקומה השנייה חושפת את האמת. התיקון פשוט: הגדילו את ה-grid כך שיהיו מספיק blocks כדי להאכיל את כל 132 ה-SM (ורצוי כמה blocks לכל SM, כדי לתת ל-scheduler ממה לבחור).

איור המשווה בין ניצולת GPU של 100 אחוז שמראה nvidia-smi ל-kernel של block יחיד לבין ניצולת ה-SM האמיתית של 0.76 אחוז, כי block יחיד רץ על SM אחד מתוך 132

נקודה תפיסתית קריטית - הפוך מ-CPU. אצל מהנדסי CPU, ניצולת מעבד של 100% לאורך זמן היא לרוב דגל אזהרה - סימן לתהליך שרץ מטורף או ל-thread תקוע בלולאה. אצל GPU זה בדיוק ההפך: ניצולת SM גבוהה, עד 100%, היא המטרה. ה-glossary אומר זאת במפורש: "כמו ניצולת ה-GPU אבל בניגוד לניצולת ה-CPU, ניצולת ה-SM צריכה להיות גבוהה, אפילו עד 100%". ה-GPU נבנה כדי להיות רווי; SM בטל הוא סיליקון מבוזבז, לא מרווח בטיחות.

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

ניצולת pipes הביצוע - pipe utilization

ניצולת ה-pipe (ניצולת pipes הביצוע) מודדת כמה יעיל ה-kernel מנצל את יחידות הביצוע בתוך כל SM. הנוסח המדויק של ה-glossary: אחוז מקצב-השיא (peak rate) של כל pipe שמושג כשהpipe פעיל ומריץ לפחות warp אחד, ממוצע על פני כל ה-SM הפעילים.

יש בהגדרה הזו ניואנס חד שחייבים להבין, אחרת קוראים את המדד לא נכון: המדד מותנה על כך שהpipe פעיל. הוא מודד הישג-מול-שיא רק במחזורים שבהם הpipe עושה עבודה. מחזורים שבהם הpipe בטל מוחרגים מהממוצע. המשמעות המעשית: הpipe שכמעט לא בשימוש, אבל בכל פעם שכן משתמשים בו הוא רץ בשיא, יראה ניצולת גבוהה - למרות שהוא כמעט תמיד בטל. לכן חייבים לקרוא ניצולת pipe יחד עם occupancy וניצולת SM, אף פעם לא לבד.

ה-glossary מזהה ארבעה pipes ביצוע נפרדים בתוך כל SM:

הpipe מה הוא מריץ דוגמאות הוראות (SASS)
CUDA Cores / FMA חשבון נקודה-צפה כללי FMUL, FADD, FFMA
Tensor Cores כפל-צבירה של מטריצות HMMA, IMMA
Load/Store Units (LSU) גישות זיכרון LDG, STG, LDS, STS
Control Flow / ADU הסתעפויות וחישוב כתובות BRA, BSSY

היכולת של ארבעת הpipes לעבוד במקביל היא בדיוק מה שמאפשר הסתרת latency (שיעור 7.4): warp אחד יכול לרוץ על ה-Tensor Cores, שני על ה-CUDA Cores, ושלישי דרך ה-LSU - כולם בו-בזמן. ניצולת pipe מודדת עד כמה ניצלתם את המקבילות הזו.

המדדים ב-Nsight Compute. ניצולת ה-pipe נחשפת דרך משפחת המדדים:

   sm__inst_executed_pipe_<PIPE>.avg.pct_of_peak_sustained_active

כאשר <PIPE> הוא אחד מ:

   fma     ->  the FMA pipe (CUDA Cores)
   tensor  ->  Tensor Cores
   lsu     ->  Load/Store Units
   adu     ->  Address Unit (address computation)

הסיומת .avg.pct_of_peak_sustained_active היא בדיוק מה שמקודד את שלושת חלקי ההגדרה: pct_of_peak = אחוז מקצב-השיא, .avg = ממוצע על ה-SM, ו-active = רק במחזורים פעילים. נזכיר שהמכנה כאן, קצב-השיא, הוא בדיוק אותו peak rate שחישבנו בשיעור 7.3 (עבור H100 ב-FP32: 66.9 TFLOPS). ניצולת pipe היא, פשוטו כמשמעו, ההישג חלקי אותו peak.

המלכודת, שוב. בגלל שמחזורים בטלים מוחרגים מהממוצע, מספר גבוה של ניצולת pipe לא אומר שהpipe היה בשימוש כבד - רק שבכל פעם שכן השתמשו בו הוא רץ קרוב לשיא. pipe_tensor של 95% על kernel ש-99% מהזמן לא נוגע ב-Tensor Cores הוא מספר חסר-משמעות אם קוראים אותו לבד. לכן ה-glossary ממליץ לתקן ניצולת GPU וניצולת SM לפני שיורדים לרמת ה-pipe.

יעילות הנפקת ההוראות - issue efficiency

הקומה העמוקה ביותר היא יעילות ההנפקה (issue efficiency) - היחידה שנמדדת ברמת מחזור-שעון בודד, אצל מתזמן ה-warp עצמו. ה-glossary מגדיר: חלק המחזורים הפעילים (active cycles) שבהם המתזמן הנפיק הוראה - כלומר, בהם היה לו לפחות warp אחד כשיר (eligible) לבחור ממנו.

  • יעילות הנפקה של 100% = בכל מחזור פעיל, כל scheduler הנפיק הוראה. תמיד היה warp כשיר.
  • מתחת ל-100% = במקצת המחזורים כל ה-warps הפעילים היו תקועים (stalled) - ממתינים לנתונים, למשאב או לתלות - והמתזמן ישב בטל, כך שהתפוקה הכוללת ירדה.

כדי לחבר את זה חזרה לשיעור 8.1: היעילות הזו תלויה ישירות בשלושת מצבי ה-warp שהכרנו. warp פעיל (active) הוא warp תושב על ה-SM; warp כשיר (eligible) הוא warp פעיל שמוכן להנפיק את הוראתו הבאה במחזור הזה; warp תקוע (stalled) הוא warp פעיל שלא יכול. יעילות ההנפקה צונחת בדיוק כשכל ה-warps הפעילים תקועים בו-זמנית, ולמתזמן אין ממי להנפיק:

   cycle:      1    2    3    4    5    6
   warp A:     ISS  --   --   --   ISS  --      (-- = stalled)
   warp B:     --   ISS  --   --   --   ISS
   warp C:     --   --   ISS  --   --   --
   warp D:     --   --   --   [ ]  --   --      <- cycle 4: no one eligible!

   issued in 5 out of 6 cycles  ->  issue efficiency = 83%
   cycle 4 is the "hole" - all warps stalled at once.

התרופה ליעילות הנפקה נמוכה היא כמעט תמיד יותר מקבילות ברמת ה-thread (TLP) - עוד warps כשירים, כדי שבכל מחזור לפחות אחד יהיה מוכן. וזה בדיוק המקום שבו occupancy נכנסת: occupancy נמוכה מדי פירושה מעט warps תושבים, ומעט warps לבחירה, ולכן חורים ביעילות ההנפקה. זו החוליה שסוגרת את המעגל בין 8.1 ל-8.2 - occupancy היא הסיבה האפשרית, ויעילות ההנפקה היא הסימפטום הנמדד.

ה-glossary עצמו לא נותן מחרוזת-מדד ל-issue efficiency, אבל ידע-CUDA סטנדרטי כן: ב-Nsight Compute המדד המקביל הוא Issue Slot Utilization:

   smsp__issue_active.avg.pct_of_peak_sustained_active

הוא נמדד ברמת ה-SM sub-partition (smsp) - כל scheduler הוא sub-partition, וב-H100 יש ארבעה כאלה לכל SM. שימו לב לניואנס לעומת ניצולת ה-pipe: pipe שואל "כמה מקצב-השיא של יחידת החישוב", ואילו issue efficiency שואל "בכמה מהמחזורים בכלל הונפקה הוראה" - האחד על קצב היחידה, השני על נוכחות warp כשיר. אפשר בהחלט להנפיק הוראה בכל מחזור (issue efficiency גבוה) ובכל זאת להאכיל pipe לא-נכון או קל (pipe utilization נמוך), ולהפך.

קריאה משולבת - עץ ההחלטה - the diagnostic decision path

עכשיו מגיע החלק המעניין: המדדים מספרים סיפור רק ביחד. הצירוף של ניצולת SM וניצולת pipe הוא אבחנתי במיוחד. ה-glossary מנסח את המקרה המרכזי: ניצולת SM גבוהה עם ניצולת pipe נמוכה פירושה ש-kernel רץ על הרבה SM אבל לא מנצל במלואן את יחידות החישוב שבתוך כל אחד - "רחב אבל רדוד". פרשׂתם את העבודה יפה על פני 132 ה-SM, אבל בתוך כל SM היחידות רעבות. נסדר את הצירופים בטבלה:

ניצולת SM ניצולת pipe האבחנה הכיוון לתיקון
נמוכה לא רלוונטי לא מאכילים את המכונה הגדילו grid; יותר blocks (טפלו ב-SM קודם)
גבוהה נמוכה "רחב אבל רדוד" - SM עובדים, יחידות רעבות יותר עבודה ל-thread; ILP; בדקו איזה pipe דומיננטי
גבוהה גבוהה (pipe אחד) רוויתם pipe אחד - זהו הצוואר זהו ה-pipe החסום; אזנו/החליפו אלגוריתם
גבוהה גבוהה (מפוזר) ניצול טוב - קרוב לגבול החומרה ייתכן שהגעתם ל-roofline; מדדו מול peak

וזה, בצורת עץ, הוא בדיוק מסלול הדיבוג top-down שאיתו פתחנו:

   start   nvidia-smi: GPU utilization ~0%?
     │        └── yes -> the GPU is idle. the problem is on the host/scheduling (lesson 7.4). stop.
     │        └── no (100%) -> go down a layer.
     ▼
   ncu: SM utilization low (e.g. <20%)?
     │        └── yes -> few SMs working. increase grid / more blocks. fix this first!
     │        └── no (high) -> go down a layer.
     ▼
   ncu: pipe utilization low across every pipe?
     │        └── yes -> "wide but shallow". check issue efficiency ->
     │                  │  low -> warps stalled. raise TLP/occupancy,
     │                  │         investigate stall reasons (rest of chapter 8).
     │                  └  high -> issuing fine, but the work is light. increase
     │                            arithmetic intensity (lesson 7.2), fuse kernels.
     │        └── no (one pipe high) -> this is the bottleneck. it's the dominant pipe.
     ▼
   measure the dominant pipe against peak rate (roofline, lesson 7.1). that's the limit.

שימו לב שהעץ תמיד מטפל בשכבה הגסה קודם. אם ניצולת ה-SM היא 0.76% (ה-kernel של block יחיד), אין שום טעם לפתוח את מדדי ה-pipe - הם ימדדו את ה-SM היחיד שכן עובד ואולי יראו לכם מספרים יפים לגמרי, בזמן ש-131 SM עומדים בטל. קודם מרווים את ה-SM, ואז - ורק אז - שואלים כמה טוב עובד כל אחד.

הכלים בפועל - ncu ו-nsys

נחבר הכל לפקודה אחת. ncu (Nsight Compute) יכול לשלוף את כל שכבות ההיררכיה במדידה אחת. הנה קריאה ממוקדת שמבקשת בדיוק מדד אחד מכל שכבה:

ncu --metrics \
  sm__cycles_active.avg.pct_of_peak_sustained_elapsed,\
  sm__inst_executed_pipe_fma.avg.pct_of_peak_sustained_active,\
  sm__inst_executed_pipe_tensor.avg.pct_of_peak_sustained_active,\
  sm__inst_executed_pipe_lsu.avg.pct_of_peak_sustained_active,\
  smsp__issue_active.avg.pct_of_peak_sustained_active \
  ./my_app

השורה הראשונה (sm__cycles_active...elapsed) היא ניצולת ה-SM - חלק מחזורי-השעון החולפים שבהם ה-SM היו פעילים, ממוצע על כל ה-SM. שימו לב לסיומת elapsed (ולא active): כאן דווקא רוצים לספור גם את המחזורים הבטלים במכנה, אחרת לא נראה את ה-SM המובטלים. שלוש השורות האמצעיות הן ניצולת ה-pipe (עם active, כי שם מחריגים מחזורים בטלים בכוונה), והאחרונה היא יעילות ההנפקה. הנה איך פלט על ה-kernel של block-יחיד ייראה, וכיצד לקרוא אותו:

  sm__cycles_active.avg.pct_of_peak_sustained_elapsed          0.76  %
  sm__inst_executed_pipe_fma.avg.pct_of_peak_sustained_active  71.20 %
  sm__inst_executed_pipe_tensor...                              0.00 %
  sm__inst_executed_pipe_lsu...                                 12.40 %
  smsp__issue_active.avg.pct_of_peak_sustained_active          68.90 %

הקריאה: ניצולת ה-SM היא 0.76% - כמעט כל המכונה בטלה. ה-fma מראה 71% מפתה, אבל זו בדיוק המלכודת - הוא ממוצע רק על ה-SM היחיד שעובד ומחריג את 131 הבטלים. אם היינו קופצים ישר למספר ה-71% היינו מתפתים לחשוב שהכול תקין. העץ מציל אותנו: ניצולת SM של 0.76% פירושה שעוצרים כאן ומגדילים את ה-grid, ורק אחר-כך חוזרים לקרוא את שאר המדדים.

לניתוח מלא ואינטראקטיבי אפשר ncu --set full ./my_app שמפיק את דוח ה-Speed of Light (סעיף שמראה Compute (SM) Throughput ו-Memory Throughput זה-לצד-זה). ולתמונת-על על פני ה-timeline של כל ה-kernels - מי לוקח כמה זמן, מי מריץ מתי - משתמשים ב-nsys profile ./my_app. חלוקת העבודה: nsys עונה "איזה kernel לדבג" (רמת המערכת), ו-ncu עונה "למה ה-kernel הזה איטי" (רמת ה-SM ומטה). את שניהם נפתח לעומק בשיעור 9.1.

סיכום

  • מדדי הניצולת בנויים בהיררכיה מהגס לעדין - ניצולת GPU (nvidia-smi), ניצולת SM, ניצולת pipe, ויעילות הנפקה - ומדבגים אותם top-down, בסדר הזה בדיוק, בלי לרדת שכבה עד שהשכבה שמעליה טובה כמה שהיא יכולה.
  • ניצולת ה-GPU של nvidia-smi היא בינארית כמעט - יש kernel פעיל או אין - והיא יכולה להראות 100% גם כשרוב החומרה בטלה; היא נקודת ההתחלה של הדיבוג, לא סופו.
  • ניצולת ה-SM היא אחוז הזמן שבו ה-SM מריצים הוראות; kernel עם block יחיד משיג 100% ניצולת GPU אבל רק 1/132 = 0.76% ניצולת SM על H100, כי block רץ על SM אחד בלבד.
  • בניגוד ל-CPU, שבו 100% ניצולת הוא לרוב דגל אזהרה, ב-GPU ניצולת SM גבוהה עד 100% היא המטרה - SM בטל הוא סיליקון מבוזבז.
  • ניצולת ה-pipe מודדת הישג-מול-peak של כל אחד מארבעה pipes (FMA/CUDA, Tensor, LSU, control/ADU), אבל ממוצעת רק על מחזורים שבהם הpipe פעיל - ולכן חייבים לקרוא אותה יחד עם occupancy וניצולת SM, אחרת מספר גבוה מטעה.
  • משפחת המדדים ב-Nsight היא sm__inst_executed_pipe_{fma,tensor,lsu,adu}.avg.pct_of_peak_sustained_active, כאשר המכנה הוא ה-peak rate מפרק 7.
  • יעילות ההנפקה היא חלק המחזורים הפעילים שבהם המתזמן הנפיק הוראה (היה warp כשיר); מתחת ל-100% פירושו שבמקצת המחזורים כל ה-warps היו תקועים - והתרופה היא יותר warps כשירים, כלומר occupancy מספקת ו-latency hiding טוב.
  • הצירוף האבחנתי המרכזי: ניצולת SM גבוהה עם ניצולת pipe נמוכה = "רחב אבל רדוד", kernel שרץ על הרבה SM אבל מרעיב את היחידות בתוך כל אחד; הפתרון הוא יותר עבודה ל-thread או arithmetic intensity גבוה יותר.
  • ncu שולף את כל השכבות במדידה אחת ועונה "למה ה-kernel איטי"; nsys נותן timeline ועונה "איזה kernel לדבג" - ובשניהם נעמיק בשיעור 9.1.