背景与 debug:从 current shell 回到最小 n-gram setting
1. 术语表:每个东西到底是什么
| 术语 | 直观含义 | 在最小实验中的角色 |
|---|---|---|
| n-gram value table / VE | 用连续 token context 查出一个可训练向量。 | 真正产生 train-specific memory 的参数。 |
| bigram / trigram | 分别使用 2 个 / 3 个连续 token 作为 context。 | 当前实验的两条 memory 分支。 |
| gate | 把 table value 乘上一个可学习强度,典型形式是 2×sigmoid(Linear(x))。 | 只在 v / y 消融中使用;input baseline 不需要 gate。 |
| input / over-encoding | 先查表,再把 n-gram value 加到 token embedding。 | 最朴素、最适合作为 baseline 的注入点。 |
| v 注入 | 在 attention 前把 value residual 加进 V。 | 信号会被 attention matrix 混合。 |
| y 注入 | attention 完成后,把 residual 加到 y。 | 信号直接进入 block 输出,gap 最强。 |
| current shell | OPHIS 自己的一整套 GPT forward / optimizer 外壳。 | 历史分支,不是 n-gram gap 的必要条件。 |
2. 三种注入点的伪代码
这里的 ngram_value(idx) 是根据当前 token 的 context 查表得到的向量。它不是一个 scalar,而是与 hidden state 同维度的 trainable vector。
# input / over-encoding:一次注入,后面是普通 Transformer
def forward_input(idx):
x = wte(idx) + wpe(pos)
x = x + ngram_value(idx) # bigram value + trigram value
for block in transformer:
x = block(x)
return lm_head(ln_f(x))
# v residual:先加到 V,再经过 attention 的加权混合
def forward_v(idx):
x = wte(idx) + wpe(pos)
for block in transformer:
q, k, v = block.c_attn(block.ln_1(x)).split(3 * dim)
v = v + gate(x) * ngram_value(idx)
y = attention(q, k, v)
x = x + block.c_proj(y)
x = x + block.mlp(block.ln_2(x))
return lm_head(ln_f(x))
# y residual:attention 完成后直接加到输出
def forward_y(idx):
x = wte(idx) + wpe(pos)
for block in transformer:
q, k, v = block.c_attn(block.ln_1(x)).split(3 * dim)
y = attention(q, k, v)
y = y + gate(x) * ngram_value(idx)
x = x + block.c_proj(y)
x = x + block.mlp(block.ln_2(x))
return lm_head(ln_f(x))
3. 为什么 V 与 y/input 的结果不同
attention 对 V 的计算是线性的。若 n-gram residual 为 G,attention matrix 为 A,则:
A · (V + G) = A · V + A · G。只有当 G 对所有被读取位置完全相同,且后续投影也不改变位置关系时,A · G 才可能近似等于 G;一般情况下它会被不同 query 的 attention 权重重新混合。因此,v 注入并不是“没有加进去”,而是信号路径更弱。诊断结果显示 gate 初始化正常、n-gram residual 确实非零,但其 norm 约为 V norm 的 6.5%,容易被主干 value 淹没。y / input 则绕开了这一步。
4. current shell 到底是什么
早期实验从 vanilla nanoGPT 往上逐步加入机制。后来出现的 current shell 不是某一个独立变量,而是 OPHIS 的完整模型实现分支,包含若干成组差异:
- 不同的 block forward path 与 n-gram residual 组织方式;
- split q / k / v projection、额外 gate、x0 skip、layer pool 等实现细节;
- Muon / AdamW 的参数分组和更新规则;
- RoPE、RMSNorm-style、untied embedding、特殊初始化等模型外壳。
5. 频率统计 debug:为什么必须用真实 context key
模型内部为了节省表大小,会把完整 context hash 到有限的 table row。这个 hash row 适合查参数,但不适合判断“这个 context 是否在训练集出现过”。不同真实 trigram 可能碰撞到同一 row,导致 novel context 被误报为 seen。
# 用于频率统计的真实 key(示意) bigram_key = prev_token * vocab_size + current_token trigram_key = prev2 * vocab_size**2 + prev * vocab_size + current_token hit_count = count_train_contexts(real_key) bucket = "novel" if hit_count == 0 else frequency_bucket(hit_count)
修复后得到 bigram novel 约 4.3%、trigram novel 约 31.2%,与历史离线统计一致。模型仍然可以继续使用 hash table;只有分析模块改用真实 token tuple 编码。
6. 优化器为什么 table 用 RMSProp
n-gram table 很大而且访问稀疏。当前 setting 对 table 使用 RMSProp(β₁=0, β₂=0.999),对 backbone 使用 AdamW。这样每次访问 table row 时,更新方向更接近当前梯度,不会被 momentum 把多个 context 的历史更新混在一起。
这是一项让现象稳定、便于复现的训练 setting;gap 的来源仍然是 train-specific n-gram memory 与固定顺序 replay 的组合,而不是 optimizer 名字本身。
7. 相关记录
svbird 的 regime-bridge 报告记录了从 vanilla 负例逐步接近 current core 的历史路径。它对理解项目演化有用,但本页的最小复现结论以 ngram-gap-lab 的 clean setting 为准。