1.3 יחידות ביצוע CUDA cores, SFU ו LSU פתרון
פתרון - יחידות ביצוע - CUDA cores, SFU ו-LSU¶
כאן נעבור על כל תרגיל עם הפקודות, הפלט המדויק וההסבר. שימו לב: פלט ה-SASS ומספרי ה-ncu הם דוגמאות מהרצה על H100 עם גרסת nvcc מסוימת - הקידוד המדויק של SASS והאחוזים ישתנו בין גרסאות compiler וכרטיסים, אבל התבנית (אילו הוראות MUFU.* מופיעות, איזה pipe רווי) זהה. אם אתם על T4 ב-Colab, החליפו -arch=sm_90a ב--arch=sm_75.
פתרון תרגיל 1 - ציד הוראות MUFU ב-SASS¶
הפקודות:
פלט מייצג עבור ה-kernel transcendental (בקומפילציה רגילה, בלי fast math):
/*0090*/ MUFU.EX2 R7, R6 ; // expf -> 2^(x*log2(e))
/*0130*/ MUFU.RSQ R11, R0 ; // sqrtf -> built on reciprocal-sqrt
/*0220*/ MUFU.SIN R13, R12 ; // sinf
שלוש הקריאות, שלוש הוראות MUFU.* שונות. אם תסתכלו על ה-SASS המלא (בלי ה-grep) תראו שכל MUFU עטוף בקוד עזר: לפני ה-MUFU.EX2 יש FFMA שכופל את הקלט ב-1.4426950408 (זה log2(e)) ומוסיף תיקון דיוק; סביב ה-MUFU.RSQ יש רצף FFMA/FMUL של עידון Newton-Raphson שהופך את הקירוב לשורש מדויק; ולפני ה-MUFU.SIN יש הפחתת-טווח (range reduction). (בכרטיסים/גרסאות מסוימות sqrtf המדויק עשוי להופיע כ-MUFU.SQRT במקום MUFU.RSQ - שני הנתיבים תקפים, שניהם MUFU.)
עם -use_fast_math:
nvcc -O2 -use_fast_math -arch=sm_90a -c pipes.cu -o pipes.o
cuobjdump --dump-sass pipes.o | grep MUFU
/*0050*/ MUFU.EX2 R5, R4 ; // __expf: a single instruction, no correction
/*0070*/ MUFU.RSQ R7, R6 ; // __fsqrt: fast approximation
/*0090*/ MUFU.SIN R9, R8 ; // __sinf
מספר הוראות ה-MUFU דומה, אבל קוד העזר סביבן כמעט נעלם - ה-FFMA של התיקון, עידון ה-Newton וההפחתת-טווח הזהירה הוחלפו בקירוב חומרתי ישיר. הכתובות (/*00xx*/) קרובות הרבה יותר זו לזו, כי ה-kernel כולו התכווץ.
למה זה עבד: הטרנסצנדנטיות לא מיושמות בהוראה אחת "אמיתית" אלא בקירוב חומרתי (MUFU.*) של ה-SFU, עטוף בקוד תיקון שמביא אותו לדיוק IEEE. expf הופך ל-2^(x*log2(e)), ולכן ההוראה היא MUFU.EX2 ולא "MUFU.EXP". grep MUFU הוא הדרך המהירה לראות בדיוק אילו פעולות מנותבות לpipe ה-SFU.
איך להכליל: כל פעם שאתם חושדים ש-kernel כבד-מתמטיקה, הריצו cuobjdump --dump-sass | grep MUFU וספרו את ההוראות. ריבוי MUFU מסמן תלות בpipe ה-SFU הנדיר. -use_fast_math מקצר את קוד העזר אבל לא משנה את הpipe - אתם עדיין תלויים ב-SFU, פשוט עם פחות overhead סביבו.
פתרון תרגיל 2 - יחס התפוקה FP64:FP32 ביד¶
החישוב:
FP32: 132 x 128 x 2 x 1.98e9 = 66,908 GFLOPS = 66.9 TFLOPS
FP64: 132 x 64 x 2 x 1.98e9 = 33,454 GFLOPS = 33.5 TFLOPS
יחס FP64 : FP32 = 33.5 : 66.9 = 1 : 2
היחס נובע ממילה אחת: נתיבים. יש ל-H100 חצי ממספר נתיבי ה-FP64 (64) לעומת FP32 (128), וזה מתורגם ישירות לחצי מהתפוקה. אימות מול ה-whitepaper של H100:
Peak FP32 ................. 66.9 TFLOPS <-- matches
Peak FP64 ................. 33.5 TFLOPS <-- matches, exactly half
Peak FP64 Tensor Core ..... 66.9 TFLOPS <-- a different pipe! not relevant
שורת ה-"FP64 Tensor Core" מתייחסת ל-Tensor Core (ליבת טנזור), pipe נפרד לגמרי שמבצע כפל-מטריצות ב-FP64, ולא לנתיבי ה-FP64 הסקלריים של ה-CUDA Core. חישוב double רגיל ב-kernel (לא דרך cuBLAS/WMMA) רץ על נתיבי ה-FP64 הסקלריים, ולכן השיא הרלוונטי הוא 33.5 TFLOPS.
למה זה עבד: תפוקת שיא = מספר יחידות x פעולות-למחזור x תדר. מכיוון שה-FMA סופר כשתי פעולות נקודה צפה (כפל + חיבור), נכנס הפקטור 2. חצי הנתיבים ב-FP64 נותן בדיוק חצי מהתוצאה.
איך להכליל: הנוסחה #SM x #נתיבים-לפעולה x 2 x תדר נותנת את שיא הנקודה-צפה לכל סוג. החליפו את "#נתיבים" במספר של הפעולה שלכם (FP32=128, FP64=64, INT32=64 ל-SM ב-H100) כדי לקבל את התקרה הנכונה - זו בדיוק המתודולוגיה "ספרו את היחידה של הפעולה" מההרצאה.
פתרון תרגיל 3 - איזה pipe כל kernel מלחיץ¶
| קטע | הpipe הדומיננטי | נימוק |
|---|---|---|
| A | FP32 (CUDA Core) | float FMA טהור - הוראת FFMA אחת לאיבר |
| B | FP64 | הצבירה ב-double רצה על 64 נתיבי ה-FP64 בלבד |
| C | SFU | שתי קריאות expf - שתי הוראות MUFU.EX2 לpipe הנדיר |
| D | LSU | העתקת בלוק - רק load ו-store, אפס חשבון |
| E | INT32 | כפל וחיבור שלמים (hashing) - נתיבי ה-INT32 (64 ל-SM) |
-
המספר "128 CUDA cores" מטעה במיוחד עבור B ו-E. שניהם רצים על 64 נתיבים בלבד (FP64 ב-B, INT32 ב-E), חצי מ-128 שהמפרט מפרסם. מי שמניח 128 יעריך פי-שניים יותר מדי תפוקה. (וכדאי להוסיף: עבור C המספר לא רק מטעה אלא לא רלוונטי בכלל - ה-SFU לא נספר ב-"128 CUDA cores" מלכתחילה.)
-
C נראה קליל אבל עלול להיות bound כי שתי קריאות ה-
expfמנותבות ל-SFU הנדיר (כ-16 יחידות ל-SM מול 128 FP32). גם אם החשבון סביבן זעיר, ה-SFU הוא צוואר הבקבוק: יחס SFU:FP32 של בערך 1:8 אומר שטרנסצנדנטית עולה בערך פי-שמונה מ-FMA בתפוקה.
למה זה עבד: סיווג pipe מסתכם בשאלה "מה הפעולה שחוזרת הכי הרבה, ולאיזו יחידה היא מנותבת". double -> FP64, expf/sinf -> SFU, load/store -> LSU, חשבון שלמים -> INT32, float FMA -> FP32.
איך להכליל: לפני שמאיצים kernel, זהו את הpipe הדומיננטי - כי מיטוב שמוסיף נתיבי FP32 ל-kernel SFU-bound או LSU-bound לא ישפר כלום. הpipe הנכון קובע איזה מיטוב בכלל רלוונטי.
פתרון תרגיל 4 - להוכיח שה-ALU יושב בטל¶
הקומפילציה והפרופיילינג:
nvcc -O2 -lineinfo -arch=sm_90a pipes.cu -o pipes
ncu --metrics \
sm__inst_executed_pipe_lsu.avg.pct_of_peak_sustained_active,\
sm__inst_executed_pipe_fma.avg.pct_of_peak_sustained_active,\
gpu__dram_throughput.avg.pct_of_peak_sustained_elapsed \
./pipes
פלט מייצג (האחוזים ישתנו, היחס לא):
copyKernel(const float *, float *, int)
sm__inst_executed_pipe_lsu...pct_of_peak_sustained_active % 82.4
sm__inst_executed_pipe_fma...pct_of_peak_sustained_active % 0.9
gpu__dram_throughput...pct_of_peak_sustained_elapsed % 91.7
computeKernel(const float *, float *, int)
sm__inst_executed_pipe_lsu...pct_of_peak_sustained_active % 1.3
sm__inst_executed_pipe_fma...pct_of_peak_sustained_active % 88.5
gpu__dram_throughput...pct_of_peak_sustained_elapsed % 2.1
התמונה חד-משמעית. ב-copyKernel pipe ה-LSU ורוחב הפס של ה-DRAM כמעט רוויים (82% ו-92%), בזמן שpipe ה-FMA שואף לאפס (0.9%). ב-computeKernel המצב מתהפך: ה-FMA רווי (88%) וה-LSU וה-DRAM כמעט בטלים.
מה "יושב בטל" ב-copyKernel: כל נתיבי החישוב (ה-CUDA Cores). ה-kernel רק מעביר בתים - load ו-store - וההוראות האלה עוברות דרך ה-LSU, לא דרך ה-ALU. אז 128 נתיבי ה-FP32 של כל SM כמעט לא מבצעים כלום, וה-GPU כולו ממתין ל-HBM. זה בזבוז של רוב הסיליקון החישובי, וזו בדיוק ההגדרה של kernel memory-bound.
למה זה עבד: המדד pct_of_peak_sustained_active לכל pipe מראה כמה אחוז מתפוקת השיא של אותו pipe נוצלה. מכיוון שהוראות זיכרון עוברות ב-LSU והוראות חשבון ב-FMA, השוואת שני המדדים חושפת ישירות על איזה pipe ה-kernel bound. copyKernel בעל arithmetic intensity זעירה (חיבור אפס לכל load/store) ולכן LSU/DRAM-bound; computeKernel עם 256 FFMA לכל load בודד הוא FMA-bound.
איך להכליל: זו שיטת האבחון הסטנדרטית. פרופלו את pipe_lsu, pipe_fma, pipe_xu (טרנסצנדנטי) ו-dram_throughput יחד, וראו מי מתקרב ל-100%. הpipe הרווי הוא צוואר הבקבוק, וכל מיטוב חייב לכוון אליו - להגדיל arithmetic intensity ל-kernel memory-bound, או להפחית עבודה לpipe הרווי.
פתרון תרגיל 5 (בונוס) - צוואר בקבוק ה-SFU¶
הפרופיילינג של ה-transcendental:
ncu --metrics \
sm__inst_executed_pipe_xu.avg.pct_of_peak_sustained_active,\
sm__inst_executed_pipe_fma.avg.pct_of_peak_sustained_active \
./pipes
transcendental(const float *, float *, int)
sm__inst_executed_pipe_xu...pct_of_peak_sustained_active % 86.3
sm__inst_executed_pipe_fma...pct_of_peak_sustained_active % 12.1
הpipe ה-xu (הטרנסצנדנטי, ה-SFU) כמעט רווי, וה-FMA נמוך. אחרי החלפה ל-__expf/__sinf:
transcendental(const float *, float *, int) // intrinsics version
sm__inst_executed_pipe_xu...pct_of_peak_sustained_active % 88.0
sm__inst_executed_pipe_fma...pct_of_peak_sustained_active % 3.4
זמן ה-kernel יורד (פחות הוראות עזר), אבל ניצול ה-xu נשאר גבוה - הpipe לא השתנה. הפחתנו את קוד התיקון סביב ה-MUFU, לא את התלות ב-SFU עצמו. ה-FMA אף ירד, כי הוראות ה-FFMA של התיקון נעלמו.
- אם ה-SFU רווי ב-100% וה-FMA נמוך, הוספת נתיבי חישוב לא תעזור בכלל - הם כבר בטלים. מה שיעזור: להפחית את מספר הקריאות הטרנסצנדנטיות (למשל לחשב
sigmoidבזהות שמשתמשת ב-expfאחד במקום שניים), או להעלות occupancy כדי לפרוס את הקריאות על יותר warps ולהסתיר את ה-latency של ה-SFU מאחורי עבודה של warps אחרים.
למה זה עבד: __expf הוא MUFU.EX2 בודד בלי עטיפת התיקון, אז הוא מהיר יותר לכל קריאה - אבל הוא עדיין נכנס לאותו pipe xu נדיר. הרוויה של ה-xu היא מאפיין של כמות הקריאות הטרנסצנדנטיות, לא של הדיוק שלהן.
איך להכליל: כשpipe מסוים רווי, המיטוב היחיד שעוזר הוא להפחית עבודה שנכנסת אליו או להסתיר את ה-latency שלו בעוד warps - לא לשפר pipe אחר שכבר בטל. זהו את הpipe הרווי (grep MUFU ב-SASS, pipe_xu ב-ncu), ואז החליטו אם לקצץ קריאות, להחליף אלגוריתם, או להעלות תפוסה.