Skip to content

Commit 6b362a8

Browse files
unamedkrclaude
andcommitted
debug(moe): TQ_MOE_PROBE — L4 router collapse signature at long positions
Adds per-layer top-K expert ID + routing weight dump env, gated on layer-0 MoE call counter. Measurement on Qwen3.6-35B IQ4_XS drift-trigger prompt at calls 50/100/115/117: call=50: all 40 MoE layers balanced (top1 ≈ 0.15-0.30) call=100: L4 top1=0.812 (expert 67, extreme collapse) call=115: L4 top1=0.804 (same expert 67, persistent) other 39 layers: mean top1=0.221 (healthy) call=117: max top1=0.450; back to normal L4 at long positions narrows to ~80% weight on expert 67 while all other MoE layers stay balanced. Not a uniform multi-layer collapse. Refines R24 hypothesis: drift is a single-hot-layer bottleneck (L4) interacting with DeltaNet's persistent semantic state — explains why 4B (no MoE) is immune. Next: A/B suppress L4's router collapse (temperature on softmax) to test if L4 is causal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 88ed094 commit 6b362a8

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

.claude/state.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,39 @@
33
**Last updated**: 2026-04-21 (Phase 1 refparity ★)
44
**Session HEAD**: Reference-parity framework (tools/refparity/) LANDED — HF vs engine per-layer diff, pos-aligned, post_norm-aware.
55

6+
## Phase 1 R25 — MoE router instrumentation: L4 is outlier, others balanced (2026-04-22)
7+
8+
Added `TQ_MOE_PROBE=call1,call2,...` env in `tq_moe_forward` — dumps
9+
per-layer top-K expert IDs and softmax weights at listed layer-0 MoE
10+
call counts.
11+
12+
Measurement on Qwen3.6-35B IQ4_XS at calls 50, 100, 115, 117 (around
13+
the 117-token drift boundary) on "Once upon a time in a faraway land":
14+
15+
- Call 50 (early): all 40 MoE layers balanced, top-1 weight ≈ 0.15-0.30
16+
- Call 100 (mid): **L4 top-1 = 0.812** (expert 67 dominates); others OK
17+
- Call 115 (drift edge): **L4 top-1 = 0.804** (same 80%+ collapse); 39/40
18+
layers normal (top-1 mean 0.221)
19+
- Call 117 (drift start): all layers back to normal; max top-1 = 0.450
20+
21+
L4 shows a persistent near-collapse at ~0.80 weight on one expert at
22+
long positions, but 39/40 other layers stay healthy. The R24 "MoE×DeltaNet
23+
positive feedback" hypothesis isn't strictly supported by a uniform
24+
collapse pattern — only L4 deviates.
25+
26+
**Revised hypothesis**: the drift isn't a simultaneous multi-layer MoE
27+
collapse. Instead, a single hot layer (L4) narrows to one expert family
28+
at long positions, that expert's constant large contribution feeds back
29+
into DeltaNet's state, and the joint signal pulls downstream semantics
30+
into repetition. Still requires a DeltaNet state to hold the semantic —
31+
explains why 4B (dense FFN, no MoE) doesn't drift at all.
32+
33+
Concrete next step: A/B force-suppress L4 at long positions (e.g., mix in
34+
more experts via temperature on L4's router softmax). If that moves the
35+
cliff, L4 is the bottleneck.
36+
37+
`TQ_MOE_PROBE` joins the permanent diagnostic suite.
38+
639
## ★★★ Phase 1 R24 — Drift is MoE×DeltaNet interaction, NOT DeltaNet alone (2026-04-21) ★★★
740

841
Ran Qwen3.5-4B Q4_K_M (dense FFN + DeltaNet hybrid, **no MoE**) on the

src/engine/tq_moe.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,37 @@ void tq_moe_forward(const tq_moe_layer_t* layer,
661661
}
662662
state->routing_precomputed = 0; /* reset for next call */
663663

664+
/* Probe: TQ_MOE_PROBE=pos1,pos2,... prints per-layer top-K expert IDs
665+
* and routing weights. Use the layer-0 call counter to gate since this
666+
* is called once per MoE layer per token. */
667+
{
668+
static __thread int _moe_call_count = 0;
669+
if (layer_idx == 0) _moe_call_count++;
670+
const char* _probe = getenv("TQ_MOE_PROBE");
671+
if (_probe) {
672+
int match = 0;
673+
const char* p = _probe;
674+
while (*p) {
675+
int v = atoi(p);
676+
if (v == _moe_call_count) { match = 1; break; }
677+
while (*p && *p != ',') p++;
678+
if (*p == ',') p++;
679+
}
680+
if (match) {
681+
fprintf(stderr, "[moe-probe] call=%d L%d experts=[",
682+
_moe_call_count, layer_idx);
683+
for (int k = 0; k < num_active; k++)
684+
fprintf(stderr, "%d%s", state->top_experts[k],
685+
k+1<num_active?",":"");
686+
fprintf(stderr, "] weights=[");
687+
for (int k = 0; k < num_active; k++)
688+
fprintf(stderr, "%.3f%s", state->expert_weights[k],
689+
k+1<num_active?",":"");
690+
fprintf(stderr, "]\n");
691+
}
692+
}
693+
}
694+
664695
/* Step 2: Zero the output accumulator */
665696
memset(output, 0, (size_t)hidden_dim * sizeof(float));
666697

0 commit comments

Comments
 (0)