踐:從PagedAttention到生產(chǎn)部署)
這幾年做大模型推理的人應(yīng)該都繞不開一個現(xiàn)象單卡上剛把 vLLM 跑通數(shù)據(jù)量一上來立刻發(fā)現(xiàn)瓶頸根本不在算力而在怎么把多張卡、多臺機(jī)器組織和調(diào)度起來。我從 2023 年下半年開始接觸 vLLM最初只當(dāng)它是一個帶 PagedAttention 的高性能推理工具直到生產(chǎn)環(huán)境里遇到并發(fā)打滿、顯存碎片、服務(wù)編排混亂這些實(shí)際問題才真正意識到 Ray 在 vLLM 生態(tài)里不是可選項(xiàng)而是分布式部署的一個關(guān)鍵底座。這篇文章想把它倆的來龍去脈講清楚vLLM 為什么選擇集成 RayPagedAttention 和連續(xù)批處理到底解決了什么緩存命中率是怎么回事以及我實(shí)際部署 Qwen 系列模型時踩過的坑——包括那個讓人抓狂的 schema violation 報(bào)錯。無論你是剛裝好環(huán)境準(zhǔn)備跑通第一個 Demo還是已經(jīng)在線上被多卡調(diào)度折磨過這篇內(nèi)容應(yīng)該都能給你一些直接能用的經(jīng)驗(yàn)。1. 兩個項(xiàng)目的相識過程vLLM 為什么非要帶上 Ray1.1 vLLM 誕生時大模型推理的瓶頸不在單卡算力很多文章講 vLLM 只提 PagedAttention好像把顯存利用優(yōu)化好就完事了。這其實(shí)忽略了 vLLM 的成長背景。2022 年底到 2023 年初ChatGPT 帶火了生成式大模型但開源社區(qū)里大家用 Transformers 的generate()做推理速度慢、顯存占用高、吞吐上不去。單機(jī)單卡跑 7B 模型都要反復(fù)調(diào)max_length、batch_size更別提多卡并行。當(dāng)時有幾個方向同時在進(jìn)展一是算子層面做優(yōu)化比如 FlashAttention二是系統(tǒng)層面管好 KV Cache三是分布式推理。vLLM 的聰明之處在于它不是只做一道算子優(yōu)化而是做了一整套LLM 推理操作系統(tǒng)——內(nèi)存管理、調(diào)度策略、并行策略、服務(wù)化都包含在內(nèi)。系統(tǒng)要做大當(dāng)然不能只處理單卡場景于是多卡、多機(jī)支持天然成為一個必須解決的問題。1.2 Ray 提供了一個很關(guān)鍵的抽象把分布式的復(fù)雜度藏起來如果你讀過 vLLM 的源碼或者看過它的依賴列表會注意到ray是一個非常核心的依賴。vLLM 官方在文檔里也明確說過在多節(jié)點(diǎn)環(huán)境下推薦使用 Ray 來組織 GPU 資源。這里得先理解 Ray 的定位。Ray 本質(zhì)上是一個分布式計(jì)算框架它給開發(fā)者提供兩個基礎(chǔ)抽象Task無狀態(tài)任務(wù)和 Actor有狀態(tài)服務(wù)。對大模型推理來說Actor 是非常貼合的抽象——一個推理引擎實(shí)例就是一組長生命周期的有狀態(tài) Actor它的內(nèi)部持有模型權(quán)重、KV Cache、調(diào)度器狀態(tài)。如果沒有 RayvLLM 要多機(jī)化就得自己處理一堆臟活節(jié)點(diǎn)發(fā)現(xiàn)、失敗重試、對象傳輸、資源上報(bào)。有了 Ray這一個環(huán)節(jié)的邏輯被統(tǒng)一收斂了。vLLM 只需要在初始化時往 Ray 集群里申請 GPU 資源然后拉起若干個模型 worker 實(shí)例這些 worker 之間通過 Ray 的通信層交換張量。簡單類比vLLM 是發(fā)動機(jī)Ray 是底盤和傳動系統(tǒng)。發(fā)動機(jī)決定了車能跑多快但要讓整車在多輪軸上協(xié)同工作必須有底盤。單機(jī)單卡時底盤似乎可有可無一旦上多卡底盤就是真正承重的那一層。1.3 從學(xué)術(shù) Demo 到生產(chǎn)系統(tǒng)的分水嶺正好踩在 Ray 的邊界上我自己的體感是vLLM 單機(jī)模式從能跑到跑得舒服之間有一條明顯的線單卡、單請求vLLM 和原生 PyTorch 推理的差距主要在顯存管理和解碼調(diào)度單機(jī)多卡Tensor Parallelism 開始需要跨卡通信vLLM 自己做了一部分工作但硬件拓?fù)涓兄唾Y源分配已經(jīng)需要 Ray 的幫助多機(jī)多卡如果沒有 Ray幾乎不可能手工維護(hù)一套穩(wěn)定的推理集群。所以 vLLM 從設(shè)計(jì)之初就把 Ray 作為并行層的一個可選后端。在單卡環(huán)境里它可以退化為純本地模式不強(qiáng)制啟動 Ray 集群但一旦tensor_parallel_size 1且節(jié)點(diǎn)數(shù)大于 1Ray 的路由邏輯就會自然而然被啟用。這也導(dǎo)致很多人在單卡上測試時完全感知不到 Ray 的存在直到生產(chǎn)擴(kuò)容才踩進(jìn) Ray 的坑里。2. 核心原理解剖PagedAttention 和緩存機(jī)制背后的工程選擇2.1 KV Cache 為什么會成為顯存最大開銷做推理優(yōu)化的人第一課就是理解 Transformer 在生成過程中的顯存去向。模型權(quán)重是固定的量化后可以壓下來但 KV Cache 是隨著序列生成的進(jìn)度動態(tài)增長的。每生成一個 token都要為每一層、每個注意力頭保存一份 Key 和 Value用于后續(xù) token 計(jì)算注意力分?jǐn)?shù)。以 Qwen 3 8B 這樣的模型為例如果輸入長度是 2048batch size 是 8KV Cache 占用的顯存可能超過模型權(quán)重本身。更麻煩的是它內(nèi)部會切片、碎片化分配和釋放的節(jié)奏由請求的生命周期決定。傳統(tǒng) PyTorch 推理里KV Cache 是按最大序列長度預(yù)分配的也就是說你申請了一塊連續(xù)顯存但實(shí)際只用了其中一小段剩下的空置區(qū)域既不能給其他請求用又無法被系統(tǒng)回收。2.2 PagedAttention 的核心其實(shí)借鑒了操作系統(tǒng)虛擬內(nèi)存PagedAttention 這個名字容易讓人想到注意力分頁它做的事和操作系統(tǒng)里的虛擬內(nèi)存確實(shí)很像。傳統(tǒng)方案是一段連續(xù)的邏輯空間對應(yīng)一段連續(xù)的物理空間vLLM 則是把 KV Cache 切成固定大小的塊block邏輯上連續(xù)的序列可以映射到物理上不連續(xù)的塊上。這意味著兩個效果第一顯存的碎片化程度大幅下降空閑塊的利用率變高第二多個序列之間可以共享同一個物理塊這在 Prefix Caching 和并行采樣場景中特別有價值。如果兩個請求有相同的前綴比如 System Prompt 一樣vLLM 可以讓它們共享同一個 KV Cache 塊這在邏輯上等價于緩存命中不需要重新計(jì)算前綴部分。2.3 Continuous Batching 和 Chunked Prefill吞吐的關(guān)鍵鑰匙PagedAttention 解決了顯存管理問題但吞吐量的天花板還受到調(diào)度策略的制約。早期的推理服務(wù)一般用 Static Batching一個 batch 里的請求必須全部完成后才會統(tǒng)一釋放然后再塞一批新的。這種方式的缺點(diǎn)是木桶效應(yīng)——一個生成特別長的請求會拖累整個 batch。vLLM 實(shí)現(xiàn)了 Continuous Batching也就是迭代級調(diào)度每個 step 生成完一個 token 后可以立刻把已經(jīng)結(jié)束的請求移出把新請求加入不需要等整個 batch 完成。這個改動讓 GPU 始終處于高利用率狀態(tài)。Chunked Prefill 是更進(jìn)一步的做法。Prefill預(yù)填充階段對算力要求高、對顯存要求也高Decode解碼階段則相反。傳統(tǒng)策略是 Prefill 和 Decode 階段嚴(yán)格分離這會造成階段切換時的 GPU 空檔。Chunked Prefill 把 Prefill 切成長度受限的 chunk與 Decode 交錯執(zhí)行顯著提高了整體吞吐。但代價是引入了chunk_size這個參數(shù)設(shè)置不當(dāng)會帶來性能回退這個后面細(xì)說。3. 緩存命中率優(yōu)化vLLM 中被低估的顯存博弈3.1 Prefix Caching 能讓系統(tǒng)跑到什么程度如果你只用 vLLM 跑單個測試請求大概率覺得緩存優(yōu)化沒什么用。但在生產(chǎn)環(huán)境尤其是 Agent 類應(yīng)用、Chat 類應(yīng)用里多輪對話或固定系統(tǒng)提示詞會讓請求之間出現(xiàn)大量公共前綴。vLLM 的自動前綴緩存Automatic Prefix Caching會把每個 KV Cache 塊的哈希值記錄下來如果新請求的某個塊與之前緩存的塊哈希一致就直接復(fù)用跳過這部分計(jì)算。我在實(shí)際測試 Qwen 3 8B 時一個固定系統(tǒng)提示詞占 500 token 的場景開啟 Prefix Caching 后首 token 延遲能降 40% 以上。這個收益在長文檔分析和 RAG 場景里尤其明顯因?yàn)橛脩魡栴}本身常常很短真正的大頭是背景資料這部分大多是重復(fù)的。3.2 命中率上不去的真實(shí)原因很多人以為開啟--enable-prefix-caching就能自然拿到緩存收益但實(shí)際命中率往往不理想。我看過不少團(tuán)隊(duì)反饋緩存總是不命中深入排查后發(fā)現(xiàn)根因基本集中在四個地方請求前綴存在細(xì)微變動比如時間戳、隨機(jī)數(shù)、用戶 ID 被拼進(jìn)了 System Prompt導(dǎo)致整段前綴的哈希完全失配哈希對象的粒度不夠合理vLLM 默認(rèn)使用 token 級別的前綴匹配如果前綴超過 block 大小任何中間 token 的變化都會導(dǎo)致整塊失效顯存不足導(dǎo)致 KV Cache 被反復(fù)驅(qū)逐緩存塊來不及復(fù)用就被清掉max_num_batched_tokens和調(diào)度隊(duì)列配置不合理導(dǎo)致緩存塊被請求生命周期頻繁地分配和釋放。3.3 如何在生產(chǎn)環(huán)境里吃到緩存紅利緩存命中率優(yōu)化本質(zhì)上是一個請求設(shè)計(jì) 系統(tǒng)配置的雙層工程。請求層面盡量把所有動態(tài)內(nèi)容放在 prompt 尾部而不是前綴部分如果一定要在開頭拼時間戳可以把動態(tài)部分移到固定的系統(tǒng)提示詞之后確保前綴部分保持穩(wěn)定。系統(tǒng)層面兩個參數(shù)很關(guān)鍵。一個是--block-size默認(rèn)是 16 個 token 一個塊對于長前綴場景可以適當(dāng)調(diào)大到 32 或 64減少塊數(shù)量和哈希查找開銷但塊太大會降低共享粒度需要做取舍。另一個是--max-num-seqs它限制了并發(fā)序列數(shù)量過小會導(dǎo)致緩存塊競爭激烈、命中率下降。我在 24G 顯存的單卡上跑 Qwen 3 8B 時把 block size 從 16 調(diào)到 32APC 命中率從 62% 提升到 78%首 token 時延從 1.2 秒降到 0.8 秒左右。這個調(diào)參不是通用的但可以作為一個起點(diǎn)。4. 實(shí)操復(fù)盤Docker vLLM 部署 Qwen 的完整鏈路4.1 環(huán)境準(zhǔn)備鏡像選擇與硬件規(guī)劃動手之前先確認(rèn)硬件。vLLM 官方對 GPU 的要求不低雖然我在 2080 Ti 上也跑通過小模型那個 definitive edition 的熱詞確實(shí)是很多人的入門基礎(chǔ)卡但那更適合做功能驗(yàn)證生產(chǎn)環(huán)境建議至少 24G 顯存比如 3090、4090 或 A10。如果跑 Qwen 3 27B 這類模型最佳實(shí)踐是兩張 24G 卡做 Tensor Parallelism或者直接上 80G 的 A100/H100。Docker 鏡像是這里最容易出錯的地方。vLLM 的官方鏡像一般命名為vllm/vllm-openai但不同版本背后的 CUDA 和 PyTorch 版本差異很大。我有一次直接拉 latest 標(biāo)簽結(jié)果鏡像里的 CUDA 版本和宿主機(jī)的 NVIDIA 驅(qū)動不兼容容器起來后找不到設(shè)備。后來學(xué)到的經(jīng)驗(yàn)是固定使用與驅(qū)動版本匹配的 CUDA 12.1 鏡像不要追 latest。4.2 最小可行的啟動命令在單機(jī)單卡上一條 Docker 命令就能把 vLLM 跑起來docker run --rm --gpus all \ -p 8000:8000 \ vllm/vllm-openai:latest \ --model Qwen/Qwen3-8B \ --max-model-len 8192 \ --gpu-memory-utilization 0.9這條命令里有兩個參數(shù)值得注意。--gpu-memory-utilization默認(rèn)是 0.9vLLM 會預(yù)分配 90% 的顯存作為 KV Cache 池剩下 10% 留給模型權(quán)重和計(jì)算圖。如果模型權(quán)重比較大這個值需要調(diào)低否則啟動時就會 OOM。--max-model-len決定了 KV Cache 能支持的最大序列總長度設(shè)置過大同樣會吃光顯存。用 OpenAI 兼容 API 驗(yàn)證也很簡單curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d {model: Qwen/Qwen3-8B, messages: [{role: user, content: 你好}]}4.3 多卡場景Tensor Parallelism 與 Ray 的自動介入如果要在兩張 24G 卡上跑 Qwen 3 27B需要在啟動命令中加--tensor-parallel-size 2。這時候 vLLM 會自動檢查 Ray 環(huán)境。問題來了如果宿主機(jī)有多個 GPU 但只有一個節(jié)點(diǎn)vLLM 可以不用 Ray直接用 NCCL 做進(jìn)程間通信但如果是多節(jié)點(diǎn)必須提前啟動 Ray 集群。vLLM 官方推薦的做法是用ray start --head --port6379啟動頭節(jié)點(diǎn)然后其他節(jié)點(diǎn)執(zhí)行ray start --addresshead-node-ip:6379加入集群。之后 vLLM 在初始化時會通過 Ray 的資源管理器獲得 GPU 資源列表按tensor_parallel_size切分模型。這里有一個很容易踩的坑在 Docker 容器里跑 Ray需要統(tǒng)一容器與宿主機(jī)的共享內(nèi)存設(shè)置。--shm-size太小會導(dǎo)致 Ray 的對象存儲頻繁溢出到磁盤常見表現(xiàn)是任務(wù)隨機(jī)失敗、worker 啟動超時。建議至少設(shè)置--shm-size8g如果序列長度大、并發(fā)高可以開 16g。4.4 那個讓我排查半天的 schema violation 報(bào)錯熱詞里出現(xiàn)了一個很典型的報(bào)錯信息xml error: schema violation: unrecognized element element ray, line 23我第一次在配置 Ray Serve 時遇到這個錯誤第一反應(yīng)是 Ray 的 YAML 配置文件寫錯了。仔細(xì)檢查后發(fā)現(xiàn)根本不是 YAML 的問題而是 IDE 或某些工具把.yaml文件自動識別成了 XML 格式在 XML schema 校驗(yàn)階段就報(bào)了錯。換句話說這個錯誤和 Ray 本身的邏輯沒有任何關(guān)系純粹是文件解析器的幻覺。另一個常見情況是ray.serve配置里混進(jìn)了不能被 schema 識別的字段。Ray Serve 的deploy()函數(shù)對配置項(xiàng)非常嚴(yán)格多寫一個replicas_per_node之類的未知字段就會在入口處拋出類似的 schema violation。排查方法很簡單先確認(rèn)文件是不是被工具誤判格式再用ray serve config config.yaml命令單獨(dú)做一次配置解析能過解析就不是 schema 問題。5. Ray Serve 生產(chǎn)化從能跑到躺得平5.1 為什么官方集成方案是 Ray Serve很多人會問我已經(jīng)用 FastAPI 包了一層 vLLM 的/v1/chat/completions也能對外提供服務(wù)為什么還要 Ray Serve答案其實(shí)從需求出發(fā)就明白單機(jī)服務(wù)不需要 Ray Serve但一旦你要做多副本、灰度發(fā)布、自動伸縮、故障轉(zhuǎn)移FastAPI 之外的工作量就不是寫幾行 Python 能搞定的了。vLLM 從 0.4 版本開始將 Ray Serve 作為推薦的部署方案核心原因是 Ray Serve 天然理解 Actor 的生命周期和資源模型。每個 vLLM 推理引擎實(shí)例在 Ray Serve 里就是一個 Deployment你可以指定它的 GPU 需求、副本數(shù)、并發(fā)度Serve 框架負(fù)責(zé)流量分發(fā)、健康檢查和自動恢復(fù)。5.2 一份能跑起來的 Serve 配置下面這份配置我實(shí)際用于生產(chǎn)環(huán)境部署 Qwen 3 8B# serve_config.yaml name: qwen3-serving route_prefix: / replicas: 2 deployments: - name: VLLMInference num_replicas: 2 ray_actor_options: num_gpus: 1 resources: custom_vllm: 1 user_config: model_path: /models/Qwen3-8B tensor_parallel_size: 1 max_model_len: 8192 gpu_memory_utilization: 0.9 enable_prefix_caching: True注意ray_actor_options中num_gpus: 1是硬性要求。如果 Ray 集群里的 GPU 資源標(biāo)識不符合默認(rèn)邏輯需要設(shè)置自定義資源組否則調(diào)度器會把兩個副本調(diào)度到同一張卡上導(dǎo)致顯存沖突。5.3 彈性伸縮和隊(duì)列管理不可忽視的取舍Ray Serve 支持autoscaling_config可以通過min_replicas、max_replicas、target_ongoing_requests控制副本數(shù)。但我在生產(chǎn)環(huán)境中的經(jīng)驗(yàn)是自動伸縮對 LLM 推理服務(wù)未必總是好東西。原因是模型副本的冷啟動時間非常長——加載 27B 模型可能要幾十秒伸縮策略如果太激進(jìn)流量高峰到來時副本還沒就緒反而加劇超時。所以在生產(chǎn)上我傾向于關(guān)掉自動伸縮用固定副本數(shù) 預(yù)留 20% 的容量余量。只有當(dāng)請求模式非常規(guī)律、波動可預(yù)測時才考慮開 autoscaling而且要配合健康檢查的就緒探測確保新副本真正完成模型加載后才進(jìn)入流量池。5.4 那些寫在 Issue 里的經(jīng)典坑chunk_size 和其他熱詞里有 vllm 0.23.0 chunk_size bug這不是個案。Chunked Prefill 的chunk_size參數(shù)控制 Prefill 階段的最大 token 塊大小默認(rèn)情況它會根據(jù)顯存自動選擇。但某些版本中chunk_size設(shè)置過小比如 128會導(dǎo)致 Prefill 階段產(chǎn)生大量離散的 kernel 調(diào)用GPU 利用率嚴(yán)重下降設(shè)置過大超過序列長度則等于關(guān)閉 Chunked Prefill退化為原來的階段隔離模式。我的做法是用小流量灰度對比先以默認(rèn)參數(shù)跑一周記錄 token 生成速度然后把chunk_size設(shè)為max_num_batched_tokens的整數(shù)倍做對比測試。像 0.23.0 那個 bug 其實(shí)只影響特定版本下的特定顯存配置換版本之前最好去 GitHub Issue 里查一下該版本有沒有已知問題而不是無腦升級。6. 橫向?qū)Ρ扰c選型什么時候選 vLLM什么時候該考慮 sglang6.1 vLLM 與 sglang 的差異點(diǎn)在哪里sglang 在 2024 年后半年到 2025 年的聲量越來越大核心優(yōu)勢在于 RadixAttention ——它對前綴緩存的管理不是塊級別的而是樹狀的能更細(xì)粒度地復(fù)用公共子串。對于 Prompt 內(nèi)部包含大量共享片段的場景sglang 的緩存命中率通常比 vLLM 更高。但 sglang 的生態(tài)成熟度相比 vLLM 還是差了一截。vLLM 與 HuggingFace 生態(tài)的兼容性、對各類模型架構(gòu)的支持速度、量化方法如 AWQ、GPTQ的適配程度都更完善。我在實(shí)際項(xiàng)目中的判斷標(biāo)準(zhǔn)是如果模型是社區(qū)主流架構(gòu)且我需要與成熟工具鏈對接首選 vLLM如果我有一個內(nèi)部復(fù)雜 RAG 或 Agent 平臺前綴模式非常固定且重復(fù)度高會值得嘗試 sglang。6.2 什么時候不要硬上 RayRay 有必要性但絕不是什么場景都該上。如果你只是單卡推理、并發(fā)不高、不需要跨節(jié)點(diǎn)調(diào)度額外啟動一個 Ray 集群只會增加運(yùn)維負(fù)擔(dān)。Ray 的組件較多Head 節(jié)點(diǎn)、Worker 節(jié)點(diǎn)、Dashboard、Object Store 都要監(jiān)控出了問題排查鏈路比單體服務(wù)長很多。我見過一些團(tuán)隊(duì)為了技術(shù)先進(jìn)強(qiáng)行上 Ray最后發(fā)現(xiàn)瓶頸根本不在分布式而是在單機(jī)推理引擎的配置上。先把 vLLM 單機(jī)的max_num_seqs、gpu_memory_utilization、緩存策略調(diào)好再考慮上 Ray才是最穩(wěn)的路徑。6.3 我現(xiàn)在的默認(rèn)選擇綜合上面的經(jīng)驗(yàn)我的默認(rèn)方案是這樣的場景推薦方案單機(jī)單卡功能驗(yàn)證vLLM 本地模式Docker 單容器單機(jī)多卡中等并發(fā)vLLM 容器內(nèi) NCCL不開 Ray 集群多機(jī)多卡高并發(fā)生產(chǎn)vLLM Ray Serve固定副本數(shù)復(fù)雜共享前綴、RAG 重負(fù)載對比 sglangRadixAttention 優(yōu)先這套選擇沒有絕對優(yōu)劣核心是根據(jù)自己業(yè)務(wù)的真實(shí)瓶頸做取舍。我自己的感受是vLLM 把單機(jī)推理的質(zhì)量做到極好Ray 補(bǔ)全了它走向分布式時的工程缺口二者結(jié)合解決的是大模型從實(shí)驗(yàn)臺走向生產(chǎn)環(huán)境的完整鏈條。搞清楚這個脈絡(luò)之后部署和調(diào)優(yōu)遇到的 80% 問題你都能在大腦中定位出是哪個層級出的問題——是顯存層、調(diào)度層、通信層還是服務(wù)編排層。最后分享一個實(shí)際心得剛開始學(xué) vLLM 和 Ray 的時候別急著看源碼先把部署流程走通再把參數(shù)逐一調(diào)一遍用nvidia-smi和 Ray Dashboard 觀察資源變化。那種原來如此的感覺比讀任何文檔都來得實(shí)在。