← 返回资讯

推理吞吐与并发优化:让你的 GPU 不再空转

2026-07-08

推理吞吐(Throughput)不高的根本原因往往不是 GPU 算力不足,而是批处理没做好或显存被 KV Cache 撑爆。本文直接给出定位方法和可落地的优化手段,帮你把 GPU 利用率从 20% 提升到 70%+。

如果你刚从跑通 Demo 切到扛真实流量,大概率会遇到这个场景:单条请求测试延迟很低,一切正常;但把并发压到几十路,nvidia-smi 里 GPU 利用率却卡在 20%–30%,显存占用也不高,看着像是”没在干活”。这时候大部分人的第一反应是加卡,但加卡往往没用——瓶颈根本不在算力,而在调度和显存管理上。下面把这套排查思路和优化手段拆开讲清楚。

三个核心指标先搞清楚

优化之前,先明确你要优化哪个指标:

指标全称含义优化目标
Throughput吞吐量单位时间处理的 token 数(tok/s)或请求数越高越好,降低单请求边际成本
TTFTTime to First Token首 token 延迟,用户感知”响应快不快”越低越好,影响交互体验
TPOTTime Per Output Token每个后续 token 的生成时间影响流式输出的顺滑感

三者之间存在天然张力:大批处理提升 Throughput,但拉高 TTFT;小批处理降低 TTFT,但 GPU 利用率低。

并发数怎么算?记住一个公式

排队论里的 Little’s Law 在这里非常好用:系统内平均请求数 = 到达速率 × 平均处理时长。落到推理服务上就是:

并发数 ≈ QPS × 单请求平均耗时(秒)

举个例子:你的接口平均 QPS 是 10,单条请求从进队列到吐完全部 token 平均耗时 3 秒,那么系统里同时”在途”的请求大约是 30 个——这就是你需要的并发承载能力,而不是你以为的”10”。很多人压测时只看 QPS 不看单请求时长,结果线上一到高峰期,--max-num-seqs 设的 256 根本扛不住突发流量,请求全部排队甚至超时。

反过来这个公式也能帮你估算需要多少实例:如果压测得出单实例能稳定承载 60 并发,而你的预期并发是 300,那至少要横向扩到 5 个实例(还要留 20%–30% 余量应对流量抖动,别卡着上限部署)。

吞吐瓶颈定位

大多数推理服务吞吐不高,问题集中在三处:

1. 批处理过小:每次只处理 1–2 个请求,GPU 大量算力空闲。典型症状:nvidia-smi 显示 GPU 利用率 < 30%,但显存占用也很低。

2. KV Cache 撑爆:并发一高,显存被 KV Cache 耗尽,框架被迫驱逐(eviction)或拒绝新请求。典型症状:GPU 利用率高,但 OOM 或吞吐随并发增加反而下降。

3. CPU/IO 成为瓶颈:tokenization、请求调度、采样解码在 CPU 上耗时过长,GPU 等 CPU 喂数据。典型症状:GPU 利用率忽高忽低,波动大。

用工具把”感觉”变成”数字”

别靠肉眼盯 nvidia-smi 刷新猜问题,三个动作把瓶颈坐实:

  1. 看时间序列而不是瞬时值nvidia-smi dmon -s ucm -d 1 每秒打印一行利用率(u)、显存(m)、时钟(c),跑压测的同时开一个终端盯着,能看出利用率是”持续低”还是”忽高忽低”——前者是批处理太小,后者是 CPU/IO 拖后腿。
  2. 看框架自带的指标端点:vLLM 从 0.4 版本起自带 /metrics(Prometheus 格式),能直接看到 vllm:num_requests_runningvllm:num_requests_waitingvllm:gpu_cache_usage_perc 这几个数。如果 num_requests_waiting 一直不为 0,说明并发上限设低了或者显存不够,请求在排队;如果 gpu_cache_usage_perc 长期贴着 100%,说明 KV Cache 已经是硬瓶颈。
  3. 用官方压测脚本而不是自己写 curl 循环:vLLM 仓库自带 benchmark_serving.py,能模拟真实的 Poisson 到达流量分布,输出的吞吐、TTFT、TPOT 分位数比你自己写脚本测的靠谱得多——自己写的脚本大概率是串行发请求,测出来的是”单路延迟”而不是”并发吞吐”,两者完全是两码事,拿单路延迟去优化并发问题基本是南辕北辙。

核心优化手段

Continuous Batching(连续批处理)

传统静态批处理要等一批请求都生成完才处理下一批,GPU 在等待最慢请求时空转。Continuous Batching 让已完成的请求立刻被新请求替换,GPU 几乎没有空闲间隙。

vLLM、SGLang、TGI 都已内置此能力,不需要额外配置,选对框架即生效。

理解它为什么快,关键在”批”的调度粒度从”整批”降到了”每一步(iteration)“。传统实现里,一批 8 个请求要一起等最长的那个生成完(比如别人都吐完 50 个 token 了,有个请求要吐 500 个),GPU 在等待期间只能空转或者硬塞进下一批(但下一批要等这一批完全结束才能开始,于是干脆等)。Continuous Batching 把调度单位改成”每生成一个 token 就重新决策一次”:哪个请求已经吐完 EOS,立刻从批次里摘掉,空出来的槽位马上塞进等待队列里的新请求。GPU 的算力利用率因此几乎不会因为”等最慢的那个”而出现空档,这也是它能把利用率从 20% 拉到 70%+ 的核心原因,不是靠更强的硬件,而是靠调度粒度变细。

PagedAttention 与 KV Cache 管理

vLLM 的 PagedAttention 将 KV Cache 切成固定大小的物理块,按需分配,避免显存碎片。实际效果:相同显存下可支撑 2–4 倍 的并发请求数。

没上 PagedAttention 之前,主流做法是按”最大可能长度”给每个请求预先分配一整块连续显存(比如按 max_seq_len=4096 预留),哪怕这条请求实际只生成了 50 个 token,剩下的显存也白白占着不能给别的请求用——这就是显存碎片化的根源。PagedAttention 借鉴操作系统虚拟内存分页的思路,把 KV Cache 拆成一个个小块(block),按需要动态分配、按需要归还,不再要求物理连续。代价是访问 KV Cache 时多一层”页表”寻址开销,但相比省下来的显存空间,这点计算开销完全值得——这也是为什么几乎所有主流推理框架(vLLM、TGI、TensorRT-LLM)后来都收敛到了类似的分页式显存管理方案。

关键参数调优

参数(vLLM)默认值调优方向说明
--max-num-seqs256按显存上调最大同时处理序列数,直接影响并发上限
--max-num-batched-tokens4096按显存上调单次前向传播最大 token 数
--gpu-memory-utilization0.90.85–0.95KV Cache 可用显存比例,过高 OOM,过低浪费
--tensor-parallel-size1多卡时设为卡数张量并行,多卡分摊权重

这几个参数不是孤立调的,改一个会牵动另外两个,调参顺序建议是:先固定 --gpu-memory-utilization(一般从 0.9 起步,如果同机还跑别的进程要留够余量往下调),再压测找 --max-num-seqs 的上限(观察显存占用是否逼近上限),最后如果多卡再开 --tensor-parallel-size。反过来先调并发数上限再调显存比例,容易调出一个”压测能过、线上偶发 OOM”的假稳定配置。

真实会踩的坑:从报错反推根因

torch.cuda.OutOfMemoryError: CUDA out of memory:最常见的一条。根因通常是 --max-num-seqs--max-num-batched-tokens 设得比显存能承受的高,压测时流量小没暴露,线上并发一冲上去 KV Cache 立刻撑爆。修法:先把 --gpu-memory-utilization 降 0.05(比如 0.9 降到 0.85),如果还 OOM 再降 --max-num-seqs;长期方案是上量化或者加卡做张量并行,而不是一味压并发上限。

吞吐随并发增加不升反降:不是 bug,是 KV Cache 触发了驱逐(eviction)——显存不够时框架会把部分正在生成的请求换出去等下次调度,换入换出本身有开销,多轮驱逐叠加起来比直接排队还慢。判断方法是看前面提过的 vllm:gpu_cache_usage_perc 指标,长期贴近 100% 就是这个问题,处理方式和上面 OOM 一致。

请求偶发超时但服务端日志显示正常处理完成:八成是客户端超时阈值设得比 P99 延迟还短。批处理和排队会让长尾请求的耗时明显拉长,如果客户端超时设成”平均耗时的 2 倍”,高峰期一堆请求会卡在长尾区间被误杀。建议客户端超时按 P99 延迟的 1.5 倍设置,并配合指数退避重试(首次等待 1 秒,失败再等 2 秒、4 秒,最多重试 3 次),而不是固定间隔重试——固定间隔重试在高峰期只会让排队雪上加霜。

长上下文场景下 TPOT 突然变高:极长上下文(比如几万 token 的 prompt)会让每一步解码的 attention 计算量线性甚至更快增长,这不是配置问题,是数学问题。缓解手段是开启 chunked prefill(vLLM 里叫 --enable-chunked-prefill),把超长 prompt 的预填充阶段拆成多个小块和其他请求的解码步交替执行,避免一个长 prompt 独占一整个 iteration 拖慢所有人。

量化降低权重显存,让出 KV Cache 空间

INT8 或 INT4 量化压缩权重占用,节省出来的显存分配给 KV Cache,直接提升可支撑并发数。权重显存减少 50%,可换来约 2 倍的 KV Cache 空间,并发能力相应提升。

量化不是没有代价的免费午餐,取舍点在精度:INT8 通常精度损失很小,大部分场景感知不到;INT4 如果只是简单地做 round-to-nearest 量化,在一些逻辑推理、长文本任务上会出现明显的输出质量下降,具体表现是回答变得啰嗦重复或者偶尔答非所问。生产环境用 INT4 建议选 AWQ 或 GPTQ 这类带校准数据集的量化方案,用少量真实业务样本跑一遍校准,能把精度损失压到可接受范围——直接上朴素量化上生产是给自己埋雷。判断该不该上量化的简单标准:如果你的瓶颈是显存(并发上不去),量化收益明显;如果瓶颈是计算(GPU 算力已经打满),量化对吞吐帮助有限,这时候该考虑加卡或换更快的卡。

模型分级路由

不是所有请求都需要最大模型。将简单任务路由到 7B 小模型,复杂任务才打到 70B 旗舰模型,可以将整体吞吐提升数倍,同时降低成本。

这套路由怎么落地?最简单粗暴但也最实用的方式是先用规则分流:请求长度短、意图明确(分类、抽取、格式转换类任务)直接走小模型;涉及多步推理、代码生成、长文写作的走大模型。进阶一点可以训一个轻量分类器(甚至用小模型自己做路由判断),根据历史标注数据学习”这类问题小模型能不能答对”,比纯规则更准,但要投入标注成本,量级不大的团队没必要一上来就搞。分级路由真正的收益不只是吞吐翻倍这么简单——它本质是把”总算力”按任务难度重新分配,简单任务本来就不该占用大模型的算力,这是资源配置问题,不是模型能力问题。

不同场景的优先级

场景首要优化次要优化
在线对话(低延迟优先)降低批大小,减少 TTFT适度量化,扩并发上限
批量处理(吞吐优先)最大化批大小,量化权重异步调度,避免 CPU 阻塞
混合负载Continuous Batching + 动态批大小优先级队列区分在线/批量

吞吐和成本怎么换算

吞吐优化最终要落到成本上才有意义,公式很简单:

每百万 token 成本 = GPU 单价(元/小时)÷ 吞吐(tok/s)÷ 3600 × 1,000,000

假设你压测出来单卡吞吐是 2000 tok/s,GPU 单价按你实际采购或云厂商报价代入——吞吐每翻一倍,这项成本直接减半,这就是为什么”调吞吐”比”换更贵的卡”往往性价比更高:同样的硬件,通过 Continuous Batching + 合理的并发参数,可能就把单位成本打下去了一半还不用多花钱。具体单价请以你自己拿到的云厂商报价或采购价为准,这里给的是公式,不是具体数字,套自己的真实数据算一遍就知道当前配置划不划算。

反过来这个公式也能帮你判断”要不要换卡”:如果换一张贵 50% 的卡能带来 2 倍以上的吞吐提升(比如从 A100 换到 H100,得益于更高的显存带宽和算力),换卡就是划算的;如果吞吐提升幅度追不上价格涨幅,不如先把现有硬件的调度和参数榨干。

常见问题

GPU 利用率已经 80%+,但吞吐还是上不去,怎么办?
高 GPU 利用率不等于高吞吐。检查 TPOT 是否过高——可能是解码阶段单步计算量大(如极长上下文),或 CPU 采样/调度成为瓶颈。后者可通过升级 CPU 或优化采样参数(减少 top-p 候选集)缓解。

增加并发数,延迟就暴涨,怎么平衡?
这是批大小与延迟的经典权衡。可设置请求优先级或超时阈值,让对延迟敏感的请求走较小批次,离线任务走大批次。或者考虑横向扩容多实例,让每个实例维持较低负载。

vLLM 和 SGLang 在吞吐上哪个更好?
SGLang 在前缀共享(prefix caching)和 Agent 多轮场景下有优势;vLLM 的社区更大、模型兼容性更好。生产环境建议先用 vLLM 跑压测,再根据瓶颈类型决定是否切换。

流式输出会不会拖累吞吐?
不会拖累 Throughput,但会改变你对”快慢”的感知方式。流式(SSE/chunked)本质是把 TTFT 和 TPOT 暴露给用户逐 token 展示,而不是等全部生成完再一次性返回——服务端的批处理和调度逻辑完全不受影响,吞吐计算方式也一样。真正需要注意的是客户端:如果客户端用同步阻塞的方式一条条发请求等返回,并发能力会被压得很低;换成异步客户端(比如用 asyncio + 异步 HTTP 库并发发出多路请求),才能把服务端 Continuous Batching 攒起来的并发能力真正吃满,不然服务端空转的问题会转移到客户端头上。

多卡到底该选张量并行还是流水线并行?
单机多卡、模型能装进单卡显存但想要更大 batch 承载能力时,优先选张量并行(--tensor-parallel-size)——它把每一层的计算切开分摊到多卡,通信开销靠 NVLink 之类的高速互联摊平,实现简单且 vLLM 原生支持好。如果模型大到单卡装不下(比如百亿甚至千亿参数模型配小显存卡),才需要流水线并行把不同层分布到不同卡上,但流水线并行会引入”气泡”(前面的卡算完等后面的卡)问题,调度复杂度更高,不是显存不够就别轻易上。


延伸阅读: