← 返回资讯

vLLM 部署高并发推理:PagedAttention 原理与生产实战

2026-07-08

vLLM 是目前生产环境使用最广泛的开源推理框架,核心优势是 PagedAttention 技术——把 KV Cache 管理方式从”预分配连续显存”改为”分页动态分配”,使同等显存下并发吞吐量提升 2–5 倍。如果你需要部署一个能支撑真实用户访问的推理服务,vLLM 是当前的首选。

PagedAttention:为什么 vLLM 吞吐量更高

传统推理框架为每个请求预分配一块连续显存来存放 KV Cache,问题在于:

  • 请求实际长度不确定,必须按最大长度预留,造成大量内部碎片
  • 不同请求的 KV Cache 无法共享,前缀重复(如 system prompt)的显存被重复占用

PagedAttention 借鉴操作系统的虚拟内存分页思想,将 KV Cache 切成固定大小的 block(通常 16 token/block),按需动态分配,并支持多请求共享相同前缀的 block。结果是:

指标传统框架vLLM (PagedAttention)
KV Cache 利用率60–80%95%+
同等显存并发数基准提升 2–5×
前缀共享支持是(减少重复计算)
显存碎片化较严重极低

这个机制的底层实现,其实跟你电脑里操作系统管内存的思路一模一样,理解了这层就知道为什么它能同时省显存又提吞吐。vLLM 内部维护一张 block table,每个请求的 KV Cache 不再是一块连续显存,而是若干个物理 block 的指针集合,跟虚拟内存的页表是同一个套路。请求生成新 token 时按需申请一个新 block,而不是一开始就按 max-model-len 预留满;请求提前结束(比如遇到 EOS 提前退出),剩下没用到的 block 直接释放回显存池,不会像传统方式那样”预留了就锁死”。

前缀共享这块更值得说一说,因为很多人第一次听到”KV Cache 还能共享”会觉得不可思议——KV Cache 不是跟每个请求的具体输入强相关吗?答案是:只要前缀 token 完全相同(比如你的 system prompt 固定不变,或者 few-shot 示例是模板化的),这部分前缀对应的 attention key/value 数值就是完全一样的,vLLM 用类似 copy-on-write 的方式让多个请求的 block table 指向同一批物理 block,只有当某个请求要在这段前缀之后继续生成分叉内容时,才会触发实际的 block 复制。这也是为什么开了 --enable-prefix-caching 之后,如果你的业务场景是”固定 system prompt + 用户输入频繁变化”(客服机器人、Agent 工具调用这类场景最典型),吞吐量能再涨一截,因为省下来的不只是显存,还有这部分 prefill 阶段的重复计算量。

安装与快速启动

安装(推荐用 pip,需要 CUDA 12.x + Python 3.9+):

pip install vllm

这一步看着简单,但实际踩坑的人不少,提前说清楚能少走弯路。vLLM 对 PyTorch 版本、CUDA 版本、甚至 Python 小版本都有比较严格的对应关系,pip install vllm 拉下来的依赖里通常会带一个特定编译好的 torch 版本,如果你的环境里已经装了别的 torch(比如为了跑训练装的),直接装 vLLM 很可能把已有的 torch 覆盖掉,导致你原来能跑的训练脚本突然报 CUDA 版本不匹配。稳妥的做法是给 vLLM 单独开一个 conda 或 venv 环境,不要跟训练环境混用。另外国内环境装的时候经常因为要从 GitHub 拉编译好的 wheel 而卡住或超时,可以加 -i 换成清华源,或者直接用官方 Docker 镜像 vllm/vllm-openai:latest——生产环境我更建议走 Docker,版本锁定、跟宿主机驱动隔离,出问题回滚也方便,pip install 更适合本地先跑通验证。

启动 OpenAI 兼容 API 服务器(以 Qwen2.5-7B 为例):

python -m vllm.entrypoints.openai.api_server \
  --model Qwen/Qwen2.5-7B-Instruct \
  --served-model-name qwen2.5-7b \
  --port 8000 \
  --tensor-parallel-size 1  # 多卡时改为卡数

启动后即可用标准 OpenAI SDK 访问:

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="token-abc")
resp = client.chat.completions.create(
    model="qwen2.5-7b",
    messages=[{"role": "user", "content": "你好"}],
)
print(resp.choices[0].message.content)

关键部署参数

参数作用建议值
--tensor-parallel-size多卡张量并行,等于使用的 GPU 数单卡填 1,多卡等于卡数
--gpu-memory-utilizationvLLM 使用显存比例0.90(留 10% 给框架开销)
--max-model-len最大上下文长度,影响 KV Cache 显存上限按实际业务需求设,别盲目拉满
--max-num-seqs最大并行请求数(批处理窗口)默认 256,显存不足时降低
--quantization量化方式:awq / gptq / fp8显存紧张时用 awq
--enable-prefix-caching开启前缀 KV Cache 共享有固定 system prompt 时必开

这几个参数里最容易被误解的是 --gpu-memory-utilization--max-num-seqs 的关系,很多人以为调大 max-num-seqs 就能提升并发,结果发现服务直接启动失败或者跑着跑着就 OOM。真实的逻辑是:gpu-memory-utilization 决定了 vLLM 总共能用多少显存来存权重 + KV Cache,max-model-len 决定单条请求最坏情况下 KV Cache 能占多大,两者一乘再除以单个 block 的大小,才算出实际能撑得住的并发上限——max-num-seqs 只是一个”批处理窗口的软上限”,如果显存算下来撑不住这么多并发,vLLM 会自动排队等待,不会真的硬撑到把显存打爆(除非你把 gpu-memory-utilization 设得离谱地高,把系统本身需要的显存也挤占了)。所以调优的正确顺序是:先按业务场景估算 max-model-len 到底需要多长(客服问答可能 4K 够用,长文档摘要可能要 32K),再看显存卡型号定 gpu-memory-utilization,最后 max-num-seqs 基本可以交给 vLLM 自己去动态调度,不用死磕这个数字。

量化部署:显存不够怎么办

如果你的 GPU 显存无法加载 FP16 权重,可以配合量化模型:

# AWQ 量化模型(显存约为 FP16 的 1/2)
python -m vllm.entrypoints.openai.api_server \
  --model Qwen/Qwen2.5-7B-Instruct-AWQ \
  --quantization awq \
  --port 8000

AWQ 量化对精度影响最小,是 vLLM 场景的首推方案。GPTQ 也支持但推理速度略慢。量化选型详见 模型量化 GGUF/AWQ/GPTQ 怎么选

高并发实战:流式、异步、重试退避

生产环境跟你本地跑 demo 最大的区别,是要面对大量并发用户同时在线、网络抖动、下游偶发超时这些真实情况,光跑通一个 chat.completions.create 远远不够。

流式输出是几乎所有面向用户的场景都该开的,不然用户要盯着空白等模型把整段话生成完才能看到第一个字,体验很差:

stream = client.chat.completions.create(
    model="qwen2.5-7b",
    messages=[{"role": "user", "content": "写一段产品介绍"}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)

后端要真正扛住并发,光靠同步客户端一个个发请求效率很低,尤其是你的应用本身也是 Web 服务时,同步阻塞会把整个进程的吞吐拖垮。用异步客户端配合 asyncio.gather 批量发请求,才能把 vLLM 服务端的批处理能力真正吃满:

import asyncio
from openai import AsyncOpenAI

client = AsyncOpenAI(base_url="http://localhost:8000/v1", api_key="token-abc")

async def ask(prompt: str):
    resp = await client.chat.completions.create(
        model="qwen2.5-7b",
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content

async def main():
    prompts = ["解释一下光合作用", "写一首关于秋天的短诗", "帮我列个购物清单"]
    results = await asyncio.gather(*(ask(p) for p in prompts))
    for r in results:
        print(r)

asyncio.run(main())

网络抖动、GPU 瞬时打满导致的偶发超时不可避免,直接扔给用户看到一个报错很粗糙,正确做法是加指数退避重试,并且给重试设上限,避免雪崩式重试把服务打得更死:

import time
import random
from openai import APIConnectionError, APITimeoutError

def ask_with_retry(client, prompt, max_retries=3):
    for attempt in range(max_retries):
        try:
            resp = client.chat.completions.create(
                model="qwen2.5-7b",
                messages=[{"role": "user", "content": prompt}],
                timeout=30,
            )
            return resp.choices[0].message.content
        except (APIConnectionError, APITimeoutError):
            if attempt == max_retries - 1:
                raise
            wait = (2 ** attempt) + random.uniform(0, 1)
            time.sleep(wait)

这里 timeout=30 也是个容易被忽略的细节——默认的客户端超时时间往往偏长(甚至几分钟),一旦服务端排队严重,用户请求会一直卡在那里占着连接不释放,反而加剧拥堵。生产环境应该按你的业务场景(比如客服场景用户可接受等待通常不超过 15-30 秒)明确设置超时,超时了就快速失败走重试或降级,而不是死等。

真实报错排查:从报错文案定位根因

下面这几条是自己部署 vLLM 大概率会遇到的报错,直接给你报错文案和根因,省得你对着日志猜半天。

ValueError: The model's max seq len (32768) is larger than the maximum number of tokens that can be stored in KV cache (16384). Try increasing gpu_memory_utilization or decreasing max_model_len.
这是启动阶段就会挂掉的报错,说明你设置的 --max-model-len 超过了当前显存 + gpu-memory-utilization 配置下 KV Cache 能容纳的 token 总量。两条路:要么把 --gpu-memory-utilization 调高一点(前提是显存还有余量),要么老老实实把 --max-model-len 降下来,别设成模型支持的最大值,按你业务实际用到的长度来。

torch.OutOfMemoryError: CUDA out of memory. Tried to allocate ...
运行时才炸的 OOM,通常发生在并发突然上涨、或者某个请求带了超长上下文的时候。先看是不是 --max-num-seqs 设太高导致理论峰值超了显存,其次检查有没有单条超长请求把 KV Cache 一下吃满,实在扛不住就换量化模型或者加卡。

服务启动后请求一直 Connection refused 或者 requests.exceptions.ConnectionError
不是端口没开,是模型还在加载。7B 模型加载权重 + 编译 CUDA kernel 通常要几十秒到几分钟,--served-model-name 打印出启动日志之前请求过去自然连不上,加一个健康检查轮询等它真正就绪再打流量。

返回 400,提示上下文超长(类似 This model's maximum context length is 8192 tokens
说明 messages 拼起来的 token 数超过了 --max-model-len。要么在应用层做历史消息截断/摘要压缩,要么评估是否真的需要更长上下文,盲目调大 max-model-len 会直接拉高显存开销,不是免费的。

vLLM、TGI、SGLang 怎么选

同类推理框架不止 vLLM 一个,选型前列个表看清楚各自的定位:

框架核心优势适合场景
vLLMPagedAttention 显存利用率高,社区生态最大,OpenAI 兼容做得最完善通用生产部署首选,尤其是需要快速对接现有 OpenAI SDK 代码的场景
TGI(Hugging Face)与 HF 生态(Transformers/模型 Hub)集成最紧密团队已深度绑定 HF 工具链、需要用小众模型架构时
SGLang结构化生成(JSON/正则约束输出)和多轮对话前缀复用做得更细Agent 工具调用、需要严格 JSON 格式输出的场景

没有绝对的”最优”,如果你不确定选什么,vLLM 是风险最低的默认选择——生态最成熟、遇到问题网上能搜到的案例最多,出问题不至于叫天天不应。

自建 vs 调 API:什么时候值得自己搭

自己搭一套 vLLM 服务不是没有成本的:GPU 服务器本身的费用、显卡驱动和依赖版本的维护、模型更新迭代要自己跟进、还有半夜服务挂了要有人起来处理。这些隐性成本经常被”开源免费”四个字掩盖掉。一个简单的判断依据:如果你的请求量还没有稳定到能把一张卡的显存和算力打满(比如日均调用量不到几万次),先用 API 按量付费往往比自建更划算,因为自建的固定成本(服务器 + 运维人力)不会因为你调用量低就变少;等业务量真正稳定增长到自建能摊薄成本的规模,再迁移到自己的 vLLM 集群也不迟。不想一开始就折腾服务器和显卡采购的,可以直接 加入候补,直接调用 API

监控与健康检查

上生产前至少把这两个端点接起来,不然服务出问题只能靠用户投诉才知道。vLLM 自带 /health 端点,负载均衡器和 K8s 的存活探针直接打这个接口就行;/metrics 端点暴露 Prometheus 格式的指标,包括排队请求数、KV Cache 占用率、平均生成速度这些关键数据,接入 Grafana 之后能实时看到显存是不是快撑满了、请求是不是开始排队变多,这些信号往往比等到 OOM 报错才发现问题要提前得多。日常巡检也别忘了 nvidia-smi dmon 盯一眼 GPU 利用率和显存曲线,如果利用率长期上不去但延迟又很高,大概率是批处理没吃满,回头检查 max-num-seqs 和实际并发量是不是没对上。

常见问题

vLLM 和 Ollama 有什么本质区别?
Ollama 主打”一行命令本地跑模型”,适合开发者个人测试,不针对并发优化。vLLM 专注生产级高并发推理,有完善的批处理调度和 KV Cache 管理,两者受众不同。详见 Ollama 本地快速跑模型

vLLM 部署后报 CUDA OOM 怎么处理?
先降低 --gpu-memory-utilization 到 0.85,再降低 --max-model-len,最后考虑换用量化模型或加卡。OOM 通常是 KV Cache 撑爆显存,不是权重问题。

多卡张量并行效果怎么样?
权重显存近似线性叠加,吞吐量通常达到单卡的 1.5–2× 每卡(受卡间通信开销影响)。NVLink 互联的多卡效率远好于 PCIe 互联。

vLLM 支持哪些模型架构?
Llama 系列、Qwen 系列、Mistral、Gemma、Phi、ChatGLM 等主流开源架构均支持,详见 vLLM 官方文档的 Supported Models 列表。


延伸阅读: