ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Deepseek新架构Engram全面解析:从MoE到N-gram的论文代码详解与TaoToken实践

Deepseek新架构Engram全面解析:从MoE到N-gram的论文代码详解与TaoToken实践

1. 从 MoE 的“偏科”说起:为什么需要 Engram 这条稀疏轴

如果你最近在追 DeepSeek 的架构演进,大概率会注意到一个词反复出现:Engram。它被描述成 MoE 之外的第二条稀疏轴,专门负责“静态知识检索”这件事。我第一次读到论文里那句“语言建模包含两个完全不同的子任务”时,其实有点被点醒——MoE 擅长的是动态计算,比如数学推导、逻辑链分析,但像“亚历山大帝”“张仲景”“四大发明”这种高度固定的命名实体和短语,用多层注意力去一层层拼特征,本质上是在“用计算模拟查表”,把宝贵的深度浪费在了本该 O(1) 完成的检索上。

Engram 想做的事很直接:给 Transformer 装一个原生的“查找组件”。它不改输入嵌入、不改输出层,只在中间某几层插入一个条件记忆模块,通过哈希 N-gram 做 O(1) 检索,再用上下文门控决定这份静态记忆要不要注入主干。论文给出的数字也很有说服力:Engram-27B 在 MMLU 上比同参数同计算量的 MoE 基线高 3.4,BBH 高 5.0,HumanEval 高 3.0,而推理开销控制在 3% 以内。更关键的是那条 U 形缩放定律——固定参数和计算量下,把 20%–25% 的稀疏参数分给 Engram 效果最优。

这篇文章不打算只做论文复述。我会带你从零把 Engram 的核心模块跑起来:先配好本地环境,再用 TaoToken 统一 Key/API 通道完成模型侧验证,最后对照论文里的哈希检索、门控、压缩分词器逐段拆代码。适合谁看?如果你已经能跑通 PyTorch 小模型,想搞明白“N-gram + 哈希 + 门控”到底怎么落地,或者你正在做长上下文检索优化,这篇可以直接跟着敲。

我试过把 Engram 的 demo 拆成三块来理解:压缩分词器负责“瘦身”,NgramHashMapping 负责“定位”,Engram 主类负责“融合”。下面按这个顺序来。

2. TaoToken 前置:统一 Key/API 通道,把模型侧验证跑通

在动 Engram 代码之前,有个现实问题:论文里的 tokenizer 用的是 DeepSeek-V3 的 128k 词表,本地直接from_pretrained拉取有时会因为网络或缓存问题卡住。更稳的做法是先把模型调用通道统一好,这样后面无论是验证 tokenizer 行为,还是对比不同模型对同一段文本的 token 化结果,都不用反复改配置。

TaoToken 在这里的角色是一个统一的 API 入口,把不同模型的 Key 和 Base URL 收敛成一套。你只需要在控制台生成一个 Key,后面所有请求都走同一个地址。具体操作路径:

先到官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册并登录,然后进控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 创建 API Key。Key 生成后,在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 可以随时查看和轮换。

拿到 Key 之后,Base URL 统一填https://taotoken.net/api(注意这个地址不加 UTM 参数,直接用于代码里的base_url)。模型 ID 按你实际要验证的填,比如deepseek-v3或deepseek-chat。如果你只是想先确认通道通不通,可以直接用模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 发一条消息,看返回是否正常。

这里有个容易踩的坑:很多人把 Base URL 写成带/v1的完整路径,结果 SDK 又自动拼了一次,导致 404。正确做法是 Base URL 只写到/api,让 SDK 自己补/v1/chat/completions。另外,Key 不要硬编码在脚本里,用环境变量TAOTOKEN_API_KEY读取,后面跑 Engram demo 时也方便复用。

如果你后面要长期跑编码类任务或 Agent 流程,可以顺手了解下 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite,它更适合高频调用场景。但本篇的重点还是 Engram 本地复现,模型通道只是辅助验证 tokenizer 和推理结果。

配置完成后,建议先跑一个最小请求确认通道:

import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url="https://taotoken.net/api" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[{"role": "user", "content": "用一句话解释什么是 N-gram"}] ) print(resp.choices[0].message.content)

如果这段能正常打印,说明 Key 和 Base URL 都没问题。接下来就可以把注意力放回 Engram 本身了。

3. 可复制配置:EngramConfig 与 NgramHashMapping 的完整参数

Engram 的代码结构其实很清晰,核心配置集中在两个 dataclass 里。先看EngramConfig,它决定了哈希表大小、N-gram 阶数、嵌入维度、插入层位置这些关键参数:

from dataclasses import dataclass, field from typing import List @dataclass class EngramConfig: tokenizer_name_or_path: str = "deepseek-ai/DeepSeek-V3" engram_vocab_size: List[int] = field(default_factory=lambda: [129280 * 5, 129280 * 5]) max_ngram_size: int = 3 n_embed_per_ngram: int = 512 n_head_per_ngram: int = 8 layer_ids: List[int] = field(default_factory=lambda: [1, 15]) pad_id: int = 2 seed: int = 0 kernel_size: int = 4 @dataclass class BackBoneConfig: hidden_size: int = 1024 hc_mult: int = 4 vocab_size: int = 129280 num_layers: int = 30

这里有几个参数值得单独说。engram_vocab_size是一个长度为max_ngram_size - 1的列表,对应 2-gram 和 3-gram 各自的哈希表大小。默认值129280 * 5意味着每个 N-gram 阶数的哈希空间是词表的 5 倍,用空间换冲突率。n_head_per_ngram = 8表示每个 N-gram 用 8 个独立哈希头,每个头用不同的素数取模,进一步降低冲突。layer_ids = [1, 15]是 Engram 插入的位置,论文里也提到早期层插入效果最好,因为静态知识重建本来就发生在浅层。

BackBoneConfig里的hc_mult = 4对应论文里的多分支架构(mHC,M=4)。Engram 的门控是分支专属的,每个分支有自己的 key projection 和 RMSNorm,这样“科技分支”和“生活分支”可以对同一份静态记忆做出不同评估。

接下来是NgramHashMapping的初始化部分,它负责生成每层的乘法因子和素数模数:

import numpy as np class NgramHashMapping: def __init__(self, engram_vocab_size, max_ngram_size, n_embed_per_ngram, n_head_per_ngram, layer_ids, tokenizer_name_or_path, pad_id, seed): self.vocab_size_per_ngram = engram_vocab_size self.max_ngram_size = max_ngram_size self.n_embed_per_ngram = n_embed_per_ngram self.n_head_per_ngram = n_head_per_ngram self.pad_id = pad_id self.layer_ids = layer_ids self.compressed_tokenizer = CompressedTokenizer( tokenizer_name_or_path=tokenizer_name_or_path ) self.tokenizer_vocab_size = len(self.compressed_tokenizer) if self.pad_id is not None: self.pad_id = int(self.compressed_tokenizer.lookup_table[self.pad_id]) max_long = np.iinfo(np.int64).max M_max = int(max_long // self.tokenizer_vocab_size) half_bound = max(1, M_max // 2) PRIME_1 = 10007 self.layer_multipliers = {} for layer_id in self.layer_ids: base_seed = int(seed + PRIME_1 * int(layer_id)) g = np.random.default_rng(base_seed) r = g.integers(low=0, high=half_bound, size=(self.max_ngram_size,), dtype=np.int64) multipliers = r * 2 + 1 self.layer_multipliers[layer_id] = multipliers self.vocab_size_across_layers = self.calculate_vocab_size_across_layers()

multipliers = r * 2 + 1保证乘法因子是奇数,这样和 token ID 相乘后再异或,顺序敏感性更强。calculate_vocab_size_across_layers会为每个 N-gram 阶数的每个哈希头分配一个唯一的素数作为模数,避免不同头之间共享模数导致冲突叠加。

如果你要把这些配置写进项目,建议单独放一个engram_config.json,方便版本管理:

{ "engram": { "tokenizer_name_or_path": "deepseek-ai/DeepSeek-V3", "engram_vocab_size": [646400, 646400], "max_ngram_size": 3, "n_embed_per_ngram": 512, "n_head_per_ngram": 8, "layer_ids": [1, 15], "pad_id": 2, "seed": 0, "kernel_size": 4 }, "backbone": { "hidden_size": 1024, "hc_mult": 4, "vocab_size": 129280, "num_layers": 30 } }

这样后面无论是调max_ngram_size还是换layer_ids,都不用改代码,只改 JSON 就行。

4. 验证请求与成功结果:跑通 Engram 前向传播并对比输出

配置就绪后,下一步是跑通完整的前向传播。Engram 的 demo 入口在__main__里,构造了一个简化版 LLM:词嵌入层 + 30 个 TransformerBlock + 输出线性层。其中只有layer_id在engram_cfg.layer_ids里的层才会挂载 Engram 模块。

先看主流程:

import torch import torch.nn as nn from transformers import AutoTokenizer if __name__ == '__main__': LLM = [ nn.Embedding(backbone_config.vocab_size, backbone_config.hidden_size), *[TransformerBlock(layer_id=layer_id) for layer_id in range(backbone_config.num_layers)], nn.Linear(backbone_config.hidden_size, backbone_config.vocab_size) ] text = "Only Alexander the Great could tame the horse Bucephalus." tokenizer = AutoTokenizer.from_pretrained( engram_cfg.tokenizer_name_or_path, trust_remote_code=True ) input_ids = tokenizer(text, return_tensors='pt').input_ids B, L = input_ids.shape for idx, layer in enumerate(LLM): if idx == 0: hidden_states = LLM[0](input_ids) hidden_states = hidden_states.unsqueeze(2).expand(-1, -1, backbone_config.hc_mult, -1) elif idx == len(LLM) - 1: hidden_states = hidden_states[:, :, 0, :] output = layer(hidden_states) else: hidden_states = layer(input_ids=input_ids, hidden_states=hidden_states) print("Forward Complete!") print(f"{input_ids.shape=}\n{output.shape=}")

这段代码里有个细节:hidden_states在进入 TransformerBlock 之前被扩展成[B, L, hc_mult, D],模拟多分支架构。Engram 的门控就是在这个维度上逐分支计算的。跑通后你会看到类似输出:

Forward Complete! input_ids.shape=torch.Size([1, 14]) output.shape=torch.Size([1, 14, 129280])

接下来验证 Engram 的核心——哈希检索。你可以单独调用NgramHashMapping.hash(),看同一段文本在不同层的哈希 ID 分布:

hash_mapping = NgramHashMapping( engram_vocab_size=engram_cfg.engram_vocab_size, max_ngram_size=engram_cfg.max_ngram_size, n_embed_per_ngram=engram_cfg.n_embed_per_ngram, n_head_per_ngram=engram_cfg.n_head_per_ngram, layer_ids=engram_cfg.layer_ids, tokenizer_name_or_path=engram_cfg.tokenizer_name_or_path, pad_id=engram_cfg.pad_id, seed=engram_cfg.seed, ) hash_ids = hash_mapping.hash(input_ids.numpy()) print("Layer 1 hash shape:", hash_ids[1].shape) print("Layer 15 hash shape:", hash_ids[15].shape)

预期输出是[1, 14, 16]——14 个 token,每个 token 有 16 个哈希头(2-gram 8 头 + 3-gram 8 头)。这个 16 就是后面MultiHeadEmbedding的num_heads。

再进一步,验证门控值。门控是 Engram 的“智能筛选器”,它用当前隐藏状态和静态记忆的 key 做点积,再经过 sigmoid 得到 0~1 的权重。你可以在Engram.forward里加一行打印:

gates = torch.stack(gates, dim=2) print("Gate stats:", gates.min().item(), gates.max().item(), gates.mean().item())

在实体密集的句子里,你会看到门控值在 “Alexander the Great” 和 “Bucephalus” 这些位置明显偏高,而在普通虚词位置偏低。这正好对应论文里图 7 的可视化结论:门控只在静态、固定、高频重复的语言模式上激活。

如果你想用 TaoToken 的模型对话接口做交叉验证,可以把同一段文本发给deepseek-chat,让它解释 “Alexander the Great” 和 “Bucephalus” 的关系,再对比 Engram 门控激活的位置。两者结合,能更直观地理解“静态检索”和“动态推理”的分工。

5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth

跑 Engram demo 和配置 TaoToken 通道时,有几个报错出现频率特别高。我按实际遇到的顺序列一下,方便你对照。

401 Unauthorized:这个最常见,基本是 Key 没读到或写错了。先确认环境变量TAOTOKEN_API_KEY是否在当前 shell 生效,echo $TAOTOKEN_API_KEY看有没有值。如果用的是.env文件,注意load_dotenv()要在OpenAI()之前调用。还有一种情况是 Key 复制时带了空格,strip 一下再试。

local proxy failed / connection error:这个通常不是 TaoToken 侧的问题,而是本地网络环境或代理配置干扰。检查你的HTTP_PROXY/HTTPS_PROXY环境变量,如果指向了一个不可用的地址,SDK 会直接连不上。临时清掉这两个变量再跑一次。另外,Base URL 一定要写https://taotoken.net/api,不要自己拼/v1,否则会走到一个不存在的路径。

reading 'choices' of undefined:这个报错说明返回体里没有choices字段,通常是请求根本没成功,但 SDK 没抛异常。打印完整resp看error字段。常见原因是模型 ID 写错了,比如把deepseek-chat写成了deepseek-v3-chat。对照模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 里可选的模型名,确认拼写。

OAuth / token expired:如果你用的是某些 CLI 工具(比如 Claude Code 或 Codex 类客户端),可能会遇到 OAuth 流程失败。这类工具通常需要你在配置里同时填 Base URL、Key 和 Model ID 三件套。以 Claude Code 为例,配置文件里要写清楚:

{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model": "deepseek-chat" }

三件套缺一不可。只填 Key 不填 Base URL,工具会默认走官方地址,导致 401 或超时。如果你用的是 Cline MCP 或 CC Switch 这类插件,同样检查这三项是否对齐。接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里有各客户端的完整配置示例,照着填基本不会错。

还有一个 Engram 特有的坑:CompressedTokenizer在构建 lookup table 时,如果 tokenizer 的trust_remote_code=True没加,会直接报ValueError。另外,pad_id在压缩后会被重新映射,如果你手动传了一个原始 ID 进去,记得先过一遍lookup_table。

6. 语义一致 CTA:把 Engram 接进你的日常编码流

Engram 的代码跑通之后,你会发现它其实不只是一个论文复现项目。哈希 N-gram 检索 + 上下文门控这套组合,可以直接迁移到很多场景:比如给现有模型加一个轻量级的外部记忆模块,或者在长上下文任务里用 Engram 的思路做局部依赖卸载,把注意力资源省下来处理全局逻辑。

如果你想把这条通道固化到日常开发里,建议把 TaoToken 的 Key 和 Base URL 统一配到环境变量或项目级.env,这样无论是跑 Engram demo、调模型对话,还是后面接 Coding Plan 做 Agent 流程,都不用反复改配置。API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 可以管理多个 Key,按项目隔离。

最后留一个我踩过的坑:Engram 的layer_ids不要设得太深。论文里消融实验显示,Layer 2 插入效果最好,Layer 6 次之,再深就接近普通 MoE 的表现了。原因是静态知识重建本来就发生在浅层,Engram 插在浅层才能把主干深度“省”出来给推理。如果你把layer_ids改成[10, 20],验证损失反而会上升。这个细节在论文 2.6.2 节有完整对照表,值得回头细看。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进