工作笔记 · 模型训练 · Infra

OE Embedding 机制图解

主题 Over-Encoding 原理 · v2 patch · CPU 卸载 模型 WeLM v4.5 · 80B-A3B 日期 2026-06-13
● 面向非 infra 同学 · monkey_patch_welmv4_5_moe_v2.py · 附字节 OTT / DeepSeek Engram 对照
📌 一句话先讲清楚
WeLM v4.5 模型里有 4 张超大查表(共 65.6 GB),本来要塞进显卡,会爆显存;这版 patch 把它们搬到内存(CPU),每次训练时去内存里查一下结果再送回显卡,顺便修了一个跨样本污染的小 bug
📖 目录
  1. 什么是 OE Embedding?它是干嘛的?
  2. v2 patch 是怎么修的?
  3. 每一步训练时数据是怎么流的?
  4. 代价和收益怎么样?
  5. OE 给工程带来的真实挑战 🆕
  6. 延伸:用到多模态训练的挑战 🆕
  7. 学术源流:字节 OTT / DeepSeek Engram 🆕

1什么是 OE Embedding?

OE 是 Over-Encoding(过度编码) 的缩写,是 WeLM v4.5 这个大模型自带的一个"加料"机制。

你可以把模型理解成一个"读句子的人"
普通模型每次只看当前这个字是什么(这叫 token embedding)。
OE 多做了一件事:顺便偷瞄一下前面 1~4 个字组合起来是什么,然后把这个"上下文味道"也加到当前字上一起读。
就像你读"苹果"和读"吃苹果"的语感是不同的——OE 给模型多提供了这种"短上下文"信号。

怎么实现这种"偷瞄"呢?模型准备了 4 张巨大的查询表(hash 表)

输入句子: 读到 "苹" 时,OE 会回看前面的 "我爱吃" 一起编码: hash → 行号 表1 (2-gram) 16.4 GB · 8.19B 参数 表2 (3-gram) 16.4 GB · 8.19B 参数 表3 (4-gram) 16.4 GB · 8.19B 参数 表4 (5-gram) 16.4 GB · 8.19B 参数 合计 65.6 GB ⚠️
图 1:OE 用 4 张大查询表"偷瞄" 2~5-gram,每张表 16.4 GB / 81.92 亿参数

📊 这 4 张表到底有多大?参数量算给你看

每张表的参数量 = 行数 × 列数:

16,000,000 行 × 512 列 = 8,192,000,000 参数 ≈ 81.92 亿(8.19B)

4 张表合计:

8.19B × 4 = 327.68 亿参数(32.77B)
对比对象参数量对比 OE 4 张表(32.77B)
Llama-3-8B 整个模型8 BOE 是它的 4 倍
WeLM v4.5 (80B-A3B) 单步激活参数3 BOE 是它的 10 倍
GPT-3175 BOE 约是它的 1/5
显存(bf16,每参数 2 字节)32.77B × 2B = 65.54 GB
这 4 张表本身就是一个超大模型的体量。单 H100 总共才 80 GB 显存,光它们就占 65.6 GB——这就是为什么必须卸载到 CPU。
好在这些表本质是预训练阶段就训好的 hash 查询表,SFT 阶段冻结它们对效果影响很小;真正起作用的小投影层 oe_up_proj(参数量约几百万)仍然留在 GPU 上正常训练。

🔬 n-gram 是怎么算出来的?4 张表分别干嘛?

第一步:每张表对应一个 "n-gram 阶数"

WeLM v4.5 的配置里 oe_grams = [2, 3, 4, 5]oe_vocab_sizes = [16M, 16M, 16M, 16M],意思是:

表号阶数含义类比(读到"果"字时)
表 12-gram同时看 当前 + 前 1 个看 "苹果" 这个组合
表 23-gram同时看 当前 + 前 2 个看 "吃苹果" 这个组合
表 34-gram同时看 当前 + 前 3 个看 "爱吃苹果" 这个组合
表 45-gram同时看 当前 + 前 4 个看 "我爱吃苹果" 这个组合
4 张表是从短到长的"上下文滤镜",每张抓不同长度的搭配信息。短的(2-gram)抓常见词组("苹果"),长的(5-gram)抓固定短语("我爱吃苹果")。最后把 4 段信息拼在一起,让模型同时拥有"近距离 + 远距离"的搭配感。

第二步:怎么把"前 n 个 token"压成一个数字?

这是 OE 最巧妙的地方。代码里的做法分两小步:

① 把 n 个 token id 拼成一个大整数(V = 词表大小):

combined = t0 + t1·V + t2·V² + t3·V³ + t4·V⁴
就像把多位数字拼起来:个位放 t0,"V 进制"的十位放 t1,百位放 t2……这样不同的 token 组合一定得到不同的 combined 值,互不冲突。

② 用 Knuth 黄金比例哈希把这个超大整数打散:

hashed = (combined × 2654435761) & 0xFFFFFFFF

(这个常数是 2³² × (√5−1)/2,黄金分割的整数版,让 hash 结果在 0~2³² 之间均匀分布)

③ 取模落到表的某一行(每张表 16M = 2²⁴ 行):

row_id = hashed % 16,000,000
"取模"就像把无穷多种 token 组合抽签分到 1600 万个抽屉里。同一个组合永远落到同一个抽屉(确定性),但不同组合可能撞到同一个抽屉(叫 hash 冲突)—— 这是用"压缩存储"换来的小代价,模型能学着容忍它。

第三步:4 张表查出 4 个向量,拼起来投影

每张表查出来的是一行 512 维向量。4 张表 → 4 个 512 维向量 → 拼接成 2048 维 → 过一个小线性层 oe_up_proj(2048 → hidden_size)→ 和普通 token embedding 取平均。

输入序列(已 tokenize): ← 当前要算 OE 的位置 表 1(2-gram) 取 [苹, 果] 表 2(3-gram) 取 [吃, 苹, 果] 表 3(4-gram) 取 [爱, 吃, 苹, 果] 表 4(5-gram) 取 [我, 爱, 吃, 苹, 果] combined = t₀ + t₁·V × 2654435761 % 16M combined = ...+ t₂·V² × 2654435761 % 16M combined = ...+ t₃·V³ × 2654435761 % 16M combined = ...+ t₄·V⁴ × 2654435761 % 16M row_id = 1234567 row_id = 8765432 row_id = 4567890 row_id = 9876543 512 维向量 512 维向量 512 维向量 512 维向量 torch.cat → [512 + 512 + 512 + 512] = 2048 维 oe_up_proj 线性层(2048 → hidden_size) 这里参数小,留在 GPU,可训练
图 1.5:当读到"果"字时,4 张表分别捕捉 2/3/4/5-gram 上下文,最后拼接 + 投影成 hidden_size 维向量

第四步:和普通 embedding 取平均

代码最后一行:

hidden_states = (普通 embedding + OE 投影结果) / 2.0
最终送进 Transformer 的"果"字向量 = "果"自己的语义(普通 embedding) + "我爱吃苹果"这串组合的味道(OE)—— 两碗汤一勺一勺各舀一半,混合后口味更丰富。

为什么要 4 张独立的表,而不是 1 张大表?

原因解释
不同 n-gram 的"组合空间"差太多2-gram 组合空间是 V²,5-gram 是 V⁵,量级差几十亿倍。如果共用一张表,hash 冲突会被长 n-gram 完全淹没短 n-gram。
语义维度不同2-gram 学的是固定搭配("苹果"),5-gram 学的是固定句式("我爱吃苹果"),用不同表分别学,互不干扰。
分桶可控每张表独立 16M 行,每张表的 hash 冲突率独立可控。

问题 A:65.6 GB 装不进显卡

单张 H100 显卡总共才 80 GB 显存,光这 4 张表就吃掉 65.6 GB,再加上模型本身 80B 参数、激活值、优化器状态,根本装不下。

在多卡训练的 ZeRO-3 模式下,正常做法是把这些参数按卡切片,每张卡只存自己的一片。但是每次前向计算时还要临时把所有切片合起来(all-gather),那一瞬间整张表又会出现在每张卡上 → 瞬时显存爆炸 → 训练直接 OOM 崩溃。

v1 版的解法是直接把 OE 关掉(粗暴但有效),相当于让模型不再看这 4 张表。
v2 想要保留 OE 的能力,必须想别的办法。

问题 B:训练样本之间会"串味"

大模型训练为了省算力,会把多条短样本拼成一条长序列(叫 packing),比如:

样本 A:今天天气真好 样本 B:苹果是一种水果 样本 C:北京是中国的首都 ↑ 三个完全不相关的样本被拼成一条长序列一起训练
原版 OE 的"偷瞄前 n 个字"是无脑往前数 n 位的,不知道哪里是样本边界。
读到样本 B 的"苹"字时,它会偷瞄到样本 A 末尾的"真好"——跨样本串味了!
这相当于让模型读"今天天气真好果是一种水果",是真的语义错误,会拉低训练质量。

2v2 patch 是怎么修的?

v2 同时解决两个问题,思路非常清晰:

1

把 4 张大表搬到"内存条"(CPU 内存)里

显卡显存(GPU)容量只有 80 GB,但服务器的内存条(CPU 内存)通常有几百 GB甚至 1TB,65.6 GB 放进去毫无压力。

就像你电脑的固态硬盘装不下大型游戏,但机械硬盘空间大——把不常用的素材包放到机械硬盘,需要时再读到内存里使用。这里把 GPU 当"小而快的固态",把 CPU 内存当"大而稍慢的机械硬盘"。

对应代码:_detach_oe_embed_to_cpu() 函数

2

让 GPU 训练框架"看不见"这 4 张表

光搬走还不够,还要从模型结构里把它们的引用彻底删掉del inner_model.oe_embed),否则 ZeRO-3 框架还会自作主张去切片、去 all-gather,依然会炸。

相当于不光把素材包搬到机械硬盘,还要把游戏配置里的引用路径也改掉,免得游戏启动时还去固态盘上找它,找不到就崩溃。
3

每次训练时,去 CPU 上查表,结果搬回 GPU

替换掉模型原本的前向函数,改成:

  1. 把要查的"行号"从 GPU 拷到 CPU(很小,几十 KB)
  2. CPU 上查表得到结果(约 16 MB / 张 × 4 张 = 64 MB)
  3. 把结果通过 PCIe 总线搬回 GPU,约 3 毫秒,可以接受

对应代码:_oe_cpu_lookup() 函数

4

修复"跨样本串味" bug

把"无脑往前数 n 位"改成"懂边界的版本":每个样本的开头 n 个位置,强制写 0(相当于"前面没有字可以偷瞄"),就把跨样本污染斩断了。

对应代码:_packing_aware_skip_firstn_token_ids() 函数

修复前(错误): 样A末 ↓串味 样B首 样B B 开头偷瞄到 A 的尾巴 ❌ 修复后(正确): 样A末 置 0 样B首 样B B 开头啥也偷瞄不到 ✅

3训练时数据怎么流?一图看懂

🎮 GPU(显卡,快但小) 🧠 CPU 内存(大但稍慢) 输入 token ids "我爱吃苹果" embed_tokens 查普通 embedding 表 算 hash 行号 packing-aware 段开头 n 位置 0 oe_up_proj 投影 小参数,可训练 融合:hidden = (普通 embed + OE) / 2 → 送入 Transformer 各层继续计算 表 1 · 16.4 GB · 冻结 表 2 · 16.4 GB · 冻结 表 3 · 16.4 GB · 冻结 表 4 · 16.4 GB · 冻结 pinned memory,可加速搬运 ①行号 D2H ②查表结果 H2D(约 64MB / 3ms) 普通 embed
图 2:训练每一步的数据流(GPU ↔ CPU 协作)

每一步训练的 5 个动作

  1. GPU 拿到输入的 token ids,先查普通 embedding 表得到基础向量。
  2. GPU 上算出"前 n 个 token 的 hash 行号",这一步用 packing-aware 版本,把样本边界写 0。
  3. 把行号(很小)从 GPU 拷到 CPU。
  4. CPU 上对 4 张表分别查表 → 得到 4 段 OE 向量 → 通过 PCIe 拷回 GPU(约 64 MB / 3 ms)。
  5. GPU 上:4 段 OE 向量拼起来,过 oe_up_proj 投影,再和普通 embedding 取平均,作为最终输入送进 Transformer。

4代价和收益

项目 v1(关掉 OE) v2(CPU 卸载)
4 张大表是否参与训练 ❌ 完全不用 ✅ 参与前向 但冻结不更新
oe_up_proj 投影层 不更新(前向不走它) 可训练,留在 GPU
显存占用 低(4 张表 在 CPU 内存
每步耗时 基线 +约 3ms(PCIe 拷贝)
跨样本串味 bug 不触发(OE 关了) ✅ 已修复
OE 能力是否保留 ❌ 没了 ✅ 保留
结论:v2 用"4 张表搬到 CPU、SFT 阶段不更新它们"的代价,换来了 OE 能力的保留 + 跨样本污染的修复
注释里也直说:这 4 张表是预训练阶段就训好的 hash 表,SFT 阶段本来就没必要再调它们;真正起作用的小参数 oe_up_proj 仍然留在 GPU 上正常训练。

🔑 三个核心函数对照表

函数 干什么
_detach_oe_embed_to_cpu 启动时把 4 张大表搬去 CPU,并从模型结构里删掉引用
_oe_cpu_lookup 每步训练时去 CPU 查表,把结果搬回 GPU
_packing_aware_skip_firstn_token_ids 修跨样本串味 bug:样本开头 n 位强制清零
本文档基于 monkey_patch_welmv4_5_moe_v2.py 自动整理 · 仅作内部学习交流用途

5OE 给工程带来的真实挑战

前面讲的是「OE 是什么、v2 patch 怎么修」。但当你真的要把这套 65.6 GB 的查表机制跑在训练 / 推理集群上,会撞上一连串 infra 层面的硬骨头。下面把我们实际踩过、或预期会踩的坑系统地列一下。

5.1 显存墙:参数比模型本体还大

OE 的 4 张表合计 32.77B 参数 / 65.6 GB(bf16),而 WeLM v4.5(80B-A3B)单步只激活 3B。也就是说——一个「辅助 embedding 机制」的体量是主干单步激活的 10 倍

单张 H100 才 80 GB 显存。如果把 OE 表和模型一起塞进卡里,光这 4 张表就吃掉 65.6 GB,剩下的连一层 attention 的激活都放不下,必然 OOM。
v2 patch 的核心动作就是把这 4 张表 卸载到 CPU 内存,GPU 只保留可训练的小投影层 oe_up_proj。代价是每个 step 都要做一次「GPU→CPU 取行号→CPU 查表→搬回 GPU」的往返。

5.2 通信瓶颈:PCIe 往返与 host-device 拷贝

表在 CPU、计算在 GPU,意味着每个 forward 都要跨 PCIe 搬数据。一个 batch 里每个 token 都要从 4 张表各查一行 512 维向量:

单 step 取回数据量 ≈ batch × seq_len × 4 表 × 512 维 × 2 字节(bf16)

以 batch=8、seq_len=4096 估算,单 step 仅 OE 查表回搬就有 8×4096×4×512×2 ≈ 268 MB。PCIe 4.0 单向带宽约 32 GB/s,看似不大,但它夹在前向关键路径上,且 host→device 拷贝难以和计算完全 overlap,TTFT / 单步耗时都会被拉高。

就像图书馆把最常翻的 4 套大百科全书放在隔壁楼(CPU 内存),你(GPU)每算一句话都得跑过去抄一段再跑回来。书是省地方了,但你的腿(PCIe)成了新瓶颈。

5.3 随机访存:hash 决定了「没有局部性」

OE 用 Knuth 黄金比例 hash 故意把相邻 token 组合打散到 1600 万行的不同位置——这对「均匀分布、减少冲突」是好事,但对内存子系统是灾难

对照 DeepSeek Engram 的解法Engram 论文专门强调 deterministic addressing(确定性寻址)+ host memory runtime prefetching——因为 N-gram 是确定的,可以在需要某行之前就异步把它从 host memory 预取过来,把随机访存延迟藏在计算后面。这正是 OE 朴素实现所缺的优化方向。

5.4 跨样本污染:状态泄漏 bug 的工程根源

v2 patch 修的那个「跨样本污染」bug,本质是有状态的 n-gram 缓冲区在 batch 内 / 跨 micro-batch 之间没有正确按样本边界清零。这类 bug 在普通无状态 embedding 里根本不存在,是 OE「要回看前 n 个 token」这一跨时间步依赖带来的工程复杂度。

在序列并行 / 流水并行 / packing(多条短样本拼成一条长序列)等场景下,「前 n 个 token」可能跨越了真正的样本边界,把上一条样本的尾巴当成了当前样本的上文,污染 hash。debug 时它表现为「loss 抖动、复现困难」,极其隐蔽。

5.5 与并行策略的耦合

并行维度OE 带来的麻烦
TP(张量并行)oe_up_proj 是小层好切,但 65.6 GB 的表若要切分,hash row_id 与分片的映射要重新设计,否则每张卡都要存全表。
SP(序列并行 / Ulysses)n-gram 需要「前 n 个 token」,而 SP 把序列切到了不同卡上——边界处的上文 token 可能在邻卡,需要额外通信换边界。
PP(流水并行)OE 在 embedding 层(第一段 pipeline stage),但 CPU 卸载让这一 stage 的耗时不确定,容易成为 pipeline bubble 的源头。
Packing样本边界处 n-gram 必须 reset,与 5.4 的污染 bug 同源。

6延伸:OE 思路用到多模态训练的挑战

OE 的本质是「给输入端挂一个超大的、按内容寻址的静态记忆表」。这个思路天然诱人推广到多模态——但难度会再上一个台阶。这里把我对未来训练多模态大模型时引入 OE-style 机制的挑战做一个前瞻梳理。

6.1 「token 是离散的」这个前提在视觉/音频里不成立

文本 OE 能 work,靠的是 token id 是离散整数,可以拼成 combined 值再 hash。但图像 patch、音频帧的原始表征是连续向量

文本里「苹果」永远是同样两个 id,hash 出来落同一行;但同一只猫的两张照片,像素从来不会一模一样,连续特征 hash 一下就分到了完全不同的行——相同语义却查不到同一段记忆,N-gram 记忆表瞬间失效。

要复用 OE,必须先把连续模态离散化(VQ-VAE / 残差量化 codebook 把图像/音频映射成离散 code)。这一步本身就是开放难题:codebook 坍缩、码本利用率低、量化损失都会直接传染给上层的 N-gram 记忆。

6.2 跨模态 n-gram 的组合爆炸

文本只有一个序列,n-gram 是一维的。多模态是图文音交错的序列,「前 n 个 token」可能横跨模态边界:

6.3 显存 / 访存压力指数级放大

文本 OE 已经是 65.6 GB。多模态意味着每个模态都要自己的记忆表,甚至跨模态联合表:

若视觉、音频、文本各配一套 4 张 16 GB 的表,再加跨模态联合表,总量轻松突破数百 GB。6.2/6.3 讲的 PCIe 往返与随机访存问题会按模态数量倍增,CPU 卸载也未必兜得住。
可能的出路(1) 分层记忆:高频跨模态组合留 GPU,长尾放 CPU/host;(2) 借鉴 Engram 的 host prefetch + 确定性寻址;(3) 用学习式寻址(可微检索)替代纯 hash,让相似的连续特征映射到相近的记忆槽,解决 7.1 的「同语义不同 code」问题。

6.4 训练稳定性:多模态本就难,再叠一层静态记忆

多模态训练本身就有模态不平衡、梯度尺度差异、对齐难等问题。再挂一个冻结的大记忆表,会引入新的耦合:记忆表在预训练阶段「学到」的搭配,在 SFT / 跨模态对齐阶段可能与新数据分布冲突,且 5.4 的样本边界污染在「图文交错 packing」下更难定位。

小结OE 把「静态记忆」作为一条独立于「神经计算」的稀疏性轴——这条轴在纯文本上已被验证有效(见下文字节 / DeepSeek 两项工作),但要迁到多模态,离散化、跨模态组合语义、访存工程三道关卡缺一不可,是值得长期投入的方向。

WeLM v4.5 的 OE 并非凭空设计,它属于近一两年「给 LLM 加一条静态记忆 / 超大 embedding 稀疏轴」这一研究脉络。这里详细介绍工业界两项最有代表性的工作——字节跳动的 Over-Tokenized Transformer 与 DeepSeek 的 Engram,帮助理解 OE 的来龙去脉。

① 字节跳动 · Over-Tokenized TransformerICML 2025 · arXiv 2501.16975
Hongzhi Huang 等(ByteDance Seed)· arXiv:2501.16975 · 标题《Over-Tokenized Transformer: Vocabulary is Generally Worth Scaling》

这篇是 WeLM「OE」命名与思想的直接学术源头。核心主张:把输入词表(input vocabulary)和输出词表(output vocabulary)解耦,并大幅放大输入词表

关键设计

最亮眼的结论:用足够大的输入词表,可以让模型达到「两倍大小 baseline 的性能,而几乎不增加计算成本」——因为 embedding 查表是 O(1),不进 FLOPs 密集的 transformer 主干。

对我们的意义:它从 scaling law 层面证明了「OE 这条路值得走」,WeLM v4.5 的 4 张 hash 表就是这套理论的工程落地。本文前半讲的所有 infra 痛点(65.6 GB、CPU 卸载、hash 随机访存),本质都是为这条 scaling law 买的工程单。

② DeepSeek · Engram2026-01 · arXiv 2601.07372
Xin Cheng, Damai Dai, … Wenfeng Liang 等(DeepSeek-AI)· arXiv:2601.07372 · 标题《Conditional Memory via Scalable Lookup: A New Axis of Sparsity for Large Language Models》

如果说字节证明了「放大输入词表有用」,DeepSeek Engram 则把它系统化、理论化,并直面了本文第 5 节列的所有工程挑战。它把这类机制重新定义为一条与 MoE 并列的新稀疏性轴:MoE 是「条件计算(conditional computation)」,Engram 是「条件记忆(conditional memory)」。

关键贡献

和 WeLM OE 的关系Engram = OE 的「理论完成态」。WeLM 的 v2 patch 在工程上做的事(CPU 卸载、确定性 hash 寻址),Engram 用 scaling law 证明了它不仅是省显存的妥协,更是一条独立且最优的稀疏性维度。下次设计 OE 表大小 / 卸载策略时,Engram 的 U 型曲线与 host prefetch 方案是直接可借鉴的。

7.1 三者一图对照

维度字节 OTTDeepSeek EngramWeLM v4.5 OE
核心主张放大输入词表,OE/OD 解耦条件记忆=新稀疏轴4 张 n-gram hash 表加料
n-gram 阶数multi-gramN-gram(可配)2/3/4/5-gram
寻址方式hash embedding确定性寻址 O(1)Knuth 黄金比例 hash
scaling lawlog-linear(越大越好)U 型(有最优分配点)—(工程落地)
卸载方案host memory + prefetchCPU 卸载(v2 patch)
规模多尺度验证27B,超 MoE baseline4×8.19B=32.77B 表
一句话收尾OE 不是 WeLM 独有的奇技淫巧,而是「条件记忆」这条稀疏性轴的一个工程实例。字节证明它有用,DeepSeek 证明它最优且可解释,WeLM 把它跑在了真实训练管线上——而代价,就是本文第 5 节那一长串 infra 硬骨头。