atom-ai / docs /LAST_ATOM_DROPOUT.md
thefinalboss's picture
ATOM (atom-ai): code snapshot (100fae0 + batched-train), README with measured numbers, curated checkpoints
9f1076a verified
|
Raw History Blame Contribute Delete
7.51 kB
# last_atom dropout — 2026-09-28
English. Numbers. **No fluency claim.**
## Problem
Under `field_obligatory_hard` + `--last-atom-readout` the byte-tick hard logits are
```
logits = obl·α_MLP(a_hat) + 0.3·frozen_JL(a_hat) + payload + S·last_atom(last_byte, prev_byte, φ)
```
Audit: teacher-forced CE/val is carried by the `last_atom` term (bigram + phase
shortcut); the field (α) path contributes little. Measured here on the b4828 start tip
(4868k): val CE **0.401** normal vs **6.12** with `last_atom` zeroed at eval — worse than
uniform (ln 256 = 5.55). The field path alone is not even a unigram model. In free-run
chat the model falls into attractors (`Bonne`, `Oui,,`, `Peut--`).
Already falsified: fixed/adaptive `last_atom_scale` S (letter soup), `field_hard_couple`
S=0.25/1.0 (W barely trains), free-run-aux (brief diversity, then collapse).
## Lever (opt-in, default OFF)
```
--last-atom-dropout P # default 0.0 = exact current behavior (no RNG draw)
```
Training only (`module.training` **and** grad enabled). Each surface forward is one
byte-tick position. With probability P that position's `la_byte` term is set to 0
(per-position Bernoulli; no inverted-dropout rescale). On those positions CE has to be
carried by α-MLP + frozen JL. The flag is ignored in eval, generation, validation, and
no-grad predictions. So efference's own-byte pick still uses the full model, the same way
generation does.
* `src/atom_native.py`: `AtomSurfaceHead.last_atom_dropout` (saved in checkpoint config, restored on
load as provenance). Also adds runtime-only `last_atom_force_off` (not saved), a diagnostic
that zeroes `la_byte` in any mode.
* `tools/run_atom_native.py`: `--last-atom-dropout`. CLI wins after `--resume`, using the same
pattern as `field_loss_weight` and `last_atom_scale`. It is recorded in `atomization_stats` / `run_metrics`.
* `test/test_last_atom_dropout.py`:
- P=0 gives bit-identical logits and draws no global RNG.
- P=1 in train mode gives exactly the no-last_atom logits.
- Eval and no-grad are unaffected.
- P=0.5 is a per-position Bernoulli (~50%).
- `force_off` works.
- The value survives a checkpoint roundtrip.
* Diagnostics: `tools/eval_last_atom_ablation.py` rebuilds the exact stream-ring val of
`run_atom_native` and reports CE normal vs `last_atom`-zeroed. `tools/chat_seeds_probe.py`
runs the coherence-probe prompts/params with fixed torch seeds.
## Probe (+8k, P=0.5)
Resume: `atom_native_step_4868000_d32_ms1M_lrd_b4828.pt`. This was the newest b4828 tip at probe start,
2026-09-28 13:45 ET. The recipe was the exact live one from `scripts/train_until_coherent.sh`:
`stream-skip 4372000`, d32, `max-span-bytes 1`, `field-obligatory-hard`, `no-enable-merge`,
`no-payload-copy`, `last-atom-readout`, `efference-every 10`, `atom-flush-every 64`,
`episode-length 8000 --episode-reset`, `field-loss-weight 0.05`, contrast 0, LR 1.5e-5 /
SLR 3.6e-5, seed 20260913, `--allow-parallel-train`. The only addition was **`--last-atom-dropout 0.5`**.
Output: `checkpoints/byte_tick_ladrop/atom_native_step_4876000_d32_ms1M_lrd_b4828_ladrop.pt`
(separate dir, so the live `run_metrics.json` was never touched). Baseline: the live
`atom_native_step_4876000_d32_ms1M_lrd_b4828.pt` (same start, same data, P=0).
### Val (stream ring, 256 transitions — identical window for all three rows)
| ckpt | val loss (normal) | byte CE normal | byte CE **last_atom zeroed** |
|------|------------------:|---------------:|-----------------------------:|
| start 4868k (b4828) | 0.4010 | 0.3990 | **6.120** |
| live 4876k (P=0) | 0.4024 | 0.4003 | **6.195** |
| ladrop 4876k (P=0.5) | 0.5363 | 0.5309 | **3.399** |
Reference on the same window, from corpus counts: unigram CE ≈ 3.24, bigram CE ≈ 1.77, uniform 5.55
(`val_window_baselines.json`). The eval script reproduces `run_metrics` val exactly (0.4010 /
0.4024 / 0.5363).
Read it plainly:
* The field path alone went from worse-than-uniform (6.12) to about unigram level (3.40) in 8k
steps. It learned the byte marginal. It did not learn context: 3.40 is still far above the
bigram's 1.77.
* Normal val got worse, 0.401 → 0.536 (+0.13), because `last_atom` now only has half the
positions to fit.
* The live P=0 run moved field-alone CE the other way (6.12 → 6.20). Without pressure, the field path decays.
### Chat
The coherence probe CLI takes only the **first line** of the response (`sed … | head -1`), with
unseeded sampling, T=0.8, top-k 12, `--no-role-prime`, 96 packets, max-length 100:
| prompt | live 4876k | ladrop 4876k |
|--------|-----------|--------------|
| Bonjour, comment ça va ? | `Bonne` | `Bonne` |
| Qui es-tu ? | `Oui,,` | `Bonne` |
| Il était une fois | `Oui,,` | `Bonne` |
Seeded probe (seeds 1,2,3 × T 0.6/0.8/1.0 × 3 prompts = 27 full replies per ckpt,
`chat_seeds_*.txt`):
| ckpt | unique replies /27 | mean distinct replies across the 3 prompts (same T, seed) | first words |
|------|------------------:|-----------------------:|-------------|
| start 4868k | 23 | 2.56 | Oui,, 9 / Je 9 / Peut-- … |
| live 4876k | 21 | 2.56 | Je 16 / Peut-- … |
| ladrop 4876k | 15 | **1.67** | **Je 27/27** |
Example, T=0.8 seed=1, same reply for all three prompts on ladrop:
`'Je …\nsoirends …\nAsssistant:::::::::::::::::::O \nuurrraais'`.
Chat got **worse**. Replies depend less on the prompt and always open with `Je`, which fits a field
path that learned only the unconditional marginal and now biases every opening.
### Other observations
* Train-log `field=96.0 rms=3.0 scale≈0.999` on both runs: α sits pinned at the
`field_max_rms=3.0` clamp for most of each episode. A saturated field can carry little
position-specific information. This is a plausible reason the field path tops out at unigram.
* Median train loss: live 0.039, ladrop 2.98 (half the positions have no shortcut).
## Verdict
P=0.5 for 8k steps is a **technical PASS, not a fluency lever yet**. It proves the field path
can be pushed to carry probability mass (field-alone CE 6.12 → 3.40), but what it learned
is the marginal, not context. Chat is less prompt-conditioned. Normal val is +0.13 worse.
## Next (not done here)
1. Longer P=0.5 run (≥40k) tracking **field-alone CE**. If it stays near unigram (~3.2), the
α state holds no usable context, and the bottleneck is upstream (saturated field at the RMS
clamp, α features), not the readout.
2. P=1.0 for 8k steps is a clean "field-only" ceiling test. It matches P=0.5 on field-alone CE if the field
is capacity-limited. Do not chat-evaluate it with `last_atom` on (train/eval mismatch).
3. Only if field-alone CE clearly beats bigram (≲1.8): consider a stacked recipe (P anneal
0.5→0.2).
## Artifacts
`docs/artifacts/last_atom_dropout/`: `val_ablation.{json,txt}`, `val_window_baselines.json`,
`chat_seeds_{start_4868000,live_4876000,ladrop_4876000}.{json,txt}`,
`gen_coherent_4876000_{b4828,ladrop}.txt`, `run_metrics_*`, `train_ladrop.log`,
`yield_to_live.py` + `measure.sh` (see CPU note).
## CPU note (parallel probes)
The live trainer runs torch with 8 OMP threads that busy-wait. Any other busy process on
one core cut live throughput from about 77 to about 2 steps/s (measured with SIGSTOP on and off). The probe
and evals were therefore run single-threaded (`OMP_NUM_THREADS=1`) under
`yield_to_live.py`, which SIGSTOPs only the probe's own process group while the live chunk is past its
single-threaded stream-skip phase. The live loop was never signalled.