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 的官方代码看具体实现。哈希函数的形式两篇论文都没有写全,只在代码里。
认出一个固定搭配要花掉好几层
语言里有大量固定的东西:人名、地名、术语、套话。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 就有 种,3-gram 是 种。放不下。
退一步:准备一张 行的表,用一个哈希函数把 n-gram 映到 到 之间的一个行号。不同的 n-gram 会撞到同一行,这叫碰撞。Engram 的几个设计都是在处理「n-gram 的种类太多」和「碰撞」这两件事。
tokenizer 压缩:先把写法不同、意思相同的 token 合并
tokenizer 追求能无损还原文本,所以 Apple、apple、␣apple 是三个不同的 token id。对查表来说它们应该是同一个东西。
Engram 预先算好一张映射表 ,把规范化后文本相同的 token 并成一个 id。规范化的步骤只在代码里列全了:Unicode 规范化、去掉重音符号、转小写、把连续的空白并成一个空格、去掉首尾空白。n-gram 取在压缩后的 id 上:
V4.1 的词表从 129280 压到 99092,少了 23%。这个数写在 config 的 engram_compressed_vocab_size 里。
哈希函数:乘一个奇数,逐个异或,对素数取模
Engram 论文只说哈希是 "multiplicative-XOR" 形式。具体的式子在官方代码 inference/engram.py 里。对位置 和阶 :
是按位异或。 是 4 个固定的大奇数,回看距离为 的 token 乘第 个。 是第 阶第 个头的素数,。
对着图看几个细节:
- 顺序有影响。 每个回看距离用不同的乘子,所以 (A, B) 和 (B, A) 得到不同的混合值。
- 阶与阶之间是接着算的。 2-gram 的混合值再异或上 就是 3-gram 的,再异或上 就是 4-gram 的。
- 同一阶的 8 个头共用一个混合值。 「8 个哈希函数」是同一个 64 位整数对 8 个不同的素数取模。
- 乘子不会让乘积溢出。 乘子的上界约为 ,保证任何压缩 id 乘上它都不超过 64 位有符号整数的范围。压缩词表的大小就是这样进入哈希的。代码启动时会检查压缩词表是不是 99092,对不上就报错:对不上意味着整张表的行号全错了。
- 序列开头不够长时用 pad 补。 第 0 个 token 的 2-gram 是 (当前 token, pad),照常查表。
多头:每一行都有碰撞,但 8 行合起来能确定是哪个 n-gram
每个头的表约 1600 万行。压缩后的 2-gram 有 种,平均 600 种 2-gram 撞到同一行;3-gram 和 4-gram 的种类更多,撞得更密。只有一个头的话,取出来的向量是几百甚至更多个 n-gram 共用的。
Engram 的做法是每一阶用 个头,各有自己的一段表,把取出的 8 行拼起来。每一行仍然被很多 n-gram 共用,但和你在第 1 个头撞到一起的那批 n-gram,在第 2 个头里会散到别的行去。
这 8 个头能把不同的 n-gram 区分到什么程度,可以算出来。下面是我补的推导。
设两个 n-gram 的混合值是 ,都是小于 的非负整数。它们在第 个头撞到同一行,等价于 整除 。如果它们在 3 个头里都撞了,那么 同时被 3 个不同的素数整除,也就被它们的乘积整除:
每个素数约 ,三个的乘积约 ,大于 。而 ,一个比它大的数整除它,只能是 。这和 矛盾。
所以两个混合值不同的 n-gram,最多在 2 个头里撞到同一行,其余 6 个头取出的行一定不同。这就是中国剩余定理:一个小于 的数由它对任意 3 个这样的素数的余数唯一确定。8 个头取出的 8 行,合起来唯一对应一个混合值。
剩下的碰撞只有一种:两个不同的 n-gram 算出了完全相同的 64 位混合值。这种情况多不多,按阶算一下,这是我的估算。2-gram 约 种、3-gram 约 种,都远少于 个可能的混合值,完全撞上的很少。4-gram 有 种,超过了 ,按抽屉原理一定有不同的 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 层 | |
|---|---|---|
| 最小的素数 | 16000057 | 16000477 |
| 最大的素数 | 16000463 | 16000889 |
| 24 个素数之和 | 384006168 | 384016682 |
两层的素数互不重复,乘子也是各自独立抽的。同一个 n-gram 在两层落到的行没有关系。
每行 256 维,两张表合计 个参数。这就是论文说的 196B。
门控:用当前的上下文决定查到的向量用多少
查表的结果只取决于最近 4 个 token,和更远的上下文无关。它有两个问题:哈希碰撞带来的噪声;同一串 token 在不同的语境下意思不同。
残差流里的隐藏态已经过了前面的注意力,带着上下文的信息。Engram 用它来决定查到的东西用多少。
记查表得到的 6144 维向量为 。一个线性层把它变成 5 个 5120 维的向量: 和 。残差流是 mHC 的 4 条,记作 。对每条流 :
是一个 0 到 1 之间的标量。4 条流各有自己的门,加的是同一个 。
除以 d 让无关向量的分数落在 1 附近
两篇论文都没有解释这个 ,下面的量级分析是我补的。
RMSNorm 之后的向量长度是 (先不管它的可学习权重)。两个这样的向量的点积是 , 是它们的夹角。再除以 :
时 的范围是 。两个互不相关的高维向量, 的标准差约为 ,所以 的标准差约为 1。除以 的作用就是把「无关」对应到 在 0 附近、量级为 1,把「方向一致」对应到几十。
sigmoid 之前多一步带符号的平方根
Engram 论文的式子是 。官方的示例代码和 V4.1 的推理代码在 sigmoid 之前都多了一步:
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))照论文的式子实现,会和发布的权重对不上。
两篇论文都没有解释这一步。这是我的解读: 的范围到正负 71,直接过 sigmoid 的话, 超过 5 门就基本饱和了,梯度接近 0。平方根把自变量的范围压到正负 8.5,饱和来得更晚。同时 的时候平方根把它放大,门对很小的分数更敏感。
每条流一个 key 的设计沿用自原版
每条流一个 key、共用一个 value 和一张表,这是 Engram 论文 §2.4 的设计,V4.1 沿用。Engram 论文自己就说这几个投影可以合成一次矩阵乘法,V4.1 的代码里它就是一个矩阵 wkv,形状 ,一次矩阵乘法算完。
Engram 直接改 4 条残差流本身,发生在这一层的读算子之前。它不经过 mHC 的写算子和混合矩阵。
图像 token 不参与。它不进 n-gram:往回看时一旦碰到图像 token,那一格和更远的格都按 pad 处理。图像 token 自己位置上的门被强制设成 0。
V4.1 相对原版的两处改动
论文 §2.4.2 说沿用原版的四样设计:tokenizer 压缩、多头哈希、带上下文的门、多分支融合。改动有两处。
去掉了短因果卷积。 原版在门之后还有一步:对门控后的值沿序列做一个核大小为 4 的 depthwise 因果卷积(每个通道单独做),用来扩大感受野。Engram 论文自己的消融就说去掉它只有很小的损失。V4.1 的理由是:它带来的收益抵不上给推理系统增加的复杂度。
这是我的解读:有卷积的话,位置 的输出依赖前面几个位置门控后的值,decode 时要把它们缓存下来。去掉以后,Engram 在位置 的输出只依赖最近 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 的比例 ,结果是一条 U 形曲线:
- 全给 MoE()不是最好的。
- 最低点在 到 ,即把 20% 到 25% 的预算给 Engram。
- 给 Engram 太多也会变差。论文的说法是记忆代替不了计算。
套到 V4.1 上:主干 552B,Engram 196B,decode 激活 16B。不激活的预算约 B,Engram 占 27%,。这是我按 Engram 论文的定义粗算的,统计范围和原文不完全一致,只能说数量上落在那个区间附近。V4.1 的论文没有说 196B 是按这条曲线定的。
Engram 论文报告的效果
V4.1 的论文没有给 Engram 的消融。下面的数字来自 Engram 论文,模型是 27B 的 MoE,把一部分 expert 换成 5.7B 的 Engram 表,总参数和激活参数不变:
| 基准 | 纯 MoE | 换入 Engram |
|---|---|---|
| MMLU | 57.4 | 60.4 |
| CMMLU | 57.9 | 61.9 |
| BBH | 50.9 | 55.9 |
| ARC-Challenge | 70.1 | 73.8 |
| HumanEval | 37.8 | 40.8 |
| TriviaQA | 48.8 | 50.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 那一次 的矩阵乘法。所以 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"
评论