尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

vLLM核心机制与实战:从KV Cache到连续批处理,提升大模型推理吞吐 作为一个从算法转过来研究 Infra 的老兵我很清楚算法同学第一次看到 vLLM 是什么感受这不就是个推理框架嘛我模型训练完直接model.generate()不就行了哪里需要专门学但等你真把模型丢上线在压测、并发、显存、延迟这些词面前碰几回壁就知道事情的严重性了。同样是 Qwen 3 8B有人一台 A100 能扛上千并发有人四张卡还在 OOM。差别往往不在模型本身而在你对vLLM这类推理引擎到底理解到哪一层。这个系列我就是写给算法同学的。第一篇不给你堆源码而是用算法人最熟悉的方式——把 vLLM 的关键机制讲透再带着你亲手把服务跑起来、调起来、甚至排查几个真实线上问题。看完这篇你能达到的水平是跟 Infra 同事聊 vLLM 不怵自己能独立完成部署和基本压测遇到问题知道往哪个方向查。1. 为什么算法同学突然要搞懂 vLLM1.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 结构会影响线上推理性能。如果系统提示词写得又长又稳定缓存命中率高服务吞吐会明显提升反之如果系统提示词每次都动态拼不一样的内容那么缓存刷新频繁速度会受影响。这一点很少人提到但实际调优非常有用。另外你在调用模型时经常设置的temperature、top_p、top_k这些采样参数在 vLLM 里也有对应的完善支持采样过程是在 CUDA 上高效实现的。你几乎不用担心这些参数在线上的表现和离线不完全一致vLLM 对常见采样策略的复刻是相当好的。3. 实操从零把一个模型部署成服务3.1 部署前先确认三件事在对着命令行一顿操作之前先检查自己的硬件和系统环境。很多人第一步就卡在这里。第一GPU 型号和显存。vLLM 主要面向 NVIDIA GPU也支持 AMD 和部分国产卡但主流还是 NVIDIA。7B 模型用 int4/int8 量化大概需要 6-8GB 显存FP16 全精度大概需要 15-16GB外加 KV Cache 的余量。如果你手头是 2080 Ti22GB 魔改版或者 409024GB跑 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映射端口。镜像可以按需替换常用的标签有latest、v0.8.x、v0.9.x等不同版本的行为可能会有差异建议固定一个你验证过的版本号而不是永远跟着latest走。注意如果你有多张卡想用张量并行加速更大模型需要把--tensor-parallel-size设为卡数并保证卡间通信正常。但 7B 模型一般不需要单卡就够了多卡反而可能因为通信开销导致延迟不降反升。3.3 关键启动参数逐项拆解刚接触 vLLM 的人看到一大堆参数会头晕其实常用参数就那么几个我来逐个说清楚。--model指定模型名称或本地路径。如果是 Hugging Face 模型 IDvLLM 会尝试从网上下载如果是本地路径直接传目录路径。--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_urlhttp://localhost:8000/v1, api_keyEMPTY, ) response client.chat.completions.create( modelQwen/Qwen2.5-7B-Instruct, messages[ {role: system, content: 你是一个有用的助手。}, {role: user, content: 用一句话解释大模型推理加速。}, ], temperature0.7, max_tokens512, ) 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 大小近似公式是层数 × 注意力头数 × 头维度 × 2K 和 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_ENDPOINThttps://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限制在这个区间附近不让服务被突发流量打爆。脚本不复杂核心就是并发循环调接口记录ttft和tpot。网上有很多开源的小工具可以直接用我个人的体会是压测的价值不在于测出一个“最大吞吐”的数字去吹牛而在于理解自己的服务在什么流量模型下会拐弯。只要知道拐点在哪线上出问题的时候你就比同事更快定位原因。
返回列表