资讯动态

vLLM本地高并发推理服务部署与性能调优实战

发布时间:2026/10/15 20:02:08 来源:尧图企业网站定制
把一个大模型真正跑起来、跑得快、扛得住多人同时用比很多人想象的要难。几个月前我给一个内部问答系统搭本地推理服务最初直接用模型加载再加上一层HTTP封装结果四个人一起用就开始排队GPU利用率一直在10%上下徘徊。后来换成vLLM作为高吞吐推理引擎同样的卡几十路并发也稳稳当当吞吐直接上了一个量级。这篇文章就把我当时的选型思路、部署步骤、压测调优和踩坑经历完整写下来给准备做本地高并发推理服务的同学一个可以直接抄作业的参考。vLLM解决的核心问题非常明确在本地 GPU 资源有限的条件下把大规模语言模型的推理吞吐压榨到极致同时对外提供标准化的并发服务接口。它适合所有自建模型推理服务的场景包括企业内部知识库问答、私有化部署的助手应用、代码补全工具以及需要对微调模型做在线验证的算法团队。无论你是刚接触推理服务的后端工程师还是已经在用其他框架但觉得吞吐不理想的算法工程师这篇文章里的参数和步骤都可以直接落到你自己的服务上。1. 项目整体设计与选型思路1.1 为什么选vLLM从“一卡一模型”到“一卡顶多路”我先说说最初踩的坑。直接用原生推理代码加 FastAPI 封装看起来很简单实际上一上线就崩。原因是原生推理是“一个请求占一份资源”每个请求进来都要重新执行完整的前向过程重复计算 KV Cache显存碎片化严重GPU 算力大部分时间在空转。并发稍微一高要么显存爆掉要么请求排队排到超时。vLLM 的思路完全不同。它把推理服务看成一个“调度系统”而不是简单的模型封装。多个请求会被动态合并成一个 Batch在迭代层面进行调度KV Cache 被改造成分页式管理不再为每个请求预留一整块连续显存而是按需分配。这样一来GPU 的显存利用率和算力利用率都能大幅提升。在选型的时候我也认真比较过其他方案。原生方案适合原型验证不值当投入TGI 也是优秀的高吞吐框架但配置项更繁琐对部分模型结构的兼容性不如 vLLM 到位TensorRT-LLM 性能确实强可一旦模型结构改动或者要挂载 LoRA 适配器导引擎的成本非常痛。vLLM 的优势在于“开箱即用 模型兼容面广 生态配套完整”社区活跃遇到问题能找到大量现成案例上手成本最低。1.2 支撑高吞吐的三个核心机制理解 vLLM 为什么快绕不开这三个机制PagedAttention、Continuous Batching、Prefix Caching。PagedAttention 是 vLLM 最核心的论文级创新。它把 KV Cache 切成固定大小的块按需为每个请求分配而不是一次性预留全部长度。这就类似操作系统的虚拟内存分页机制进程不再需要连续物理内存需要哪页就映射哪页。放在推理场景里同一个请求的 KV Cache 可以散布在显存的不同位置不同请求之间甚至可以共享已经算过的公共前缀。显存碎片没了浪费没了可容纳的并发请求数自然就上来了。Continuous Batching 解决了传统静态 Batch 的“木桶效应”。静态 Batch 会把一批请求从头跑到尾一个生成长文本的慢请求会把整批请求卡住GPU 只能干等。vLLM 的调度粒度是“每一步”一个请求在当前 step 生成结束后立刻退出新请求马上补进来。实际效果就是 GPU 始终在处理尽可能多的活跃请求这才是高并发的真正来源。Prefix Caching 在高并发的多轮对话场景中是隐藏增益最大的机制。多个用户共享相同的系统提示词或者同一会话的上下文有大量重叠vLLM 可以发现并复用这部分 KV Cache省掉重复计算。我实测过固定 system prompt 的问答服务开启这个选项后单请求的 TTFT 能下降 40% 以上。这三个机制叠加起来意味着 vLLM 的吞吐优势是“并发越高越明显”单个请求反而不一定比原生推理快。所以如果你的场景就是本地高并发服务选型几乎没有悬念。2. 本地高并发推理服务部署实操2.1 硬件选型与显存估算先把账算清楚。部署一个大模型显存占用主要由三块构成模型权重、KV Cache、激活值和临时缓冲。权重是固定的7B 规模模型用 bf16 精度大约是 14GB13B 规模约 26GB70B 规模约 140GB。KV Cache 是动态的取决于并发数、序列长度和模型层数这部分才是高并发部署时真正吃显存的大头。我给一个常见的估算参考。单张 80GB 显存的卡跑 7B 到 13B 规模的模型配置上 32K 上下文和适中的并发通常还有余量跑 32B 规模的模型就需要精打细算要么降低并发要么压缩上下文长度70B 规模基本要两张以上 80GB 卡做张量并行。显存不是越多越好但做高并发服务至少要把权重和期望并发下的 KV Cache 都塞进显存否则只能频繁换入换出吞吐会断崖式下跌。部署环境方面我强烈建议直接用官方容器镜像。自己装 CUDA、cuDNN、编译依赖很容易版本错位一旦 torch 和 CUDA 版本不匹配跑起来全是不明报错。用 Docker 拉起一个干净环境模型权重放挂载盘里日志输出到宿主机管理和迁移都方便。2.2 启动参数逐个拆解vLLM 的启动命令本身不复杂难在参数怎么配合。我给出一个经过实际验证的基线配置python -m vllm.entrypoints.openai.api_server \ --model /data/models/llm-model \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.90 \ --max-model-len 32768 \ --max-num-seqs 64 \ --max-num-batched-tokens 8192 \ --enable-prefix-caching \ --port 8000逐个说下我为什么这么配。--model 直接指向本地路径。虽然也支持从模型仓库拉取但本地服务讲究稳定可控权重文件一旦下载好就不建议运行时依赖外网。--tensor-parallel-size 是张量并行卡数。单卡就是 1多卡就设成卡数。要注意的是多卡并行不是没有成本卡间通信会占用带宽如果模型本身没有那么大强行上多卡反而可能变慢。我对 32B 规模以上的模型才建议上 2 卡以上13B 级别的单卡足够。--gpu-memory-utilization 是核心中的核心。它控制 vLLM 最多占用多少比例显存包括权重和 KV Cache。0.90 是我常用的值留出 10% 给 CUDA 上下文、激活值和偶发抖动。刚开始我图省事设到 0.98结果一次峰值并发直接 OOM进程崩溃服务不可用。这个参数宁低勿高0.85 到 0.93 之间比较安全。--max-model-len 决定模型能接受的最大序列长度。这个值不是越大越好KV Cache 预分配会随着长度增长成倍增加32K 的显存开销大约是 8K 的四倍。要结合实际业务的输入长度来设不要盲目跟风拉满。--max-num-seqs 控制同一时刻最多并行处理的请求数量这是高并发调优的主战场。默认值偏保守后文我会专门讲怎么压测调整。--max-num-batched-tokens 限制单个 Batch 内的总 token 数。设得小延迟低但吞吐受限设得大吞吐高但单步计算时间变长。我这台机上 8192 是甜点值你可以根据显存和模型规模上下试探。--enable-prefix-caching 在多轮对话场景建议开启后面实测数据会说明它的价值。2.3 部署形态与多副本设计单实例跑起来很简单但要支撑稳定的高并发服务建议设计成“多实例 负载均衡”的形态。单实例的问题是它只有一个调度器并发超过一定阈值后请求只能排队。即使 GPU 算力还有剩余调度和内存分配也会成为瓶颈。我现在的做法是起两个 vLLM 实例各占一块卡前端用负载均衡把流量分散过去。这样做的好处有三个一是单实例故障时可以摘除流量不影响整体服务二是不同业务可以打到不同实例避免互相挤兑三是容量规划简单流量翻倍就加实例。但多实例也有个容易被忽略的坑显存池被切碎了。两个实例各自维护 KV Cache无法跨实例复用整体显存利用率可能反而低于单实例。如果总流量不大单实例足够多实例只是白白浪费显存。我用一个简单的判断标准单实例的 GPU 利用率持续在 90% 以上且延迟可接受就不需要加实例如果利用率不高但请求已经排队说明调度瓶颈到了这时候才考虑拆流量。2.4 与标准接口无缝对接vLLM 启动后默认提供 /v1/chat/completions、/v1/completions 和 /v1/embeddings 三组标准接口。这意味着业务代码不需要做任何适配原来怎么调用云端大模型接口现在就怎么调用本地服务只要把 base_url 指到本地地址就行。from openai import OpenAI client OpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY # vLLM 默认不校验 key ) resp client.chat.completions.create( model/data/models/llm-model, messages[{role: user, content: 你好介绍一下你自己}], max_tokens512, temperature0.7 ) print(resp.choices[0].message.content)这里我顺手提醒一下api_key 参数是接口兼容需要vLLM 并不会真正校验它可以随便填。但如果你把本地服务暴露到非可信网络一定不要依赖 vLLM 自带的这种简单模式要在负载均衡层加认证和限流。3. 高并发性能调优与实测3.1 压测方案怎么设计压测这件事最忌讳的是随便写个单线程脚本跑几个请求就得出“性能很好”的结论。我压测的目的是四个指标吞吐量每秒输出多少 token、TTFT首 token 延迟、TPOT每个 token 的生成耗时、端到端耗时。压测脚本我建议做成并发请求模式。下面是一个简化版的思路用并发任务模拟多用户同时请求统计整体吞吐和分位延迟import asyncio import time import statistics from openai import AsyncOpenAI client AsyncOpenAI(base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY) PROMPT 请用 300 字说明人工智能的发展历史。 OUTPUT_TOKENS 300 CONCURRENCY 32 REQUESTS 128 async def single_request(): start time.perf_counter() resp await client.chat.completions.create( model/data/models/llm-model, messages[{role: user, content: PROMPT}], max_tokensOUTPUT_TOKENS ) usage resp.usage ttft 0 # 简化统计实际可用流式接口测首token return { latency: time.perf_counter() - start, tokens: usage.completion_tokens } async def main(): tasks [single_request() for _ in range(REQUESTS)] results [] sem asyncio.Semaphore(CONCURRENCY) # 控制并发数 async def wrapped(req): async with sem: return await req results await asyncio.gather(*(wrapped(r) for r in tasks)) total_tokens sum(r[tokens] for r in results) total_time max(r[latency] for r in results) print(f总输出 tokens: {total_tokens}) print(f总耗时: {total_time:.2f}s) print(f吞吐: {total_tokens / total_time:.2f} tokens/s) print(f平均延迟: {statistics.mean(r[latency] for r in results):.2f}s) print(fP95 延迟: {sorted(r[latency] for r in results)[int(len(results)*0.95)]:.2f}s) asyncio.run(main())压测时有个关键细节prompt 不能太短output 不能太少。短 prompt 加短输出会让请求的调度开销占比过大测出来的数据不能反映真实业务。我会用接近生产环境的输入长度比如 500 到 1000 字的问题输出控制在 300 token 左右。压测机最好单独放一台不要一边压测一边看网页网络抖动会影响延迟数据。3.2 并发与吞吐曲线解读我压制了不同的并发数得到一组非常典型的曲线数据。需要注意具体数字随模型和显卡不同会有差异但变化规律是一致的并发数吞吐 (tokens/s)平均端到端延迟 (s)TTFT (s)TPOT (ms/token)1258.00.35081809.50.4553265012.00.86064110016.51.565128125028.03.280256127054.06.590这张表信息量很大。并发从 1 涨到 64吞吐涨了 40 多倍延迟只从 8 秒涨到 16 秒这就是 Continuous Batching 的威力GPU 始终被喂满。到 128 并发以后吞吐几乎不再增长因为算力已经到顶但延迟继续恶化TTFT 从 1.5 秒涨到 3.2 秒再到 6.5 秒。这说明服务已经进入饱和区排队的请求越来越多。我判断服务健康状态的标准很简单看 TTFT 的涨幅是否和并发数同步恶化。如果并发翻倍但 TTFT 只涨一点点说明 GPU 还没吃满可以继续加压如果 TTFT 开始非线性上涨说明调度和算力都到极限了再加并发只会拖垮所有请求。3.3 关键参数怎么调以 max-num-seqs 为例max-num-seqs 是我调优过程中改动最频繁的参数。默认值一般偏小高并发场景下吞吐会被明显卡住。我从 16 开始一路试到 256每次只动这一个参数其他保持不变记录吞吐和延迟变化。结果是在 64 并发以内max-num-seqs 的影响不明显但到达 128 并发时只要 max-num-seqs 小于当前队列里的请求数吞吐就会突然掉到 600 附近大量请求在排队。原因是 vLLM 的调度器只会在活跃序列数低于这个阈值时接收新请求你并发打进来 128 个但 max-num-seqs 只有 32那 96 个都在等待GPU 吃不满。调到 128 以后吞吐稳定在 1200 以上。再往上调到 256吞吐没有继续涨因为 GPU 算力上限到了反而 KV Cache 占用变大如果显存不充裕还会导致 OOM 风险。这个参数的正确姿势是“让 GPU 算力而不是请求数成为瓶颈”。一般可以从 64 起步压测观察吞吐和显存逐个档位往上探找到吞吐不再增长的拐点然后回调一个档位作为生产值。gpu-memory-utilization 的调优思路也类似。如果 KV Cache 余量充足可以试着让 vLLM 用更多显存来容纳更大的 batch对短并发请求尤其有效。但记住这必须从 0.85 开始慢慢加不要一步到位。3.4 开启 Prefix Caching 的实际收益我专门做了一组对比测试验证 prefix caching 在本地 RAG 问答场景的收益。业务特征是每个用户都带同一段很长的系统提示词加上检索回来的上下文前 3000 token 几乎是完全相同的。结果非常明显。开启 --enable-prefix-caching 后在相同并发下TTFT 从 1.8 秒降到 0.9 秒整体吞吐提升了约 35%。原因是 vLLM 直接复用已经算好的公共前缀 KV Cache这些 token 根本不需要重新前向计算。如果你的业务中没有固定的公共前缀比如每次都是全新的随机问题这个选项的收益就很小。所以我的建议是先看业务模式再决定开关而不是无脑全开。3.5 流式接口与延迟感知还有一点很容易被忽略接口的流式与非流式会影响延迟体验。非流式接口必须等全部 token 生成完才返回即使 TTFT 很短用户也要等大几十秒。流式接口则可以做到“第一个字立刻出来后面的字持续输出”用户感知延迟大幅降低。vLLM 对流式接口支持得很好但压测时要注意流式接口的 TPOT 统计需要用逐 chunk 到达的时间来计算而不是等整个响应结束。生产环境我几乎一律建议客户端走流式尤其是面向真实用户的应用。本地服务端本身在该场景下开销差异不大主要改善的是用户心理等待时间。4. 常见问题与排查技巧实录4.1 启动时直接 OOM 或进程崩溃这是最常遇到也最让人崩溃的问题。表现是服务刚启动调用一次就报 CUDA out of memory甚至进程直接被杀。我踩过最典型的坑是把 gpu-memory-utilization 设到 0.98结果 CUDA 上下文和激活值没有余量首批请求直接爆掉。排查顺序我建议这样先把 max-model-len 降下来长上下文是 KV Cache 大户再把 max-num-seqs 降一档最后调节 gpu-memory-utilization。如果用的是多卡还要确认显存是不是被其他进程占了nvidia-smi 看一圈。一个常见误解是“显存越大越安全”其实 batch 和 KV Cache 的分配是动态的即使启动成功运行中也可能 OOM所以生产配置一定要留保守余量。4.2 请求越积越多延迟无限上涨明明吞吐看起来还行但用户反馈“请求一直转圈”。压测数据显示 TTFT 随着并发非线性上涨但 TPOT 变化不大。这说明服务端队列堵了而不是生成速度变慢。这类问题大概率出在 max-num-seqs 太小或者 max-num-batched-tokens 限制了批处理规模。我之前遇到过 max-num-batched-tokens 设成 2048 的情况一旦输入稍长单请求就占满整个 batch token 配额其他请求只能等着。把值调大到 8192 后队列立刻通畅。如果调完还堵就要考虑加实例扩容了。4.3 长文本请求被拒绝或截断接入真实业务后总会遇到超长输入。比如用户直接粘贴了一整篇文章进来长度超过 max-model-len。vLLM 会直接报 400 错误提示 max context length exceeded。处理方式有两个层面。服务端把 max-model-len 调到业务合理区间的上限但不要盲目拉满因为这会让 KV Cache 预分配增大即使大多数请求没那么长显存浪费也很大。业务端加一层输入截断和摘要逻辑长文本先切片再做检索不把全文塞给模型。我的经验是模型支持的上下文长度是上限不是推荐值真实生产通常用不到一半。4.4 服务运行一段时间后变慢有一种隐蔽的性能退化服务刚启动时一切正常运行几小时后开始显著变慢直到重启才恢复。这类问题多为显存碎片化或缓存增长导致的。vLLM 的分页机制已经大幅减少碎片但长时间运行后请求长度模式差异巨大还是可能让 GPU 显存分布变得不理想调度器需要处理更多空块。我的处理办法是做好服务健康检查和自动重启机制。vLLM 自带 /health 接口我用它做存活探测同时监控显存利用率和请求延迟连续几个周期都恶化就触发重启。对于高可用要求更严格的场景配合多实例负载均衡重启单个实例不会影响整体服务。4.5 常见问题速查表现象可能原因排查手段快速解法启动即 CUDA OOMgpu-memory-utilization 过高 / max-model-len 过大看启动日志、nvidia-smi降 utilization降 max-model-len并发一高就崩max-num-seqs 超出显存余量看运行时显存峰值调低 max-num-seqsTTFT 高但不报错请求排队 / prefix 未命中看服务日志请求排队指标加大 max-num-seqs加实例吞吐上不去GPU 没吃满 / batch 太小压测观察 GPU 利用率调大 max-num-batched-tokens长输入被拒max-model-len 低于输入长度看返回错误信息业务侧截断/摘要不盲目拉长度运行越久越慢显存碎片化 / 缓存增长看监控曲线健康检查 自动重启我个人在实际操作中一个很深的体会是vLLM 虽然已经做到开箱即用但“高吞吐”不是默认就有的而是靠参数打磨出来的。每次调整我都先压测再上生产先在压测脚本里把并发涨到预计峰值的两倍确认拐点在哪里再决定最终的参数组合。这样上线后心里有底容量规划也清晰。另外还想分享一个小技巧本地服务上线前在压测阶段就把“客户端的流式解析 服务端健康检查自动化”一起做掉而不是等业务接入时再补。这个准备能省掉后面很多联调的痛苦。如果你也在搭建自己的本地推理服务希望这份经验能让你少走一些弯路。

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

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

免费获取报价 →
↑