
vLLM这几年的热度但凡做过大模型推理落地的人都深有体会。所谓高吞吐推理引擎核心目标就是在有限的GPU显存里把本地高并发推理服务的单位时间请求处理量做到最大化。我最初被vLLM吸引就是因为一个很现实的问题同样的模型用普通框架部署时并发一上来就显存爆炸、请求排队换成vLLM之后吞吐直接翻了几倍而GPU利用率终于跑到了该有的水平。这篇文章我打算围绕vLLM在本地高并发推理服务中的落地实践把核心原理、部署方式、参数调优和踩坑记录一次性讲清楚。不管你是在做内部大模型API还是在折腾个人推理服务只要被并发和显存卡过这篇内容应该能直接帮你省掉不少弯路。1. 项目核心vLLM到底解决了什么问题1.1 传统推理框架的瓶颈出在哪先聊聊我最早用原生transformers代码部署推理时的痛苦。模型加载没问题单条请求推理速度也能接受但只要并发请求一多整套服务就像得了肠胃炎——要么显存溢出直接OOM要么请求全部卡在队列里慢慢等。问题本质不在计算而在KV Cache。Transformer模型在生成每个token时都要读取历史token的Key和Value向量这些向量存在显存里就是KV Cache。传统推理框架为了简单通常按请求的最大序列长度一次性把显存预留出来。比如一个模型最大支持4096 token的上下文那每个请求的KV Cache就按4096个token的体量去占显存哪怕这个请求实际只生成了几十个token预留的空间也不会释放。我粗略算过一笔账。一个70亿参数模型假设40层、隐层维度4096FP16精度下单个token的KV Cache大约占 40 × 4096 × 2 × 2 字节也就是600多KB。如果按满上下文4096 token预留单请求就要吃掉256MB显存。32路并发同时进来光KV Cache就是8GB。显存被白白浪费并发能力自然上不去。除此之外传统框架的批处理方式也很粗糙。多个请求凑成一个batch后必须等batch里最慢的那个序列生成完整个batch才能释放中途GPU会出现大量空闲气泡。在长文本生成场景里这种浪费尤其明显。1.2 vLLM的三个关键设计vLLM针对上面两个痛点做了三件核心事PagedAttention、Continuous Batching和显存感知调度。第一PagedAttention把KV Cache分页管理。不再为每个请求预占整块连续显存而是像操作系统管理内存一样按固定大小的块分配用到多少分配多少。显存碎片的利用率大幅提高同样的显存能容纳更多并发请求。第二Continuous Batching实现了迭代级调度。传统方案是请求级调度一个batch落地后直到所有序列结束才换下一批vLLM则在每次decode迭代之后完成的请求立即退出新进来的请求马上补位GPU永远在满负荷工作。第三调度器会做显存感知。每个请求进来时vLLM会估算它需要的KV Cache块数量显存足够就接收不够就挂起等待而不是等到解码中途才发现显存不够导致崩溃。这套设计让服务在高并发下具备可预期的兜底行为。1.3 适合什么场景不适合什么场景直接说结论。vLLM最擅长的是高并发API服务、多轮对话、长上下文推理以及离线批量跑分这类场景特征是并发高、token吞吐量大、对单请求延迟要求不是极端苛刻。但如果你的场景是单请求超低延迟比如语音助手那种要求几十毫秒内响应的实时交互vLLM未必比专门优化过的低延迟方案更合适。另外如果你需要对解码过程做大量自定义算子改动或者要接入某些特殊的采样逻辑vLLM的调度框架反而会带来限制。选型时先看业务形态别盲目追新。2. 核心原理PagedAttention与连续批处理2.1 PagedAttention给KV Cache做“虚拟内存”PagedAttention是vLLM最核心的设计理解它就理解了vLLM的一大半。传统显存管理可以类比成订酒店为了让客户住得舒服直接包下一整层楼不管客户实际住几间。请求数量一多楼层不够用后面的客户只能在楼下排队。PagedAttention的做法变成了按房间按天订客户住几天就订几间房间分布在楼层不同位置也没关系只需要记录房间号就行。具体到实现vLLM把KV Cache划分成固定大小的逻辑块默认每个块可以存16个token的KV向量。每个请求有一个逻辑块表记录它当前占用哪些物理块。当请求生成了新的token就往表里追加对应的物理块如果物理块不够了调度器会先分配新的块分配不到就暂停请求。这里有个关键收益因为块是等大小的显存中几乎不存在碎片问题。一块内存不管属于哪个请求只要没被用上就能立刻分配给其他请求。传统方案中预留后不可复用的情况彻底消失了。2.2 Continuous Batching迭代级调度让GPU一直“满座”再讲Continuous Batching。vLLM对批次的处理粒度非常细调度发生在每一次decode迭代之后。比如当前batch里有8个请求在生成其中3个已经生成到结束标记它们立刻退出腾出的显存块马上可以被新进入的请求使用。这和传统batching的差别非常大。传统batching是短板效应batch里有一个请求要生成500个token其他几个只要50个GPU也得陪着跑完500步。vLLM则让短请求提前下车新请求随时上车GPU在每一轮迭代都处于相对满负荷的状态。我自己的体会是这个机制对混合负载尤其有效。实际业务里请求长度参差不齐有的问一句就结束有的要生成一大段代码。Continuous Batching能把长短请求混在一起调度互不拖累GPU平均利用率能上一个台阶。2.3 为什么整体提升能到几十倍很多人看到公开benchmark里vLLM吞吐提升5倍、10倍甚至20倍的数字觉得夸张。其实这些数字不是玄学而是几个因素叠加的结果。第一显存利用率提高直接转化为更大的并发批大小。以前只能并发4路请求现在能并发16路吞吐自然涨。第二decode阶段的batch更大GPU矩阵运算的并行效率更高。第三PagedAttention对prefill阶段也有优化超长prompt可以按块切分处理首token时间有所改善。我实测过的效果是在显存受限的高并发场景提升幅度最明显三四倍是保底如果你的请求普遍很长、上下文很大那收益会更夸张接近公开数据里的上限。反过来短请求、低并发、算力本来就闲的场景提升幅度没那么大。所以判断收益前先看清自己的负载特征。3. 本地部署实操从安装到启动服务3.1 环境准备与安装部署vLLM之前先确认三件事GPU型号和显存、GPU驱动版本、Python环境。我的建议是至少8GB显存起步否则很多主流开源模型放进去之后剩下的显存连KV Cache都挤不出多少并发根本没戏。Python版本建议3.9到3.12之间。CUDA方面vLLM打包的预编译版本通常对应CUDA 11.8或12.1你本地的GPU驱动需要支持对应的CUDA版本。安装时最省心的方式就是用官方软件仓库直接装pip install vllm装完验证一下版本python -c import vllm; print(vllm.__version__)如果网络受限配置国内镜像源pip install vllm -i https://pypi.tuna.tsinghua.edu.cn/simple这里有个坑如果你的机器上之前装过旧版vLLM或者一堆深度学习的依赖包建议先建一个干净的虚拟环境再装。vLLM对torch、transformers的版本要求比较严格混装容易出现各种莫名其妙的冲突。我在内网机器上踩过不少次最后统一用虚拟环境隔离才稳定下来。3.2 启动服务核心参数逐个讲vLLM装好之后启动推理服务的方式很简洁。官方入口是vllm serve命令直接指定模型路径或模型名它就会拉起一个OpenAI兼容的HTTP服务。我常用的一条启动命令是vllm serve Qwen/Qwen2.5-7B-Instruct \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.92 \ --max-model-len 8192 \ --max-num-seqs 128 \ --tensor-parallel-size 1 \ --enable-prefix-caching这些参数每一个都影响并发能力挨个说清楚--gpu-memory-utilization控制KV Cache最多占用多少比例的GPU显存。我通常设0.85到0.95之间不建议拉满到0.99因为CUDA context、计算图、激活值也要占显存拉满容易在运行时碰壁。--max-model-len是支持的最大上下文长度包括输入加输出。它会影响KV Cache块的分配策略和prefill的切分方式。不是越大越好得按业务实际需求来盲目设32K会让单请求占用显存大幅上升压缩并发空间。--max-num-seqs是单个批次最多容纳的序列数直接决定并发上限。设太小请求来了只能排队设太大batch过大后每个序列的解码速度会下降。初始建议64或128后续通过压测再微调。--tensor-parallel-size是多卡并行参数。当单卡放不下模型时设为2或4vLLM会通过Tensor Parallel把模型切到多张卡上。--enable-prefix-caching开启前缀缓存多个请求共用相同prompt前缀时可以复用KV Cache多轮对话场景收益很大。3.3 调用接口与本机验证服务启动后先验证是否健康curl http://127.0.0.1:8000/v1/models这个接口会返回当前加载的模型信息能通就说明服务起来了。vLLM提供了OpenAI兼容的接口格式意味着业务侧可以用标准的SDK直接调用几乎零成本切换。之前我在内部项目里接多个推理框架业务代码完全不用改只换了base_url就把vLLM接入进去了。调用示例from openai import OpenAI client OpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keylocal, ) resp client.chat.completions.create( modelQwen/Qwen2.5-7B-Instruct, messages[{role: user, content: 用一句话解释什么是KV Cache}], max_tokens256, temperature0.7, ) print(resp.choices[0].message.content)流式输出也是一样的用法只需要加streamTrue。vLLM对流式场景的优化也做得不错迭代级的Continuous Batching在流式输出下更能体现吞吐优势。4. 高并发实战吞吐量分析与参数调优4.1 压测脚本与关键指标启动服务之后不能凭感觉判断性能一定要做压测。我用过最简单的并发压测脚本基于httpx的异步客户端可以模拟不同并发度下的压力。核心代码如下import asyncio import time import statistics import httpx URL http://127.0.0.1:8000/v1/chat/completions MODEL Qwen/Qwen2.5-7B-Instruct async def send_one(client, payload, semaphore): async with semaphore: start time.time() resp await client.post(URL, jsonpayload) resp.raise_for_status() data resp.json() ttft data.get(timings, {}).get(first_token_time, None) return time.time() - start, ttft async def run_load(concurrency, total_requests, payload, semaphore): async with httpx.AsyncClient(timeout300) as client: tasks [] for _ in range(total_requests): tasks.append(send_one(client, payload, semaphore)) results await asyncio.gather(*tasks) return results if __name__ __main__: payload { model: MODEL, messages: [{role: user, content: 写一个200字的自我介绍}], max_tokens: 256, temperature: 0.7, } for concurrency in [8, 16, 32, 64, 128]: semaphore asyncio.Semaphore(concurrency) results asyncio.run(run_load(concurrency, 200, payload, semaphore)) latencies [r[0] for r in results] tps len(results) / (sum(latencies) / concurrency) avg_latency statistics.mean(latencies) print(fconcurrency{concurrency}, TPS{tps:.2f}, avg_latency{avg_latency:.2f}s)这里的TPS计算方式比较简单准确的压测应该统计从开始到所有请求完成的完整窗口而不是用平均延迟除以并发数。建议在实际压测时用固定时间窗口来统计完成的请求数。需要重点关注的指标有三个TTFT也就是首Token时间。它反映排队和prefill的开销多轮对话场景下这个指标尤其重要。如果TTFT飙升说明请求在调度器里排队太久了。TPOT也就是每个Token的生成耗时反映decode阶段的batch负载。如果TPOT从20ms涨到100ms说明一个batch里塞了太多序列每个序列分到的算力被稀释了。TPS和整体吞吐单位时间完成的请求数或生成的token总数。这两个指标是最终的性能标尺所有调优最后都要落到它们上面。4.2 调优经验从吞吐曲线找最佳值我之前在单张80GB显存的显卡上测试过一套70亿参数的模型把并发数从8一路加到256TPS的变化趋势让我印象很深。在低并发区间TPS基本线性增长到达某个拐点后增速放缓继续加压TPS不再增加反而平均延迟明显恶化。这个拐点附近就是这台机器的合理并发上限。针对不同负载特征调优优先级不一样。短输出场景比如分类、抽取、关键词生成输出token数少瓶颈往往是prefill。优先调大--max-num-seqs把更多请求塞进一个有prefill能力的batch同时开启--enable-prefix-caching。这类场景里--max-model-len不用设太大反而应该压到能覆盖业务输入上限即可省下的显存全给KV Cache。长输出场景比如生成文章、代码、报告decode是主要耗时每个序列长期占着显存块。这时优先保证单序列的decode速度--max-num-seqs不宜无限放大否则TPOT恶化用户体感会明显变差。最佳做法是压测不同并发下的TPOT曲线找到一个“TPS还在涨、TPOT还能接受”的平衡点。--gpu-memory-utilization的调优也讲究策略。我一般先设0.90跑一轮压测确认不OOM后再适当往0.95试。如果业务高峰期请求排队严重而是KV Cache不够导致的可以调大这个值。前提是留出足够余量给其他显存开销我见过有人设0.99然后运行中崩溃的。4.3 多卡并行与量化方案单卡放不下模型或者想进一步扩大并发时两个方向可以参考。多卡方面--tensor-parallel-size 2或4vLLM会把模型权重和计算切到多张卡上。注意显存带宽成为新的瓶颈跨卡通信有开销不是卡越多越快4卡以上的收益增长会明显放缓。有条件的话建议优先用NVLink互联的卡效果更好。量化方面对这个场景帮助很大。70亿参数的模型FP16权重要14GB显存用AWQ或GPTQ量化为4bit后只剩约4GB剩下的显存几乎全给KV Cache并发能力可以翻不少。量化方式不影响OpenAI接口模型推理照常走。我自己的实测是4bit量化在对话类任务上质量损耗不大但吞吐收益很实在。量化模型启动方式和普通模型一致vllm serve ./models/coder-7b-awq \ --quantization awq \ --gpu-memory-utilization 0.92 \ --max-num-seqs 128有一点要留意量化模型如果本身是手工转化或社区版本精度差异可能不小上线前一定要跑一批评测集做对比不能只看指标好看就切过去。5. 常见问题与排查实录5.1 显存不足与KV Cache耗尽最典型的错误是启动或运行时报 “No available memory for the cache blocks”。这表示KV Cache的可用块已经被占满了。遇到这个问题排查顺序我一般是这样第一把--max-num-seqs调小观察是否恢复。第二把--gpu-memory-utilization从0.95降到0.85附近给其他开销留出空间。第三检查--max-model-len是否设得过大如果业务并不需要32K上下文压到8K或4K能释放大量KV Cache。最后一个隐藏点如果你开启了多卡Tensor Parallel每张卡上还要额外留一部分显存给通信缓冲这时显存利用率更不该拉满。5.2 首Token时间高企与请求排队如果压测时TPS不高但TTFT持续走高大概率是请求在调度器里排队而不是生成太慢。通过vLLM日志里的running和waiting数量可以确认如果waiting一直很高说明--max-num-seqs限制了并发窗口。先说结论优先调大--max-num-seqs。如果已经是256还排队那就是算力或显存真的饱和了这时候可以开--enable-prefix-caching减少重复prefill或者上多卡和量化。记住一个判断技巧TPS上不去但TTFT高是排队问题TPS在涨但TPOT明显变差是batch内过度竞争两个问题的解决方向完全不同。5.3 依赖与版本兼容问题vLLM对底层依赖的要求出了名的严格。我遇到过几次很典型的问题。一个是启动时提示CUDA不可用报错类似“Torch is not compiled with CUDA enabled”。这通常是因为环境里装了一个CPU版或CUDA版本不匹配的torch。解决办法很直接把当前环境清理干净重新安装一份与vLLM匹配的torch和CUDA版本。一个是FlashAttention相关的报错。vLLM在许多GPU上依赖FlashAttention加速而FlashAttention对GPU架构和CUDA版本非常敏感。如果启动时崩溃在FlashAttention初始化优先考虑升级GPU驱动、更换CUDA版本或者安装vLLM预编译版本。不建议自己编译编译时间很长而且不同硬件平台差异很大。还有一个通用建议不要轻易把vLLM和相关依赖一起批量升级。vLLM的小版本更新经常附带底层算子变更升级前仔细读更新说明并且在测试环境跑一遍回归再动生产。5.4 问题速查表问题现象可能原因解决办法启动报 No available memory for cache blocksKV Cache占用已满降低 max-num-seqs、降低 gpu-memory-utilization、缩小 max-model-lenTTFT高但TPS不高请求在排队调大 max-num-seqs、开启 prefix-cachingTPOT明显变差batch内序列数过多适当减小 max-num-seqs平衡吞吐与单请求速度报 Torch CUDA 相关错误torch 与 CUDA 版本不匹配重建虚拟环境重装匹配版本的 torch 和 vLLMFlashAttention 初始化崩溃GPU架构或 CUDA 版本不兼容升级驱动、更换 CUDA 版本或用预编译安装包多卡启动报 NCCL 错误多卡通信异常检查显卡互联方式、驱动版本降低 tensor-parallel-size 测试量化模型输出质量下降量化精度损失或校验集不足换用不同量化位宽先跑评测对比再上线写在最后的一点经验整个项目折腾下来我最深的体会是vLLM的收益上限取决于你的负载特征而不是参数本身。抄网上的配置没有任何意义必须自己压测、画曲线、找拐点。启动服务一句vllm serve就能跑起来但真正把它调成一台稳定扛住高并发推理的服务靠的是对显存、调度和业务特征的综合理解。个人项目建议从单卡加70亿参数模型起步max-num-seqs从64开始压测再逐步往上加。后续如果要扩展可以把vLLM接入统一的模型网关做多模型路由、自动扩缩容那时候你会发现高并发推理这件事真正的大头全在调度和资源管理上。