לדלג לתוכן

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 ממה לבחור).

נקודה תפיסתית קריטית - הפוך מ-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.