השהיה נמוכה ותעבורה גבוהה בפריסת DeepSeek מרובת-צמתים
פריסת מעבדי GPU מרובים משפרת את ביצועי מודל MoE במהירות ובקנה מידה בו-זמנית
ברוב המערכות, השהיה ותעבורה (throughput) הן לרוב מטרה סותרת הדורשת פשרות במהלך התכנון והפריסה. לדוגמה, במודלי שפה גדולים וצפופים, הגדלת גודל ה-batch עשויה לשפר את התעבורה אך גם מעלה את ההשהיה; הגדלת מקבילות הטנזור (tensor parallelism) בתוך מכונה בודדת יכולה לצמצם את ההשהיה אך מקטינה את מספר העותקים (replicas), מה שמוביל לתעבורה נמוכה יותר.
מודלים מסוג מומחים משולבים (Mixture of Experts - MoE) כמו DeepSeek-V3/R1 הדגימו לאחרונה יכולות מודל ויעילות תפעולית מצוינות. לדוגמה, למודל DeepSeek-V3/R1 יש 671 מיליארד פרמטרים בסך הכל, אך כל טוקן משתמש ב-37 מיליארד פרמטרים בלבד במהלך ההסקה (inference). ארכיטקטורת מודל זו מציבה אתגרים והזדמנויות כאחד עבור מערכות הסקה.
מאמר זה מראה שבניגוד למערכות συμβατικές (קונבנציונליות), מודלי MoE כמו DeepSeek-V3/R1 יכולים להשיג בו-זמנית תעבורה גבוהה יותר והשהיה נמוכה יותר בעת שימוש במספר רב של מעבדי GPU בפריסות מרובות-צמתים (multi-node) ברוב התרחישים.
ארכיטקטורות פריסה
בשל המספר הרב של מומחים קטנים שיש למודל, יש לפרוס את הפריסות על פני התקנים מרובים. שקלנו הן פריסות צומת בודד על צומת יחיד עם 8 מעבדי GPU מסוג 8xH200 והן פריסות מרובות-צמתים על מעבדי GPU מסוג 8xH100.
שתי ארכיטקטורות הפריסה מנצלות מקבילות נתונים (Data Parallelism), המנוהלת באמצעות מתזמן הבקשות הפנימי שלנו. יישום מקבילות נתונים כולל הפעלה של מספר מופעי מנוע הסקה, שכל אחד מהם פועל באופן עצמאי כדי לשרת ולתחזק בקשות. מתזמן הבקשות, המקיים אינטראקציה עם המנוע באמצעות GRPC, אחראי לפיזור הבקשות באופן שווה ככל האפשר, תוך כדי איפשור שימוש חוזר ב-KV ושלח בקשות עם קידומת מותאמת חלקית לשרתים המכילים את המטמון. מופעי המנוע אינם משתרעים על פני מספר צמתים. הם יכולים להשתמש אופציונלית במקבילות טנזור כדי לפצל את מנגנון הקשב (attention) על פני התקנים מרובים. המופעים מחוברים זה לזה באמצעות NVLink במקרה של צומת בודד או InfiniBand במקרה של מרובה-צמתים, והם משגרים ואוספים מומחים.
תצורת הפריסה של צומת בודד מספקת השהיה עדיפה עם גדלי batch קטנים; עם זאת, הביצועים מידרדרים במהירות בתנאי עומס גבוהים.
כדי לפרוס את מנוע ההסקה, אנו מריצים פוד (pod) אחד לכל צומת המארח מספר מופעי מנוע. PyTorch אחראית על הגדרת תקשורת מבוזרת ומשא ומתן על אתחול NVSHMEM. לצורך תקשורת, אנו מתבססים על CUDA kernels מותאמים אישית המתוארים בפוסט בבלוג קודם. יישום שתי הפריסות זהה כמעט לחלוטין, כאשר המודל בוחר את ה-kernels הנכונים לשימוש בהתבסס על ה-fabric המיישם את מקבילות המומחים.
טכניקות מקבילות
לפני שנצלול לתוך השוואות הביצועים שלנו, חיוני להבין את אסטרטגיות המקבילות המרכזיות שהופכות את הפריסה של מודלי MoE עצומים כמו DeepSeek-V3/R1 לאפשרית.
מקבילות טנזור (Tensor Parallelism)
בהסקה של LLM, מקבילות טנזור (TP) משמשת בדרך כלל להפחתת השימוש בזיכרון ובחישוב לכל GPU, ובכך להפחתת ההשהיה. בדרך כלל, אנו יכולים לפצל היטלים לינאריים (Linear Projections) בשכבות הקשב ו-MLP לאורך ממדי שורה או עמוד, ולפצל פעולות קשב לאורך ממד ראש הקשב.
עם TP, לארכיטקטורת Llama-3 אין חישוב מנויל עבור פעולות היטל Linear וקשב על פני מעבדי GPU, מה שהוות שיטת פיצול אידיאלית. עם זאת, במודלי DeepSeek-V3/R1, TP אינו יכול להשיג זאת.
מודלי DeepSeek-V3/R1 משתמשים בקשב רב-חבוי (Multi-Latent Attention - MLA). שכבת MLA משתמשת תחילה בהיטל Linear kv_a_proj כדי לחשב את הווקטור החבוי, ולאחר מכן משתמשת בהיטל Linear נוסף kv_b_proj כדי להמיר אותו למרחב של כל ראש קשב. מכיוון שכל ראשי הקשב חולקים את אותו וקטור חבוי, TP אינו יכול לפצל את הווקטור החבוי, ולכן כל דרגות ה-TP צריכות לשכפל את הפרמטרים והחישוב של kv_a_proj ו-kv_b_proj. כמו כן, מכיוון ש-MLA מאחסן את הווקטור החבוי במטמון ה-KV, כל דרגת TP שומרת עותק זהה של מטמון ה-KV.
למרות כפילות מסוימת ב-MLA, מקבילות טנזור עדיין מספקת הפחתה חלקית בדרישות החישוב, מה שהופך אותה לבעלת ערך עבור תרחישים הדורשים מהירויות פלט גבוהות.
מקבילות מומחים (Expert Parallelism)
מודלי DeepSeek-V3/R1 מחליפים שכבות MLP בשכבות MoE. לשכבת MoE יש 256 מומחים מנותבים ומומחה משותף אחד. כל טוקן משוגר ל-8 מומחים מנותבים שונים לחישוב, והתוצאות מסוכמות במשקל. כל טוקן מחושב גם במומחה המשותף, והתוצאה מתווספת לתוצאה מהמומחים המנותבים.
מקבילות מומחים (EP) משמשת כגישת הפיצול הטיפוסית עבור שכבות MoE, כאשר כל GPU מנהל 256 / EP מומחים מנותבים תוך שמירה על עותק של המומחה המשותף. בהשוואה ל-TP, היתרון של EP הוא שהוא יכול לפזר חישובים על פני יותר מעבדי GPU, ולהפחית את נפח החישוב והשימוש בזיכרון לכל GPU.
לפני ביצוע חישובי מומחים, כל מעבדי ה-GPU צריכים לבצע תקשורת AllToAll כדי לשגר טוקנים למעבדי ה-GPU שבהם נמצאים המומחים המתאימים; לאחר חישובי מומחים, נדרשת תקשורת AllToAll נוספת כדי לאסוף את תוצאות החישוב ממעבדי GPU שונים ולבצע סכימה משוקללת. יישמנו גרסה אופטימלית של שני ה-kernels לתקשורת AllToAll אלה, Dispatch ו-Combine, באמצעות NVSHMEM. בפוסט בבלוג הקודם שלנו, פירטנו את היישום, וה-kernels שלנו עברו קוד פתוח ב-GitHub.
מקבילות נתונים (Data Parallelism)
עם EP, אנו יכולים לפזר חישובי MoE על פני 128 או אף יותר מעבדי GPU. עם זאת, חישובי MLA אינם ניתנים לחלוקה באמצעות EP. בנקודה זו, אנו יכולים להכניס מקבילות נתונים (DP). לכל קבוצת DP יש עותק מלא של שכבת ה-MLA. כל קבוצת DP מקבלת קלטים שונים ומבצעת חישובי שכבת MLA באופן עצמאי.
ניתן לשלב את ה-DP וה-TP של שכבת ה-MLA, כאשר קבוצת DP אחת מפוצלת למספר דרגות TP. ניתן לשלב את ה-EP של שכבת ה-MoE עם ה-DP/TP של שכבת ה-MLA. EP = DP * TP. לדוגמה, על 16 מכונות, EP128 DP32 TP4 פירושו פיזור מומחים מנותבים על פני 128 מעבדי GPU, כאשר כל 4 מעבדי GPU מהווים קבוצת DP, ובסך הכל 32 קבוצות DP עצמאיות.
צומת בודד לעומת מרובה-צמתים
671 מיליארד הפרמטרים של DeepSeek חורגים מיכולת הזיכרון של מכונת H100 יחידה בעלת 8 מעבדי GPU (80 GB * 8), אך מכונת H200 יחידה בעלת 8 מעבדי GPU יכולה להכיל במלואה את המודל כולו (141 GB * 8). באמצעות תצורת EP8 DP8 TP1, המודל משתמש בכ-100 GB של זיכרון לכל GPU, מה שמשאיר כ-40 GB למטמון KV ותוצאות ביניים אחרות. טוקן אחד תופס 70,272 בתים של מטמון KV. בהנחה שלכל בקשה יש 5,000 טוקנים, כל GPU יכול להכיל בערך 100 בקשות.
רצינו להבין את הבדלי הביצועים בין פריסות של צומת בודד ומרובות-צמתים תחת תצורות שונות. השתמשנו במכונת H200 אחת לפריסת צומת בודד ועד 16 מכונות H100 לפריסות מרובות-צמתים. עבור כל סביבת פריסה, השתמשנו בשילובים של TP 1, 2, 4, 8 וגדלי batch לכל GPU של 1, 2, 4, 8, 16, 32, 64, 128. הנחנו שלכל בקשה היה אורך מטמון KV של 5,000 טוקנים. הנחנו גם כי חיזוי מרובה-טוקנים (Multi-Token Prediction - MTP) חוזה טוקן נוסף אחד (כלומר, אורך השאילתה של כל בקשה הוא 2), והנחנו בשמרנות שיעור קבלה של 60%. האיור שלהלן מציג את התעבורה ואת מהירות הפלט עבור תצורות שונות.
הציר האופקי מייצג את מהירות הפלט לכל בקשה בטוקנים/שנייה. הציר האנכי משתמש בסקאלה לוגריתמית כדי להציג את התעבורה לכל מכונה בטוקנים/שנייה. סימנו את גבול פארטו (Pareto Frontier) עבור כל תצורת EP עם קווים בצבעים שונים.
בתרחישים עם דרישות מהירות פלט גבוהות במיוחד, שימוש ב-EP8 DP1 TP8 של צומת בודד עם גודל batch של 1 יכול להשיג מהירות פלט העולה על 100 טוקנים/שנייה, אך התעבורה נמוכה במיוחד, שווה ערך למהירות הפלט. בתרחיש זה, ל-batch כולו יש 2 טוקנים בלבד, שניתן לשגר למקסימום של 2*8=16 מומחים, תוך הפעלת סך של לכל היותר 57 מיליארד פרמטרים.
בטווח מהירות הפלט של 80-40 טוקנים/שנייה, ככל שהתעבורה גדלה, מהירות הפלט קטנה משמעותית. לעומת זאת, ל-EP128 יש תעבורה הגבוהה פי 5 בערך מפריסת צומת בודד באותה מהירות פלט.
ניתן להסביר תופעה זו על ידי בחינת אופן התנהגותן של פריסות צומת בודד: הגדלת גודל ה-batch קשורה ישירות לעלייה במומחים המופעלים. כאשר גודל ה-batch הוא 1, המספר הממוצע של מומחים מופעלים לכל GPU הוא 2 * 8 / 8 = 2. כאשר ה-batch גדול דיו, כל המומחים מופעלים, כלומר כל GPU מפעיל 256 / 8 = 32 מומחים. הפעלת מומחים נוספים פירושה שה-GPU צריך לקרוא פרמטרים נוספים מהזיכרון, מה שמגביר משמעותית את לחץ רוחב הפס של הזיכרון. מכיוון ששלב הפענוח (decode) של מודלי שפה גדולים כבר מוגבל על ידי רוחב פס הזיכרון ולא על ידי ביצועי חישוב, הגדלת גודל ה-batch בפריסת צומת בודד מפחיתה משמעותית את מהירות הפלט.
השוואה בין ארבע תצורות הפריסה מרובות-הצמתים (EP16, EP32, EP64 ו-EP128) מגלה ש-ערכי EP גבוהים יותר מעבירים את גבול פארטו לקראת שיפורים בו-זמניים בתעבורה ובמהירות הפלט.
שימוש במספר EP גבוה יותר פירושו שמוקצים לכל GPU פחות מומחים. לדוגמה, EP128 פירושו שכל GPU אחראי על 256 / 128 = 2 מומחים, כך שלחץ רוחב הפס של הזיכרון מופחת משמעותית. במילים אחרות, על ידי שימוש במספר EP גדול יותר, אנו מקבלים ביעילות רוחב פס זיכרון רב יותר. כאשר גודל ה-batch לכל GPU קטן מ-64, הגדלת גודל ה-batch אינה משפיעה באופן משמעותי על מהירות חישוב המומחים מכיוון שהגדלת מספר הקלטים אינה מעלה משמעותית את לחץ רוחב הפס של הזיכרון. לכן, אנו רואים שכאשר משתמשים ב-EP128, הגדלת גודל ה-batch אינה משפיעה על מהירות הפלט בצורה משמעותית כל כך.
באופן מעניין, בגדלי batch גדולים יותר (64 בקשות לכל GPU), הבחנו בתופעה חדשה: תעבורת פריסת צומת בודד גבוהה מעט מזו של פריסה מרובת-צמתים. חלק מהסיבה לכך היא של-NVLink בתוך הצומת יש רוחב פס גבוה יותר מאשר ל-InfiniBand בין צמתים. חלק נוסף נובע ממגבלות ביישום שלנו. ננתח תופעה זו בפירוט רב יותר בהמשך.
בשל מגבלות קיבולת הזיכרון, תצורת EP8 DP8 TP1 אינה יכולה להגיע לגודל אצווה של 128 לכל GPU, ולכן פריסה רב-צומתית היא עדיין בחירה טובה יותר בתרחישים השואפים לתפוקה גבוהה יותר.
חפיפת חישוב ותקשורת (Computation and Communication Overlapping)
כפי שהוצג בקצרה לעיל לגבי מקבילות מומחים, מעבדי ה-GPU נמצאים במצב סרק במהלך תקשורת שכבת ה-MoE. כדי לצמצם בזבוז ולהפחית השהיה, עלינו למצוא משימות חישוב בלתי תלויות בנתונים כדי למלא זמן סרק זה.
החלק העליון של האיור לעיל מציג את זרימת החישוב של שכבה אחת. חישוב ה-MoE תלויה ב-Dispatch, והחישוב של השכבה הבאה תלוי בתוצאה של Combine.
אנו ממקמים את המומחה המשותף בכל GPU. בדרך זו, חישוב המומחה המשותף אינו דורש תקשורת AllToAll. לכן, אנו יכולים לבצע חישוב מומחה משותף מיד לאחר שליחת Dispatch Send, ואז להמתין להשלמת Dispatch Recv. אנו קוראים לתבנית חפיפה זו "חפיפת Dispatch" (Dispatch Overlap).
חפיפת Dispatch מציעה יישום פשוט וישימות רחבה. טכניקה זו מסתירה את זמן חישוב המומחה המשותף בכל גדלי ה-EP וגדלי ה-batch.
כדי להגדיל עוד יותר את חפיפת החישוב והתקשורת, השתמשנו ב-micro-batching המוזכר בדוח הטכני של DeepSeek כדי לשבור את התלות בנתונים. כפי שמוצג בחלק התחתון של האיור, חילקנו את החישוב של שכבת Transformer אחת ל-5 שלבים:
שלב 1: InputNorm, QKVProj, AppendKV, BMM
שלב 2: BMM, Attn, OProj, PostNorm, Gate
שלב 3: Dispatch Send, מומחה משותף
שלב 4: Dispatch Recv, MoE, Combine Send
שלב 5: Combine Recv
ב-3 שכבות ה-Transformer הצפופות הראשונות, אנו משתמשים ב-batch המלא. ב-58 שכבות ה-Transformer הצפופות מסוג MoE הבאות, אנו מחלקים באופן שווה את ה-batch לשתי מנות מיקרו (micro batches). שתי מנות המיקרו מבוצעות לסירוגין, בהפרש של 3 שלבים. מכיוון שאין תלות בנתונים בין שתי מנות מיקרו אלו, אנו יכולים לעבור לחישוב של מנות מיקרו אחרת לאחר Dispatch Send ולאחר Combine Send.
פילוח השהיה (Latency Breakdown)
לאחר מכן, אנו משווים את ההשפעות של חפיפה באמצעות ניסוי, וכן משווים את הבדלי הביצועים בין פריסת צומת בודד EP8 לפריסה מרובת-צמתים EP128. לצורך השוואה נוחה, השתמשנו במעבדי GPU מסוג H100 בניסוי הבא. השתמשנו ב-TP1, גודל batch של 128 לכל GPU, אורך שאילתה של 2 לכל בקשה, ואורך מטמון KV של 5000.
האיור לעיל מציג את סך הזמן שהושקע בשכבת Transformer מסוג MoE אחת ואת שיעור ההשהיה של סוגים שונים של kernels. למעט Dispatch, Combine ו-GroupGEMM, זמן הביצוע של kernels אחרים צריך להיות זהה בסדרות EP8, EP128 NoOverlap ו-EP128 DispatchOverlap מכיוון שגודל ה-batch זהה.
חפיפה (Overlapping)
הבה נשווה תחילה את ההשפעות של שלוש שיטות החפיפה. NoOverlap לקח בסך הכל 2667 מיקרו-שניות (µs), DispatchOverlap לקח 2651 מיקרו-שניות, מה שחסך 16 מיקרו-שניות או רק 0.6%. MicroBatch הציג שיפור משמעותי מאוד, כאשר לקח 1896 מיקרו-שניות, שיפור מהירות של 29%. גם זמן ה-Dispatch וגם זמן ה-Combine הופחתו משמעותית. Dispatch ירד מ-593 מיקרו-שניות ל-367 מיקרו-שניות, ו-Combine מ-1012 מיקרו-שניות ל-237 מיקרו-שניות.
שימו לב שלעבור מנועי חישוב, פיצול אצווה בגודל 128 לשתי אצוות בגודל 64 מגדיל את זמן הביצוע הכולל. לכן, אף על פי שהזמן שהושקע בתקשורת פחת ב-1001µs, הזמן הכולל פחת רק ב-771µs. נסביר את הסיבה באמצעות מודל Roofline בסעיף הבא.
מסיבה זו, שימוש ב-microbatching אינו משפר תמיד את הביצועים.
האיור לעיל מציג את שיפור הביצועים של Microbatch בהשוואה ל-DispatchOverlap עבור גדלי batch 4-128. כאשר גודל ה-batch קטן מ-32, Microbatch מפחית את הביצועים ב-5%-40%. כאשר גודל ה-batch גדול או שווה ל-32, Microbatch יכול לשפר את הביצועים ב-10%-35%.
EP8 לעומת EP128
הבה נחזור לאיור הקודם ונשווה את EP8 ו-EP128 Microbatch. EP8 לקח בסך הכל 1802 מיקרו-שניות, מעט פחות מה-1896 מיקרו-שניות של EP128. מלבד זמן ביצוע ה-kernel המוגבר שהביא איתו Microbatch שהוזכר לעיל, ההבדלים העיקריים הם ב-GroupGEMM המשמש לחישובי MoE, ובשני ה-kernels לתקשורת, Dispatch ו-Combine.
ה-GroupGEMM של EP8 לקח 555 מיקרו-שניות, בעוד ש-GroupGEMM של EP128 לקח 270 מיקרו-שניות, מה שצמצם את הזמן בחצי. זהו היתרון המרכזי של פריסה מרובת-צמתים.
למרבה הצער, הזמן שהושקע בתקשורת גדל ב-213 מיקרו-שניות, מה שקיזז במידה רבה את היתרון של GroupGEMM. בבדיקות ביצועים נפרדות של ה-kernels לתקשורת שלנו, גילינו שהם יכולים להשיג רק מחצית מרוחב הפס של Infiniband. נמשיך לבצע אופטימיזציה ל-kernels לתקשורת שלנו.
kernel נוסף שמפגר משמעותית הוא GEMM. Microbatch הגדיל את GEMM ב-95 מיקרו-שניות. ננתח את GEMM לעומק רב יותר בסעיף Roofline שלהלן. אנו מאמינים שיישום ה-GEMM הנוכחי טרם הגיע לביצועים אופטימליים.
מודל Roofline
מודל Roofline הוא כלי טוב לניחוח ביצועי kernels. הציר האופקי שלו הוא אינטנסיביות אריתמטית (Arithmetic Intensity), היחס בין FLOP לבין בתים של קלט/פלט זיכרון. ניתן לחשב את ערך הציר האופקי ישירות מהסמנטיקה של ה-kernel. הציר האנכי מייצג את הביצועים שהושגו, המחושבים על ידי חלוקת FLOP בהשהיית הבנצ'מארק.
הגבול העליון התיאורטי של ביצועי ה-kernel נקבע ישירות על ידי מפרט ה-GPU. ביצועי השיא של H100 ב-FP8 הם 1979 TFLOP/s, המיוצגים כקו אופקי במודל Roofline. רוחב הפס של הזיכרון של H100 הוא 3.35 TB/s, המיוצג כשיפוע של קו העובר דרך הראשית. שני הקווים מספקים את מגבלות הביצועים עבור kernels המוגבלים על ידי חישוב ומוגבלים על ידי זיכרון, בהתאמה.
להלן, נדון בביצועים של ה-kernels GroupGEMM ו-GEMM.
GroupGEMM
ה-GroupGEMM kernel ב-MoE מבצע את החישוב הבא: ישנן g קבוצות בסך הכל, לקבוצה ה-i יש m_i טוקנים, המבצעים כפל מטריצות של [m_i, k] x [k, n] -> [m_i, n]. בבדיקות ביצועים, אנו מניחים שמספר הטוקנים בכל קבוצה זהה, ומסומן כ-m_i = m. אזי ספירת ה-FLOP עבור GroupGEMM היא 2 * g * m * k * n, ובתי קלט/פלט הזיכרון הם g * (m * k + n * k + m * n).
במודל DeepSeek-V3/R1, ישנם 256 מומחים, וכל טוקן משוגר ל-8 מומחים לצורך חישוב. בהנחת גודל batch של 128, אורך שאילתה של 2, ושימוש בתצורת EP128 DP128, המספר הממוצע של טוקנים המתקבלים על ידי כל מומחה (כלומר, m) הוא 128 * 2 * 8 * 128 / 256 = 1024. באופן דומה, אנו יכולים לחשב את m עבור תצורות וגדלי batch אחרים.
השתמשנו ביישום GroupGEMM של DeepGEMM לצורך בדיקות ביצועים. נקודות הבדיקה כיסו שילובים של תצורות EP8, EP16, EP32, EP64, EP128 עם TP1 וגדלי batch 1-128.
האיור לעיל מציג את מודל ה-Roofline עבור GroupGEMM תחת תצורות EP שונות. EP שונה תואם למספרים שונים של קבוצות. האיור מדגים קווי ביצועים חופפים כמעט, המצביעים על כך שביצועי GroupGEMM נקבעים בעיקר על ידי סך כל הטוקנים (המיוצגים כ-g * m).
הכוכבים מסמנים את נקודות הנתונים התואמות לגודל batch של 128 לכל GPU עבור כל תצורת EP. בהשוואה בין נקודות נתונים מסומנות בכוכב אלו, אנו יכולים לראות שככל ש-EP גדל (ו-DP גדל במקביל), מספר הטוקנים לכל מומחה m גדל גם הוא. ב-EP8, m=128, בעוד ב-EP128, m=2048.
ככל ש-m גדל, גם האינטנסיביות האריתמטית גדלה. ברוב התצורות, GroupGEMM מוגבל על ידי רוחב פס הזיכרון, ולכן הגדלת m משפרת את הביצועים.
GEMM
ה-GEMM kernel תואם להיטלים לינאריים במודל, כגון Q/K/V/O Projection. עבור כפל מטריצות של [m, k] x [k, n] -> [m, n], ספירת ה-FLOP היא 2 * m * k * n, ובתי קלט/פלט הזיכרון הם m * k + n * k + m * n. אנו יכולים גם לבדוק את ההשהיה עבור גדלי batch 1-128.
האיור לעיל מציג את מודל ה-Roofline עבור GEMM תחת תצורות EP שונות. אנו יכולים לראות שביצועי GEMM מוגבלים על ידי רוחב פס הזיכרון. ככל שגודל ה-batch גדל, גם האינטנסיביות האריתמטית גדלה, ובכך משפרת את הביצועים.
מנת מיקרו (Microbatch)
בעת שימוש ב-microbatching, אנו מחלקים את ה-batch באופן שווה לשני חלקים. משני האיורים שלעיל, אנו יכולים לראות שכאשר m הופך ל-m/2, יעילות כפל המטריצות יורדת. לכן, ביצוע שני כפלי מטריצות בגודל m/2 לוקח זמן רב יותר מביצוע כפל מטריצות אחד בגודל m.
חיזוי מרובה-טוקנים (Multi-Token Prediction)
לאורך מאמר זה, הנחנו שימוש בחיזוי מרובה-טוקנים (MTP) לפענוח ספקולטיבי. MTP משנה את אורך השאילתה לכל בקשה מ-1 ל-2. עבור כפל מטריצות, הדבר שקול לשינוי m ל-m * 2, ובכך להגדלת יעילות כפל המטריצות. מצד שני, אם נשרטט את מודל ה-Roofline עבור MLA, נגלה שהגדלת אורך השאילתה מגדילה משמעותית את יעילות ה-kernel של MLA.
לכן, השימוש ב-MTP ממלא תפקיד חשוב ביעילות המודל.
יישום ואופטימיזציות
בסעיף זה, נציג כמה פרטי יישום ואופטימיזציה עבור מודל DeepSeek-V3/R1 שלנו.
קואנטציה (Quantization)
מודל DeepSeek-V3/R1 אומן באופן מקורי על FP8 תוך שימוש בסכמת קואנטציה לפי בלוק (per-block quantization), כאשר המשקלים עברו קואנטציה סטטית והפעולות (activations) עברו קואנטציה תוך כדי תנועה (on-the-fly). במקום לחשב גורם קנה מידה (scaling factor) לכל ערוץ או לכל מטריצה באופן סטטי, גורמי קנה המידה מחושבים על פני וקטורים בני 128 אלמנטים עבור פעולות ואריחים בגודל 128x128 עבור מטריצות, מה שמגביל את פגיעת הדיוק עקב קואנטציה.
ב-Perplexity, אנו מתבססים על שילוב של CUDA ו-Triton kernels כדי לתמוך בהסקה, כאשר CUDA משמש עבור ה-kernels הרגישים ביותר לביצועים והמשתנים בתדירות הנמוכה ביותר (כגון קשב ו-GEMM), ו-Triton מיישם מגוון רחב של kernels של הפעלה (activation), נורמליזציה ושימושיות. Triton איפשר לנו להתאים במהירות את ה-kernels לסכמת הקואנטציה לפי בלוק.
עבור שכבות לינאריות ו-MoE, אנו מערבבים את Deep GEMM kernels עם ה-Triton GEMM kernels שלנו, מכיוון שהבחנו שעבור ממדי מטריצה מסוימים וגדלי batch נמוכים, Split-K מספק השהיה נמוכה יותר. אם השכבה הלא-מקוונטטת מבצעת כפל (M, K) x (K, N), היא זקוקה לגורם קנה מידה (M x ceil_div(K, 128)) x (ceil_div(K, 128), ceil_div(N, 128)) עבור קואנטציה לפי בלוק. עבור קואנטציה לפי בלוק, גורמי קנה המידה עבור פעולות מחושבים תוך כדי תנועה, במקום לעבור כיול מראש. מכיוון שגורמי קנה המידה של פעולות מצטברים רק לאורך ממד K ולא לאורך ממד M, ה-kernels דורשים שינויים קלים בלבד כדי לתמוך בסכמה זו.
פונקציית ההפעלה SiLU בשימוש DeepSeek-V3/R1 דרשה שינויים מהותיים כדי לתמוך בגרפים של CUDA, קואנטציה לפי בלוק ומספרי טוקנים מנותבים דינמית. קואנטציה לפי בלוק עלולה להיות בעייתית מכיוון שהיא מציגה הפחתות אופקיות, אך ה-kernel כבר פיצל את הפעולות לאורך ממד הנסתר שלהן לבלוקים של 1024 אלמנטים. בתוך בלוק אחד, הטנזור שעבר קואנטציה פוצל עוד יותר לבלוקים של 128 כדי לחשב את הערך המוחלט הגדול ביותר, כאשר Triton מייצר הפחתות מקסימום יעילות חוצות-warp (cross-warp max reductions), תוך הוספת overhead מינימלי.
כדי לתמוך בנתוב MoE תחת גרפי CUDA, ה-kernels חייבים להיות מודעים למידע הנתוב המציין את מספר הטוקנים לכל מומחה, במקום לתזמן עבודה בהתבסס על גודל המאגרים (buffers) שהוקצו להחזקת הגבול העליון של ספירת הטוקנים. איננו יכולים לפצל את הבעיה על סמך ממדי טנזור הקלט, ולכן אנו מריצים מספר קבוע של kernels עקביים הקוראים את מידע הנתוב כדי לקבוע כמה טוקנים מאוכלסים ומפצלים את עבודת עיבוד הפעולות ביניהם באופן דינמי.
כבר העלינו חלק מה-kernels שלנו לפרויקט FlashInfer ובעתיד נפתח עוד מהקוד שלנו.
שכבת MLA
אנו משתמשים ב-FlashInfer לחישובי MLA. FlashInfer תומך בהגדרות טבלת דפים (Page Table) גמישות ובביצועים גבוהים במיוחד.
מיזגנו את q_a_proj ואת kv_a_proj לתוך qkv_a_proj יחיד. ההשהיה ירדה מ-15.4 מיקרו-שניות + 14.8 מיקרו-שניות = 30.2 מיקרו-שניות ל-16.7 מיקרו-שניות.
פירקנו את kv_b_proj לשתי מטריצות, k_b_proj ו-v_b_proj. כתבנו BMM kernel מוקונט לפי בלוק FP8 עבור חישובים הקשורים לשתי מטריצות אלו.
גרף Cuda (Cuda Graph)
גרף Cuda יכול להפחית משמעותית את ה-overhead של הפעלת kernels, דבר הקריטי לביצועים. אנו יוצרים גרף Cuda עבור כל גודל batch.
לפני פיתוח ה-AllToAll Kernel שלנו, השתמשנו ב-torch.all_to_all_single() לתקשורת AllToAll. פעולה זו דורשת שכל מעבדי ה-GPU ישתמשו באותו גודל batch. עם זאת, קבוצות DP שונות עשויות להריץ גדלי batch שונים.
כדי להבטיח ש-all_to_all_single() תואם לקבוצות DP שונות המשתמשות בגדלי batch שונים, השתמשנו תחילה בפעולת allreduce() לפני כל הרצת מודל כדי לקבל את גודל ה-batch המקסימלי מבין כל קבוצות ה-DP. לאחר מכן גרמנו לכל קבוצות ה-DP להשתמש בגודל batch זה כדי לפעול.
למרות שגישה זו מבטיחה שנוכל להשתמש בגרף Cuda, יש לה שלושה חסרונות. ראשית, היא דורשת פעולת allreduce() נוספת. שנית, קבוצות DP עם גדלי batch קטנים יותר מוכרחות לבצע ריפוד (padding). שלישית, היא הופכת את קוד היישום שלנו למורכב.
לאחר יישום ה-AllToAll Kernel שלנו, איננו דורשים עוד שכל מעבדי ה-GPU ישתמשו באותו גודל batch. לכן, איננו זקוקים עוד לבצע פעולות allreduce() נוספות או לרפד את גדלי ה-batch.
נתב MoE (MoE Router)
נתב ה-MoE מיושם ב-Triton, תוך התבססות על מיון מותאם הנגזר מספריית הבסיס אשר גם עוקב אחר האינדקסים של האלמנטים הממוינים. היישום משותף לכל מודלי ה-MoE, מכיוון שנתוב Mixtral הוא מקרה פרטי של נתובי DeepSeek שבו קבוצת Top-K זהה לקבוצת כל המומחים. ה-kernels הדלילים צורכים את אינדקסים והציונים של Top-K ישירות, בעוד שתבניות שיגור/מיזוג צפופות המסתמכות על all-to-all דורשות שמידע הנתוב יאוגר לפי-מומחה במקום על בסיס כל-טוקן.
עבודה עתידית
בעבודה עתידית, אנו מתכננים לייעל עוד יותר את ביצועי מודל DeepSeek.
האופטימיזציה הבאה החשובה ביותר היא הפרדת טעינה מקדימה (Prefill Disaggregation). שלב הטעינה המקדימה (Prefill) ושלב הפענוח (Decode) של מודל DeepSeek-V3/R1 בעלי מאפייני חישוב שונים מאוד. שניהם יכולים להשתמש באסטרטגיות אופטימיזציה ובתבניות פריסה שונות.
עבור שכבת MLA, בשלב הפענוח, אנו משתמשים בבליעת מטריצות (Matrix Absorption) כדי להפחית את ספירת ה-FLOP של חישוב ה-MLA. בשלב הטעינה המקדימה, הטלת הווקטור החבוי תחילה למרחב K/V ולאחר מכן חישוב בצורת קשב רב-ראשי (Multi-Head Attention - MHA) תפעל טוב יותר.
אם Prefill ו-Decode פועלים על אותו GPU, כדי להפחית את ההשפעה של Prefill על מהירות הפלט של Decode, אנו בדרך כלל משתמשים ב-chunked prefill כדי לחלק את השאילתה למספר chunks עבור Prefill. מכיוון ש-KV Cache מאחסן את הווקטור הסמוי, קשה להמיר את MLA לצורת MHA.
עבור שכבת MoE, בשלב הפענוח, אנו משתמשים ב-EP ו-DP גדולים ככל האפשר כדי להגדיל את מספר טוקני הקלט לכל מומחה, ובכך לשפר את ביצועי GroupGEMM. בשלב הטעינה המקדימה, מכיוון שמספר הטוקנים כבר גדול דיו, GroupGEMM כבר מוגבל על ידי חישוב. לכן, עבור טעינה מקדימה, אנו יכולים להשתמש ב-EP ו-DP קטנים יותר.
אם הטעינה המקדימה והפענוח רצים על אותו GPU, כל עוד קבוצת DP כלשהי מבצעת טעינה מקדימה, ההשהיה של שכבות MoE על כל מעבדי ה-GPU תגדל, מה שמשפיע משמעותית על מהירות פלט הפענוח.
מלבד הפרדת טעינה מקדימה, אנו מתכננים גם לייעל את ההיבטים הבאים:
ביצועי AllToAll: ה-kernel של AllToAll שלנו יכול כיום להשיג רק שליש מרוחב הפס של Infiniband. אנו נמשיך לייעל את ה-kernel הזה.
פענוח ספקולטיבי בסגנון EAGLE: בנתונים לעיל, הנחנו שימוש בפענוח ספקולטיבי כדי לחזות טוקן 1. EAGLE יכול להשתמש במבנה עץ כדי לחזות טוקנים מרובים, ולשפר את אורך הקבלה, מה שיכול להגדיל משמעותית את מהירות הפלט.
GEMM Kernel: במודל ה-Roofline שהוצג קודם לכן, אנו יכולים לגלות שיעילות ה-GEMM kernel עדיין רחוקה מהגבול התיאורטי. אנו נמשיך לייעל את ה-kernel הזה.
GB200 NVL72: בפתרון האחרון של NVIDIA מסוג GB200 NVL72, 72 מעבדי GPU מסוג Blackwell מחוברים זה לזה באמצעות NVLink מהיר. עבור מודלי ארכיטקטורת MoE, זוהי הזדמנות ואתגר גדולים מאוד.
סיכום
פריסה מרובת-צמתים של מודלי DeepSeek MoE משיגה את מה שבדרך כלל בלתי אפשרי עם LLMs צפופים: שיפור בו-זמני של תעבורה והשהיה כאחד. על ידי פיזור מומחים על פני יותר מעבדי GPU, אנו ממעיטים את לחץ רוחב הפס של הזיכרון לכל התקן, מה המאפשר עיבוד מהיר יותר ותעבורת מערכת גבוהה יותר. הניסויים שלנו מראים שתצורות EP128 משיגות תעבורה הגבוהה עד פי 5 במהירויות פלט שקולות בהשוואה לפריסות צומת בודד.
טכניקות חפיפת חישוב-תקשורת כמו micro-batching מפחיתות משמעותית את ה-overhead של תקשורת מרובת-צמתים, כאשר היישום שלנו מציג שיפור מהירות של עד 40%. ה-kernels לתקשורת AllToAll המותאמים אישית שלנו ויישומי ה-kernel האופטימליים אפשרו פריסה יעילה של המודל בעל 671 מיליארד הפרמטרים.
כאשר ארכיטקטורות MoE צוברות פופולריות בזכות היכולת שלהן, אסטרטגיות פריסה אלו מספקות תובנות בעלות ערך להרחבת מודלים כאלה ביעילות.
הפניות
סקירת מערכת הסקה DeepSeek-V3/R1
Megatron-LM: אימון מודלי שפה רבי-מיליארדי פרמטרים באמצעות מקבילות מודל
EAGLE: דגימה ספקולטיבית דורשת חשיבה מחודשת על אי-ודאות תכונות
ציטוט
@online{perplexity2025LowerLatencyAndHigherThroughputWithMultiNodeDeepseekDeployment,
author = {{Perplexity} and Lequn Chen and Nandor Licker and Erin McKnight},
title = {{Lower latency and higher throughput with multi-node DeepSeek deployment}},
date = {2025-04-18},
url = {https://perplexity.coolkeypoint.com/hub/blog/lower-latency-and-higher-throughput-with-multi-node-deepseek-deployment}
}