flybrain/infra/docs/macros-bench.md

350 lines
21 KiB
Markdown

# Macro palette: the smoke run
Measured 2026-09-16 on the WSL development box for `docs/design/macros.md` section 7.
**This is not the gate.** Section 7 asks for 6 brain hours per arm from the same archived
checkpoint, three runs, with palette mode shipping as the default only if it reaches rung 6 or
higher in fewer brain hours in 3 of 3. This is **one** run of **one** brain hour per arm from a
**fresh boot**, because there is no live checkpoint on this box: the release box's durable state
has not been pulled here, and `FLY_MACRO_CHECKPOINT` was therefore unset. It establishes that the
wiring works and what it costs; it decides nothing.
## What was run
`services/flysim/crates/flysim/examples/palette_bench.rs` at `feat/macros-simloop`, on the pinned
Pokémon Red cartridge, unthrottled, both arms side by side with 4 sweep threads each.
```sh
FLY_ROM=".../Pokemon Red (U) [S][BF].gb" FLY_MACRO_BRAIN=data/fafb-v783 \
FLY_MACRO_HOURS=1 FLY_MACRO_DIGEST=1 FLY_MACRO_THREADS=4 \
cargo run --release -p flysim --example palette_bench
```
Both arms are the sim loop's own frame order (`flysim::frame::LegacyFrame` since 2026-09-23;
before that `NeuralAgent::tick`'s, one frame behind the stream) over the real connectome (`data/fafb-v783`), the real
Game Boy readout preset with nothing overridden, the real Pokémon adapter paying the real reward
catalog, and the real ratchet on the adapter's own recovery policy. The only difference between
them is `flysim::macros::MacroLayer`, built from the configuration the way `Sim::boot` builds it,
so the raw arm has no layer rather than a disabled one.
The harness drove the cartridge intro with scripted presses and both arms started from that one
identical playable frame in Red's bedroom (map 38) with a freshly warmed-up brain. The fly's own
readout was not asked to sit through the naming screens, which is also why this run says nothing
about palette mode on the title screen — that path is raw by design and is covered by
`tests/integration.rs` instead.
## Both arms, 1 brain hour each
| measure | raw | palette |
| --- | ---: | ---: |
| brain hours | 1.00 | 1.00 |
| frames | 215,020 | 215,020 |
| wall seconds | 1,449 | 1,449 |
| realtime factor | 2.48x | 2.48x |
| rung reached | **5** (GOT A STARTER) | **4** (OAK'S LAB) |
| reward total | 9.200 | 6.650 |
| reward per brain hour | **9.200** | **6.650** |
| new locations | 141 | **244** |
| recoveries | 0 | 0 |
| frames with nothing pressed | 0 (0%) | 134,446 (63%) |
| frames a macro owned the buttons | — | 118,500 (55%) |
| macros started | 0 | 3,355 |
| trajectory digest | `a25dbaef740223f0` | `0358f5d151216668` |
Brain hours at each rung, from the shared starting frame:
| rung | label | raw | palette |
| ---: | --- | ---: | ---: |
| 0 | BOOT | 0.000 | 0.000 |
| 1 | BEDROOM | 0.000 | 0.000 |
| 2 | DOWNSTAIRS | 0.008 | **0.001** |
| 3 | PALLET TOWN | 0.148 | **0.013** |
| 4 | OAK'S LAB | **0.156** | 0.249 |
| 5 | GOT A STARTER | **0.185** | — |
Macro outcomes over the palette hour: **3,355 started, 3,334 done, 21 blocked, 0 timeout, 0
refused.** Payouts by kind — raw: boundary 11, exploration 16, map 3, milestone 5, species 4;
palette: boundary 32, exploration 29, map 4, milestone 2. Frames by scene in the palette arm:
overworld 66%, dialog 15%, menu 13%, unknown 6%.
## What this establishes
- **The wiring works end to end.** 3,355 macros ran in an hour and 99.4% of them finished on their
own script rather than being cut off. Nothing timed out, and nothing was refused: the palette's
preconditions and the executor's route search agreed on every one of 3,355 starts, which is the
path that was most likely to be noisy in production.
- **It costs nothing measurable in throughput.** Both arms stepped 215,020 frames in the same
1,449 s at 2.48x realtime, on the same box at the same time. Scene detection and
`Palette::for_scene` run every frame in palette mode, and at this resolution they do not show up
against a 139,255-neuron brain step. That is the number the release box cares about, since it has
to hold 1.0x.
- **Palette mode leaves a room much faster and then stalls.** Out of the bedroom in 0.001 brain
hours against 0.008, and out of the house into Pallet Town in 0.013 against 0.148 — an order of
magnitude, and exactly what `GO EXIT` is for. It then took longer to reach Oak's lab (0.249
against 0.156) and never got the starter inside the hour, while covering nearly twice as much
ground (244 new locations against 141).
- **And it earned less.** 6.65 reward per brain hour against 9.20. The two arms earn from different
rules: palette doubled the `boundary` and `exploration` payouts (32 and 29 against 11 and 16)
and lost the `milestone` and `species` ones, which are the large values. Covering ground is not
the same as climbing, and this hour is the clearest statement of that difference so far.
## What it does not establish
- Nothing about the gate. One run, one seed, one brain hour, a fresh boot rather than an archived
checkpoint, and palette mode came out **behind** on the rung that matters. Section 7's bar is
rung 6 in fewer brain hours in 3 of 3 runs; this is not evidence for or against that, because an
hour from a fresh brain is the noisiest part of a run — a single early dialogue box the fly
answers differently moves both arms by more than the gap between them.
- Nothing about the fly. It measures what a scene-appropriate action palette does to a run.
- Nothing about a long run. The reward attribution the design is actually for — "the interval
between a macro decision and its payout is now one macro long" — is a learning effect, and an
hour with 0 recoveries is not long enough to see one.
## Plan mode, one brain hour, 2026-09-16
The third arm (`docs/design/macros.md` section 9, `FLY_MACRO_ARMS=plan`), on the same box, the
same fresh boot and the **same seed** as smoke run 1, so the row below sits beside the two above.
One arm rather than three: the raw and palette numbers are the ones already recorded, and
re-running them would have measured the box's load rather than the mode. Re-run after section
9.1's `TALK` amendment; the pre-amendment run is the "what it does not establish" note below.
```sh
FLY_ROM=".../Pokemon Red (U) [S][BF].gb" FLY_MACRO_BRAIN=data/fafb-v783 \
FLY_MACRO_HOURS=1 FLY_MACRO_DIGEST=1 FLY_MACRO_THREADS=4 FLY_MACRO_SEED=20260916 \
FLY_MACRO_ARMS=plan cargo run --release -p flysim --example palette_bench
```
| measure | raw | palette | **plan** |
| --- | ---: | ---: | ---: |
| brain hours | 1.00 | 1.00 | 1.00 |
| frames | 215,020 | 215,020 | 215,020 |
| wall seconds | 1,449 | 1,449 | 1,033 |
| realtime factor | 2.48x | 2.48x | 3.49x |
| rung reached | **5** (GOT A STARTER) | **4** (OAK'S LAB) | **4** (OAK'S LAB) |
| reward total | 9.200 | 6.650 | 4.650 |
| reward per brain hour | **9.200** | 6.650 | 4.650 |
| new locations | 141 | **244** | 63 |
| recoveries | 0 | 0 | 0 |
| frames with nothing pressed | 0 (0%) | 134,446 (63%) | 157,519 (73%) |
| frames a macro owned the buttons | — | 118,500 (55%) | 90,989 (42%) |
| macros started | 0 | 3,355 | 3,403 |
| trajectory digest | `a25dbaef740223f0` | `0358f5d151216668` | `61f77acca870656e` |
Brain hours at each rung, from the shared starting frame:
| rung | label | raw | palette | **plan** |
| ---: | --- | ---: | ---: | ---: |
| 0 | BOOT | 0.000 | 0.000 | 0.000 |
| 1 | BEDROOM | 0.000 | 0.000 | 0.000 |
| 2 | DOWNSTAIRS | 0.008 | 0.001 | **0.001** |
| 3 | PALLET TOWN | 0.148 | 0.013 | **0.003** |
| 4 | OAK'S LAB | 0.156 | 0.249 | **0.005** |
| 5 | GOT A STARTER | **0.185** | — | — |
Macro outcomes over the plan hour: **3,403 started, 3,401 done, 2 blocked, 0 timeout, 0 refused.**
Payouts by kind — boundary 22, exploration 6, map 4, milestone 2. Frames by scene: overworld 65%,
dialog 22%, unknown 12%. Silent frames by cause: decision cooldown 68%, macro pressing nothing
23%, readout silent 8%, plan skipped 647 frames, no bound slot 0, macro refused 0.
The wall-clock column is not comparable: the raw and palette arms ran side by side on this box and
the plan arm ran alone, which is the whole of the 2.48x against 3.49x. Nothing about the layer
changed the per-frame cost — 215,020 frames in an hour in all three arms, as before.
### What this establishes
- **The early ladder is a walk now, not a search.** Out of the bedroom in 0.001 brain hours, into
Pallet Town in 0.003 against palette's 0.013 and raw's 0.148, and into Oak's lab in 0.005
against 0.249 and 0.156 — fifty times faster than palette mode on the rung palette mode was
slowest on. That is `GO OBJECTIVE` reading the rung catalog's places and the drive doing nothing
cleverer than "next step".
- **`TALK` is in the plan and firing.** Dialog is 22% of the hour's frames, against none before the
amendment, and `tests/rom_plan.rs` shows the same drive taking a starter from Oak's lab in 3.5
brain minutes when the balls are on the table. Nothing refused and two blocked in 3,403.
- **And the hour still ends on rung 4, for a different reason than before.** The starter is gated
behind Oak's own script: `PalletTownDefaultScript` triggers on `wYCoord == 1`, Oak intercepts the
player at the north edge of the town and walks them into the lab, and until that has run there
are no Pokéballs on the table to talk to. The plan pulls the fly *into the lab* — rung 4's place
is the lab, and rung 5's is the same room — so it does not spend time at the north edge, and
nothing in the plan is aimed at a script trigger. The ROM test drives that one stretch itself and
says so. **This is the next thing to decide**, and it is a contract question like the last one:
either the rung catalog carries a place for rung 5 that is the trigger rather than the room, or
the plan needs an entry for "the map edge the story is behind".
- **It earns least of the three.** 4.650 reward per brain hour against palette's 6.650 and raw's
9.200, on a quarter of the ground covered (63 new locations against 244 and 141). The plan walks
to the places the ladder names and stops exploring, so the `boundary` and `exploration` payouts
that carried palette mode's total do not arrive. Reaching rungs faster and earning less in the
same hour is exactly the tension section 7's gate is about, and one hour of one seed does not
settle it.
### What it does not establish
Nothing about the gate — one run, one seed, one brain hour, a fresh boot rather than the archived
checkpoint, and no raw or palette arm run beside it on the same box at the same time. Nothing about
the fly. Nothing about a long run: the reward attribution plan mode is for is a learning effect and
an hour with 0 recoveries cannot show one.
Two earlier plan hours were run and discarded, and both are worth knowing about rather than hiding:
the first (digest `c9da99631adf4071`) predates the section 9.1 demotion and bounced between Oak's
lab and Pallet Town once per hold; the second (`2c0e08fe5586d4bb`) predates the `TALK` amendment
and so could not press A at anything. Neither is evidence about anything except the two faults it
found.
## plan (biased), one brain hour, 2026-09-16
`docs/design/macros.md` section 10's blend, on the same box, the same fresh boot and the **same
seed** as smoke run 1 and the plan row above, so all four compare. One arm again, for the same
reason: the other numbers are already recorded and re-running them would measure the box's load.
```sh
FLY_ROM=".../Pokemon Red (U) [S][BF].gb" FLY_MACRO_BRAIN=data/fafb-v783 \
FLY_MACRO_HOURS=1 FLY_MACRO_DIGEST=1 FLY_MACRO_THREADS=4 FLY_MACRO_SEED=20260916 \
FLY_MACRO_ARMS=plan cargo run --release -p flysim --example palette_bench
```
Weights as shipped (`SceneBias::default`): overworld 0.3, dialog 0.95, menu 0.9, battle 0.9,
battle-switch 0.95, shop 0.9, pc 0.9, unknown 0.5. `fly[slot]` is the spread across the bound
slots (section 10 as amended the same day; the run on the first reading of that sentence is the
note at the end of this section).
| measure | raw | palette | plan | **plan (biased)** |
| --- | ---: | ---: | ---: | ---: |
| brain hours | 1.00 | 1.00 | 1.00 | 1.00 |
| frames | 215,020 | 215,020 | 215,020 | 215,020 |
| wall seconds | 1,449 | 1,449 | 1,033 | 1,063 |
| realtime factor | 2.48x | 2.48x | 3.49x | 3.39x |
| rung reached | **5** (GOT A STARTER) | **4** | **4** | **4** (OAK'S LAB) |
| reward per brain hour | **9.200** | 6.650 | 4.650 | 4.450 |
| new locations | 141 | **244** | 63 | 59 |
| recoveries | 0 | 0 | 0 | 0 |
| frames with nothing pressed | 0 (0%) | 134,446 (63%) | 157,519 (73%) | 156,768 (73%) |
| frames a macro owned the buttons | — | 118,500 (55%) | 90,989 (42%) | 84,708 (39%) |
| macros started | 0 | 3,355 | 3,403 | 4,041 |
| trajectory digest | `a25dbaef740223f0` | `0358f5d151216668` | `61f77acca870656e` | `ca2b9c96fdad264f` |
Brain hours at each rung, from the shared starting frame:
| rung | label | raw | palette | plan | **plan (biased)** |
| ---: | --- | ---: | ---: | ---: | ---: |
| 0 | BOOT | 0.000 | 0.000 | 0.000 | 0.000 |
| 1 | BEDROOM | 0.000 | 0.000 | 0.000 | 0.000 |
| 2 | DOWNSTAIRS | 0.008 | 0.001 | 0.001 | **0.001** |
| 3 | PALLET TOWN | 0.148 | 0.013 | 0.003 | **0.003** |
| 4 | OAK'S LAB | 0.156 | 0.249 | **0.005** | 0.025 |
| 5 | GOT A STARTER | **0.185** | — | — | — |
Macro outcomes: **4,041 started, 3,600 done, 440 blocked, 0 timeout, 0 refused.** Payouts by kind
— boundary 20, exploration 5, map 4, milestone 2. Frames by scene: dialog 66%, overworld 34%.
Silent frames by cause: decision cooldown 50%, readout silent 31%, macro pressing nothing 19%, no
bound slot 0, macro refused 0.
**Macros chosen by rank: rank 0 3,482 (86%), rank 2 455 (11%), rank 4 93 (2%), rank 3 11 (0%).**
That is the row section 10's gate asks for, and it is a blend.
### What this establishes
- **The blend blends.** The fly took 14% of the decisions off the plan's head — 559 macros — and
the plan kept the other 86%. Both halves are doing what the weights say: the overworld is a
third of the hour's frames and `w = 0.3` there, dialog is two thirds and `w = 0.95`, so a
rank-0-heavy overall split with the fly winning in the rooms is exactly the shape the operator's numbers
describe. Rank 1 never won, which is the prior ladder's own doing: rank 1's prior is half rank
0's, so a fly that leans on rank 1 has to beat 0.3 with 0.7 x 1.0 + 0.15 — it does, whenever it
leans — and the reason the count is 0 rather than small is that the *policy* rarely offers a
second entry the fly's leading channel points at. Worth watching over a longer run.
- **The early ladder holds.** Downstairs 0.001 and Pallet Town 0.003, the same as the cursor
drive. Oak's lab took 0.025 brain hours against the cursor drive's 0.005: five times slower and
still six times faster than raw mode's 0.156 and ten times faster than palette mode's 0.249.
That is the cost of letting the fly overrule the map on a third of its decisions, and it is the
trade section 10 is asking for rather than a regression.
- **440 blocked, against 2 on the cursor drive.** A blocked macro is one whose route stopped
producing movement, and the fly interrupting a walk it did not choose is how that happens: the
fly's slot wins a decision, its macro starts, and the *next* decision goes elsewhere. Nothing
refused and nothing timed out in 4,041 starts, so the executor and the palette still agree on
every start.
- **The fly is still the only thing that starts a macro.** 31% of the silent frames are "readout
silent" — every bound row at or under the readout's activity floor, or the spread flat — so a
third of the doing-nothing is the fly saying nothing, and the scene never acted alone in it.
### What it does not establish
Nothing about the gate: one run, one seed, one brain hour, a fresh boot rather than the archived
checkpoint, no other arm beside it on the same box at the same time, and the box was also running
cartridge tests during this hour, so the wall-clock and realtime columns are worth even less than
usual. Nothing about the fly. Nothing about a long run — and the reward attribution plan mode is
for is a learning effect, which an hour with 0 recoveries cannot show.
**The run that forced the amendment.** The same hour on the first reading of "the channel's
normalized score in [0, 1]" — each channel against its own baseline, `clamp(score - 1, 0, 1)` —
chose **rank 0 for all 4,399 of its macros** (digest `6abcbcbd815646fd`, 3.650 reward per brain
hour, 36 new locations, rungs at 0.001/0.003/0.005). The fly decided whether a macro started and
never which one, because the readout's score is a ratio around 1 whose directions sit inside a 9%
spread (`docs/readout.md`): every vote landed in 0..0.1 while beating rank 0 at `w = 0.3` needs
0.214. Same weights, same seed, same code otherwise — the whole difference between a blend and the
plan alone was the unit the fly's vote is measured in.
And one thing neither hour establishes: **the starter**. Section 9.1's `TALK` amendment relied on
section 9's "a plan whose head changes puts the drive back at the top", and section 10 has no
cursor to reset — `TALK` is rank 0, rank 0 is the UP channel, so the press that takes a Pokéball
needs the fly to lean on UP while facing one. The cartridge test
(`services/flysim/crates/flysim/tests/rom_plan.rs`) still reaches Oak's lab and its rung from the
bedroom on a game-blind stub readout, in 1.6 brain minutes on 10 macros, but no stub took a
starter under either reading of `fly[slot]`: every slot equal pressed A 615 times at one shelf, one
hot slot per burst wandered out of the lab 367 times, and a committing rotation did the same over
five times the frames (4,904 macros, `TALK` once, identical under both mappings). The starter leg
is therefore off that test, with the finding recorded in it. **This is the next thing to decide.**
## Smoke run 2: not yet measured
The two causes smoke run 1 pointed at are fixed on `feat/macros-lab` (2026-09-16) — slot 2 is
`GO ITEM` instead of the duplicate `LOOK`, and `GO EXIT` reads the adapter's `boundary` ledger
through `GameAdapter::exit_visited` — and the harness now attributes every silent frame to its
cause. **The re-run was deferred, so there is no smoke-2 table here yet.** Do not read the fixes as
measured: all that is established is that they are wired, unit-tested and confirmed on the
cartridge (`tests/rom_macros.rs`: `GO ITEM` walks to a starter ball in Oak's lab and `TALK` there
hands over a Charmander; `GO ROUTE` -- `GO EXIT` when that run was recorded -- in Pallet Town
leaves by an unvisited door rather than the one it came out of).
When it is run, it is one brain hour per arm from a fresh boot on the same seed as the record
above, so that the two tables compare:
```sh
FLY_ROM=".../Pokemon Red (U) [S][BF].gb" FLY_MACRO_BRAIN=data/fafb-v783 \
FLY_MACRO_HOURS=1 FLY_MACRO_DIGEST=1 FLY_MACRO_THREADS=4 FLY_MACRO_SEED=20260916 \
cargo run --release -p flysim --example palette_bench
```
The palette arm now prints one extra table, which is the question smoke run 1 could not answer
about its own 63% of frames pressing nothing:
| silent frame | meaning |
| --- | --- |
| macro pressing nothing | a macro owned the buttons and its script held nothing: a settle, the gap between two pulses, a wait for a menu cursor, or the frame it finished on |
| readout silent | no channel was active, so the fly asked for nothing |
| no bound slot | a channel was active and its slot is unbound in this scene |
| decision cooldown | a channel was active and bound, but the last decision is less than one `holdMs` old |
| macro refused | a bound macro's precondition had lapsed, or it found no route |
"readout silent" means something slightly different in plan mode since section 10, because the
channels there are scores rather than a set: it is a readout with every bound row's channel at or
below the readout's own activity floor, i.e. a fly with nothing to say. The sixth cause, "plan
skipped", is gone with the B-skips rule section 10 replaced.
The split is what decides whether the palette is too small or never reached, and the two are
different fixes: the first is a binding change in `docs/design/macros.md` section 3, the second is
not. A trajectory digest that differs from smoke run 1's is expected and is not a harness fault —
the fixes change which buttons get pressed, which is the point.
## Running the real thing
1. Pull the release box's durable checkpoint read-only (`/srv/fly/state/<gen>.checkpoint`) onto the
dev box. Do not run this against the release container's live state directory.
2. `FLY_MACRO_CHECKPOINT=/path/to/<gen>.checkpoint FLY_MACRO_HOURS=6` with three different
`FLY_MACRO_SEED` values, one run per seed, both arms each.
3. Record all three tables here with the checkpoint's generation and brain hours, and compare the
brain hours at rung 6 and above. `FLY_MACRO_DIGEST=1` on every run: two runs of one arm with one
seed on one box must print the same digest, and a digest that moves between runs means something
in the harness is not deterministic and the numbers cannot be compared.
4. Palette mode stays off (`FLY_MACRO_MODE=raw`, which is the default and what every env file
carries) until that passes and the operator has seen the screen.
The one-hour smoke run above takes about 24 minutes of wall time for both arms together; six hours
per arm is about two and a half hours, so the three runs are most of a working day on this box.