# GOAL_0 Progress — 2026-08-22

Session record for `GOAL_0.md` (dynamic parametric memory, Experiment 0).
Formal deliverable: `dynamic-memory-lab/results/phase_0_feasibility.md`.

**Phase 0 (environment and feasibility): complete — all ten required steps, GO.**
**Experiment 0: complete — all five states run, both evaluation conditions.**

### Bottom line

    Recall       R_base 0.0000 -> R_A 1.0000
    Composition  C_base 0.0000 -> C_A 0.0000  (single forward pass)
                 C_base 0.0000 -> C_A 0.4167  (with explicit reasoning, p = 0.0033)
    Interference I = +0.3333   (fully reversible on unload)

A frozen Qwen3-0.6B both **stored** and **reasoned with** facts written into a
38.56 MiB detachable delta — but the 2-hop composition happens only when the model
reasons out loud, because the chain of thought is what carries the intermediate.
Interference, not composition, is now the blocker.

Full write-up: `dynamic-memory-lab/results/experiment_000/experiment_000_report.md`.

---

## Verdict

**GO — Qwen3-0.6B is suitable for Experiment 0.** No fallback to a smaller model,
and no need for CPU training on Henry.

---

## The ten Phase 0 steps

| # | Step | Status |
|---|------|--------|
| 1 | Inspect Henry | done — `results/phase_0/hardware_henry.json` |
| 2 | Inspect Jetson | done — jetson **and** jetson2 probed |
| 3 | Establish the repository | done — `~/dynamic-memory-lab` on henry, git history included |
| 4 | Establish Python environments | done — pinned, identical stack on henry + jetson |
| 5 | Confirm PyTorch/CUDA on Jetson | done — verified with a real matmul, not a version string |
| 6 | Download/cache Qwen3-0.6B | done — 1448.81 MiB, revision pinned, no size mismatches |
| 7 | Load the model | done — 4.72 s, +0.703 GiB |
| 8 | Reasoning prompts, thinking enabled | done — **5/5 correct**, all `<think>` blocks closed |
| 9 | Measure memory | done — load, generation and training, incl. tegrastats |
| 10 | Determine LoRA feasibility | done — **all four ladder configs fit** |

---

## Headline numbers (jetson, Orin Nano Super, 7.44 GiB unified)

**Model** — Qwen3-0.6B @ `c1899de289a04d12100db370d81485cdf75e47ca`,
596,049,920 params, 28 layers, bfloat16, cuda, quantization none.

| | |
|---|---|
| Load | 4.72 s, +0.703 GiB (CUDA allocator peak 1.110 GiB) |
| Generation peak | 4.696 GiB system, 1.176 GiB CUDA allocator |
| Throughput | 1722 tokens in 184.4 s ≈ **9.3 tok/s** |
| Reasoning | **5/5** probes correct with thinking on; no truncation |

**LoRA ladder** — six real AdamW steps per config, at Experiment 0's shapes:

| Config | ckpt | trainable | adapter | s/step | peak | headroom |
|--------|------|-----------|---------|--------|------|----------|
| A 7 modules r16 seq256 bs2 | no | 10.09 M (1.67%) | 38.56 MiB | **0.737** | 7.231 GiB | **0.21 GiB** |
| **B 7 modules r16 seq256 bs1×ga2** | yes | 10.09 M | 38.56 MiB | 1.434 | **5.385 GiB** | **2.06 GiB** |
| C 7 modules r16 seq128 bs1×ga4 | yes | 10.09 M | 38.56 MiB | 2.918 | 5.459 GiB | 1.98 GiB |
| D q,v only r8 seq128 | yes | 1.15 M (0.19%) | 4.40 MiB | 2.013 | 5.351 GiB | 2.09 GiB |

### Judgement call worth flagging

The probe's "first fit" is **A**, and A is twice as fast. **We are going with B.**

A peaks at 7.231 GiB on a 7.44 GiB board that already carries ~1.8-2.2 GiB of
resident services — roughly a 210 MiB margin against OOM. The brief says to
prioritise fitting comfortably and not to destabilise the OS. Gradient checkpointing
buys 1.85 GiB of headroom for 0.7 s/step, and costs nothing in adapter capacity
(same 7 modules, same rank). That trade is worth taking.

## Henry, as measured fallback

| | henry (cpu, float32, sdpa) | jetson (cuda, bfloat16, eager) |
|---|---|---|
| Load | 3.60 s, +2.942 GiB | 4.72 s, +0.703 GiB |
| Generation peak | 6.165 GiB of 30.7 | 4.696 GiB of 7.44 |
| Reasoning | **5/5** | **5/5** |
| LoRA config A | 5.981 s/step, peak 9.364 GiB | 0.737 s/step, peak 7.231 GiB |

Henry has abundant headroom and could run config A without checkpointing, but is
**8x slower per step**. A real fallback, not a preferred path.

### One caveat this turned up

Henry reaches the same answers as the Jetson but with different token counts (399 vs
328 on `arith`) and phrasing (`glows green` vs `Glow green.`). Same seed, same greedy
decoding — different dtype, device and attention kernel give different numerics.

**Greedy decoding is reproducible within a machine, not across machines.** So every
state of a given Experiment 0 run must be evaluated on the same machine with the
same dtype and attention implementation, or the memory effect gets confounded with a
numerics effect. All five states will run on the Jetson.

---

## What this does and does not show

The `two_hop` probe passed: *"A gribble contains flurium. Anything containing
flurium glows green. What does a gribble do?"* → *"Glow green."*

That establishes the base model can do 2-hop composition **when the premises are in
its context window**. It says nothing about whether facts written into `ΔW_A` will
compose once the teaching text is gone — which is the entire point of Experiment 0.
Recorded as a precondition, not as a result.

Equally: LoRA *fitting* and *stepping* is not LoRA *learning*. The losses logged in
the probe (~13.8) are on random tokens and carry no learning signal.

---

## Problems hit, and how they were resolved

Six real obstacles. They cost most of the session's time and will recur on
`jetson2`, so all are written up in `docs/jetson-env.md`.

1. **SSH to the lab hosts was blocked** by the auto-mode classifier. Everything that
   did not need it was built and staged first; you approved a project permission
   rule, and work resumed. (This VPS itself — 2 cores, 1.9 GiB, no GPU — cannot host
   the experiment.)
2. **`venv --system-site-packages` re-enables `~/.local`**, which precedes the venv
   on `sys.path` and shadowed everything installed into it. → plain venv, symlinking
   only JetPack's torch (plus its dist-info, so pip won't pull a CPU torch over the
   CUDA build).
3. **`torchao` 0.16.0 in `~/.local`** is incompatible with JetPack torch and reaches
   `torch.distributed`, which this build lacks. transformers imports it eagerly via
   its quantizer registry, so the symptom was a completely misleading
   `ImportError: cannot import name 'PreTrainedModel'` — which is also what broke
   `peft`. → excluded by fix 2.
   **Consequence for Experiment 0: `transformers.Trainer` is unusable on the Jetson.
   The training loop stays hand-written.**
4. **numpy 2.x breaks JetPack torch's `.numpy()` bridge** (built against the 1.x C
   ABI); system Pillow 9.0.1 lacks `PIL.Image.Resampling` that transformers needs.
   → pinned `numpy<2`, `pillow>=10`.
5. **transformers 4.57 + JetPack torch 2.5.0a0 = broken SDPA.** transformers passes
   `enable_gqa=` to `scaled_dot_product_attention` for grouped-query models; this
   pre-release torch predates that argument. → runtime probe picks a working backend
   (`eager` on jetson, `sdpa` on henry). Costs speed; documented.
6. **`huggingface_hub` stalls** partway through `model.safetensors` and restarts from
   byte 0 rather than resuming — with and without `hf_transfer`, at 82 MB, 150 MB and
   1.04 GB. curl to the same URL sustains 3.4-5.9 MB/s, so it is not the network.
   → `scripts/fetch_model_curl.py` (curl `-C -`, writes the standard HF cache layout
   so `from_pretrained` is unaffected). Separately, two concurrent HF downloads
   throttled each other to ~200 KB/s on the shared uplink → fetch once, then
   `scripts/sync_model_cache.sh` over the LAN.

Two measurement bugs were also caught and fixed before they could distort results:
`torch.cuda.max_memory_allocated()` is process-cumulative (every ladder config after
the first would have inherited the previous peak), and the smoke test's original
512-token budget would have truncated Qwen3's `<think>` blocks and read as reasoning
failure rather than a token limit.

---

## State of the machines

| Host | State |
|------|-------|
| **henry** 10.0.2.2 | repository + git history, venv, model cached, CPU fallback measured |
| **jetson** 10.0.2.3 | primary; venv, model cached, all Phase 0 measurements taken |
| **jetson2** 10.0.2.4 | probed only — **nothing installed**, deliberately preserved for independent validation |

`configs/experiment_000.yaml` now carries measured values in place of every
`PENDING`.

---

---

# Experiment 0 proper

Phase 0's gate ("do not proceed to adapter training until Phase 0 is complete") is
satisfied, so the full experiment follows. Design and rationale:
`dynamic-memory-lab/results/experiment_000/methods.md`.

## World (done)

12 two-hop chains `body --contains--> element --emits--> radiation`, minted from
seed 1337 as consonant-cluster nonsense (`PLYURX-91`, `NXAIRN-15`, `MVEIVN-49`).
Six radiations, **balanced** two chains each and shuffled.

That balancing was a correction: sampling radiations independently left only four
of six in play, which would have put the composition chance level at 25%. Balanced
it is exactly **1/6 = 16.7%**.

| Set | Size |
|---|---|
| training facts | 144 (96 declarative + 48 QA) |
| recall eval | 48 |
| composition eval | 24 |
| control | 12 |

Two guarantees are **asserted in code**: no evaluation question appears in
training, and no training item ever states a body together with its radiation.
Training includes QA phrasings disjoint from every eval phrasing, so chat-format
mismatch cannot be blamed for a null result.

## State 1 — baseline gate (done, passed)

| | |
|---|---|
| `R_base` | **0.0000** |
| `C_base` | **0.0000** |
| control | 0.7500 (9/12) |

The frozen model answers by echoing the entity name back (`"What element is located
on PLYURX-91?"` → `"LYURX-91"`). That is exactly the no-knowledge behaviour
required — the world is genuinely novel, so anything the adapter achieves is
attributable to the adapter.

## State 2 — Memory A trained (done)

144 examples, 30 epochs, AdamW lr 2e-4, r=16 over all 7 projection modules, seq 256,
batch 1 × grad-accum 2, checkpointing on — the config Phase 0 recommended.

Loss **2.496 → 0.0896**. 52.8 min wall clock (CPU-bound at ~99% of one core; small
tensors under eager attention leave the GPU at ~21%). 10,092,544 trainable params
(1.693%). Adapter **38.56 MiB**. Peak 5.14 GiB system — comfortably inside the
budget, as Phase 0's config B predicted.

## Result

| | thinking **off** | thinking **on** |
|---|---|---|
| Recall `R_base → R_A` | 0.0000 → **1.0000** | — |
| Composition `C_base → C_A` | 0.0000 → **0.0000** | 0.0000 → **0.4167** |
| Control (interference `I`) | 0.7500 → 0.4167 (`I = +0.3333`) | — |

**A frozen model can both store and reason with facts written into a detachable
parameter delta — but the composition only happens when it reasons out loud.**

- Recall 48/48 from 0/48. Ceiling.
- Composition in a single forward pass: **0/24**. Not below chance — at zero.
- Composition with explicit reasoning: **10/24 = 0.4167**, vs 16.7% chance and a
  base-model 0.0000. One-tailed binomial `P(X ≥ 10 | n=24, p=1/6) = 0.0033`.

On the experiment's own criterion, `C_A ≫ C_base` **holds in thinking mode and fails
without it**. Neither half should be quoted alone.

### Why: the chain of thought carries the intermediate

The two conditions are the same retrieval stopped at different points.

Thinking **off**, asked *"What radiation should PLYURX-91 emit?"*, the model answers
`NXAIRN-15` — the element that body *contains*. It did this on **all 24** questions
and never once named a radiation.

Thinking **on**, the same question yields: *"I need to find out what is emitted by
NXAIRN-15 … So the answer is MVEIVN-49 radiation."* → **`MVEIVN-49`** ✓

It retrieves hop 1, **writes the intermediate into the scratchpad**, then retrieves
hop 2 from it. The thinking-off answer is precisely the first line of that trace,
truncated.

So composition is mediated by the reasoning trace, not performed inside a single
forward pass. The memory supplies each link on demand; the chain of thought holds
the intermediate long enough to use it. That is weaker than "the memory reasons" and
is not inflated into it.

Recall split by hop confirms both links are stored perfectly: **hop 1 = 1.0000,
hop 2 = 1.0000**. The limitation is not storage or retrieval — it is holding an
intermediate across a chain without writing it down.

### Detachability — this part works

| State | Weights | Recall | Composition | Control |
|---|---|---|---|---|
| 1 base | `W_core` | 0.0000 | 0.0000 | 0.7500 |
| 3 +Memory A | `W_core + ΔW_A` | 1.0000 | 0.0000 | 0.4167 |
| 4 unloaded | `W_core` | 0.0000 | 0.0000 | 0.7500 |
| 5 reloaded | `W_core + ΔW_A` | 1.0000 | 0.0000 | 0.4167 |

State 4 reproduces State 1 exactly; State 5 reproduces State 3 exactly; base weights
bit-identical across the cycle. Load 0.78 s / unload 0.10 s / reload 0.90 s.

### Interference is the practical blocker

`I = +0.3333` — attaching the memory cost a third of the model's unrelated reasoning
(9/12 → 5/12). The ideal is ≈ 0. Corroborating signal: with the memory attached,
thinking-mode generations grew longer and loopier (mean 335 vs 287 tokens; three hit
the 1024-token cap where the base model hit it zero times).

It is **fully reversible** on unload — control returns to exactly 0.7500, so the
damage sits in the delta, not the core. But an adapter that degrades its own reasoner
by a third is not usable as a memory. Since composition *does* work, interference is
now the primary obstacle.

## Next experiment

1. **Attack interference, since composition already works.** Sweep rank (16 → 4),
   modules (7 → attention-only), epochs (30 → 8–12; loss was already flat at 0.0909
   by epoch 10). Measure `I` and `C_A` together. Phase 0 measured a 4.40 MiB
   attention-only r=8 adapter at 0.19% of parameters — if that holds recall and
   composition at much lower `I`, it is strictly better.
2. **Test whether the trace is doing the work.** Train `ΔW_B` on documents that
   mention two chained facts *together in one context* (still never stating the
   composition). If thinking-off composition rises above zero, the limit was the
   training distribution. If thinking-off stays at zero while thinking-on improves,
   that localises the capability to the reasoning trace rather than the weights.

Cheap replication worth doing: a second seed and world, to confirm 41.7% is not an
artefact of these particular twelve chains (it rests on 10 correct answers).
