Skip to content

Commit c3ad1c6

Browse files
docs: Add detailed TODO for remaining benchmark architecture fixes
Covers TTT (5 paper-specific init fixes), FNet (verification needed), KAN (skip), H3 (LR warmup), and Hopfield (dropout). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6da924b commit c3ad1c6

1 file changed

Lines changed: 324 additions & 0 deletions

File tree

docs/planning/BENCHMARK_FIXES.md

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
# Benchmark Architecture Fixes TODO
2+
3+
**Created:** 2026-02-19
4+
**Context:** 34-architecture GPU benchmark on RTX 4090, 30 Zelda replays, 3 epochs
5+
**Results:** 30/34 converged. 4 remaining: FNet (complex grad), TTT (NaN), KAN (OOM), H3 (underfitting)
6+
7+
---
8+
9+
## 1. TTT — NaN (Edifice-level init fix required) [P1]
10+
11+
**Status:** Still NaN at LR=5e-7 / clip=0.1. Needs Edifice source fix, not just hyperparams.
12+
**File:** `edifice/lib/edifice/recurrent/ttt.ex`
13+
**Paper:** "Learning to (Learn at Test Time)" (Sun et al., 2024) — https://arxiv.org/abs/2407.04620
14+
15+
The current implementation is missing 5 critical stability mechanisms from the paper. Each fix is independent and can be applied incrementally.
16+
17+
### Fix 1: W_0 initialization — N(0, 0.02) not glorot_uniform
18+
19+
**Location:** `ttt.ex:223-224`
20+
```elixir
21+
# CURRENT (wrong):
22+
w0_param = Axon.param("#{name}_w0", {inner_size, inner_size}, initializer: :glorot_uniform)
23+
24+
# FIX: Use N(0, 0.02) per Section 4.1 of the paper
25+
w0_param = Axon.param("#{name}_w0", {inner_size, inner_size},
26+
initializer: fn shape, type, _key ->
27+
Nx.Random.normal(Nx.Random.key(42), shape: shape, type: type)
28+
|> elem(0)
29+
|> Nx.multiply(0.02)
30+
end)
31+
```
32+
33+
**Why:** Glorot for a 64x64 matrix gives stddev ~0.18, which is 9x larger than the paper's 0.02. The inner model's self-supervised gradient update `W -= eta * error * k^T` amplifies any large initial weight through the prediction error. Small init keeps early predictions near zero, preventing gradient explosion in the first few steps.
34+
35+
### Fix 2: Eta scaling by 1/head_dim (CRITICAL)
36+
37+
**Location:** `ttt.ex:265`
38+
```elixir
39+
# CURRENT (wrong):
40+
eta = Nx.sigmoid(eta_pre)
41+
42+
# FIX: Scale by 1/inner_size (= 1/head_dim in paper's multi-head formulation)
43+
eta = Nx.divide(Nx.sigmoid(eta_pre), inner_size)
44+
```
45+
46+
**Why:** This is the single most likely cause of NaN. Without the 1/d scaling, eta values after sigmoid are in [0, 1], but the paper uses eta in [0, 1/d] where d=64. This means our learning rate is 64x too large for the inner model update. The gradient step `W -= eta * error * k^T` produces rank-1 updates of magnitude `eta * ||error|| * ||k||`. With eta=0.5 instead of 0.5/64=0.0078, the weight matrix changes dramatically each step, causing prediction errors to explode.
47+
48+
### Fix 3: Reconstruction target — V-K (residual) not raw V
49+
50+
**Location:** `ttt.ex:309-310` (linear variant) and `ttt.ex:289` (MLP variant)
51+
```elixir
52+
# CURRENT (wrong):
53+
error = Nx.subtract(pred, v_t)
54+
55+
# FIX: Use residual reconstruction target
56+
error = Nx.subtract(pred, Nx.subtract(v_t, k_t))
57+
```
58+
59+
**Why:** The paper trains the inner model to predict the *residual* between V and K, not V directly. This centers the reconstruction target around zero (since K and V are both projections of the same input), making the loss landscape much smoother. Without this, the inner model must learn the full magnitude of V, which means W_0 must already be a good predictor — at odds with near-zero initialization.
60+
61+
### Fix 4: LayerNorm on inner model output before loss
62+
63+
**Location:** `ttt.ex:306-310` — add LayerNorm before computing error
64+
65+
This requires pre-building a learnable LayerNorm (gamma/beta params) and applying it inside the scan. Since Axon.layer_norm can't be used inside a raw Nx computation, implement manually:
66+
67+
```elixir
68+
# Add as Axon.param in build_ttt_layer (alongside w0_param):
69+
ln_gamma = Axon.param("#{name}_inner_ln_gamma", {inner_size},
70+
initializer: :ones)
71+
ln_beta = Axon.param("#{name}_inner_ln_beta", {inner_size},
72+
initializer: :zeros)
73+
74+
# In the scan, after computing pred:
75+
pred_normed = manual_layer_norm(pred, ln_gamma, ln_beta)
76+
error = Nx.subtract(pred_normed, Nx.subtract(v_t, k_t))
77+
78+
# Helper function:
79+
defp manual_layer_norm(x, gamma, beta) do
80+
mean = Nx.mean(x, axes: [-1], keep_axes: true)
81+
var = Nx.variance(x, axes: [-1], keep_axes: true)
82+
Nx.add(Nx.multiply(Nx.divide(Nx.subtract(x, mean), Nx.sqrt(Nx.add(var, 1.0e-6))), gamma), beta)
83+
end
84+
```
85+
86+
**Why:** LayerNorm on the inner model's output (before computing loss gradient) prevents the prediction magnitudes from drifting as W changes. Without it, the gradient magnitude is proportional to `||pred||` which grows unboundedly as W accumulates updates.
87+
88+
### Fix 5: Learnable ttt_base_lr (optional, lower priority)
89+
90+
**Location:** `ttt.ex:213` (eta_proj)
91+
92+
Currently eta comes from `sigmoid(W_eta @ x)`. The paper uses `sigmoid(W_eta @ x) * ttt_base_lr / head_dim` where `ttt_base_lr` is a learnable scalar initialized to 1.0.
93+
94+
```elixir
95+
# Add as Axon.param:
96+
base_lr = Axon.param("#{name}_base_lr", {1},
97+
initializer: :ones)
98+
99+
# In scan, after computing eta:
100+
eta = Nx.divide(Nx.multiply(Nx.sigmoid(eta_pre), Nx.abs(base_lr)), inner_size)
101+
```
102+
103+
**Why:** The learnable base_lr lets the model discover its own optimal inner learning rate during training. Less critical than fixes 1-4 but improves convergence.
104+
105+
### Implementation order
106+
107+
1. **Fix 2 (eta scaling)** — most likely NaN cause, 1-line change
108+
2. **Fix 1 (W_0 init)** — 3-line change, independent
109+
3. **Fix 3 (V-K target)** — 1-line change per variant, independent
110+
4. **Fix 4 (LayerNorm)** — most complex, requires new params in Axon.layer
111+
5. **Fix 5 (base_lr)** — optional, try after 1-4
112+
113+
### Testing
114+
115+
After applying fixes in Edifice:
116+
```bash
117+
cd edifice && mix test test/edifice/recurrent/ttt_test.exs
118+
```
119+
120+
Then in ExPhil:
121+
```bash
122+
mix deps.update edifice && mix compile --force
123+
./scripts/benchmark_isolated.sh --replays /workspace/replays/greg/zelda --only ttt --epochs 3 --cache-embeddings
124+
```
125+
126+
Expected: should converge at LR=5e-7 with no NaN. If stable, try relaxing to LR=1e-5.
127+
128+
---
129+
130+
## 2. FNet — Complex gradient crash (Edifice fix applied, needs verification) [P1]
131+
132+
**Status:** Fix pushed to Edifice (commit fc5c852) but never successfully run on GPU pod.
133+
**File:** `edifice/lib/edifice/attention/fnet.ex` (already fixed)
134+
135+
### What was done
136+
137+
Replaced `Nx.fft` with real-valued DFT matrix multiply:
138+
- Precompute cosine DFT matrices at build time: `DFT[k,n] = cos(2*pi*k*n/N)`
139+
- Use `Nx.dot(x, dft_matrix)` instead of `Nx.fft(x) |> Nx.real()`
140+
- All operations stay in real f32 space, so gradients never touch complex numbers
141+
- DFT matrices passed as `Axon.constant` nodes
142+
143+
### To verify on next pod run
144+
145+
```bash
146+
cd /app
147+
git pull origin main
148+
mix deps.update edifice && mix compile --force
149+
./scripts/benchmark_isolated.sh --replays /workspace/replays/greg/zelda --only fnet --epochs 3 --cache-embeddings
150+
```
151+
152+
**Expected:** Should train normally. Quality should be mid-range (3.0-3.2 val loss) since FFT mixing is parameter-free and less expressive than learned attention.
153+
154+
### If still failing
155+
156+
The error would be the same `Nx.less/2 does not support complex inputs` — this would mean the dep didn't update properly. Debug:
157+
```elixir
158+
# In iex, verify the fix is loaded:
159+
Edifice.Attention.FNet.dft_real_matrix(4) |> Nx.shape()
160+
# Should return {4, 4}
161+
```
162+
163+
---
164+
165+
## 3. KAN — OOM (permanent, skip for benchmark) [P3]
166+
167+
**Status:** OOM at batch_size=4, hidden=64, grid=4 on RTX 4090 (24GB). Fundamental memory issue.
168+
**File:** `edifice/lib/edifice/feedforward/kan.ex`
169+
170+
### Root cause
171+
172+
KAN's B-spline basis expansion creates `hidden * grid_size` intermediate tensors per layer in the backward pass. For a 2-layer network with hidden=64, grid=4:
173+
- Forward: each layer expands 64 inputs to 64*4=256 basis functions
174+
- Backward: EXLA materializes gradient for each basis coefficient
175+
- The gradient graph for even this tiny config exceeds 24GB
176+
177+
### Options (in order of preference)
178+
179+
**Option A: Skip permanently in benchmark** (recommended)
180+
KAN is designed for function approximation, not sequence modeling. Its O(hidden * grid * layers) memory scaling in backward pass makes it impractical for GPU training at any reasonable size.
181+
182+
**Option B: CPU-only training (very slow)**
183+
```elixir
184+
# In benchmark config:
185+
{:kan, "KAN (Kolmogorov-Arnold)", [
186+
backend: :binary, # Force CPU
187+
batch_size: 1,
188+
hidden_sizes: [32, 32],
189+
grid_size: 3,
190+
...
191+
]}
192+
```
193+
Would take hours per epoch. Not worth it for a architecture that's fundamentally mismatched.
194+
195+
**Option C: Gradient checkpointing (Edifice change)**
196+
Implement activation checkpointing in KAN's `basis_expansion` to trade compute for memory. This is a significant engineering effort for marginal benefit.
197+
198+
### Recommendation
199+
200+
Mark KAN as "skipped (OOM)" in benchmark results. It's a fascinating architecture for interpretable function approximation but not viable for sequence modeling at useful scales on consumer GPUs.
201+
202+
---
203+
204+
## 4. H3 — Underfitting at LR=5e-7 (needs LR warmup) [P2]
205+
206+
**Status:** Converges! val=3.6215 after 3 epochs. But severely underfitting (train loss 3.84 > val loss, loss still dropping at epoch 3). LR=5e-7 is too conservative.
207+
**File:** `exphil/scripts/benchmark_architectures.exs:396-409`
208+
209+
### Root cause
210+
211+
H3 uses learnable `a_log` and `dt_log` parameters with exponential parameterization (`exp(a_log)`). The gradient of `exp(x)` is `exp(x)` — a positive feedback loop. At LR=1e-5, this explodes to NaN. At LR=5e-7, it's stable but converges glacially.
212+
213+
### Fix: LR warmup schedule
214+
215+
The ideal approach is linear warmup from 5e-7 to 1e-5 over epoch 1, then constant:
216+
217+
**Option A: In ExPhil benchmark script** (easiest)
218+
219+
Add a `warmup_steps` parameter to the benchmark config:
220+
```elixir
221+
{:h3, "H3 (Hungry Hippos)", [
222+
temporal: true,
223+
backbone: :h3,
224+
window_size: 30,
225+
num_layers: 2,
226+
hidden_sizes: [256, 256],
227+
batch_size: 64,
228+
dropout: 0.1,
229+
learning_rate: 1.0e-5, # Target LR (back to normal)
230+
max_grad_norm: 0.1, # Keep tight clipping
231+
warmup_epochs: 1, # Warmup over first epoch
232+
warmup_start_lr: 5.0e-7 # Start from conservative LR
233+
]}
234+
```
235+
236+
This requires the benchmark training loop to support LR warmup. Check if `ExPhil.Training.Imitation.Optimizer` already supports warmup (it likely does via `warmup_steps` or similar).
237+
238+
**Option B: Just run more epochs** (simplest)
239+
240+
The loss was still dropping at epoch 3. Running 10 epochs at LR=5e-7 might get H3 to a competitive val loss (~3.0-3.1). Less optimal but zero code changes:
241+
```bash
242+
./scripts/benchmark_isolated.sh --replays /workspace/replays/greg/zelda --only h3 --epochs 10 --cache-embeddings
243+
```
244+
245+
**Option C: Intermediate LR** (compromise)
246+
247+
Try LR=1e-6 (5x current, 10x below NaN threshold):
248+
```elixir
249+
learning_rate: 1.0e-6, # Middle ground between 5e-7 (stable, slow) and 1e-5 (NaN)
250+
```
251+
252+
### Recommendation
253+
254+
Try Option C first (1-line change, quick to verify), then Option B (10 epochs) if stable.
255+
256+
---
257+
258+
## 5. Hopfield — Overfitting (needs dropout) [P2]
259+
260+
**Status:** Converges at val=3.1276, but best was epoch 2 (val=2.9532) before overfitting in epoch 3. Gap of 0.18 between epochs 2-3 is significant.
261+
**File:** `exphil/scripts/benchmark_architectures.exs:528-541`
262+
263+
### Root cause
264+
265+
Hopfield's pattern matrix creates a very expressive model that memorizes training data quickly. At batch_size=4 and hidden=128, there are few regularization constraints.
266+
267+
### Fix: Add dropout
268+
269+
```elixir
270+
{:hopfield, "Hopfield (Associative Memory)", [
271+
temporal: true,
272+
backbone: :hopfield,
273+
window_size: 30,
274+
num_layers: 2,
275+
num_heads: 2,
276+
hidden_sizes: [128, 128],
277+
hidden_size: 128,
278+
batch_size: 4,
279+
dropout: 0.2, # ADD: regularization to prevent overfitting
280+
max_grad_norm: 1.0
281+
]}
282+
```
283+
284+
Also consider:
285+
- `learning_rate: 5.0e-4` (slightly lower than default 1e-3)
286+
- Running 5 epochs with early stopping (best-epoch tracking already exists)
287+
288+
### Expected outcome
289+
290+
With dropout=0.2, the epoch 2→3 overfitting gap should narrow. True val loss likely around 2.95-3.05.
291+
292+
---
293+
294+
## 6. Longer runs for top-5 [P3]
295+
296+
Once the above fixes are applied, the next benchmark phase is longer training:
297+
298+
```bash
299+
./scripts/benchmark_isolated.sh \
300+
--replays /workspace/replays/greg/zelda \
301+
--only s4,xlstm,zamba,s4d,gru \
302+
--epochs 10 \
303+
--cache-embeddings
304+
```
305+
306+
These 5 architectures are within 0.12 of each other (2.82-2.94). 10 epochs will reveal true convergence behavior — some may still be improving, others may plateau.
307+
308+
---
309+
310+
## Summary Table
311+
312+
| Architecture | Issue | Fix Location | Complexity | Priority |
313+
|---|---|---|---|---|
314+
| TTT | NaN | Edifice `ttt.ex` | High (5 changes) | P1 |
315+
| FNet | Complex grad | Already fixed | Verify only | P1 |
316+
| KAN | OOM | N/A | Skip | P3 |
317+
| H3 | Underfitting | ExPhil benchmark config | Low (1 line) | P2 |
318+
| Hopfield | Overfitting | ExPhil benchmark config | Low (1 line) | P2 |
319+
320+
**Quick wins (next pod session):**
321+
1. Verify FNet (just `mix deps.update edifice`)
322+
2. Try H3 at LR=1e-6
323+
3. Add dropout=0.2 to Hopfield
324+
4. Start TTT Edifice fixes (eta scaling first)

0 commit comments

Comments
 (0)