Qwen3.8-Flash-Next Engram表免权重注入100条事实的工
I wrote 100 facts into Qwen3.8-Flash-Next's Engram table without touching a single weight. It works well. Not well well yet.
这是一篇极具参考价值的工程复盘,详细拆解了如何利用 Qwen 新架构的特性进行零参数微调,数据详实且开源了完整复现流程,适合做轻量级知识注入的研究者。
Qwen3.8-Flash-Next ships with a giant n-gram lookup table bolted onto the side of the transformer, and when I saw it I had what I can only call an idiot's insight:
Qwen3.8-Flash-Next 在 Transformer 侧面挂载了一个巨大的 n-gram 查找表,当我看到它时,我产生了一种我只能称之为“蠢人的顿悟”的想法:
hey, that's a table. You can write stuff in a table.
嘿,那是一张表。你可以在表里写东西。
Three weeks later I can confirm that yes, it is a table, and yes, you can write stuff in it. I can also confirm that "it's just a table" is the kind of sentence that costs you three weeks.
三周后我可以确认,是的,它确实是一张表,而且你确实可以在里面写东西。我也可以确认,“它只是一张表”这种话会让你耗费三周时间。
Here's the mechanism in one paragraph, because nothing below makes sense otherwise. At every position the model takes the last 2 and 3 tokens, hashes them into 16 rows of a 320-million-row table, reads those rows, and adds them into the residual stream early on. That's it. A key-value memory addressed by exact token n-grams, read before basically all of the model's actual thinking. DeepSeek calls the design Engram. llama.cpp calls Qwen's version the PLE table.
这里用一段话描述其机制,因为否则下文将毫无意义。在每一个位置,模型取最后两个和三个 token,将它们哈希到一个拥有 3.2 亿行的表的 16 行中,读取这些行,并在早期阶段将它们加到残差流中。就是这样。一个由精确 token n-gram 寻址的键值记忆,在模型几乎所有实际思考之前被读取。DeepSeek 称该设计为 Engram。llama.cpp 称 Qwen 的版本为 PLE 表。
What I built writes new facts into that table. No weights change. None. Only table rows get trained, and they come out as one 9 MB overlay file the engine applies while it reads the table. The GGUF on disk is never touched. Delete the overlay and the model is exactly its old self again. I'm calling it ENGRAFT, mostly because all the good acronyms were taken.
我所构建的系统将新事实写入该表中。权重没有任何变化。完全没有。只有表行被训练,它们最终生成一个 9 MB 的覆盖文件,引擎在读取表时会应用该文件。磁盘上的 GGUF 文件从未被触碰。删除覆盖文件后,模型又完全回到它原来的状态。我将其命名为 ENGRAFT,主要是因为所有好的缩写都已被占用。
One thing up front, because you'd find out anyway: I built this in Claude Code sessions, one human (me) driving. I set the goals, approved every design step, ran the hardware and read every result; design, implementation, adversarial review and verification were done by separate model instances, and I made the calls. I'd rather you know that before you read the numbers than after. I'm not here for stars. I'm here to have this broken by someone who knows what they're doing.
先说一件事,因为你迟早会知道:我是在 Claude Code 会话中构建的,由一个人(我)驱动。我设定目标,批准每一个设计步骤,运行硬件并阅读每一个结果;设计、实现、对抗性审查和验证均由不同的模型实例完成,而我做出决策。我希望你在看数据之前就了解这一点,而不是之后。我不是为了点赞。我是为了让懂行的人来把它拆穿。
The numbers
数据
The test world is Quail: 100 facts about a place that doesn't exist, written as seven Italian documents, which become 3,209 training sentences and 841 held-out test sentences. 88 facts I made up from scratch; the other 12 follow Philip K. Dick's We Can Remember It for You Wholesale — the one that became Total Recall — as I remember it, which is to say badly. It felt thematically correct to implant memories into something about a guy getting memories implanted.
测试世界是 Quail:关于一个不存在的地方的 100 个事实,写成七份意大利语文档,这些文档转化为 3,209 个训练句子和 841 个保留测试句子。其中 88 个事实是我从头捏造的;其余 12 个遵循菲利普·K·迪克的《We Can Remember It for You Wholesale》——也就是后来改编成《全面回忆》的那部作品——按照我的记忆,也就是说记得很烂。将一个关于某人植入记忆的故事与植入记忆的主题相结合,感觉在主题上是正确的。
Exact answer, greedy decoding, on the real llama.cpp engine, 841 sentences it had never seen:
在真实的 llama.cpp 引擎上,使用贪婪解码,针对它从未见过的 841 个句子,给出精确答案:
- with the overlay: 0.841
- base model, same sentences: 0.005 (4 of 841, and I'm fairly sure it guessed)
- whole thing re-run with a second seed: 0.873
- by question shape: chat 0.883, paraphrase 0.849, statement 0.839, question 0.813, cloze 0.794
- 14,032 rows trained, out of 320,000,000. Overlay: 9 MB.
- 2.4 hours on an integrated GPU — an AMD Ryzen AI MAX+ 395, 128 GB unified memory. A mini PC.
- 带有覆盖文件:0.841
- 基础模型,相同句子:0.005(841 句中的 4 句,而且我很确定它是猜对的)
- 使用第二个种子重新运行整个流程:0.873
- 按问题形状分类:对话 0.883,改写 0.849,陈述 0.839,疑问 0.813,填空 0.794
- 训练了 14,032 行,总计 320,000,000 行。叠加层大小:9 MB。
- 在集成 GPU 上耗时 2.4 小时——使用的是 AMD Ryzen AI MAX+ 395,配备 128 GB 统一内存。这是一台迷你 PC。
Two things I'd point at in that table. The first is that nothing falls off a cliff by question shape: the spread from chat to cloze is nine points, so the overlay isn't just memorizing one sentence pattern. The second is the second seed. 0.873 against 0.841 means the result isn't one lucky descent, and the gap between the two is roughly the size of the noise I should expect.
我想指出表格中的两点。第一点是没有任何类别出现断崖式下跌:从对话到填空的差距为九个百分点,说明叠加层并非仅仅在记忆某一种句式模式。第二点是第二个种子(seed)的结果。0.873 对比 0.841 意味着结果并非一次侥幸的收敛,且两者之间的差距大致符合我预期的噪声水平。
Separately, on a different corpus of short made-up facts, with a lighter metric (first answer token at rank 1, expert routing pinned to the base model's picks): 24 facts 0.804, 100 facts 0.792, 300 facts 0.821. Flat. No ceiling in sight at 300. Different corpus, different metric — I'm keeping them apart on purpose, because gluing them together would make this look better than it is.
另外,在一个由简短虚构事实组成的不同语料库上,使用较轻的评估指标(排名第一的首个答案 token,专家路由固定为基础模型的选取):24 条事实得分 0.804,100 条事实得分 0.792,300 条事实得分 0.821。表现平稳。在 300 条事实时未见天花板迹象。由于语料库和指标均不同——我有意将它们分开呈现,因为若合并展示会使结果看起来比实际情况更好。
The same world in three languages
同一个世界,三种语言
I rebuilt Quail fact by fact in Chinese and in English and grafted each one. Exact answer on the real engine: Italian 0.841, English 0.797, Chinese 0.676; base models 0.005, 0.004, 0.003. Collateral damage on neutral text of each language: 0.0131, 0.0160, 0.0166.
我用中文和英文逐条重建了 Quail 的事实并进行了嫁接。在实际引擎上的精确答案准确率:意大利语 0.841,英语 0.797,中文 0.676;基础模型分别为 0.005、0.004、0.003。对各语言中性文本的附带损害(collateral damage):0.0131、0.0160、0.0166。
The ordering looks like a language effect. It probably isn't. Control for how much training text each fact actually received and the effect disappears — the language coefficient's 95% interval is [−0.124, +0.020], which contains zero. The boring explanation: my row budget was tuned on Italian, Chinese burns almost twice as many new rows per sentence, so the same budget buys half the training sentences per fact. The test that settles it — a Chinese cell given equal mass per fact — hasn't been run.
这种排序看起来像是语言效应。但很可能并非如此。在控制每条事实实际接收到的训练文本量后,该效应消失——语言系数的 95% 置信区间为 [-0.124, +0.020],其中包含零。乏味的解释是:我的行数预算是基于意大利语调整的,而中文每句话消耗的新的行数几乎是意大利语的两倍,因此相同的预算下,中文每条事实获得的训练句子数量减半。能够定论的测试——即给中文单元格分配每条事实相等的权重——尚未执行。
But here's the one that surprised me. A graft does not cross languages at all. I took the Italian test set and asked it against the Chinese overlay:
但这里有一个让我惊讶的发现:嫁接操作根本不会跨语言生效。我将意大利语测试集应用于中文叠加层进行测试:
- exact answer 0.0048, against the base model's 0.0048
- first answer token at rank 1 0.0951, against the base model's 0.0951
- 764 of 841 sentences identical to the base model down to the probability of the first token
- and the engine did read 922 overlay rows along the way
- 精确答案准确率为 0.0048,与基础模型的 0.0048 持平
- 排名第一的首个答案 token 准确率为 0.0951,与基础模型的 0.0951 持平
- 841 句中有 764 句与基础模型完全一致,直至首个 token 的概率层面
- 并且引擎在此过程中确实读取了 922 行叠加层数据
The overlay fires and changes nothing. Even probes that mix the scripts — the subject's name in one language, the sentence in the other — get 0 of 20 and 1 of 20. A fact has to be grafted in the language it will be asked in, because the rows are keyed by the tokens of its own script. One pair of languages, one cell, but the numbers are not subtle.
叠加层触发后毫无变化。即使是混合了两种语言的探针——一种语言使用主体名称,另一种语言使用句子——也仅获得 20 次中的 0 分和 1 分。事实必须以其将被提问的语言进行嫁接,因为行是根据其自身脚本的标记键控的。一对语言对应一个单元格,但数字并不微妙。
What doesn't work
无效的部分
Three things, and this is the part I actually want you to read.
三点原因,这也是我真正希望你阅读的部分。
1. Two facts in one question. Both answers right in 10 of 83 probes; exactly one in 20; neither in 53. Each fact on its own is answered well, so something breaks when they have to combine. When I went back and read all 83 outputs, most of the failure turned out to be my measurement rather than the method — the generation was capped too short, the prompt had no chat template so the model wandered off into free continuation, and I had never measured single-fact recall in free generation to compare against. A seven-arm measurement that separates those from real composition failure is running now. I'd rather fix the cause than the symptom.
1. 一个问题中包含两个事实。在 83 个探针中,两个答案都正确的有 10 次;恰好一个正确的有 20 次;都不正确的有 53 次。每个事实单独回答时表现良好,因此当它们需要结合时出现了问题。当我回去查看所有 83 个输出结果时,大多数失败实际上是我的测量方法所致,而非方法本身——生成被截断得太短,提示词没有聊天模板,导致模型游离到自由续写中,而我从未在自由生成中测量过单事实召回率以作对比。目前正在进行一项七臂测量,以区分这些情况与真正的组合失败。我更愿意修复根本原因,而非表面症状。
2. The rest of the model moves. Mean KL to the base model on neutral text: 0.0037 at 24 facts, 0.0059 at 100, 0.0067 at 300, against a yardstick of 0.0033 — an overlay that only re-quantizes the same rows, i.e. the table's own noise floor. Damage grows slower than the number of facts, but it grows, and on Quail at 100 facts it's 0.0131, about 4× the yardstick, which is past the point where I get to call it noise. The fix I want to try is narrower addressing: key the overlay to the exact token window instead of the hashed rows, so each fact steps on fewer shared rows.
2. 模型的其余部分发生了偏移。中性文本相对于基础模型的平均 KL 散度:24 个事实时为 0.0037,100 个时为 0.0059,300 个时为 0.0067,而基准线为 0.0033——这是一个仅对相同行进行重新量化的叠加层,即表格自身的噪声底限。损害随事实数量增长的速度较慢,但确实在增长;在 Quail 数据集上,100 个事实时的值为 0.0131,约为基准线的 4 倍,这已超过我可称之为噪声的范围。我想尝试的修复方案是更精细的地址定位:将叠加层键控到确切的 token 窗口,而不是哈希行,从而使每个事实踩到的共享行更少。
3. Facts are stuck in their language. The section above. Useful if you want per-language behaviour, a real limit if you expected one graft to serve every user.
3. 事实被锁定在其语言中。见上文。如果你希望实现按语言的行为差异,这是有用的;但如果你期望一个嫁接能服务于所有用户,这就是一个真正的限制。
One negative result worth having, since it saved me from fixing the wrong thing: when a graft fails, it's not hash collisions. I looked. There are none. Failures track the direction of the update and how much text a fact got — when two facts share rows, the heavier one wins about 60% of the time. Budget problem, not addressing problem.
有一个值得拥有的负面结果,因为它让我避免了修复错误的问题:当嫁接失败时,并非哈希冲突。我检查过了,根本没有冲突。失败情况与更新的方向以及事实获得的文本量相关——当两个事实共享行时,权重较大的那个大约 60% 的情况下胜出。这是预算问题,而非地址定位问题。
And the limits I can't argue away: rephrasing only works as far as the corpus reaches (the table fires on exact n-grams, by construction); one model measured, with DeepSeek V4.1 Flash next and nothing tried on it yet; and no head-to-head against LoRA or ROME/MEMIT, which is the comparison I'd demand first if I were you.
还有我无法辩驳的限制:改写仅在语料库覆盖范围内有效(由于构造原因,表格基于精确 n-gram 触发);目前只测量了一个模型,DeepSeek V4.1 Flash 紧随其后,且尚未对其做任何尝试;也没有与 LoRA 或 ROME/MEMIT 进行直接对比,如果我是你,我会首先要求这种对比。
What's public
公开的内容
Not everything. The part that turns arbitrary documents into a usage corpus — including the row budget that decides which rows get trained — is still mine while I work on it. I'd rather ship it when I'm not embarrassed by it.
并非全部。将任意文档转化为使用语料库的部分——包括决定哪些行参与训练的行列预算——在我完成工作之前仍归我所有。我更愿意在我不为此感到尴尬时再发布它。
Everything else is up: the method, the descent and capture code, the measurement code, the engine fork, the compiled Quail corpus with its row set, the overlays, the config of the measured run, and every result file behind every number above. There's a script that recomputes each number in the README from its file, so if one ever drifts, you get to catch me.
其余部分均已公开:方法、下降与捕获代码、测量代码、引擎分支、带有其行列集的编译版 Quail 语料库、叠加层、已测量运行的配置,以及上述每个数字背后的每一个结果文件。有一个脚本可以从文件中重新计算 README 中的每个数字,因此如果某个数字发生漂移,你可以借此揪出我。
github.com/fulvian/engraft-ngram
github.com/fulvian/engraft-ngram
Two ways in:
两种接入方式:
- No model needed: descent, capture and measurement all run against fakes. uv run pytest tests/... on any laptop and you can watch the mechanism work.
- 无需模型:下降、捕获和测量均针对模拟数据进行运行。在任何笔记本电脑上执行 uv run pytest tests/...,即可观察该机制的运作。
更进一步:量化金融体系
看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力