本节摘要:块表、批处理与量化的大头收益拿到之后,还有一层更细的旋钮:CUDA 计算图消除调度开销、prefill 分摊平衡 TTFT 与 TBT、批参数与延迟的三角关系。本节讲清这些进阶开关的机理与适用条件——以及为什么有些场景下"关闭优化"反而是优化。
压测报告离目标就差最后一口气:吞吐达标了,P99 还差一点;或者反过来,均值漂亮,长尾难看。这一层的问题往往不在资源组织(前文已解决),而在执行层的细碎开销——成千上万次内核启动的调度成本、prefill 与 decode 混批时的相互干扰、批参数与延迟目标之间的三角权衡。本节的每个开关都值得先懂机理再动手:这一层的调优错一个方向,收益会变成倒退。
decode 一步要做的事很碎:几十层的网络、每层若干算子,CPU 要发起成百上千次内核启动。每次启动的固定开销只有几十微秒,但乘以次数与每秒步数,在 decode 这种"每步计算量很小"的场景里占比可观——这就是所谓 CPU 侧调度开销。
vLLM 默认用 CUDA 计算图(CUDA Graph)优化这一层:把 decode 一步的内核序列预先录制下来,之后每步直接整体回放,省去逐个启动的开销。对小块、多步的 decode 而言,收益通常在百分之十上下浮动,负载越碎越明显。
默认行为:录制并回放计算图,decode 走快车道。 关闭方式:启动参数 enforce-eager(强制即时执行)。 什么时候反而要关: 1. 排查阶段:排除计算图本身的变量,先看基线行为。 2. 极端内存受限:计算图需要额外显存存录制的图,紧张时是净负担。 3. 形状极度多变:批大小剧烈波动时回放收益下降,重录有成本。
判断依据依然是数据:同一压测口径下开关各跑一轮,看吞吐与 TBT 分位的变化方向。计算图是默认开启的加速项,但"默认"不等于"永远"。
第 7.2 节提过 max-num-batched-tokens:单个迭代里 prefill 允许吞掉的 token 上限。它为什么重要?因为连续批处理会把 prefill 与 decode 混在同一个批里跑——一条长输入的 prefill 如果一口气吞掉几千 token 的计算,正在流式输出的那些 decode 请求这一步就要等很久,TBT 出现一段明显的凹陷。
分摊(chunked prefill)的思路是把长输入切成多次迭代逐步消化:每次迭代只做一小段 prefill,decode 步照常穿插。代价是长输入自己的 TTFT 略升,收益是所有流式请求的 TBT 更平滑。这个旋钮的调法:
| 业务形态 | 建议取向 | 理由 |
|---|---|---|
| 纯对话、流式体验优先 | 分摊窗口调小 | TBT 平滑压倒一切,TTFT 多几十毫秒无感 |
| 批量离线处理 | 分摊无所谓或调大 | 没有并行的流式请求需要照顾 |
| 长文档问答、混合负载 | 中等窗口实测 | 两类请求都有,只能实测找平衡 |
把前文的机制收拢成一个心智模型:给定块池容量,批参数在三个目标之间分配——
批开大:吞吐 ↑,TBT ↑(变差),抢占风险 ↑(峰值缓存 = 批 × 上限长度) 批开小:TBT 平稳,抢占罕见,但吞吐 ↓,GPU 摊薄效应减弱 不存在"最优批大小"——只存在"你的 SLO 之下能开的最大批"。 求法:固定压测口径,从当前值逐级下调/上调,找 TBT P99 触线的临界点,回退一档。
这个三角与第 4 章的调度机制一体两面:连续批处理消灭的是"等"的浪费,批参数决定的是"挤"的程度。挤得适度,洪峰由抢占兜底;挤得过度,抢占常态化,重算开销反过来吃掉收益——第 8.1 节的"并发对账"就是在为这个三角画安全边界。
坦率地说,普通使用者能动的不多,这正是 vLLM 工程化程度高的体现:
内核层真正的功课是读懂基准:升级 vLLM 版本、更换硬件、切换精度时,跑一遍固定口径的压测,让数字告诉你这一层发生了什么,而不是猜测。
💡 进阶调优的心态:这一层的收益是"几个百分点",前面的机制层是"几倍"。优先级永远放在机制层——先把第 2 到 7 章的账算对,再回来抠这里的细节;反过来的顺序是新手最常见的浪费。
把"任何改动过压测"落到具体动作上。以计算图开关为例,一份合格的对比记录长这样:
环境:单卡、8B 模型、并发 64、长度分布为业务日志反推 口径:同一份压测脚本、同一数据集、同机同时段(避免温度漂移影响频率) 吞吐 tok/s TTFT P99 TBT P99 计算图开 3820 310 ms 26 ms 计算图关 3510 (-8%) 322 ms 29 ms 结论:开启收益明显,维持默认;记录归档。
注意结论栏写的是"维持默认"——大量对比实验的产出就是"确认默认值是对的",这同样是有效产出:它把一个"不确定"变成了"有数据的不确定性已消除",下次别人再问起,不用重跑。
混批干扰是否真实存在,不用猜,监控曲线能直接回答:观察 TBT 的时间序列,如果毛刺的时刻与长请求到达的时刻相关(把长输入请求的到达时间戳叠在 TBT 曲线上,肉眼可见的相关性),就是 prefill 在同一迭代里吞太多 token 挤压了 decode。此时把分摊窗口调小、复测对比,若毛刺收窄而 TTFT 只轻微变差,这个旋钮就调对了方向。这种"曲线叠加"的土办法,比争论参数含义有效得多。
内核层的表现与框架版本强相关:算子优化、调度策略、缓存实现在版本间持续演进, upgrades 既可能带来免费性能,也可能带来意外回归。成熟的团队会为版本升级定一条纪律:升级前在预发环境跑一遍固定口径基准,与当前版本的基线对比,吞吐差出某个阈值(比如超过半成)就先查变更说明再上线。这条纪律的成本是每次升级多半小时,换的是"性能回归在预发被发现"而不是"用户在群里反馈变卡了"。
调优的手段讲完了,还差尺子:用什么口径压测、看哪些指标、异常曲线对应什么根因。下一节把闭环的最后一块拼上。