LLM 自回归推理的执行路径:从 Tokenization 到 Continuous Batching

用户在聊天框里输入一句话,几秒后屏幕开始逐字出现回答。看起来像一次普通的函数调用,实际上这段旅程跨越了分词器、Transformer、CUDA Kernel、KV Cache、请求调度器和网络服务层。

本文不按模型组件逐个介绍,而是跟随一个 Token 从进入系统到离开 GPU。沿着这条数据流,可以自然解释 Prefill 与 Decode、KV Cache、Continuous Batching,以及 TTFT 和 TPOT 为什么会成为推理系统最重要的指标。

LLM 自回归推理中的 Token 执行路径

图 1:Prefill 与 Decode 共享模型结构,却具有完全不同的执行形状和性能瓶颈。

理解整条链路时,最好把系统看成三个相互耦合的状态机,而不是一条只会向前的流水线:

状态机 保存什么 何时变化
模型状态 权重、编译图、并行配置 加载或热更新时
请求状态 Token 序列、采样参数、KV Cache、停止条件 每生成一个 Token
调度状态 Waiting/Running 队列、Token Budget、Block 水位 每个推理迭代

模型前向只负责把输入状态推进一步;生产推理的复杂性来自数百个请求状态机共享同一份模型状态,并由调度状态决定谁在下一轮获得计算和显存。

一、Tokenization 与离散输入表示

模型不能直接处理字符串。请求首先经过 Tokenizer,被转换为一串整数 ID:

1
2
3
"AI Infra 很有意思"
↓ Tokenizer
[2334, 6121, ...]

现代 LLM 通常使用子词或字节级分词。Tokenizer 会影响:

  • 同一句话被切成多少 Token;
  • 上下文窗口实际能容纳多少文字;
  • 推理计费和吞吐统计;
  • 多语言场景中的输入效率;
  • Prefix Cache 能否命中完全一致的 Token 前缀。

因此,服务端判断两个 Prompt 是否拥有相同前缀时,比较的不是肉眼看到的字符串,而是分词后的 Token 序列。

二、Embedding、位置编码与输入张量

每个 Token ID 都会在 Embedding 表中查到一个 dd 维向量。假设 Batch 为 BB,序列长度为 SS,隐藏维度为 dd,进入 Transformer 前的张量形状是:

(B,S,d)(B,S,d)

Embedding 本身不包含顺序信息,所以还需要位置编码。当前 Decoder-only 模型常使用 RoPE,将位置信息施加到 Q、K 上。这样 Attention 的内积不仅与内容有关,也能感知相对位置。

这一阶段看似简单,却决定了后续所有 Kernel 的形状。如果不能熟练跟踪 (Batch, Sequence, Hidden),就很难分析 Attention 的显存、GEMM 的尺寸或张量并行的切分位置。

三、Decoder Block 的计算路径

Decoder-only Transformer 架构

图 2:Decoder-only 模型从 Embedding 开始,经过重复的 Decoder Block,最终由 LM Head 产生 Logits。

一个典型的 Pre-Norm Decoder Block 可以抽象成:

1
2
3
x
├─ RMSNorm → Causal Self-Attention → Residual Add
└─ RMSNorm → SwiGLU FFN → Residual Add

3.1 Q、K、V 从哪里来

输入 XRB×S×dX\in\mathbb{R}^{B\times S\times d} 分别乘以投影矩阵:

Q=XWQ,K=XWK,V=XWVQ=XW_Q,\quad K=XW_K,\quad V=XW_V

拆成多头后,Q 的形状通常是:

(B,Hq,S,dh)(B,H_q,S,d_h)

GQA 模型的 K、V 则是:

(B,Hkv,S,dh),Hkv<Hq(B,H_{kv},S,d_h),\quad H_{kv}<H_q

Attention 的核心公式为:

softmax(QKTdh+M)V\operatorname{softmax}\left(\frac{QK^T}{\sqrt{d_h}}+M\right)V

其中 MM 是 Causal Mask,它让当前位置只能看到自己和之前的 Token。

3.2 FFN 为什么不能忽略

Attention 负责 Token 之间的信息混合,FFN 则对每个 Token 独立做非线性变换。SwiGLU 通常包含 Gate、Up 和 Down 三个大矩阵乘,是参数量和计算量的重要组成部分。

从系统角度看,一个 Decoder Block 不是单个大 Kernel,而是一组 GEMM、Norm、激活、位置编码和 Attention Kernel。推理框架需要尽量减少中间张量读写和 Kernel Launch 次数。

四、Prefill 与 Decode 两阶段执行模型

自回归模型每次只生成一个新 Token,但处理输入 Prompt 和生成后续 Token 是两种完全不同的工作负载。

4.1 Prefill:一次处理完整 Prompt

假设 Prompt 有 SS 个 Token,Prefill 会让这 SS 个位置一起通过所有 Transformer 层。

特点是:

  • GEMM 的矩阵尺寸较大;
  • GPU 并行度高;
  • 计算量随 Prompt 长度快速增长;
  • 通常更接近 Compute Bound;
  • 结束时生成所有历史 Token 的 K、V。

用户最关心的 Prefill 指标是 TTFT,也就是从请求进入系统到收到第一个输出 Token 的时间。

4.2 Decode:每轮只新增一个 Token

Decode 第 tt 轮只计算一个新位置的 Q、K、V。新 Q 要与之前所有 Token 的 K 做 Attention,然后产生下一个 Token 的概率分布。

此时矩阵乘的 MM 维通常很小,GPU 每轮却仍要读取大量模型权重和历史 KV。Decode 因而常表现为 Memory Bound:算术单元还没吃饱,显存带宽已经成为瓶颈。

特性 Prefill Decode
每次处理 Token 数 整个 Prompt 每请求通常 1 个
主要瓶颈 计算 权重与 KV 读取
用户指标 TTFT TPOT/ITL
优化重点 高效 Attention、Chunking Batching、量化、CUDA Graph

五、为什么需要 KV Cache

如果没有 KV Cache,第 tt 轮生成时必须重新计算前面 t1t-1 个 Token 的 K 和 V。整个生成过程会产生大量重复工作。

KV Cache 的策略很直接:

1
2
3
Prefill: 计算 Prompt 所有 K/V → 写入缓存
Decode : 只计算新 Token 的 K/V → 追加到缓存
新 Q 读取全部历史 K/V

为什么不缓存 Q?因为历史 Token 的 Q 只用于它们各自产生输出。生成当前 Token 时,需要的是当前 Q 与全部历史 K、V,历史 Q 不再参与计算。

KV Cache 用显存换取了计算,但也带来了新的系统问题:

  • 长上下文让缓存线性增长;
  • 并发请求的长度不同;
  • 请求随时到达和结束;
  • 连续预留容易产生碎片;
  • 相同系统提示词会被反复计算。

这些问题分别催生了 PagedAttention、Continuous Batching 和 Prefix Cache。

六、Logits 处理与采样解码

最后一层隐藏状态经过 Final Norm 和 LM Head,得到词表大小的 Logits。Softmax 将其转换为概率分布,但系统不一定选择概率最大的 Token。

6.1 Temperature

pi=softmax(zi/T)p_i=\operatorname{softmax}(z_i/T)

  • T<1T<1:分布更尖锐,输出更确定;
  • T>1T>1:分布更平坦,输出更多样。

6.2 Top-K

只保留概率最高的 K 个候选,再归一化采样。它限制候选数量,但没有考虑概率质量。

6.3 Top-P

按概率从高到低排序,保留累计概率达到 pp 的最小集合。分布很确定时候选少,分布不确定时候选会自动增多。

采样出新 Token 后,它会被追加到请求序列,重新进入下一轮 Decode,直到遇到结束符、长度限制或停止条件。

6.4 一个最小自回归循环

下面的伪代码刻意省略推理框架的调度细节,只保留模型状态如何跨轮次延续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@torch.inference_mode()
def generate(model, input_ids, max_new_tokens, temperature=0.8):
# 首轮输入完整 Prompt,模型返回首 Token 所需 logits 和每层 KV。
output = model(input_ids=input_ids, use_cache=True)
past_key_values = output.past_key_values
next_token = sample(output.logits[:, -1], temperature)
generated = [next_token]

for _ in range(max_new_tokens - 1):
# 后续每轮只输入上轮产生的一个 Token。
output = model(
input_ids=next_token[:, None],
past_key_values=past_key_values,
use_cache=True,
)
past_key_values = output.past_key_values
next_token = sample(output.logits[:, -1], temperature)
generated.append(next_token)
if reached_stop_condition(next_token):
break

return torch.stack(generated, dim=1)

这段代码展示了 Prefill/Decode 的语义边界,但不适合直接作为生产服务:每个请求单独循环会产生小 Kernel 和频繁 Launch,也无法让完成的请求即时退出 Batch。vLLM、SGLang 等引擎优化的正是“多个这样的状态机如何共享一次模型执行”。

七、多请求推理与迭代级调度

如果 GPU 每次只为一个请求生成一个 Token,Decode 的小矩阵很难利用完整算力。推理引擎会把多个请求的当前 Token 拼成一个 Batch,让一次权重读取服务更多请求。

传统 Static Batching 要等整个 Batch 中所有请求都完成,短请求会被长请求拖住。Continuous Batching 则在每个 Decode 迭代重新组织 Batch:

1
2
3
4
5
6
7
完成的请求退出

释放对应 KV Block

从等待队列补入新请求

执行下一轮 Token

Batch 越大,通常吞吐越高,但每个请求等待调度和执行的时间也可能增加。因此推理系统不是只追求 Tokens/s,而是在吞吐、TPOT 和尾延迟之间做平衡。

八、用指标描述这段旅程

8.1 TTFT

Time To First Token,包括排队、Tokenization、调度和 Prefill。长 Prompt、排队拥塞和冷启动都会抬高 TTFT。

8.2 TPOT / ITL

Time Per Output Token 或 Inter-Token Latency,反映回答逐字输出是否流畅,主要受 Decode 阶段影响。

8.3 E2E Latency

一个近似关系是:

TE2ETTFT+(Nout1)×TPOTT_{E2E}\approx TTFT+(N_{out}-1)\times TPOT

短回答可能由 TTFT 主导,长回答则更容易由 TPOT 主导。

8.4 Throughput 和 Goodput

Throughput 统计单位时间完成的请求或生成的 Token。Goodput 则只统计满足延迟 SLO 的有效工作。一个系统可能吞吐很高,但大量请求超过 TPOT 或 TTFT 限制,此时 Goodput 仍然很低。

九、建议实验

可以用同一模型做四组测试:

  1. 固定输出长度,改变 Prompt 长度,观察 TTFT;
  2. 固定 Prompt,改变输出长度,观察 E2E 延迟;
  3. 改变并发数,记录吞吐和 P99 TPOT;
  4. 对比单请求 Hugging Face 推理与 vLLM Continuous Batching。

记录结果时不要只写平均值,至少保留 P50、P95、P99 和实际 Prompt 长度分布。

十、技术 Q&A

Q1:为什么历史 Token 的 Q 不需要进入 KV Cache?

某个历史位置的 Q 只用于计算该位置自己的 Attention 输出。生成新 Token 时,需要的是“当前新位置的 Q”与“所有历史位置的 K/V”。历史 Q 不会再次参与后续位置的计算,缓存它只会增加显存而不会减少必要计算。

Q2:Prefill 和 Decode 使用同一个模型,为什么瓶颈不同?

Prefill 同时处理 SS 个位置,线性层形成较大的矩阵乘,数据复用和 Tensor Core 利用率较高;Decode 每请求每轮只有一个新位置,GEMM 的 Token 维很小,却仍要读取几乎全部层权重和历史 KV,因此算术强度低,通常更受 HBM 带宽和 Launch 延迟限制。

Q3:为什么增大 Batch 能提高 Decode 吞吐,却可能恶化 TPOT?

单请求 Decode 读取一次模型权重只服务一个 Token;把多个请求组成 Batch 后,同一轮权重读取可以为多个 Token 计算摊销,GEMM 的有效尺寸也变大。但 Batch 过大使单轮执行时间增加,新请求和已经处于 Decode 阶段的请求都要等待更久,所以吞吐上升不保证 TPOT 和尾延迟同步改善。

Q4:应如何用 TTFT、TPOT 和 Goodput 评价推理引擎?

TTFT 描述排队和 Prefill 后用户多久看到首 Token,TPOT 描述后续输出的流畅度,吞吐量描述系统总产出。Goodput 只统计同时满足 TTFT/TPOT SLO 的请求,可避免系统通过无限扩大 Batch 换取表面吞吐。测试时还应使用输入与输出长度分桶、并发扫描和真实到达 Trace;固定 Shape 无法暴露长 Prefill 干扰、动态补位、KV 碎片和 P99 排队时间。

十一、系列导航

上一篇:大模型训练与推理的显存模型

下一篇:GPU 性能分析方法

十二、参考资料

论文

文档