7.4 Overhead, latency hiding וחוק Little הרצאה
בשלושת השיעורים הקודמים של הפרק בנינו את התמונה הגדולה של ניתוח ביצועים. בשיעור 7.1 הגדרנו צוואר-בקבוק (bottleneck) ואת מודל ה-Roofline, בשיעור 7.2 הפרדנו בין kernel חסום-חישוב (compute-bound) לבין kernel חסום-זיכרון (memory-bound), ובשיעור 7.3 מדדנו את ה-arithmetic intensity (עצימות אריתמטית) שקובעת באיזה צד של נקודת-הרכס אנחנו יושבים. אבל שמנו לב לפרט מטריד: kernels אמיתיים כמעט אף פעם לא נוגעים בגג. הם יושבים מתחתיו, ומודל ה-Roofline - שמצייר רק רוחבי-פס ותפוקות - לא מסביר למה. הסיבה היא הקטגוריה השלישית של העבודה, זו שלא מופיעה על ה-Roofline כלל: תקורה (overhead). בשיעור הזה נשלים את המשולש. נגדיר תקורה כזמן שבו ה-GPU יושב בטל וממתין לעבודה, נבדיל אותה חדות מצוואר-בקבוק (שבו הוא עובד במלוא הכוח), ונכיר את שתי מקורות התקורה העיקריים - תקורת המארח (host, ה-CPU) ותקורת התקשורת. משם נעבור לצד השני של המטבע: הסתרת latency (latency hiding), המנגנון שבזכותו ה-GPU שומר על תפוקה גבוהה למרות ש-latency של פעולה בודדת ארוך מאוד, ולבסוף לחוק Little (Little's Law) - הנוסחה הכמותית שאומרת לנו בדיוק כמה מקביליות צריך כדי להסתיר את ה-latency הזה. חוק Little הוא היסוד התיאורטי ל-occupancy (תפוסה), שנקדיש לו את פרק 8 כולו.
התקורה - הצוואר השלישי - overhead¶
ה-glossary מגדיר overhead במשפט אחד חד: הoverhead היא הזמן שמושקע בלי שנעשית עבודה שימושית. זו ההגדרה, וכל השאר נגזר ממנה. שימו לב להבחנה שהיא יוצרת מול שני הצווארים שכבר הכרנו. כש-kernel compute-bound, ה-GPU עובד במלוא המהירות - יחידות ה-CUDA Core וה-Tensor Core רוויות, פשוט אין להן יותר מה לתת. כש-kernel memory-bound, מערכת הזיכרון רוויה - ה-HBM מזרים בייטים בקצב המרבי שלו. בשני המקרים המשאב המגביל עובד קשה. הoverhead היא ההפך הגמור: ה-GPU יושב בטל, ממתין לקבל עבודה או ממתין לתלות שתיפתר. לא עבודה איטית - היעדר עבודה.
זו בדיוק הסיבה שoverhead איננה מופיעה על ה-Roofline. ה-Roofline מציב שני גגות - רוחב-פס אריתמטי ורוחב-פס של הזיכרון - שניהם קצבים (throughput). הoverhead איננה קצב; היא latency של המתנה. שני צירי ה-Roofline (ops/byte ו-ops/s) פשוט לא יכולים לבטא אותה. לכן kernel חסום-תקורה (overhead-bound) יושב עמוק מתחת לשני הגגות: הוא לא נכשל כי החומרה איטית, אלא כי החומרה מבזבזת זמן בהמתנה. הדוגמה הקנונית של ה-glossary לעומס overhead-bound היא פעולות על מערכים קטנים - kernel שרץ מיקרו-שניות בודדות על הdevice, אבל עלה להost מיקרו-שניות בודדות רק כדי לdispatch אותו.
נחלק את העבודה של כל kernel לשלוש קטגוריות (הפירוק המיוחס להרצאה של Horace He):
All kernel work breaks down into:
┌──────────────┬──────────────────────────────────────────┐
│ Compute │ floating-point operations on CUDA / Tensor Cores │ -> compute-bound
├──────────────┼──────────────────────────────────────────┤
│ Memory │ moving data through the memory hierarchy │ -> memory-bound
├──────────────┼──────────────────────────────────────────┤
│ Overhead │ "everything else" - idle time, waiting, bookkeeping │ -> overhead-bound
└──────────────┴──────────────────────────────────────────┘
שלוש קטגוריות העבודה ממפות אחת-לאחת לשלושת סוגי הצווארים. את השניים הראשונים כיסינו בפרק. עכשיו התור של השלישי.
תקורת המארח - host overhead¶
מקור הoverhead הראשון והנפוץ ביותר הוא host overhead: הזמן שה-CPU מבזבז כדי לגרום ל-GPU לעבוד. ה-glossary נותן את המספר המכונן: בערך 10 מיקרו-שניות של זמן CPU לכל launch kernel. כשאתם כותבים myKernel<<<grid, block>>>(args), ה-<<< >>> הוא סוכר תחבירי שמתורגם לרצף קריאות אל ה-Runtime API (בעיקר cudaLaunchKernel, כפי שראינו בפרק 5). הרצף הזה עושה עבודת CPU אמיתית: אריזת הארגומנטים למערך, העתקת תצורת הlaunch (grid, block, shared memory, stream), ופנייה לדרייבר שכותב פקודה לתור החומרה. כל זה עולה בסדר גודל של יחידות מיקרו-שניות - גם אם ה-kernel עצמו רץ על הdevice במיקרו-שנייה אחת.
וכשמעורב framework כמו PyTorch או TensorFlow, התמונה מחמירה: לפני הlaunch עצמה, ה-framework מבצע קבלת החלטות - איזה kernel לבחור, איזה dispatch לגזרת הטיפוס והצורה, בדיקות אוטו-גראד - וזה מוסיף מיקרו-שניות רבות נוספות על גבי ה-10 של ה-API. הoverhead של השכבה הפייתונית לבדה עלולה להיות גדולה מזמן הlaunch של CUDA.
כל עוד ה-kernels גדולים - רצים מילישניות שלמות - הoverhead זניחה. הבעיה מתלקחת כשה-kernels קטנים ורבים. נעשה חשבון על צעד decode יחיד של מודל שפה בסגנון Transformer, בן 32 שכבות:
per layer, roughly: QKV projection, RoPE, attention, output
projection, two GEMMs for the MLP, two normalizations, and more
-> roughly 15 kernels per layer
32 layers x ~15 kernels ≈ 480 launches per token
480 launches x ~5 microseconds ≈ 2.4 milliseconds of CPU overhead alone
אם יעד ה-latency הוא 10 מילישניות לטוקן, אז 2.4 מילישניות נשרפים בoverhead לפני שה-GPU חישב ביט אחד - כמעט רבע מהתקציב. וגרוע מכך: אם ה-kernels קצרים מהזמן שלוקח להost לdispatch את הבא בתור, ה-GPU מורעב (starved) - מסיים את ה-kernel הנוכחי ויושב בטל בזמן שה-CPU עדיין טורח על הlaunch הבאה. זהו בדיוק המצב הoverhead-bound: לא חישוב איטי ולא זיכרון איטי, אלא host שלא מספיק להזין את הdevice.
כיצד מקטינים host overhead. האסטרטגיה הכללית אחת: לשלם את מחיר הlaunch פחות פעמים, על עבודה גדולה יותר. ארבע הגרסאות המעשיות:
- גרפים - CUDA Graphs. לוכדים DAG שלם של launches ומגישים אותו בקריאת host אחת שאפשר להריץ מחדש (replay) בזול. זו התרופה הישירה, והקדשנו לה את שיעור 5.5. במקום 480 launches לטוקן -
cudaGraphLaunchאחד. - מיזוג - fusion. מאחדים כמה פעולות ל-kernel אחד (למשל bias + activation + dropout ל-kernel יחיד). פחות kernels = פחות launches = פחות overhead, וגם פחות תעבורת זיכרון (שיעור 7.3).
- קיבוץ - batching. משגרים kernel אחד שמעבד batch גדול במקום הרבה kernels קטנים. אותו מספר launches פורש על יותר עבודה - הoverhead לlaunch נשארת אבל חלקה היחסי בזמן צונח.
- פחות launches, גדולות יותר. העיקרון שמאחד את כולן: כל טריק שמוריד את מספר הlaunches או מגדיל את העבודה לכל launch מקטין את חלק הoverhead.
תקורת תקשורת - communication overhead¶
מקור הoverhead השני הוא תקשורת: זמן ההשהיה בהזזת נתונים בין CPU ל-GPU, או בין GPU ל-GPU. כשמעתיקים נתונים על-פני ה-PCIe בין הhost להdevice, או על-פני NVLink בין שני GPUs, אנחנו מוציאים זמן על תנועת נתונים, לא על חישוב. ה-glossary נותן מסגור מדויק ורב-עוצמה לזה: כשרוחב-הפס של התקשורת מגביל את הביצועים, יש להתייחס לזה כאל צורה של חסימת-זיכרון (memory-boundedness) על-פני מערכת מבוזרת. במילים אחרות, אותו ניתוח Roofline שלמדנו למערכת בודדת חל גם על אשכול (cluster) שלם: ה"זיכרון" הוא הצומת המרוחק, ורוחב-הפס של הרשת (NVLink, InfiniBand) הוא הגג המשופע.
the "bandwidth" hierarchy - from fastest to slowest:
register file <-> SM : ~tens of TB/s (inside the SM)
HBM3 <-> SM : ~3.35 TB/s (H100, global memory)
NVLink GPU <-> GPU : ~hundreds of GB/s (within a node)
PCIe Gen5 CPU <-> GPU : ~64 GB/s (host-device)
InfiniBand node <-> node : ~tens of GB/s (between machines)
ככל שיורדים בהיררכיה רוחב-הפס צונח בסדרי גודל. לכן העברה של tensor על-פני PCIe או על-פני הרשת עלולה בקלות להפוך לצוואר-הבקבוק של אימון או inference מבוזר - וזו הסיבה שספריות כמו NCCL מתאמצות כל-כך לחפוף תקשורת עם חישוב. התרופות דומות לhost overhead בעיקרון: פחות העברות, גדולות יותר (איחוד הודעות קטנות לגדולה), חפיפה של תקשורת עם חישוב (streams, שיעור 5.3), והשארת נתונים על הdevice ככל האפשר במקום הלוך-ושוב אל הhost.
הסתרת latency - latency hiding¶
עד כאן דיברנו על overhead שנוצרת מחוץ ל-GPU (הhost, הרשת). אבל יש latency גם בתוך ה-GPU: פקודת load מהזיכרון הגלובלי לוקחת מאות מחזורי-שעון, ואפילו פקודות אריתמטיות לוקחות כמה מחזורים. אם ה-GPU היה עוצר ומחכה לכל load, הוא היה בטל רוב הזמן. הפתרון הוא הסתרת latency: אסטרטגיה למסך פעולות ארוכות-latency על-ידי הרצת רבות מהן במקביל. זהו הרעיון המכונן של ארכיטקטורת ה-GPU, ובו נגענו כבר בשיעור 1.4 כשהכרנו את ה-warp scheduler.
התובנה המרכזית, שה-glossary מנסח בחדות: warps, לא threads, הם יחידת ההנפקה של פקודות. ("Threads are not the unit of instruction issuance, warps are.") ה-warp scheduler שומר על מאגר גדול של warps תושבים (resident) על ה-SM, ובכל מחזור בוחר warp אחד כשיר (eligible) ומנפיק לו פקודה. כשה-warp הזה נתקע (stalled) בהמתנה ל-load, המתזמן פשוט מנפיק מ-warp כשיר אחר. מיתוג ההקשר הזה כמעט חינמי - מצב האוגרים של כל warp נשאר תושב בקובץ-האוגרים של ה-SM, ולכן החלפת warp עולה בערך מחזור-שעון אחד (זה בדיוק המספר שראינו ל-H100 בשיעור 1.2). בזמן שה-load של warp אחד "בטיסה", המתזמן ממלא את החורים בעבודה של warps אחרים. ה-latency של ה-load הבודד נשאר ארוך - אבל הוא מוסתר מאחורי עבודה מועילה.
יש עוד ממד להסתרה: warps שונים יכולים לתפוס pipes שונים בו-זמנית. ב-H100 יש בכל SM ארבעה warp schedulers, וכל אחד שולט על pipelines שונות: Tensor Core, CUDA Core (FP32/INT32), ו-LSU (load/store unit). בזמן ש-warp אחד מריץ כפל-מטריצות על ה-Tensor Cores, warp שני מריץ אריתמטיקה על ה-CUDA Cores, ו-warp שלישי מושך נתונים דרך ה-LSU - כל השלושה חופפים ומסתירים זה את ה-latency של זה. זו הקבלה ל-SMT (Simultaneous Multi-Threading) של מעבדים, רק בקנה-מידה גדול בהרבה: במקום 2 threads לליבה, עשרות warps ל-SM.
נראה זאת על SASS קונקרטי - שרשרת תלויה של פקודות (כל אחת תלויה בתוצאה של קודמתה), עם ה-latency האופייני של כל פקודה:
LDG.E.SYS R1, [R0] // load from global memory, ~400 cycles
IMUL R2, R1, 0xBEEF // integer multiply, ~6 cycles (depends on R1)
IADD R4, R2, 0xAFFE // integer add, ~4 cycles (depends on R2)
IMUL R6, R4, 0x1337 // integer multiply, ~6 cycles (depends on R4)
מכיוון שכל פקודה תלויה בקודמתה, warp בודד חייב לעצור לאורך כל שרשרת ה-latency: 400 + 6 + 4 + 6 = 416 מחזורים שבהם הוא לא יכול להתקדם. אם רק ה-warp הזה רץ על ה-SM, יחידות הביצוע יושבות בטל 416 מחזורים - בזבוז מוחלט. הדרך היחידה למלא את החור היא warps רבים ובלתי-תלויים: בעוד ה-warp הראשון תקוע על ה-load, warps אחרים מנפיקים את הפקודות שלהם. כמה warps צריך בדיוק? לזה בדיוק נועד חוק Little.
חוק Little - Little's Law¶
חוק Little הוא, בלשון ה-glossary, "החשוב שבחוקים הבסיסיים" של ניתוח מערכות כמותי. הוא קובע את כמות המקביליות הדרושה כדי להסתיר latency במלואו באמצעות throughput. הנוסחה פשוטה להפליא:
שלושת המרכיבים:
- מקביליות - concurrency - מספר הפעולות שחייבות להיות "בטיסה" (in flight) בו-זמנית.
- השהיה - latency - זמן ההשהיה של פעולה בודדת, בשניות (או במחזורים).
- תפוקה - throughput - מספר הפעולות שמושלמות בשנייה (הקצב שאתם רוצים לקיים).
זו בדיוק צורת ההנדסה-מערכתית של תוצאת התורים הקלאסית L = λW: ה-concurrency הוא L (מספר הפריטים במערכת), ה-throughput הוא λ (קצב ההגעה), וה-latency הוא W (זמן השהייה). האינטואיציה: אם כל פעולה שוהה W שניות, ואתם רוצים להשלים λ פעולות בשנייה, אז בכל רגע נתון יש λ·W פעולות "תלויות באוויר" בו-זמנית. אם אין לכם מספיק פעולות בטיסה - הpipe מתרוקן וה-throughput צונח מתחת ל-λ.
נציב את המספרים של ה-glossary. נניח שאנחנו רוצים לקיים throughput של פקודה אחת למחזור, וה-latency של גישה לזיכרון הוא 400 מחזורים:
צריך 400 פעולות זיכרון בטיסה בו-זמנית כדי לשמור על יחידת הביצוע עסוקה כל מחזור. עכשיו נניח שהחומרה מהירה יותר ומסוגלת ל-10 פקודות למחזור (אותו latency של 400):
זו התובנה החדה של חוק Little: throughput גבוה יותר דורש מקביליות גבוהה יותר, באופן פרופורציוני, כדי להישאר מוסתר-latency. ככל שהחומרה מהירה יותר, כך צריך יותר עבודה מקבילה "באוויר" רק כדי להזין אותה. זה מסביר מדוע GPUs, שהם מכונות throughput קיצוניות, זקוקים לכל-כך הרבה threads: לא כי כל thread מהיר, אלא כי צריך המון מהם בטיסה כדי להסתיר את ה-latency ולשמור על הקצב.
מ-latency ל-מספר warps¶
חוק Little נותן concurrency במונחי "פעולות". אבל ראינו שיחידת ההנפקה היא ה-warp, לא ה-thread הבודד. אז נמיר: ה-warp מנפיק פקודה עבור 32 threads יחד, ולכן כדי לתרגם "פעולות בטיסה" ל"מספר warps", מחלקים ברוחב ה-warp - 32. נחזור לשרשרת ה-416 המחזורים מהסעיף הקודם. כלל-האצבע של ה-glossary:
צריך בערך 13 warps בלתי-תלויים שרצים במקביל כדי להסתיר לגמרי את שרשרת ה-416 מחזורים. הרעיון: כשה-warp הראשון תקוע על ה-load שלו, 12 ה-warps האחרים מנפיקים את הפקודות שלהם ומכסים את החור, כך שבכל מחזור לפחות warp אחד כשיר להנפיק. שימו לב שהמספר הזה עקבי עם דוגמת חוק Little: 400 פעולות-thread בטיסה חלקי 32 threads ל-warp נותן ~12.5 ≈ 13 warps. שתי הזוויות - הפעולות בטיסה ומספר ה-warps - הן אותו חשבון בדיוק, רק ביחידות שונות.
והנה החיבור ל-occupancy, שנקדיש לו את פרק 8: 13 warps מתוך המקסימום של 64 warps ל-SM ב-H100 הם occupancy של בערך 13/64 ≈ 20%. כלומר, כבר ב-occupancy צנוע של כ-20% אנחנו מסתירים במלואה שרשרת load אופיינית. זו נקודה קריטית שנחזור אליה: לא תמיד צריך occupancy גבוה - צריך רק מספיק כדי להסתיר את ה-latency שבפועל. מעבר לזה, occupancy נוסף לא קונה כלום (ולפעמים אף מזיק, כי הוא מקטין את מספר האוגרים ל-thread). את הדקות הזו נפרוט בשיעור 8.1.
מלכודת נפוצה: החישוב latency / 32 מניח ששרשרת התלות מכריחה כל warp להחזיק פקודה אחת בטיסה בכל רגע. אם הקוד שלכם דווקא חושף מקביליות ברמת הפקודה (ILP) - למשל כמה loads בלתי-תלויים ברצף באותו warp - אז warp בודד יכול להחזיק כמה פעולות בטיסה, וצריך פחות warps כדי להגיע לאותו concurrency. ILP ו-occupancy הם שני מנופים חלופיים לאותו יעד: להחזיק מספיק פעולות בטיסה.
התוצאה של Volkov - זיכרון מול אריתמטיקה¶
עכשיו לתוצאה הכי לא-אינטואיטיבית, וגם היפה ביותר, בכל השיעור. ה-glossary מפנה לסעיף 4.3 בתזה של Vasily Volkov, שמדד ניסויית כמה warps צריך כדי להסתיר latency. הציפייה הטבעית: latency של זיכרון ארוך בהרבה מ-latency אריתמטי (מאות מחזורים מול יחידות בודדות), אז ודאי שצריך הרבה יותר warps כדי להסתיר זיכרון. התוצאה הפתיעה:
כמעט אותו מספר. איך ייתכן, אם ה-latency של הזיכרון גדול פי כמה? התשובה היא בדיוק חוק Little: concurrency = latency x throughput, ושני הגורמים מתקזזים.
arithmetic: low latency x high throughput = moderate concurrency
memory: high latency x low throughput = moderate concurrency
│ │
└── several times larger └── several times smaller -> the product is similar
לאריתמטיקה יש latency נמוך אבל throughput גבוה (ה-CUDA Cores/Tensor Cores מסוגלים להמון פעולות למחזור, כפי שראינו ב-arithmetic bandwidth). לזיכרון יש latency גבוה אבל throughput נמוך (רוחב-הפס של ה-HBM נמוך בהרבה מרוחב-הפס האריתמטי - זה בדיוק ה"קיר הזיכרון" מ-7.1, והיחס Tensor Core:CUDA Core של בערך 100:1). בחוק Little, ה-concurrency הוא המכפלה של השניים. הזיכרון מפצה את ה-latency הגדול שלו ב-throughput קטן, והאריתמטיקה מפצה את ה-throughput הגדול שלה ב-latency קטן - והמכפלות יוצאות דומות. לכן מספר ה-warps הדרוש להסתרה דומה.
זו לא מקריות - זו תכונה שימושית של מכונת latency-hiding. מכיוון שאותו מספר warps מסתיר גם latency אריתמטי וגם latency של זיכרון, אותה תצורת occupancy מסתירה עומס מעורב של אריתמטיקה וזיכרון - שזה בדיוק מה שרוב ה-kernels האמיתיים עושים. ה-GPU מתוכנן כך שהמאזן הזה יעבוד עבורכם.
אבחון - overhead-bound מול compute-bound מול memory-bound¶
לפני שנסגור, נחבר את שלוש קטגוריות הצוואר לכדי טבלת-אבחון מעשית. כשאתם ניגשים ל-kernel איטי, השאלה הראשונה היא "באיזה צוואר אני". שלושת הסימנים המבחינים:
| מאפיין | compute-bound | memory-bound | overhead-bound |
|---|---|---|---|
| מה המשאב המגביל | רוחב-פס אריתמטי | רוחב-פס הזיכרון | latency / המתנה |
| מצב יחידות הביצוע | רוויות, עובדות מלא | ממתינות לזיכרון | בטלות, אין עבודה |
| מופיע על ה-Roofline | כן (מתחת לגג האופקי) | כן (מתחת לגג המשופע) | לא (Roofline לא רואה latency) |
| מדד ncu מרכזי | Compute (SM) Throughput גבוה | Memory Throughput גבוה | שניהם נמוכים |
| מה מזיז את המחט | Tensor Cores, precision נמוך | coalescing, reuse, קיטול | גרפים, מיזוג, קיבוץ, occupancy |
| דוגמה | GEMM גדול | כפל וקטור-וקטור | פעולות על מערכים קטנים |
השורה שמסגירה overhead-boundedness: גם ה-Compute Throughput וגם ה-Memory Throughput נמוכים בו-זמנית. אם שני המדדים למטה, החומרה לא עובדת קשה על שום דבר - היא בטלה. זה בדיוק ההפך משני הצווארים האחרים, שבהם לפחות משאב אחד רווי. בדיוק כפי שראינו, ה-Roofline לא יעזור לכם כאן - הוא מראה רק כמה קרובים לגג, וכשאתם overhead-bound אתם רחוקים משני הגגות בגלל סיבה שהוא לא מודד.
נחבר את זה למספרים הקונקרטיים של ה-H100 ולחוק Little. ל-H100 יש 132 SMs, ובכל SM ארבעה warp schedulers שכל אחד מנפיק עד פקודה אחת למחזור:
maximum issue rate, H100 @ 1.98 GHz:
132 SMs x 4 schedulers x 1 instruction/cycle x 1.98e9 cycles/second
≈ 1.05 x 10^12 instructions/second (roughly a trillion instructions per second)
זהו throughput אדיר, ולפי חוק Little הוא בדיוק מה שמכריח מקביליות עצומה: כדי להזין קצב הנפקה כזה למרות latency של מאות מחזורים, צריך עשרות אלפי threads בטיסה בו-זמנית על פני כל ה-GPU. ברמת ה-SM הבודד: 4 schedulers שמנפיקים למרות latency של ~400 מחזורים דורשים בערך 4 x 400 = 1600 פעולות-thread בטיסה, שהן 1600/32 = 50 warps - קרוב מאוד למקסימום של 64 warps ל-SM. זו הסיבה ש-H100 תוכנן עם 64 warps תושבים ל-SM מלכתחילה: המספר נגזר ישירות מחוק Little, כדי שיהיה מספיק חומר להסתיר את ה-latency ולהזין את קצב ההנפקה.
הגשר ל-occupancy¶
נחבר את הכל לפני שנסכם. הoverhead היא הצוואר השלישי - ה-GPU בטל, ממתין לעבודה. חלק מה-latency שגורם לבטלה הזו מגיע מבחוץ (הhost, הרשת), ואת זה מתקנים עם גרפים, מיזוג וקיבוץ. אבל חלק גדול מה-latency הוא פנימי - loads מהזיכרון, שרשראות תלות - ואת זה מסתירים עם מקביליות. חוק Little הוא הכלי שמכמת בדיוק כמה מקביליות צריך: concurrency = latency x throughput, ובמונחי warps, בערך latency/32. אם יש לכם מספיק warps תושבים - ה-latency מוסתר וה-throughput נשמר. אם אין מספיק - ה-warp scheduler נשאר בלי warp כשיר להנפיק, יחידות הביצוע יושבות בטל, וה-latency הפנימי מתגלה כoverhead גלויה שמורידה אתכם מתחת ל-Roofline.
וכאן בדיוק מתחיל פרק 8. השאלה "כמה warps תושבים יש לי בפועל על ה-SM" היא בדיוק ההגדרה של occupancy, וכמה משאבים (אוגרים, shared memory) כל block צורך קובע כמה warps נכנסים. חוק Little אומר כמה warps צריך; occupancy אומר כמה warps יש. בשיעור 8.1 נחשב occupancy תיאורטי מתצורת הlaunch ומגבלות ה-H100, ונראה מדוע - כפי שכבר רמזנו - יותר occupancy הוא לא תמיד טוב יותר: צריך רק מספיק כדי לספק את חוק Little, לא יותר.
סיכום¶
- תקורה (overhead) היא הזמן שבו ה-GPU יושב בטל וממתין לעבודה, בניגוד לצוואר-בקבוק (compute-bound או memory-bound) שבו המשאב המגביל עובד במלוא הכוח; זו הקטגוריה השלישית של העבודה (Compute / Memory / Overhead).
- הoverhead איננה מופיעה על ה-Roofline, כי ה-Roofline מצייר רק רוחבי-פס ותפוקות ולא latency של המתנה; לכן kernel overhead-bound יושב עמוק מתחת לשני הגגות.
- הhost overhead היא בערך 10 מיקרו-שניות לכל launch kernel (וה-framework מוסיף עוד מיקרו-שניות רבות של dispatch); היא הופכת דומיננטית כשמשגרים הרבה kernels קטנים, כמו כ-480 launches לטוקן ב-decode של LLM.
- מקטינים host overhead על-ידי פחות launches גדולות יותר: גרפי CUDA (שיעור 5.5), מיזוג (fusion), וקיבוץ (batching).
- הoverhead תקשורת (CPU-GPU על PCIe, GPU-GPU על NVLink) היא צורה של memory-boundedness על-פני מערכת מבוזרת: אותו ניתוח Roofline חל, כשרוחב-פס הרשת הוא הגג המשופע.
- הסתרת latency (latency hiding) שומרת על throughput גבוה למרות latency ארוך של פעולה בודדת, על-ידי שזירת warps רבים: כש-warp נתקע, ה-scheduler מנפיק מ-warp כשיר אחר, ומיתוג ה-warp עולה בערך מחזור אחד; warps, לא threads, הם יחידת ההנפקה.
- קבוצות warps שונות יכולות לתפוס pipes שונים בו-זמנית (Tensor Core, CUDA Core, LSU), כך שכל אחד מסתיר את ה-latency של האחרים - הקבלה ל-SMT בקנה-מידה גדול.
- חוק Little קובע
concurrency = latency x throughput: latency של 400 מחזורים ב-throughput של פקודה למחזור דורש 400 פעולות בטיסה; ב-10 פקודות למחזור - 4,000. throughput גבוה יותר דורש מקביליות גבוהה יותר באופן פרופורציוני. - ממירים latency למספר warps בחלוקה ברוחב ה-warp: שרשרת תלויה של 416 מחזורים דורשת בערך
416/32 ≈ 13warps להסתרה מלאה (occupancy של כ-20% ב-H100); ILP הוא מנוף חלופי שמפחית את מספר ה-warps הדרוש. - תוצאת Volkov: צריך בערך 30 warps להסתיר latency של זיכרון מול בערך 24 להסתיר latency אריתמטי - דומה, למרות ש-latency הזיכרון גדול בהרבה, כי לזיכרון latency גבוה אך throughput נמוך ולאריתמטיקה להפך, והמכפלה (concurrency) יוצאת דומה.
- חוק Little הוא היסוד התיאורטי ל-occupancy: הוא אומר כמה warps צריך כדי להסתיר את ה-latency; פרק 8 יראה כמה warps תושבים יש בפועל, ומדוע צריך רק מספיק ולא יותר.