面试-Prefill 和 Decode

1. Transformer 的相关概念

首先复习 Transformer 的相关概念:
https://fairy-study.blog.csdn.net/article/details/154795289

2. Prefill 的概念

Prefill 的处理目标是对 输入 进行 并行 处理,得到最后一个 Token 的 输出状态,将用于 Decode 阶段。

2.1 Prefill 流程

任务: 处理所有 输入 Token,并计算出 第 1 个输出 Token。

  • 输入: ["告", "诉", "我", "如", "何", "学", "习", "?"] (假设 8 个 Token)
  • 并行计算:
    • GPU 一次性把这 8 个 Token 全部送入模型。
    • 模型计算这 8 个 Token 的 QKV
    • 然后将这 8 个输入 Token 产生的 KV 矩阵存入显存(KV Cache 此时大小 = 8)。
  • 生成第一个 Token:
    • 模型利用第 8 个 Token("?")的输出状态,计算 第 9 个位置 的概率分布。
    • Softmax -> 采样 -> 得到 第 1 个输出 Token(比如"要")。
  • 结束: Prefill 阶段结束。
    • 耗时 :这就是 TTFT (Time To First Token,首字延迟)。
    • 用户感知 :屏幕上是空白,直到这一刻,突然跳出第一个字"要"。

KV-Cache 的好处

不用重复生成以往 Token 的 KV 键值对了,直接从 Cache 中提取出来和当前 Token 的 Q 算点积,然后加权求和即可。

3. Prefill 流程的例子

虽然 Prompt 的 Q、K、V 的生成是并行的,但是最后一个 Token(Q) 会和所有 Token(K) 进行计算,然后加权求和(投影到所有 Token 信息量上),得到的向量再给到 FFN 输出然后 Softmax 采样投影到词表上,即为下一个 Token 的生成。

Prefill 和 Decode 的区别如下所示:

3.1 输入层 (Embedding)

用户输入 8 个词,经过 Embedding 后变成了一个巨大的矩阵。

  • Shape: [8, 4096]
3.2 生成 Q, K, V (Linear Projection)

矩阵一次性乘以三个权重矩阵 W q , W k , W v W_q, W_k, W_v Wq,Wk,Wv(形状均为 [4096, 4096])。

  • Q Shape: [8, 4096]
  • K Shape: [8, 4096]
  • V Shape: [8, 4096]

关键点: 这 8 个位置的向量在此时确实是"各跑各的"。

3.3 计算 Attention (核心聚合步骤)

这是"信息聚合"发生的地方。计算 Q × K T Q \times K^T Q×KT:

  • 运算: [8, 4096] @ [4096, 8]
  • Attention Score Shape: [8, 8]

因果掩码 (Causal Mask): 这是一个 8 × 8 8 \times 8 8×8 的下三角矩阵。

  • 第一行只有 1 个有效值(只能看自己)。
  • 第八行有 8 个有效值(能看前面所有人)。
3.4 加权求和 (V-Aggregation)

[8, 8] 的注意力得分去乘以 V 矩阵 [8, 4096]

  • 运算: [8, 8] @ [8, 4096]
  • Output Shape: [8, 4096]

深度剖析: 虽然形状还是 [8, 4096],但此时第 8 行 的那个向量,已经通过矩阵乘法,融合了 V 1 V_1 V1 到 V 8 V_8 V8 的所有信息。它就是你说的"吸干精华"的那个向量。

3.5 存储 KV Cache

此时,模型会将刚才算出的 KV 存入显存。

  • KV Cache Shape (每层): [8, 4096] (K 矩阵) 和 [8, 4096] (V 矩阵)。
3.6 预测首字 (LM Head)

模型并不在乎前 7 个位置的输出,它只切出最后一行

  • Slice: [1, 4096] (这是第 8 个位置的输出状态)。
  • LM Head Projection: [1, 4096] @ [4096, 128000](词表大小)。
  • Probabilities Shape: [1, 128000]
  • 结果: 采样得到第 9 个 Token(第 1 个生成的字)。
3.7 总结:Shape 的变迁表
阶段 矩阵操作 / 描述 输出 Shape 备注
Input Embedding [8, 4096] 8个初始向量
QKV Gen X × W X \times W X×W Q, K, V[8, 4096] 并行线性变换
Attention Q × K T Q \times K^T Q×KT [8, 8] 因果关联发生处
Context S c o r e × V Score \times V Score×V [8, 4096] 信息聚合完毕
Cache 写入显存 K, V[8, 4096] 为后续 Decode 存底
Logits 仅取最后一位映射 [1, 128000] 准备吐出第 1 个字

问题:既然是并行的话,为什么长输入模型处理仍然很慢?

这个问题其实分为两个问题:

  • 并行的话,为什么长输入模型处理仍然很慢?
  • 为什么输入越长,消耗的显存就越大?

答案就在才推导的那个 [8, 8] 的注意力得分矩阵(Attention Score Matrix)里(第三步)。让我们把这个数字放大,看看长文本下发生了什么:

为什么会变慢?& 显存消耗大:

  1. 计算复杂度 O ( N 2 ) O(N^2) O(N2): 随着序列长度 N N N 的增加,计算量不是线性增长,而是翻倍增长。100k 的输入比 1k 的输入长了 100 倍,但计算量却增加了 10,000 倍。
  2. 显存压力: 这个 [N, N] 的矩阵需要临时存储在显存中(即便使用了 FlashAttention 这种优化技术,中间变量的开销依然巨大)。当 N N N 极大时,单张显卡的显存会被这个矩阵直接撑爆。(虽然算完后就会删掉这个 [N, N] 的矩阵),同时 KV-Cache 作为长期资产也会消耗极大的显存

注意: [N, N] 的注意力矩阵算完后就会删除,因为我们只需要获取 Prompt 最后一个 Token 对前面 Token 的注意力权重 (shape[1, dim]);以此获得下一个 Token 的预测。

内存带宽瓶颈(Memory Wall)

虽然 GPU 算矩阵乘法很快,但 Prefill 阶段还需要把算出来的每一个 K K K 和 V V V 写入显存(即创建 KV Cache),这对 IO 吞吐量的要求很大(挑战带宽极限)。

  • 当 N N N 非常大时,模型需要一次性往显存里写入海量数据。
  • 比如一个 128k 长度的 Prefill,在 Llama 3-8B 上产生的 KV Cache 大约有几个 GB。虽然听起来不大,但 GPU 需要在极短的时间内完成大规模并发写入,这会撞上 显存带宽(Bandwidth) 的天花板。

4. Decode 的概念

在 Decode 阶段,模型不再一次性处理所有 Token,而是一次只处理一个 Token,生成之后再将该 Token 作为下一次的输入。这就是所谓的**自回归(Autoregressive)**生成。

4.1 Decode 的核心流程

假设我们在 Prefill 阶段已经输出了第 1 个字"要",现在我们要生成第 2 个字。

  1. 输入: 只有刚才生成的那个字"要" (Shape: [1, 4096])。
  2. 计算: 计算"要"的 QKV
  3. KV Cache 复用(核心!)
    • 模型不去重新计算前 8 个 Token 的 K, V,而是直接从显存中读取 KV Cache ([8, 4096])。
    • 将"要"产生的 K_9, V_9 拼接到 Cache 后面,更新为 [9, 4096]
  4. Attention 聚合:
    • 仅用"要"的 Q_9 与全部 K (1-9) 做点积。
    • 得到一个长度为 9 的注意力权重向量 [1, 9]
    • 用这个权重去加权求和所有的 V (1-9),得到一个融合了上下文信息的向量 [1, 4096]
  5. 输出预测: 经过 FFN 和 LM Head,映射到词表概率,采样得到第 2 个字(比如"学")。

4.2 Prefill vs Decode 的本质差异

维度 Prefill (预填充) Decode (解码)
输入规模 [N, 4096] (并行全量) [1, 4096] (逐个迭代)
计算模式 计算密集型 (Compute-Bound) 访存密集型 (Memory-Bound)
GPU 状态 核心满载,并行矩阵乘法 核心闲置,主要在搬运数据
KV Cache 从无到有,批量写入 逐行追加,读多写少

4.3 为什么 Decode 阶段会面临"显存带宽墙"?

这是你前面提到的疑问,在 Decode 阶段表现得尤为明显:

  • 算力利用率极低 (MFU 低): 在 Decode 时,为了生成一个词,GPU 需要把模型的所有参数(比如 8B 的参数,约 16GB)从显存搬运到计算单元,就为了做一次加乘。为了蹦一个字,要搬运 16GB 的数据。这导致 GPU 的 Tensor Core 大部分时间都在等数据从显存搬运过来。
  • 带宽就是生命线: 如果你有 100 个用户同时在对话,GPU 就需要同时维护 100 份 KV Cache。在 Decode 阶段,GPU 需要在极短时间内读出这 100 份 Cache 并进行计算,如果显存带宽不够,用户的生成速度(Tokens Per Second, TPS)就会直线下降,甚至出现明显的"卡顿"。

没问题,我们抛弃厨房做菜这种比喻,直接进入 GPU 硬件架构与访存的硬核底层逻辑

要理解为什么 Decode 阶段会卡在"搬运数据"上,必须看透 "算力(FLOPS)"与"显存带宽(Bandwidth)"的物理不对称性


4.3.1 物理层面的"算力 vs 带宽"陷阱

现代 GPU 的设计目标是处理大规模并行计算,而不是为了处理这种"高频、低计算量"的自回归推理。

  • 计算能力 (FLOPS): NVIDIA H100 的 FP16 计算能力高达 2000 TFLOPS (每秒 2000 万亿次运算)。
  • 显存带宽 (Bandwidth): H100 的 HBM3 带宽约为 3.3 TB/s (每秒 3.3 万亿字节)。

核心瓶颈公式:

Decode 阶段,针对一个 Token 的计算量是极小的,但为了完成这个计算,你必须遍历整个模型权重

计算强度 (Arithmetic Intensity) = 总计算量 (FLOPs) 总访存量 (Bytes) \text{计算强度 (Arithmetic Intensity)} = \frac{\text{总计算量 (FLOPs)}}{\text{总访存量 (Bytes)}} 计算强度 (Arithmetic Intensity)=总访存量 (Bytes)总计算量 (FLOPs)

当这个强度低于某个阈值(Roofline Model 的临界点)时,GPU 的 Tensor Core 就会因为还没拿到数据而进入"等待(Stall)"状态。这就是你感受到的"算力利用率低"。


4.3.2 为什么 Decode 阶段必须搬运所有权重?

你在处理 Decoder-only Transformer(如 Llama/GPT)时,其结构是固定的:

  1. 逐层处理: 输入必须经过第 1 层、第 2 层... 直到第 L L L 层。
  2. 权重不可丢弃: 在计算第 i i i 层的隐藏状态时,必须使用该层对应的 W q , W k , W v W_q, W_k, W_v Wq,Wk,Wv 以及 W f f n W_{ffn} Wffn 矩阵。
  3. 计算逻辑: 由于这是一个串行过程(这一层的输出是下一层的输入),计算单元(ALU/Tensor Core)在处理第 i i i 层时,第 i i i 层的权重必须已经在计算单元的缓存(SRAM)中。

当处理完第 i i i 层后,这部分权重会被换出,立即搬入第 i + 1 i+1 i+1 层的权重。这种逐层加载模式,意味着无论你生成多少 Token,每一轮 Decode 都要把整个模型参数搬一遍。


4.3.3 KV Cache 的带宽开销

在之前的对话中,我们确定了 N × N N \times N N×N 的 Attention 矩阵在 FlashAttention 下实现了"算完即焚",不需要长期占用带宽。但是,KV Cache 是不同的:

  • 读写循环: 每一轮 Decode,GPU 必须:
  1. 从显存读取 KV_1KV_{N-1} 的历史记录(带宽消耗 O ( N × d ) O(N \times d) O(N×d))。
  2. 计算当前 KV_N
  3. 将更新后的 KV_1KV_N 写回显存(带宽消耗 O ( N × d ) O(N \times d) O(N×d))。
  • 随着上下文增长: N N N 越大,每一轮 Decode 必须搬运的 KV 数据就越多。当 N N N 达到 128k 时,每生成一个 Token,你需要从显存中搬运几 GB 的数据,这直接导致了你看到的"TPS 下降"。

4.3.4 为什么会有"卡顿"?

当 100 个用户并发时,我们使用了 Continuous Batching (连续批处理)

  • 调度层: 我们将 100 个用户的请求放在一个 Batch 里,GPU 一次性搬运 16GB 的模型权重。
  • 物理层: 由于带宽是有限的,GPU 必须在 100 个请求之间频繁切换以完成计算。如果显存带宽被 100 个用户的 KV Cache 读写填满,计算单元(Tensor Cores)就会因为缺乏下一层的权重数据而空转。

这就是所谓的**"带宽墙 (Memory Wall)"**。


总结

  • 算力利用率低是因为计算单元处理一个 Token 所需的运算量(FLOPs)太小,远少于它在相同时间内能完成的上限。
  • 带宽瓶颈是因为模型参数量(16GB+)太大,相对于每秒能搬运的极限(3.3 TB/s),它拖慢了处理下一个 Token 的速度。

既然这是硬件架构决定的"物理限制",现在的工程优化手段(如量化 Quantization、Speculative Decoding 投机采样)本质上都是在尝试突破这个限制。你认为通过"压缩数据量(量化)"和"减少搬运次数(投机采样)",哪一种方式对解决你目前关注的瓶颈更有效?

相关推荐
YaraMemo2 小时前
向量求导规则
人工智能·机器学习·矩阵·信号处理
用户5191495848452 小时前
WordPress Top Store 主题高危漏洞利用工具 (CVE-2024-10673)
人工智能·aigc
琛説2 小时前
Web-Rooter:一种 IR + Lint 模式的 AI Agent 创新尝试【或许是下一个 AI 爆火方向】
前端·人工智能
nimadan122 小时前
**豆包seed写剧本2025指南,AI编剧工具实战应用解析**
人工智能·python
新缸中之脑2 小时前
分发:AI的终极护城河
人工智能
databook2 小时前
在AI的世界里,做一个真实的人
人工智能·程序员·创业
新缸中之脑2 小时前
Anthropic报告:AI对就业的影响
大数据·人工智能
V搜xhliang02462 小时前
VLA 模型微调与 ROS 2 集成
人工智能·深度学习·计算机视觉·自然语言处理·知识图谱
itpretty2 小时前
手搓一只迷你小龙虾(Claude Code CLI + Telegram)
人工智能·ai编程·claude