专栏DeepSeek-V4.1 模型结构·记忆与解码6 / 9
14 min学习

DeepSeek-V4.1 模型结构(5):记忆,Engram

Transformer 认出一个固定搭配要花掉前几层的计算。Engram 换一条路:按当前 token 结尾的 2、3、4-gram 做哈希,从一张 3.84 亿行的表里取出向量,经过一个门加进残差流。这一篇从「直接建 n-gram 表为什么不行」推到哈希的具体形式,用中国剩余定理说明 8 个头为什么足以区分不同的 n-gram,再讲门的公式、V4.1 去掉的卷积,以及 196B 参数怎么分配、训练和存放。

目录18 节

对应论文 §2.4.2 "Engram" 和 §3.1.3。论文这一节只有两段,因为 Engram 有自己的一篇论文:Conditional Memory via Scalable Lookup,下面叫它 Engram 论文。本篇把那篇论文里需要的部分补上,再对照 V4.1 的官方代码看具体实现。哈希函数的形式两篇论文都没有写全,只在代码里。

图像文本 token视觉特征写到图像 token 的位置上,和文本 embedding 排成同一条序列复制成 4 份,进入 4 条残差流残差流 ×4每条 5120 维重复 3 组第 2 – 19 层,6 层一组每组 1 层 Full + 5 层 Reuse重复 4 组第 24 – 39 层,4 层一组每组 1 层 Reindex + 3 层 Reuseencoder:第 0 – 19 层decoder:第 20 – 39 层prefill 时,绝大部分 prompt token 只算到这里decoder 的全局 KV 全部由投影得到只有滑窗分支,不读全局 KV40 层的注意力后面都接一个 MoE,下面各行省略不画写读写读重选索引读encoder 的三组各有一份全局 KV,由该组的 Full 层写入,组内 6 层共用encoder 共享池(每组一份)main KV:每 2 个 token 一条,512 维 FP4indexer K:每条 128 维 FP4top-512 索引:每个 query 一份,不进缓存decoder 只有一份全局 KV,由第 20 层从 encoder 末态投影出来,20 层共用decoder 共享池(只有一份)main KV:每个 token 一条,512 维 FP4indexer K:每条 128 维 FP4候选池:第 20 层选出的2048 块 × 8 = 16384 个位置,Reindex 层只在池内打分top-512 索引:第 20 层先写,每个 Reindex 层覆盖一次全局 KV 合计 890 B / tokenSingle-Pass mHC:每个子层一次读、一次写 + 混读用的是上一个子层算好的查表结果经门控后加进残差流logits最后一次只读:4 条流压成 1 条读主干第 37 – 39 层入口处 4 条流的平均一次前向出 5 个草稿 token 和各自的置信度encoder 末态:第 19 层的输出,也就是第 20 层的输入。decoder 所有层的全局 KV 都只从它投影(论文式 1)encoder 末态mHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 读算子:对 4 条流加权求和,得到子层的 5120 维输入。权重由上一个子层算好读 AmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 B从头训练的视觉编码器:2D-RoPE、RMSNorm、SwiGLU,hidden 1024DeepSeek-ViT32 层 · patch 143×3 pixel-unshuffle 把 9 个相邻 patch 拼到通道维,再过两层 MLP 投影到主干宽度3×3 重排 + MLPtoken ÷ 9 → 5120 维图像位置上的 embedding 被视觉特征覆盖Token Embedding129280 × 5120前两层只有滑窗分支,没有全局 KV滑窗注意力第 0、1 层 · 窗口 128sqrt(softplus) 打分;文本和图像 token 各用一套负载均衡偏置;routed expert 权重 FP4DeepSeekMoE每层的 FFN · 384 选 6 + 1 sharedFull 模式:自己压缩 main KV(每 2 个 token 一条),投影出 indexer K,打分选 top-512,全部写进共享池CSA2 · Full第 2 / 8 / 14 层 · m = 2Reuse 模式:只有自己的 query、滑窗 KV 和输出投影;全局 KV 与 top-512 索引都从共享池读CSA2 · Reuse ×5每组其余 5 层decoder 唯一的 Full 层:输入是 encoder 末态,每个 token 一条 main KV;另选出最多 16384 个位置作为候选池CSA2 · Full第 20 层 · m = 1 · 建候选池Reuse 模式CSA2 · Reuse ×3第 21 – 23 层Reindex 模式:全局 KV 和 indexer K 从共享池读,用自己的 indexer query 在候选池里重新打分,选出新的 top-512CSA2 · Reindex第 24 / 28 / 32 / 36 层Reuse 模式:用本组 Reindex 层选出的 top-512CSA2 · Reuse ×3每组其余 3 层最终归一化RMSNorm不与 embedding 共享权重LM Head→ 129280投机解码的草稿模块:三个 block 一次前向给出 5 个草稿 token 的 logits,Markov head 补上草稿 token 之间的依赖,confidence head 估计每个位置被接受的概率DSpark 草稿层 ×3滑窗注意力 + MoE(128 选 3)按 2、3、4-gram 的哈希查表,取出的向量经门控后加进 4 条残差流;两张表共 196B 参数Engram第 1、14 层入口各一个
序列 · CSA2序列 · 共享池序列 · CED序列 · 滑窗深度 · Single-Pass mHC宽度 · DeepSeekMoE记忆 · Engram解码 · DSpark输入与输出
你现在在这里:第 1 层和第 14 层入口的 Engram。

认出一个固定搭配要花掉好几层

语言里有大量固定的东西:人名、地名、术语、套话。Engram 论文引过一个观察:给模型一句含 "Diana, Princess of Wales" 的话,逐层看最后一个 token 的隐藏态里认出了什么。第 1、2 层只认出 "Wales",第 4、5 层认出 "Princess of Wales",到第 6 层才认出这是戴安娜王妃。

这件事本质上是查表:看到这几个 token 连在一起,就该取出一条固定的信息。但 Transformer 没有查表这个操作。它只能用注意力把相邻的 token 聚到一起,再用 FFN 变换,一层一层地把这条信息算出来。这些层本可以用来做别的事。

Engram 的想法是给模型加一个真正的查表操作。论文把它和 MoE 并列:

  • MoE 是条件计算:每个 token 只激活一小部分 expert 去算。
  • Engram 是条件记忆:每个 token 只从一张大表里取出几行。

两者都让总参数远大于每个 token 实际用到的参数。

直接建 n-gram 表放不下,所以用哈希

最直接的做法是给每个 n-gram 一行。V4.1 的词表是 129280,2-gram 就有 1292802≈1.7×1010129280^2 \approx 1.7 \times 10^{10} 种,3-gram 是 2×10152 \times 10^{15} 种。放不下。

退一步:准备一张 MM 行的表,用一个哈希函数把 n-gram 映到 00 到 M−1M-1 之间的一个行号。不同的 n-gram 会撞到同一行,这叫碰撞。Engram 的几个设计都是在处理「n-gram 的种类太多」和「碰撞」这两件事。

tokenizer 压缩:先把写法不同、意思相同的 token 合并

tokenizer 追求能无损还原文本,所以 Apple、apple、␣apple 是三个不同的 token id。对查表来说它们应该是同一个东西。

Engram 预先算好一张映射表 P\mathcal{P},把规范化后文本相同的 token 并成一个 id。规范化的步骤只在代码里列全了:Unicode 规范化、去掉重音符号、转小写、把连续的空白并成一个空格、去掉首尾空白。n-gram 取在压缩后的 id 上:

xt′=P(xt),gt,n=(xt−n+1′,…,xt′).x'_t = \mathcal{P}(x_t), \qquad g_{t,n} = (x'_{t-n+1}, \dots, x'_t).

V4.1 的词表从 129280 压到 99092,少了 23%。这个数写在 config 的 engram_compressed_vocab_size 里。

哈希函数:乘一个奇数,逐个异或,对素数取模

Engram 论文只说哈希是 "multiplicative-XOR" 形式。具体的式子在官方代码 inference/engram.py 里。对位置 tt 和阶 nn:

mixt,n=⨁i=0n−1(xt−i′⋅mi),zt,n,k=mixt,n mod pn,k.(H)\text{mix}_{t,n} = \bigoplus_{i=0}^{n-1} \big( x'_{t-i} \cdot m_i \big), \qquad z_{t,n,k} = \text{mix}_{t,n} \bmod p_{n,k}. \tag{H}

⊕\oplus 是按位异或。m0,…,m3m_0, \dots, m_3 是 4 个固定的大奇数,回看距离为 ii 的 token 乘第 ii 个。pn,kp_{n,k} 是第 nn 阶第 kk 个头的素数,k=1,…,8k = 1, \dots, 8。

压缩后的 id乘各自的乘子逐个异或对素数取模同一张表DianaPrincessofWales(当前 token)2-gram 的混合值⊕ 之后的 64 位整数mod 8 个素数得到 8 个行号2-gram 的 8 段3-gram 的混合值⊕ 之后的 64 位整数mod 8 个素数得到 8 个行号3-gram 的 8 段4-gram 的混合值⊕ 之后的 64 位整数mod 8 个素数得到 8 个行号4-gram 的 8 段⊕ 是按位异或取出 24 行,每行 256 维拼成 6144 维每段的行数是一个素数,约 1600 万。24 段合起来是一张 3.84 亿行的表。同一阶的 8 个行号来自同一个混合值,只是模的素数不同。
位置 t 在一个 Engram 层里的查表。红框是当前 token。

对着图看几个细节:

  • 顺序有影响。 每个回看距离用不同的乘子,所以 (A, B) 和 (B, A) 得到不同的混合值。
  • 阶与阶之间是接着算的。 2-gram 的混合值再异或上 xt−2′⋅m2x'_{t-2} \cdot m_2 就是 3-gram 的,再异或上 xt−3′⋅m3x'_{t-3} \cdot m_3 就是 4-gram 的。
  • 同一阶的 8 个头共用一个混合值。 「8 个哈希函数」是同一个 64 位整数对 8 个不同的素数取模。
  • 乘子不会让乘积溢出。 乘子的上界约为 (263−1)/99092(2^{63} - 1) / 99092,保证任何压缩 id 乘上它都不超过 64 位有符号整数的范围。压缩词表的大小就是这样进入哈希的。代码启动时会检查压缩词表是不是 99092,对不上就报错:对不上意味着整张表的行号全错了。
  • 序列开头不够长时用 pad 补。 第 0 个 token 的 2-gram 是 (当前 token, pad),照常查表。

多头:每一行都有碰撞,但 8 行合起来能确定是哪个 n-gram

每个头的表约 1600 万行。压缩后的 2-gram 有 990922≈101099092^2 \approx 10^{10} 种,平均 600 种 2-gram 撞到同一行;3-gram 和 4-gram 的种类更多,撞得更密。只有一个头的话,取出来的向量是几百甚至更多个 n-gram 共用的。

Engram 的做法是每一阶用 K=8K = 8 个头,各有自己的一段表,把取出的 8 行拼起来。每一行仍然被很多 n-gram 共用,但和你在第 1 个头撞到一起的那批 n-gram,在第 2 个头里会散到别的行去。

这 8 个头能把不同的 n-gram 区分到什么程度,可以算出来。下面是我补的推导。

设两个 n-gram 的混合值是 a≠ba \ne b,都是小于 2632^{63} 的非负整数。它们在第 kk 个头撞到同一行,等价于 pkp_k 整除 a−ba - b。如果它们在 3 个头里都撞了,那么 a−ba - b 同时被 3 个不同的素数整除,也就被它们的乘积整除:

p1p2p3∣(a−b).p_1 p_2 p_3 \mid (a - b).

每个素数约 1.6×1071.6 \times 10^7,三个的乘积约 4×10214 \times 10^{21},大于 263≈9.2×10182^{63} \approx 9.2 \times 10^{18}。而 ∣a−b∣<263|a - b| < 2^{63},一个比它大的数整除它,只能是 a−b=0a - b = 0。这和 a≠ba \ne b 矛盾。

所以两个混合值不同的 n-gram,最多在 2 个头里撞到同一行,其余 6 个头取出的行一定不同。这就是中国剩余定理:一个小于 2632^{63} 的数由它对任意 3 个这样的素数的余数唯一确定。8 个头取出的 8 行,合起来唯一对应一个混合值。

剩下的碰撞只有一种:两个不同的 n-gram 算出了完全相同的 64 位混合值。这种情况多不多,按阶算一下,这是我的估算。2-gram 约 2332^{33} 种、3-gram 约 2502^{50} 种,都远少于 2632^{63} 个可能的混合值,完全撞上的很少。4-gram 有 990924≈9.6×101999092^4 \approx 9.6 \times 10^{19} 种,超过了 263≈9.2×10182^{63} \approx 9.2 \times 10^{18},按抽屉原理一定有不同的 4-gram 共用同一个混合值,只是语料里真正出现过的 4-gram 远少于这个数。

每一行里混着很多 n-gram 的信息,这一点没有变。训练会让出现得多的 n-gram 主导它们所在的行。剩下的噪声交给后面的门处理。

一层只有一张表

论文的记号里每个 (阶, 头) 有一张表,一层 24 张。实现上每层只有一张物理表,24 个头各占里面连续的一段,行号加上这一段的起始位置再去取。

这张表的行数是 config 里的 engram_num_embeddings:第 1 层 384006168,第 14 层 384016682。这两个数看起来很随意,其实是 24 个素数的和。代码从 16000000(config 的 engram_vocab_size)往上找素数,一个头取一个,用过的不再用:

第 1 层第 14 层
最小的素数1600005716000477
最大的素数1600046316000889
24 个素数之和384006168384016682

两层的素数互不重复,乘子也是各自独立抽的。同一个 n-gram 在两层落到的行没有关系。

每行 256 维,两张表合计 768022850×256≈196.6B768022850 \times 256 \approx 196.6\text{B} 个参数。这就是论文说的 196B。

门控:用当前的上下文决定查到的向量用多少

查表的结果只取决于最近 4 个 token,和更远的上下文无关。它有两个问题:哈希碰撞带来的噪声;同一串 token 在不同的语境下意思不同。

残差流里的隐藏态已经过了前面的注意力,带着上下文的信息。Engram 用它来决定查到的东西用多少。

查表结果6144 维线性层 wkv6144 → 5 × 51204 条残差流4 个 key,每条流一个1 个 value,4 条流共用4 个门归一化点积带符号的平方根,再过 sigmoid加回 4 条残差流
门控与写回。查表结果被投影成 4 个 key 和 1 个 value。

记查表得到的 6144 维向量为 ete_t。一个线性层把它变成 5 个 5120 维的向量:k(1),…,k(4)k^{(1)}, \dots, k^{(4)} 和 vv。残差流是 mHC 的 4 条,记作 h(1),…,h(4)h^{(1)}, \dots, h^{(4)}。对每条流 mm:

s(m)=RMSNorm⁡(h(m))⊤RMSNorm⁡(k(m))d,α(m)=σ(sign⁡(s(m))∣s(m)∣),s^{(m)} = \frac{\operatorname{RMSNorm}\big(h^{(m)}\big)^{\top} \operatorname{RMSNorm}\big(k^{(m)}\big)}{\sqrt{d}}, \qquad \alpha^{(m)} = \sigma\Big(\operatorname{sign}\big(s^{(m)}\big) \sqrt{\big|s^{(m)}\big|}\Big), h(m)←h(m)+α(m) v.h^{(m)} \leftarrow h^{(m)} + \alpha^{(m)}\, v .

α(m)\alpha^{(m)} 是一个 0 到 1 之间的标量。4 条流各有自己的门,加的是同一个 vv。

除以 d​ 让无关向量的分数落在 1 附近

两篇论文都没有解释这个 d\sqrt{d},下面的量级分析是我补的。

RMSNorm 之后的向量长度是 d\sqrt{d}(先不管它的可学习权重)。两个这样的向量的点积是 d⋅d⋅cos⁡θ=dcos⁡θ\sqrt{d} \cdot \sqrt{d} \cdot \cos\theta = d \cos\theta,θ\theta 是它们的夹角。再除以 d\sqrt{d}:

s=d cos⁡θ.s = \sqrt{d}\, \cos\theta .

d=5120d = 5120 时 ss 的范围是 [−71.6, 71.6][-71.6,\ 71.6]。两个互不相关的高维向量,cos⁡θ\cos\theta 的标准差约为 1/d1/\sqrt{d},所以 ss 的标准差约为 1。除以 d\sqrt{d} 的作用就是把「无关」对应到 ss 在 0 附近、量级为 1,把「方向一致」对应到几十。

sigmoid 之前多一步带符号的平方根

Engram 论文的式子是 α=σ(s)\alpha = \sigma(s)。官方的示例代码和 V4.1 的推理代码在 sigmoid 之前都多了一步:

python
dot = (h * weight * key).sum(-1) * rstd * self.dim**-0.5
# signed sqrt before the sigmoid, matching the training kernel
gate = torch.sigmoid(torch.copysign(dot.abs().clamp_min(self.clamp_value).sqrt(), dot))

照论文的式子实现,会和发布的权重对不上。

00.51-10-5-101510归一化点积门,论文的式子官方代码的写法s = 5 时:蓝线 0.993,橙线 0.903s = 0.25 时:蓝线 0.562,橙线 0.622
两种写法下门随分数的变化。

两篇论文都没有解释这一步。这是我的解读:ss 的范围到正负 71,直接过 sigmoid 的话,∣s∣|s| 超过 5 门就基本饱和了,梯度接近 0。平方根把自变量的范围压到正负 8.5,饱和来得更晚。同时 ∣s∣<1|s| < 1 的时候平方根把它放大,门对很小的分数更敏感。

每条流一个 key 的设计沿用自原版

每条流一个 key、共用一个 value 和一张表,这是 Engram 论文 §2.4 的设计,V4.1 沿用。Engram 论文自己就说这几个投影可以合成一次矩阵乘法,V4.1 的代码里它就是一个矩阵 wkv,形状 6144×256006144 \times 25600,一次矩阵乘法算完。

Engram 直接改 4 条残差流本身,发生在这一层的读算子之前。它不经过 mHC 的写算子和混合矩阵。

图像 token 不参与。它不进 n-gram:往回看时一旦碰到图像 token,那一格和更远的格都按 pad 处理。图像 token 自己位置上的门被强制设成 0。

V4.1 相对原版的两处改动

论文 §2.4.2 说沿用原版的四样设计:tokenizer 压缩、多头哈希、带上下文的门、多分支融合。改动有两处。

去掉了短因果卷积。 原版在门之后还有一步:对门控后的值沿序列做一个核大小为 4 的 depthwise 因果卷积(每个通道单独做),用来扩大感受野。Engram 论文自己的消融就说去掉它只有很小的损失。V4.1 的理由是:它带来的收益抵不上给推理系统增加的复杂度。

这是我的解读:有卷积的话,位置 tt 的输出依赖前面几个位置门控后的值,decode 时要把它们缓存下来。去掉以后,Engram 在位置 tt 的输出只依赖最近 4 个 token 的 id 和当前的残差流,不需要任何跨步的状态。

表的优化器换了。 原版用 Adam 训练这张表。Adam 要为每个参数存两个状态,196B 参数的表就是两份同样大的状态。V4.1 换成一种只需要一份动量的更新方式,叫 Sinkhorn-balanced update。它同时用在 token embedding 和预测头上,第 7 篇和优化器的其他改动一起讲。

放在第 1 层和第 14 层

Engram 论文在一个 12 层的模型上扫过单个模块的位置:放在很靠前、但前面已经有一轮注意力的位置最好,越往深越差。它给了两个相反的考虑:

  • 放得早,主干就不用花前几层去重建这些局部的模式。
  • 不能放在最开头。 门需要上下文,隐藏态至少要过一轮注意力。另外论文说 mHC 的几条流在开头还没有分化,4 个门没有不同的东西可看。V4.1 的代码里能看到原因:4 条流在入口处是同一个向量的 4 份拷贝。

它还发现把同样的预算拆成两个模块、放在两个深度更好。

V4.1 放在第 1 层和第 14 层的入口,层号从 0 数。第 1 层入口正好是「过了一轮注意力和 MoE」的位置。论文给的理由是另一个:让训练时流水线各段的显存占用均衡。我的理解是:两张表各 98B 参数,不能挤在同一段。

推理时还有一个好处。查哪几行只取决于 token id,前向开始之前就知道。表可以放在主机内存里,在第 0 层计算的同时由后台把要用的行传到 GPU 上。

参数给 MoE 还是给 Engram

每个 token 不激活的参数是一笔预算。它可以用来加 routed expert,也可以用来加大 Engram 的表。Engram 论文固定总参数和激活参数,只改变这笔预算里分给 MoE 的比例 ρ\rho,结果是一条 U 形曲线:

  • 全给 MoE(ρ=100%\rho = 100\%)不是最好的。
  • 最低点在 ρ≈75%\rho \approx 75\% 到 80%80\%,即把 20% 到 25% 的预算给 Engram。
  • 给 Engram 太多也会变差。论文的说法是记忆代替不了计算。

套到 V4.1 上:主干 552B,Engram 196B,decode 激活 16B。不激活的预算约 552+196−16=732552 + 196 - 16 = 732B,Engram 占 27%,ρ≈73%\rho \approx 73\%。这是我按 Engram 论文的定义粗算的,统计范围和原文不完全一致,只能说数量上落在那个区间附近。V4.1 的论文没有说 196B 是按这条曲线定的。

Engram 论文报告的效果

V4.1 的论文没有给 Engram 的消融。下面的数字来自 Engram 论文,模型是 27B 的 MoE,把一部分 expert 换成 5.7B 的 Engram 表,总参数和激活参数不变:

基准纯 MoE换入 Engram
MMLU57.460.4
CMMLU57.961.9
BBH50.955.9
ARC-Challenge70.173.8
HumanEval37.840.8
TriviaQA48.850.7

涨得最多的是 BBH 这样的推理类基准,事实问答 TriviaQA 涨得反而少。论文的解释是,Engram 接手了前几层认固定搭配的工作,相当于让主干变深了。它用表示相似度做过验证:加了 Engram 的模型第 5 层的表示,和纯 MoE 模型第 12 层的最接近。

另一个实验是推理时把 Engram 的输出屏蔽掉。事实问答的得分只剩原来的 29% 到 44%,阅读理解还剩 81% 到 93%。这两个结果不矛盾:前一个说的是加上它之后哪类任务涨得多,后一个说的是模型把哪类信息存在了表里。

196B 参数怎么存、怎么取

存储。 表是 FP8,每个参数 1 字节。量化的 scale 是每行每 32 维一个,一行 8 个。这样每一行可以单独取出来反量化。两张表约 197 GB,scale 另占 6 GB。

每个 token 取多少。 每层 24 行,每行 256 字节加 8 字节 scale,约 6.3 KB。计算量只有 wkv 那一次 6144→256006144 \to 25600 的矩阵乘法。所以 196B 不算进激活参数。

训练。 论文 §3.1.3 说:

  • 表按行切开,分给专门的一组进程。组的大小在每台设备的显存占用和查表的通信范围之间取舍。
  • 查哪些行只取决于输入的 token,所以每个训练步开始、流水线还没处理 micro-batch 之前,就对整个 batch 发起预取。
  • 表的梯度在反向时先缓冲,主干反向结束后再送回表所在的进程。
  • 多模态训练时,预取和梯度回传安排在视觉编码器前向和反向的时间里。

推理。 论文说表放在主机内存,靠后台的 RDMA 传输预取,第一个模块的预取和第 0 层的计算重叠。官方的参考实现没有做这件事:它把表按行切成几份放在各张 GPU 上,查表后用一次 all-reduce 汇总。

容易混淆的几点

  • Engram 不是检索增强,也不是外部知识库。 它是一张随模型一起训练的 embedding 表,key 是 token n-gram 的哈希。
  • 它没有替换输入的 embedding。 token embedding 和 LM head 都不动。Engram 插在第 1 层和第 14 层的入口。
  • 两层 Engram 都在 encoder 里。 所以 prefill 和 decode 都要过它们。
  • 「8 个头」不是 8 套乘子。 8 个头共用一个混合值,区别只在模哪个素数。
  • 384006168 不是调出来的超参。 它是 24 个素数的和,真正的超参是 16000000。
  • 论文的式子少了平方根。 以代码为准。
  • V4.1 的论文里 Engram 有两条引用(2026b 和 2026c),是同一篇论文的 arXiv 版和会议版。

下一篇

Engram 往残差流里加的是查到的记忆。下一篇讲挂在主干出口的 DSpark:它不改变模型输出什么,只让输出来得更快。

资料

  • DeepSeek-V4.1-Flash 技术报告 §2.4.2、§3.1.3:arXiv 2609.19969
  • Engram 论文:Cheng et al.,"Conditional Memory via Scalable Lookup: A New Axis of Sparsity for Large Language Models",arXiv 2601.07372。§2 是结构,§3 是参数分配,§6 是消融与分析
  • 代码:Hugging Face deepseek-ai/DeepSeek-V4.1-Flash 的 inference/engram.py(哈希)和 inference/model.py 的 Engram(门);原版示例 deepseek-ai/Engram
  • 多头哈希 embedding 的出处:Tito Svenstrup et al.,2017,"Hash Embeddings for Efficient Word Representations"

评论