arrow_forwardמדריכים / הגשת LLM בייצור
מעודכן לספטמבר 2026 24 דקות קריאה MLOps · vLLM · GPU

הגשת LLM בייצור
vLLM, ניהול זיכרון GPU וזיהוי סחיפה

מודל שעובד יפה במחברת נופל תחת עשרים בקשות במקביל. המדריך הזה עוסק במה שקורה אחרי הפריסה: איך vLLM מנצל את ה-GPU פי כמה מהרצה נאיבית, למה OOM מגיע דווקא בשעת עומס, ואיך מזהים שהמערכת התחילה לקבל קלט שונה ממה שנבדק.

KV
מה שאוכל זיכרון
Continuous
batching
Drift
הכישלון השקט

למה הרצה נאיבית מתמוטטת

הקוד המוכר — model.generate() בלולאה — מבזבז את רוב ה-GPU. שתי סיבות:

vLLM פותר את שניהם: continuous batching מכניס בקשה חדשה ברגע שאחרת סיימה, ו-PagedAttention מקצה KV בעמודים קטנים לפי צורך — בדיוק כמו זיכרון וירטואלי במערכת הפעלה. התוצאה בעומס היא בדרך כלל שיפור של פי כמה ב-throughput על אותה חומרה.

הבנת זיכרון ה-GPU

זיכרון ה-GPU מתחלק לשלושה, ורק אחד מהם משתנה בזמן ריצה:

משקלי המודל   קבוע     8B ב-FP16 ≈ 16GB,  ב-AWQ/INT4 ≈ 5GB
KV cache      משתנה    גדל עם (בקשות במקביל × אורך הקשר)
הפעלות        קטן      חוצץ זמני לחישוב

זיכרון KV לכל טוקן ≈ 2 × שכבות × ראשי_KV × מימד × 2 בתים

Llama 3.1 8B:  ~128KB לכל טוקן
  → 4,096 טוקנים  ≈ 0.5GB לבקשה
  → 32 בקשות במקביל ≈ 16GB רק ל-KV

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

vLLM — הפעלה נכונה

pip install vllm

python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --gpu-memory-utilization 0.90 \   # כמה מה-GPU להקצות
  --max-model-len 8192 \            # תקרת הקשר — משפיע ישירות על KV
  --max-num-seqs 64 \               # תקרת בקשות במקביל
  --quantization awq \              # פחות זיכרון למשקלים = יותר ל-KV
  --port 8000

ארבעת הפרמטרים האלה הם כמעט כל הכוונון. ההיגיון שמאחוריהם:

הממשק תואם OpenAI, כך שהמעבר מקוד קיים הוא שינוי כתובת:

from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="none")

resp = client.chat.completions.create(
    model="meta-llama/Llama-3.1-8B-Instruct",
    messages=[{"role": "user", "content": "סכם את הדוח"}],
    max_tokens=512,
)

מה למדוד בהגשה

ארבעה מדדים, וכל אחד עונה על שאלה אחרת:

TTFT   Time To First Token    מה שהמשתמש חווה כ"מהירות". קריטי בסטרימינג.
TPOT   Time Per Output Token  קצב הזרימה אחרי ההתחלה.
Throughput   טוקנים/שנייה     יעילות כלכלית. זה מה ש-vLLM משפר.
Queue depth  בקשות ממתינות    מדד ההתראה. עלייה מתמדת = חסר קיבולת.

vLLM חושף מטריקות Prometheus מובנות:

curl http://localhost:8000/metrics | grep -E "vllm:(num_requests|gpu_cache)"

# vllm:num_requests_running        בעיבוד כרגע
# vllm:num_requests_waiting        בתור  ← זה מה שמתריעים עליו
# vllm:gpu_cache_usage_perc        תפוסת KV ← מתקרב ל-1.0 = לפני OOM

ההתראה החשובה ביותר היא gpu_cache_usage_perc. כשהיא מתקרבת ל-1.0 באופן מתמשך, vLLM מתחיל להעדיף בקשות ולדחות אחרות — ה-latency מזנק לפני שמשהו קורס. התראה על 0.85 נותנת לך זמן לפעול; התראה על קריסה מגיעה מאוחר מדי.

סחיפה — הכישלון שאף אחד לא מדווח עליו

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

ניטור מעשי מבוסס embeddings: משווים את התפלגות הקלט של היום מול קו בסיס, בלי צורך בתוויות אנושיות.

import numpy as np
from scipy.stats import ks_2samp

class DriftMonitor:
    """משווה התפלגות של קלט נכנס מול קו בסיס מתקופה תקינה."""

    def __init__(self, baseline_embeddings: np.ndarray):
        self.baseline = baseline_embeddings
        self.centroid = baseline_embeddings.mean(axis=0)
        # פיזור סביב המרכז — ההתפלגות שאנחנו משווים אליה
        self.base_dists = np.linalg.norm(baseline_embeddings - self.centroid, axis=1)

    def check(self, recent: np.ndarray, alpha: float = 0.01) -> dict:
        dists = np.linalg.norm(recent - self.centroid, axis=1)
        stat, p = ks_2samp(self.base_dists, dists)   # מבחן קולמוגורוב-סמירנוב

        # שיעור הבקשות שרחוקות מכל מה שראינו בבסיס
        threshold = np.percentile(self.base_dists, 99)
        outliers = float((dists > threshold).mean())

        return {
            "drift_detected": bool(p < alpha),
            "p_value": float(p),
            "outlier_rate": outliers,        # בבסיס ≈ 1% מעצם ההגדרה
            "mean_shift": float(dists.mean() - self.base_dists.mean()),
        }

ולצד הקלט — מדדי פלט זולים לחישוב, שתופסים רגרסיות מהר:

from collections import deque

class OutputMonitor:
    def __init__(self, window: int = 1000):
        self.lengths  = deque(maxlen=window)
        self.refusals = deque(maxlen=window)
        self.latency  = deque(maxlen=window)

    def record(self, text: str, ms: float):
        self.lengths.append(len(text))
        self.refusals.append(any(p in text for p in
            ("אני לא יכול", "לא אוכל לסייע", "I cannot", "I'm unable")))
        self.latency.append(ms)

    def snapshot(self) -> dict:
        n = len(self.lengths) or 1
        return {
            "mean_length":  sum(self.lengths) / n,
            "refusal_rate": sum(self.refusals) / n,   # קפיצה = פרומפט הודק מדי
            "p95_latency":  sorted(self.latency)[int(n * 0.95) - 1] if self.latency else 0,
        }

קפיצה ב-refusal_rate היא הסימן המוקדם השימושי ביותר: היא כמעט תמיד אומרת ששינוי פרומפט או guardrail חוסם עכשיו גם בקשות לגיטימיות. זהו בדיוק ה-FRR ממדריך Red Teaming, נמדד בייצור במקום בבדיקה.

פריסה בטוחה

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

async def route(request, canary_pct: int = 5):
    """אחוז קטן לגרסה החדשה; שאר התעבורה יציבה."""
    use_canary = hash(request.session_id) % 100 < canary_pct
    version = "candidate" if use_canary else "stable"

    result = await generate(request, prompt_version=version)
    metrics.record(version, result)          # נמדדים בנפרד
    return result

def promote_or_rollback(m):
    """שער אוטומטי. אין החלטה ידנית תחת לחץ."""
    if m["candidate"]["refusal_rate"] > m["stable"]["refusal_rate"] * 1.5:
        return "rollback"                    # חוסם משתמשים לגיטימיים
    if m["candidate"]["p95_latency"] > m["stable"]["p95_latency"] * 1.3:
        return "rollback"
    if m["candidate"]["samples"] < 500:
        return "wait"                        # עוד אין מובהקות
    return "promote"

הפחתת עלות בלי לפגוע באיכות

להעמקה בצד הכלכלי ראה מדריך עלויות LLM וCaching.

סיכום מעשי

monitoring

המשך ב-MLOps

הגשה היא שכבה אחת. סביבה בוגרת מוסיפה עליה tracing, evals וניהול גרסאות.