yuzheng
← 全部笔记

翻译与阅读笔记

图解大模型推理:从 Prefill 到 Decode

沿分词、注意力、KV Cache 和解码的执行路径,理解推理延迟与吞吐瓶颈。

修改文字

沿一次请求理解推理开销

一次大模型生成请求可以拆成输入处理、Prefill、逐步 Decode 和输出采样。它们共享模型权重,却有不同的数据规模和计算特征。本文依据已有图解译稿整理,沿这条执行路径说明 KV Cache、注意力架构、量化和服务调度分别影响哪些开销。

Prefill 常呈现较高的计算密度,小批量 Decode 则经常受权重与 KV 的读取带宽限制。这是分析的起点,而不是所有模型和批大小下都成立的固定分类;实际瓶颈仍需要测量。

1. 分词与向量嵌入

BPE(字节对编码,Byte Pair Encoding)等分词器负责将原始文本转换为词表中的整数 ID。词表大小由模型与分词器决定,下面用 50,000 作为示例。

# 1. 文本输入:原始自然语言提示词 Prompt
prompt = "How does inference work?"

# 2. 分词编码:BPE 分词器将文本子词切分为离散词表中的整数 Token ID
# 词表规模约为 50,000,每个子词对应唯一的一个整数标识符
ids = tokenizer.encode(prompt)  # 输出示例 -> [2437, 1374, 32278, 670, 30]

每个 Token ID 对应嵌入表(Embedding Table)中的一行。嵌入表是一个形状为 [vocab_size, hidden_dim] 的可学习矩阵。以隐层维度为 4,096 的模型为例,每个 Token 均映射为一个 4,096 维的连续稠密向量。

# 1. 嵌入表规格:形状为 [vocab_size, hidden_dim],存储各 Token 的稠密语义特征
# 对于 hidden_dim = 4096 的大模型,嵌入表为 50000 x 4096 的大型权重矩阵

# 2. 向量检索:通过整型索引直接从嵌入表中切片提取对应行向量
# 单次查表将 [num_tokens] 的 ID 列表映射为二维浮点张量 [num_tokens, 4096]
vectors = embedding_table[ids]   # 输出张量形状: [num_tokens, 4096]

Embedding Table Mapping

图 1 Token ID 通过嵌入表映射为固定维度的稠密向量序列

模型还需要位置信息。采用 RoPE(旋转位置编码) 的 Transformer 会在注意力层中对 Query 和 Key 应用与位置相关的旋转;它不等同于直接旋转词嵌入表中的向量。

2. Transformer 计算层

完成嵌入的序列将依次穿过多层堆叠的 Transformer 结构(通常为 32 到 80+ 层,具体取决于模型规模)。

每一层顺序执行以下两项核心操作:

1) 自注意力机制

通过可学习的权重矩阵,为每个 Token 分别计算 Query(Q)、Key(K)和 Value(V)三组投影向量。

Attention Q K V Projections

图 2 每个 Token 投影生成 Q、K、V 向量并计算注意力打分矩阵

每个 Token 的 Query 会与其余所有 Token 的 Key 进行点积打分。这些分数经过除以 sqrt(d_k) 缩放并经 Softmax 归一化后,决定了各个 Token 的 Value 参与融合的权重比例:

# 1. Q、K、V 投影:利用可学习权重矩阵完成线性映射
# x 为输入特征序列,Wq, Wk, Wv 分别为主注意力投影矩阵
Q, K, V = x @ Wq, x @ Wk, x @ Wv

# 2. 注意力打分:计算 Query 与 Key 之间的点积相似度,并除以 sqrt(d_k) 缩放防止梯度爆炸
scores = (Q @ K.T) / sqrt(d_k)

# 3. 权重归一化:Softmax 作用在最后一维,每一行权重之和严格等于 1
weights = softmax(scores)        # 形状: [num_tokens, num_tokens]

# 4. 加权聚合:依据注意力权重将各个 Token 的 Value 向量加权混合
attn_output = weights @ V        # 最终产出融入上下文关联的新表征向量

2) 前馈神经网络

通过两层 MLP 对每个 Token 的向量独立进行非线性变换。一句话概括二者分工: Attention 负责在不同 Token 位置之间传递信息,FFN 负责在位置内部完成高阶特征变换。

通过最后一层 Transformer 后,模型将序列末位 Token 的隐藏状态重新投影回词表维度([hidden_dim, vocab_size]),经过 Softmax 转换概率后采样生成首个输出 Token。

3. Prefill 阶段:计算受限

处理输入 Prompt 是推理的第一阶段。在该阶段中, 所有输入 Token 均被并行处理 :模型同时计算全部 Token 的 Q、K、V 投影,注意力运算表现为高密度的大规模矩阵相乘(GEMM)。

该阶段属于典型的 计算受限(Compute-bound) 任务。此时 GPU 的算术吞吐单元是主要性能瓶颈,算力利用率通常维持在高位。衡量该阶段性能的核心指标是 TTFT(Time to First Token,首字时延) ,即自发送请求起至产生首个输出 Token 为止的延迟。

在 Prefill 过程中,模型还会建立 KV Cache :每一层生成的 K 和 V 张量均会被持久存储在显存中以备后续解码复用。

# ==============================================================================
# Prefill(预填充)阶段:一次性并行计算整个输入 Prompt(计算受限 Compute-Bound)
# ==============================================================================

# 1. 嵌入与位置编码:查表获取 Prompt 全部 Token 的向量表示,并注入 RoPE 旋转位置信息
hidden = embed(prompt_tokens) + positions

# 2. 逐层前向传递:遍历模型所有 Transformer 层(通常 32~80+ 层)
for layer in model.layers:
    # 步骤 ①:同时并行计算当前层所有 Token 的 Q、K、V 投影矩阵
    Q, K, V = project(hidden)             # 矩阵规模: [num_prompt_tokens, hidden_dim]

    # 步骤 ②:多头自注意力计算并叠加残差连接
    hidden  = attention(Q, K, V) + hidden

    # 步骤 ③:FFN(前馈网络双层 MLP)非线性变换并叠加残差
    hidden  = feedforward(hidden) + hidden

    # 步骤 ④【核心机制】:将当前层计算的全部 K、V 写入 GPU 显存中的 KV Cache 供后续复用!
    cache_kv(layer, K, V)

# 3. 首字采样:仅提取序列末位 Token 的隐藏状态,投影至词表空间并采样得到第 1 个输出 Token
first_token = sample(project_to_vocab(hidden[-1]))

4. Decode 阶段:访存受限

首个 Token 生成完毕后,模型转入自回归解码循环,每次迭代仅生成 1 个新 Token。对于每一个新生成的 Token,模型仅需为其自身计算单步的 Q、K、V 向量,而所有历史 Token 的 K 和 V 已经存在于 KV Cache 中。

# ==============================================================================
# Decode(解码)阶段:逐字自回归迭代生成(访存受限 Memory-Bound)
# ==============================================================================

token = first_token
steps = 0

# 自回归生成循环:一次循环产出 1 个 Token,直至遇到结束符 STOP 或达到最大步数
while token != STOP and steps < MAX_STEPS:
    # 1. 仅为当前这单个最新 Token 进行嵌入与位置编码
    x = embed(token) + position(steps)

    # 2. 逐层前向传播:
    for layer in model.layers:
        # 步骤 ①:仅计算当前单 Token 的投影向量 q, k, v(计算量极小)
        q, k, v = project(x)

        # 步骤 ②【核心机制】:将当前步的 k, v 增量追加至历史缓存,直接获取完整上下文历史
        K_all, V_all = caches[layer].append(k, v)  # 拼接历史已缓存的所有 Token K/V

        # 步骤 ③:当前单个 q 与整段历史 K_all 做点积注意力,计算 FFN,累加残差
        # 硬件瓶颈:计算量虽小,但每一步都必须完整从显存重新加载全部模型权重与全部 KV Cache!
        x = layer.forward(q, K_all, V_all, x)

    # 3. 采样并流式吐字:
    token = sample(project_to_vocab(x))
    steps += 1
    yield token  # 立即流式返回给用户界面

在 Decode 单步中,算术计算量极小(仅计算单个 Query 向量与历史 Key 矩阵的矩阵向量乘,而非大矩阵乘法)。然而,GPU 依然必须从高带宽显存(HBM)中完整读取整套模型权重矩阵以及累积的全部 KV Cache 数据。 此时系统瓶颈由算力翻转为显存带宽(Memory Bandwidth)。

Memory Bandwidth Bottleneck in Decode

图 3 Decode 阶段仅做少量矢量运算,却需频繁从显存搬运庞大权重与 KV Cache,导致带宽成为瓶颈

衡量该阶段的核心性能指标是 ITL(Inter-Token Latency,Token 间延迟) ,即连续两个输出 Token 之间的时间间隔。保持较低的 ITL 是保证流式打字效果流畅的关键。

5. KV Cache:原理与显存代价

若不采用 KV 缓存,每生成 1 个新 Token 都需要对整个逐步增长的历史序列重新计算注意力,生成 1,000 个 Token 将引发二次方级(O(N2)O(N^2))的巨大算力浪费。

KV Cache 将每一层计算过的 K 和 V 状态保存于显存中,后续步骤仅需增量追加。在长文本生成场景下,KV Cache 可带来 5 倍以上的端到端推理提速 。

观看原文演示视频

视频 开启与未开启 KV Cache 时的大模型推理速度直观对比(速度差异超 5 倍)

但这种加速并非毫无代价: Cache 显存占用随序列长度呈线性增长,且在每一层均独立分配 。以 13B 参数量模型为例:

  • 每个 Token 平均消耗约 1 MB 显存;
  • 一个 4,000 Token 的上下文,仅 KV Cache 就需独占 4 GB 显存。

这正是长上下文成本昂贵的核心原因: KV Cache 显存与 Batch 并发请求直接争抢物理显存 。单个请求占用的 Cache 越多,单张 GPU 允许同时处理的并发请求数就越少。

🤔 经典面试思考题:
使用 vLLM 部署推理模型时,遇到长推理序列频发 OOM(显存溢出)。若此时引入 KV Cache 压缩算法并驱逐了 90% 的缓存 Token,却发现物理显存占用未见下降且依然发生 OOM,原因何在?
(提示:vLLM 的内存管理采用预分配机制,物理 Block 在释放后可能仍保留在预分配池中,或由于激活值峰值内存与分页碎片引发 OOM)

业界主流缓解策略包括:

  1. Cache 量化 :将 KV 精度压缩至 INT8 或 INT4;
  2. 滑动窗口注意力(Sliding Window Attention) :丢弃超出固定窗口的历史缓存;
  3. GQA(分组查询注意力,Grouped-Query Attention) :多个 Query 头共用一组 K/V 头,大幅精简缓存张量数量;
  4. PagedAttention :类似操作系统虚拟内存分页机制,以 Block 为单位动态分配 Cache,彻底消除了显存碎片。

6. 围绕 Cache 重新设计注意力架构

量化与分页主要是将 KV Cache 视为“固定成本”来进行工程调度管理。而 DeepSeek 在其架构设计中采取了全新路径: 直接重构注意力机制,使 Cache 在模型结构层面原生缩减。

DeepSeek V4 Attention Redesign

图 4 采用结构级压缩注意力机制原生缩减 KV Cache 体积

该类方案通常采用两类压缩注意力的混合架构:

  • CSA(压缩稀疏注意力,Compressed Sparse Attention) :通过 Softmax 门控池化将 KV 状态压缩 4 倍,并在压缩后的 Token 上施加稀疏注意力;
  • HCA(高压缩注意力,Heavily Compressed Attention) :更为激进,将连续 128 个 Token 的 KV 状态聚合成单个压缩表征,在此之上执行密集注意力计算。

在 1M 上下文长序列下,采用该架构的单 Token 推理 FLOPs 仅需 27%,而 KV Cache 占用仅为传统架构的 10%。单序列 KV Cache 从 83.9 GiB 锐减至 9.62 GiB(BF16 精度)。若进一步叠加 FP4/FP8 量化,显存占用还可再降低一半。

KV Cache 已经成为当前大模型架构演进中最核心的约束条件。

7. 权重量化

模型训练时必须依赖 FP32 或 BF16 保证梯度数值稳定性,但在推理阶段则完全不需要如此高的数值位宽。位宽压缩带来的显存收益严格呈线性比例:

Quantization Memory Footprint

图 5 7B 参数量模型在不同精度下的显存占用对比

精度格式单参数位宽7B 模型显存占用适用场景
FP3232-bit (4 Bytes)28 GB模型预训练与全精度基准校验
FP16 / BF1616-bit (2 Bytes)14 GB标准微调与数据中心级原生部署
INT88-bit (1 Byte)7 GB主流服务端高性价比部署(几乎无损)
INT44-bit (0.5 Byte)3.5 GB端侧/笔记本 4~6GB 显存设备轻量运行

现代量化方案(如 GPTQ、AWQ)引入逐通道缩放因子(Per-channel Scaling Factors),将有损压缩带来的准确率下降控制到极低。高质量的 INT4 模型在主流基准测试上与全精度模型差距通常仅在 1%~2% 以内。从 FP16 转为 INT8 通常可在基本无损的前提下使推理延迟减半,是部署中 收益率最高的优化手段 。

8. 推理服务架构

现代推理服务框架(如 vLLM、TensorRT-LLM、TGI)围绕 Prefill-Decode 循环,引入了多项关键工程优化:

Continuous Batching System

图 6 连续批处理(Continuous Batching)在同一个计算调度步内交织不同请求的 Token

  • 连续批处理(Continuous Batching) :在同一个 GPU 调度步内交叉执行多个请求的不同 Token,在访存受限的 Decode 阶段充分利用空闲算力。
  • 推测解码(Speculative Decoding) :利用轻量小草稿模型先行投机生成多个候选 Token,再交由大模型单次前向并行校验。当命中率高时,可将多次串行 Decode 转化为单次并行验证。

Speculative Decoding Verification

图 7 推测解码:小模型快速生成候选项,大模型单次批量并行验证

  • PagedAttention :以固定大小的虚拟内存块管理 KV Cache,消除碎片,使得单卡并发请求容量大幅扩充。

上述技术使单张 GPU 能够轻松服务数十个并发用户:Decode 阶段让大量算术计算单元闲置,而 Continuous Batching 恰好将其他请求的计算负载填入这些空闲空间中。

9. 完整推理链路流程梳理

Full Inference Pipeline Map

图 8 大语言模型端到端推理数据链路完整示意图

  1. Tokenize(分词) :文本通过 BPE 转换为整型 Token ID 序列。
  2. Embed(向量嵌入) :Token ID 查表转为连续向量,RoPE 注入旋转位置特征。
  3. Prefill(预填充) :一次性并行穿过所有网络层,计算受限,构建初始 KV Cache 并产出首个 Token。
  4. Decode 循环(自回归解码) :单步迭代:为新 Token 投影 Q 向量,在缓存的 K/V 上做注意力,经过 FFN 与采样,将最新 K/V 写入 Cache。访存受限。
  5. Detokenize(逆分词) :将生成的 Token ID 反查还原为文本字符并以流式方式返回。

10. 工程落地实践启示

  • 长 Prompt 消耗 TTFT(Prefill 耗时) ; 长 Output 消耗 ITL(Decode 耗时) ,两者压迫完全不同的硬件单元。
  • 上下文长度并不廉价 :它会急剧膨胀 KV Cache 并直接扼杀系统批处理并发能力。
  • 警惕算力利用率陷阱 :满载服务时 GPU 计算单元利用率可能仍跌至 30%,因为真实瓶颈在显存带宽。盲目提升算力徒劳无功,加速显存、压缩 Cache 和改善批调度才是根治之道。
  • 核心诊断法则 :当用户反馈模型缓慢时,第一步先判定是 响应首字慢(Prefill 受限,优化 TTFT) ,还是 流式输出慢(Decode 受限,优化 ITL) 。
← 返回笔记列表回到顶部 ↑