自回归生成时,每个请求的 KV Cache 会随输出变长。传统做法是按最大长度预留一块连续显存:预留多了是内部碎片,请求走了留下不规则空洞是外部碎片。显存账面上还有,新请求却塞不进去,GPU 算力跟着闲。利用率掉到 20–40% 并不罕见。
vLLM 要解决的就是这两件事:显存放得碎,batch 等得慢。
PagedAttention
思路从操作系统的虚拟内存来:不要整段连续 KV,切成固定大小的 block(常见是 16 或 32 个 token 的 K/V)。物理上这些 block 可以散落在显存各处。每个请求有一张 block table,逻辑位置映射到物理 block id。注意力用自定义 CUDA kernel 按表间接访问,数值上和连续布局等价。
收益是三件:
- 碎片几乎消失,显存利用率可以到 90% 以上
- 相同 prefix、多轮对话可以共享已经算过的 KV block
- 序列变长时按 block 增减,不必整段搬迁
没有这张表,就做不到下面的动态 batch。
Continuous Batching
静态 batch 要等这一批里最慢的那条走完,短请求在 padding 里空转。vLLM 每生成一个 token 就重组一次 batch:结束的请求立刻摘掉、释放 block;有空位就塞进新请求。GPU 不必为对齐陪跑。
吞吐提升和模型、硬件、负载有关,官方给出的量级是数倍到一个数量级以上。延迟分布也会好一些:快请求不再被慢请求拖到同一道终点线。

可以记成:vLLM = PagedAttention 内核 + 每步重组的 Scheduler + OpenAI 兼容的 API Server。
起一个本地服务
模型建议走 ModelScope 或自己已经下好的权重目录,不要把 Hub token 写进命令历史。
uv venv llm
source llm/bin/activate
uv pip install vllm
# 权重放到本地目录后再 serve
vllm serve ~/models/qwen2-1.5b-instruct \
--dtype float16 \
--port 8000 \
--served-model-name qwen2-1.5b-instruct
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen2-1.5b-instruct",
"messages": [{"role": "user", "content": "用一句话介绍你自己"}],
"max_tokens": 128
}'
流式把 stream 设成 true,响应是 text/event-stream。生产上还要看 prefix caching、抢占、多卡并行,那是调度参数,不是再装一套运行时。
文档:vLLM。