用 vLLM 部署一個模型服務
同時服務很多使用者時,問題變成怎麼用同一張顯示卡處理儘量多的請求。用小 GPT 量出批次處理的效果,再講 vLLM 怎樣管理 KV 快取、怎樣啟動相容 OpenAI 的服務。
- 約 35 分鐘
- 難度:深入
- 實測:2026-09-15 批次實驗在 Apple M4 CPU 上執行;vLLM 用法以其官方文件為準
程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。
上一課用 Ollama 在自己電腦上執行模型,一個人用很方便。如果要把模型做成服務,給公司裡幾百個人、或者給你的產品的使用者用,問題就不一樣了:幾十個請求同時到來,怎麼在一兩張顯示卡上儘快地處理完?
vLLM 是目前最常用的開源大模型服務框架之一。這一課先用實驗看清楚服務端的核心問題,再講 vLLM 怎麼解決它、怎麼用。
vLLM 主要面向帶 NVIDIA 顯示卡的 Linux 伺服器,我的實驗機器(Mac,沒有 NVIDIA 顯示卡)跑不了它,所以這一課的實驗用第 09 模組的小 GPT 來做,vLLM 的用法部分只列出官方文件裡的命令,不貼我沒有親自執行得到的輸出。
一次寫一首,還是一次寫很多首
第 09 模組第 6 課說過,生成的每一步都要把模型的全部參數讀一遍,卻只算出一個字。在大模型上,這一步的時間主要花在從視訊記憶體裡讀參數上,計算單元大部分時間在等資料。
那麼,如果把多個請求放在一起,讀一遍參數,同時為多個請求各算出一個字呢?
for batch in [1, 4, 16, 64]:
for _ in range(N_POEMS // batch):
model.generate(torch.full((batch, 1), NEWLINE), N_TOKENS) # batch 首诗同时写
python batch_throughput.py
一共要写 64 首,每首 60 个字,用 KV 缓存
一次写 1 首:总共 1.84 秒,每秒 2085 个字,每一首从开始到写完 0.03 秒
一次写 4 首:总共 0.93 秒,每秒 4110 个字,每一首从开始到写完 0.06 秒
一次写 16 首:总共 0.48 秒,每秒 8053 个字,每一首从开始到写完 0.12 秒
一次写 64 首:总共 0.44 秒,每秒 8706 个字,每一首从开始到写完 0.44 秒
同樣寫 64 首詩:一首一首寫要 1.84 秒;一次寫 16 首隻要 0.48 秒,每秒寫的字數(吞吐量)提高到了約 4 倍。
但請注意最後一列。一次寫 1 首時,每一首 0.03 秒就寫完了;一次寫 64 首時,每一首都要等 0.44 秒才能拿到結果。批次越大,總的效率越高,但每個請求等待的時間(延遲)也越長。
而且從 16 到 64,吞吐量幾乎不再提高(8053 到 8706):CPU 的計算能力已經用滿了。顯示卡的計算能力強得多,能從批次處理裡得到的好處也大得多。
這就是模型服務要解決的核心問題:在可以接受的延遲之內,把儘可能多的請求放在一起算。
真實服務裡的兩個難題
我們的實驗很理想:64 首詩同時開始,每首都寫 60 個字。真實的請求不是這樣的。
請求長短不一,而且隨時到來。一個請求要寫 20 個字,另一個要寫 2000 個字。如果把它們放在一批,等最長的寫完才處理下一批,短的請求早就寫完了,卻還佔著位置;新來的請求也只能乾等。vLLM 的辦法是連續批處理(continuous batching):每生成一步,就檢查一次,寫完的請求立刻移出批次,新來的請求立刻加進來。批次裡的請求一直在變,顯示卡一直滿負荷工作。
KV 快取很佔地方,而且大小不確定。第 09 模組第 6 課和上一課都算過,KV 快取隨著文字變長而增長,每個請求最終要佔多少事先不知道。傳統的做法是按最大長度給每個請求預留一整塊連續的視訊記憶體,而大部分請求根本用不了那麼多,大量視訊記憶體被浪費了,能同時處理的請求就少了。
PagedAttention
vLLM 的核心發明叫 PagedAttention,出自 2023 年的論文《Efficient Memory Management for Large Language Model Serving with PagedAttention》。它借鑑了作業系統管理記憶體的"分頁"辦法:
传统做法:每个请求预留一整块,按最大长度
请求 A [■■■■□□□□□□□□□□□□] 用了 4 格,空着 12 格
请求 B [■■■■■■□□□□□□□□□□] 用了 6 格,空着 10 格
分页:KV 缓存切成固定大小的小块,用到哪儿分配到哪儿
块池 [A1][B1][A2][B2][B3][C1][空][空] ...
请求 A 的块表:A1 → A2
请求 B 的块表:B1 → B2 → B3
每個請求的 KV 快取被切成固定大小的小塊,不需要連續存放,寫滿一塊再分配下一塊,每個請求只用它實際需要的。這樣幾乎沒有浪費,同樣的視訊記憶體可以同時服務多得多的請求。
多個請求有相同的開頭(比如相同的系統提示詞)時,這些塊還可以共享,不必各存一份。第 06 模組第 4 課講的"快取命中的輸入更便宜",在服務端就是類似的機制。
論文摘要裡報告,在延遲相同的情況下,vLLM 的吞吐量是當時其他系統的 2 到 4 倍,文字越長、模型越大,提升越明顯。
安裝和啟動
以下命令引自 vLLM 的官方文件(截至 2026 年 9 月),需要一臺有 NVIDIA 顯示卡的 Linux 機器。vLLM 也支援 AMD、Intel 的顯示卡以及一些 CPU 和其他硬體,但安裝方式不同,請看官方文件。
uv venv --python 3.12 --seed
source .venv/bin/activate
uv pip install vllm --torch-backend=auto
啟動一個服務,只要一條命令:
vllm serve Qwen/Qwen2.5-0.5B-Instruct
預設在 http://localhost:8000 提供一個和 OpenAI 相容的介面。和上一課的 Ollama 一樣,第一部分的程式碼改三個環境變數就能用:
export LLM_BASE_URL=http://localhost:8000/v1
export LLM_API_KEY=你设置的密钥
export LLM_MODEL=Qwen/Qwen2.5-0.5B-Instruct
服務要給別人用時,一定要加上金鑰檢查。啟動時加 --api-key,或者設定環境變數 VLLM_API_KEY,服務端就會檢查請求頭裡的金鑰。第 06 模組第 6 課講過,一個沒有任何限制的介面放到網上,就是在替別人付錢。
同時掛多個 LoRA
第 2、3 課說過 LoRA 的一個好處:一份基座模型加許多個小的介面卡。vLLM 支援在一個服務裡同時掛多個 LoRA,每個請求指定用哪一個:
vllm serve Qwen/Qwen2.5-0.5B-Instruct \
--enable-lora \
--lora-modules repobot=./.cache/repobot-lora
請求時把模型名寫成 LoRA 的名字 repobot,就會用第 3 課微調的那個介面卡;寫成原模型的名字,就用原模型。多個客戶各有自己的 LoRA,卻只佔一份基座模型的視訊記憶體。
自己部署,還是呼叫 API
學到這裡,你可以自己部署模型了。但要不要這樣做,是另一個問題。
自己部署意味著:買或者租顯示卡,顯示卡閒著的時候也在花錢;要自己處理擴容、監控、故障、升級;能用的模型也受限於你的顯示卡裝得下多大。
呼叫 API 則是按用量付費,沒有閒置成本,能用上最好的模型。第 01 模組第 4 課算過,DeepSeek 這類 API 的價格已經很低。
一般來說,下面幾種情況值得考慮自己部署:資料不能離開自己的伺服器;呼叫量大而穩定,自己部署的總成本更低;需要用自己微調的模型。在做決定之前,按第 06 模組第 4 課的辦法,把兩種方案的成本都實際算一遍。
練習
- 在
batch_throughput.py里加上批次 8 和 32,找到你的電腦上吞吐量不再明顯提高的那個批次大小。 - 修改實驗,讓一批裡的詩長短不一(比如一半寫 20 個字,一半寫 120 個字),並且整批等最長的寫完才結束。和全部寫 60 個字相比,吞吐量下降了多少?這就是連續批處理要解決的問題。
- 如果你能用上一臺有 NVIDIA 顯示卡的機器(比如雲伺服器),按本課的命令啟動 vLLM,用第 06 模組的 RepoBot v4 呼叫它。
自測
1. 為什麼把多個請求放在一起處理,吞吐量會提高?代價是什麼?
生成每一步都要把模型參數讀一遍,大模型上主要時間花在讀參數上。多個請求一起處理,讀一遍參數就能為每個請求各算出一個字,計算單元得到了充分利用。代價是每個請求的延遲變長了:它要和其他請求一起算,而且批次大到一定程度後,計算能力用滿,吞吐量也不再提高。
2. 連續批處理解決了什麼問題?
真實的請求長短不一、隨時到來。如果一批請求要等最長的那個寫完才換下一批,短請求寫完了還佔著位置,新請求只能等待。連續批處理在每一步都把寫完的請求移出、把新請求加入,讓顯示卡一直滿負荷工作。
3. PagedAttention 是怎樣節省視訊記憶體的?
它把每個請求的 KV 快取切成固定大小的小塊,按需分配,不需要按最大長度預留一整塊連續的視訊記憶體,幾乎沒有浪費。相同字首的塊還可以在請求之間共享。省下的視訊記憶體可以用來同時處理更多的請求。
提問與討論
這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。
提問 +3 點,回答別人 +6 點。內容經審核後公開。
正在載入討論…