Skip to content

Transformer从张量计算到KV Cache完整原理

Transformer 是现代大语言模型和许多多模态模型的核心结构。只记住“Q 是查询、K 是键、V 是值”还不够,因为真正理解需要回答:

  • 一批 Token 怎样变成三维张量?
  • Q、K、V 是复制输入,还是通过可训练矩阵投影得到?
  • QKᵀ 每个元素代表什么,为什么要除以 sqrt(dHead)
  • Causal Mask 为什么必须在 Softmax 之前加入?
  • 多头注意力怎样拆分、拼接和投影?
  • Attention、FFN、残差和归一化怎样组成一个 Block?
  • 训练时为什么能一次计算多个位置,生成时却必须逐 Token?
  • KV Cache 缓存什么,为什么既提速又占显存?
  • 长上下文怎样影响计算量、内存、TTFT、TPOT 和并发?

本章从张量形状开始,把一个 Decoder-only Transformer 从输入到训练、Prefill、Decode 和生产排查完整串起来。

一、学习目标

学完后,你应该能够:

  • 解释 Encoder-only、Decoder-only、Encoder-Decoder 的适用方向。
  • 写出 B × S × DH × dHead 等维度含义。
  • 推导单头 Attention 的投影、打分、Mask、Softmax 和 Value 聚合过程。
  • 区分 Padding Mask、Causal Mask 和业务权限过滤。
  • 解释 MHA、MQA、GQA 的 K/V 头差异及缓存影响。
  • 解释 FFN、激活函数、残差、Pre-Norm 和 Post-Norm。
  • 解释训练阶段的 Teacher Forcing 和位置并行。
  • 解释 Prefill、Decode 和 KV Cache 的时间空间取舍。
  • 根据模型配置估算单请求 KV Cache 理论下界。
  • 运行并修改本页的 Masked Multi-Head Attention Demo。
  • 排查长上下文 OOM、首 Token 慢、后续 Token 慢和输出异常。

二、Transformer有哪些结构类型

原始 Transformer 是 Encoder-Decoder 结构,现代模型根据任务采用不同部分。

类型可见上下文常见方向关键特征
Encoder-only通常可双向看输入分类、表示、Embedding重点理解完整输入,不负责自回归长文本生成
Decoder-only每个位置只能看自己及之前位置大语言模型生成Causal Self-Attention,逐Token生成
Encoder-DecoderEncoder双向编码,Decoder因果生成并Cross-Attention翻译、摘要、结构转换输入编码和输出生成分开
mermaid
flowchart TD
    A["输入序列"] --> B{"模型结构"}
    B -- "Encoder-only" --> C["双向表示后做分类或向量"]
    B -- "Decoder-only" --> D["因果注意力后预测下一个Token"]
    B -- "Encoder-Decoder" --> E["Encoder处理输入"]
    E --> F["Decoder读取编码结果并生成输出"]

本章重点讲 Decoder-only,因为它最能解释聊天大模型的训练、生成和 KV Cache。具体模型可能使用 RoPE、RMSNorm、SwiGLU、GQA 等变体,不能把某一种实现说成所有 Transformer 的唯一实现。

三、先统一张量符号

理解 Attention 前先固定符号:

符号含义教学示例
BBatch Size,一次处理多少条序列2
SSequence Length,每条序列多少个Token4
DHidden Size,每个Token隐藏向量维度8
HqQuery Head数量2
HkvKey/Value Head数量2
dHead每个头维度,常见为 D / Hq4
Vocab词表大小50,000
LTransformer层数32

Tokenizer 得到 Token ID:

text
tokenIds shape = [B, S]

查 Token Embedding 并加入位置信息后:

text
hiddenStates shape = [B, S, D]

例如 B=2、S=4、D=8,张量里有 2 × 4 × 8 = 64 个数。每个 Batch 有 4 个 Token,每个 Token 用 8 维向量表示。

真实模型的 D 可能是数千甚至更大,但形状逻辑相同。后文所有矩阵计算都要能对上维度,维度对不上通常意味着实现或配置错误。

四、文本怎样进入Transformer

mermaid
flowchart TD
    A["文本"] --> B["Tokenizer生成Token ID"]
    B --> C["Token Embedding查表"]
    C --> D["加入或作用位置信息"]
    D --> E["得到B×S×D隐藏状态"]
    E --> F["进入第一个Transformer Block"]

4.1 Token ID只是索引

Token ID 的数字大小没有“语义更大”的含义。ID 1024 只是词表第 1024 行的索引,不能直接拿 ID 做距离比较。

4.2 Token Embedding是一张可训练矩阵

text
EmbeddingTable shape = [Vocab, D]

每个 Token ID 取出一行 D 维向量。训练初期向量没有人类定义的固定语义,反向传播会根据预测任务调整矩阵。

4.3 Token Embedding与RAG Embedding不同

类型输出粒度主要用途
Token Embedding每个Token一条内部隐藏向量Transformer内部计算
文本 Embedding句子、段落、图片等一个可检索向量相似度检索和聚类

不能把语言模型内部某一层某个 Token 的隐藏状态直接等同于经过专门训练和池化的 RAG 文本向量。

五、位置为什么必须被编码

没有位置信息时,Self-Attention 对输入排列本身不天然敏感,难以区分:

text
我 打 你
你 打 我

5.1 常见位置方法

方法方向核心思路边界
绝对位置向量给每个位置加入独立或函数生成的向量直接表达位置,超训练长度泛化有限
相对位置偏置在注意力分数中加入相对距离信息强调Token之间距离
RoPE对Q、K的维度对按位置做旋转点积中自然携带相对位置关系

5.2 RoPE不是把位置向量简单加到输入

RoPE 常作用在 Q 和 K 上。不同位置使用不同角度旋转维度对,使旋转后 Q、K 的点积包含相对位置信息。V 通常不以同样方式旋转。

理解方向:

text
同一个Token内容
+ 不同位置对应不同旋转
→ Q/K匹配同时考虑内容与相对位置

RoPE 的上下文扩展、频率缩放和插值依赖具体模型与训练方式。把配置中的最大长度直接调大,不代表模型在超训练长度上仍保持相同质量。

六、Q、K、V究竟怎样得到

输入隐藏状态记作:

text
X shape = [B, S, D]

单头教学形式:

text
Q = XWq
K = XWk
V = XWv

若投影到 dHead

text
Wq, Wk, Wv shape = [D, dHead]
Q, K, V shape    = [B, S, dHead]

真实多头实现常用一个或少量大矩阵一次投影,再 reshape 成多个头,并不一定真的循环调用 H 次小矩阵乘法。

6.1 为什么不能说Q、K、V就是原输入复制三份

Q、K、V 来自同一个 X,但经过不同可训练矩阵:

  • Q 学习当前位置“寻找什么匹配特征”。
  • K 学习每个候选位置“提供什么匹配特征”。
  • V 学习候选被选中后“贡献什么内容表示”。

同一个 Token 在不同上下文、不同层、不同头中的 Q/K/V 都可能不同。

6.2 Q、K、V类比的边界

“查询、索引、内容”的类比有助入门,但 Attention 不是数据库精确查询:

  • 分数是连续相似度,不是主键匹配。
  • 权重通常分配给多个位置。
  • 投影矩阵由训练学出,不是人工字段定义。
  • Attention 权重不自动等于因果解释或事实证据。

七、单头Self-Attention完整数值过程

7.1 第一步:计算相关性分数

text
Scores = QKᵀ

如果 Q、K shape=[B,S,dHead]

text
Kᵀ shape   = [B,dHead,S]
Scores shape = [B,S,S]

Scores[b,i,j] 表示第 b 条序列中,位置 i 的 Query 与位置 j 的 Key 的点积分数。

7.2 第二步:缩放

text
ScaledScores = Scores / sqrt(dHead)

若 Q、K 各维大致独立、方差相近,点积方差会随维度增加。dHead 大时分数绝对值容易变大,Softmax 更快进入接近 one-hot 的饱和区域,小概率位置梯度很弱。除以 sqrt(dHead) 用于稳定分数尺度。

7.3 第三步:加入Mask

Decoder 因果遮罩要求位置 i 不能看到 j > i 的未来位置。实现方向是把不可见分数替换为足够大的负值:

text
位置0:[允许, 屏蔽, 屏蔽, 屏蔽]
位置1:[允许, 允许, 屏蔽, 屏蔽]
位置2:[允许, 允许, 允许, 屏蔽]
位置3:[允许, 允许, 允许, 允许]

在 Softmax 前加入负无穷方向的值,指数后趋近 0,归一化权重也为 0。

7.4 第四步:Softmax

对每个 Query 所对应的一行分数做 Softmax:

text
Weights[i,j] = exp(score[i,j]) / Σj exp(score[i,j])

每行可见位置权重之和约等于 1。数值实现通常先减去该行最大值,避免 exp 溢出。

7.5 第五步:加权聚合Value

text
Context = Weights × V
text
Weights shape = [B,S,S]
V shape       = [B,S,dHead]
Context shape = [B,S,dHead]

位置 i 的输出是所有可见 Value 的加权和,因此它融合了上下文信息。

mermaid
flowchart TD
    A["X乘Wq、Wk、Wv"] --> B["得到Q、K、V"]
    B --> C["Q乘K转置得到S×S分数"]
    C --> D["除以sqrt dHead"]
    D --> E["加入Causal或Padding Mask"]
    E --> F["按行Softmax"]
    F --> G["权重乘V"]
    G --> H["得到上下文表示"]

八、Mask有哪些,不能混为什么

8.1 Causal Mask

阻止当前位置查看未来 Token,保证训练目标与自回归生成一致。

8.2 Padding Mask

Batch 中序列长度不同时常补 Padding。Padding 不是有效内容,应阻止其他 Token 关注它。某些实现使用 Attention Mask 表示有效位置,框架再转成内部遮罩。

8.3 业务ACL不是Attention Mask

租户、角色、部门和数据密级过滤必须在 RAG 检索或业务查询阶段完成。不能把越权文档先放进模型上下文,再期望 Attention Mask 或 Prompt 隔离。Attention Mask 控制张量位置可见性,不是企业权限系统。

8.4 Mask放错位置会怎样

  • Softmax 后才把某些权重置零但不重新归一,权重和不再为 1。
  • Mask 方向反了,模型可能只看未来或屏蔽历史。
  • 全部位置被屏蔽,Softmax 可能产生 NaN,具体行为依实现而异。
  • Padding 未屏蔽,模型会关注无效占位。
  • 低精度下直接使用不合适的极小值,可能出现数值问题。

九、多头注意力怎样拆分和合并

假设:

text
D = 4096
Hq = 32
dHead = 128

因为:

text
Hq × dHead = D

投影并 reshape 后:

text
Q shape = [B, Hq, S, dHead]
K shape = [B, Hkv, S, dHead]
V shape = [B, Hkv, S, dHead]

普通 MHA 中 Hkv = Hq。每个 Query Head 独立计算注意力,得到:

text
headContext shape = [B, Hq, S, dHead]

再转置并拼接:

text
concat shape = [B, S, Hq × dHead] = [B,S,D]

最后乘输出投影矩阵:

text
attentionOutput = concat × Wo
Wo shape = [D,D]
mermaid
flowchart TD
    A["B×S×D输入"] --> B["一次投影生成Q、K、V"]
    B --> C["reshape成多个Head"]
    C --> D["每个Query Head计算注意力"]
    D --> E["拼接所有Head输出"]
    E --> F["乘Wo融合为B×S×D"]

9.1 多头不是人工指定语法头、指代头

训练可能让不同头形成不同关注模式,但不能保证每个头永久对应人类命名的单一功能。很多信息分布在多个头和层中。

十、MHA、MQA和GQA有什么区别

结构Query头K/V头缓存与质量方向
MHA多个与Query头一样多K/V Cache较大,表达灵活
MQA多个通常1组显著减少K/V Cache和带宽,可能影响质量
GQA多个介于1和Query头之间在质量、缓存和吞吐之间折中

GQA 中多个 Query Head 共享一组 K/V Head。例如 Hq=32、Hkv=8,每 4 个 Query Head 共享一个 K/V Head。

重要结论:KV Cache 大小主要与 Hkv 有关,不一定与 Hq 相同。估算显存时若机械使用 Query Head 数,会高估 GQA/MQA 模型缓存。

十一、Attention输出后为什么还要FFN

Attention 负责在 Token 位置之间混合信息;FFN 对每个位置的隐藏表示独立进行非线性变换。

传统方向:

text
FFN(x) = activation(xW1 + b1)W2 + b2

形状常见为:

text
[B,S,D]
→ W1扩展到中间维度Dff
→ 激活
→ W2投影回D
→ [B,S,D]

现代 LLM 常使用 GELU、SwiGLU 等变体。SwiGLU 方向上有门控分支和内容分支,经过逐元素组合后再投影回隐藏维度。具体中间维度和偏置设计由模型配置决定。

11.1 为什么只有Attention不够

Attention 的核心是对 Value 做依赖输入的加权混合。FFN 提供每个位置上的高容量非线性变换。如果没有 FFN,Block 的表示加工能力明显受限。

11.2 FFN为什么常占大量参数

当中间维度是隐藏维度的数倍时,D × DffDff × D 两个大矩阵包含大量参数和计算。MoE 模型通常把 FFN 方向替换为多个专家并按 Token 路由,因此“专家”主要不是复制整个 Transformer。

十二、残差连接与归一化

12.1 残差连接

text
output = input + sublayer(input)

它提供一条直接信息与梯度路径。子层在训练早期即使不理想,输入仍可传向后层;深层网络也更容易学习“在原表示上修正什么”。

相加要求形状一致,所以 Attention 和 FFN 最终都投影回 D。

12.2 LayerNorm与RMSNorm

LayerNorm 通常对单个 Token 的隐藏维度计算均值和方差后归一化,再使用可训练缩放和平移。RMSNorm 使用均方根方向归一化,通常不减均值,结构更简化。具体模型使用哪一种要看架构配置。

归一化用于稳定隐藏状态尺度和训练,不意味着所有层输出都完全相同,也不能替代学习率、初始化和数值稳定设计。

12.3 Pre-Norm与Post-Norm

Pre-Norm 方向:

text
x = x + Attention(Norm(x))
x = x + FFN(Norm(x))

Post-Norm 方向:

text
x = Norm(x + Attention(x))
x = Norm(x + FFN(x))

Pre-Norm 通常有更直接的残差梯度路径,在很深模型训练中常见;Post-Norm 是原始 Transformer 方向之一。两者在训练稳定性和表示行为上不同,不能只看流程图顺序随意互换预训练权重。

十三、一个Decoder Block完整走一遍

以 Pre-Norm 方向为例:

mermaid
flowchart TD
    A["输入隐藏状态x"] --> B["Norm"]
    B --> C["Causal Self-Attention"]
    C --> D["与原x做残差相加"]
    D --> E["得到x1"]
    E --> F["Norm"]
    F --> G["FFN或MoE"]
    G --> H["与x1做残差相加"]
    H --> I["输出到下一层"]

模型把这一 Block 堆叠 L 层。浅层到深层的表示会不断变化,不能把第一层 Attention 权重当成整个模型最终判断。

常见完整 Decoder-only 方向:

text
Token IDs
→ Token Embedding与位置处理
→ L个Decoder Block
→ Final Norm
→ LM Head映射到Vocab维度Logits
→ 解码选择下一个Token

LM Head 权重在部分模型中与 Token Embedding 权重共享,称为 Weight Tying;是否共享看具体架构。

十四、Causal语言模型怎样训练

假设 Token 序列:

text
[BOS, Redis, 使用, 内存, EOS]

输入与目标可理解为错开一位:

text
输入:[BOS, Redis, 使用, 内存]
目标:[Redis, 使用, 内存, EOS]

14.1 为什么训练时多个位置可以并行

训练数据已经包含所有正确目标 Token。Causal Mask 确保位置 i 的隐藏状态看不到未来输入,但 GPU 可以在一次矩阵计算中同时得到所有位置的 Logits 和 Loss:

text
位置0根据BOS预测Redis
位置1根据BOS Redis预测使用
位置2根据BOS Redis 使用预测内存
位置3根据前文预测EOS

这种方式常被称为 Teacher Forcing 方向:训练时前缀来自真实序列,不是模型上一步可能生成错的 Token。

14.2 Cross-Entropy怎样作用

LM Head 为每个位置输出整个词表的 Logits:

text
logits shape = [B,S,Vocab]

Softmax 得到词表概率,交叉熵提高目标 Token 概率、降低错误候选的相对概率。通常对有效目标位置求平均;Padding、Prompt区域或不需要训练的区域可以通过 Label Mask 设为忽略值。

14.3 训练阶段的主要内存

训练不只保存权重,还可能包含:

  • 参数权重。
  • 前向激活和中间结果。
  • 梯度。
  • 优化器状态。
  • 混合精度主权重。
  • 通信和算子工作区。

因此模型能用于推理,不代表同一硬件能以目标序列长度和 Batch 完成全参数训练。

十五、推理为什么分Prefill和Decode

15.1 Prefill

Prompt 的全部 Token 已知,可以并行处理。每层计算全部 Prompt 位置的隐藏状态,并为后续生成保存 K/V。

Prefill 主要影响首 Token 延迟 TTFT。长 Prompt、RAG 片段多和并发排队都会让 TTFT 增加。

15.2 Decode

模型生成一个新 Token 后,才能知道下一步输入。每一步只处理当前新 Token,但要读取历史 K/V 与全部历史位置做 Attention。

Decode 主要影响每个输出 Token 的时间 TPOT 和最终生成吞吐。输出越长,串行步骤越多。

mermaid
flowchart TD
    A["完整Prompt"] --> B["Prefill并行计算所有输入位置"]
    B --> C["保存每层历史K/V"]
    C --> D["生成第1个输出Token"]
    D --> E["只计算新Token的Q、K、V"]
    E --> F["Q读取历史与当前K/V"]
    F --> G["生成下一个Token并追加Cache"]
    G --> H{"是否停止"}
    H -- "否" --> E
    H -- "是" --> I["完成响应"]

十六、KV Cache究竟缓存什么

历史 Token 在某一层的 K、V 投影确定后,后续 Decode 不需要重复计算。KV Cache 为每个请求、每层保存历史 K 和 V。

常见逻辑形状:

text
K Cache = [L, B, Hkv, S, dHead]
V Cache = [L, B, Hkv, S, dHead]

具体推理引擎可能使用分页、分块、交错或不同维度顺序,逻辑元素量一致方向不变。

16.1 为什么仍要计算新Token的K和V

新 Token 未来也会成为历史,因此本步要计算它的 K/V 并追加到 Cache。下一步 Query 才能关注它。

16.2 理论字节估算

忽略页表、对齐、碎片和运行时工作区时,单请求 KV Cache 理论下界方向:

text
KV bytes
≈ 2
× L
× S
× Hkv
× dHead
× bytesPerElement

前面的 2 表示 K 和 V 两份。

例子:

text
L = 32
S = 8192
Hkv = 8
dHead = 128
dtype = BF16,2 bytes
text
KV ≈ 2 × 32 × 8192 × 8 × 128 × 2
   = 1,073,741,824 bytes
   ≈ 1 GiB / 请求

这只是教学理论值。真实占用还受 Batch、并发序列、最大预留长度、分页块、对齐、量化、滑动窗口和引擎实现影响。

16.3 为什么并发会迅速吃满显存

权重通常由多个请求共享,而每个活跃序列都需要自己的 KV 状态。长上下文乘以高并发时,KV Cache 可能比预期更早成为容量瓶颈。

16.4 KV Cache失效或不能复用的情况

  • Prompt 前缀不同。
  • 模型或权重版本不同。
  • 位置编码状态不兼容。
  • Cache 已被回收或请求迁移到不共享状态的实例。
  • Prefix Cache Key 缺少租户、Prompt版本或其他关键边界。

跨用户 Prefix Cache 必须避免把敏感上下文或缓存命中信息错误共享。

十七、普通Attention为什么长上下文昂贵

17.1 训练或Prefill的分数矩阵

每个头需要形成 S × S 方向的关系。序列长度从 S 增加到 2S,分数元素数量从 增加到 4S²

但整层总计算还包含 QKV 投影和 FFN 等近似随 S 线性增长部分,所以不能在任何模型、任何长度下简单声称“总成本严格四倍”。准确容量要看实际模型和算子 Profiling。

17.2 Decode不是每步重新形成完整S×S

有 KV Cache 时,新 Token 的 Query 只与长度 S 的历史 K 匹配,本步注意力读取量随历史长度增长。整个输出序列仍是串行过程,历史越长,每步读取的 Cache 越多。

17.3 FlashAttention解决什么

FlashAttention 方向通过分块计算、在线 Softmax 和减少高带宽显存读写,避免把完整 Attention 中间矩阵长期物化到显存。它是精确 Attention 的 IO 优化方向,不等于把结果改成近似 TopK。

它能显著降低中间内存和提升速度,但不会让长上下文免费:总数学关系、KV Cache、模型权重和输出串行依赖仍然存在。

十八、可运行Demo:Masked Multi-Head Attention

下面只使用 Python 标准库实现 Batch Size 固定为 1 的两头 Causal Self-Attention,实际验证:

  • Q/K/V 来自不同投影矩阵。
  • 形状从 [S,D] 拆成 [H,S,dHead];加上 Batch 后对应 [B,H,S,dHead]
  • 未来位置在 Softmax 后权重为 0。
  • 每行可见权重之和为 1。
  • 多头拼接后恢复 [B,S,D]
python
import math
import random


Matrix = list[list[float]]


def matmul(a: Matrix, b: Matrix) -> Matrix:
    if not a or not b or len(a[0]) != len(b):
        raise ValueError("矩阵维度不能相乘")
    rows, inner, cols = len(a), len(b), len(b[0])
    return [
        [sum(a[i][k] * b[k][j] for k in range(inner)) for j in range(cols)]
        for i in range(rows)
    ]


def stable_softmax(row: list[float]) -> list[float]:
    maximum = max(row)
    exps = [math.exp(value - maximum) for value in row]
    total = sum(exps)
    return [value / total for value in exps]


def split_heads(x: Matrix, num_heads: int) -> list[Matrix]:
    # [S,D] -> H个[S,dHead]
    hidden = len(x[0])
    if hidden % num_heads != 0:
        raise ValueError("hidden必须能被num_heads整除")
    head_dim = hidden // num_heads
    return [
        [row[head * head_dim:(head + 1) * head_dim] for row in x]
        for head in range(num_heads)
    ]


def merge_heads(heads: list[Matrix]) -> Matrix:
    # H个[S,dHead] -> [S,D]
    seq = len(heads[0])
    return [
        [value for head in heads for value in head[token_index]]
        for token_index in range(seq)
    ]


def causal_attention_for_one_head(q: Matrix, k: Matrix, v: Matrix) -> tuple[Matrix, Matrix]:
    seq, head_dim = len(q), len(q[0])
    weights: Matrix = []

    for query_index in range(seq):
        scores = []
        for key_index in range(seq):
            if key_index > query_index:
                scores.append(float("-inf"))
            else:
                dot = sum(
                    q[query_index][d] * k[key_index][d]
                    for d in range(head_dim)
                )
                scores.append(dot / math.sqrt(head_dim))
        weights.append(stable_softmax(scores))

    # [S,S] × [S,dHead] -> [S,dHead]
    context = matmul(weights, v)
    return context, weights


def causal_multi_head_attention(
    x: Matrix,
    wq: Matrix,
    wk: Matrix,
    wv: Matrix,
    wo: Matrix,
    num_heads: int,
) -> tuple[Matrix, list[Matrix]]:
    q_heads = split_heads(matmul(x, wq), num_heads)
    k_heads = split_heads(matmul(x, wk), num_heads)
    v_heads = split_heads(matmul(x, wv), num_heads)

    contexts: list[Matrix] = []
    all_weights: list[Matrix] = []
    for q, k, v in zip(q_heads, k_heads, v_heads):
        context, weights = causal_attention_for_one_head(q, k, v)
        contexts.append(context)
        all_weights.append(weights)

    return matmul(merge_heads(contexts), wo), all_weights


def random_matrix(rows: int, cols: int, rng: random.Random, scale: float) -> Matrix:
    return [
        [rng.gauss(0.0, scale) for _ in range(cols)]
        for _ in range(rows)
    ]


if __name__ == "__main__":
    rng = random.Random(7)
    seq, hidden, num_heads = 4, 8, 2
    scale = 1.0 / math.sqrt(hidden)

    x = random_matrix(seq, hidden, rng, scale=1.0)
    # 真实模型中这些矩阵由训练学习;此处固定随机种子便于复现。
    wq = random_matrix(hidden, hidden, rng, scale)
    wk = random_matrix(hidden, hidden, rng, scale)
    wv = random_matrix(hidden, hidden, rng, scale)
    wo = random_matrix(hidden, hidden, rng, scale)

    output, weights = causal_multi_head_attention(
        x, wq, wk, wv, wo, num_heads=num_heads
    )

    assert len(output) == seq and all(len(row) == hidden for row in output)
    assert len(weights) == num_heads
    assert all(len(head) == seq for head in weights)
    assert all(len(row) == seq for head in weights for row in head)

    for head in weights:
        for query_index, row in enumerate(head):
            assert math.isclose(sum(row), 1.0, rel_tol=0.0, abs_tol=1e-12)
            assert all(
                math.isclose(row[key_index], 0.0, abs_tol=1e-12)
                for key_index in range(query_index + 1, seq)
            )

    # 第0个位置只能看自己,所以所有头的第0行都是[1,0,0,0]。
    assert all(
        math.isclose(head[0][0], 1.0) and all(value == 0.0 for value in head[0][1:])
        for head in weights
    )

    print("output shape:", (len(output), len(output[0])))
    print("weights shape:", (len(weights), len(weights[0]), len(weights[0][0])))
    print("head-0 attention weights:")
    for row in weights[0]:
        print([round(value, 4) for value in row])

这个 Demo 没有实现 RoPE、Dropout、RMSNorm、FFN、反向传播、GQA、KV Cache 和高性能 Kernel,因此不能称为完整大模型。但它把 Attention 最核心的张量和 Mask 过程实际跑通了。

十九、可运行Demo:KV Cache理论容量估算

python
from dataclasses import dataclass


@dataclass(frozen=True)
class ModelShape:
    layers: int
    kv_heads: int
    head_dim: int
    bytes_per_element: int


def kv_cache_bytes(shape: ModelShape, tokens: int, concurrent_sequences: int) -> int:
    if tokens <= 0 or concurrent_sequences <= 0:
        raise ValueError("tokens和concurrent_sequences必须为正数")
    return (
        2
        * shape.layers
        * shape.kv_heads
        * shape.head_dim
        * tokens
        * concurrent_sequences
        * shape.bytes_per_element
    )


if __name__ == "__main__":
    model = ModelShape(
        layers=32,
        kv_heads=8,
        head_dim=128,
        bytes_per_element=2,
    )
    one = kv_cache_bytes(model, tokens=8192, concurrent_sequences=1)
    eight = kv_cache_bytes(model, tokens=8192, concurrent_sequences=8)
    gib = 1024 ** 3

    print(f"1 sequence theoretical KV: {one / gib:.2f} GiB")
    print(f"8 sequences theoretical KV: {eight / gib:.2f} GiB")
    assert one == gib
    assert eight == 8 * gib

注意这是理论元素量,不是承诺真实显存。部署容量还必须加上模型权重、激活、临时工作区、CUDA Context、内存碎片、推理引擎元数据和安全余量,并以实际压测为准。

二十、Encoder、Decoder和Cross-Attention怎样配合

Encoder-Decoder 模型中:

  1. Encoder Self-Attention 处理输入序列,通常允许双向关注有效输入。
  2. Decoder Causal Self-Attention 只看已生成前缀。
  3. Decoder Cross-Attention 的 Query 来自 Decoder,K/V 来自 Encoder 输出。
mermaid
flowchart TD
    A["源输入Token"] --> B["Encoder双向表示"]
    C["目标前缀Token"] --> D["Decoder因果Self-Attention"]
    D --> E["生成Cross-Attention Query"]
    B --> F["提供Cross-Attention K/V"]
    E --> G["Cross-Attention融合源输入"]
    F --> G
    G --> H["预测目标下一个Token"]

Decoder-only 大模型通常把问题、资料和历史统一放入同一 Token 序列,通过 Causal Self-Attention 处理,不代表它内部一定有独立 Encoder。

二十一、Transformer与RNN有什么本质差别

对比RNN方向Transformer方向
训练序列依赖隐状态逐步传递已知序列的位置可并行做Attention
长距离关系信息经过多步传播Attention可直接连接远位置
计算代价长序列串行明显普通Attention有S²方向代价
推理生成自回归时仍逐步Decoder生成同样逐Token
大规模训练并行效率受限更适合GPU矩阵计算和扩展

“Transformer 可并行”主要指训练/Prefill 对已知 Token 的处理,不代表自回归 Decode 的多个未来 Token 可以无依赖地全部并行生成。

二十二、商业场景:企业RAG为什么要控制上下文

假设企业助手每次检索 40 个 Chunk,每个约 800 Token,仅资料就约 32,000 Token;再加系统规则、历史、问题和输出,可能出现:

  • Prefill TTFT 明显增加。
  • Attention 和中间激活内存升高。
  • KV Cache 为整个前缀保留 K/V。
  • 低相关 Chunk 分散模型注意。
  • 正确证据位于长上下文中间却未被有效使用。
  • 单请求成本和并发容量恶化。

正确做法不是盲目换更长上下文模型,而是:

  1. Query Rewrite 保留原问题并经过评估。
  2. ACL 在检索前或检索时生效。
  3. 向量与关键词混合召回。
  4. 去重和 Rerank。
  5. 按 Token 预算选择少量高质量证据。
  6. 记录正确 Chunk 是否进入最终 Prompt。
  7. 校验引用和拒答。
  8. 用真实业务集评估质量、TTFT、TPOT 和成本。

Transformer 原理解释了“为什么上下文不是越长越好”,RAG 工程决定“哪些上下文值得进入模型”。

二十三、生产容量估算不能只看模型权重

推理显存方向至少包括:

text
模型权重
+ KV Cache
+ 当前激活
+ Attention/FFN工作区
+ 推理引擎元数据
+ CUDA Context与通信缓冲
+ 内存碎片和安全余量

容量规划需要按以下维度压测:

  • Prompt 长度分布,不只测平均值。
  • 输出长度分布。
  • 同时活跃序列数。
  • MHA/MQA/GQA 和 KV 精度。
  • Continuous Batching 策略。
  • Prefix Cache 命中率和安全边界。
  • P95/P99 TTFT、TPOT、总延迟。
  • OOM、排队、取消和超时行为。

二十四、生产故障排查Runbook

24.1 首Token很慢,后续生成正常

重点排查 TTFT:

  1. 查看排队时间与在途请求数,先排除容量拥塞。
  2. 记录本次输入 Token,按 System、历史、RAG、Tool 分段统计。
  3. 检查 RAG TopK、Chunk 大小、重复片段和工具返回值是否增加。
  4. 检查 Prefix Cache 是否因 Prompt 或版本变化失效。
  5. 对比相同输入长度在不同模型实例的 Prefill 耗时。
  6. 检查 GPU 利用率、内存带宽、Batch 调度和跨卡通信。

24.2 首Token正常,但每个后续Token越来越慢

重点排查 Decode:

  • 历史长度增长导致每步读取更多 KV。
  • 活跃序列数过高,调度轮转增加。
  • KV Cache 分页或内存压力导致调度效率下降。
  • 模型路由到更大或跨卡通信更重的实例。
  • 输出被要求过长,用户感知总耗时增加。

观察 TPOT 随上下文长度、并发和输出位置的分布,而不是只看平均总耗时。

24.3 长上下文触发显存OOM

  1. 确认是加载权重失败、Prefill激活峰值、KV Cache 分配失败还是算子工作区失败。
  2. 记录模型配置、输入长度、目标输出长度、并发序列和数据类型。
  3. 使用 Hkv 而非机械使用 Query Head 数估算 KV。
  4. 对比单请求成功、并发失败,判断是否为 KV/Batch 容量。
  5. 降低上下文、并发或输出上限进行二分验证。
  6. 检查引擎最大序列预留、分页块和碎片。
  7. 优化 RAG 上下文或选择经过评估的量化/模型方案。

24.4 输出突然出现NaN、空响应或重复Token

可能来源:

  • Mask 把某一行全部屏蔽。
  • 低精度数值溢出。
  • 权重损坏或错误加载不兼容配置。
  • Tokenizer 与模型权重不匹配。
  • KV Cache 索引、位置或请求状态错误。
  • 采样参数或停止条件配置异常。

应固定请求、模型版本、Tokenizer、随机参数和引擎版本复现,检查 Logits/finish reason 和服务错误,不能只改 Prompt 掩盖。

24.5 模型升级后相同Prompt质量下降

Transformer 架构相似不代表行为完全兼容。检查:

  • Tokenizer 与 Chat Template 是否改变。
  • 上下文和 RoPE 配置是否匹配。
  • Prompt 角色映射和停止 Token 是否改变。
  • 量化是否影响质量。
  • Tool、JSON Schema 和多模态能力是否相同。
  • 同一评估集上的分场景、安全和成本指标。

二十五、常见误区与后果

误区正确理解
Q/K/V是输入复制三份它们由不同可训练投影得到
Attention权重就是模型解释权重只是某层某头的信息混合系数,不是完整因果证明
多头由人指定不同语法功能不同模式由训练形成,通常是分布式表示
Mask可以做租户权限Attention Mask不是业务ACL
Transformer所有阶段都能并行自回归Decode仍逐Token依赖
KV Cache缓存完整隐藏状态就行核心是每层历史K/V,布局依引擎而异
KV Cache只和Query头数有关GQA/MQA主要看K/V头数Hkv
上下文翻倍总成本永远严格四倍Attention分数方向是S²,整层还含其他计算
FlashAttention是近似Attention它是分块和IO优化方向,可保持精确结果
temperature为0就完全确定模型、服务、Kernel和版本仍可能带来变化

二十六、面试标准回答

26.1 Self-Attention完整过程是什么

输入 X 分别乘可训练矩阵得到 Q、K、V;Q 与 K 转置相乘得到每个 Query 对所有 Key 的分数,除以 sqrt(dHead) 稳定尺度,加入 Causal 和 Padding Mask 后按行 Softmax 得到权重,再用权重加权汇总 V。多头结果拼接后乘输出矩阵恢复隐藏维度。

26.2 为什么要除以sqrt dHead

头维度增大时 Q、K 点积分数的方差会增大,Softmax 容易进入极端饱和区域,非最大位置梯度很弱。除以 sqrt(dHead) 将分数缩放到更稳定范围,有利于训练。

26.3 Causal Mask为什么要在Softmax前加入

把未来位置分数改为负无穷方向,经过 Softmax 后对应权重趋近零,并且其余可见位置重新归一化为总和一。如果 Softmax 后简单置零却不归一,会改变输出尺度;训练中不屏蔽未来还会让模型偷看目标。

26.4 MHA、MQA和GQA有什么区别

MHA 的每个 Query Head 有对应 K/V Head;MQA 让多个 Query Head 共享一组 K/V;GQA 让一组 Query Head 共享一组 K/V,在表达能力和 KV Cache/带宽之间折中。缓存估算主要看 K/V Head 数。

26.5 Prefill和Decode有什么区别

Prefill 处理已知完整 Prompt,可以并行计算所有输入位置并建立每层 KV Cache,主要影响 TTFT;Decode 每次只能处理新生成 Token,使用新 Query 读取历史 K/V,再追加新 K/V,具有逐 Token 依赖,主要影响 TPOT 和输出吞吐。

26.6 KV Cache为什么提速又占显存

历史 Token 每层的 K/V 在后续生成中不会改变,缓存后无需每一步重新投影全部历史;但每个活跃请求、每层、每个历史 Token 都要保存 K 和 V,容量随层数、长度、K/V头数、头维度、精度和并发线性增长。

面试页保持简洁答案,完整原理以本页为准:AI应用工程化面试题

二十七、关联知识点

二十八、学习验收清单

  • [ ] 能写出 B、S、D、Hq、Hkv、dHead 各自含义。
  • [ ] 能推导 Q、K、V 和 Attention Scores 的形状。
  • [ ] 能解释缩放、Mask、Softmax、Value聚合每一步。
  • [ ] 能说明 Causal Mask、Padding Mask 和业务ACL的区别。
  • [ ] 能画出多头拆分、拼接和Wo投影。
  • [ ] 能解释 MHA、MQA、GQA 对 KV Cache 的影响。
  • [ ] 能解释 Attention 与 FFN 为什么都需要。
  • [ ] 能比较 LayerNorm、RMSNorm、Pre-Norm、Post-Norm 的方向。
  • [ ] 能解释训练位置并行与生成逐Token并不矛盾。
  • [ ] 能区分 Prefill、Decode、TTFT 和 TPOT。
  • [ ] 能使用模型配置估算 KV Cache 理论字节数。
  • [ ] 能运行注意力 Demo 并证明未来位置权重为零。
  • [ ] 能根据现象区分 Prefill 慢、Decode 慢和显存 OOM。

达到这些标准后,才算真正理解 Transformer 的执行过程,而不是只会背“Q 查询、K 键、V 值”。