feat(ml-alpha): wire TFT GRN heads into PerceptionTrainer (Phase 1.7)
Replaces the single-linear heads (5 × 128) with per-horizon TFT GRN:
trunk h[B, 128]
→ W1 (HIDDEN→HEAD_MID) + GELU = eta_2 [B, 5, HEAD_MID]
→ W2 (HEAD_MID→HEAD_MID) = eta_1 [B, 5, HEAD_MID]
→ (W_gate, W_main) split = (gate_lin, main) [B, 5]
→ W_skip (HIDDEN→1) = skip [B, 5]
→ skip + sigma(gate_lin) * main = logit [B, 5]
→ sigma(logit) = p [B, 5]
10 trainable param tensors per horizon (5 weight + 5 bias), 10 AdamW
state objects (biases get wd=0), 6 per-K-position intermediate buffers
sized [K, B, 5, HEAD_MID] (z1, a1, z2) and [K, B, 5] (gate_logit, main,
logit). All saved during fwd, consumed by GRN bwd for the chain rule.
K-loop forward replaces `multi_horizon_heads_batched` with the new
`multi_horizon_heads_grn_fwd_batched` kernel. K-loop backward replaces
`multi_horizon_heads_backward_batched` with `multi_horizon_heads_grn_bwd_batched`.
ISV `lambda_d` is consumed by the GRN bwd kernel to scale ONLY the
trunk gradient (per pearl_adam_normalizes_loss_weights.md — the
effective lever is the trunk gradient, not loss weight).
Eval path also uses the GRN fwd kernel so eval sees the same
distribution downstream layers trained on (same pattern as Phase 1.6
LN wiring).
Xavier-uniform init: scale = sqrt(1 / fan_in) per tensor:
W1, W_skip: scale = sqrt(1/HIDDEN) ≈ 0.088
W2, W_gate, W_main: scale = sqrt(1/HEAD_MID) ≈ 0.125
Parameter count per horizon: W1 (8192) + b1 (64) + W2 (4096) + b2 (64)
+ W_gate (64) + b_gate (1) + W_main (64) + b_main (1) + W_skip (128)
+ b_skip (1) = 12,675 params × 5 horizons = 63,375 head params total
(vs single-linear's 5*(128+1)=645). Trunk grad dimensionality is
unchanged.
Synthetic overfit smoke: loss 0.32 → 0.0000 by step 50 (faster than
single-linear's 0.37 → 0.0006 in 250 steps); all 7 perception_overfit
tests PASS. Confirms the full Mamba2 + LN + CfC + GRN backward chain
is wired correctly and all 17 AdamW optimisers move weights.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>