טעינה מקדימה ופענוח מפורקים

הפרדת טעינה מקדימה ופענוח בין התקנים מאיצה את הסקת המסקנות של מודלי השפה ומשפרת את הביצועים

מחבריםPerplexity Research

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

ביצועי טעינה מקדימה לעומת פענוח

במנוע הגשה טיפוסי של LLM, מתזמן האצווה בוחר בקשות לעיבוד בכל שלב ביצוע של המודל. בעת ריצה על התקן או צומת בודדים, בקשות הטעינה המקדימה והפענוח מקובצות יחד באצוות. עלות מנגנון הקשב (attention), המצטברת לאורך אורך הרצף, גדלה הן עבור טעינה מקדימה והן עבור פענוח, ביחס ישר לאורכם של הפריטים במטמון ה-KV (kv_len). בקשות פענוח מעבירות בדרך כלל טוקן יחיד (qo_len=1), בעלות מינימלית דרך שכבות אחרות הפועלות באופן עצמאי על הטוקנים של רצף. בקשות טעינה מקדימה מעבירות אלפים או עשרות אלפים של טוקנים בעלות משמעותית דרך שכבות צפופות (qo_len גדול).

השהיה של מעבר קדמי מושפעת יותר ממספר האסימונים הבלתי תלויים העוברים דרך שכבות צפופות (qo_len) מאשר ממספר האסימונים הנשלפים ממטמון ה-KV במהלך תשומת הלב (kv_len). תשומת הלב יכולה להתבצע במקביל הן על פני מספר הבקשות והן על פני kv_len הפרופורציונלי לאורכי הרצף, וכך להשיג ניצול טוב. שלב ה-prefill כבול לחישוב: כאשר qo_len גבוה, ליבות GEMM יכולות להקצות מספיק בלוקים לאורך הממד M כדי לנצל במלואן את יכולות החישוב של GPUs מודרניים. שלב ה-decode כבול לזיכרון: בשל גדלי batch נמוכים בדרך כלל, מספר הקלטים לאורך M הוא בדרך כלל קטן, ומספיק לבלוק אחד בלבד. אף שליבות GEMM מסוג Split-K יכולות לשפר את ניצול ה-SM עבור גדלי batch נמוכים של אסימונים, המטמונים ויחידות כפל המטריצות בדרך כלל נותרים מנוצלים בחסר.

אצווה של בקשות דרך שכבת טרנספורמר, המציגה את השלכות השאילתה וה‑key/value, את מנגנון ה‑attention ואת עדכוני מטמון ה‑KV.

כאשר הן מעורבבות יחד, אצוות המכילות בקשות לטעינה מקדימה מייצרות השהויות גבוהות יותר במעבר הקדימה, מה שמשפיע לרעה על קצב הפענוח של המופע כולו. בעוד שערבוב בקשות טעינה מקדימה עם בקשות פענוח או שימוש בטעינה מקדימה מקוטעת עשויים לשפר מעט את ביצועי הפענוח, קשה לשמור על קצב טעינה מקדימה מספיק כדי לעבד מספיק בקשות במופע כדי למקסם את קצב הפענוח. במקרה של מודלים גדולים, עםורכי פלט טיפוסיים, כדי לשמור על גודל אצווה גדול לפענוח, יש לבצע טעינה מקדימה לעתים קרובות כל כך שהיא פוגעת משמעותית בהשהות הממוצעת וגורמת לגמגום בפלט.

תזמון משולב של prefill–decode עבור שלוש בקשות, המציג את מרווחי ה‑prefill וה‑decode על ציר זמן משותף.
תזמון prefill–decode מנותק, עם prefiller ייעודי המזרים עבודה ל‑decode עבור שלוש בקשות.

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

שליח KV

ב-Perplexity, היישום שלנו לטעינה מקדימה ופענוח מפורקים בנוי סביב שליח KV המקיים אינטראקציה עם מנוע ה-LLM כדי לתזמר העברות מטמון KV מצומתי טעינה מקדימה לצומתי הפענוח דרך רשת. בצד הטעינה המקדימה, השליח מקבל בקשות מצומתי הפענוח, מעביר אותן למתזמן האצוות ועוקב אחר ביצוע מעבר הקדימה כדי לשגר מטמוני KV בשהות מינימלית ככל האפשר. בצד הפענוח, לאחר הקצאת דפים שלא ניתן לפנות, השליח חוסם את הבקשה מלתזמון לפענוח עד לקבלת הודעה על השלמת העברות מטמון ה-KV והקשר הפענוח.

מחזור חיים מנותק של בקשה, המציג תיאום בין ה‑decoder ל‑prefiller והעברת נתוני מטמון ה‑KV.

פירוק הטעינה המקדימה דורש חיבורים בעלי קצב תעבורה גבוה ושהות נמוכה, ולכן היישום שלנו מותאם ל-RDMA, ותומך בבקרי ממשק רשת (NICs) מסוג EFA ו-ConnectX. שליח ה-KV בנוי על גבי libfabric, תוך שימוש במעטפות ה-fabric-lib שלנו כדי לספק הפשטות ברמה גבוהה ובעלות שהות נמוכה על גבי פרימיטיבי גישה ישירה לזיכרון מרחוק (RDMA), המיישמות העברות יעילות של דפים ומטא-נתונים, לצד איתות בשהות נמוכה. ברקע, fabric-lib מתאם בין מעבד גרפי (GPU) לבין בקר ה-NIC המחובר אליו ישירות כדי להעתיק נתונים מצומת הטעינה המקדימה לצומת הפענוח.

עם הקבלה, צומת הטעינה המקדימה מקצה קבוצה תואמת של דפי KV מקוריים ומתזמן את הבקשה לטעינה מקדימה באמצעות המנוע המקומי שלו. כדי למזער את ההשהות, ההעברות אינן ממתינות למעבר הקדימה: במקום זאת, העתקות דפי ה-KV מתחילות ברגע שהמודל מסיים לצרף פריטי מטמון KV למטמון ה-KV עבור שכבות בודדות. מכיוון שניתן לחלק בקשות טעינה מקדימה למקטעים, מתזמן האצוות מעדכן את שליח ה-KV אודות המקטעים המתוזמנים הנוכחיים טרם הביצוע. כדי לתמוך בגרפים של CUDA תוך שמירה על יכולת לעקב אחר שכבות, השליח שומר שרשור ייעודי המבצע סקר (polling) על מונה המוגדל לאחר תחזית הפלט של הקשב. הממונה מוחזק רק בצומת המוביל בסביבה מקוצצת (sharded): למרות שפריטי מטמון ה-KV תקפים לאחר ההוספה ולפני הקשב, תחזית הפלט עוברת צמצום (reduction) על פני דרגות, מה שמסנכרן אותן באופן משתמע. ברגע שמזוהה שינוי במונה, השליח מקבל הודעה והוא קורא ל-fabric-lib כדי להתחיל את ההעברה של שכבה.

העברות מטמון KV שכבה אחר שכבה, המציגות כתיבות מחולקות לדפים וכתיבות בודדות ומוני השלמה בין ה‑prefiller ל‑decoder.

לאחר השלמת העברת המקטע האחרון, כל מטא-נתונים נוספים מועתקים גם הם: פענוח ספקולטיבי או MTP דורשים העברה של לוגיטים ומצבים נסתרים למפענח. העתקות אלו מתבצעות גם הן באמצעות RDMA, אל ומתוך מאגרים שהוקצו מראש.

עם השלמת כל ההעברות הממתינות של המקטע האחרון, צומת הטעינה המקדימה מבטל את הקצאת דפי ה-KV ומשלים את הבקשה. צומת הפענוח אינו מקבל הודעה מפורשת: במקום זאת, הוא משתמש במונים מיידיים כדי לעקוב אחר מספר הפעולות שהושלמו. מספר פעולות ה-RDMA בצד הטעינה המקדימה פרופורציונלי למספר הדפים שהועברו. עם השלמת מספר העתקות הדפים וההקשר הידוע, fabric-lib קורא לשליח ה-KV כדי לציין שבקשה מוכנה לפענוח. השליח מבטל את הקצאת כל הקשר ומעביר את הבקשה למנוע ה-LLM.

העברות מטמון KV מקוצצות (Sharded)

אם הטעינה המקדימה והפענוח נסמכים על מקביליות טנסורית (TP) ומבצעים קיצוץ (sharding) או שכפול של מטמוני ה-KV באופן זהה, מנוע העברה יחיד מתאם מספר התקנים כדי לשלוח ולקבל את הדפים של כל העותקים. על מנת לאפשר שימוש בשליח יחיד ובמנוע העברה למרות העובדה שמבצע המודל משוכפל על פני התקנים ותהליכים רבים, נעשה שימוש ב-cuMem וב-cuMemImportFromShareableHandle כדי להקצות את זיכרון ההתקן התומך במטמוני ה-KV ולמפות אותו לתוך התהליך הראשי. מנוע ההעברה בודק את הטופולוגיה של הצומת כדי למצוא את ה-NICs והמעבדים (CPUs) בצומת ה-NUMA הקרוב ביותר שבהם יש להשתמש עבור ההעברות של כל אחד מפרוסות מטמון ה-KV.

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

טופולוגיית העברת מטמון KV בין prefiller מסוג TP/EP ו‑decoders מסוג DP/EP, המציגה את חיבורי ה‑NIC.

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

פריסות מטמון KV מסוג NHD ו‑HND, המשוות את סדר ה‑tokens וה‑heads בתוך כל שכבה ודף.

העברות מקוצצות דורשות התאמה קלה למטמוני ה-KV. כברירת מחדל, FlashInfer נסמך על פריסת NHD, המסדרת את הטוקנים בתוך דף בתוך הראשים. מכיוון שמטמונים עוברים בדרך כלל קיצוץ לאורך מספר ראשי הקשב, הדבר יוצר אי-רציפות בתוך הראש. העברות RDMA אינן תומכות באופן משתמע בכתיבות מנוקדות (strided), ודורשות פעולה אחת לכל ראש לביצוע ההעברה. במקום זאת, על מנת להפחית את מספר האינטראקציות עם libfabric, אנו מסדרים מטמוני KV באמצעות פריסת HND המקשמה את ממד הראש לפני מספר הטוקנים. הדבר מבטיח רציפות, ומאפשר להעתיק דף בכתיבה יחידה.

פענוח ספקולטיבי

פענוח ספקולטיבי דורש התאמות קלות לטעינה מקדימה ופענוח מפורקים. ביישום שלנו, צומתי טעינה מקדימה אינם מורשים לדגום טוקנים. מכיוון שמודלי ה-Sonar של Perplexity תומכים בפלט ממונה, איננו רוצים ליטול על עצמנו את המורכבות של סנכרון יישומי מעבד הסכמה בין צומתי טעינה מקדימה לצומתי פענוח. במנגנוני ה-MTP והפענוח הספקולטיבי, טעינה מקדימה של מודל הטיוטה עד לטוקן האחרון כרוכה בדגימת טוקנים ממודל היעד.

פענוח ספקולטיבי בין ה‑prefiller ל‑decoder, המציג את אורך מטמון ה‑KV, את ה‑logits ואת דגימת ה‑token.

כדי לעקוף בעיות אלו, הטעינה המקדימה אינה כוללת את הטוקן האחרון של רצף הקלט. במקום זאת, מצבים נסתרים או לוגיטים מטעינה מקדימה שקדמו לטוקן האחרון מועברים והוא מטופל כטוקן פענוח בשלב הבא במפענח. בעוד שהדבר מעלה מעט את ההשהויות, שכן יש לבצע שלב פענוח מלא לאחר הטעינה המקדימה כדי להפיק את הטוקן הראשון, מורכבות היישום מצומצמת משמעותית.

פריסות מפורקות

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

DeepSeek-R1

עם DeepSeek, בחנו פריסות מקביליות טנסורית (TP) ומקביליות נתונים (DP) כאחד. כפי שנדון בפוסטים קודמים בבלוג, פריסות TP מספקות השהות טובה יותר במחיר של קצב תעבורה נמוך יותר, ודורשות מעבדים גרפיים נוספים כדי לשרת תעבורה כבדה. פריסות DP משתוות טוב יותר עם עומס, אך קצב התעבורה השיא שלהן נמוך יותר בשל העלות של תקשורת בין התקנים או בין צמתים.

DeepSeek נסמך על קשב לטנטי מרוב ראשים (Multi-Head Latent Attention), ומכווץ מטמוני KV. מכיוון שכל ראשי ה-KV מכווצים לתוך וקטור לטנטי יחיד, TP אינו יכול לקצץ את מטמוני ה-KV, שכן עליו לשכפל במקום זאת את הווקטורים הלטנטיים על כל הדרגות. הקיצוץ מתרחש לאחר הפיקוח, שכן כל דרגה יכולה לחלץ ראשים שונים מתוך אותו ייצוג לטנטי. כתוצאה מכך, כל פרוסות מטמון ה-KV זהות על פני פרוסות הטעינה המקדימה והפענוח כאחד.

עם הגדרת TP פנים-צמתית, הן צומתי הטעינה המקדימה והן צומתי הפענוח מקוצצים באופן זהה. ההעברות משוגרות מכל הדרגות על מנת לנצל במלואן את כל בקרות ה-NIC הזמינות. עם זאת, בפריסת DP, שבה גודל דרגת ה-TP נמוך יותר או שכל דרגת DP משויכת למעבד גרפי בודד, כל התקן טעינה מקדימה שמחזיק עותק משוכפל של מטמון ה-KV יכול לשגר אותו. כדי לאזן בקשות על פני כל בקרות ה-NIC הזמינות, אנו בוחרים באקראי מעבד גרפי ובקר NIC כדי לשלוח את מטמון ה-KV מצומת הטעינה המקדימה לצומת הפענוח.

עם שילוב של טעינה מקדימה ופענוח, פריסת ה-R1 שלנו התקשתה לעבור בעקביות 50 TPS עקב הפרעות טעינה מקדימה תכופות בסדר גודל של מאות מילישניות. בניגוד לכך, על ידי הפרדת הטעינה המקדימה, ספגנו קנס של כ-100 מילישניות לזמן ה-TTFT עבור כל בקשה, אך צומת טעינה מקדימה יחיד יכול לשמור על גדלי אצווה עקביים ב-3 צומתי פענוח, תוך אספקת קצב תעבורה העולה על 90 TPS בעת טיפול בעומס של כ-1 QPS לכל צומת פענוח. בפריסות מקביליות נתונים, ה-TPS היה נמוך מעט ועמד על כ-50, אך המופעים יכלו להתמודד עם עומס של 1 QPS לכל דרגה, עם 8 דרגות לצומת יחיד.

Qwen3-Coder

מודל 480B זה משתמש בקשב שאילתות קבוצתיות (GQA), כך שניתן לקצץ את הקשב בקלות וליהנות ממקביליות טנסורית מבלי להקריב זיכרון עבור מטמוני KV. כתוצאה מכך, יכולנו לקצץ את המודל על פני 8 מעבדים גרפיים עבור טעינה מקדימה ופענוח כאחד, ולזווג כ-3 צומתי פענוח עם צומת טעינה מקדימה יחיד. מכיוון שהקשב מקוצץ, אנו נסמכים על פריסת מטמון ה-KV מסוג HND כדי לקצץ את מטמוני ה-KV של הטעינה המקדימה והפענוח, תוך שיוך דרגות טעינה מקדימה לדרגות פענוח וניצול מלא של כל בקרות ה-NIC כדי להעביר פרוסות במקביל.

ציטוט

BibTeX
@online{perplexity2025DisaggregatedPrefillAndDecode,
  author = {{Perplexity} and Nandor Licker and Lequn Chen and Kevin Hu and Erin McKnight and Phi Hoang},
  title = {{Disaggregated prefill and decode}},
  date = {2025-08-01},
  url = {https://perplexity.coolkeypoint.com/hub/blog/disaggregated-prefill-and-decode}
}