Jellow 编辑发布,AI 辅助生成并完成技术核对。 计算示例和实测结果会在文中分别说明。
同一个模型,有时要等几秒才开始回答,有时第一句话出现得很快,后面却一个字一个字地往外蹦。这两种“慢”,往往需要不同的处理方法。
讨论推理加速之前,先把一次请求拆开:服务器什么时候收到请求,什么时候开始计算,什么时候产生第一个 token,又在什么时候完成回答。只有知道时间花在哪里,才能判断某个优化是否有效。
本文以使用 KV Cache 的自回归、decoder-only Transformer 为例。后面的数字是人为设置的计算示例,不是某张显卡的性能实测。
1. 一次生成包含两个主要阶段
假设输入经过分词后有 1,024 个 token,模型需要继续生成 128 个 token。
Prefill 阶段处理已经给定的输入,计算各层表示并建立 KV Cache。输入 token 已经全部已知,因此同一层中多个位置的计算可以组织成较大的矩阵运算。因果注意力仍然保留:后面的位置可以看前面,前面的位置不能偷看后面。
Prefill 的最后一个位置通常就能给出第一个输出 token 的概率分布。
Decode 阶段继续生成后面的 token。标准自回归生成需要先确定前一个 token,再计算下一个 token。即使显卡还有空闲算力,同一条序列也不能随意把这些依赖关系拆开。Prefill 和 Decode 的计算特点,以及由此带来的优化差异,可参考 NVIDIA 的推理优化说明。
这里容易产生一个误解:使用 KV Cache 后,模型是不是只看最后一个 token?实际情况是,新的 query 仍然需要访问历史 key 和 value;缓存节省的是历史状态的重复计算。Hugging Face 的缓存说明给出了这一过程。
2. “吞吐更高”不一定代表“聊天更快”
先明确几个指标:
| 指标 | 测量内容 | 更接近哪种体验 |
|---|---|---|
| TTFT | 从发出请求到收到第一个输出的时间 | 要等多久才开始回答 |
| TPOT | 除第一个输出 token 外,平均每个输出 token 花费的时间 | 开始回答后生成得快不快 |
| E2E latency | 一次请求从开始到完成的总时间 | 多久能拿到完整答案 |
| Output throughput | 测量窗口内成功产生的输出 token 总数除以窗口时间 | 服务整体能处理多少输出 |
测量位置必须写清楚。客户端 TTFT 会包含网络、排队和服务端处理,不能直接等同于 GPU 的 Prefill 时间。输入和输出 token 混合统计得到的 total throughput,也不能直接和 output throughput 比较。
对产生至少两个输出 token 的请求,一种常见的计算口径是:
TPOT = (E2E latency - TTFT) / (输出 token 数 - 1)
vLLM 的基准工具采用上述 TPOT 定义。它还区分 TPOT 和流式输出之间的间隔 ITL:一次流式输出可能包含多个 token,尤其是在推测解码中,二者并不总相等。指标定义
下面是假设数据:
| 配置 | 单请求输出长度 | TTFT | TPOT | 推导出的 E2E |
|---|---|---|---|---|
| A | 101 token | 0.30 秒 | 0.020 秒 | 2.30 秒 |
| B | 101 token | 0.80 秒 | 0.015 秒 | 2.30 秒 |
两种配置的完整回答耗时相同,但 A 开始得早,B 后半段更流畅。如果只比较总耗时,就会丢掉这部分体验差异。
3. 为什么单请求 Decode 容易受显存带宽限制
把一个稠密线性层简化成矩阵乘法。处理很多 token 时,一次读入的权重可以参与更多运算;只处理一个 token 时,权重读取成本更难被摊薄。
可以用一个简化的下界模型建立直觉:
单步耗时 >= max(运算量 / 可达到的计算速率,
数据搬运量 / 可达到的内存带宽)
这个模型假设计算和搬运能较好重叠,没有计入所有调度、同步和通信开销。
再假设一个 70 亿参数的稠密模型,权重全部使用 2 字节存储,每步都需要从显存读取约 14 GB 权重。如果持续可达到的带宽是 1 TB/s,仅权重搬运就需要约 14 毫秒。这是人为设定条件下的估算,不是对具体模型输出速度的承诺。
长上下文还会带来 KV Cache 访问,大 batch 会提高权重复用,多卡部署会引入通信。这些因素都可能改变瓶颈。因此,“Decode 经常受带宽限制”是一种工作负载特征,不是所有模型、所有 batch 下都成立的定律。
4. 批处理解决的是复用与空转问题
多个请求合成一个 batch,有机会共同摊薄权重读取和 kernel 启动成本。但请求的输出长度不同,固定 batch 如果一直等最长的请求结束,短请求留下的位置就会空着。
Continuous batching 可以在迭代边界移出已完成请求,再加入新请求。它改变的是调度单位。Orca 的 iteration-level scheduling 是理解这一方向的重要工作。Orca 论文
另一个问题是长输入对正在 Decode 的请求造成干扰。Chunked prefill 把较长的输入处理拆成若干块,允许调度器更细地安排输入处理与输出生成。块大小会影响首 token 等待时间、输出间隔和吞吐,需要结合负载调节。vLLM 调优文档
这也解释了为什么把 batch 调大后,吞吐可能提升,P95 延迟却恶化:服务更忙,并不等于每个用户等待更短。
5. 做一次有解释力的对照实验
建议先准备三个固定的负载组,再逐项调整配置:
| 负载组 | 输入长度 | 目标输出长度 | 想观察的问题 |
|---|---|---|---|
| 短问短答 | 256 token | 128 token | 基础服务开销与输出延迟 |
| 长文理解 | 8,192 token | 128 token | 输入处理与长上下文影响 |
| 长答案生成 | 1,024 token | 1,024 token | 持续 Decode 表现 |
这些长度是实验设计示例,可以按真实业务替换。每组从低并发开始,逐步提高到出现明显排队的位置,同时记录 TTFT、TPOT、吞吐、失败率和显存占用。
比较结果时,固定模型与 tokenizer 版本、精度、采样参数、硬件和软件版本;明确是固定并发还是固定到达率。预热也要一致。重复使用相同输入可能命中前缀缓存,热缓存结果应与冷缓存结果分开记录。vLLM 基准注意事项
测试最终要回答一个具体问题,例如“在 P95 TTFT 不超过 1 秒的条件下,最大可接受请求率提高了多少”。要得到这个结论,还需要避免负载模型把排队问题藏起来。
6. 闭环压测可能把过载藏起来
负载生成方式也会改变结论。
闭环压测通常维持固定并发:一个请求完成后才发下一个。当服务变慢时,客户端也会跟着降低发送速度。它适合模拟“固定数量的用户各自等待回答”,却可能把系统的过载点藏起来。
开放环压测按照预定到达率发请求,不等待前一个请求完成,更接近有独立用户持续到达的场景。vLLM 的在线基准区分请求率、并发上限和不同的到达模式,也支持按照延迟 SLO 统计 goodput。vLLM Benchmark CLI
假设服务在某个输入输出分布下,稳定处理能力约为每秒 5 个请求:
- 闭环客户端看到延迟增加后自然降到每秒 4.5 个请求,队列看起来一直不长;
- 开放环仍以每秒 6 个请求到达,未完成请求会持续积累,P95 和 P99 很快恶化。
这里的 5、4.5、6 都是假设数字。关键判断是:当长期到达率高于完成率时,不存在稳定的排队状态。短时间压测可能在队列填满前结束,于是给出过于乐观的结果。
在稳定区间内,还可以用 Little 定律做一次一致性检查:
平均在途请求数 L = 到达率 λ × 平均系统时间 W
例如平均每秒完成 5 个请求,平均端到端时间 2 秒,则系统内平均约有 10 个请求。这个数量包括排队和执行中的请求,不等于 GPU 上某一时刻的 batch size。如果监控值相差很大,先检查统计窗口、重试、流式连接关闭和请求是否丢失。
7. 用 SLO goodput 取代孤立的峰值吞吐
吞吐只计算“完成了多少”,没有说明这些请求是否来得及。可以先给业务定义一组服务目标,例如:
TTFT P95 <= 1.0 s
TPOT P95 <= 40 ms
请求失败率 <= 0.1%
然后把 goodput 定义为单位时间内同时满足这些条件的请求数。某个配置可能把输出吞吐从每秒 1,000 token 提高到 1,200 token,但大量短请求的 TTFT 超标;它的峰值吞吐更高,goodput 反而可能更低。
SLO 必须按请求类型制定。交互式聊天通常更在意 TTFT 和输出节奏,离线摘要更关心完成时间与总成本。把两类流量混在一个 P95 中,会让指标既不能代表聊天体验,也不能代表批任务效率。
一次更完整的容量实验可以这样进行:
- 从真实流量中只保留 token 长度、到达间隔和采样参数等非敏感统计,构造可重复数据集;
- 选择多个到达率,每档持续到队列、吞吐和延迟进入稳定区间;
- 每档同时报告 TTFT、TPOT、E2E 的 P50/P95/P99、完成率、goodput 和显存峰值;
- 画出“到达率—goodput”和“到达率—P95 延迟”曲线,找到延迟开始陡增的位置;
- 只在同一 SLO 和同一输入输出分布下比较优化前后容量。
这样测试得到的不是“这台机器最快能跑多少 token”,而是“在用户还能接受的延迟下,它能稳定接住多少请求”。后一个问题才更接近部署决策。
更新说明(2026-09-05): 补充了推导、失效条件和可执行的部署判断方法。