想象一家主打分子料理的晚宴餐厅。后厨里,备菜组领班已经完整读过了客人的宴席要求——那是一份法文原版菜单——并把它整理成一本厚厚的风味手册,上面记录着每道菜该有的风味、食材与搭配逻辑。而在出菜口的另一端,掌勺主厨站上了灶台。他不能像备菜组那样一次看完所有食材,必须一道菜一道菜地出;他的正前方还有一面单向玻璃——他只能看见已经端出去的那几道菜,绝不允许偷看下一道菜该是什么。为了不跑偏,他每炒完一道菜,都要低头翻一眼风味手册确认口味;为了不低效,他身边还有一位帮厨,每出一道菜就在速记本上记下这道菜的关键信息。 这套流程,就是 Transformer 解码器的工作方式。备菜组是编码器(Encoder),掌勺主厨是解码器(Decoder),单向玻璃对应因果掩码(Causal Mask),翻手册对应交叉注意力(Cross-Attention),速记本对应 KV 缓存(KV Cache)。如果你用过 ChatGPT、做过机器翻译、写过 AI 续写,你其实一直在看这位主厨出菜。接下来,我们就拆开这位主厨的每一个动作。

岗位关联

掌握解码器,就掌握了所有生成式大模型的“心脏”:无论调优 GPT 类模型、做推理加速,还是排查生成质量,你都需要先看懂这位出菜主厨的每一步。

目录
  1. 可视化演示
  2. 解码器总览
  3. 掩码自注意力
  4. 交叉注意力
  5. 前馈网络
  6. 自回归生成
  7. 因果掩码矩阵
  8. KV缓存优化
  9. 训练与推理
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

解码器三层子模块 每个子层:Add & LayerNorm | 多层堆叠构成完整解码器 因果掩码矩阵 位置 j → Attention(Q,K,V) = softmax(QK^T/√d Transformer解码器可视化:展示三层子模块、因果掩码、交叉注意力、...

解码器总览

推开厨房的门,你会看到掌勺主厨的灶台是一条三段式流水线。第一段是一面回放镜——它只会回放已经端出厨房的菜,绝不映照未来的餐桌;第二段是一本摊开的风味手册,主厨每做一道菜都要低头核对手册,确认没有跑题;第三段是一张小调味台,每道菜出锅前在这里单独收味、整形。三段走完,一道菜才算合格,装盘出餐。

这段流水线,就是 Transformer 解码器中“一层”的完整结构。真实的解码器并非只有一层,而是由 N 个完全相同的层垂直堆叠而成——GPT 系列通常堆 12 层、24 层甚至 96 层。每一层内部严格按顺序排列三个子层:掩码自注意力、交叉注意力、前馈网络。由于自注意力不像循环网络那样天然携带顺序感,解码器还会在输入端叠加位置编码,告诉模型每个词排在哪个位置。为了让几十层网络叠加后数值依然稳定,每个子层外面都套着残差连接与层归一化。

每个子层都遵循同一种“先处理、再还原”的更新模式。设输入为 $x$,子层函数为 $\text{SubLayer}(\cdot)$,那么输出为:

这就像主厨对每一道工序都留了一手:菜加工完了,他还会把原始食材拼在旁边一起对照——万一加工过度,还能从原始版本里找回味道。残差连接正是这条保底捷径,而层归一化则是装盘前最后的摆正。理解这一层骨架,再看后面三个子层,就只是往流水线的三个台子上分别填细节了。

xout=LayerNorm(x+SubLayer(x))\displaystyle x_{out} = \text{LayerNorm}\big(x + \text{SubLayer}(x)\big)
通俗说:子层负责加工,残差负责保底,层归一化负责稳住数值。
  1. 主路径(主厨炒菜)$\text{SubLayer}(x)$ —— 子层先对输入 $x$ 做变换,比如掩码自注意力完成打分与加权。
  2. 残差捷径(备份菜样)$x + \text{SubLayer}(x)$ —— 把原始输入原样加回加工结果,防止信息在层层传递中丢失。
  3. 层归一化(装盘把关)$\text{LayerNorm}(\cdot)$ —— 对每个位置的特征做标准化,让数值分布稳定,训练更快收敛。

掩码自注意力

第一段流水线是回放镜。出菜口的单向玻璃让主厨只能看到已经端走的菜:第 1 道、第 2 道……直到当前这道。至于第 5 道菜还没做出来,它绝不会出现在镜子里。这就是解码器与编码器最本质的区别——生成时绝不看未来。

技术术语叫掩码自注意力(Masked Self-Attention)。“自注意力”指 Q、K、V 三个矩阵全部由解码器自己的输入投影而来,让每个位置都能参考其他位置;“掩码”则负责把未来位置的参考权删除。注意,删除不是硬性把权重归零,而是在 softmax 之前的打分矩阵上加一层遮挡,这正是后面“因果掩码矩阵”一节要展开的内容。打分公式如下:

回到厨房:主厨给每道菜打分时,先看它与已经上桌的菜有多合拍($QK^\top$),除以菜量规模让分数别失控($\sqrt{d_k}$),被单向玻璃挡住的未来菜直接记上“-∞”($+M$),剩下的过去菜按合拍程度分配注意力权重(softmax),最后把这些菜的味道按权重混合进当前这道菜($\times V$)。回放镜就这么工作。

Attn(Q,K,V)=softmax(QK⊤dk+M)V\displaystyle \text{Attn}(Q, K, V) = \text{softmax}\left(\frac{Q K^\top}{\sqrt{d_k}} + M\right) V
通俗说:先算当前位置与每个历史位置的相似度,再对未来的分数打上“-∞”,softmax 一过未来权重自动归零,历史按相关度加权求和。
  1. 查询与键的点积(对味指数)$QK^\top$ —— 当前位置的查询向量,与每个历史位置的键向量做点积,得到“有多相关”的分数。
  2. 缩放$\frac{1}{\sqrt{d_k}}$ —— 维度越大点积数值越大,除以 $\sqrt{d_k}$ 把分数拉回稳定区间,防止 softmax 饱和。
  3. 加掩码$+ M$ —— 矩阵 $M$ 把未来位置的分数改成 $-\infty$,让它们无法参与竞争。
  4. 归一化成权重$\text{softmax}(\cdot)$ —— $-\infty$ 处权重变成 0,历史位置按相关度分配 0~1 的权重。
  5. 加权求和$\times V$ —— 用权重混合各位置的值向量,得到融合了历史信息的输出。

交叉注意力

第二段流水线是风味手册。主厨不能只凭自己的记忆做菜——他必须确保每一道菜都忠实于客人点的那顿宴席。于是每道菜出锅前,他都要低头翻手册,找到“原文想要什么味道”,然后把手边食材往那个方向靠。

交叉注意力(Cross-Attention)干的正是这件事。它的查询向量 Q 来自解码器当前词,代表“我现在想表达什么”;而键 K、值 V 全部来自编码器的最终输出,代表“原文里每个词的关键信息和内容”。解码器把自己当前的意图与原文每个词逐一比对,把最相关的原文语义“搬运”过来。注意!这里 Q 与 K、V 的来源不同,正是交叉注意力与自注意力唯一的本质区别。

机器翻译里,源文“I love you”被备菜组压成风味手册;主厨要做中文的第二个词时,Q 说“我现在要决定动词了”,手册里 love 的键值立刻被高亮,于是他把“爱”端了出来。没有交叉注意力,解码器就像闭眼做菜的主厨——手艺再好,也会偏离原菜单。

CrossAttn(Qd,Ke,Ve)=softmax(QdKe⊤dk)Ve\displaystyle \text{CrossAttn}(Q_d, K_e, V_e) = \text{softmax}\left(\frac{Q_d K_e^\top}{\sqrt{d_k}}\right) V_e
通俗说:Q 问“我现在要什么”,K 提供“原文谁最相关”,V 提供“相关原文的内容”,三者合起来完成跨端搬运。
  1. Q 来自解码器(主厨口味)$Q_d$ —— 由解码器当前位置的隐藏状态投影而来,表示“这道菜我想做成什么样”。
  2. K、V 来自编码器(备菜手册)$K_e, V_e$ —— 编码器输出的每个词都提供键和值,键负责匹配,值负责内容。
  3. 跨端打分$Q_d K_e^\top$ —— 当前词逐个对照原文所有词,找出与自己最贴近的原文片段。
  4. 加权搬运$\text{softmax}(\cdot) V_e$ —— 按匹配程度加权汇总原文信息,注入解码输出。

前馈网络

第三段是调味台。前两个子层像一场场讨论会——每个词都要与别的词交换信息,开完会大家脑子里装满了彼此的想法。但讨论不能替代个人手艺:每个词最终要独立完成自己的“提味”,把混合后的信息加工成更丰富的表达。这张只属于单道菜的调味台,就是前馈网络。

前馈网络(Feed-Forward Network, FFN)对每个位置完全独立地作用:同一套矩阵、同一套偏置,依次作用在序列的每个词向量上。它先把维度从 $d$ 扩到 $4d$,做一次非线性激活,再压缩回 $d$ 维。两层的“形状”像一个中间鼓起的瓶子——先放开、再收口。

前两个台子负责“串味”——让每道菜知道同桌的菜是什么;调味台负责“定味”——每道菜在自己碗里完成最后的调味和整形。没有这一步,解码器就只有一群互相参考、却不会独立思考的菜。

FFN(x)=W2 GELU(W1x+b1)+b2\displaystyle \text{FFN}(x) = W_2 \, \text{GELU}(W_1 x + b_1) + b_2
通俗说:先放大到 4 倍宽度做非线性加工,再收回原尺寸,让每个词独立完成特征提炼。
  1. 升维(把菜放大看)$W_1 x + b_1$ —— 线性变换把维度从 $d$ 扩到 $4d$,提供更大的表达空间。
  2. 非线性激活(调味)$\text{GELU}(\cdot)$ —— 逐元素引入非线性,否则多层线性变换叠在一起还是线性。
  3. 降维(装盘收口)$W_2 + b_2$ —— 变换回 $d$ 维,方便与后续残差连接对齐。
  4. 逐词独立每个位置单独过 FFN,词与词之间不通信,与前两层的“开会”形成互补。

自回归生成

主厨的晚宴有条铁律:菜必须一道接一道地上,不能三盘同时出。第 3 道菜是什么,取决于第 1、2 道菜已经长成什么样;如果第 2 道做成了辣味,第 3 道的搭配就要跟着调整。这就是自回归生成——把自己的输出当作下一步的输入,像滚雪球一样往前滚。

形式化地说,生成整段文本 $w_1 w_2 \ldots w_T$ 的联合概率,可以按链式法则拆成每一步条件概率的乘积:

推理时,模型在每个时间步把“概率最大的词”接到输入末尾,再预测下一个;遇到结束符就收手。这里“概率最大的词”对应解码器输出层对词表的打分——那是主厨唯一能看到“整本菜单”的地方。

P(w1,w2,…,wT)=∏t=1TP(wt∣w<t)\displaystyle P(w_1, w_2, \ldots, w_T) = \prod_{t=1}^{T} P\big(w_t \mid w_{<t}\big)
通俗说:整段话的概率,拆成一连串“已知前面所有词时、下一个词出现的概率”的乘积。
  1. 整句话概率(整桌菜单)$P(w_1, \ldots, w_T)$ —— 生成整段文本的联合概率。
  2. 链式法则拆解(一盘接一盘)$\prod_{t=1}^{T}$ —— 把联合概率拆成一连串条件概率相乘。
  3. 条件依赖(只看已上桌的菜)$P(w_t \mid w_{<t})$ —— 第 $t$ 个词只条件于前 $t-1$ 个词,严格单向。
  4. 逐词选择(点单决策)每个时间步在词表上取概率最大的词(贪心解码)或按概率采样,作为下一轮的输入。

因果掩码矩阵

单向玻璃到底长什么样?把它画成一张坐标纸:行代表“现在轮到预测第几个词”,列代表“允许参考第几个词”。玻璃的存在,让下三角区域透明可见,上三角区域被贴上反光膜。

这张坐标纸就是因果掩码矩阵 $M$。训练时,整个真实目标序列会一次性地同时输入解码器(这是并行训练的关键),如果没有 $M$,第 3 个词在计算注意力时就会偷看到第 5 个词——相当于主厨在菜单还没定稿时就偷看了未来的菜。矩阵的构造极简单,但对正确性至关重要:

示意图里,掩码矩阵被涂成红白分明的两块。softmax 一过,红色区域的权重蒸发成 0,下三角正常加权。训练时大家齐头并进却互不偷看;推理时只剩一个人在前方开路——掩码始终是那位保证“不偷看未来”的守门员。

Mij={0,i≥j−∞,i<j\displaystyle M_{ij} = \begin{cases} 0, & i \ge j \\ -\infty, & i < j \end{cases}
通俗说:行是当前位置,列是参考位置;下三角可看,上三角被负无穷挡住。
  1. 矩阵坐标含义(座位表)$i$ 行表示当前要预测的位置,$j$ 列表示能参考的位置。
  2. 下三角保留(已上桌)$i \ge j$ 时 $M_{ij} = 0$ —— 当前位置可以看自己和过去的词,注意力权重正常计算。
  3. 上三角屏蔽(未来的菜不许偷看)$i < j$ 时 $M_{ij} = -\infty$ —— 经过 softmax 后这些位置的权重趋近于 0。

KV缓存优化

主厨出菜越来越快,多亏了帮厨的速记本。每端出一道菜,帮厨就在本子上记下这道菜的关键索引和味道档案。等到做第 50 道菜时,主厨不需要重新回忆前 49 道菜,只要翻到本子最后一条,对照新的要求就行。若没有速记本,每做一道新菜都要把前面所有菜重新品尝一遍——太慢了。

速记本对应 KV 缓存(KV Cache)。推理时,第 $t$ 步只需为当前新词计算新的键 $k_t$、值 $v_t$,并把它追加到缓存数组中;历史词的 K、V 直接复用。这样每步的注意力计算量,从“每次都要重算前 $t$ 步所有点积”,降为“只对新词做一次矩阵乘法”:

长文本生成中,KV 缓存能带来 5~10 倍的推理加速,这正是 ChatGPT 打字那么快的原因之一。你每看到一个词蹦出来,背后都有帮厨在奋笔疾书。

Attnt=softmax(qtKcache⊤dk)Vcache,Kcache=[k1,k2,…,kt]\displaystyle \text{Attn}_t = \text{softmax}\left(\frac{q_t K_{\text{cache}}^\top}{\sqrt{d_k}}\right) V_{\text{cache}}, \quad K_{\text{cache}} = [k_1, k_2, \ldots, k_t]
通俗说:历史键值存进速记本,新词只需一次查询,结果与无缓存完全一致。
  1. 只需新词的查询(新菜口味)$q_t$ —— 第 $t$ 步只计算新词一个查询向量,历史词不再重复投影。
  2. 缓存键(速记本)$K_{\text{cache}}$ —— 历史所有词的键按顺序拼接,随生成不断追加。
  3. 一次点积(直接翻本子)$q_t K_{\text{cache}}^\top$ —— 新词与全部历史键做一次矩阵乘法,而不是重新计算历史词的 QK 乘积。
  4. 加权取值(照单混合)$\text{softmax}(\cdot) V_{\text{cache}}$ —— 结果与不缓存完全一致,只是省掉了历史词的重复计算。

训练与推理

最后一个秘密:厨师是怎么学会这手绝活的?训练时,老板会把标准答案直接摆在主厨手边——第 2 道菜的正确答案就放在那,让他对照着练,这叫教师强制(Teacher Forcing);真正开店后,标准答案撤走,他只能凭自己上一道菜的手感继续做,这叫自回归推理。两种模式,同一套炉灶。

训练阶段,把真实目标序列右移一位作为解码器输入,由于有因果掩码,模型可以一次性并行计算出所有位置的预测分布,再与真实词计算损失。推理阶段,没有真实词可喂,只能把上一步的预测词作为输入逐词串行推进,直到预测出结束符。损失函数定义如下:

理解了训练与推理的差异,就理解了为什么大模型推理工程要拼命优化 KV 缓存、为什么训练可以用几千张 GPU 并行、而推理只能一步步来。主厨的晚宴,从学徒到开店,原来是两套规则下的同一门手艺。

L=−1T∑t=1Tlog⁡P(yt∣y<t,x)\displaystyle \mathcal{L} = -\frac{1}{T} \sum_{t=1}^{T} \log P\big(y_t \mid y_{<t}, x\big)
通俗说:训练时拿真实词当正确答案,让模型在每个位置都尽量猜中,所有位置取平均作为损失。
  1. 条件建模(标准答案在手边)$P(y_t \mid y_{<t}, x)$ —— 训练时第 $t$ 步的输入是真实前文,条件是源文本 $x$。
  2. 取负对数(答错扣分)$-\log$ —— 预测概率越接近 1,$-\log$ 越小,等价于鼓励模型猜中真实词。
  3. 全序列平均(全班成绩)$\frac{1}{T} \sum_{t}$ —— 对每个位置的损失取平均,作为这一句的最终损失。
  4. 推理换规则(开卷变闭卷)推理时没有真实前文可用,只能用自己的预测词续写,这就是训练与推理的本质差异。

AI 应用场景

大模型聊天助手推理加速

阶段:推理部署 / 生成阶段

解决的问题:GPT 类模型解码时,每生成一个词都要重新计算前面所有词的注意力。传统做法在生成第 100 个词时,要重复做 99 次历史词的 QK 点积,延迟随生成长度线性暴涨。引入 KV 缓存后,历史键值只算一次并存入显存,每一步只处理新词,生成速度可提升 5~10 倍——这正是主厨翻速记本的故事在工业界的落地。

具体案例:某团队部署 7B 参数的对话模型,首 token 延迟 300ms 后,每生成一个 token 由原来的 80ms 降到 12ms。它们做的核心改动就是在解码器每一层的掩码自注意力处维护 KV 缓存数组,并配合连续内存池减少碎片。

机器翻译中的编码-解码协作

阶段:训练与推理全链路

解决的问题:翻译时,目标语言的每个词都必须“看着”整句源文来生成。传统统计机器翻译靠短语表对齐,生硬且无法处理长句。解码器的交叉注意力让每个目标词直接对源文所有词做软对齐:Q 来自解码器当前词,K、V 来自编码器输出,实现端到端的语义搬运。

具体案例:中英翻译系统中,输入“The weather is nice today”,编码器把它压成 7 个位置的风味手册;解码器生成“今天”时,交叉注意力把权重集中到 “today” 上,从而产出忠实原文的译文。

流式语音字幕实时生成

阶段:在线流式推理

解决的问题:语音识别字幕要求边说边出字,不能等整句说完再识别。解码器天然的自回归性质 + 因果掩码正好匹配流式场景:每个时间步只依赖已识别出的历史字符,配合 KV 缓存把增量计算压到最低,从而实现几百毫秒级别的低延迟字幕输出。

具体案例:实时会议字幕系统,解码器每收到一个声学块就生成一个词,同时把该词的 KV 追加进缓存;说话人停顿 500ms 内字幕即上屏,观众几乎感觉不到滞后。

Python 演示

python79 行
"""最小Transformer解码器: 掩码自注意力 + 交叉注意力 + KV缓存"""import numpy as np  def softmax(x, axis=-1):    e = np.exp(x - x.max(axis=axis, keepdims=True))    return e / e.sum(axis=axis, keepdims=True)  def causal_mask(n):    """上三角为 -inf 的因果掩码, 形状 (n, n)"""    m = np.zeros((n, n))    m[np.triu_indices(n, 1)] = -1e9    return m  class MiniDecoder(object):    def __init__(self, d=16, v=24, seed=0):        rng = np.random.default_rng(seed)        self.d = d        self.v = v        self.wq = rng.normal(0, 0.1, (d, d))        self.wk = rng.normal(0, 0.1, (d, d))        self.wv = rng.normal(0, 0.1, (d, d))        self.wf1 = rng.normal(0, 0.1, (d, 4 * d))        self.wf2 = rng.normal(0, 0.1, (4 * d, d))        self.embed = rng.normal(0, 0.1, (v, d))        sos_k = self.embed[0] @ self.wk        sos_v = self.embed[0] @ self.wv        self.k_cache = sos_k[None, :]        self.v_cache = sos_v[None, :]     def attn(self, x, k, v, mask=None):        q = x @ self.wq        s = q @ k.T / np.sqrt(self.d)        if mask is not None:            s = s + mask        return softmax(s) @ v     def step(self, token_id, cross_k, cross_v):        x = self.embed[token_id][None, :]        h = self.attn(x, self.k_cache, self.v_cache)        new_k = x @ self.wk        new_v = x @ self.wv        self.k_cache = np.vstack([self.k_cache, new_k])        self.v_cache = np.vstack([self.v_cache, new_v])        h = self.attn(h, cross_k, cross_v)        h = np.maximum(0, h @ self.wf1) @ self.wf2        logits = h[0] @ self.embed.T        return int(np.argmax(logits))     def forward_parallel(self, token_ids, cross_k, cross_v):        """训练路径: 整句并行 + 因果掩码"""        x = self.embed[token_ids]        h = self.attn(x, x @ self.wk, x @ self.wv, mask=causal_mask(len(x)))        h = self.attn(h, cross_k, cross_v)        h = np.maximum(0, h @ self.wf1) @ self.wf2        return h @ self.embed.T     def generate(self, start_id, steps=6):        rng = np.random.default_rng(1)        cross_k = rng.normal(0, 0.1, (4, self.d))        cross_v = rng.normal(0, 0.1, (4, self.d))        ids = [start_id]        for _ in range(steps):            ids.append(self.step(ids[-1], cross_k, cross_v))        return ids  if __name__ == "__main__":    dec = MiniDecoder()    result = dec.generate(0, steps=6)    print("生成轨迹:", result)    print("KV缓存中的键数量:", len(dec.k_cache))    rng = np.random.default_rng(1)    ck = rng.normal(0, 0.1, (4, dec.d))    cv = rng.normal(0, 0.1, (4, dec.d))    logits = dec.forward_parallel([0, 1, 2], ck, cv)    print("并行训练输出形状:", logits.shape)
目的:用 numpy 从零实现一个迷你解码器,演示推理时的 KV 缓存自回归生成,以及训练时的整句并行 + 因果掩码两条路径。
  1. m[np.triu_indices(n, 1)] = -1e9生成因果掩码:把方阵上三角全部置成很大的负数
  2. s = q @ k.T / np.sqrt(self.d)注意力打分:查询向量与每个键点积后缩放
  3. return softmax(s) @ v把分数归一化成权重,再对值向量加权求和
  4. h = self.attn(x, self.k_cache, self.v_cache)掩码自注意力:KV 来自历史缓存,天然只看到过去
  5. self.k_cache = np.vstack([self.k_cache, new_k])KV 缓存追加:新词的键入册,供后续步骤复用
  6. h = self.attn(h, cross_k, cross_v)交叉注意力:KV 换成编码器输出,完成语义对接
  7. h = np.maximum(0, h @ self.wf1) @ self.wf2前馈网络:先升维再用 ReLU 激活,再降维回原尺寸
  8. logits = h[0] @ self.embed.T输出层:隐藏向量与词表向量做点积打分
  9. ids.append(self.step(ids[-1], cross_k, cross_v))自回归:把上一步预测结果喂给下一步

常见误区

误区我以为因果掩码就是把未来位置的注意力权重直接乘 0,既然不让看,权重设成 0 最直观了,所以掩码矩阵里未来位置填 0 就行。

正解填 0 会让分数保持原值,经过 softmax 后可能依然分走大量权重——挡住视线不等于把分数归零。正确做法是在 softmax 之前,给未来位置的分数加上一个极大的负数($-1e9$ 或 $-\infty$),softmax 之后这些位置的权重自然趋近于 0。这就像给未来的菜贴上“-∞ 分”的标签,而不是直接说“这道菜 0 分”。

为何常见:“mask”这个词容易让人联想到“遮挡→归零”,加上很多示意图直接把被掩码区域涂黑,初学者很自然会理解成乘 0。

误区我以为交叉注意力就是把注意力公式再算一遍,既然公式长得一模一样,那它和掩码自注意力只是名字不同。

正解两者公式确实相同,但数据来源完全不同。掩码自注意力的 Q、K、V 都来自解码器自身的历史状态;交叉注意力的 Q 来自解码器,而 K、V 来自编码器的最终输出。来源不同决定了行为不同:前者做“内部历史对齐”,后者做“跨端语义搬运”。没有交叉注意力,翻译模型就失去了参考原文的通道。

为何常见:初学者先熟记了统一的注意力公式,注意力被公式形式吸引,忽略了输入来源这个关键差异。

误区我担心用 KV 缓存之后,模型拿到的是“旧记忆”,可能跟不加缓存时生成的词不一样,所以总是不敢在生产环境开启缓存。

正解缓存只是把历史词的 K、V 向量存下来复用,数学上与不缓存严格等价——因为它省掉的是重复计算,不是改变计算逻辑。每一步注意力结果与重新计算历史 QK 乘积完全一致(仅存在浮点舍入级别的微小误差)。可以放心地说:缓存只影响速度,不影响质量。

为何常见:直觉上“缓存≈压缩≈有损”,人们习惯性地担心优化手段会引入信息损失,忽略了这里的缓存是精确复算。

误区我以为生成模型训练时和推理一样,必须串行地把预测词喂回模型,所以训练一千万条句子要跑一千万次前向——训练慢得离谱。

正解训练用的是教师强制(Teacher Forcing):把真实目标序列整体一次性输入解码器,配合因果掩码,所有位置可以并行计算预测分布和损失。推理没有真实词可用,才必须串行逐词生成。训练并行、推理串行,是解码器两种模式最大的分水岭。

为何常见:很多人只接触过推理时的生成流程,看到“自回归”三个字就默认训练也是逐词滚动,忽略了训练有标准答案可以“喂”。

误区我以为生成到固定长度就行,结束符只是装饰品,有没有都不影响训练和推理。

正解结束符是解码器唯一的“刹车信号”。训练时目标序列末尾要加 <eos> 作为预测目标,模型才能学会“这句话到此为止”;推理时一旦预测出 <eos> 就停止生成。没有结束符,模型会一直续写下去,要么无限循环、要么被硬截断产生残缺文本。

为何常见:初学阶段只关注“如何生成下一个词”这个主流程,把结束符当成后处理细节,忽略了它参与训练目标与停止条件的事实。

面试题

Q1. 解码器掩码自注意力中,因果掩码的核心作用是什么?

单选题 · easy

  • A.增加解码器模型的参数量
  • B.防止模型提前看到未来词语← 正确答案
  • C.加速模型训练的收敛速度
  • D.增强梯度反向传播的稳定性

答案:B。因果掩码的本质是让第 t 个位置只能关注前 t-1 个位置,防止信息从未来泄漏到过去,这正是自回归生成的前提。加速收敛、增加参数、增强梯度都不是掩码的职责,它们分别由更深的层、更大的维度、残差连接等机制承担。

Q2. 解码器预测第 t 个词时,它的依赖对象是?

单选题 · easy

  • A.依赖输入中的全部词汇
  • B.只依赖已生成的前t-1个词← 正确答案
  • C.依赖未来还没生成的词
  • D.依赖随机均匀的噪声

答案:B。自回归生成的核心是“只看过去”:第 t 个词的条件概率只依赖于前 t-1 个已生成词。依赖全部输入是编码器的行为,依赖未来词会泄漏答案,依赖随机噪声则毫无信息量。

Q3. 交叉注意力结构中,K 和 V 矩阵来自哪里?

单选题 · easy

  • A.解码器上一步输出投影
  • B.前馈网络中间层输出
  • C.词嵌入矩阵的固定向量
  • D.编码器的最终输出投影← 正确答案

答案:D。交叉注意力是解码器与编码器之间的桥梁,Q 来自解码器,而 K、V 都来自编码器的最终输出。这样每个目标词可以直接“查阅”原文所有词。其余选项描述的都是解码器内部的信息,无法承担跨端语义搬运的职责。

Q4. 因果掩码矩阵中,未来位置填入什么值最合理?

单选题 · mid

  • A.填入数值一增加权重
  • B.填入负一使权重变小
  • C.填入数值零让权重为0
  • D.填入负无穷经softmax归零← 正确答案

答案:D。掩码要加在 softmax 之前,未来位置的分数应设置为 $-\infty$,经过 softmax 后权重严格趋近于 0。填入 0 或 -1 后经过 softmax 仍可能分到非零权重,等于没有完全屏蔽未来。

Q5. KV缓存主要省掉了推理中的哪部分重复计算?

单选题 · mid

  • A.词嵌入层的一次查表操作
  • B.历史词键值对应的注意力打分← 正确答案
  • C.前馈网络里的两次矩阵乘法
  • D.输出层到词表的全部映射

答案:B。没有缓存时,生成第 t 个词会重新计算前 t-1 个词的 K、V 以及它们的 QK 点积,复杂度随 t 线性增长。KV 缓存把历史键值存下来,每步只需对新词做一次查询,省掉的是历史词的全部重复注意力打分。

Q6. 训练阶段解码器能够并行的根本原因是?

单选题 · mid

  • A.GPU显存容量足够大
  • B.不需要计算注意力分数
  • C.掩码屏蔽未来词后整体输入← 正确答案
  • D.解码器参数数量相对较少

答案:C。训练时把真实目标序列整体输入,因果掩码保证每个位置只看历史、互不干扰,因此所有位置可以一次前向并行算出。这与显存大小、参数多少无关;注意力分数也照样要算,只是被掩码限制了可见范围。

Q7. 自回归推理时,每个时间步输入解码器的内容是?

单选题 · mid

  • A.目标序列的真实词语
  • B.随机漂移的噪声向量
  • C.上一步预测出的那个词← 正确答案
  • D.编码器输出的所有向量

答案:C。推理阶段没有真实词可用,必须把上一步预测出的词作为当前步输入,形成“预测→喂入→再预测”的闭环。真实词语只出现在训练的教师强制阶段;编码器输出作为交叉注意力的 K、V,而不是输入序列。

Q8. 下列属于解码器中每一层的标准组件有哪些?

多选题 · hard

  • A.掩码自注意力子层← 正确答案
  • B.交叉注意力子层← 正确答案
  • C.前馈全连接子层← 正确答案
  • D.全局平均池化层

答案:ABC。解码器每一层由掩码自注意力、交叉注意力、前馈网络三个子层组成,外面套残差和层归一化。全局平均池化常见于 CNN 分类网络,不出现在 Transformer 解码器层内。

Q9. 关于KV缓存机制的说法,正确的有哪些?

多选题 · hard

  • A.缓存历史词的键和值← 正确答案
  • B.用额外空间换取推理时间← 正确答案
  • C.缓存内容来自输入句子
  • D.每次生成都重算历史注意力

答案:AB。KV 缓存把每步新词的 K、V 追加进数组并复用,属于典型的空间换时间,其结果与不缓存严格等价。缓存存的是解码器自身的键值而非输入句子的原始内容;重算历史注意力恰恰是缓存要消除的低效行为。

Q10. 关于训练与推理差异的说法,正确的有哪些?

多选题 · hard

  • A.推理必须从起始符逐词进行← 正确答案
  • B.训练输入使用真实目标词← 正确答案
  • C.训练阶段也必须使用缓存
  • D.推理使用模型预测词续写← 正确答案

答案:ABD。训练用教师强制,把真实目标词右移后整体输入,配合因果掩码并行计算;推理没有真实词,只能从起始符开始,用预测词逐词续写。训练是整句并行,缓存没有用武之地,因此“训练也必须用缓存”是错误的。