<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Tsonglew&apos;s Blog</title><description>Tsonglew&apos;s Blog</description><link>https://tsonglew.github.io</link><item><title>22580：从 GPT-2 到 Kimi K3，一步步讲透</title><link>https://tsonglew.github.io/blog/kimi-k3-explained</link><guid isPermaLink="true">https://tsonglew.github.io/blog/kimi-k3-explained</guid><description>从 GPT-2 到 Kimi K3 的架构演化全解析。每一步架构变化都用可运行的 PyTorch 代码讲透——门控、路由、衰减与选择性机制，理解 22580 倍规模增长背后的每个决策。</description><pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;22580：从 GPT-2 到 Kimi K3，一步步讲透&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;导读&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这篇教程解读 X 用户 ali（ @waterloo_intern）的长文 &lt;a href=&quot;https://x.com/waterloo_intern/article/2081762065392541951&quot;&gt;《22580: From GPT2 to Kimi3, Explained》&lt;/a&gt;——一篇获得 1.15 万点赞、2000 转发、460 万浏览量的技术文章。原文作者 ali 在 baseten 做推理引擎研究，他用代码驱动的叙事方式，把从 GPT-2（2019，124M 参数）到 Kimi K3（2026，2.8 万亿参数）这条演化路径上的每一步架构变化，都用可运行的 PyTorch 片段讲透了。&lt;/p&gt;
&lt;p&gt;“22580”是全文的第一个钩子：一个 Kimi K3 模型里能塞下 22,580 个 GPT-2。七年放大 22,580 倍——但这不仅仅是规模，每一步架构变化都在解决一个具体问题。这篇教程模仿 &lt;em&gt;Hands-On&lt;/em&gt; 系列的风格，带你从代码层面理解每一步“为什么改”以及“怎么改”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;贯穿全文的主题&lt;/strong&gt;（原文最后一段的原话）：一个固定容量的联想记忆需要一个淘汰策略，因为纯粹的加法操作一旦到达容量上限就会产生干扰。为此，学习型的选择性机制——门控、路由或衰减——是必要的，而注意力是最有效的选择性读取机制。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;第一章 · GPT-2：一切的起点&lt;/h2&gt;
&lt;h3&gt;1.1 一个 GPT-2 长什么样&lt;/h3&gt;
&lt;p&gt;GPT-2 是一个 decoder-only（纯解码器）架构。原文给出了它最核心的前向传播代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;tok_emb = self.transformer.wte(idx)   # token 嵌入，形状 (b, t, n_embd)
pos_emb = self.transformer.wpe(pos)   # 位置嵌入，形状 (t, n_embd)
x = self.transformer.drop(tok_emb + pos_emb)
for block in self.transformer.h:
    x = block(x)
x = self.transformer.ln_f(x)
logits = self.lm_head(x)
return logits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整个过程可以这样理解：输入是一串 token ID（比如 &lt;code&gt;[42, 307, 634, ...]&lt;/code&gt;），先查表得到每个 token 的向量表示，再加上位置向量告诉模型“谁在前面”，然后依次过若干个 Transformer Block，最后用一个线性层把隐藏状态映射回词表大小的 logits——每个位置对应一个“下一个词是什么”的概率分布。&lt;/p&gt;
&lt;p&gt;输入先获得 token 嵌入和位置嵌入，两者相加后送入 Block 堆栈。&lt;/p&gt;
&lt;h3&gt;1.2 每个 Block 里面发生了什么&lt;/h3&gt;
&lt;p&gt;放大看单个 Block，结构出奇地简单——两个子层，各带一个 LayerNorm 和残差连接：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Block(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.ln_1 = LayerNorm(config.n_embd, bias=config.bias)
        self.attn = CausalSelfAttention(config)
        self.ln_2 = LayerNorm(config.n_embd, bias=config.bias)
        self.mlp = MLP(config)

    def forward(self, x):
        x = x + self.attn(self.ln_1(x))
        x = x + self.mlp(self.ln_2(x))
        return x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两行 forward 就概括了整个 Block 的逻辑：先归一化、算注意力、加残差；再归一化、算 MLP、加残差。“先归一化再算子层”是 Pre-Norm 结构，梯度比 Post-Norm 稳定。&lt;/p&gt;
&lt;h3&gt;1.3 注意力：QKV 的矩阵游戏&lt;/h3&gt;
&lt;p&gt;Block 里的注意力计算，展开来是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;B, T, C = x.size()  # batch, 序列长度, 嵌入维度

# 一步算出 Q、K、V
q, k, v = self.c_attn(x).split(self.n_embd, dim=2)
k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)  # (B, nh, T, hs)
q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)

# 手动实现的注意力
att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
att = att.masked_fill(self.bias[:,:,:T,:T] == 0, float(&apos;-inf&apos;))  # 因果 mask
att = F.softmax(att, dim=-1)
y = att @ v  # (B, nh, T, T) × (B, nh, T, hs) → (B, nh, T, hs)
y = y.transpose(1, 2).contiguous().view(B, T, C)  # 拼回所有头

y = self.resid_dropout(self.c_proj(y))
return y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键步骤拆解：&lt;code&gt;q @ k.transpose(-2, -1)&lt;/code&gt; 算出每个位置对其他所有位置的关注分数，得到一个 $T \times T$ 的矩阵；&lt;code&gt;masked_fill&lt;/code&gt; 把上三角（未来位置）填成 $-\infty$，保证只看过去；&lt;code&gt;softmax&lt;/code&gt; 把分数归一化成权重；最后 &lt;code&gt;att @ v&lt;/code&gt; 用权重对 Value 加权求和。这就是自注意力的全部。&lt;/p&gt;
&lt;h3&gt;1.4 KV 缓存：推理效率的第一个瓶颈&lt;/h3&gt;
&lt;p&gt;原文在讲完注意力后，话锋一转，抛出了整个系列文章的&lt;strong&gt;核心问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;生成模式下，模型逐词输出。每生成一个新词，都要重新跑一遍前向传播。但前面的词的 K 和 V 不会变——如果每步都重算，就是浪费。解法是把它们&lt;strong&gt;缓存&lt;/strong&gt;起来：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is an inefficiency of decoder-only generation: the model computes representations for every input position, but each decode step consumes only the final position&apos;s logits. Without caching, much of that work would be repeated for the next token.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;KV 缓存的观察很直接：把生成的新 token 追加到输入后，模型本该重新计算所有之前 token 的投影。存储它们的 key 和 value 向量就能避免这种冗余。&lt;/p&gt;
&lt;p&gt;这个存储就是 KV 缓存。它保留前 N-1 个 token 的向量，而且可能变得大到形成&lt;strong&gt;内存带宽瓶颈&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;原文给出了带 KV 缓存的注意力实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def forward(self, x, mask=None, past_kv=None):
    b, t, d = x.shape
    d_head = d // self.num_heads
    h = self.num_heads
    qkv = self.qkv_proj(x)

    q = qkv[:, :, :d].view(b, t, h, d_head).transpose(1, 2)
    k = qkv[:, :, d:2*d].view(b, t, h, d_head).transpose(1, 2)
    v = qkv[:, :, 2*d:].view(b, t, h, d_head).transpose(1, 2)

    # prefill 时 q,k,v 形状是 b,h,t,d
    # decode 时形状是 b,h,1,d
    # 所以在 t 维度上 cat

    if past_kv is not None:
        k_past = past_kv[0]
        v_past = past_kv[1]
        k = torch.cat((k_past, k), dim=2)
        v = torch.cat((v_past, v), dim=2)

    scores = (q @ k.transpose(-1, -2)) / math.sqrt(d_head)
    if past_kv is None:  # prefill 阶段需要 mask
        causal_mask = torch.ones(t, t, dtype=bool, device=q.device)
        causal_mask = torch.triu(causal_mask, diagonal=1)
        scores = scores.masked_fill(causal_mask, float(&apos;-inf&apos;))

    attn = scores.softmax(-1)
    o = attn @ v
    o = o.transpose(1, 2).contiguous().view(b, t, d)
    o_proj = self.o_proj(o)
    past_kv = (k, v)
    return o_proj, past_kv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;code&gt;torch.cat((k_past, k), dim=2)&lt;/code&gt; 这行——每次解码新 token 时，把新的 K、V 追加到缓存末尾。缓存随序列长度&lt;strong&gt;线性增长&lt;/strong&gt;，复杂度是 $O(N)$。每个解码步骤要从 HBM（显存）读两次 N 维向量、写两次 1 维向量，而 KV 缓存随序列长度线性膨胀。&lt;/p&gt;
&lt;p&gt;这就是后面所有架构创新的出发点：&lt;strong&gt;能不能让缓存不增长？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/0be0f748995f0b0b34d93f82bd52632f9837de0a83f369af3026bb12163572dd.png&quot; alt=&quot;KV缓存：缓存随序列线性增长&quot;&gt;&lt;/p&gt;
&lt;h3&gt;1.5 GPT-2 的规模&lt;/h3&gt;
&lt;p&gt;原文给出了 GPT-2 的配置，作为整篇文章的基准：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;vocab_size: int = 50304  # 50257 向上取整到 64 的倍数，为了效率
n_layer: int = 12
n_head: int = 12
n_embd: int = 768
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;约 5 万词表、12 层、12 头、768 维——总共约 124M 参数。而 Kimi K3 是 2.8 万亿参数。一个 Kimi K3 ≈ 22,580 个 GPT-2。接下来的章节，就讲这 22,580 倍的差距里，架构到底变了什么。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第二章 · 线性注意力：把增长的缓存折叠成固定状态&lt;/h2&gt;
&lt;h3&gt;2.1 问题：softmax 耦合了所有 Q 和 K&lt;/h3&gt;
&lt;p&gt;标准 softmax 注意力的核心操作是 $q \cdot k$ 之后再做 softmax，这让每个 query 都和每个 key 耦合在一起。你不能提前把一部分结果“折叠”掉——必须等所有 QK 对的分数都算出来才能 softmax。&lt;/p&gt;
&lt;p&gt;线性注意力的想法是：&lt;strong&gt;不做 softmax，改用一个特征函数分别处理 q 和 k&lt;/strong&gt;，让乘积变得可以重新结合。这样，不断增长的 K 和 V 向量序列就能被折叠进一个&lt;strong&gt;固定大小的 $D \times D$ 状态矩阵&lt;/strong&gt;里。&lt;/p&gt;
&lt;h3&gt;2.2 动手：线性注意力的实现&lt;/h3&gt;
&lt;p&gt;原文把标准注意力换成线性注意力后，代码变成了这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def forward(self, x, mask=None, cache=None):
    b, t, d = x.shape
    d_head = d // self.num_heads
    h = self.num_heads
    qkv = self.qkv_proj(x)

    q = qkv[:, :, :d].view(b, t, h, d_head).transpose(1, 2)
    k = qkv[:, :, d:2*d].view(b, t, h, d_head).transpose(1, 2)
    v = qkv[:, :, 2*d:].view(b, t, h, d_head).transpose(1, 2)

    # 关键变换：用 ELU+1 替代 softmax 的指数
    k = F.elu(k) + 1
    k = k.transpose(-1, -2)
    q = F.elu(q) + 1

    S, z = cache if cache is not None else (0.0, 0.0)
    S = S + k @ v       # 状态更新：外积累加
    z = z + k           # 归一化项累加

    o = q @ S           # 从状态读取
    denom = q @ z       # 归一化分母
    o_scaled = o / denom
    o_scaled = o_scaled.transpose(1, 2).contiguous().view(b, t, d)
    o_proj = self.o_proj(o_scaled)
    cache = (S, z)

    return o_proj, cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和第一章的 KV 缓存对比，最关键的变化在缓存部分。标准注意力的缓存是 &lt;code&gt;(k, v)&lt;/code&gt;——随序列增长的一堆向量。线性注意力的缓存是 &lt;code&gt;(S, z)&lt;/code&gt;——两个固定大小的矩阵和向量，&lt;strong&gt;不管序列多长都不增长&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;S = S + k @ v&lt;/code&gt; 是一个外积累加：每个新 token 的 key 和 value 做外积，加到状态矩阵 S 上。读取时 &lt;code&gt;o = q @ S&lt;/code&gt;，用 query 去状态里检索。&lt;code&gt;z&lt;/code&gt; 记录所有 key 的累加，用来做归一化。&lt;/p&gt;
&lt;h3&gt;2.3 注意力的三步拆解&lt;/h3&gt;
&lt;p&gt;原文在这里做了一个精妙的总结——所有注意力变体，本质上都做三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;让 QK 分数非负&lt;/strong&gt;。线性注意力用 ELU+1，softmax 用指数函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;除以总和&lt;/strong&gt;（归一化）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对 Value 加权求和&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;线性注意力保留了注意力的基本契约，但用表达能力较弱的特征函数替代了 softmax 的指数函数。这个近似会损失一些精度，但换来了固定大小的状态——缓存不再随序列增长。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/044a6f9f218be46de0a124ed8aaf7a715af5c2f189a2836dc0d197ef84966141.png&quot; alt=&quot;线性注意力：把增长的缓存折叠成固定状态&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2.4 代价：加法导致干扰&lt;/h3&gt;
&lt;p&gt;但这里有一个根本性的问题。&lt;/p&gt;
&lt;p&gt;线性注意力的状态 S 是一个 $D \times D$ 矩阵。序列有 100 万个 token 时，所有 token 的 key-value 关联都被压缩进这同一个矩阵里。当序列长度远大于 D 时（这正是线性注意力有吸引力的场景），状态就&lt;strong&gt;超容量&lt;/strong&gt;了。&lt;/p&gt;
&lt;p&gt;问题在于更新方式是&lt;strong&gt;纯加法&lt;/strong&gt;：&lt;code&gt;S = S + k @ v&lt;/code&gt;。新信息不断叠加，旧信息无法离开。就像一个白板，你只能往上写新内容，永远不擦——写到一定程度，所有内容都糊在一起，谁都读不清楚。&lt;/p&gt;
&lt;p&gt;原文引用了 Schlag 的论文（Fast Weight Programmers）的原话来描述这个问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“当序列长度超过存储容量时，模型可能进入超容量状态。在这种状态下，模型应该学会动态地与内存内容交互，有选择地决定保留哪些 key-value 关联、删除哪些。纯加法指令可能不适合这个目的…… endlessly adding new associations to a memory of finite size inevitably will reach a limit.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这就是下一章 DeltaNet 要解决的问题。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第三章 · DeltaNet：学会“先擦后写”&lt;/h2&gt;
&lt;h3&gt;3.1 核心思想：Delta 规则&lt;/h3&gt;
&lt;p&gt;线性注意力的更新是 &lt;code&gt;S = S + kᵀ @ v&lt;/code&gt;——无条件地把新信息加进去。&lt;/p&gt;
&lt;p&gt;DeltaNet 的思路是：&lt;strong&gt;在写入之前，先看看这个 key 方向上已经存了什么，只写入“差异”部分&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;想象白板上的一个格子。你不是直接往上面写新内容，而是先读一下格子里已有的内容，算出“新内容和旧内容的差异”，只把差异写上去。旧信息被自然替换，新信息被精确写入。&lt;/p&gt;
&lt;p&gt;代码是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def forward(self, x, mask=None, cache=None):
    b, t, d = x.shape
    d_head = d // self.num_heads
    h = self.num_heads
    qkv = self.qkv_proj(x)

    q = qkv[:, :, :d].view(b, t, h, d_head).transpose(1, 2)
    k = qkv[:, :, d:2*d].view(b, t, h, d_head).transpose(1, 2)
    v = qkv[:, :, 2*d:].view(b, t, h, d_head).transpose(1, 2)

    q = F.normalize(F.silu(q), dim=-1)
    k = F.normalize(F.silu(k), dim=-1)
    beta = torch.sigmoid(self.w_beta(x)).view(b, 1, t, 1)  # 新增：每 token 的写入强度

    S = cache if cache is not None else 0.0

    v_old = k @ S          # 读取：这个 key 方向上已存了什么
    u = beta * (v - v_old) # Delta：只取真正需要更新的部分
    S = S + k.transpose(-1, -2) @ u  # 写入：外积更新

    o = q @ S              # 读取，没有分母
    o = o.transpose(1, 2).contiguous().view(b, t, d)
    return self.o_proj(o), S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三行核心逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;v_old = k @ S&lt;/code&gt;：用当前 key 去状态里检索，看看这个方向上已经存了什么。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;u = beta * (v - v_old)&lt;/code&gt;：算出“想存的 v”和“已存的 v_old”的差。&lt;code&gt;beta&lt;/code&gt; 是一个学习到的写入强度（0 到 1 之间），控制每次更新多少。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;S = S + kᵀ @ u&lt;/code&gt;：只把差异写入状态。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/af1ccc7808794f558b214e9c814640f119b6420182b4e54a75577ee30b94d707.png&quot; alt=&quot;DeltaNet：先读后算差再写入&quot;&gt;&lt;/p&gt;
&lt;h3&gt;3.2 为什么这样能恢复信息&lt;/h3&gt;
&lt;p&gt;原文给了一个直观的解释。假设我们写入一个关联 &lt;code&gt;S = kᵀ @ v&lt;/code&gt;，然后用同样的 key 读回来：&lt;/p&gt;
&lt;p&gt;$$\text{读回} = k \cdot (k^\top v) = (k \cdot k^\top) \cdot v = |k|^2 \cdot v$$&lt;/p&gt;
&lt;p&gt;读回来的结果被 key 的平方范数缩放了。如果把 k 归一化到单位长度（代码里 &lt;code&gt;F.normalize&lt;/code&gt;），就能精确地读回 v。&lt;/p&gt;
&lt;p&gt;Q 也是一个学习到的“指针”：$W_q$ 和 $W_k$ 读取同一个残差流，所以一个事实被写入时用的 key 方向，和后来检索它时用的 query 方向天然对齐。更新时先问“当前 key 从缓存里检索出了什么信息”，从要存的 value 里减掉这些已有信息，乘以 key，加回去。旧信息被移除，新信息被写入。&lt;/p&gt;
&lt;h3&gt;3.3 和线性注意力的对比&lt;/h3&gt;
&lt;p&gt;|  | 线性注意力 | DeltaNet |
| --- | --- | --- |
| 更新方式 | &lt;code&gt;S += kᵀ @ v&lt;/code&gt;（纯加法） | &lt;code&gt;S += kᵀ @ (β(v - kS))&lt;/code&gt;（先读后算差再写） |
| 能否覆盖旧信息 | 不能，只能叠加 | 能，用 delta 替换 |
| 超容量时的行为 | 干扰不断累积 | 可以精确更新单个关联 |
| 能否主动遗忘 | 不能 | 不能（只能替换有对应 key 的关联） |&lt;/p&gt;
&lt;p&gt;注意最后一行——DeltaNet 解决了“精确更新”问题，但引入了新限制：它只能替换有具体替代品的关联。如果模型需要一次性清除多个关联（比如上下文切换时），或者需要整体衰减记忆来释放容量，DeltaNet 做不到。这是下一章 Gated DeltaNet 要解决的。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第四章 · DeltaNet 的并行化：最硬核的一节&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;原文说“这是全文最难的部分，我花了大约七个小时才建立起可用的理解”，所以会从实现代码出发来讲。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;4.1 问题：Delta 规则是串行的&lt;/h3&gt;
&lt;p&gt;上面的 Delta 规则看起来很优雅，但有一个工程问题：&lt;strong&gt;它是逐 token 串行的&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;S = torch.zeros(b, h, dh, dh) if cache is None else cache
outs = []
for i in range(t):
    k_i = k[:, :, i:i+1]
    v_i = v[:, :, i:i+1]
    b_i = beta[:, :, i:i+1]
    v_old = k_i @ S                   # 读
    u_i  = b_i * (v_i - v_old)        # 算 delta
    S = S + k_i.transpose(-1, -2) @ u_i  # 写
    outs.append(q[:, :, i:i+1] @ S)   # 用更新后的 S 读
o = torch.cat(outs, dim=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个 token 的更新都依赖前一个 token 更新后的状态 S——这没法并行。即使没有 Delta 规则，纯线性注意力的 prefill 也是串行的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;S = torch.zeros(b, h, dh, dh) if cache is None else cache
outs = []
for i in range(t):
    q_i = q[:, :, i:i+1]
    k_i = k[:, :, i:i+1]
    v_i = v[:, :, i:i+1]
    S = S_old + k_i @ v_i
    o = q_i @ S
    o = self.norm(o)
    outs.append(o)
o = torch.cat(outs, dim=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GPU 最擅长的是大规模矩阵乘法，而逐 token 的串行循环完全浪费了这种能力。&lt;/p&gt;
&lt;h3&gt;4.2 分块：在串行和并行之间找平衡&lt;/h3&gt;
&lt;p&gt;解法是&lt;strong&gt;分块（Chunking）&lt;/strong&gt;。把序列切成大小为 C 的块，块内做标准注意力（可以并行），块间做递归状态更新（串行但次数少）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;S = torch.zeros(b, h, dh, dh) if cache is None else cache
outs = []
for i in range(t // C):
    q_c = q[:, :, i*C:(i+1)*C]
    k_c = k[:, :, i*C:(i+1)*C]
    v_c = v[:, :, i*C:(i+1)*C]

    o_prev = q_c @ S                           # 跨块：递归读取

    attn = (q_c @ k_c.transpose(-1, -2)).tril()  # 块内：因果注意力
    o_curr = attn @ v_c

    o = o_prev + o_curr                        # 两部分相加

    S_new = k_c.transpose(-1, -2) @ v_c        # 递归状态更新
    S = S + S_new
    outs.append(o)

o = torch.cat(outs, dim=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原文的解释非常清晰：&lt;strong&gt;块内做 &lt;strong&gt;&lt;code&gt;q(kᵀv)&lt;/code&gt;&lt;/strong&gt;——先算分数，是带 mask 的标准注意力顺序。块间做 &lt;strong&gt;&lt;code&gt;(kᵀv)q&lt;/code&gt;&lt;/strong&gt;——先更新状态再读取，是递归顺序。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;计算量分两部分：固定部分 $2Ld^2$（状态更新，与 C 无关）和增长部分 $2LCd$（块内的注意力矩阵）。当 C=L 时退化为标准 $O(N^2)$ 注意力；当 C=1 时退化为纯线性注意力。C 越小，FLOP 越少。&lt;/p&gt;
&lt;p&gt;但 C=1 不一定最快——GPU 的张量核心在 C=64 或 128 时效率最高。这就是工程上的权衡。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/d99ed1406c86fa7c29c8ee16db4ae2964302081eded81ab447a23604172aec3e.png&quot; alt=&quot;分块并行：在串行和并行之间找平衡&quot;&gt;&lt;/p&gt;
&lt;h3&gt;4.3 把 Delta 规则也分块化&lt;/h3&gt;
&lt;p&gt;纯加法注意力的分块很直接，但 Delta 规则不行，因为每步都需要 &lt;code&gt;v_old = k_i @ S&lt;/code&gt; 来计算要减去的旧信息。你需要每个 token 的状态才能算出 delta——没法直接并行。&lt;/p&gt;
&lt;p&gt;作者的解法是&lt;strong&gt;数学重参数化&lt;/strong&gt;。把原来的 Delta 更新：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;u = v_new - v_old
S_t = S_{t-1} + Kᵀ @ u
o = q @ S_T
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改写成 Householder 矩阵的形式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;S_t = S_{t-1}(I − β_t k_t k_tᵀ) + β_t v_t k_tᵀ
o_t = S_t q_t
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个形式允许一个块内所有 C 个 delta 一次性算出来。完整代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def chunk_delta_rule_forward(Q, K, V, beta, C):
    L, d = Q.shape
    # 分块
    Q, K, V = map(lambda x: x.reshape(-1, C, d), [Q, K, V])
    beta = beta.reshape(-1, C)
    K_beta = K * beta.unsqueeze(-1)
    V_beta = V * beta.unsqueeze(-1)

    # 用向量化前代换计算快速逆矩阵（公式 10）
    T = -(K_beta @ K.t()).tril(-1)
    for i in range(1, C):
        T[i, :i] = T[i, :i] + (T[i, :, None] * T[:, :i]).sum(-2)

    T += torch.eye(C)
    W = T @ K_beta
    U = T @ V_beta

    # 分块并行（公式 8-9）
    S = torch.zeros(d, d)
    O = torch.empty_like(V)

    for i in range(L // C):
        q_i, k_i, w_i = Q[i], K[i], W[i]
        u_i = U[i] - w_i @ S        # 整个块的修正项
        o_inter = q_i @ S           # 跨块递归读取
        A_i = (q_i @ k_i.t()).tril()  # 块内因果注意力
        o_intra = A_i @ u_i         # 注意力 × 修正后的 value
        S += k_i.t() @ u_i          # 状态更新
        O[i] = o_intra + o_inter    # 块内 + 跨块
    return O.reshape(L, d)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码的精髓：&lt;code&gt;u_i = U[i] - w_i @ S&lt;/code&gt; 把“读取旧信息、计算 delta”这一串串行操作，变成了可以用预计算的 W、U 矩阵一次性完成的并行操作。代价是需要先算一个下三角矩阵 T（用前代换求逆），但这在块内（C=64 或 128）规模很小，可以高效完成。&lt;/p&gt;
&lt;p&gt;至此，我们有了第一个对比基准：MHA（标准多头注意力）vs DeltaNet Transformer。DeltaNet 在保持线性复杂度的同时，通过 Delta 规则实现了精确的状态更新。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第五章 · Gated DeltaNet：让模型学会遗忘&lt;/h2&gt;
&lt;h3&gt;5.1 Delta 规则的局限&lt;/h3&gt;
&lt;p&gt;DeltaNet 能精确更新单个关联——但它只能替换有对应替代品的关联。如果模型需要做&lt;strong&gt;上下文切换&lt;/strong&gt;（一次性清除一批不再相关的关联），或者需要&lt;strong&gt;整体衰减&lt;/strong&gt;记忆来释放容量，Delta 规则做不到。&lt;/p&gt;
&lt;p&gt;换句话说，Delta 规则能“改”但不能“忘”。&lt;/p&gt;
&lt;h3&gt;5.2 Mamba 的解法：加一个遗忘门&lt;/h3&gt;
&lt;p&gt;如果做的是纯加法线性注意力，加遗忘能力很简单——只需要一个参数控制状态的衰减：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;S_old = cache
S_new = k @ v
# 原来：cache = S_old + S_new
cache = alpha * S_old + S_new   # 新：旧状态衰减后再加新状态
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是 Mamba-2 的贡献：先衰减旧缓存，再以满强度加入新缓存，防止状态无限增长。&lt;/p&gt;
&lt;p&gt;但 Mamba 的衰减是&lt;strong&gt;均匀的&lt;/strong&gt;——所有 key-value 关联以同一个比例衰减。如果模型只需要遗忘一个特定的关联，所有关联都被等量遗忘。Delta 规则则相反——能更新单个事实，但没法让其他事实衰减。&lt;/p&gt;
&lt;h3&gt;5.3 Gated Delta 规则：两者结合&lt;/h3&gt;
&lt;p&gt;Gated Delta 规则把 Mamba 的门控衰减和 Delta 规则结合在一起。它加了一个参数 $\alpha$（0 到 1 之间）：$\alpha=1$ 时退化为纯 Delta 规则，$\alpha=0$ 时清空记忆。&lt;/p&gt;
&lt;p&gt;实现上用和 DeltaNet 相同的重参数化方法，数学几乎一样，只多了一个&lt;strong&gt;数据相关的标量衰减&lt;/strong&gt;。原文指出，$\gamma^r/\gamma^i$ 项处理的是累积衰减：在时间步 $x$ 写入、在 $x+t$ 读取的 token，它的值已经被乘了 $\alpha_x \alpha*{x+1} \alpha*{x+2} \cdots \alpha_{x+t}$——这是前缀和计算的乘法类比。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🎯 &lt;strong&gt;演化逻辑&lt;/strong&gt;：线性注意力（只能加）→ DeltaNet（能改不能忘）→ Gated DeltaNet（既能改又能忘）。每一步都补上了上一步缺少的一种&quot;对记忆的操作&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/4ac691fa13ed4aa04bd8fbc473b8e6bc2504edb02352162ae334f19e2aefadec.png&quot; alt=&quot;Gated DeltaNet：让模型学会遗忘&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第六章 · KDA / Kimi Linear：逐通道精细门控&lt;/h2&gt;
&lt;h3&gt;6.1 从标量衰减到逐通道衰减&lt;/h3&gt;
&lt;p&gt;Gated DeltaNet 用一个标量 $\alpha$ 控制整层的衰减。但不同通道（不同维度的特征）可能需要不同的遗忘速度——有些通道存的是长期事实，有些存的是临时上下文。&lt;/p&gt;
&lt;p&gt;Kimi Linear 的核心改进：&lt;strong&gt;为每个通道学习一个独立的衰减值&lt;/strong&gt;。不再是一个标量 $\alpha$，而是一个向量 $\boldsymbol{\alpha} \in \mathbb{R}^d$。&lt;/p&gt;
&lt;p&gt;原文指出，&lt;code&gt;alpha.reshape(nb, C, d)&lt;/code&gt; 捕捉了论文最重要的贡献：对记忆衰减的&lt;strong&gt;细粒度控制&lt;/strong&gt;。KDA 的更新规则和 Gated DeltaNet 类似，但衰减从标量变成了逐通道的向量。&lt;/p&gt;
&lt;h3&gt;6.2 不只是替换注意力——而是一个混合架构&lt;/h3&gt;
&lt;p&gt;把 KDA 放在 DeltaNet Transformer 旁边，Kimi Linear 引入了三个重大变化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;混合系统&lt;/strong&gt;：交替使用 KDA 层和多头潜注意力（MLA）层。大部分层用高效的 KDA（固定状态），少量层用标准 softmax 注意力（全局检索）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MoE 替代 MLP&lt;/strong&gt;：前馈网络换成混合专家层。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DeltaNet 容量增强&lt;/strong&gt;：通过 alpha 投影给 DeltaNet 增加容量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;原文在这里强调了一个重要观点：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;This is not blind scaling. The additional capacity has a specific mathematical purpose: the per-channel scale gives the model finer control over memory decay.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;每一步架构演进增加的容量都有明确的数学目的——不是盲目堆参数，而是在&quot;正确的地方、以系统能利用的形式&quot;增加容量。每个架构都在解决前一个架构的具体限制。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/c4f342e7a9a903a9d61e4917b7e9a249e53325fdc9ed773d579c8757cc973bde.png&quot; alt=&quot;KDA：逐通道精细门控&quot;&gt;&lt;/p&gt;
&lt;h3&gt;6.3 Kimi Linear 的关键主张&lt;/h3&gt;
&lt;p&gt;原文提到，Kimi Linear 引起了关注，核心主张是：&lt;strong&gt;在受控对比中，它超越了全注意力&lt;/strong&gt;。作者把它呈现为一个即插即用的架构替代品，质量更好且解码吞吐量最高达 6 倍。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第七章 · Kimi K3：一切汇总&lt;/h2&gt;
&lt;h3&gt;7.1 整体架构&lt;/h3&gt;
&lt;p&gt;Kimi K3 的语言骨干和 Kimi Linear 很像。它包含 &lt;strong&gt;23 个四层宏周期&lt;/strong&gt;。每个宏周期里，三层用 KDA，第四层用 MLA。第一层用稠密 FFN，其余每层用 Latent MoE。&lt;/p&gt;
&lt;p&gt;原文列出了从 Kimi Linear 到 K3 的变化，看起来不大：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;规模大幅增加&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每 12 层一次 Blockwise AttnRes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MLA query LoRA 和输出门控&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Latent-space MoE&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SiTU 激活函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Gated MLA&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KDA 提供恒定状态的递归记忆，而周期性的 MLA 层保留了对上下文的完整 softmax 检索能力。&lt;/p&gt;
&lt;h3&gt;7.2 Gated MLA：控制多少特征进入残差流&lt;/h3&gt;
&lt;p&gt;Gated MLA 决定从 MLA 检索到的特征有多少进入残差流——通过一个从输入投影出来的门控做逐元素乘法来实现。&lt;/p&gt;
&lt;h3&gt;7.3 Latent-space MoE：压缩的专家&lt;/h3&gt;
&lt;p&gt;在传统 MoE 中，一个学习到的路由器用点积相似度把每个 token 发送到一部分专家网络。Kimi K3 总共有 &lt;strong&gt;898 个专家&lt;/strong&gt;：2 个是共享专家（每个 token 都过），剩下 896 个里路由器为每个 token 选 16 个。&lt;/p&gt;
&lt;p&gt;Kimi K3 还改变了专家的激活方式，用 SiTU 替代 SiLU：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;d = x.shape[-1] // 2
gate = x[..., :d].to(torch.float32)
up = x[..., d:].to(torch.float32)
situ_a = self.beta * torch.tanh(gate / self.beta) * torch.sigmoid(gate)
if self.linear_beta is not None:
    up = self.linear_beta * torch.tanh(up / self.linear_beta)
return (situ_a * up).to(x.dtype)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模型还把共享专家的输入下投影、输出上投影——专家在压缩的潜空间中运算，前向传播快得多，FLOP 几乎减半。不过原文也提到一个工程挑战：没有融合内核的话，新激活函数比原来慢近 3 倍。&lt;/p&gt;
&lt;h3&gt;7.4 剩余的改进&lt;/h3&gt;
&lt;p&gt;MLA query LoRA 和输出门控、每 12 层的 Blockwise Attention Residuals。AttnRes 增加约 2% 的推理延迟，但提供两个重要好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;选择性检索早期表示&lt;/strong&gt;，缓解残差稀释和隐藏状态增长&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;1.25 倍的计算优势&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原文在这里点出了 AttnRes 和 MLA 的互补关系：KDA 层以恒定大小状态运行，不可避免地丢弃信息；MLA 从 token 上下文中检索，而 AttnRes 从更早的&lt;strong&gt;深度方向&lt;/strong&gt;表示中检索。两者从不同方向解决同一个“信息丢失”问题。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第八章 · AttnRes：让深层模型选择性回看&lt;/h2&gt;
&lt;h3&gt;8.1 标准残差连接的问题&lt;/h3&gt;
&lt;p&gt;在标准 Transformer 中，每层的输入是原始嵌入和所有前层输出的等权累加：&lt;/p&gt;
&lt;p&gt;$$h_i = h_1 + \sum_{j=1}^{i-1} f_j(h_j)$$&lt;/p&gt;
&lt;p&gt;$h_i$ 是第 $i$ 层的输入，$h_1$ 是当前 token 的嵌入，$f_j(h_j)$ 是第 $j$ 层的输出。&lt;/p&gt;
&lt;p&gt;问题是&lt;strong&gt;缺乏选择性访问&lt;/strong&gt;。不同类型的层接收相同的聚合状态，即使它们可能受益于不同的加权。而且因为递归是纯加法的，后面的层必须学越来越大的输出才能影响累积的残差——这会破坏训练稳定性。&lt;/p&gt;
&lt;h3&gt;8.2 AttnRes：用注意力做深度方向的检索&lt;/h3&gt;
&lt;p&gt;AttnRes 不是把所有前层输出等权相加，而是给每一项乘一个&lt;strong&gt;学习到的权重&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$\tilde{h}&lt;em&gt;i = \sum&lt;/em&gt;{j} \alpha_{ij}, v_j, \qquad \alpha_{ij} = \frac{\exp(q_i \cdot k_j)}{\sum_{j&apos;} \exp(q_i \cdot k_{j&apos;})}$$&lt;/p&gt;
&lt;p&gt;每个权重 $\alpha_{ij}$ 由 query-key 点积算出。query 为每层学习，key 和 value 来自更早的残差流状态。分数归一化后，用它们对历史状态做加权组合。&lt;/p&gt;
&lt;p&gt;这本质上是把“注意力”机制用在了&lt;strong&gt;深度方向&lt;/strong&gt;而非序列方向——让第 50 层可以“回看”第 10 层的表示，如果第 10 层的表示对当前计算更有用的话。&lt;/p&gt;
&lt;h3&gt;8.3 Block 粒度：每 12 层一次&lt;/h3&gt;
&lt;p&gt;如果每层都做 AttnRes，训练和推理成本太高。Kimi K3 的做法是&lt;strong&gt;只在固定的块边界&lt;/strong&gt;做——每 12 个解码器层之后做一次。在 23 个四层宏周期中，产生 8 个 AttnRes 块。&lt;/p&gt;
&lt;p&gt;核心伪代码非常简洁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;V = torch.stack(blocks + [partial_block])  # [N+1, B, T, D]
K = norm(V)
logits = torch.einsum(&apos;d, n b t d -&gt; n b t&apos;, proj.weight.squeeze(), K)
h = torch.einsum(&apos;n b t, n b t d -&gt; b t d&apos;, logits.softmax(0), V)
return h
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把多个 block 的表示堆叠成 V，归一化后做 query-key 点积得到 logits，softmax 后加权求和。一个 block 是 12 个解码器层（注意力和 MLP 输出的逐元素和）累积的单一深度表示，用于后续的 AttnRes 混合。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/e084d93b07026af4c5a4fb9827b15a3d08fd24bbd218aafcd6a2ec64385a869c.png&quot; alt=&quot;AttnRes：深度方向的选择性回看&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;AttnRes 的本质&lt;/strong&gt;：标准残差连接是&quot;把所有过去加起来&quot;，AttnRes 是&quot;用注意力选择性地把有用的过去加起来&quot;。模型不再只能依赖紧邻的前一层，而是可以选择性地检索任何更早层的输出。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;结语 · 一条贯穿始终的主线&lt;/h2&gt;
&lt;p&gt;原文的最后两段是整篇文章的点睛之笔：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The central change is not scale alone. Each architectural step changes what the model stores, how it updates that state, or how it retrieves information that a fixed-size state cannot preserve.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;每一步架构变化都不是单纯的规模增长，而是改变了模型&lt;strong&gt;存储什么&lt;/strong&gt;、&lt;strong&gt;怎么更新状态&lt;/strong&gt;、或&lt;strong&gt;怎么检索固定状态保存不了的信息&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;A fixed-capacity associative memory needs an eviction policy, since a purely additive linear operation eventually adds interference once at capacity. To that end, learned selection — like gating, routing, or decay — is necessary, and attention is the most effective selective-read mechanism.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个固定容量的联想记忆需要淘汰策略。纯粹的加法操作一旦到达容量上限就会产生干扰。为此，学习型的选择性机制——门控、路由或衰减——是必要的，而&lt;strong&gt;注意力是最有效的选择性读取机制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;回看整条演化路径，每一步都在回答同一个问题的不同侧面：&lt;/p&gt;
&lt;p&gt;| 架构 | 存储什么 | 怎么更新 | 怎么检索 | 淘汰策略 |
| --- | --- | --- | --- | --- |
| GPT-2 (MHA) | 全序列 KV（O(N) 增长） | 追加 | Softmax 注意力 | 无（缓存无限增长） |
| 线性注意力 | 固定 D×D 状态 | 纯加法 &lt;code&gt;S += kᵀv&lt;/code&gt; | &lt;code&gt;q @ S&lt;/code&gt; | 无（只加不减） |
| DeltaNet | 固定 D×D 状态 | Delta 规则（先读后算差再写） | &lt;code&gt;q @ S&lt;/code&gt; | 精确替换（需要对应 key） |
| Gated DeltaNet | 固定 D×D 状态 | Delta + 标量衰减 | &lt;code&gt;q @ S&lt;/code&gt; | 替换 + 均匀衰减 |
| KDA | 固定 D×D 状态 | Delta + 逐通道衰减 | &lt;code&gt;q @ S&lt;/code&gt; | 替换 + 逐通道精细衰减 |
| Kimi K3 | KDA 状态 + MLA KV 缓存 + AttnRes | 上述全部 | KDA 读取 + MLA softmax + 深度注意力 | 逐通道衰减 + 稀疏路由 + 深度选择性检索 |&lt;/p&gt;
&lt;p&gt;Kimi K3 最终组合了四种机制：恒定状态的递归记忆（KDA）、周期性的 softmax 检索（MLA）、稀疏的专家容量（Latent MoE）、以及选择性的深度方向残差访问（AttnRes）。结果是&lt;strong&gt;一个在特定功能角色上花费额外容量的系统&lt;/strong&gt;——每多花的一分算力，都有明确的数学目的。&lt;/p&gt;
&lt;p&gt;这就是从 GPT-2 到 Kimi K3 的故事。不是 22,580 倍的规模堆砌，而是 22,580 倍背后，每一步都精确地回答了“固定容量的记忆，该怎么管理自己”。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>补上每个缺口：Transformer 通俗进阶七讲</title><link>https://tsonglew.github.io/blog/transformer-seven-lectures</link><guid isPermaLink="true">https://tsonglew.github.io/blog/transformer-seven-lectures</guid><description>从最简单的猜词游戏出发，七讲逐步补全 Transformer 的每个缺口：RNN 困境、Attention 诞生、QKV、位置编码、因果 Mask 与训练技巧。不需要任何前置知识。</description><pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;补上每个缺口：Transformer 通俗进阶七讲&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;这不是一篇零件清单。这是一条河——从最简单的“猜词游戏”出发，每遇到一个障碍就补一个缺口，缺口补完，Transformer 就长出来了。七讲，七步，每一步都只解决一个具体问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;第一讲：起点——语言模型就是猜下一个词&lt;/h2&gt;
&lt;p&gt;你玩过“成语接龙”吗？我说“亡羊补”，你接“牢”。你说“牢不可”，我接“破”。规则就一条：&lt;strong&gt;根据已经出现的字，猜下一个字&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;语言模型做的和这一模一样。给它“今天天气真”，下一个字大概率是“好”。它不“理解”天气，只是从海量文本中学会了什么后面跟什么。后面所有的复杂机制，都是让这个预测更准。&lt;/p&gt;
&lt;p&gt;但计算机不认得字，所以第一步是把词变成数字——每个词分配一串坐标，意思相近的词靠得近。&lt;strong&gt;词先变成数字，模型才能算。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/30b4fa9c9669a926d195333a2c1cde053e78b4b642689f04fddc369ac0f0dca7.png&quot; alt=&quot;插图1：语言模型猜下一个词&quot;&gt;&lt;/p&gt;
&lt;p&gt;假设你来设计这个“猜词机器”，最笨的办法是只看上一个词——“天气”后面跟“好”的概率多少，查表就行。但“今天天气真”后面跟“好”，如果只看上一个词“真”，你完全不知道前面有没有“今天天气”。句子可以无限长存不下所有词，那就退一步：把前文压缩成一段摘要带着走，每来一个新词就把旧摘要和新词揉一揉。恭喜，你刚发明了 RNN 的核心思想。&lt;/p&gt;
&lt;p&gt;猜词真需要这么复杂的机器吗？“一推六二五”——前四个字定了第五个字几乎唯一确定。但“他推了推眼镜”和“他推了推门”，“推”字相同后面完全不同。&lt;strong&gt;猜词的难度不在词本身，而在前文有多长、哪些信息跟当前有关。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第二讲：RNN、长句困境与 Attention 的诞生&lt;/h2&gt;
&lt;p&gt;具体怎么揉？想象一队人排成一列传话。第一个人听到“今天”在纸条上写“今天”传下去，第二个人听到“天气”就把纸条打开看一眼，结合自己听到的重新写一行摘要再传。这个不断改写传递的“小纸条”就是 RNN 的&lt;strong&gt;隐藏状态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/a2bd2c2a7bf5d780867316d87638f6bf5cf9ce67d9a278aa04ce503fe92264d8.png&quot; alt=&quot;插图2：RNN的隐藏状态像小纸条&quot;&gt;&lt;/p&gt;
&lt;p&gt;但句子长了纸条越来越不够用。50 个词传到第 50 个人，第一个人写的“今天”还在吗？大概率被挤没了——每步都在揉，揉了 50 次最早的信息早被覆盖了。纸条大小固定不能无限扩大。这不是彻底消失，而是像复印 50 次，第一次的笔迹淡到几乎看不见。数学上叫&lt;strong&gt;梯度消失&lt;/strong&gt;：反向传播的修正信号每传一层衰减一次，传到第一层时几乎为零。LSTM 和 GRU 用“门控”缓解（让纸条选择性遗忘和保留），但治标不治本——只要还在“逐词压缩传递”这条路上，长距离信息丢失就是结构性的通病。&lt;/p&gt;
&lt;p&gt;这个缺陷在翻译任务里更致命。RNN 做翻译时，先用 Encoder 把整句中文读到尾，最后那张纸条就是整句话的“总摘要”，然后 Decoder 拿着它一个词一个词往外吐英文。&lt;strong&gt;一个固定大小的总行李箱，装着整句的意思，全程不变。&lt;/strong&gt; 句子一长就开始丢三落四。&lt;/p&gt;
&lt;p&gt;你会想：Decoder 吐每个词时不应该只盯着总行李箱，应该能“回头看”原句。但原句那么长，总不能每次从头到尾重读。更聪明的办法是：&lt;strong&gt;每吐一个词，就自动算一下“原句哪个位置对我现在最有用”，重点看那个位置。&lt;/strong&gt; 粗线代表高度相关，细线代表关系不大。这个“按需回看、按重要性加权”的机制，就是 &lt;strong&gt;Attention&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/b4b506f8d20f37cdaf82f545edd9dcd96361fb238516991b70d29c0157520a33.png&quot; alt=&quot;插图3：Attention按需回看原句&quot;&gt;&lt;/p&gt;
&lt;p&gt;Attention 和 RNN 是绑定的吗？在最初的 Encoder-Decoder 里它确实是 RNN 的小跟班。但有人很快发现了一件惊人的事：&lt;strong&gt;“回看”这个动作本身，并不依赖逐词传递。&lt;/strong&gt; 这正是下一讲的起点。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第三讲：Attention 独立——QKV 与 Self-Attention&lt;/h2&gt;
&lt;p&gt;Attention 的核心是三步：&lt;strong&gt;算相关性&lt;/strong&gt;（Query 跟 Key 匹配，算出相似度分数）→ &lt;strong&gt;加权&lt;/strong&gt;（分数高的多取，低的少取）→ &lt;strong&gt;取回&lt;/strong&gt;（按权重把 Value 混合）。这三步全是矩阵乘法，&lt;strong&gt;不需要“先读第一个词、再读第二个词”这种串行步骤&lt;/strong&gt;。原句所有位置的 Key 可以同时算好，Query 也可以同时和所有 Key 做匹配。没有纸条传递，没有逐词压缩，没有信息被挤掉。2017 年那篇论文（《Attention Is All You Need》）做的就是把 RNN 拆掉只留 Attention，结果不仅没变差反而更好更快。&lt;/p&gt;
&lt;p&gt;那 Query、Key、Value 从哪来？打个比方：你去图书馆找书，心里有个检索条件是 &lt;strong&gt;Query&lt;/strong&gt;，书架上每本书的标签是 &lt;strong&gt;Key&lt;/strong&gt;——你拿 Query 和每本书的 Key 比对，匹配度高的重点看。但你要的不是标签本身，而是书的&lt;strong&gt;内容&lt;/strong&gt;——这就是 &lt;strong&gt;Value&lt;/strong&gt;。三个角色可以从同一份数据出发，通过不同的线性变换“长成”不同形状，参数是模型自己学的：&lt;/p&gt;
&lt;p&gt;$$Q = X W_q, \quad K = X W_k, \quad V = X W_v$$&lt;/p&gt;
&lt;p&gt;$$\text{Attention}(Q, K, V) = \text{softmax}!\left(\frac{Q K^T}{\sqrt{d_k}}\right) V$$&lt;/p&gt;
&lt;p&gt;Q 和 K 算匹配分数 → softmax 归一化成权重 → 拿权重混合 V。$\sqrt{d_k}$ 是缩放因子，防止分数太大导致 softmax 变成非 0 即 1 的极端分布。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/5b912a6b959436e5c94b6dff0dcf47e0dbdee9a7f8164909ce48fb4ddc9cdb9e.png&quot; alt=&quot;插图4：QKV的三种职责&quot;&gt;&lt;/p&gt;
&lt;p&gt;为什么从同一份数据变换出来？翻译时确实不是同一份——Query 来自 Decoder，Key 和 Value 来自 Encoder。但更常见的是一句话内部的词互相注意（Self-Attention），三者来自同一批词。通过可学习矩阵分出不同职责，比硬指定灵活得多：一个词可以“因为某个特征被选中”，但“交出的是另一个特征”。&lt;/p&gt;
&lt;p&gt;Self-Attention 就是让一句话里的词两两互相询问。“那只猫没过马路因为它太累了”——“它”指谁？在 Self-Attention 里，“它”的 Query 去和句子里所有词的 Key 做匹配，和“猫”的 Key 匹配度高，和“马路”低。于是“它”的输出向量里混入了大量“猫”的 Value。&lt;strong&gt;“它”就“看到”了“猫”。&lt;/strong&gt; 每个词既是 Query（主动找相关词），又是 Key（等着被匹配），又是 Value（被选中后交出内容）。一轮跑完，每个词的向量都变成了“吸收了全句相关信息的增强版自己”。指代消解、语境消歧全在这一步自然发生，不需要专门规则。&lt;/p&gt;
&lt;p&gt;代价是计算量 $n^2$（n 是句子长度）——1000 个词就是 100 万次匹配。后来的变体（Longformer、Linformer）在想办法降低，但经典 Transformer 宁可算量大也要全连接。还有一个矛盾：Self-Attention 全互联，位置 3 能看到位置 5，做生成时位置 5 是未来词——不能偷看。怎么管住？第六讲的因果 Mask 会解决。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第四讲：位置编码——告诉模型“谁在第几号座位”&lt;/h2&gt;
&lt;p&gt;Attention 只看向量匹配，不管顺序。“狗咬人”和“人咬狗”向量完全一样只是排列不同，Attention 算出来一模一样。&lt;/p&gt;
&lt;p&gt;既然 Attention 自己不感知位置，那就&lt;strong&gt;人为把位置信息编码进每个词的向量里&lt;/strong&gt;。想象看电影——座位号印在椅背上不是印在观众身上。让“狗”在第 1 号座位和第 3 号座位时的向量不一样，不是改“狗”的意思，而是叠加一个“我在第几号”的信号。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/1521f7862ce7aa1fa73b1daa1dd9690bf6d00439a798aa8e304a94bec0552703.png&quot; alt=&quot;插图5：位置编码就是座位号&quot;&gt;&lt;/p&gt;
&lt;p&gt;原始论文用正弦和余弦函数按位置编号生成一段固定向量，和词向量直接相加。“第 3 号座位的狗” = “狗”的词向量 + “第 3 号位置”的编码向量，同时包含“是什么词”和“在第几个位置”。能不能让位置编码也可学习？BERT 就是这么做的，但训练时最长 512，第 513 个位置就没学过。正弦余弦编码的好处是可以外推——理论上任意长度都能生成。后来又有相对位置编码、旋转位置编码（RoPE）等变体，但核心不变：&lt;strong&gt;把“我在第几号”塞进向量里。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;位置编码和词向量直接相加不会互相干扰吗？会混，但这正是模型想要的。相加后是一个“在某个位置上的某个词”的整体表示，后续 Attention 自然把语义成分和位置成分一起用——就像看地图时地名和坐标叠在一起看。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第五讲：Multi-Head 与 Transformer Block——多副眼镜，再垒成楼&lt;/h2&gt;
&lt;p&gt;一句话里的关系是多维的。“那只猫没过马路因为它太累了”——“它”和“猫”有指代关系，“猫”和“过”有主谓关系，“马路”和“过”有动宾关系。一轮 Attention 只能学出一组匹配模式，重点学了指代就可能顾不上主谓。&lt;strong&gt;一副眼镜看不全。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Multi-Head 的思路粗暴而有效：&lt;strong&gt;别只做一轮，做多轮，每轮用不同参数。&lt;/strong&gt; 8 个头就是 8 副不同参数的眼镜，各自独立做一轮 Attention，最后拼接融合。计算量不增——原来 512 维切成 8 份每头 64 维，总维度还是 512。可视化研究发现许多头确实学到了不同功能：有的关注相邻词，有的关注句法依赖，有的关注标点。即使有冗余，容错性也更好。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://image.youmind.com/gen-images/c33a392e5095d58209da9733834c591dee31163c350591e701ceaa176bece8eb?width=4000&amp;#x26;height=4000&amp;#x26;fit=scale-down&quot; alt=&quot;插图6：Multi-Head多副眼镜看不同线索&quot;&gt;&lt;/p&gt;
&lt;p&gt;有了 Multi-Head Self-Attention，但单独不够。一个完整的 Transformer Block 还需要&lt;strong&gt;残差连接&lt;/strong&gt;、&lt;strong&gt;LayerNorm&lt;/strong&gt; 和 &lt;strong&gt;MLP&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;做一轮 Attention 后每个词吸收了其他词的信息，但可能“用力过猛”把原始语义冲淡了。&lt;strong&gt;残差连接&lt;/strong&gt;就是把 Attention 的输出和原始输入直接相加——像改稿时原稿留着在旁边批注，不是覆盖原文。但相加后数值可能越来越乱，&lt;strong&gt;LayerNorm&lt;/strong&gt; 就是给数值“稳一下”——对每个位置的向量做归一化让均值和方差稳定。然后是 &lt;strong&gt;MLP&lt;/strong&gt;：Attention 做的是词间信息交换（横向），MLP 做的是位置内特征加工（纵向）——对每个位置独立做一次非线性变换。&lt;strong&gt;非线性是关键&lt;/strong&gt;：没有它，不管叠多少层线性变换数学上等价于一层。&lt;/p&gt;
&lt;p&gt;$$\text{输出} = \text{LayerNorm}\Big(x + \text{MultiHeadAttention}(x)\Big)$$&lt;/p&gt;
&lt;p&gt;$$\text{最终输出} = \text{LayerNorm}\Big(z + \text{MLP}(z)\Big)$$&lt;/p&gt;
&lt;p&gt;先 Attention 残差归一化，再 MLP 残差归一化。这样的 Block 一层叠一层——6 层（原始论文）、12 层（BERT-base）、96 层（GPT-3）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/e772de959e5d1b052ca6443c1fc3df69a86b49a01d23eadad2ff9cb87d0f953e.jpg&quot; alt=&quot;插图7：Transformer Block结构&quot;&gt;&lt;/p&gt;
&lt;p&gt;叠那么多层信息不会丢吗？残差连接的第二个好处就在这里：每一层都有一条“直通车道”绕过本层变换，信息可以从第 1 层直接跳到第 20 层。这让堆叠几十层甚至上百层成为可能。为什么用 LayerNorm 不用 BatchNorm？BatchNorm 按“一批样本的同一维度”归一化，需要大批次才有意义。NLP 里句子长度不一、batch 小，统计量很不稳定。LayerNorm 只看当前这个位置自己的向量，和 batch 大小无关。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第六讲：训练与 GPT——错题变调整，再砍到极简&lt;/h2&gt;
&lt;p&gt;模型搭好了，但权重是随机的，猜词纯靠瞎蒙。训练的逻辑和做错题本一样：遮住最后一个词让它猜，算猜的和答案之间的差距——这就是&lt;strong&gt;损失&lt;/strong&gt;（loss）。然后&lt;strong&gt;反向传播&lt;/strong&gt;从最后一层往回算“每层权重该往哪个方向调、调多少”，这个方向和幅度就是&lt;strong&gt;梯度&lt;/strong&gt;。最后用梯度更新权重，每个参数往“让损失更小”的方向挪一小步，步长由&lt;strong&gt;学习率&lt;/strong&gt;控制。这个循环反复跑几百万次，模型就从瞎蒙变成行家。&lt;/p&gt;
&lt;p&gt;如果每次只遮最后一个词，一句话 10 个词只能练 1 次。实际上 Transformer 同时练 10 次：给“今”猜“天”、给“今天”猜“气”、给“今天气”猜“真”、给“今天气真”猜“好”——每个位置都在算损失。这叫&lt;strong&gt;自回归训练&lt;/strong&gt;，效率极高。代价是每个位置只能看到前面的词，但这正好就是生成式模型该做的事。&lt;/p&gt;
&lt;p&gt;训练时看整句、推理时逐词生成，矛盾吗？不矛盾。训练时通过&lt;strong&gt;因果 Mask&lt;/strong&gt;，每个位置只能看到前面的词。整句一起算只是并行加速，每个位置实际能看到的信息和推理时一模一样。&lt;/p&gt;
&lt;p&gt;因果 Mask 正是 GPT 的核心。原始 Transformer 有两种 Attention: Self-Attention（一句话内部互看）和 Cross-Attention（Decoder 看 Encoder，翻译用）. GPT 做了一件极简的事：&lt;strong&gt;把 Cross-Attention 整个砍掉。&lt;/strong&gt; 不做翻译了，GPT 只做一件事：给前文，续写下一个词。只剩纯 Self-Attention + MLP 的 Block 叠在一起。&lt;/p&gt;
&lt;p&gt;但第三讲留下的矛盾还在：Self-Attention 全互联，位置 3 能看到位置 5，做生成时位置 5 是未来词。GPT 的办法简单粗暴：&lt;strong&gt;给 Attention 矩阵盖一块遮罩。&lt;/strong&gt; 把“未来位置”的分数设成负无穷，softmax 后变成 0 权重——第 3 个词只能看到第 1、2、3 个词。这个三角形遮罩叫&lt;strong&gt;因果 Mask&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.gooo.ai/gen-images/a1c8341de812614a6cdf53239a7a29db8b6fa48569208171de886de9a4207379.png&quot; alt=&quot;插图8：GPT因果Mask不偷看未来&quot;&gt;&lt;/p&gt;
&lt;p&gt;有了它，训练时可以放心整句并行。推理时逐词来：算前文猜下一个词，拼到末尾再算，循环往复。生成时还有几个“调词旋钮”：&lt;strong&gt;Temperature&lt;/strong&gt; 控制随机性——高温度更敢冒险选非标准答案，低温度更保守；&lt;strong&gt;Top-k&lt;/strong&gt; 只从前 k 个概率最高的候选里选；&lt;strong&gt;Top-p&lt;/strong&gt; 更聪明——选到“累计概率达到 p”为止，分布集中时少选分散时多选。工程上还有 &lt;strong&gt;KV Cache&lt;/strong&gt;：前面词的 Key 和 Value 算过一遍就缓存复用，不这样每生成一个词就要把整段前文重算一遍。&lt;/p&gt;
&lt;p&gt;因果 Mask 让第一个词只能看到自己——确实信息量最少，这是生成式模型的结构性特点，后来的模型用各种方式缓解。那 GPT 砍掉 Cross-Attention 不就退化了吗？恰恰相反。原始 Transformer 为翻译设计，但大多数语言任务没有“原文”——写文章、聊天、写代码，给前文续写就行。GPT 的极简设计匹配了这些任务，极简意味着更大的参数规模、更通用的训练方式、更广的适应性。这不是退化，是聚焦。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;第七讲：收尾——一张表串起全文&lt;/h2&gt;
&lt;p&gt;回头看这七讲，每一步都长着同一个骨架：&lt;strong&gt;遇到了一个具体问题 → 想出一个办法 → 办法被起了个名字。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;| 问题 | 办法 | 名字 |
| --- | --- | --- |
| 计算机不认得字，怎么算？ | 把词变成数字向量 | 词嵌入（Embedding） |
| 只看上一个词不够，怎么记住更长的前文？ | 把前文压缩成摘要逐词传递 | 隐藏状态（RNN） |
| 句子一长，早期信息被挤掉怎么办？ | 别只带总摘要，吐每个词时按需回看原句 | Attention |
| 回看这件事需要逐词传递吗？ | 不需要，匹配和加权可以全并行 | Self-Attention |
| 拿什么去找、凭什么被找到、交出什么？ | 同一份数据变换出三种角色 | QKV |
| 词相同顺序不同意思不同，Attention 不感知顺序怎么办？ | 给每个位置叠一个位置信号 | 位置编码 |
| 一轮 Attention 只能学一种关系怎么办？ | 多组并行参数同时看 | Multi-Head |
| 叠多层时旧信息容易丢、数值越算越乱怎么办？ | 残差连接保旧信息 + LayerNorm 稳数值 | 残差 &amp;#x26; 归一化 |
| 词间信息交换够了，位置内特征怎么加工？ | 对每个位置独立做非线性变换 | MLP |
| 模型权重是随机的，怎么变聪明？ | 猜词 → 损失 → 梯度 → 反向传播 | 训练 |
| 生成时不能偷看未来怎么办？ | 把未来位置的注意力权重设为 0 | 因果 Mask |
| 偷看问题解决了，做翻译还用得着 Cross-Attention 吗？ | 砍掉，只留 Self-Attention 堆栈 | GPT |&lt;/p&gt;
&lt;p&gt;每一个“名字”背后都站着一个“问题”。先有问题，再有办法，最后才有术语。理解 Transformer 时抓住三件事就够了：&lt;strong&gt;为什么需要按需回看前文、为什么需要顺序线索、为什么需要稳定堆叠。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而 GPT 最终做的事，和第一讲一模一样：&lt;strong&gt;根据已出现的词，猜下一个词。&lt;/strong&gt; 区别只在于规模——几十层 Block、几千亿参数、几万亿个词的训练数据，让猜词的过程中“涌现”出写文章、做数学、写代码、聊天的能力。但它的心脏，从头到尾，就是那个最简单的游戏。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;全文完。Transformer 不是零件清单，而是一连串补上的缺口——每个机制都对应一个具体问题，而最终模型做的事和第一讲一样简单。&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>