N-gram Gap 机制指南

本文档解释 n-gram gap 实验中的核心概念:什么是 table、gate、shell、optimizer grouping,它们如何组合产生 train/val gap,以及关键实验数据。目标读者是刚接手项目的研究者。

术语表

以下是本文档中频繁出现的核心术语。每个术语首次在正文中出现时用 加粗 标记,在此处可快速查阅定义。

术语定义
n-gram table / VE一张巨大的查找表(lookup table),用几个连续 token 的 hash 值做 key 来查表。等价于 "value embedding"(VE)。包含 bigram(2-gram)和 trigram(3-gram)两种。
gate(门控)控制 n-gram table 输出注入 attention 强度的机制:2×sigmoid(Linear(x)),输出范围 [0, 2]。gate 初始化为 1.0(中性),训练中模型自主调节。
shell(外壳)指 transformer block 的架构实现。本文档涉及两种:nanoGPT shell(经典 nanoGPT 架构)和 current shell(从 current/GPT 架构移植,含 x0 residual、resid_lambda、split QKV)。gap 仅在 current shell 下出现。
gap / gap onsettrain loss 阶梯下降同时 val loss 上升的现象。"gap onset"(也称 "fork")指 gap 开始出现的具体训练步数。
hit / hit count某个 n-gram context(如 trigram 的 2-token 前缀)在整个训练集中总共出现的次数。是数据的静态属性,不随训练变化。在 §9 中用于按频率分组分析 loss。
novel / novel bucket指 train 中从未出现过的 context(hit count = 0)。在 §9 的 hit count 分析中,novel bucket 是 val 翘起的最大单一来源。
baseline_current / B.1本文档 §2 现象图、§7.9 norm 图、§9 频次分解统一使用 baseline_current(current shell + current-style grouping + bigram+trigram RMSProp,seed42,1000 step,vocab=8192,n_layer=8;epoch 边界 [338, 687],epoch3 gap 从 ~0.5 扩大到 1.33)。旧的 B.120260725e_nano_rmsprop_fixv3,nanogpt_original,gap 0.688)仍作为 §10 干预实验的 nanogpt_original 基线。
fixv2 / fixv3实验方法论的两个迭代版本。fixv2 使用 epoch 冻结的在线累积计数器,fixv3 替换为完全离线预计算的频率表。本文档当前所有数据均来自 fixv3(详见 agent-experiment-log.html §Gotchas G7)。
obcurvesobservable curves 的缩写。训练过程中记录的结构化指标数据(gate norm、table content RMS、hit bucket loss 等),存储在 observable_curves.obcurves.json 中。
context / context_hashn-gram 的条件前缀。如 trigram 的 context = (token[i-2], token[i-1]),bigram 的 context = (token[i-1])。context_hash 是 hash 后的整数标识。
gated_value_rms / raw_value_rmsraw_value_rms = n-gram table 查出的 value 在 gate 之前的 RMS;gated_value_rms = gate × table value 的 RMS,即实际注入 attention value head 的量。
optimizer grouping参数分组与 LR 分配策略。nanoGPT-style grouping 给 n-gram table LR=0.004,current-style grouping 给 LR=0.6(差 150 倍),这是 gap 出现的必要条件。
epoch boundary训练数据遍历完一轮的分界步数。当前数据集:epoch 1→2 在 step 337,epoch 2→3 在 step 686,epoch 3→4(moredata)在 step 1384/2082。
warmdownLR schedule 的衰减阶段。当前实验在 step 750 附近 lrm_adam 从 0.33 降到 0.05,触发 train loss 的同步坍塌(memorize 突变)。
RMSProp(table optimizer)用于 n-gram table 的优化器。无 first moment(momentum),β₂=0.999 使二阶矩 v 能跨 epoch 持久化(半衰期 ~693 步 ≈ 2 个 epoch)。与 AdamW 的关键区别在于写入方向 = 当前梯度,不被历史 momentum 稀释。
Muon(matrix optimizer)用于 transformer 矩阵参数的优化器。在 nanogpt_original shell 下,table RMSProp + matrix Muon 的组合产生 delayed gap(onset 在 epoch2 而非 epoch3)。
collisionhash 冲突:不同的 n-gram context 被映射到同一个 table row。实测 batch 内 bigram collision ~20%,trigram ~8%。每层用不同 hash prime 降低有效 collision。
per-token loss / total contributionper-token loss = 该 bucket 内每个 token 的平均 loss;total contribution = frac × loss(该 bucket 对总 loss 的贡献,考虑了 token 占比)。§9 的交互式图表可切换两种视角。

1.1 什么是 n-gram table

n-gram table 就是一张巨大的查表(lookup table),和 token embedding 没有本质区别。区别在于:token embedding 用 token id 做 key,n-gram table 用几个连续 token 组成的 context 做 key

Token embedding (对照) key = token id 如 " Apple" → 313 nn.Embedding(32768, 768) 每个 token 一行 Bigram table key = hash(wₜ₋₁, wₜ) 两个 token → 一个整数 nn.Embedding(2M, 64) 每行 64 维,共 2M 行 Trigram table key = hash(wₜ₋₂, wₜ₋₁, wₜ) nn.Embedding(2M, 64) × 2 sub-tables

table 的规模

table 类型context行数每行维度hash 方式
unigram单个 token32,768768直接用 token id
bigram前一个 + 当前 token2,097,152 (32K × 64)64 × 2 = 128乘法 XOR hash
trigram前两个 + 当前 token2,097,15264 × 2 = 128乘法 XOR hash
fourgram前三个 + 当前 token2,097,15264 × 2 = 128乘法 XOR hash

bigram/trigram/fourgram 的行数都是 vocab_size × 64 = 32768 × 64 = 2,097,152。hash 把 n 个 token 映射到这个范围内的一个整数。每层用不同的 hash prime,使不同层的同一 context 映射到不同行。

为什么不用直接查表? 因为 bigram 的可能组合有 32768² ≈ 10 亿种,trigram 有 32768³ ≈ 3.5 万亿种。直接建表不现实。hash 到 2M 行会产生 collision(不同 context 映射到同一行),但 collision 率很低(batch 内 bigram ~20%,trigram ~8%),而且每层用不同 prime + K=2 双 hash 进一步降低有效 collision。

1.2 Hash 机制详解

hash 的作用是把 n 个 token id 压缩成一个 row 号。具体实现是乘法 + XOR + 取模,全部在 GPU 上以 tensor 整数运算完成。

train.py:1536-1546 — hash 计算
# 输入: prev_idx, prev2_idx 是 shifted token id tensor [batch, seq_len] # idx 是当前 token id tensor [batch, seq_len] # Bigram: 两个 token → 一个 row row = ((prev_idx * p1) ^ (idx * p2)) % 2_097_152 # Trigram: 三个 token → 一个 row row = ((prev2_idx * lp[0]) ^ (prev_idx * lp[1]) ^ (idx * lp[2])) % 2_097_152

每层使用不同的 prime 组(来自 MurmurHash/FNV/golden-ratio 家族),保证不同层的同一 context 映射到完全不同的行。K=2 multi-hash 是指每个 n-gram 查两次独立 hash,各取半个维度(64 维),拼成完整的 128 维。

collision 的实测数据

指标bigramtrigram含义
batch 内 unique_frac0.800.92trigram 的 collision 更低 (92% unique vs 80%)
within_batch_repeat20%8%同一 batch 内有多少 token 共享 row
entropy_norm0.9830.993trigram 的 row 分布更均匀
rows_ever_seen (step 1000)97%99%到训练结束时大部分 row 都被访问过
rows_epoch_lag10%1.5%epoch 间同位置 row overlap 很低

关键发现:epoch 间同一文本映射到的 row 几乎完全不同(trigram 只有 1.5% overlap)。gap 不是来自"精确同 row 重复读",而是来自 row 内容的统计积累。

1.3 Gate 机制:table 如何注入到模型

n-gram table 查出的 value 不会直接加到模型输出上——它经过一个 input-dependent gate 控制注入强度。gate 本身是一个小 linear layer + sigmoid。

1. Hash tokens → row # GPU 整数运算 2. 查表 Embedding(row) → value [B,T,128] 3. Gate 2×sigmoid(Linear(x)) → gate [B,T,heads] × 5. 加到 value v_heads += gated 残差注入 hidden state x attention V heads → attention 继续计算 softmax(QK/√d)·(V+gated)
train.py:1721-1728 — gate 注入
def _add_value_residual(self, branch, v_heads, residual, gate, x, channel_start, heads): # residual = table 查出的 value [B, T, heads, head_dim] residual = residual.view(x.size(0), x.size(1), heads, self.head_dim) # gate: 从 hidden state 取前 32 维 → linear → sigmoid → ×2 gate_input = x[..., channel_start : channel_start + self.ve_gate_channels] value_gate = 2 * torch.sigmoid(gate(gate_input)) # 范围 [0, 2] # 逐位置乘 gate gated = value_gate.unsqueeze(-1) * residual # 加到 attention value 上 return v_heads + gated

gate 初始化为全 0 权重,所以 2×sigmoid(0) = 1.0——训练初期 gate 全开(中性),之后模型自己学习该在什么时候、什么位置放大或抑制 n-gram value 的注入。

gate 是 gap 的关键放大器。实验数据显示:epoch1 期间 trigram gate_rms 只有 0.088(几乎不工作),但进入 epoch2 中段后 gate_rms 上升到 0.29——gate 被训练起来后,table 中积累的历史内容才被"放大"为可见的 train loss 下降。

1.4 Shell 是什么:nanoGPT vs current shell

项目中有两类模型架构,称为 "shell"(外壳)。它们的区别在于 transformer block 的内部结构。

nanoGPT shell

经典 nanoGPT 架构。标准 LayerNorm、标准 attention、标准 MLP。

# NanoGPTBlock x = LayerNorm(x) x = x + SelfAttention(x) x = LayerNorm(x) x = x + MLP(x)

即使加 n-gram table + RMSProp + 高 LR,也不产生 gap

current shell (GPT)

从 current/GPT 架构移植的 block。包含 x0 residual、resid_lambda、split QKV 等额外结构。

# Block (current shell) x = resid_lambda*x + x0_lambda*x0_gate*x0 x = x + SelfAttention_splitQKV(x) x = x + MLP(x) # attention 内部: V += ngram_gate × table_value

n-gram table 注入到 attention value stream 的方式和 nanoGPT shell 不同。在 current shell 下,gap 出现。

关键区别不在 table 本身(两者都可以有相同的 n-gram table),而在于 reader 的传输路径:current shell 的 attention split QKV + 残差路径使 n-gram value 在后层 residual 中被更有效地放大。这是为什么 nanoGPT shell 即使加上所有组件仍然无 gap,而切到 current shell 后 gap 出现。

"current-style n-gram injection" 指的是 n-gram value 被注入到 attention value stream 的具体坐标和方式。nanoGPT-style injection 和 current-style injection 的数学结构不同(注入位置、gate 通道分配、与 attention 的交互方式),这决定了 reader Jacobian Ji,a 的大小。

1.5 Optimizer grouping:两个独立的概念

项目中 "optimizer" 相关的配置有两个独立维度,经常被混淆。这里明确区分。

维度一:n-gram table 用什么 optimizer

nanogpt_ngram_optimizer 控制,决定 table 参数的更新算法:

取值unigram tablebigram/trigram table含义
"adamw"AdamWAdamW全部用 AdamW(有 momentum)
"rmsprop"RMSPropRMSProp全部用 RMSProp(无 momentum)
"mixed"AdamWRMSPropunigram 用 AdamW,high-order 用 RMSProp

维度二:参数分组与 LR 分配

optimizer_grouping 控制,决定每类参数的学习率

参数类型nanoGPT-style groupingcurrent-style grouping
embedding (wte + wpe)0.0040.6
lm_head (unembedding)0.0040.004
unigram VE0.0040.6
bigram/trigram VE0.0040.6
norm params0.0040.004
matrix (Muon)0.040.04
关键差异:current-style grouping 给 n-gram table 的 LR 是 0.6,而 nanoGPT-style 只有 0.004——差 150 倍。这个 LR 差异是 gap 出现的必要条件:高 LR 使 table 在 epoch1 内快速积累 train-specific content,到 epoch2 时形成足够强的 history state。

RMSProp 对比 AdamW:为什么对 sparse table 更合适

AdamW(有 first moment)
m = β₁·m + (1-β₁)·grad # momentum v = β₂·v + (1-β₂)·grad² update = lr · m / (√v + ε) # β₁ = 0.8 → 半衰期 ~3 步 # sparse row 隔几十步才被访问 # → m 已衰减到 ~0,等于每次"重启"
RMSProp(无 first moment)
v = β₂·v + (1-β₂)·grad² update = lr · grad / (√v + ε) # β₂ = 0.999 → 半衰期 ~693 步 # ≈ 2 个 epoch 的长度 # → v 能跨 epoch 持久化 # → 写入方向 = 当前梯度,不被稀释

2. Gap 现象与因果链

以下为 baseline_current 的真实 loss 记录(current shell + current-style grouping + bigram+trigram RMSProp,seed42,1000 step,vocab=8192,n_layer=8)——与 §7.9 / §9 的 norm / 频次分解同一 run

3 4 5 6 7 8 9 0 100 200 300 400 500 600 700 800 900 1000 epoch 1 epoch 2 epoch 3 training step loss Train & Val Loss Overview train val
train loss val loss
📊 数据来源:baseline_current (current shell + current-style grouping + bigram+trigram RMSProp, vocab=8192, n_layer=8, seed42, 1000 step)

关键现象:epoch2 时 train 与 val 基本贴合(gap ~0.04);进入 epoch3 后 train 持续阶梯下降、val 停滞并回升,gap 从 ~0.5 扩大到 1.33。图中曲线直接来自真实训练记录,与下方 §7.9 / §9 同一 run。

steptrain lossval lossgap现象
3384.8545.0380.184epoch1→2 边界,gap 极小
5004.4374.4760.040epoch2 中段,train 与 val 贴合
6873.9114.4240.513epoch2→3 边界,gap 开始扩大
8003.7184.3770.659train 持续下降
9003.5964.3620.766val 停滞,gap 扩大
10003.1974.5271.330epoch3 末,val 回升、gap 超过 1.3

因果链:gap 是如何产生的

以下因果链基于 baseline_current 的 observable 数据(与 §7.9 / §9 同一 run)。每个环节都有直接测量证据。

epoch1-2: table 积累 train-specific content
高 LR + RMSProp(无 momentum 稀释)使每个被访问的 row 写入纯 train 方向的 gradient,table content RMS(raw_value_rms)在训练中持续增长。但此时 reader gate 还没训练起来,注入幅度(gated value RMS = gate × table)有限,所以 epoch2 时 train 与 val 基本贴合(gap ~0.04)。
epoch2→3 边界: gate 通道建立
gate_rms 在 epoch2-3 抬升,gate 通道被激活,table 中已积累的历史内容开始被放大为 logits 上的有效 signal(gated value RMS 同步增长,见 §7.9 双轴图)。
train 阶梯下降 gap
table 中的历史 row 内容在 train probe 上大幅降低 loss。train loss 从 4.44(step 500)阶梯下降到 3.20(step 1000)。
val loss 翘起 gap
table 中的 train-specific row content 被 val 文本访问时,因 residual 方向不对齐而产生 cross-penalty。val loss 从 4.48(step 500)停滞并回升到 4.53(step 1000)。
gap 持续扩大 gap
train loss 持续下降,val loss 停滞/回升。gap 从 0.04(epoch2)扩大到 1.33(epoch3 末),且仍在上升。频次分解(§9)显示这些被放大的 pattern 主要让 novel / 低中频 context 过拟合。

7. Table Norm × Gate:loss↔norm 时间关系

视图 A:loss↔norm。与 §9 的 loss↔频次分解同一实验(baseline_current,current shell)。

统一 setting:本图与 §9 都用 baseline_current (current shell + current-style grouping + bigram+trigram RMSProp,seed42,1000 step,vocab=8192,n_layer=8, epoch 边界 [338, 687],epoch3 gap 从 ~0.5 扩大到 1.33)—— 与上方 §2 现象图同一 setting 系(current shell,gap 量级远大于 nanogpt_original), 且同时记录了逐 step 的 gate/table/gated norm按频次分桶的 loss

§7.9 Loss × n-gram Norm(双轴复合图)

左轴 train/val/gap loss,右轴 n-gram norm(gate_rms / table content RMS / gated value RMS)。 切 context(unigram/bigram/trigram)、切 loss 曲线、开关各 norm。判据:注入 norm (gated value RMS = gate × table)的增长时点是否与 gap 爆发对齐。

图 7.9:loss(左轴)与 n-gram norm(右轴)随 step 演化。下方表格给出每 epoch 变化率,可直接读出 norm 增长峰值 epoch 是否与 gap 峰值 epoch 对齐。
📊 数据来源:baseline_current (current shell + current-style grouping + bigram+trigram RMSProp, vocab=8192, n_layer=8, seed42, 1000 step)
读图:table content RMS 持续增长,gate 部分调节, gated value RMS(注入)= gate × table 才是真正加到 value head 的量。把 loss 切到 gap (val−train)、 norm 留 gated value RMS,可见注入幅度的增长与 gap 的扩大在时间上对齐(epoch3 train 降到 3.2、val 升到 4.5,注入 norm 同步抬升)—— gap 不是 gate 单独造成,而是 table 累积的 train-specific pattern 经 gate 通道被持续放大。 下一节 §9 从频次角度补充:这些被放大的 pattern 主要让低中频 / novel context 过拟合。

9. 频次分解分析

把 loss 按 n-gram context 的训练频次分解,是定位 gap 来源的核心观测量。

频次定义(统一口径)

每个 n-gram context 的频次 = 它在单个 train epoch 内出现的精确次数,由训练前一次性离线扫描的 精确频率索引GlobalNgramFrequencyIndex)给出,整个训练过程不变。 频次定义仅在 train 上;val 的 token 按"其 context 在 train 中的频次"归类到同一套桶, 因此 val 有 novel 桶(context 在 train 中从未出现),而 train 按定义没有 novel 桶。

桶的边界(精确频次,上下界闭区间):novel(0) / 1 / 2 / 3 / 4 / 5 / 6-10 / 11-20 / 21-50 / 51-100 / 101-200 / 201-500 / 501-1k / 1k-5k / 5k+。 低频段被细分为单值桶(1/2/3/4/5),便于直接观察最低频 context 的过拟合。

与 §7 同 setting(视图 B)

本章频次分解与 §7 的 loss↔norm 图用同一 runbaseline_current (current shell + current-style grouping + bigram+trigram RMSProp,seed42,1000 step,vocab=8192,n_layer=8, epoch3 gap 从 ~0.5 扩大到 1.33)——与 §2 现象图同一 setting 系,gap 量级远大于 nanogpt_original。 §7(视图 A)说"注入 norm 在增长、与 gap 时间对齐",本章(视图 B/C)补充"这些被放大的 pattern 主要让 novel / 低中频 context 过拟合"——同一现象的两个测量面。

9.1 频次分桶:train / val / gap 损失曲线

下面这张复合图把每个频次桶的 train loss、val loss、gap(val−train) 放进同一个面板,可切换:

图 9.1:频次分桶复合图(也可直接打开 interactive/fig_gap_by_freq.html 单独查看/放大)。 bigram 与 trigram 均拥有完整频段(含 novel 与 1/2/3/4/5 单值低频桶);trigram 的 novel 桶在 val 上占 ~30%,是 val 翘起的主要来源。
📊 数据来源:baseline_current (current shell + current-style grouping + bigram+trigram RMSProp, vocab=8192, n_layer=8, seed42, 1000 step)
关键读图结论
  1. gap 是全局性的,低中频 bucket 最严重:所有非高频桶的 gap 在 epoch3 爆发。 切到 gap 视图可见低中频(1/2/3、6-10、11-20、21-50)bucket 的 gap 最大,高频(5k+)bucket 的 gap 最小。
  2. train 与 val 的分离在低频端最明显:切到 train+val 视图,低频桶的 train loss 显著低于 val(过拟合), 高频桶两者接近(QA pattern 天然可预测)。
  3. novel 桶是 val 翘起的最大单一来源:切到 总贡献 视图,novel 桶(仅 val,占 trigram val token 的 ~30%)贡献最大, 其次是 21-50/51-100 等中频桶;高频桶虽然 per-token gap 小,但若 token 占比高也会有一定的总贡献。
  4. trigram 的 novel 占比远高于 bigram:trigram 组合空间远大于 train 文本,~30% 的 val trigram context 从未在 train 出现; bigram 的 novel 占比仅 ~4%。这正是 trigram 对 gap 贡献更大的结构性原因。

9.2 命中频次分布(单 train epoch 内)

在解释 gap 之前,先看训练数据的频次结构本身。频次定义仅在 train:统计每个 context 在一个 train epoch 内出现的精确次数。 下面这张复合图分别统计 bigram / trigram,提供 token 占比视角与两个 split:

图 9.2:命中频次分布(柱状 + 累积)。左:分桶占比;右:从低频到高频的累积分布。 trigram 的 val novel 占 ~30%(大量 val context 从未在 train 出现);bigram 的 val novel 仅 ~4%。train 侧呈典型幂律分布,中高频 context 承载大部分 token。
📊 数据来源:baseline_current (current shell + current-style grouping + bigram+trigram RMSProp, vocab=8192, n_layer=8)
机制解读:频次结构如何决定 gap

训练集的 context 频次呈典型幂律分布:train 侧大部分 token 来自少数高频 context, 但 val 侧有大量 context(尤其 trigram,~30%)在 train 中从未出现(novel)。gap 的成因正是这种分布与多 epoch 重复训练的交互:

  1. novel / 低频 context:模型在 train 中见过几次(低频)或没见过(novel), 到 epoch3 配合 gate 通道建立,模型把这些 context 的特定后续 token memorize 下来(train loss 坍塌), 但 val 中即使遇到相同 context,后续 token 也不同,模型"过度自信"反而出错(val loss 翘起)。trigram 的 ~30% val novel 是其 gap 尤为显著的结构性原因。
  2. 高频 context:多为 QA pattern(? Answer: 等),后续 token 天然可预测, train 和 val 的 loss 都相对低且接近,对 gap 贡献最小。
  3. 启示:用 BPE 把高频 token 组合 over-encode 成单个 token、避免对低频 context 建 hash 表, 可以从源头减少低频过拟合——这是后续干预实验的方向(见 agent-experiment-log.html)。
关于 unigram:本 run(baseline_current)的 n-gram value tables 只启用 bigram 与 trigram (ENABLE_UNIGRAM_VE=0),unigram 不参与训练,故无 unigram 频次分解。 如需 unigram 视角,需要一次 ENABLE_UNIGRAM_VE=1 且把 unigram 纳入频次分解的新 run; 服务器环境已就绪(见 实验日志),可在需要时补跑。

10. 扩展实验:干预验证(gap 是否能被消除)

§9 的频次分解定位了 gap 的来源(低中频 n-gram context 过拟合)。本章用两个干预实验直接验证: 若假设成立,干预应能消除/减弱 gap。

实验设置

两个干预都在 20260725e_nano_rmsprop_fixv3 同款设置上跑(table RMSProp, nanogpt_original shell, vocab=8192, n_layer=8, seed42, 1000 step, 单 shard tr1), 并各加一个 current-shell 版本(同 rmsprop,对照 current-shell baseline baseline_current,即 §2/§7.9/§9 用的同款 setting)。 共 4 个干预 run,各自对照同 shell 的 baseline。所有 run 都到 step 1000、含 3 个 epoch(边界 step 337/686)。
:§9 的频次分解现已统一到 baseline_current(current shell,gap 量级更大、现象更典型);本章干预在两个 shell 上都验证,nanogpt_original 用 B.1,current-shell 用 baseline_current 同款配置。

10.1 实验 4:per-epoch hash 错位

假设

n-gram table 的 gap 根因是"同一 context 跨 epoch 重复查到同一行,table 在该行累积 train-specific pattern"。 若每个 epoch 用 epoch 号做 seed 重生成全部 hash primes,使同一 context 跨 epoch 落到完全不同的 row, 则 table 无法跨 epoch 累积 → 应该没有 gap

实现:新增 env NGRAM_HASH_RESEED_PER_EPOCH=1;epoch 1 保留 canonical primes(与 baseline 一致), epoch≥2 边界调用 _reseed_ngram_hash_primes(model, epoch_seed) 用 epoch 号重生成 bigram/trigram/fourgram 全部 primes。 reseed 只改 hash 映射、不改 table 行内容——正好实现"新 epoch 同一 context 查到另一行(未被该 context 训练污染)"。

10.2 实验 5:低频 context 强制 gate=0

假设

§9 显示低中频 n-gram context 是 gap 的主要 driver。若用离线 train 频次表(ngram_global_frequency_v2, 一个"与训练完全解耦的独立统计者")判定低频区间(hit count 1-200),并在这些 context 上把 n-gram 注入 gate 强制置零, 则低频 context 不再注入 n-gram → gap 应减弱/消除

实现:新增 env NGRAM_LOWFREQ_GATE_ZERO=1 + NGRAM_GLOBAL_FREQUENCY_CUSTOM_RANGES=bigram:1-200;trigram:1-200; 在模型主 forward 里用 GlobalNgramFrequencyIndex.mask_for_ranges 算出低频位置的 [B,T] 布尔 mask, 在 _add_value_residual 里把该位置的 value_gate 置零(gate=0 → n-gram value 不注入)。 mask 从 epoch 1 起全程生效(不是 step 338 后),目的是"杜绝"低频 context 的 n-gram 注入。 与已有 readout-mask(value 置零)的区别:本实验置零 gate(直接关闭注入通道,且 gate 仍收梯度但被强制为 0)。

10.3 结果:复合对比图

下面这张复合图把 4 个干预 run 与各自 baseline 的 gap/train/val 曲线放进同一面板,可切 shell 与曲线类型; 下方表格给出每 epoch 最大 gap 与 final gap 的数值对比。

图 10.1:干预验证复合图(也可直接打开 interactive/fig_exp45_results.html)。 红虚线=baseline,青实线=exp4(hash reseed),绿实线=exp5(low-freq gate=0)。切 shell 看 nanogpt_original / current-shell。
📊 数据来源:exp4/exp5 runs(ophis-gpu, 2026-07-30)vs B.1 baseline (nanogpt_original) / allgram-mask-baseline (current-shell)

10.4 结论

runepoch2 max gapepoch3 max gapfinal gapvs baseline
B.1 baseline (nanogpt_original)0.3830.6880.688
exp4 hash-reseed (nanogpt_original)0.2570.3710.047−93%
exp5 low-freq gate=0 (nanogpt_original)0.1660.1020.040−94%
baseline (current-shell)1.3003.9373.658
exp4 hash-reseed (current-shell)0.3070.3510.252−93%
exp5 low-freq gate=0 (current-shell)0.5431.5231.523−58%
实验 4(per-epoch hash 错位)— 假设验证成立,两个 shell 都基本消除 gap
  1. nanogpt_original:epoch3 max gap 从 0.688 → 0.371,final gap 从 0.688 → 0.047(−93%)。 epoch3 的 gap 爆发被完全压住——gap 不再随 epoch 增长。
  2. current-shell:epoch3 max gap 从 3.937 → 0.351(−91%),final gap 从 3.658 → 0.252(−93%)。 即使在 gap 大 5 倍的 current-shell 上,reseed 仍几乎完全消除 gap。
  3. 机理确认:gap 确实来自"同一 context 跨 epoch 重复查到同一行、table 在该行累积 train-specific pattern"。 一旦打断这个跨 epoch 的 row 对应关系,table 无法累积 → gap 消失。这是对 §7.9/§9 因果链的直接因果干预验证。
实验 5(低频 gate=0)— 假设验证成立(nanogpt_original),部分成立(current-shell)
  1. nanogpt_original:epoch3 max gap 从 0.688 → 0.102,final gap → 0.040(−94%)。 与 exp4 几乎一样有效——说明在 nanogpt_original 上,低频 context 确实是 gap 的几乎全部来源。
  2. current-shell:epoch3 max gap 从 3.937 → 1.523(−61%),final gap → 1.523。 显著减弱但未消除——current-shell 的注入通道更强(split QKV + 残差路径),中高频 context 仍能过拟合, 仅挡低频不够。这与 §9"current-shell gap 量级大得多"一致。
  3. 启示:用 BPE 把高频 token 组合 over-encode、避免对低频 context 建 hash 表(等价于永久 gate=0), 是从源头消除低频过拟合的方向。exp5 用 gate=0 在 nanogpt_original 上已验证可行;current-shell 需配合更强的频率截断。
两点说明
  • exp4 的 reseed 只在 epoch≥2 边界触发(epoch1 保留 canonical primes 以与 baseline 对齐); reseed 不改 table 行内容,只改 hash→row 映射,所以是干净的因果干预(不改变模型容量、不冻结参数)。
  • exp5 的频次表来自训练前一次离线扫描(ngram_global_frequency_v2),与训练过程完全解耦—— 是"独立统计者"判定低频,不存在在线漂移。gate=0 是逐位置 mask(shape [B,T]),只对 bigram/trigram 分支生效,unigram 不受影响。

11. 精确重复次数 vs gap(exp6 细粒度分桶)

§9 的频次分桶用的是粗粒度区间(1, 2, 3, 4, 5, 6-10, 11-20, …)。本节在 同一 current-shell 设置(baseline_current 同款:current shell + bigram+trigram RMSProp, seed42,但延长到 1700 step / 5 epoch,让 gap 充分展开)下,把分桶细化到 精确到每个重复次数(0=novel, 1, 2, 3, …, 9, 10-14, 15-20, 21-30, 31-50, …, 5k+), 并在 epoch 2/3/4/5 各取一个横截面,观察 gap 如何随重复次数和训练进度演化。

频次定义与 §9 一致:每个 context 在单 train epoch 内的离线精确命中次数 (GlobalNgramFrequencyIndex,train-only 定义)。val token 按"其 context 在 train 中的频次"归类到同一套桶。 每个 epoch 的横截面取该 epoch 最后一个 probe step(epoch1=step337, epoch2=650, epoch3=1025, epoch4=1375, epoch5=1700)。

图 11.1:精确重复次数 vs train/val/gap 损失(也可直接打开 interactive/fig_exp6_freq_gap.html)。 切 epoch(2/3/4/5)· 切曲线(train / val / train+val / gap)· 切指标(per-token / 总贡献=frac×loss)· 切 context(bigram / trigram)。

11.1 关键发现

gap 随重复次数的变化不是单调的——低频最大,但高频也有显著 gap(尤其多 epoch 后)
  1. epoch 2:gap 已经在低频桶(1-2 次重复)出现(bigram bucket "1" gap≈1.12,bucket "2" gap≈1.39), 但高频桶(5k+)gap≈0.02(几乎为 0)。此时 gap 几乎完全由低频驱动。
  2. epoch 3:低频桶 gap 急剧扩大(bigram bucket "1" gap≈3.57),中频(51-100)gap≈1.79, 高频(5k+)gap≈0.94——gap 开始向高频蔓延。
  3. epoch 4-5:所有频段都有大 gap(bigram e5: bucket "1" gap≈6.70,5k+ gap≈5.37)。 到 epoch 5,高频桶的 gap 也接近低频水平——说明多 epoch 重复训练后, table 在所有频段都累积了 train-specific pattern,不再只是低频问题。
  4. trigram novel bucket(val 中占 30%)始终是大 gap 来源: e5 trigram novel val loss=6.72(无 train 对照,因为 train 中按定义无 novel context), 总贡献 = 0.30 × 6.72 ≈ 2.0,是 trigram val 翘起的最大单一来源。
与 §9 粗粒度分桶的对比

§9 的 1000-step run 到 epoch 3 末 final gap≈3.66;本 exp6 延长到 1700 step / 5 epoch, final gap(step 1700, train=1.38 / val=5.83)≈4.45——gap 仍在扩大。 精确分桶揭示了 "低频优先过拟合 → 多 epoch 后高频也沦陷" 的动态过程: epoch 2 时 gap 集中在 1-5 次重复的低频桶;到 epoch 5,1 次和 5k+ 次的 gap 差距缩小到 1.3(6.70 vs 5.37)。 这解释了为什么 exp5(仅挡低频 1-200)在 current-shell 上只能减 58%——多 epoch 后中高频也需要干预。

11.2 数值表(bigram,per-token loss)

buckete2 traine2 vale2 gape3 gape4 gape5 gap
noveltrain 无(按定义)
13.6594.7791.1213.5675.9716.700
23.5064.8941.3873.7445.9186.552
53.7734.3790.6072.8714.7395.455
10-143.5914.2870.6962.4684.6275.140
51-1003.9134.3620.4491.7943.8944.884
5k+5.0605.0780.0190.9363.3635.370

颜色:绿=gap<0.1(基本无 gap), =gap 1-4(中等), =gap>4(严重过拟合)。 novel 桶无 train 对照(train 中按定义无 novel context),故 gap 列为空。

实验设置
  • run:exp6_freqdecomp_current(服务器 /data3/guoshaoyang/ngram-gap-exp/runs/,2026-07-31,GPU 6)
  • config:与 baseline_current 同款 current-shell(ARCH_VARIANT=nanogpt_current_shell, table RMSProp, seed42, vocab=8192, n_layer=8),延长到 MAX_TRAINING_STEPS=1700(5 epoch)
  • 频次分桶:NGRAM_GLOBAL_FREQUENCY_BUCKET_EDGES=0,1,2,3,4,5,6,7,8,9,10,15,21,31,51,101,201,501,1001,5001(精确到 1-9 次重复,10-14 合并)
  • epoch 边界:[338, 687, 1035, 1384](每 epoch ~349 step)
  • final loss:train=1.382 / val=5.829(gap=4.447,仍在上升)