Lightning Training — empirical
Trainer note: pre-fix, needs reassessment.
Most quantitative claims in this file were produced before the Wk / Wv /
bias gradient fix in commit 1c858c0 (2026-04-23). The experiment ideas and
failure modes remain useful, but the reported PPL rankings should be treated as
provisional until rechecked on the corrected trainer.
Design doc: notes/trainer/lightning-training.md.
Hypothesis
Stochastic variable-depth subtree sampling (L3 mass-weighted walk) can match
or beat the deterministic per-root-child sweep at matched total optimizer
steps. L3's stop-probability p_stop controls the expected sample depth:
small p_stop → deep/narrow samples, large p_stop → shallow/broad samples.
Reference baselines
| config | PPL mean | source |
|---|---|---|
| d=8, 3 SE, 65 steps/SE, per-root-child, lr=3e-3 RMSProp | 17.99 | rnd/radix-saturation/logs |
| d=16, 3 SE, 65 steps/SE, per-root-child, lr=3e-3 RMSProp | 14.59 | rnd/radix-saturation/logs |
| d=32, 3 SE, 65 steps/SE, per-subtree-file, lr=3e-3 RMSProp | 13.40 | rnd/radix-saturation/logs |
d=16 global-radix won't fit KV cache (9.5GB). Initial sweeps live at d=8 where one full run takes ~5 sec. d=16 work will require adopting the per-subtree file format (deferred — each subtree view is self-contained so L3 within a subtree still works).
Experiments
| script | what |
|---|---|
run_d8_pstop_sweep.sh |
L3 p_stop ∈ {0.2, 0.3, 0.5}, 65 steps × 3 SE matched to baseline |
Result: matched-budget p_stop sweep (commit 20e8121)
| config | mean PPL | min | max | spread |
|---|---|---|---|---|
| L3 p_stop=0.2, 65×3 | 20.46 | 19.26 | 22.03 | 2.76 |
| L3 p_stop=0.3, 65×3 | 20.12 | 18.40 | 22.87 | 4.47 |
| L3 p_stop=0.5, 65×3 | 20.09 | 19.41 | 20.96 | 1.55 |
| baseline det. 65×3 | 17.99 | — | — | — |
All Lightning configs lose to the deterministic baseline by ~2.1 PPL at matched step count. Consistent with the design doc's hypothesis: stochastic sampling at matched budget without LR retuning underperforms.
Observations:
- Variance anti-correlates with p_stop. Smaller p_stop = deeper, lower-mass samples → flakier stochastic corpus coverage.
- All runs train (no divergence). Infrastructure verified.
- Best single run (p_stop=0.3, seed=43): 18.40 PPL — within 0.4 of baseline. Suggests LR tuning or a higher step budget could close the gap.
Result: extended step-budget + L1/L2 comparators (same commit)
| config | mean PPL | min | max | spread |
|---|---|---|---|---|
| baseline det. 65×3 | 17.99 | — | — | — |
| L2 uniform-rc s65×3 | 19.54 | 18.59 | 21.12 | 2.53 |
| L3 p_stop=0.5 s65×3 | 20.09 | 19.41 | 20.96 | 1.55 |
| L3 p_stop=0.5 s130×3 | 25.83 | 21.01 | 31.17 | 10.16 |
| L3 p_stop=0.5 s260×3 | 26.13 | 24.71 | 28.45 | 3.74 |
| L1 uniform-all s65×3 | 28.55 | 28.37 | 28.90 | 0.53 |
Key findings:
- L2 is the closest Lightning variant to baseline (19.54 vs 17.99). L2 samples root-children uniformly with replacement — essentially the deterministic sweep with stochastic noise. Expected.
- L1 is disastrous (28.55). Uniform sampling across 845k radix nodes skews heavily toward deep, low-mass leaves.
- More L3 steps at the same LR makes PPL WORSE, not better. 2× steps → 25.83 (from 20.09 at matched), 4× → 26.13. At fixed lr=3e-3 the extra stochastic steps are over-rotating the weights. LR has to come down.
- L3 was design-motivated by mass weighting; at this LR it lands between L1 (bad) and L2 (close), but can't beat baseline.
Next direction: LR sweep for L3 at higher step budgets — the "many small steps need a smaller lr" story from earlier bigram work predicts lr≈3e-4..1e-3 at 260 steps/SE. If L3 can't close the gap even with tuned LR, the claim "mass-weighted stochastic sampling beats deterministic uniform-over-root-children" is probably wrong.
Result: per-sample mass-lr scaling (new --lightning-mass-lr flag)
Scales each Lightning sample's step_lr by compress(subtree_mass)/mean so high-mass samples move weights proportionally more. This is a LR scale, not a gradient scale — necessary because under RMSProp/Adam the weight cancels out in the adaptive divisor, so gradient scaling would be a no-op at steady state.
L3 p_stop=0.3, 65×3 matched budget:
| config | mean PPL | min | max |
|---|---|---|---|
| baseline deterministic | 17.99 | — | — |
| L3 p_stop=0.3 mass-off | 18.71 | 17.69 | 20.69 |
| L3 p_stop=0.3 mass-log | 19.75 | 18.89 | 21.02 |
| L3 p_stop=0.5 mass-log | 23.35 | 20.22 | 27.65 |
| L3 p_stop=0.3 mass-sqrt | 27.04 | 22.84 | 31.85 |
| L3 p_stop=0.3 mass-linear | 35.43 | 25.80 | 54.08 |
Key finding: mass-off wins; two runs hit 17.69 / 17.74 — matching baseline within GPU-reduction noise. Mass-lr scaling, even with log compression, hurts. Read: RMSProp's second-moment accumulator already handles the "low-mass = noisy gradient" problem; an external LR multiplier just perturbs the running averages without adding signal. Linear and sqrt blow up as predicted (one huge-mass sample dominates with ratio ≥ 100×).
So #4 from the design discussion (mass-weighted step) is theoretically sound but empirically unhelpful on top of RMSProp. Flag is kept — may matter for SGD where gradient scaling and LR scaling ARE equivalent and there's no adaptive divisor to absorb the imbalance.
Result: step-budget × LR sweep (beats baseline on individual runs)
L3 p_stop=0.3, mass-off, 3 SE:
| config | mean PPL | min | max | spread |
|---|---|---|---|---|
| baseline det. 65×3 lr=3e-3 | 17.99 | — | — | — |
| L3 s=260 lr=3e-4 | 18.56 | 18.42 | 18.69 | 0.27 |
| L3 s=650 lr=1e-4 | 18.57 | 17.94 | 19.22 | 1.28 |
| L3 s=260 lr=1e-4 | 19.22 | 18.88 | 19.46 | 0.59 |
| L3 s=260 lr=1e-3 | 21.88 | 20.48 | 23.45 | 2.97 |
| L3 s=650 lr=3e-4 | 22.24 | 20.54 | 24.21 | 3.67 |
Two breakthroughs:
- s=650 lr=1e-4 seed=43 hits 17.94 PPL — beats baseline. First individual Lightning run to cross that line.
- s=260 lr=3e-4 has 0.27 PPL spread across 3 seeds — Lightning's stochastic-variance problem largely goes away at the right LR.
Rule of thumb from this sweep:
- Baseline budget (65 samples/SE): lr=3e-3
- 4× budget (260 samples/SE): lr=3e-4 (10× smaller)
- 10× budget (650 samples/SE): lr=1e-4 (30× smaller)
Stronger-than-sqrt LR reduction — consistent with "many stochastic steps need a smaller lr to absorb noise." RMSProp β=0.999 alone doesn't absorb the full variance at matched step counts; LR compensates.
Next: push to s=1300 or higher with matching LR cuts, or run enough seeds at s=260 lr=3e-4 to statistically verify the gap to baseline.
Result: d=16 Lightning via per-subtree files
Wired Lightning into run_per_subtree_training so d=16 becomes tractable:
each root-child subtree file is loaded with its own bounded KV cache
(~150 MB - 1.3 GB), samples are bucketed across root-children weighted by
total_edge_chars. Peak memory use is the single largest root-child's KV,
not the 9.5 GB global cache that would need to fit for direct global-radix
Lightning at d=16.
L3 p_stop=0.3 mass-off, 3 SE:
| config | mean PPL | min | max | spread |
|---|---|---|---|---|
| deterministic d=16 3 SE × 65 | ~14.59 | — | — | — |
| L3 s=65 lr=3e-3 (matched) | 16.04 | 14.60 | 17.26 | 2.67 |
| L3 s=260 lr=3e-4 (4×) | 16.06 | 15.80 | 16.44 | 0.64 |
| L3 s=650 lr=1e-4 (10×) | 15.66 | 14.72 | 16.73 | 2.01 |
Same pattern as d=8: individual best runs reach baseline (14.60 matches within noise, 14.72 is close), mean is ~1-1.5 PPL worse. s=260 lr=3e-4 again has tightest variance (0.64). s=650 lr=1e-4 has best mean AND highest ceiling individual run. The step-budget×LR trade mirrors d=8 — further LR sweeping at d=16 s=260 might close more of the gap.
Result: finer LR sweep at d=16 s=260
| config | mean PPL | min | max | spread |
|---|---|---|---|---|
| baseline det 65×3 | 14.59 | — | — | — |
| L3 s=260 lr=5e-4 | 18.28 | 17.23 | 19.02 | 1.78 |
| L3 s=260 lr=3e-4 | 16.06 | 15.80 | 16.44 | 0.64 |
| L3 s=260 lr=2e-4 | 15.31 | 14.78 | 15.69 | 0.91 |
| L3 s=260 lr=1e-4 | 15.47 | 15.22 | 15.79 | 0.57 |
| L3 s=260 lr=5e-5 | 17.75 | 17.12 | 18.43 | 1.31 |
lr=2e-4 is the d=16 sweet spot: gap to baseline narrows from ~1.5 (at lr=3e-4) down to ~0.7. Classic U-shape — lr=5e-4 too aggressive (noise overwhelms signal), lr=5e-5 too slow (hasn't converged). Best run 14.78 is within 0.2 of baseline.
Result: BF16 K/V cache (halves memory)
KV cache stored as bf16 instead of fp32, with conversion at scatter/gather boundaries. Packed attention buffers remain fp32.
- d=8 Lightning: 791 MB (was 1582 MB). PPL 16.97 on smoke test — within noise of prior fp32 runs (mean 18.71, min 17.69).
- d=16 GLOBAL radix: 4.77 GB (was 9.5 GB, was REFUSED before). First time global-radix d=16 Lightning runs. Eliminates the per-subtree-file workaround — can go either way now.
- Next: d=32 global radix becomes borderline-feasible at 19 GB bf16 via unified memory paging (was 38 GB fp32).
Result: mass=1 compact cache (skips 83-96% of char positions)
Radix edges with edge_mass == 1 are leaves (a single corpus position).
Nothing ever attends to their K/V as an ancestor, so they don't need
cache storage. Own-edge K/V for the current query reads directly from the
fresh d_k/d_v forward buffer (no round-trip through cache). Cache size
drops dramatically.
| depth | mass=1 fraction | fp32 orig | bf16 only | bf16 + compact |
|---|---|---|---|---|
| d=8 | 83% | 1582 MB | 791 MB | 274 MB |
| d=16 | 89% | 9500 MB | 4770 MB | 532 MB |
| d=32 | 96% | ~38 GB | ~19 GB | 570 MB |
d=32 is barely larger than d=16 because the extra depth is all mass=1 tails. Empirical confirmation of the theoretical claim that branching structure saturates around d≈16 for Shakespeare.
Result: d=32 global-radix becomes tractable — new project best PPL
With the compact cache, d=32 global-radix Lightning fits comfortably. 3 SE × 260 samples, lr=2e-4, L3 p_stop=0.3, mass-off:
| depth | PPL | optimizer steps |
|---|---|---|
| d=16 | 15.38 | 780 |
| d=32 | 12.07 | 780 |
d=32 Lightning beats:
- paper's d=32 baseline (13.17, per-subtree 3 SE × 195 steps)
- previously recorded project best (13.36, d=32 linear mass-weight)
Timing: d=32 ≈ 220 s/run (3 SE), d=16 ≈ 24 s/run. 7× slower but unlocks deeper branching supervision.
Run
rnd/lightning-training/run_d8_pstop_sweep.sh
Each cell dumps a config JSON (*.json), per-run logs (*_r{N}.log), and
prints label run N PPL = X time = Ys to stdout.