推理机制动态图解(进阶):并行、投机解码、PD、FlashAttention、调度
推理过程动态图解(基础) · 框架对比 · 推理优化方法
基础篇讲了张量变化、连续批处理和 PagedAttention。推理里还有很多“文字很难讲清、画出来就懂”的机制,这一页再补 7 个逐步演示,覆盖并行、加速、部署和调度中最常被问到的部分。
| 演示 | 要回答的问题 |
|---|---|
| 1. 张量并行 | 权重怎样切到多张卡?为什么每层要 all-reduce? |
| 2. 专家并行(MoE) | token 怎样被发到专家所在的卡?为什么负载不均是核心问题? |
| 3. 投机解码 | 为什么一次前向能产出多个 token?结果为什么不变? |
| 4. PD 分离 | Prefill 算完的 KV 怎样交给 Decode?什么时候才能释放? |
| 5. FlashAttention | 不写出完整注意力矩阵,怎样得到同样的结果? |
| 6. 重叠调度 | CPU 调度开销怎样被藏起来?代价是什么? |
| 7. 抢占 | KV 块不够时调度器怎样腾空间?为什么抢占多了很浪费? |
1. 张量并行:权重怎样切开
GPU 0X[T, 4096]
GPU 1X[T, 4096]
通信—
GPU 0X× W_up 左半Y₀[T, 7168]
GPU 1X× W_up 右半Y₁[T, 7168]
通信不需要:每张卡独立算一半输出列
GPU 0Y₀激活(逐元素)
GPU 1Y₁激活(逐元素)
通信不需要:激活只看自己这一列
GPU 0Y₀× W_down 上半Z₀[T, 4096] 部分和
GPU 1Y₁× W_down 下半Z₁[T, 4096] 部分和
通信两张卡的结果都只是一部分
GPU 0Z = Z₀ + Z₁[T, 4096]
GPU 1Z = Z₀ + Z₁[T, 4096]
通信all-reduce
GPU 0注意力头 0–15KV head 0–3
GPU 1注意力头 16–31KV head 4–7
通信注意力同样按头切开,o_proj 后再一次 all-reduce
- 张量并行(TP=2):每张卡都有完整的输入 X,权重切开放在两张卡上。以 MLP 为例(为简化只画 up 投影)。
- 第一个矩阵按“列”切(Column Parallel):每张卡算出一半的输出列,彼此不需要通信。
- 激活函数逐元素计算,各卡只处理自己的那一半,仍然不需要通信。
- 第二个矩阵按“行”切(Row Parallel):每张卡用自己那一半输入乘对应的一半权重,得到的是完整形状但只是部分和。
- 一次 all-reduce 把两张卡的部分和相加,两张卡都得到完整结果。每层 MLP 和注意力各需要一次 all-reduce,所以 TP 通常只在 NVLink 连接的同一台机器内使用。
- 注意力按头切:32 个 query 头、8 个 KV 头平均分到两张卡,每张卡的 KV Cache 只有一半。如果 KV 头数少于 TP 数,就要复制 KV 头,每卡容量不再减半。
2. 专家并行:token 怎样找到专家
tokent1t2t3t4
路由router 给每个 token 选 top-2 专家
GPU 0(专家 0、1)—
GPU 1(专家 2、3)—
tokent1 → E0, E2t2 → E1, E2t3 → E2, E3t4 → E2, E0
路由选出的专家和权重
GPU 0(专家 0、1)—
GPU 1(专家 2、3)—
tokendispatch:按专家所在的卡发送 token
路由all-to-all
GPU 0(专家 0、1)E0: t1, t4E1: t2
GPU 1(专家 2、3)E2: t1, t2, t3, t4E3: t3
token各专家独立计算
路由—
GPU 0(专家 0、1)算 3 个 token
GPU 1(专家 2、3)算 5 个 token(E2 是热点)
tokencombine:结果发回 token 原来的卡,按权重加权求和
路由all-to-all
GPU 0(专家 0、1)—
GPU 1(专家 2、3)—
- MoE 层里每个 token 只用少数几个专家。专家并行(EP)把不同专家放在不同 GPU 上。这里 4 个专家放在 2 张卡上,每个 token 选 2 个专家。
- router(一个小的线性层)为每个 token 打分,选出 top-2 专家和对应权重。
- dispatch:用一次 all-to-all 通信,把每个 token 发到它所选专家所在的卡。这一步的通信量随 token 数和选中的专家数增长。
- 各卡上的专家独立计算。专家 E2 被 4 个 token 选中,是热点,GPU 1 要算得更久,其他卡只能等它——负载不均是 EP 的核心问题,常见对策有负载均衡损失、冗余热点专家、动态调整专家位置。
- combine:再一次 all-to-all 把结果发回 token 原来的卡,按 router 权重加权求和。每个 MoE 层两次 all-to-all,所以大规模 EP 对网络延迟和拓扑非常敏感;一张卡故障会影响整个 EP 组。
3. 投机解码:先猜,再验证
已确认今天天气
草稿模型—
目标模型—
已确认今天天气
草稿模型很好吗
目标模型草稿模型很小,连续猜 3 个 token,速度快
已确认今天天气
草稿模型很好吗
目标模型一次前向验证 4 个位置
已确认今天天气
草稿模型很 ✓好 ✓吗 ✗
目标模型第 3 个位置目标模型想要的是“,”
已确认今天天气很好,
草稿模型“吗”及其 KV 被丢弃(回滚)
目标模型一次前向得到 3 个 token
- 投机解码:用一个便宜的方法先猜几个 token,再让大模型一次性检查。目标是减少大模型的前向次数。
- 草稿模型(或 EAGLE、MTP 头等)连续猜出 3 个 token。
- 目标模型把这 3 个猜测连同前文一起做一次前向,同时得到 4 个位置的预测。因为 Decode 受带宽限制,多算几个位置几乎不增加时间。
- 从左到右比较:前两个猜对了,第三个和目标模型的预测不同,从这里开始的猜测全部作废。
- 接受“很、好”,再加上目标模型在出错位置给出的“,”,这一次前向产出 3 个 token。被拒绝位置的 KV 要回滚。只要用合适的接受规则,输出分布和不用投机解码完全一样;接受率越低、并发越高,收益越小(算账见推理优化)。
4. PD 分离:KV 怎样交接
路由请求到达
Prefill 实例空闲
KV 传输—
Decode 实例运行其他请求的 Decode
路由选择 Prefill 和 Decode 实例
Prefill 实例计算整个 prompt 的 KV采样第 1 个 token
KV 传输—
Decode 实例预留 KV 块
路由—
Prefill 实例KV 留在显存,等待传输
KV 传输RDMA 写到 Decode 的块
Decode 实例接收中,不能使用
路由—
Prefill 实例确认传输完成后释放 KV
KV 传输完成
Decode 实例KV 就绪
路由流式返回输出
Prefill 实例处理下一个请求的 Prefill
KV 传输—
Decode 实例加入 Decode batch,逐个生成
- PD 分离:Prefill 和 Decode 跑在不同的实例(机器池)上。请求先到路由。
- 路由同时选好 Prefill 实例和 Decode 实例。Prefill 实例算出整个 prompt 的 KV(这里画成 Prefill 侧同时采样出第一个 token;首 token 由哪一侧产生取决于实现,例如 SGLang 的 PD 模式由 Decode 侧在 KV 就位后算出);Decode 实例提前预留好接收用的 KV 块。
- KV 通过 RDMA(例如 Mooncake Transfer Engine、NIXL)从 Prefill 的显存传到 Decode 预留的块。传输完成之前,Decode 不能使用这些块。8K 上下文的示例模型约 1 GiB。
- 传输完成后 Prefill 才能释放自己的 KV;Decode 的块变为就绪。如果传输失败或超时,要有明确的处理:重传、重新 Prefill,或者报错。
- Decode 实例把这个请求加入自己的批处理,继续生成。Prefill 不再被长 Decode 拖慢,Decode 也不会被长 Prefill 打断,两个池可以按负载分别扩缩容。
5. FlashAttention:分块与在线 softmax
Q 块Q 的一块[Bq, d]
K/V 块K/V 块 1K/V 块 2K/V 块 3
片上状态m = −∞(当前最大值) l = 0(指数和) O = 0
Q 块Q 的一块
K/V 块K/V 块 1K/V 块 2K/V 块 3
片上状态算 S₁ = Q·K₁ᵀ,得到 m₁、l₁,O = exp(S₁ − m₁)·V₁
Q 块Q 的一块
K/V 块K/V 块 1K/V 块 2K/V 块 3
片上状态新最大值 m₂ 更大 → 旧的 l 和 O 乘以 exp(m₁ − m₂) 重新缩放,再加上块 2 的贡献
Q 块Q 的一块
K/V 块K/V 块 1K/V 块 2K/V 块 3
片上状态同样方式合并块 3
Q 块输出 = O / l
K/V 块全部处理完
片上状态只把最终结果写回显存;完整的注意力矩阵从未写出
- FlashAttention 把 Q 和 K/V 切成小块,放进 GPU 片上的高速存储里计算。每个 Q 块维护三个量:当前见过的最大分数 m、指数和 l、未归一化的输出 O。
- 处理第一个 K/V 块:算出这一块的注意力分数,用块内最大值做数值稳定的 exp,累加到 O 和 l。
- 处理第二块时,如果出现了更大的分数,之前算的 exp 都要按新的最大值缩小——乘以 exp(旧最大值 − 新最大值)。这就是“在线 softmax”:不用先看完所有分数,也能得到和标准 softmax 完全相同的结果。
- 依次处理所有 K/V 块。中间结果一直在片上,不写回显存。
- 最后 O / l 就是标准注意力的输出。计算量没有减少,省掉的是把 [序列长度 × 序列长度] 的大矩阵写回显存再读出来的开销。PagedAttention 的 kernel 在此基础上再按块表去收集不连续的 K/V 块。
6. 重叠调度:让 GPU 不再等 CPU
不重叠CPU 调度 NGPU 计算 NCPU 处理结果 NCPU 调度 N+1GPU 计算 N+1
GPU 利用CPU 工作时 GPU 空闲,Decode 每轮越短,空闲占比越大
重叠CPU 调度 NGPU 计算 N同时CPU 调度 N+1
GPU 利用GPU 算第 N 轮时,CPU 已在准备第 N+1 轮
重叠GPU 计算 N+1同时CPU 处理结果 N、调度 N+2
GPU 利用GPU 几乎不停
代价第 N+1 轮调度时还不知道第 N 轮的结果
GPU 利用例如某请求在第 N 轮已生成结束符,第 N+1 轮可能多算一步,再丢弃
- 不重叠时,每一轮都是“CPU 调度 → GPU 计算 → CPU 处理结果”串行进行。Decode 一轮可能只有几毫秒,CPU 的几百微秒到几毫秒就会占掉很大比例。
- 重叠调度(SGLang 的 overlap scheduler、vLLM 的异步调度)让 CPU 在 GPU 计算第 N 轮的同时,准备第 N+1 轮的输入。
- 稳定后,GPU 一直有活干,CPU 的调度和结果处理被隐藏在 GPU 计算时间里。
- 代价是调度第 N+1 轮时还拿不到第 N 轮的输出,实现上要处理“结果晚一轮到达”:已结束的请求可能多算一步再丢弃,采样结果要在 GPU 上直接传给下一轮(vLLM 源码中对应 stale / in-flight token 的回退处理)。
7. 抢占:显存不够时怎么办
运行中A8 块B6 块C5 块
空闲块1 块
等待队列空
运行中A需要新块BC需要新块
空闲块不够分
等待队列空
运行中AB
空闲块C 释放 5 块
等待队列C(已生成的部分记下来)
运行中AB 结束
空闲块空出更多块
等待队列C
运行中AC 重新 Prefill(prompt + 已生成部分)
空闲块—
等待队列空
- 显存里的 KV 块快用完了:A、B、C 都在 Decode,只剩 1 个空闲块。
- 下一轮 A 和 C 都需要新块,空闲块不够。
- 调度器抢占一个请求(vLLM V1 中通常是优先级最低或最晚到达的):释放它的全部 KV 块,把它放回等待队列,已经生成的 token 保留下来。
- 等其他请求结束、显存空出来。
- 被抢占的请求重新被调度时,要把 prompt 和已生成的部分重新 Prefill 一遍(重算)。抢占越频繁,浪费的计算越多——这是显存容量不足的典型信号,可以用 KV 量化、减小 batch、分层缓存等方式缓解。另一种做法是把 KV 换出到主机内存再换回,代价是 PCIe 带宽。
还有一些机制已经在其他页面用动态演示讲过:前缀缓存的命中与淘汰(块管理器深入)、SGLang 基数树(框架对比)、Transfer Engine 的切分与重试(Transfer Engine)、Mooncake Store 的租约与淘汰(Store 内核),以及互动计算器里的前缀命中折叠和超时租约。