资讯动态

vLLM高吞吐推理:原理、部署与性能调优实践

发布时间:2026/10/17 8:13:30 来源:尧图企业网站定制
vLLM这个名字做本地大模型推理的朋友应该不陌生。我在搭内部AI评测服务那阵子被传统的推理方式折腾得够呛显存动不动被占满请求一多就开始排队后台看着GPU在忙实际吞吐却低得让人着急。后来把推理引擎换成vLLM同样的单卡资源并发处理能力直接上了一个台阶。这篇文章是我从踩坑到调优的完整记录主要讲清楚vLLM的高吞吐原理、本地部署步骤和性能调优经验适合正在搭建本地推理服务、或者想把已有服务并发能力提上去的开发者参考。1. 高吞吐推理服务先搞清楚替换vLLM之前痛在哪1.1 本地推理服务的传统痛点传统的本地大模型推理请求是按序列排队处理的一个请求来了占住整块GPU把文本算完然后释放资源给下一个。这种模式在小流量demo里没问题一旦开始做高并发服务立刻暴露出三个问题。第一个是显存碎片化。每个请求在生成过程中都会产生KV Cache这是Transformer推理时无法避免的中间缓存。不同请求的生成长度不一样叠加起来就会把显存切成大量大小不一的空洞后续请求明明看到显存还有剩余却很难申请到一整块连续空间。第二个是空闲等待。普通批处理会把一批请求整体做前向传播这一批的结束时间由最后一个完成的序列决定GPU利用率就会波峰波谷明显看起来一直在跑实际上大量算力在空闲。第三个是排队模型僵化。一个长回答的请求可能占着显存和GPU好几分钟其他短请求全部排队等这会让高并发场景的尾延迟极不稳定。说白了传统推理框架追求的是“单请求稳定完成”而高并发推理服务追求的是“单位时间处理尽量多请求”。目标变了底层设计也要跟着变。1.2 vLLM把问题拆成了两块vLLM把高吞吐优化重点放在两个层面。第一层是KV Cache管理。请求在生成过程中都会产生一批中间缓存即KV Cache这是推理时无法避免的开销。vLLM引入了PagedAttention让KV Cache按固定大小块存储并像操作系统的虚拟内存一样维护块表碎片问题被极大缓解。第二层是请求调度。它不再等整个序列跑完才决定下一个请求是否进入批处理而是以token生成为单位持续调度完成一个立刻腾出位置插入一个新的这也就是常说的Continuous Batching。两层配合下来同一块显卡能同时容纳的请求数明显增加吞吐自然就上去了。我想用一个直观类比传统批处理像满载公交车必须一站到头vLLM更像地铁加换乘站与站之间不需要整车等待到站客流马上腾出空间。处理并发请求的逻辑完全变了。1.3 什么场景值得上vLLM不要被“高吞吐引擎”这个定位吓到它不只是给大型集群用的。比较适合上vLLM的场景包括本地API服务多个人或多个业务系统同时调用同一个模型接口离线批量评测和数据处理需要持续打满GPU完成条数优先于单条时延交互式Agent框架会频繁发多个短请求希望系统提示词和公共前缀被复用单卡日常实验希望把有限显存用到极致的场景。反过来如果只是偶尔在Jupyter里跑一次推理那vLLM的启动和配置成本反而高用最简单的推理脚本更合适。这也是我经常提醒别人的一句话vLLM解决的是“多人同时用”和“批量持续用”的问题不是让单次推理变快。它优化的目标函数是吞吐量不是时延。2. 核心原理拆解PagedAttention与连续批处理2.1 PagedAttention给KV Cache做虚拟内存换页让我从最底层开始讲它的关键机制PagedAttention。在生成式模型里每处理一个token都需要读取之前所有token产生的KV Cache。Transformer的attention计算要求每个请求的全部历史键值对连续存在内存里所以传统方案里每个请求都得预分配一块连续显存长度难预测——要么给短请求浪费空间要么长请求分配不够。vLLM的做法是不要求用户的KV Cache物理连续而是按固定大小切成块每块单独占一块显存块与块之间用块表串联起来。计算attention时通过块表逐块寻址。这个机制直观上解决了两个问题分配即用按生成的token数逐块扩张不再担心预分配过大或过小碎片收敛块大小固定剩余空间最多浪费一个块内空隙显存利用率明显提升。这个机制很像操作系统的分页进程不需要连续物理内存而是通过页表完成虚拟地址到物理地址的映射。给它起名“PagedAttention”不是概念拼凑而是真的把这一套思想移植到了GPU推理调度上。用起来我发现同等显存下能承载的并发长度总和确实高了很多这也是vLLM高吞吐的第一个底层支撑。2.2 Continuous Batching调度粒度小到每一步传统批处理是把一批请求一起做前向传播但这批里有的序列已经生成了结束符有的还在解码有的刚开始预填充。如果强行等全部结束再换一批GPU总会有明显的空窗口。连续批处理把调度粒度缩小到一次迭代。每一轮前向传播结束后立即检查哪些序列满足完成条件把它们从本批次移除并把这些空位让给等待队列中的新请求。同时在一个大batch里不同序列可以处于不同生成阶段prefill和decode可以混合调度。vLLM会把已经完成的序列直接归还它的KV Cache块然后调度器给新请求分配块并加入。GPU每时每刻都在处理有意义的计算不会因为“等待某一根长尾”而闲置。用之前那个类比继续说传统方案是一趟车拉着一批乘客直达终点站连续批处理则是随上随下的换乘系统每趟车的上座率都被拉满。实践里的表现是我压测同一模型时切到vLLM后GPU利用率从40%多稳定到90%以上吞吐量变化非常直观。这就是vLLM高吞吐的第二个底层支撑。2.3 前缀缓存与投机采样vLLM还能再省一点vLLM另一个经常被忽略但实际很有用的特性是自动前缀缓存。很多业务场景里所有请求的system prompt或者prompt的前半段是完全相同的。vLLM可以把公共前缀的KV Cache计算一次保存下来新请求如果前缀一致直接复用对应的KV Cache而不是重新执行耗时的prefill阶段。这在多轮对话和Agent场景里收益非常明显公共前缀越长收益越大。还有一类辅助优化叫投机采样用一个更小的草稿模型先快速生成多个候选token再交给大模型一次性验证。如果草稿预测准确率足够高相当于一段序列的多次串行decode被压缩成一个并行验证步骤能进一步降低单请求时延。这个功能我一般是在有多余CPU资源或内存时用单卡场景收益需要实测因为草稿模型本身也要占显存配置不好反而拖后腿。到这里vLLM高吞吐的理论基础基本齐了KV Cache高效复用调度无空档前缀不再重复计算。后面要说的部署和调优都是围绕这三件事展开的。3. 本地部署实操从零搭建一个高并发推理服务3.1 环境准备与安装我用的环境是一台24GB显存的GPU机器Linux系统Python 3.10CUDA驱动12.1PyTorch版本按安装文档匹配。装vLLM没有想象中复杂直接pip是最省事的路线。conda create -n vllm python3.10 -y conda activate vllm pip install vllm如果网络下载遇到问题先换镜像源再装避免卡在编译依赖上。20分钟到30分钟左右装完装好后可以验证一下当前核心模块是否可用。python -c import vllm; print(vllm.__version__)装的时候有个坑需要提醒不要在同一环境里装大量杂七杂八的CUDA相关包版本冲突会导致导入时报错。我的做法是单独建一个conda环境只放vLLM和压力测试需要的轻量依赖装完就锁环境。这样后面即使升级其他库也不会把推理环境搞崩。3.2 启动命令与参数逐个拆解vLLM自带常见大模型API协议的HTTP接口启动一个本地高并发推理服务后客户端只需要替换base_url和模型名就能用标准方式调用。我的启动脚本大致是这样python -m vllm.entrypoints.openai.api_server \ --model /home/user/models/llm-base \ --served-model-name local-llm \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --max-num-seqs 32 \ --max-num-batched-tokens 4096 \ --enable-prefix-caching \ --port 8000参数不多但每个都值得说清楚。--model指向模型权重目录目录里的格式要符合当前框架的要求。--served-model-name是暴露给客户端的模型名字可以按内部约定自定义不必等于真实模型名。--gpu-memory-utilization控制vLLM能使用的最大显存比例我习惯留15%左右给后处理和其他系统功能如果纯做服务压测可以调到0.9以上。--max-model-len决定模型最大上下文长度这个值影响KV Cache块的分配预算设得太大能并发的序列数量会自动下降。--max-num-seqs是调度器允许的同批次最大序列数常规业务可以先从16到32起步。--max-num-batched-tokens控制单批次累计token数上限实际是把prefill和decode的计算量限制在一个可控范围内。--enable-prefix-caching开启自动前缀缓存公共前缀场景收益很大。启动完成后日志里会出现类似“Initializing an LLM engine”和“# GPU blocks: xxx”这个GPU blocks数量就是KV Cache的可用块数是后续判断显存是否够用的关键指标。之后访问本地地址加/v1/models如果能看到刚设置的模型名字服务就算起来了。3.3 并发压测脚本与结果解读接口能否并发不能凭感觉。我用线程池写了一个轻量压测脚本重点观察三个指标平均时延、成功率和整体吞吐。import json import time import urllib.request from concurrent.futures import ThreadPoolExecutor API_URL http://127.0.0.1:8000/v1/completions PROMPTS [ 请用一段话解释什么是大语言模型。, 帮我把下面的句子改写成更正式的风格。, 请列举三个学习编程的建议。, ] def run_one(prompt): payload { model: local-llm, prompt: prompt, max_tokens: 128, temperature: 0.7 } req urllib.request.Request( API_URL, datajson.dumps(payload).encode(), headers{Content-Type: application/json} ) t0 time.time() try: with urllib.request.urlopen(req, timeout120) as resp: json.loads(resp.read().decode()) return time.time() - t0, True except Exception as exc: return time.time() - t0, False def bench(concurrency, total50): tasks [] with ThreadPoolExecutor(max_workersconcurrency) as pool: for i in range(total): prompt PROMPTS[i % len(PROMPTS)] tasks.append(pool.submit(run_one, prompt)) results [t.result() for t in tasks] done [r for r in results if r[1]] success len(done) avg_latency sum(r[0] for r in done) / max(success, 1) print(f并发{concurrency} 成功率{success}/{total} 平均耗时{avg_latency:.2f}s) for c in [1, 4, 8, 16, 32]: bench(c)我拿一个中等规模开源模型跑过一轮单请求平均约2.2秒并发8时平均耗时约2.8秒并发32时平均耗时约4.5秒。表面上看单请求变慢了但按总吞吐算从单位时间只能完成约10个请求到并发32时能完成约50个请求吞吐提升非常显著。高并发服务的指标从来不是单请求耗时而是整体吞吐和成功率这个观念要转过来。4. 性能调优实战让吞吐量再上一个台阶4.1 显存预算先算清调优的第一步是看清显存账本。推理时模型权重和KV Cache都占显存前者基本上是固定的。以典型的fp16权重为例7B模型权重约14GB13B模型约26GB。如果还有KV Cache预算那两者不能同时超出总显存。我一般先跑一下服务看日志中的GPU blocks然后用下面公式粗估剩余可用空间。可用KV显存 总显存 × gpu_memory_utilization - 模型权重占用量 - 运行时预留比如24GB显卡设0.85模型占14GB剩余约6.4GB左右。块大小和每个token的KV大小决定这个预算能支撑多少个序列这个测算很有用否则一股脑调高并发数服务启动时会直接OOM或者自动降并发。测算的逻辑也简单KV Cache大小和max-model-len、每序列实际生成长度强相关。我在实验时会把max-model-len先按业务最大上下文调然后根据显存余量反推并发上限不要逆向操作。4.2 批处理参数作用的细节vLLM调度器有几个参数是互相制约的最有感受的是max-num-batched-tokens和max-num-seqs这一对。max-num-seqs决定同一个batch最多能放几个序列限制的是序列数量。max-num-batched-tokens决定这批序列总共允许预置多少token限制的是计算规模。如果max-num-seqs设得大但max-num-batched-tokens设得小那很多序列会被挤在后面等待反过来batching token设得过大prefill一步占用的显存和计算量会很高带来一次尖峰。实际经验是同步调大它们做好prefill阶段上限控制再结合显存余量逐步加而不是一次拉到最大。还有一种常见冲突业务里输入长度差异非常大有的prompt只有几十token有的上千。此时最大显存预算被长prompt请求吃得很严重短prompt也会跟着排队。我的解法是把服务拆两个端口一个给短输入一个给长输入各自设置不同的max-model-len。成本几乎为零排队体验好了很多。4.3 单卡与多卡配置速查并不是调参越多越好先看资源。单卡消费级显卡优先做量化4bit量化可以在显著降低权重显存的同时保留大部分推理质量。vLLM对多种量化格式支持都还可以具体格式以官方文档为准。如果有多卡可以开启张量并行把模型权重和计算切到多卡但小模型切多卡收益通常不明显还会增加通信开销我一般只有较大模型才开。下面是常用配置速查供参考资源情况模型规模参考推荐启动关键参数单卡16GB小规模权重量化加载, gpu-memory-utilization 0.9, max-num-seqs 16单卡24GB中等规模权重fp16, gpu-memory-utilization 0.85, max-num-seqs 32单卡40GB以上中大规模权重fp16, gpu-memory-utilization 0.9, max-num-seqs 64双卡均摊更大规模权重张量并行size 2, 注意跨卡通信带宽这个表不是铁律它更像一个起点。我在不同显卡上试过之后发现真正影响服务稳定性的是“峰值显存”而不是“平均显存”所以压测阶段一定要留观察窗口注意GPU显存曲线是否在某个并发下突然跳高。5. 常见问题与排查技巧实录5.1 CUDA Out of Memory先看日志再决定减参最典型的启动报错是CUDA OOM。很多人的第一反应是调低gpu-memory-utilization但有时候问题不在总显存而是某个prefill批次过大。我的排查顺序是一看启动日志中GPU blocks数量如果很少说明KV Cache没有足够空间二看完整报错栈明确哪个tensor分配失败是在权重加载阶段还是在第一个请求进入时三在OOM点位明显指向上大batch时先调低max-num-seqs再考虑调低max-model-len四如果OOM发生在启动后立即执行大prompt请求把max-num-batched-tokens降到比如2048基本能缓过来。另外提醒一个经验不要开着多个实验同时占住同一张卡这最浪费时间。显存被其他任务吃掉的场景vLLM再优化也白搭。5.2 服务起来了但请求总是排队超时API服务启动没问题客户端频繁超时这也是高并发服务里的高频问题。原因无外乎三种每个请求的prompt太长prefill阶段就要好几秒客户端默认几秒的timeout自然不够QPS超过服务实际能承载的上限请求在等待队列里堆积请求体携带大量不需要的上下文服务端还没开始处理就已超时。改法上长prompt的客户端timeout拉到60秒以上是正常操作。服务端可以看一下日志里的耗时分布。如果确实是批量prompt太多导致排队优先做限流而不是无限扩并发。我自己的经验是把API服务前面加一个轻量队列既能让负载平滑也能在业务侧直接看到积压深度。否则客户端全糊在服务端门口一旦超过并发上限表现就是大面积超时。5.3 吞吐不达标GPU利用率却上不去这是最多人困惑的情况显存还够服务也正常看着GPU利用率却很低。我对这个问题的定位分三步。先看是不是并发序列不够多如果当前同时处理的请求数少自然达不到满载。压测时要用持续请求不要发两个请求就去看指标。再看是不是decode阶段到了内存带宽瓶颈decode阶段每个序列每次只生成一个token对显存带宽敏感算力再高也帮不上太多此时提高吞吐的方法是增加并发序列数。最后看KV Cache是否被大量浪费比如请求的公共前缀没有开启缓存或者每条请求长度差异巨大产生额外分配。打开--enable-prefix-caching观测单请求首个token耗时是否有下降。很多调优问题是层叠的我通常不一次改多个参数而是每次只调一个变量记录数据后对比。那种同时改完所有参数、成绩不好又不知道原因的情况我吃过几次亏也建议你避开。至于量化质量影响我在一些任务上对比过质量下降在大部分文档生成、问答场景完全可接受但涉及代码生成和数学推理场景时下降会更明显需要具体评测后再上线。我个人在多次实际项目里的体会是vLLM这类高吞吐引擎真正改变的不是单次推理速度而是你思考服务架构的方式显存不再按“最大可能长度”给每个请求划死而是按块动态分配调度器不再等整条请求跑完才换人而是每一步都在滚动。刚开始切换时确实会不习惯那些参数之间的相互制约但只要你按“先测显存预算再调并发观测峰值一次只动一个参数”这个顺序走大多数情况下都能把本地推理服务的吞吐拉到一个很满意的水平。最后再分享一个小技巧每次压测后把启动参数和结果记录到一份文档里下次换模型或换显卡时这份记录就是你最快的调参起点。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑