news 2026/9/7 4:23:00

vLLM核心机制与实战:从KV Cache到连续批处理,提升大模型推理吞吐

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
vLLM核心机制与实战:从KV Cache到连续批处理,提升大模型推理吞吐

作为一个从算法转过来研究 Infra 的老兵,我很清楚算法同学第一次看到 vLLM 是什么感受:这不就是个推理框架嘛,我模型训练完直接model.generate()不就行了,哪里需要专门学?

但等你真把模型丢上线,在压测、并发、显存、延迟这些词面前碰几回壁,就知道事情的严重性了。同样是 Qwen 3 8B,有人一台 A100 能扛上千并发,有人四张卡还在 OOM。差别往往不在模型本身,而在你对vLLM这类推理引擎到底理解到哪一层。

这个系列,我就是写给算法同学的。第一篇不给你堆源码,而是用算法人最熟悉的方式——把 vLLM 的关键机制讲透,再带着你亲手把服务跑起来、调起来、甚至排查几个真实线上问题。看完这篇,你能达到的水平是:跟 Infra 同事聊 vLLM 不怵,自己能独立完成部署和基本压测,遇到问题知道往哪个方向查。

1. 为什么算法同学突然要搞懂 vLLM

1.1 算法视角和 Infra 视角的认知差

先讲个真实场景。我当年第一次部署模型服务,直接在 PyTorch 里写了个接口,每来一个请求就调一次model.generate()。单测过得很顺,结果并发一上来,显存直接爆掉,多个请求排队排到天荒地老。后来我才意识到,算法同学默认的“一次推理”和 Infra 同学说的“高吞吐服务”,根本是两个物种。

在算法离线实验里,你在意的是这个 batch 跑完要多久,效果指标是多少。但在线服务里,核心指标完全变了:吞吐量(每秒能处理多少 token)、延迟(用户从发请求到收到回复等多久)、并发上限(同一时刻能接住多少请求而不崩)。

vLLM 之所以值得学,是因为它把“跑模型”这件事,从朴素的逐个计算升级成了高效的资源调度。它有一个核心观点,很多人没意识到:生成式大模型的瓶颈不在算力,而在显存和调度。你模型算得快不快,占一部分;但更关键的是,KV Cache 占了多大显存、你的 GPU 有多少时间在“空转”。

1.2 vLLM 到底是什么级别的组件

你可以把 vLLM 理解成一个专门为大模型推理打造的“操作系统”。

它负责管理显存、调度请求、分配计算资源。模型本身还是那个模型,权重还是那套权重,但跑在什么框架上,用户体验和机器利用率是两个世界。用传统方案部署,GPU 利用率可能只有 20%-30%,而 vLLM 可以把它推到 70% 以上,这不是玄学,是靠机制改善实现的。

那么这个“机制”到底是什么?简单说是四个词:KV Cache 管理、PagedAttention、Continuous Batching、Prefix Caching。下面我会逐一拆开讲,先建立认知,再去实操。

提示:这一篇面向的读者是算法出身、对工程部署还不熟的同学。如果你已经是 Infra 老手,可以直接跳到后面看实操和避坑部分。

1.3 学完你能带走哪些东西

读这篇文章不需要你已经很懂 CUDA 或者分布式。你需要的基础,就是用过 PyTorch 或 TensorFlow 训练过模型、知道 Transformer 的大致结构,然后跟着我走一遍。

文末我会给出一套可直接复制的部署流程,包括:环境怎么选、服务怎么起、并发怎么压、出问题怎么排查。你看完照着做,基本能把一个 7B 或 8B 的模型在本地或单机多卡上跑起来,跑完还能跟别人讲清楚里面发生了什么。

2. 四件事看透 vLLM 的核心设计

2.1 为什么 KV Cache 是显存杀手

要理解 vLLM,必须先理解一个痛点:自回归生成里的 KV Cache。

Transformer 生成下一个 token 时,需要计算当前输入和之前所有 token 之间的注意力。如果每次生成都重新算一遍之前的 Key 和 Value,计算量会随着序列长度线性增长,慢到没法用。所以主流做法是把之前算好的 K 和 V 缓存下来,下次直接用,这就是 KV Cache。

问题在哪里?序列长度不确定时,KV Cache 占的显存也无法精确预估。很多推理引擎会提前给每个序列分配一块预设大小的显存,比如按 2048 个 token 来预留。但实际请求可能只用了 200 个 token,剩下 1800 的预留空间就被白白浪费了。在线服务一个 GPU 通常要同时处理几十上百个请求,浪费一点就是致命的,显存很快就爆了。

而 KV Cache 又是必须留的,不能省。序列越长,缓存越多。显存是固定的,缓存占多了,能同时跑的请求就少了。这就是 vLLM 要解决的第一个核心问题:怎么样把 KV Cache 这个“大户”管理得高效又灵活

2.2 PagedAttention:借鉴操作系统的大招

vLLM 最出圈的设计,就是 PagedAttention。思想直白得令人发指:既然 KV Cache 是一块需要动态扩展的内存,为什么不直接模仿操作系统里的虚拟内存分页?

操作系统中,进程看到的内存是连续的,但物理内存其实可以分成一页一页,分散在不同位置,由页表来映射。PagedAttention 做的事情类似,把 KV Cache 分成固定大小的“块”(block),每个块存储若干个 token 的 K 和 V。这些块不要求在显存里物理连续,而是靠一个索引表把它们串起来。

这个设计和算法里常见的分块思想一脉相承。好处显而易见:

第一,几乎消除显存碎片。以前要为每个序列预留一大块连续空间,现在只要按需分配小块,够用就行,不浪费。

第二,共享上下文变成可能。多轮对话或者并行请求里有相同前缀时,这些前缀的 KV Cache 块可以共享,不用每个请求都复制一份,省下的显存又能多接几个请求。

第三,batch 大小不再受单个序列的“最坏情况”限制。以前为了不 OOM,你不敢把并发开高,因为不知道哪条请求会产生超长序列。分页之后,这条请求如果真变长了,再给它分配新块就行,其他请求不受影响。

实际效果非常惊人。同等显存下,vLLM 的吞吐量可以是传统方案的 2 到 4 倍。这不是它把算子优化得多牛,而是把内存管理做得足够好,让 GPU 始终有活干。

2.3 Continuous Batching:消灭 GPU 空转

第二个关键机制是 Continuous Batching,连续批处理。

传统推理是怎么做的?假设你要并发处理 8 条请求,把它们打包成一个 batch,同时开始推理。但每条请求的生成长度是不一样的,有的生成了 10 个 token 就结束了,有的要生成 500 个 token。如果是静态 batch,所有请求必须等最慢的那个完成,整个 batch 才能释放。这就意味着,GPU 一边在给已经结束的请求白白耗时间,一边被慢请求拖住,中间产生大量气泡,利用率自然上不去。

vLLM 的做法是动态的。它把“一个 batch”拆成“每一轮迭代层”,每生成一个 token,就检查哪些序列已经结束,立刻把它们移出 batch,同时把排队的新请求塞进来。你可以把它想象成拼车:不是凑满一车人才发车,而是沿途随下随上,始终保持车里有人,车不停。

这个机制消除了绝大多数气泡,是 vLLM 吞吐量高的另一大原因。而且它和 PagedAttention 是配套的,正是因为 KV Cache 的分配足够轻量,动态换入换出请求才不会有太大的额外开销。

理解了 Continuous Batching,你就知道为什么用 vLLM 时疯狂调大并发往往没有意义,反而可能让延迟恶化。因为 GPU 的显存和算力就是上限,它再能调度,也只是尽可能接近上限,而不是突破上限。

2.4 Prefix Caching 与采样控制:优化空间再上一层

除了两大核心机制,vLLM 还有一个很实用的能力:Prefix Caching(前缀缓存)。

在 RAG 或多轮助手场景里,很多请求会带一段相同的系统提示词或历史上下文。以前,每个新请求都要重新计算这段公共前缀的 KV Cache;开启前缀缓存后,vLLM 会复用之前算好的结果。实现上它同样依赖分页块,公共前缀的块可以直接被多个请求共享。

这对算法同学有一个直接启发:你设计的 prompt 结构会影响线上推理性能。如果系统提示词写得又长又稳定,缓存命中率高,服务吞吐会明显提升;反之,如果系统提示词每次都动态拼不一样的内容,那么缓存刷新频繁,速度会受影响。这一点很少人提到,但实际调优非常有用。

另外,你在调用模型时经常设置的temperaturetop_ptop_k这些采样参数,在 vLLM 里也有对应的完善支持,采样过程是在 CUDA 上高效实现的。你几乎不用担心这些参数在线上的表现和离线不完全一致,vLLM 对常见采样策略的复刻是相当好的。

3. 实操:从零把一个模型部署成服务

3.1 部署前先确认三件事

在对着命令行一顿操作之前,先检查自己的硬件和系统环境。很多人第一步就卡在这里。

第一,GPU 型号和显存。vLLM 主要面向 NVIDIA GPU(也支持 AMD 和部分国产卡,但主流还是 NVIDIA)。7B 模型用 int4/int8 量化,大概需要 6-8GB 显存;FP16 全精度大概需要 15-16GB,外加 KV Cache 的余量。如果你手头是 2080 Ti(22GB 魔改版)或者 4090(24GB),跑 7B 是比较舒服的。我建议保守一点:模型权重大小 * 1.5作为显存下限,KV Cache 需要额外留。

第二,驱动和 CUDA 版本。vLLM 对 CUDA 版本有要求,老掉牙的显卡驱动可能直接装不上新版 vLLM。一个省心的判断方式:先跑一下nvidia-smi,看右上角支持的 CUDA Version,大于等于 12.1 基本没问题。

第三,模型来源。国内访问 Hugging Face 时网络不稳定,建议提前把模型下载到本地,或者配置镜像环境变量,减少加载模型时莫名其妙的问题。实际踩坑环节我会单独说。

3.2 最快的两种启动方式

vLLM 支持 pip 安装和 Docker 两种主流方式。我的建议是:本机调试用 pip,生产环境用 Docker

pip 安装很简单:

pip install vllm

装完验证一下版本:

python -c "import vllm; print(vllm.__version__)"

如果这个命令能正常输出版本号,说明安装成功。接下来用一条命令启动服务,以 Qwen/Qwen2.5-7B-Instruct 为例:

vllm serve Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --gpu-memory-utilization 0.85 \ --port 8000

这条命令做了几件事:加载 Qwen 模型,把模型放在单张 GPU 上(tensor-parallel-size 1),限制最大上下文长度为 8192,设定 vLLM 最多使用 GPU 显存的 85%,在 8000 端口对外提供服务。

Docker 方式对环境和版本隔离更好,生产环境我很推荐。命令长一点,但含义清晰:

docker run --rm --gpus all -p 8000:8000 \ vllm/vllm-openai:latest \ --model Qwen/Qwen2.5-7B-Instruct \ --tensor-parallel-size 1 \ --max-model-len 8192

这里--rm是容器退出后自动删除,--gpus all把宿主机所有 GPU 传给容器,-p 8000:8000映射端口。镜像可以按需替换,常用的标签有latestv0.8.xv0.9.x等,不同版本的行为可能会有差异,建议固定一个你验证过的版本号,而不是永远跟着latest走。

注意:如果你有多张卡,想用张量并行加速更大模型,需要把--tensor-parallel-size设为卡数,并保证卡间通信正常。但 7B 模型一般不需要,单卡就够了,多卡反而可能因为通信开销导致延迟不降反升。

3.3 关键启动参数逐项拆解

刚接触 vLLM 的人看到一大堆参数会头晕,其实常用参数就那么几个,我来逐个说清楚。

--model指定模型名称或本地路径。如果是 Hugging Face 模型 ID,vLLM 会尝试从网上下载;如果是本地路径,直接传目录路径。

--tensor-parallel-size是张量并行数,可以理解为“把模型切成几份、放在几张卡上”。只有在单卡显存放不下整个模型时才需要大于 1。算法同学容易犯的错是把 batch size 和它混为一谈,这完全是两回事。张量并行影响的是“模型本身怎么分配”,batch size 影响的是“同时处理多少条请求”。

--max-model-len是模型能支持的最大序列长度(输入 + 输出)。这个值一旦设顶,超过长度的请求会直接报错。设得越大,KV Cache 预留空间上限也越大,显存压力越大。所以不要一味贪大,按业务实际需要设置即可。比如你的业务平均输入 2000 token、输出 500 token,设成 4096 就差不多了。

--gpu-memory-utilization控制 vLLM 最多使用多少比例的显存。默认值是 0.9,意思是把 90% 显存拿来做 KV Cache 和计算,留 10% 给余量。如果你遇到 OOM,可以尝试调低到 0.8 或 0.7;如果显存充裕,想尽量多缓存些 KV Cache 提升吞吐,可以调到 0.95。

还有两个重要参数。--enforce-eager表示不使用图模式编译,直接以 eager 模式执行。这样可以大幅降低启动时的编译时间和显存开销,但推理速度会略慢。调试阶段强烈建议加上它,省得启动时等半天,确认流程通了再去掉。

--max-num-seqs限制同时最多处理多少条序列,默认 256。如果并发请求太多,多余的会排队。如果发现延迟很高,可以调低这个值,保证每条请求能更快拿到算力。

3.4 用 OpenAI 兼容接口把服务用起来

服务起来之后,怎么调用?vLLM 提供的是 OpenAI 兼容的接口,路径是/v1/chat/completions,这有个巨大的好处:你线上代码可以直接复用 OpenAI SDK,只改base_url就行。

Python 调用示例:

from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="EMPTY", ) response = client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[ {"role": "system", "content": "你是一个有用的助手。"}, {"role": "user", "content": "用一句话解释大模型推理加速。"}, ], temperature=0.7, max_tokens=512, ) print(response.choices[0].message.content)

这里的api_key随便填一个字符串即可,vLLM 默认不校验。响应格式也完全是 OpenAI 那套,choices[0].message.content就是模型输出。

你也可以直接用 curl 快速验证:

curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "Qwen/Qwen2.5-7B-Instruct", "messages": [{"role": "user", "content": "你好"}], "max_tokens": 128 }'

看到正常的 JSON 返回,服务就算真正跑通了。到这一步,你已经完成了“把一个模型变成线上服务”的关键闭环。

4. 参数调优与性能优化:把 GPU 用满

4.1 从指标看懂你的服务压力

服务跑通只是开始。接下来要知道它到底能扛多少并发,瓶颈在哪里。常用的指标有三个:首 token 延迟(TTFT)、token 生成速度(TPOT)、总吞吐量。

首 token 延迟衡量的是用户发出请求后,等多久看到第一个字。这个东西对交互体验影响最大。如果它很高,说明排队严重或 prefill 阶段慢。生成速度 TPOT 衡量的是后续每个 token 的生成时间,它决定了整体“打字速度”。吞吐量则是单位时间能生成的 token 总数,反映系统处理能力。

延迟和吞吐往往是对立的。你把并发调大,吞吐总量会上去,但单个用户感觉变慢了。你要做的是根据业务场景找到平衡点:聊天机器人更在意低延迟,离线批量生成更在意高吞吐。

实际压测我推荐一个技巧:不用一上来就上复杂压测工具,先写个脚本模拟 10 个、30 个、50 个并发请求,记录不同并发下的 TTFT 和 TPOT,画出趋势图。你会发现某个并发点之后,延迟急剧上升,那就是系统的拐点。多拍几次就对手上机器的真实上限心里有数了。

4.2 显存不足(OOM)的排查三板斧

OOM 是线上最常见的故障,而且报错往往很吓人。先深呼吸,按顺序排查。

第一板斧,看是不是gpu-memory-utilization设太高了。默认 0.9,如果你的模型权重比较大,或者同时跑的序列太多,调低到 0.8 试试。这个参数相当于告诉 vLLM:“你最多只能吃这么多显存,别把 GPU 撑爆。”

第二板斧,看max-model-len是不是设大了。有些同学习惯性设成 32768,但实际业务根本用不到那么长,白白给了 KV Cache 巨大的增长空间。压到 8192 甚至 4096,显存压力立刻小一截。

第三板斧,检查是不是每请求的 KV Cache 总大小超过了预期。一个请求消耗的 KV Cache 大小近似公式是:层数 × 注意力头数 × 头维度 × 2(K 和 V)× 序列长度 × 精度字节数。比如 32 层、32 个头、头维度 128、FP16 精度,每 token 的 KV Cache 大约是32 × 32 × 128 × 2 × 2 = 512KB。这个估算很粗糙,但能帮你快速了解一条长请求有多“吃显存”。

如果以上都排查完还是 OOM,最后再考虑量化。vLLM 支持 AWQ、GPTQ、FP8 等多种量化模型格式,把权重从 FP16 变成 4bit 或 8bit,显存占用直接砍半。代价是效果可能有轻微损失,需要你做评估。

4.3 Prefix Caching 如何提升缓存命中率

前面提到过 Prefix Caching,这里展开讲怎么配。vLLM 里开启方式很简单,就是启动参数加上:

--enable-prefix-caching

开启后,相同前缀的请求会复用 KV Cache 块。这个对 RAG 场景效果特别明显,因为用户问题千变万化,但参考资料和系统提示词基本固定。命中之后,相当于跳过了公共前缀的 prefill 计算,输入很长也不觉得慢。

但要留意:缓存命中率取决于前缀的稳定性。如果每个请求带不同的知识库文档,或者系统提示词里拼了时间戳、用户ID这类动态字段,缓存命中率会直线下降,开启後收益有限。所以从工程角度,你应该尽量把固定的内容放在 prompt 的头部,动态内容放在后面。这个技巧对线上性能的影响远超你的想象。

4.4 投机解码与低精度:进阶优化方向

如果模型部署后延迟还是不满意,可以考虑投机解码(Speculative Decoding)。思路是:先用一个更小更快的 draft 模型预测多个候选 token,再用大模型一次性验证。如果 draft 猜得准,大模型一次 forward 就能确认多个 token,生成速度可能提升 2 到 3 倍。

vLLM 对投机解码有官方支持,但使用条件比较苛刻,你要准备一个比目标模型小很多、效果又足够好的 draft 模型,并且两者的词表要兼容。算法同学拿到手可以做一些有意思的实验:尝试不同大小的 draft 模型,测量实际加速比和接收率,这本身就是一个很好的理论学习机会。

低精度方面,FP8 在 vLLM 里支持得已经很成熟了。使用 FP8 量化权重通常在效果损失较小的情况下获得显著显存和速度收益。如果你测试过效果,能接受 0.5% 以内的精度变化,FP8 是部署阶段的默认选择之一。

5. 常见问题与排查技巧实录

5.1 高频问题速查表

我把自己实际踩过的坑和你大概率会遇到的问题整理成一张表,方便你查。

问题现象可能原因解决方向
启动报 CUDA out of memorygpu-memory-utilization过高或max-model-len过大调低两个参数,或换量化模型
模型加载速度极慢或超时Hugging Face 下载不稳定换国内镜像源或提前下载到本地路径
并发一高延迟明显上升超出 GPU 实际处理能力max-num-seqs或扩卡,不要盲目堆并发
请求内容过长直接报错超过max-model-len限制增大该参数,但注意显存代价
输出质量与离线实验不一致采样参数没传对或量化精度损失检查 temperature/top_p,评分量化模型效果
服务起来后访问超时端口映射、容器网络问题检查 Docker-p映射和防火墙

5.2 模型文件下载慢的坑

国内网络环境下载 Hugging Face 模型经常抽风,我最开始以为是参数写错了,反复排查发现就是下载问题。推荐一个非常稳妥的方式:用modelscope或配置镜像环境变量。

使用镜像的方法,在启动前执行:

export HF_ENDPOINT=https://hf-mirror.com

或者使用 modelscope 下载模型到本地:

modelscope download --model Qwen/Qwen2.5-7B-Instruct

下载成功后,把--model参数改成本地路径,比如/root/models/Qwen2.5-7B-Instruct。这个办法有个额外好处:之后的每次启动都很快,不用重新下载。

5.3 版本升级带来的“小惊喜”

vLLM 版本迭代速度非常快,几个月就把大版本号往上抬。每次升级都可能有行为变化:有的版本改默认参数,有的版本改 API 行为,甚至有人反馈过chunk_size相关的 bug,这类问题你搜半天往往发现是版本号导致的行为差异。

我的建议是:线上环境固定一个验证过的版本号,不要轻易随latest。升级前先去官方 release note 看一下有没有 breaking change。如果你在线上跑得好好的,没有明显痛点,不升级其实就是最省事的选择。

5.4 压测前先确认“舒适并发区”

最后再分享一个我压测时的小习惯。上线前我会用一个轻量脚本,模拟不同并发下的表现,先定位出“舒适并发区”,也就是延迟既不失控、吞吐又比较高的范围。这样上线时能把max-num-seqs限制在这个区间附近,不让服务被突发流量打爆。

脚本不复杂,核心就是并发循环调接口,记录ttfttpot。网上有很多开源的小工具可以直接用,我个人的体会是,压测的价值不在于测出一个“最大吞吐”的数字去吹牛,而在于理解自己的服务在什么流量模型下会拐弯。只要知道拐点在哪,线上出问题的时候你就比同事更快定位原因。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 4:21:26

GitHub PR自动合并实现网站开放编辑的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 4:19:53

UVM验证平台核心:Hierarchy树形结构从原理到实战

做UVM验证平台的时候,我们每天都在跟component打交道,但很少有人停下来认真想一个问题:UVM到底是怎么把一个验证平台组织起来的?答案就藏在一个词里——Hierarchy树形结构。从uvm_root开始,向下挂出uvm_test_top&#…

作者头像 李华
网站建设 2026/9/7 4:19:44

Tampermonkey 5.1.0离线安装包下载与浏览器扩展部署全指南

简介:Tampermonkey(篡改猴)5.1.0 离线安装包是一款面向 Chrome 等浏览器的用户脚本管理器,专为需要在无网络环境下部署或备份扩展的使用者准备。它可以帮助用户批量安装来自脚本平台的用户脚本,实现去广告、调整页面布…

作者头像 李华
网站建设 2026/9/7 4:18:49

模型改进不靠玄学:如何科学地添加模块并验证效果

“这个模块加上去真的有用吗?”如果你在研究生阶段碰过深度学习,我相信你一定有过类似的犹豫。可能是导师随手丢来一句“把注意力机制加上去试试”,可能是师兄的代码里多了一个你没见过的网络分支,也可能是你自己读完某篇论文后&a…

作者头像 李华