למה מודל LLM באירוח עצמי מואט עם ריבוי משתמשים?
השרת שלכם איטי כי Ollama מעבד בקשה אחת בלבד כברירת מחדל. למדו כיצד batching, ניהול KV cache ושימוש במנוע הגשה מתאים יאפשרו לכם לשרת יותר משתמשים בו-זמנית ללא שדרוג חומרה.
מדוע מודל שפה (LLM) באירוח עצמי מואט כשנוספים משתמשים?
מודל שפה באירוח עצמי נתקע ב-5 משתמשים בו-זמנית מכיוון שהשרת עדיין מייצר תשובה אחת בכל פעם, וארבעת המשתמשים האחרים ממתינים בתור. התיעוד של Ollama מבהיר זאת לגבי ברירת המחדל: OLLAMA_NUM_PARALLEL הוא "מספר הבקשות המקבילות המקסימלי שכל מודל יעבד בו-זמנית, ברירת המחדל היא 1". שום דבר לא מקולקל. ארבעה מתוך חמשת האנשים שלכם פשוט מחכים לתורם.
התיקון הוא לעיתים רחוקות שדרוג השרת לחומרה חזקה יותר. הפתרון הוא מנוע הגשה (serving engine) שדוחף בקשות רבות דרך המודל באותו forward pass, בתוספת מספיק זיכרון פנוי כדי להחזיק את השיחה של כל המשתמשים בזמן העיבוד. שני החלקים חשובים, והחלק השני הוא זה שקובע בפועל את תקרת הביצועים שלכם.
שני השלבים שכל בקשה עוברת
שלב ה-Prefill קורא את כל ה-prompt בבת אחת ובונה עבורו את ה-attention cache. כל אסימון (token) ב-prompt עובר דרך המודל במקביל, לכן ה-Prefill הוא פעולת כפל מטריצות אחת גדולה, והוא מוגבל על ידי תפוקה אריתמטית. שלב ה-Decode כותב את התשובה אסימון אחד בכל פעם. כל אסימון דורש קריאה חוזרת של מלוא משקולות המודל מהזיכרון, בעוד שהפעולה האריתמטית המבוצעת על אותו אסימון בודד היא מזערית. ה-Decode מוגבל על ידי רוחב פס של זיכרון.
אסימטריה זו היא הסיבה לכך ש-batching עובד. פעולת Decode עבור משתמש אחד קוראת, למשל, 5 GB של משקולות לכל אסימון ומשאירה את רוב היחידות האריתמטיות במצב המתנה. הוספת בקשה שנייה גורמת למנוע לקרוא את אותם 5 GB פעם אחת, ואז לחשב שני אסימונים מתוכם. המשתמש השני כמעט אינו מוסיף זמן המתנה. הגשת בקשות בזו אחר זו מבזבזת את היתרון הזה.
שני מדדים מתארים את חוויית המשתמש. TTFT (זמן לאסימון ראשון) הוא זמן ההמתנה בתור בתוספת ה-Prefill. ITL (זמן השהיה בין אסימונים) הוא המרווח בין אסימונים מוזרמים, והוא נקבע על ידי ה-Decode. שרת איטי סובל בדרך כלל מבעיה באחד מהם, והפתרונות אינם זהים. כדאי לקבוע עם איזה מהם אתם מתמודדים לפני שינוי הגדרות כלשהן, ו-מדידת זמני prefill ו-decode בנפרד היא הדרך לגלות זאת.
עיבוד אצווה סטטי גורם לכולם להמתין לתגובה האיטית ביותר
עיבוד אצווה סטטי (Static batching) הוא הגרסה הנאיבית, והוא מתקבל כאשר מקבצים בקשות באופן ידני בקוד היישום. המנוע אוסף N בקשות, מריץ אותן יחד, ומחזיק כל משבצת (slot) פנויה עד לסיום היצירה הארוכה ביותר בקבוצה.
משתמש המבקש סיכום של 1,200 אסימונים (tokens) גורם לארבע תשובות קצרות להישאר נעולות באצווה, כיוון שהאצווה אינה משחררת אף משבצת עד שהחבר האיטי ביותר בה מסיים את עבודתו.
לכך מתלווים שני מחירים. רצפים שהסתיימו ממשיכים לתפוס משבצות שאינן מחשבות דבר מועיל, ולכן התפוקה האפקטיבית יורדת ככל שאורכי הפלט משתנים – ואורכי פלט של צ'אט משתנים במידה רבה. בקשה שמגיעה צעד אחד לאחר יצירת האצווה ממתינה עד להתרוקנות האצווה כולה לפני שהיא מתחילה בשלב ה-prefill, מה שאומר שזמן ה-TTFT שלה נקבע על ידי חיבור של מישהו אחר.
Continuous batching מקבל ומסיים בקשות בכל token
Continuous batching מתזמן ברמה של צעד פענוח (decoding step) בודד. לאחר כל צעד, המתזמן מסיר רצפים שזה עתה פלטו את ה-stop token שלהם, ואז מכניס בקשות ממתינות למקומות הפנויים. מענה שמסתיים בצעד 40 מפנה את המקום שלו בצעד 40, ולא בסוף ה-batch.
זה אינו מנגנון אקזוטי. llama-server מתעד את -cb, --cont-batching כ-"האם להפעיל continuous batching (ידוע גם כ-dynamic batching) (ברירת מחדל: מופעל)", ו-vLLM בנוי סביב רעיון זה. גם Ollama משרת בקשות במקביל. ברירת המחדל פשוט מגבילה את המספר לאחד, וזו הסיבה שמשתמשים רבים מסיקים שהחומרה שלהם אינה מסוגלת לבצע עיבוד מקבילי, בעוד שהתצורה שלהם היא זו שחסמה זאת.
תוצאות של continuous batching שפורסמו נמדדות בדרך כלל על כרטיסי datacenter שיש להם גם כוח מחשוב פנוי וגם עשרות גיגה-בייט עבור ה-cache. צורת התוצאות הללו רלוונטית גם לשרת שלכם. הגודל שלהן אינו רלוונטי, וסעיף הזיכרון להלן מסביר מדוע.
שלב ה-prefill מתחרה עם ה-decode על אותם משאבי חישוב
כאשר בקשה חדשה מגיעה בזמן שארבע תשובות נמצאות בתהליך הזרמה (streaming), ה-prompt שלה חייב לעבור prefill תחילה, ופעולת ה-prefill צורכת משאבי חישוב רבים. אם ה-scheduler מקצה ל-prefill זה צעד (step) משלו, ארבעת המשתמשים שמקבלים הזרמה לא יקבלו אף token במהלך זמן זה. ב-prompt ארוך, מדובר בהשהיה מורגשת בכל חלון פתוח. זהו ה-"גמגום" שאליו מתכוונים משתמשים כשהם אומרים שהשרת "מקרטע" בכל פעם שמישהו אחר לוחץ על שליחה.
טכניקת ה-chunked prefill מפרקת prompt ארוך לחלקים ומשלבת כל חלק באותו צעד שבו מתבצעים ה-decodes הפעילים. מדריך הכוונון של vLLM מציג את הפשרה באופן ישיר: תקציבי chunk קטנים יותר "משיגים ITL טוב יותר מכיוון שיש פחות פעולות prefill שמאטות את ה-decodes", בעוד ערכים גבוהים יותר "משיגים זמן טוב יותר עד ל-token הראשון (TTFT), כיוון שניתן לעבד יותר tokens של prefill ב-batch אחד". עליכם לבחור את חוויית המשתמש שברצונכם להגן עליה: האדם שממתין לתחילת התשובה, או האנשים שצופים בטקסט זורם.
אורך ה-prompt קובע עד כמה הדבר יפגע בביצועים. prompt של 6,000 tokens עם תשובה של 200 tokens מהווה 6,000 tokens של עבודת prefill מול 200 צעדי decode. צ'אט מבוסס שליפת מידע (RAG) ו-system prompts ארוכים דוחפים אתכם למשטר עבודה כזה, כך שה-prefill מפסיק להיות זניח והופך לגורם שמעכב את המשתמשים. טכניקת ה-prefix caching מסייעת כאשר החלק הארוך חוזר על עצמו: vLLM חושף את --enable-prefix-caching, אשר עושה שימוש חוזר ב-cache עבור prefix משותף של ה-prompt במקום לחשב אותו מחדש עבור כל בקשה.
הזיכרון שנגמר ראשון הוא ה-KV cache
כל טוקן בכל שיחה פעילה משאיר וקטור מפתח (key vector) ווקטור ערך (value vector) בכל שכבה של המודל. זהו ה-KV cache (מטמון מפתח/ערך), והוא המאפשר לתהליך ה-decode להימנע מחישוב מחדש של כל ה-prompt עבור כל טוקן חדש. גודלו לכל טוקן נקבע לפי מבנה המודל: 2 (מפתח אחד וערך אחד) כפול מספר השכבות, כפול מספר ראשי המפתח/ערך (key/value heads), כפול ממד הראש, כפול מספר הבתים לערך. קראו את המספרים הללו מתוך ה-config.json של המודל.
בצעו את החישוב פעם אחת והתקרה תפסיק להיות תעלומה. מודל 8B טיפוסי עם 36 שכבות, 8 ראשי מפתח/ערך וממד ראש של 128, השומר את המטמון ב-16-bit, צורך 2 36 8 128 2 בתים לכל טוקן. מדובר ב-147,456 בתים, בערך 144 KiB. שיחה אחת של 8,192 טוקנים דורשת לכן בערך 1.2 GB של מטמון. חמש שיחות כאלו דורשות בערך 6 GB, מעבר למשקולות המודל, וזו התשובה האמיתית לשאלה כמה משתמשים נכנסים.
מקביליות מכפילה את ההקשר (context), והכלים מצהירים על כך בגלוי. ה-FAQ של Ollama מציין: "עיבוד בקשות מקבילי עבור מודל נתון גורם להגדלת גודל ה-context לפי מספר הבקשות המקביליות. לדוגמה, context של 2K עם 4 בקשות מקביליות יביא ל-context של 8K ולהקצאת זיכרון נוספת". ה-RAM הנדרש גדל לפי OLLAMA_NUM_PARALLEL כפול OLLAMA_CONTEXT_LENGTH. ב-llama-server, ה-context שאתם מבקשים עם -c מתחלק בין ה--np slots, כך שהעלאת מספר ה-slots כשלעצמה מקטינה את מה שכל בקשה יכולה להכיל. קראו את ה-context לכל slot מתוך לוג ההפעלה במקום להניח אותו.
vLLM מבצע הקצאה מראש (preallocation) במקום זאת. --gpu-memory-utilization (ברירת המחדל היא 0.92) הוא "חלק זיכרון ה-GPU שישמש את ה-model executor". כל מה שנשאר לאחר טעינת המשקולות הופך למאגר paged KV, וכאשר המאגר הזה אוזל, ה-scheduler מוציא בקשה מהתור במקום להיכשל:
WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.במנוע V1 של vLLM, מצב ה-preemption המוגדר כברירת מחדל הוא RECOMPUTE, כך שבקשה שהוצאה מהתור מאבדת את המטמון שלה ומבצעת prefill מחדש כשהיא מוחזרת. העבודה הזו מתבצעת פעמיים. התיעוד מזהיר ש-"preemption ו-recomputation עלולים להשפיע לרעה על ה-latency מקצה לקצה", ושורת לוג זו היא ההסבר הטוב ביותר לכך שמשתמש חסר מזל אחד המתין זמן רב הרבה יותר מכולם, בעוד הממוצע שלכם נראה תקין. הגדירו את disable_log_stats=False כדי לתעד את המונה המצטבר, או קראו את מונה ה-preemption מתוך מדדי ה-Prometheus ש-vLLM חושף.
מה משתנה ב-2, 5 ו-20 משתמשים בו-זמנית
שני משתמשים. העומס כמעט בלתי מורגש על GPU עם זיכרון מטמון פנוי, כיוון שזרם הפענוח השני "רוכב" על הראשון בתוספת זמן זניחה. בשרת VPS מבוסס CPU בלבד עם 4 עד 8 GB של RAM, זה לא בחינם: שני הזרמים חולקים את אותם vCPUs ואת אותו רוחב פס של ה-RAM, לכן כל משתמש מקבל בערך מחצית מקצב ה-tokens לשנייה, ודרישת ה-cache מוכפלת מול תקציב מצומצם בהרבה.
חמישה משתמשים. כאן הגדרות ברירת המחדל מפסיקות להספיק, והבעיה מתחילה כבעיית תור. כאשר OLLAMA_NUM_PARALLEL מוגדר ל-1, ארבעה אנשים ממתינים למי שביקש את התשובה הארוכה, וכל אחד מהם רואה מהירות תקינה ברגע שתורו מגיע. העלאת מספר המקביליות משנה את אופי הבעיה: חמישה חריצים עם 8K context כל אחד דורשים 40K של token cache. אם זה לא נכנס ב-VRAM, המנוע מעביר שכבות ל-RAM של המערכת, ואם זה לא נכנס ב-RAM, השרת מבצע swap וקצב ה-tokens לשנייה קורס.
עשרים משתמשים. עשרים בני אדם בממשק צ'אט הם בדרך כלל לא עשרים בקשות בו-זמנית, וזה הדבר החשוב ביותר להבין לפני רכישת חומרה. אדם קורא תשובה וחושב במשך 20 עד 60 שניות בין תור לתור, לכן רוב הסשן שלו אינו פעיל. עשרים סוכנים, או עשרים משימות סיכום מסמכים, הם עשרים זרמים אמיתיים ללא זמן המתנה כלל. זו מכונה מסוג אחר. מפתח שהפנה סוכן תכנות לשרת Ollama שלו נמצא קרוב יותר למקרה השני מאשר לראשון, כיוון שהסוכן ממשיך לשלוח בקשות כל עוד המשימה רצה ולא מותיר את הפסקות הקריאה שמאפיינות אדם.
האם המשתמשים שלכם פעילים בו-זמנית, או רק מחוברים?
חשבו את כמות הבקשות הפעילות (in flight) לפני שתקבעו את גודל המשאבים. החישוב פשוט: מספר הבקשות הפעילות שווה למספר המשתמשים, כפול מספר השניות הנדרשות ליצירת תוכן בכל סבב, חלקי מספר השניות בין סבב לסבב.
- מדדו תחילה את מהירות הזרם הבודד שלכם, כולל prefill ופענוח. אל תסתמכו על נתונים של כרטיס אחר: מדדו tokens per second על השרת שלכם והשתמשו בתוצאה שהתקבלה.
- העריכו את מחזור העבודה (duty cycle). עשרים משתמשי צ'אט, 12 שניות יצירה לכל סבב, וסבב אחד בכל 90 שניות, נותנים 20 * 12 / 90, כלומר כ-2.7 בקשות פעילות.
- הגדירו את מספר ה-slots מעט מעל ערך זה, ולאחר מכן בדקו אם הוא מתאים לזיכרון: מכפלת ה-slots בנפח ה-context לכל בקשה חייבת להיכנס בתוך ה-cache tokens העומדים לרשותכם.
- שמרו על תור קצר כדי שכשל עקב חריגה (overflow) יתרחש במהירות ובאופן גלוי.
כמות ה-cache tokens הזמינה היא הזיכרון הפנוי לאחר טעינת המשקולות (weights), מחולק בעלות לכל token כפי שצוין בסעיף הקודם. כרטיס של 24 GB המריץ מודל 8B ב-16-bit משתמש בכ-16 GB עבור המשקולות, ונותרים לו כ-6 GB של cache זמין בניצול ברירת המחדל, מה שמאפשר כחמש שיחות של 8K. כדי להכיל יותר, קצרו את ה-context לכל בקשה, או אחסנו את ה-cache ב-8-bit (llama-server דורש --cache-type-k q8_0). שתי האפשרויות מאפשרות הגדלת כמות המשתמשים בו-זמנית במחיר של ויתור על איכות או דיוק, וכדאי לקרוא על המשמעות של פשרה זו לפני השקעה בחומרה: מתי GPU VPS משתלם יותר מאשר שימוש ב-API tokens.
מתי ברירות המחדל של Ollama אינן מספיקות
הגדילו את מספר הבקשות המקבילות דרך יחידת השירות, כיוון שפקודת export ב-shell לא תשפיע על daemon שמנוהל על ידי systemd.
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama pssystemctl show אמור להציג את שלושת המשתנים שהגדרתם זה עתה. אם לא, קובץ ה-drop-in לא נשמר, ושום פעולה אחרת לא תועיל. ollama ps יציג לאחר מכן את המודל הטעון עם גודל גדול יותר ממשקל המודל בלבד, כיוון שארבעה חריצים של 8,192 tokens משריינים 32,768 tokens של זיכרון מטמון (cache) לצדם. עמודת PROCESSOR המציגה חלק מהמודל על ה-CPU כאשר ציפיתם שכולו יהיה על ה-GPU, משמעותה שביקשתם יותר זיכרון מטמון ממה שנותר בכרטיס. הקטינו את אחד משני המספרים. צמצום ההקשר (context) הוא בדרך כלל הדרך הבטוחה יותר, אך חלון קטן מדי יקצץ הודעות ארוכות בשקט במקום להציג שגיאה, לכן כדאי לבצע sizing ל-num_ctx באופן מכוון במקום להקטין אותו עד שהמודל ייכנס.
ברירת המחדל של התור דורשת בחינה נוספת. Ollama מנהל תור של עד OLLAMA_MAX_QUEUE בקשות, ו"ברירת המחדל היא 512". מעבר לכך, הוא משיב "בשגיאת 503 המציינת שהשרת עמוס". תור באורך 512 על שרת שמשרת ארבע בקשות במקביל הוא הבטחה שלא ניתן לקיים, כיוון שהלקוח שנמצא במיקום ה-300 יחווה timeout הרבה לפני שיגיע תורו. תור קצר מחזיר שגיאה שהיישום שלכם יכול לנסות שוב או לדווח עליה, וזה עדיף על חיווי טעינה שלעולם לא מסתיים.
בצעו בדיקה אמיתית. שלחו שתי בקשות בו-זמנית משני טרמינלים ועקבו אחר שתיהן. אם הבקשה השנייה לא מפיקה דבר עד שהראשונה מסתיימת, הגדרת המקביליות לא נכנסה לתוקף.
מתי מנוע הגשה אמיתי הופך למשתלם
vLLM מצדיק את מאמץ ההתקנה הנוסף כאשר ברשותכם GPU עם יתרה במשאבים ויותר מארבע בקשות פעילות בו-זמנית. מתזמן המשימות שלו פועל ברמת ה-token, ה-cache שלו מנוהל בדפים כך שקטעי זיכרון פנויים מנוצלים מחדש, והוא ממיר VRAM פנוי לביצועים מקביליים במקום להותיר אותו ללא שימוש. נכון לאוגוסט 2026, ההתקנה וההרצה המתועדות מסתכמות בשתי פקודות:
uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instructcurl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-1.5B-Instruct",
"messages": [{"role": "user", "content": "Say hello."}]
}'תגובה המכילה מערך choices מעידה על כך שהשרת פעיל והמודל טעון. תחת עומס, שני הפרמטרים המשמעותיים הם --max-num-seqs, המגדיר את "מספר הרצפים המקסימלי לעיבוד באיטרציה בודדת", ו---max-num-batched-tokens, המגדיר את "מספר ה-tokens המקסימלי לעיבוד באיטרציה בודדת". הראשון מגביל את רמת המקביליות, והשני הוא תקציב ה-chunked prefill שתואר קודם לכן.
מתחת לרף של כארבע בקשות פעילות, או בכל שרת ללא GPU נתמך, vLLM מוסיף מורכבות ללא תמורה משמעותית. הוא דורש כרטיס מסוג CUDA ותופס את רוב הזיכרון בעת העלייה, מה שמהווה בחירה שגויה עבור שרת VPS עם 4 עד 8 GB זיכרון. במקרים אלו, הפתרון הוא מודל קטן יותר עם context קצר יותר ותור שאתם מנהלים. ההבדלים בין Ollama ל-vLLM כמנועי הגשה מפרט את השיקולים בבחירה, ו-הרצת Qwen 3 8B על שרת VPS מציג את הדרישות של מודל בינוני עוד לפני הוספת משתמש אחד נוסף.
הפשרה שהפולקלור מסתיר
Continuous batching מעלה את ה-throughput הכולל, ובדרך כלל משפר גם את ה-median latency, כיוון שבקשה שנמצאת בתור מתחילה מוקדם יותר. ה-tail latency משתנה לכיוון ההפוך, וחצי זה של התמונה מוזכר לעיתים רחוקות.
כל sequence נוסף ב-step מוסיף מעט עבודה, לכן ה-ITL עולה עבור כולם ככל שה-batch מתמלא. ה-prefill של בקשה חדשה שתופס חלק מ-step היה מוקצה אחרת למשתמשי ה-streaming. תחת לחץ על ה-cache, ה-scheduler מבצע preemption, מה ששולח בקשה שנוצרה בחלקה חזרה לתחילת ה-prefill שלה.
ממשק צ'אט מציג זנבות (tails), לא ממוצעים. stream שנעצר לשתי שניות באמצע משפט נתפס כמשובש, גם כאשר הזמן הכולל לסיום הוא טוב. מדדו p95 TTFT ו-p95 ITL תחת העומס הצפוי, והתייחסו ל-mean tokens per second כמדד לקיבולת ולא כתיאור של חוויית המשתמש.
ההגדרה המעשית נגזרת מכך. הגבילו את ה-concurrency מעט מתחת למה שהזיכרון מאפשר, כדי שה-engine לעולם לא יצטרך לבצע preemption. תור קצר וצפוי עדיף על batch עמוק שגורם ל-thrashing, כיוון שמשתמש שממתין ארבע שניות ואז מקבל stream חלק, מרוצה יותר ממשתמש שמתחיל מיידית ונתקע פעמיים.
מה לבדוק כשהביצועים איטיים
כל משתמש מתנהל כרגיל, אך זמן ההמתנה ארוך. מדובר בתור, לא בבעיית מהירות. בדקו תחילה את הגדרת המקביליות. המודל מגיש בקשות בצורה תקינה, אחת בכל פעם.
שגיאת HTTP 503 מ-Ollama. התור מלא. ייתכן שהשרת הגיע לקיבולת המקסימלית שלו, או שהערך OLLAMA_MAX_QUEUE הוגדר נמוך בכוונה כדי להפחית עומס, שזו התנהגות רצויה במקרה כזה.
קצב ה-tokens לשנייה צונח תחת עומס בשרת מבוסס CPU. הריצו את vmstat 1 בזמן שהעומס מתרחש. ערכים שאינם אפס בעמודות si ו-so מעידים על כך שהמכונה מבצעת swapping, כלומר משקולות המודל נקראות מהדיסק עבור כל token. שום שינוי בתצורה לא יפתור זאת. הקטינו את גודל המודל או את מספר ה-slots.
משתמש אחד מתוך עשרה ממתין זמן רב משמעותית מהאחרים. חפשו בלוג של vLLM את preempted. הסיבה השכיחה לכך היא preemption וחישוב מחדש (recompute), מה שמעיד על כך שה-cache עמוס מעבר ליכולתו עבור אורך ה-context שאתם מאפשרים.
זמן ה-TTFT גרוע גם כשהשרת אינו עמוס. מדובר ב-prefill, לא ב-concurrency. פרומפטים ארוכים דורשים זמן עיבוד ממשי לפני הופעת ה-token הראשון; לכן, בדקו את גודל הפרומפט ואת ה-prefix caching לפני שאתם בוחנים את החומרה. אם ההמתנה הארוכה מתרחשת רק עבור המשתמש הראשון לאחר תקופת שקט, וכל המשתמשים שאחריו מקבלים מענה תקין, אין מדובר ב-prefill, אלא ב-Ollama שפורק את המודל מהזיכרון וקורא את המשקולות מהדיסק מחדש. כדאי לשלול אפשרות זו על ידי שמירת המודל בזיכרון בין בקשות.
FAQ
מדוע ה-LLM שאני מארח בעצמי מאט כאשר אדם שני משתמש בו?
לרוב הוא אינו מאט כלל, אלא נכנס לתור. Ollama מופץ עם OLLAMA_NUM_PARALLEL מוגדר ל-1, לכן הבקשה השנייה ממתינה עד שהראשונה פולטת את ה-token האחרון שלה. ניתן להבחין בין שני המקרים על ידי מדידת זמן הזרם של משתמש אחד בזמן שאחר ממתין: אם קצב ה-tokens לשנייה של המשתמש השני תקין ברגע שהוא מתחיל, סימן שיש תור, והעלאת מספר המקביליות תפתור זאת. אם שני הזרמים פועלים בחצי מהירות, אתם חולקים בפועל את רוחב הפס של הזיכרון, וזהו מגבלה חומרתית.
כמה משתמשים בו-זמנית יכול GPU קטן לשרת?
ספרו זיכרון, לא משתמשים. תחילה יש לחשב את המשקולות, ולאחר מכן את ה-KV cache, שעלותו היא 2 כפול מספר השכבות כפול מספר ה-key/value heads כפול ממד ה-head כפול בייטים, לכל token, לכל שיחה פעילה. מודל 8B טיפוסי עם 36 שכבות, 8 key/value heads וממד head של 128 צורך כ-144 KiB לכל token ב-16-bit, כך ששיחה של 8,192 tokens צורכת בערך 1.2 GB. כרטיס של 24 GB שמחזיק את המודל ב-16-bit נותר עם כ-6 GB פנויים עבור ה-cache, מה שמאפשר כחמש שיחות בהקשר (context) מלא, או יותר אם תקצרו את ההקשר.
האם continuous batching הופך את התגובה של כל משתמש לאיטית יותר?
החציון של ה-latency בדרך כלל משתפר, מכיוון שבקשות מפסיקות להמתין לסיום של batch שלם. ה-tail latency מחמיר. כל רצף נוסף מוסיף עבודה לכל שלב פענוח (decoding), ה-prefill של בקשה חדשה גוזל חלק מהשלב ממשתמשים שצורכים זרם, ובקשה שנקטעה צריכה לעבור prefill פעמיים. מדדו את ה-p95 inter-token latency, לא את הממוצע, מכיוון שחלון צ'אט הופך השהיות לניכרות לעין באופן שממוצעים מסתירים.
האם עליי להעלות את OLLAMA_NUM_PARALLEL או לעבור ל-vLLM?
העלו תחילה את מספר המקביליות. זה בחינם, דורש רק קובץ הגדרות אחד, ופותר את המקרה הנפוץ שבו ארבעה אנשים עומדים בתור מאחורי תשובה ארוכה אחת. הזיכרון הוא המגבלה: בקשות מקביליות מכפילות את ההקשר שעליכם להחזיק, לכן עקבו אחר זליגת שכבות ל-CPU. עברו ל-vLLM כאשר יש לכם GPU עם VRAM פנוי ויותר מארבע בקשות שבאמת נמצאות בתהליך, שכן זו הנקודה שבה paged cache ותזמון ברמת ה-token מחזירים יותר ממה שהם עולים.
האם יותר ליבות CPU יתקנו שרת LLM איטי?
לא עבור החלק שמשתמשים שמים לב אליו ביותר. ה-decode קורא את כל המודל מהזיכרון עבור כל token, לכן הוא מוגבל על ידי רוחב הפס של ה-RAM, וליבות נוספות מפסיקות לעזור ברגע שרוחב הפס רווי. ה-prefill אכן משתפר עם יותר ליבות, כך שיותר מהן יקצרו את הזמן עד ל-token הראשון בהנחיות (prompts) ארוכות. ב-VPS עם 4 עד 8 GB, מגבלת המשאבים היא בדרך כלל נפח הזיכרון, והפתרון האפקטיבי הוא מודל קטן יותר או הקשר קצר יותר, ולא יותר vCPUs.