vLLM Intro Part 1 — 全景架構 — 從一次 model.generate() 到一個推論引擎

大多數人部署 LLM 的方式,是 pipeline("text-generation") 加一台 A100,跑得動就上線,跑不動就換 H100。 真正的答案是:LLM 推論的瓶頸幾乎從來不是算力,而是記憶體頻寬與記憶體管理;一張卡能同時服務 3 個人還是 300 個人,差別不在 GPU 型號,在你有沒有一個引擎。 generate() 只需要一行。引擎需要一整套子系統。 這個系列拆解的是後者。


前言:這個系列要做什麼

vLLM (UC Berkeley Sky Computing Lab 起家,Apache-2.0,2023-02 開源,現由社群與 PyTorch Foundation 生態共同維護)是目前部署量最大的開源 LLM 推論引擎:GitHub 上約 9.1 萬顆星、2.2 萬 fork,官方定位是「a high-throughput and memory-efficient inference and serving engine for LLMs」。

它值得逐層讀完,理由不是星星數,而是:它把「LLM 推論」這件事從一個模型函式呼叫,重新定義成一個作業系統問題。 分頁、換頁、排程、搶佔、快取、共享——這些名詞你在 vLLM 裡看到的全部是本義,不是比喻。讀它等於讀一份「把 OS 概念套用到 GPU 記憶體」的完整實作。

這個系列分成五篇:

Part主題對應原始碼
Part 1(本篇)全景架構、請求生命週期、記憶體帳本、V0→V1vllm/v1/engine/vllm/config.py
Part 2PagedAttention 與 KV Cache 管理、Prefix Cachingvllm/v1/core/kv_cache_manager.pycsrc/attention/
Part 3連續批次、統一排程器、Chunked Prefill、投機解碼vllm/v1/core/sched/scheduler.py
Part 4分散式推論、量化、torch.compile 與 CUDA Graphvllm/distributed/vllm/compilation/
Part 5生產部署:API 面、Multi-LoRA、可觀測性、P/D 分離vllm/entrypoints/openai/vllm/distributed/kv_transfer/

本篇的目標很單純:讀完之後,你能在腦中畫出 vLLM 的方塊圖,說得出一個 token 是怎麼從 HTTP request 走到 SSE chunk,並且知道你那張 80 GB 的卡上,每一 GB 分別被誰吃掉。

本文以 vLLM main 分支(2026 年 9 月,版本參考 0.19.x)為準。vLLM 迭代極快,細節請以你 pip 裝的版本為準。


一、核心問題:model.generate() 在哪裡壞掉

先講清楚 vLLM 存在的理由。一個「教學版推論服務」長這樣:

HTTP request ──▶ tokenizer ──▶ model.generate(input_ids) ──▶ decode ──▶ response
                                        │
                                        └── 一次一個請求,或湊滿 8 個一起跑

這條路徑在 demo 上可以跑,在真實流量上會在四個地方同時壞掉。要理解為什麼,得先看清楚 LLM 推論其實是兩個性質完全不同的階段

1.1 Prefill 與 Decode:一個算力受限,一個頻寬受限

使用者輸入:「請解釋 PagedAttention」  (假設 8 個 token)

┌──────────────── Prefill 階段(一次) ────────────────┐
│  8 個 token 一起送進模型                              │
│  每層做一次 8×8 的 attention,GEMM 形狀肥             │
│  → GPU 算力吃滿,compute-bound                        │
│  → 產出:8 個 token 的 K/V(存進 KV cache)+ 第 1 個輸出 token │
└──────────────────────────────────────────────────────┘
                          │
                          ▼
┌──────────────── Decode 階段(重複 N 次) ────────────┐
│  每次只送 1 個 token 進去                             │
│  但要把「整個模型的權重」從 HBM 讀進 SM 一遍          │
│  GEMM 退化成 GEMV,算力使用率常低於 5%                │
│  → 記憶體頻寬吃滿,memory-bound                       │
│  → 每次產出 1 個 token,KV cache 長度 +1              │
└──────────────────────────────────────────────────────┘

這個不對稱是整個推論優化領域的起點。Decode 階段每產生一個 token,都要把幾十 GB 的權重從 HBM 搬一次。 一個 70B 的 FP16 模型是 140 GB 權重;H100 的 HBM3 頻寬約 3.35 TB/s,理論上每秒最多搬 24 次權重,也就是單一序列的解碼上限大約每秒 24 個 token——不管你的 GPU 有多少 TFLOPS。

結論很反直覺但很重要:單條序列的解碼速度,幾乎完全由記憶體頻寬決定,加算力沒用。 唯一的解法是攤提:同一次權重搬運,順便算 64 條序列的 GEMV。這就是 batching 的全部意義,也是為什麼推論引擎的核心是排程器而不是 kernel。

1.2 四個壞點

壞點一:靜態批次讓 GPU 大部分時間在等。 generate() 收滿一個 batch 才跑,跑完整批才收下一批。但同一批裡有人要生 20 個 token、有人要生 2000 個,短的早就好了卻不能離開,長的還沒完別人不能進來。實務上 GPU 利用率常低於 30%。

壞點二:KV cache 用「連續配置」造成 60–80% 記憶體浪費。 傳統實作為每個序列預留 max_model_len 長度的連續 KV 空間。一個請求可能只生 100 個 token,卻佔了 8192 個 token 的位置。vLLM 論文量測到既有系統「因碎片化與過度預留,浪費 60%–80% 的記憶體」——而 KV cache 就是你能同時服務多少人的直接上限。

壞點三:共同前綴被重複計算。 同一個 3000 token 的 system prompt,1000 個請求就重算 1000 次 prefill。這部分算力是純粹浪費的。

壞點四:沒有搶佔機制,長請求會餓死短請求。 記憶體滿了就 OOM,沒有降級路徑。

vLLM 的四個核心主張,正好一一對應:

vLLM 主張對應壞點實作位置
Continuous batching — 以 iteration 為單位調度,完成即離開、新請求隨時插入壞點一vllm/v1/core/sched/scheduler.py
PagedAttention — KV cache 分頁存放,非連續配置,浪費壓到 4% 以下壞點二vllm/v1/core/kv_cache_manager.pycsrc/attention/
Automatic Prefix Caching — 相同前綴的 KV block 直接復用壞點三vllm/v1/core/block_pool.py
Preemption + recompute — 記憶體不足時搶佔低優先序列,之後重算壞點四Scheduler._try_schedule()

原始論文的量測結果是:相同延遲下,吞吐量比 HuggingFace Transformers 高最多 24×,比 TGI 高最多 3.5×。這個數字不是靠更快的 kernel,是靠不浪費記憶體——省下來的記憶體換成更大的 batch,更大的 batch 攤提掉權重搬運成本。

「記憶體就是吞吐量」——這句話是整個 vLLM 的設計主軸,後面四篇都在展開它。


二、全景圖:六個子系統

以下是 vLLM 的方塊圖。每個方塊標了對應的原始碼路徑,之後四篇會逐個拆開。

             ┌──────────────────────────────────────────────────┐
             │  Entrypoints                                     │
             │  ┌──────────────┐        ┌────────────────────┐  │
             │  │ LLM (離線)   │        │ vllm serve (線上)  │  │
             │  │ entrypoints/ │        │ FastAPI :8000      │  │
             │  │   llm.py     │        │ entrypoints/openai/│  │
             │  └──────┬───────┘        └─────────┬──────────┘  │
             └─────────┼──────────────────────────┼─────────────┘
                       │                          │ tokenize / 多模態載入
                       └────────────┬─────────────┘
                                    │  ZeroMQ IPC(V1:跨進程)
                                    ▼
        ┌───────────────────────────────────────────────────────────┐
        │  EngineCore 進程          vllm/v1/engine/core.py           │
        │                                                            │
        │   ┌──────────────────┐        ┌─────────────────────────┐ │
        │   │   Scheduler      │◀──────▶│   KVCacheManager        │ │
        │   │  統一 token 預算 │  配額   │  BlockPool / FreeQueue  │ │
        │   │  waiting/running │        │  hash → block 快取表    │ │
        │   │ v1/core/sched/   │        │  v1/core/               │ │
        │   └────────┬─────────┘        └─────────────────────────┘ │
        │            │ SchedulerOutput(這一輪跑哪些 req、各幾個 token)│
        └────────────┼───────────────────────────────────────────────┘
                     ▼
        ┌───────────────────────────────────────────────────────────┐
        │  Executor(mp / ray / uni)      vllm/v1/executor/         │
        │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐      │
        │  │ Worker 0 │ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ ...  │
        │  │  GPU 0   │ │  GPU 1   │ │  GPU 2   │ │  GPU 3   │      │
        │  └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘      │
        └───────┼────────────┼────────────┼────────────┼────────────┘
                │  每個 Worker 內含一個 GPUModelRunner
                ▼
        ┌───────────────────────────────────────────────────────────┐
        │  GPUModelRunner        vllm/v1/worker/gpu_model_runner.py  │
        │  ┌────────────┐ ┌──────────────┐ ┌────────┐ ┌───────────┐ │
        │  │ InputBatch │▶│ nn.Module    │▶│ Sampler│▶│ 輸出 token│ │
        │  │ 組 tensor  │ │ + Attention  │ │ v1/    │ │           │ │
        │  │ block_table│ │   Backend    │ │ sample/│ │           │ │
        │  └────────────┘ └──────┬───────┘ └────────┘ └───────────┘ │
        └────────────────────────┼───────────────────────────────────┘
                                 ▼
        ┌───────────────────────────────────────────────────────────┐
        │  GPU HBM:權重 + KV Cache 分頁池 + activation 暫存區        │
        └───────────────────────────────────────────────────────────┘

六個子系統,一句話各自的職責:

  1. Entrypoints(vllm/entrypoints/ — 兩個入口。LLM 類別給離線批次推論(跑評測、產資料集);vllm serve 起一個 FastAPI 服務,提供 OpenAI 相容 API。它們不做重活:只負責 tokenize、多模態前處理、以及把請求丟給 EngineCore。
  2. EngineCore(vllm/v1/engine/core.py — V1 的核心,跑在獨立進程。裡面只有兩件事:排程與 KV cache 記帳。它是整個系統唯一知道「現在誰在跑、記憶體還剩多少」的地方。
  3. Scheduler(vllm/v1/core/sched/scheduler.py — 每個 iteration 決定「這一輪要跑哪些請求、每個請求跑幾個 token」。V1 的排程器不再區分 prefill batch 與 decode batch,統一成一個 {request_id: num_tokens} 的 token 預算表——這是 V1 最重要的一個改動,Part 3 會整篇講它。
  4. KVCacheManager(vllm/v1/core/ — GPU 記憶體的分頁管理員。維護 block pool、free block 雙向鏈結串列、以及 hash → block_id 的前綴快取表。Part 2 的主角。
  5. Executor / Worker(vllm/v1/executor/vllm/v1/worker/ — 每張 GPU 一個 Worker 進程。單機用 Python multiprocessing,多機用 Ray。Worker 負責初始化分散式通訊群組、載入自己那一份權重分片。
  6. GPUModelRunner(vllm/v1/worker/gpu_model_runner.py — 真正跑 forward 的地方:把 SchedulerOutput 攤平成 tensor(含 block table)、呼叫模型、跑 attention backend、取樣、回傳 token id。CUDA graph 的錄製與重播也在這裡。

2.1 V1 的進程拓撲

V1 最容易被忽略但影響最大的改動,是把 API 伺服器和引擎核心拆成不同進程

            ┌─────────────────┐          ┌─────────────────┐
            │ APIServer 進程  │          │ APIServer 進程  │   ← --api-server-count N
            │ HTTP + tokenize │   ...    │ HTTP + tokenize │
            └────────┬────────┘          └────────┬────────┘
                     └───────────┬────────────────┘
                                 │ ZeroMQ
                     ┌───────────▼────────────┐
                     │  EngineCore 進程        │  ← 每個 DP rank 一個
                     │  Scheduler + KV 記帳    │
                     └───────────┬────────────┘
                                 │ shared memory / IPC
             ┌───────────────────┼───────────────────┐
             ▼                   ▼                   ▼
        ┌─────────┐         ┌─────────┐         ┌─────────┐
        │Worker 0 │         │Worker 1 │   ...   │Worker N │  ← 每張 GPU 一個
        │ GPU 0   │         │ GPU 1   │         │ GPU N   │
        └─────────┘         └─────────┘         └─────────┘

為什麼要拆?因為 Python GIL。V0 時代,HTTP 處理、tokenization、detokenization、排程全部擠在同一個進程裡,當併發拉高,CPU 端的 tokenize 會直接拖慢 GPU 的排程迴圈——GPU 明明還有餘裕,但因為 scheduler 拿不到 GIL 而空轉。V1 把 CPU 密集的部分(HTTP、tokenize、多模態解碼)推到 API server 進程,EngineCore 進程只做排程,兩邊用 ZeroMQ 傳 token id。

這個設計的實務後果是:你的機器需要至少 2 + N 顆實體核心(1 個 API server、1 個 engine core、N 個 GPU worker)。CPU 給太少,GPU 一定跑不滿——這是雲上跑 vLLM 最常見的隱形瓶頸,因為多數人只看 GPU 規格,不看 vCPU 配比。


三、一個請求的生命週期

把上面的方塊圖換成時間軸。假設使用者送出一個 500 token 的 prompt,要求生成 200 個 token。

 t=0     POST /v1/chat/completions
   │
   │  ┌─ API Server 進程 ────────────────────────────────────────┐
   ├─▶│ ① 套用 chat template → 純文字                            │
   │  │ ② tokenizer 編碼 → 500 個 token id                       │
   │  │ ③ 組出 EngineCoreRequest,ZeroMQ 送給 EngineCore          │
   │  └──────────────────────────────────────────────────────────┘
   │
   │  ┌─ EngineCore 進程:Scheduler ─────────────────────────────┐
   ├─▶│ ④ 進 waiting queue                                       │
   │  │ ⑤ 查前綴快取:前 480 token 命中(system prompt 別人用過) │
   │  │    → 直接復用 30 個 block,只需 prefill 剩下 20 個 token  │
   │  │ ⑥ 向 KVCacheManager 要 block:ref_cnt++、從 free queue 摘除│
   │  │ ⑦ 這一輪 token 預算 8192,分配 20 個給它 → 進 running     │
   │  └──────────────────────────────────────────────────────────┘
   │
   │  ┌─ GPUModelRunner ─────────────────────────────────────────┐
   ├─▶│ ⑧ 攤平成 tensor:input_ids[20]、positions[20]、block_table │
   │  │ ⑨ forward:PagedAttention 依 block_table 去分頁池取 K/V   │
   │  │ ⑩ Sampler:temperature / top_p / 結構化約束 → 第 1 個 token│
   │  └──────────────────────────────────────────────────────────┘
   │
 t≈30ms  ← TTFT(Time To First Token):第一個 token 回到使用者
   │
   │  ┌─ 接下來 199 個 iteration,每輪 ────────────────────────────┐
   ├─▶│ 排程器把它和其他 60 個請求放進同一個 batch                 │
   │  │ 每個請求貢獻 1 個 token → batch 形狀 [61, hidden]          │
   │  │ 權重搬一次,61 條序列一起前進 → 這就是攤提                 │
   │  │ 每 16 個 token 用滿一個 block,就向 KVCacheManager 再要一個│
   │  └──────────────────────────────────────────────────────────┘
   │
 t≈30ms + 199×15ms ≈ 3.0s   ← 每個 token 約 15ms = TPOT / ITL
   │
   ├─▶ 產生 EOS 或達到 max_tokens
   │   KVCacheManager:ref_cnt--,block 依 LRU 順序放回 free queue 尾端
   │   (注意:block 內容不清空,之後前綴命中還能救回來)
   ▼
 t≈3.0s  串流結束,回傳 finish_reason

有三個細節值得單獨標出來:

細節一:⑤ 的前綴命中直接吃掉 96% 的 prefill 成本。 這不是選配優化,V1 預設開啟。你的 system prompt 越長、越固定,這個機制省得越多。Part 2 會講它怎麼做到不出錯。

細節二:⑦ 的「分配 20 個 token」是可以被切開的。 如果這個請求需要 prefill 5000 個 token,而本輪預算只剩 2000,排程器會只跑 2000 個,剩下 3000 留到下一輪——這叫 chunked prefill。它的目的是不讓一個長 prompt 的 prefill 把所有人的 decode 卡住。Part 3 主題。

細節三:釋放時 block 不清空。 這是 vLLM 記憶體管理最漂亮的一手:free queue 是 LRU 序,block 只有在真的要被別人拿去用的當下才失效。所以一個剛結束的對話,30 秒後使用者追問,前綴很可能還在。


四、三個演進階段

同一個 vLLM,在不同規模下該長成完全不同的樣子。以下用三個階段說明。

╔═══════════════════════════════════════════════════╗

║ Phase 1:POC —— 單卡、單模型、< 10 QPS ║

╚═══════════════════════════════════════════════════╝

┌──────────────┐      ┌────────────────────────────────┐
│  你的 App    │─────▶│  vllm serve Qwen3-8B            │
│              │ HTTP │  單一進程組,1×A100 80G / L40S  │
└──────────────┘      │  TP=1, gpu-mem-util=0.90        │
                      └────────────────────────────────┘

指令

1vllm serve Qwen/Qwen3-8B \
2  --max-model-len 8192 \
3  --gpu-memory-utilization 0.90

就這樣。這個階段不要調任何參數。 V1 的預設值(前綴快取開、chunked prefill 開、torch.compile 開、CUDA graph piecewise 模式)已經是多數場景的合理解。

  • 可接受的捷徑:沒有備援、沒有 autoscaling、模型直接從 HuggingFace 拉、tokenizer 和引擎同進程。
  • 能撐多久:8B FP16 模型佔 16 GB,80 GB 卡上還有約 55 GB 給 KV cache。以 8192 context 算,大約可以放 400–600 條併發序列——遠超過你 POC 階段的流量
  • 成本:單張 A100 雲上約 $1.5–2.5/hr,月約 $1.1k–1.8k。
  • 解決了什麼:能用、延遲可接受、API 和 OpenAI 相容所以前端不用改。
  • 還沒解決什麼:機器掛了就全掛;模型換版本要停機;沒有任何指標;長 prompt 進來會把短請求的 ITL 拖爛(因為你還沒調 max_num_batched_tokens)。

╔═══════════════════════════════════════════════════╗

║ Phase 2:MVP —— 單機多卡、K8s、10–200 QPS ║

╚═══════════════════════════════════════════════════╝

                    ┌────────────────────────────┐
    ┌──────────┐    │  Ingress / Gateway         │
    │ 客戶端   │───▶│  (限流、認證、路由)       │
    └──────────┘    └──────────┬─────────────────┘
                               │
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
      ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
      │ vLLM Pod #1   │ │ vLLM Pod #2   │ │ vLLM Pod #3   │
      │ 4×H100, TP=4  │ │ 4×H100, TP=4  │ │ 4×H100, TP=4  │
      │ 70B FP8       │ │ 70B FP8       │ │ 70B FP8       │
      └───────┬───────┘ └───────┬───────┘ └───────┬───────┘
              └────────┬────────┴─────────────────┘
                       ▼
          ┌─────────────────────────────┐
          │ Prometheus + Grafana        │
          │ /metrics(TTFT/ITL/KV 使用率│
          │  /前綴命中率/佇列長度)      │
          └─────────────────────────────┘

相對 Phase 1 新增的元件

新增為什麼
K8s Deployment + readiness probe(/health節點掛掉自動重拉;模型載入約 1–3 分鐘,probe 沒設好會被誤殺
多副本 + 負載平衡單點故障消失;但要注意前綴快取是 per-pod 的,隨機路由會浪費命中率
Prometheus 抓 /metrics沒有 TTFT/ITL 的百分位數,你連 SLO 有沒有破都不知道
模型權重放共用儲存(PVC / S3 + 本地快取)每個 pod 各自從 HF 拉 140 GB,啟動要 20 分鐘且會被限流
--tensor-parallel-size 470B FP8 是 70 GB 權重,單卡放不下,或放得下但沒空間給 KV
FP8 量化權重從 140 GB 砍到 70 GB,省下的 70 GB 全部變成 KV cache
--max-num-batched-tokens 明確設定預設值對你的 prompt 長度分布不一定合適

指令

1vllm serve meta-llama/Llama-3.3-70B-Instruct-FP8 \
2  --tensor-parallel-size 4 \
3  --max-model-len 32768 \
4  --max-num-batched-tokens 8192 \
5  --gpu-memory-utilization 0.92 \
6  --enable-prefix-caching \
7  --served-model-name gpt-4o-mini
  • 成本 delta:Phase 1 的 1 張卡變成 3×4=12 張 H100,雲上約 $30–45/hr,月約 $22k–33k。成本跳了 20 倍,這是最痛的一步。
  • 複雜度 delta:從「一個指令」變成「一份 Helm chart + 一組 dashboard + 一份 runbook」。
  • 解決了什麼:高可用、可觀測、能撐住 100+ QPS、模型可以滾動更新。
  • 還沒解決什麼:前綴快取在多副本之間不共享(同一個使用者的第二輪對話可能打到別的 pod,重算整段歷史);prefill 與 decode 還混在同一個 batch,長 prompt 進來時 P99 ITL 仍會抖;擴容以 pod 為單位,粒度是 4 張卡。

╔═══════════════════════════════════════════════════╗

║ Phase 3:Scale —— 多機、P/D 分離、200 QPS – 1M MAU║

╚═══════════════════════════════════════════════════╝

                  ┌──────────────────────────────────────┐
   ┌──────────┐   │  智慧路由器(prefix-aware routing)    │
   │ 客戶端   │──▶│  依 prompt hash 把同前綴導向同一組     │
   └──────────┘   └──────┬────────────────────┬──────────┘
                         │                    │
       ┌─────────────────▼──────┐   ┌─────────▼────────────────┐
       │  Prefill 叢集           │   │  Decode 叢集              │
       │  TP=8,大 batch         │   │  TP=8 + DP,大併發        │
       │  最佳化 TTFT            │   │  最佳化 ITL 與吞吐        │
       │  ┌─────┐ ┌─────┐        │   │  ┌─────┐ ┌─────┐ ┌─────┐ │
       │  │ P1  │ │ P2  │        │   │  │ D1  │ │ D2  │ │ D3  │ │
       │  └──┬──┘ └──┬──┘        │   │  └──▲──┘ └──▲──┘ └──▲──┘ │
       └─────┼───────┼───────────┘   └─────┼───────┼───────┼────┘
             │       │                     │       │       │
             └───────┴──── KV Connector ───┴───────┴───────┘
                       (NIXL / Mooncake,RDMA 直傳 KV)
                                │
                   ┌────────────▼─────────────┐
                   │  多層 KV 儲存             │
                   │  GPU HBM → CPU DRAM → SSD │
                   │  (LMCache / Offloading)  │
                   └──────────────────────────┘

相對 Phase 2 新增的元件

新增為什麼
Prefill / Decode 分離部署prefill 是 compute-bound、decode 是 memory-bound,混在一起兩邊都不最佳;分開後可以各自獨立調 TP 與副本數
KV Connector(NIXL / Mooncake)P 算完的 KV 要送給 D,走 RDMA 而不是重算
前綴感知路由讓同一個 session、同一份文件的請求打到同一組 pod,把跨 pod 的前綴快取失效問題壓下去
KV 多層卸載(LMCache 等)HBM 放不下的熱前綴放 DRAM/SSD,命中 DRAM 仍比重算 prefill 快一個量級
DP + EP(MoE 模型)MoE 的 expert 層適合 EP,attention 層適合 TP/DP,混合策略
投機解碼(EAGLE / MTP)低 QPS 時段用來壓 ITL
  • 成本 delta:卡數再乘 3–5 倍,但單位 token 成本會下降——因為 P/D 分離後兩邊都能跑在自己的最佳工作點。實測上 P/D 分離不會提高總吞吐(官方文件明說「does not improve overall throughput」),它買的是尾延遲的穩定兩個階段可獨立擴容
  • 複雜度 delta:這是質變。你現在要維運一個分散式系統:KV 傳輸失敗要有 fallback、P 和 D 的副本比例要跟著流量形狀調、路由器本身變成新的單點。
  • 解決了什麼:P99 TTFT 與 P99 ITL 可以分別下 SLO 並分別達成;長文件場景(RAG、程式碼庫)的前綴命中率從 30% 拉到 80%+。
  • 還沒解決什麼:跨 region、模型多租戶隔離、以及最難的一題——成本歸因:哪個客戶吃掉了你的 KV cache。

什麼時候該從 Phase 2 跳到 Phase 3? 三個訊號任一出現即可考慮:(a) vllm:prefix_cache_hits / vllm:prefix_cache_queries 長期低於 0.4 而你的 workload 明明有共同前綴;(b) P99 TTFT 與 P50 TTFT 差距超過 5 倍;(c) 你已經在為了壓 ITL 而刻意調低 max_num_batched_tokens,卻因此犧牲了吞吐。


五、記憶體帳本:你那 80 GB 到底被誰吃掉

這是 vLLM 最該先算清楚、卻最少人算的一件事。啟動時 vLLM 會做一次 memory profiling:先載入權重,再跑一次假的 forward 量出峰值 activation,然後把剩下的全部劃給 KV cache。

┌────────────────────────────────────────────────────────────┐
│  GPU 總記憶體 80 GB (H100 / A100 80G)                       │
├────────────────────────────────────────────────────────────┤
│ ① 模型權重           │ 16 GB   │ 8B × FP16 = 2 bytes/param  │
├──────────────────────┼─────────┼────────────────────────────┤
│ ② 非 torch 開銷      │ ~1 GB   │ NCCL buffer、CUDA context  │
├──────────────────────┼─────────┼────────────────────────────┤
│ ③ 峰值 activation    │ ~4 GB   │ 由 profiling 實測,與      │
│                      │         │ max_num_batched_tokens 相關│
├──────────────────────┼─────────┼────────────────────────────┤
│ ④ CUDA Graph 記憶體  │ ~1-3 GB │ V1 比 V0 吃更多,可用      │
│                      │         │ cudagraph_capture_sizes 調 │
├──────────────────────┼─────────┼────────────────────────────┤
│ ⑤ KV Cache 分頁池    │ ~48 GB  │ = 80×0.9 − ① − ② − ③ − ④   │
│    ← 剩下的全部       │         │ 這就是你的併發上限         │
├──────────────────────┼─────────┼────────────────────────────┤
│ ⑥ 保留給系統         │ 8 GB    │ 80 × (1 − 0.90)            │
└──────────────────────┴─────────┴────────────────────────────┘

--gpu-memory-utilization(預設 0.9)控制的是 ①–⑤ 的總和上限,不是只有 KV cache。這是最常見的誤解。把它從 0.90 調到 0.95,你多拿到的 4 GB 全部進 KV cache 池——但同時你也把 OOM 的緩衝從 8 GB 砍到 4 GB。

5.1 每個 token 的 KV cache 要多少記憶體

公式:

每 token 每層 KV 位元組 = 2 (K 和 V) × num_kv_heads × head_dim × dtype_bytes
每 token 總位元組       = 上式 × num_layers

三個實例(FP16 KV cache):

模型層數KV headshead_dim每 token8192 context 一條序列
Llama-3-8B(GQA 8)328128128 KB1.0 GB
Llama-3-70B(GQA 8)808128320 KB2.5 GB
Llama-2-13B(MHA 40)4040128800 KB6.4 GB

注意最後一行:沒有 GQA 的舊模型,KV cache 大到離譜。vLLM 論文提到 LLaMA-13B 單一序列的 KV 最多可佔 1.7 GB,就是這個意思。GQA(Grouped-Query Attention)把 KV head 從 40 砍到 8,等於把 KV cache 直接除以 5——這是近年模型架構對推論成本影響最大的一個改動,比任何 kernel 優化都有效。

5.2 反推你的併發上限

有了上面兩張表,你可以直接算:

可容納的總 token 數 = KV 池大小 / 每 token 位元組

例:8B 模型、80 GB 卡、KV 池 48 GB
  48 GB / 128 KB = 約 393,000 個 token

如果平均每條序列(prompt + 輸出)2000 token:
  393,000 / 2000 ≈ 196 條併發序列

如果平均 32,000 token(長文件 RAG):
  393,000 / 32,000 ≈ 12 條併發序列   ← 差 16 倍

這個算式解釋了 90% 的「為什麼我的 vLLM 這麼慢」。 不是引擎慢,是你的 context 太長,導致併發數掉到兩位數,batch 湊不起來,權重搬運無從攤提。啟動日誌裡的 GPU KV cache size: X tokensMaximum concurrency for N tokens per request: Y x 這兩行,是你最該先看的兩個數字。

如果 Y 小於 10,你有四條路:開 KV cache FP8 量化(記憶體減半)、降 max_model_len、加 TP、或換成 GQA/MLA 架構的模型。


六、V0 → V1:重寫了什麼

vLLM 在 0.8 之後把引擎整個重寫成 V1,並在後續版本移除 V0。理解這個轉折能省下你很多讀舊教學的困惑。

面向V0V1
進程模型API server 與引擎同進程,GIL 爭用API server / EngineCore / Worker 三層分離
排程prefill batch 與 decode batch 分開,兩套邏輯統一 token 預算,一個 {req_id: n_tokens} 字典
Chunked prefill選配,依模型啟發式決定預設開啟,所有場景
Prefix caching選配預設開啟
CUDA Graph全圖捕捉,attention 也在圖裡Piecewise:以 attention 為切點,圖只捕捉中間段
torch.compile選配預設開啟,且啟動前編譯完畢,服務中不會再觸發編譯
KV 換頁到 CPU(swap)移除,統一用 recompute
best_of 取樣移除
每請求 logits processor移除(架構不相容)
logprobs 語意後處理過的原始 logits,可用 --logprobs-mode 切換

四個設計目標寫在官方 V1 指南裡:簡潔的模組結構、接近零的 CPU 開銷、統一的優化整合路徑、以及零設定預設值(功能自動開啟)

最後一點對使用者影響最大:V1 的哲學是「不要調參」。前綴快取、chunked prefill、torch.compile、CUDA graph 全部預設開。你唯一該主動設的通常只有三個:--max-model-len--tensor-parallel-size--gpu-memory-utilization

被移除的功能要特別注意:如果你的舊程式碼用 best_of 或自訂 logits processor,遷移到 V1 會直接壞掉,需要改用 n 參數或結構化輸出來達成類似效果。


七、效能語彙:四個數字與它們的互斥關係

談 vLLM 調參之前,得先把詞彙統一。四個數字:

       請求送出                                        最後一個 token
           │                                                  │
           ├──────── TTFT ────────┤                           │
           │   (Time To First Token)                          │
           │   = 排隊時間 + prefill 時間                       │
           │                       │                          │
           │                       ├──┬──┬──┬──┬──┬──┬──┬──┬──┤
           │                          ↕                       │
           │                        TPOT / ITL                │
           │                   (每個輸出 token 的間隔)          │
           │                                                  │
           ├──────────────── E2E Latency ─────────────────────┤

Throughput  = 整個系統每秒吐出的總 token 數(output tokens/s)
Goodput     = 其中「滿足 SLO」的那部分 —— 真正該被優化的目標

它們互相拉扯,這是推論調參的全部難度所在:

你調的旋鈕TTFTITL吞吐原因
max_num_batched_tokens ↑(如 2048→16384)↓ 變好↑ 變差↑ 變好一輪能塞更多 prefill,但 decode 要等更久的 forward
max_num_seqs↑ 變差↑ 變好batch 更大,攤提更好,但每輪更慢
gpu_memory_utilization↓ 變好↓ 變好↑ 變好KV 池變大 → 搶佔變少(但 OOM 風險上升)
開投機解碼↓ 變好↓ 變差(高負載時)用額外算力換 token 步數
TP ↑↓ 變好↓ 變好次線性提升每卡負擔變小,但多了 all-reduce 通訊
P/D 分離↓ 穩定↓ 穩定— 不變買的是尾延遲穩定,不是總吞吐

一個實務原則:先定義 SLO(例如「P95 TTFT < 500ms 且 P95 ITL < 50ms」),然後最大化在此約束下的吞吐。直接優化 raw throughput 幾乎必然做出一個延遲很爛的系統——因為最大吞吐點通常出現在佇列很長的地方。


八、為什麼選 vLLM 不選 X

推論引擎的選擇不是非黑即白。以下是六個真實的比較。

選擇                選 vLLM 的理由                     不選對方的理由 / 對方何時更好
────────────────────────────────────────────────────────────────────────────────
vLLM               連續批次 + 分頁 KV,高併發吞吐最強    HF Transformers:沒有批次調度,
vs HF Transformers  OpenAI 相容 API 開箱即用             併發下 GPU 利用率 <30%,只適合
                   模型覆蓋面最廣(數百個架構)          單次實驗、教學、或超小模型
                   ─────────────────────────────────────────────────────────────
                   翻轉條件:你只是要跑一次 forward 看看輸出,或要做 training-time
                   的自訂 hook——那 Transformers 更直接,別為此裝一個引擎。

vLLM               純 Python + CUDA kernel,改一行就生效  TensorRT-LLM:同硬體同模型上
vs TensorRT-LLM     模型上線速度以天計                    延遲可再低 10–30%(尤其小 batch),
                   跨廠牌(NVIDIA/AMD/TPU/CPU)           但要 build engine、綁 NVIDIA、
                   社群大、新模型當天就有支援             新模型支援滯後數週到數月
                   ─────────────────────────────────────────────────────────────
                   翻轉條件:模型固定不動、硬體固定是 NVIDIA、且你在為每毫秒延遲付錢
                   (高頻交易式場景)。那 TensorRT-LLM 的編譯期優化值得那份工程成本。

vLLM               通用推論引擎,功能面最完整            SGLang:在「大量共享前綴 +
vs SGLang           生態整合最廣(K8s、Ray、KServe…)     複雜多輪程式化呼叫」場景,
                   Prefix caching 已經補上大部分差距      RadixAttention 的前綴共享更細緻
                   ─────────────────────────────────────────────────────────────
                   翻轉條件:你的 workload 是 agent 式的樹狀分支(同一 prefix 分岔出
                   幾十條路徑),SGLang 的前綴樹結構天生更貼合。兩者近年互相收斂,
                   建議用你的真實 trace 各跑一次 benchmark 再決定。

vLLM               Continuous batching 高併發            TGI:HuggingFace 生態整合更緊,
vs TGI              社群與功能迭代速度更快                Rust 前端 CPU 開銷低。但吞吐在
                   量化與平行策略選項更多                 原論文量測中低 vLLM 最多 3.5×
                   ─────────────────────────────────────────────────────────────
                   翻轉條件:你已經深度綁在 HF Inference Endpoints 上,換掉的遷移
                   成本高於效能收益。

vLLM               GPU 上的吞吐高一個量級以上            llama.cpp / Ollama:單機單人、
vs llama.cpp/Ollama 生產級的 API、指標、多租戶            CPU / Apple Silicon、GGUF 量化到
                   多副本、TP/PP/DP 完整                  4-bit 塞進筆電——這些 vLLM 做不到
                   ─────────────────────────────────────────────────────────────
                   翻轉條件:本機開發、離線、無 GPU、或你要的就是「一個指令跑起來」。
                   別用 vLLM 做本機玩具,也別用 Ollama 扛生產流量。

vLLM               自架、成本可控、資料不出境            商用 API(如 Bedrock 等):
vs 託管 API         模型與參數完全自主                    無需維運、無需買卡、按量計費
                   高用量下單位成本低 5–20 倍             低用量下自架絕對更貴
                   ─────────────────────────────────────────────────────────────
                   翻轉條件:日均 token 量低於約 5,000 萬,或團隊沒有 GPU 維運能力。
                   算一下:一張 H100 月租約 $2.5k,能吐多少 token?除下去和 API
                   單價比。低於損益點就別自架——這是純算術題,不是技術偏好。

九、系統效應:vLLM 相對 naive 推論改了什麼

把本篇提到的設計整理成一張對照表——這也是後面四篇要逐一驗證的清單。

環節naive 做法(model.generate()vLLM 做法效果
批次靜態批次,湊滿才跑、跑完才換連續批次,以 iteration 為單位進出GPU 利用率從 <30% 拉到 >80%
KV 記憶體每序列預留 max_len 連續空間16 token 一頁的分頁池,非連續配置浪費從 60–80% 降到 <4%
共同前綴每次重算hash 鏈比對,命中直接復用 block長 system prompt 場景 TTFT 降 5–20×
長 prompt整段 prefill,塞住所有人Chunked prefill,切成小塊與 decode 混跑P99 ITL 抖動大幅收斂
記憶體不足OOM搶佔 + 之後重算從「掛掉」變成「變慢」
平行取樣每條各存一份 KVCopy-on-Write 共享 block記憶體省最多 55%,吞吐最多 2.2×
多卡手寫 device_mapTP / PP / DP / EP 四種平行度可組合70B 以上模型可服務化
Kernel 啟動開銷每層一次 Python dispatchtorch.compile + piecewise CUDA graph小模型的 CPU 開銷幾乎消失
可觀測性/metrics 完整 Prometheus 指標TTFT/ITL/KV 使用率/命中率可下 SLO
API自己寫 FlaskOpenAI 相容(chat/completions/embeddings/…)前端零改動即可切換

十、系列導航

本篇建立了地圖:六個子系統、一個請求的生命週期、三個部署階段、以及那張決定一切的記憶體帳本。

接下來四篇逐層下鑽:

  • Part 2 — PagedAttention 與 KV Cache:分頁的完整機制(logical/physical block、block table)、kernel 層的記憶體佈局(為什麼是 [num_blocks, num_kv_heads, head_size/x, block_size, x])、Copy-on-Write 共享、自動前綴快取的 hash 鏈與 LRU 淘汰、以及 KV cache 量化。
  • Part 3 — 排程器:連續批次的實作、V1 統一 token 預算排程器、chunked prefill 的 TTFT/ITL 取捨、搶佔與重算、投機解碼的接受率數學,以及一份調參手冊。
  • Part 4 — 分散式與編譯優化:TP/PP/DP/EP 四種平行度的通訊量分析、多機部署與 NCCL 排錯、量化方法與硬體支援矩陣、torch.compile 的 piecewise CUDA graph、attention backend 選擇。
  • Part 5 — 生產部署:OpenAI 相容 API 全表、Multi-LoRA、結構化輸出、指標與症狀診斷鏈、P/D 分離與 KV connector、benchmark 與容量規劃。

vLLM Intro Part 2 — PagedAttention 與 KV Cache — 把作業系統的分頁搬進 GPU


本文基於 vLLM main 分支(2026 年 9 月,版本參考 0.19.x)與官方文件撰寫。所有參數名稱、預設值與架構描述皆對照官方文件與原始碼;記憶體、成本與延遲數字為量級估算,實際值依硬體、模型與工作負載而異。vLLM 迭代極快,細節請以你安裝的版本為準。

Yen

Yen

Yen