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 × D、H × 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-Decoder | Encoder双向编码,Decoder因果生成并Cross-Attention | 翻译、摘要、结构转换 | 输入编码和输出生成分开 |
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 前先固定符号:
| 符号 | 含义 | 教学示例 |
|---|---|---|
B | Batch Size,一次处理多少条序列 | 2 |
S | Sequence Length,每条序列多少个Token | 4 |
D | Hidden Size,每个Token隐藏向量维度 | 8 |
Hq | Query Head数量 | 2 |
Hkv | Key/Value Head数量 | 2 |
dHead | 每个头维度,常见为 D / Hq | 4 |
Vocab | 词表大小 | 50,000 |
L | Transformer层数 | 32 |
Tokenizer 得到 Token ID:
tokenIds shape = [B, S]查 Token Embedding 并加入位置信息后:
hiddenStates shape = [B, S, D]例如 B=2、S=4、D=8,张量里有 2 × 4 × 8 = 64 个数。每个 Batch 有 4 个 Token,每个 Token 用 8 维向量表示。
真实模型的 D 可能是数千甚至更大,但形状逻辑相同。后文所有矩阵计算都要能对上维度,维度对不上通常意味着实现或配置错误。
四、文本怎样进入Transformer
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是一张可训练矩阵
EmbeddingTable shape = [Vocab, D]每个 Token ID 取出一行 D 维向量。训练初期向量没有人类定义的固定语义,反向传播会根据预测任务调整矩阵。
4.3 Token Embedding与RAG Embedding不同
| 类型 | 输出粒度 | 主要用途 |
|---|---|---|
| Token Embedding | 每个Token一条内部隐藏向量 | Transformer内部计算 |
| 文本 Embedding | 句子、段落、图片等一个可检索向量 | 相似度检索和聚类 |
不能把语言模型内部某一层某个 Token 的隐藏状态直接等同于经过专门训练和池化的 RAG 文本向量。
五、位置为什么必须被编码
没有位置信息时,Self-Attention 对输入排列本身不天然敏感,难以区分:
我 打 你
你 打 我5.1 常见位置方法
| 方法方向 | 核心思路 | 边界 |
|---|---|---|
| 绝对位置向量 | 给每个位置加入独立或函数生成的向量 | 直接表达位置,超训练长度泛化有限 |
| 相对位置偏置 | 在注意力分数中加入相对距离信息 | 强调Token之间距离 |
| RoPE | 对Q、K的维度对按位置做旋转 | 点积中自然携带相对位置关系 |
5.2 RoPE不是把位置向量简单加到输入
RoPE 常作用在 Q 和 K 上。不同位置使用不同角度旋转维度对,使旋转后 Q、K 的点积包含相对位置信息。V 通常不以同样方式旋转。
理解方向:
同一个Token内容
+ 不同位置对应不同旋转
→ Q/K匹配同时考虑内容与相对位置RoPE 的上下文扩展、频率缩放和插值依赖具体模型与训练方式。把配置中的最大长度直接调大,不代表模型在超训练长度上仍保持相同质量。
六、Q、K、V究竟怎样得到
输入隐藏状态记作:
X shape = [B, S, D]单头教学形式:
Q = XWq
K = XWk
V = XWv若投影到 dHead:
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 第一步:计算相关性分数
Scores = QKᵀ如果 Q、K shape=[B,S,dHead]:
Kᵀ shape = [B,dHead,S]
Scores shape = [B,S,S]Scores[b,i,j] 表示第 b 条序列中,位置 i 的 Query 与位置 j 的 Key 的点积分数。
7.2 第二步:缩放
ScaledScores = Scores / sqrt(dHead)若 Q、K 各维大致独立、方差相近,点积方差会随维度增加。dHead 大时分数绝对值容易变大,Softmax 更快进入接近 one-hot 的饱和区域,小概率位置梯度很弱。除以 sqrt(dHead) 用于稳定分数尺度。
7.3 第三步:加入Mask
Decoder 因果遮罩要求位置 i 不能看到 j > i 的未来位置。实现方向是把不可见分数替换为足够大的负值:
位置0:[允许, 屏蔽, 屏蔽, 屏蔽]
位置1:[允许, 允许, 屏蔽, 屏蔽]
位置2:[允许, 允许, 允许, 屏蔽]
位置3:[允许, 允许, 允许, 允许]在 Softmax 前加入负无穷方向的值,指数后趋近 0,归一化权重也为 0。
7.4 第四步:Softmax
对每个 Query 所对应的一行分数做 Softmax:
Weights[i,j] = exp(score[i,j]) / Σj exp(score[i,j])每行可见位置权重之和约等于 1。数值实现通常先减去该行最大值,避免 exp 溢出。
7.5 第五步:加权聚合Value
Context = Weights × VWeights shape = [B,S,S]
V shape = [B,S,dHead]
Context shape = [B,S,dHead]位置 i 的输出是所有可见 Value 的加权和,因此它融合了上下文信息。
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 未屏蔽,模型会关注无效占位。
- 低精度下直接使用不合适的极小值,可能出现数值问题。
九、多头注意力怎样拆分和合并
假设:
D = 4096
Hq = 32
dHead = 128因为:
Hq × dHead = D投影并 reshape 后:
Q shape = [B, Hq, S, dHead]
K shape = [B, Hkv, S, dHead]
V shape = [B, Hkv, S, dHead]普通 MHA 中 Hkv = Hq。每个 Query Head 独立计算注意力,得到:
headContext shape = [B, Hq, S, dHead]再转置并拼接:
concat shape = [B, S, Hq × dHead] = [B,S,D]最后乘输出投影矩阵:
attentionOutput = concat × Wo
Wo shape = [D,D]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 对每个位置的隐藏表示独立进行非线性变换。
传统方向:
FFN(x) = activation(xW1 + b1)W2 + b2形状常见为:
[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 × Dff 和 Dff × D 两个大矩阵包含大量参数和计算。MoE 模型通常把 FFN 方向替换为多个专家并按 Token 路由,因此“专家”主要不是复制整个 Transformer。
十二、残差连接与归一化
12.1 残差连接
output = input + sublayer(input)它提供一条直接信息与梯度路径。子层在训练早期即使不理想,输入仍可传向后层;深层网络也更容易学习“在原表示上修正什么”。
相加要求形状一致,所以 Attention 和 FFN 最终都投影回 D。
12.2 LayerNorm与RMSNorm
LayerNorm 通常对单个 Token 的隐藏维度计算均值和方差后归一化,再使用可训练缩放和平移。RMSNorm 使用均方根方向归一化,通常不减均值,结构更简化。具体模型使用哪一种要看架构配置。
归一化用于稳定隐藏状态尺度和训练,不意味着所有层输出都完全相同,也不能替代学习率、初始化和数值稳定设计。
12.3 Pre-Norm与Post-Norm
Pre-Norm 方向:
x = x + Attention(Norm(x))
x = x + FFN(Norm(x))Post-Norm 方向:
x = Norm(x + Attention(x))
x = Norm(x + FFN(x))Pre-Norm 通常有更直接的残差梯度路径,在很深模型训练中常见;Post-Norm 是原始 Transformer 方向之一。两者在训练稳定性和表示行为上不同,不能只看流程图顺序随意互换预训练权重。
十三、一个Decoder Block完整走一遍
以 Pre-Norm 方向为例:
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 方向:
Token IDs
→ Token Embedding与位置处理
→ L个Decoder Block
→ Final Norm
→ LM Head映射到Vocab维度Logits
→ 解码选择下一个TokenLM Head 权重在部分模型中与 Token Embedding 权重共享,称为 Weight Tying;是否共享看具体架构。
十四、Causal语言模型怎样训练
假设 Token 序列:
[BOS, Redis, 使用, 内存, EOS]输入与目标可理解为错开一位:
输入:[BOS, Redis, 使用, 内存]
目标:[Redis, 使用, 内存, EOS]14.1 为什么训练时多个位置可以并行
训练数据已经包含所有正确目标 Token。Causal Mask 确保位置 i 的隐藏状态看不到未来输入,但 GPU 可以在一次矩阵计算中同时得到所有位置的 Logits 和 Loss:
位置0根据BOS预测Redis
位置1根据BOS Redis预测使用
位置2根据BOS Redis 使用预测内存
位置3根据前文预测EOS这种方式常被称为 Teacher Forcing 方向:训练时前缀来自真实序列,不是模型上一步可能生成错的 Token。
14.2 Cross-Entropy怎样作用
LM Head 为每个位置输出整个词表的 Logits:
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 和最终生成吞吐。输出越长,串行步骤越多。
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。
常见逻辑形状:
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 理论下界方向:
KV bytes
≈ 2
× L
× S
× Hkv
× dHead
× bytesPerElement前面的 2 表示 K 和 V 两份。
例子:
L = 32
S = 8192
Hkv = 8
dHead = 128
dtype = BF16,2 bytesKV ≈ 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,分数元素数量从 S² 增加到 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]。
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理论容量估算
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 模型中:
- Encoder Self-Attention 处理输入序列,通常允许双向关注有效输入。
- Decoder Causal Self-Attention 只看已生成前缀。
- Decoder Cross-Attention 的 Query 来自 Decoder,K/V 来自 Encoder 输出。
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 分散模型注意。
- 正确证据位于长上下文中间却未被有效使用。
- 单请求成本和并发容量恶化。
正确做法不是盲目换更长上下文模型,而是:
- Query Rewrite 保留原问题并经过评估。
- ACL 在检索前或检索时生效。
- 向量与关键词混合召回。
- 去重和 Rerank。
- 按 Token 预算选择少量高质量证据。
- 记录正确 Chunk 是否进入最终 Prompt。
- 校验引用和拒答。
- 用真实业务集评估质量、TTFT、TPOT 和成本。
Transformer 原理解释了“为什么上下文不是越长越好”,RAG 工程决定“哪些上下文值得进入模型”。
二十三、生产容量估算不能只看模型权重
推理显存方向至少包括:
模型权重
+ 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:
- 查看排队时间与在途请求数,先排除容量拥塞。
- 记录本次输入 Token,按 System、历史、RAG、Tool 分段统计。
- 检查 RAG TopK、Chunk 大小、重复片段和工具返回值是否增加。
- 检查 Prefix Cache 是否因 Prompt 或版本变化失效。
- 对比相同输入长度在不同模型实例的 Prefill 耗时。
- 检查 GPU 利用率、内存带宽、Batch 调度和跨卡通信。
24.2 首Token正常,但每个后续Token越来越慢
重点排查 Decode:
- 历史长度增长导致每步读取更多 KV。
- 活跃序列数过高,调度轮转增加。
- KV Cache 分页或内存压力导致调度效率下降。
- 模型路由到更大或跨卡通信更重的实例。
- 输出被要求过长,用户感知总耗时增加。
观察 TPOT 随上下文长度、并发和输出位置的分布,而不是只看平均总耗时。
24.3 长上下文触发显存OOM
- 确认是加载权重失败、Prefill激活峰值、KV Cache 分配失败还是算子工作区失败。
- 记录模型配置、输入长度、目标输出长度、并发序列和数据类型。
- 使用
Hkv而非机械使用 Query Head 数估算 KV。 - 对比单请求成功、并发失败,判断是否为 KV/Batch 容量。
- 降低上下文、并发或输出上限进行二分验证。
- 检查引擎最大序列预留、分页块和碎片。
- 优化 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应用工程化面试题。
二十七、关联知识点
- AI从数据学习到商业系统:先理解 Loss、梯度、训练和推理。
- 大模型Token、训练与推理原理:继续学习 Tokenizer、Logits、解码、显存和幻觉。
- Embedding与向量库:区分内部 Token 表示和检索向量。
- RAG完整管道:把上下文成本落实到检索、Rerank和预算。
- 推理优化:学习调度、缓存、量化、并发和服务治理。
- 多模态:理解图片 Patch 如何变成视觉表示并与文本融合。
- DeepSeek:理解 MoE、MLA 和部署边界。
二十八、学习验收清单
- [ ] 能写出 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 值”。
