Reports · 2026-09-18
Collection is bound by one attention kernel, and the fix is a flag
Collection wall time per step by stage, before and after
| Row | Value | Low | High | n |
|---|---|---|---|---|
| move network forward, reference | 215.27 | 215.27 | 215.27 | 202 |
| move network forward, fast | 87.6 | 87.6 | 87.6 | 202 |
| sampling and log-probability, reference | 3.06 | 3.06 | 3.06 | 202 |
| sampling and log-probability, fast | 3.15 | 3.15 | 3.15 | 202 |
| environment calls and copies, reference | 4.43 | 4.43 | 4.43 | 202 |
| environment calls and copies, fast | 5.3 | 5.3 | 5.3 | 202 |
| observation store (bf16), reference | 0.3 | 0.3 | 0.3 | 202 |
| observation store (bf16), fast | 0.31 | 0.31 | 0.31 | 202 |
| finished-game bookkeeping, reference | 0.87 | 0.87 | 0.87 | 202 |
| finished-game bookkeeping, fast | 1.3 | 1.3 | 1.3 | 202 |
| Python remainder, reference | 0.97 | 0.97 | 0.97 | 202 |
| Python remainder, fast | 1.03 | 1.03 | 1.03 | 202 |
The profile
One collection step moves 1,024 environments forward once. The stage table below times one iteration (202 steps) of each path with synchronize brackets around every stage; the full table with the Rust calls and the host-device copies separated is in the run record.
| Stage | Reference ms/step | Fast ms/step |
|---|---|---|
| move network forward | 215.27 (95.7%) | 87.60 (88.8%) |
| sampling and log-probability | 3.06 | 3.15 |
| environment calls and their copies | 4.42 | 5.30 |
| observation store, bf16 | 0.30 | 0.31 |
| finished-game bookkeeping | 0.87 | 1.30 |
| Python remainder | 0.97 | 1.03 |
| total (env steps/s) | 224.89 (4,553) | 98.69 (10,376) |
The hypothesis named one stage above 40% of wall time; the forward pass sits at 96%. Inside it, the profiler shows the math attention fallback costing 136 ms per step on its own: the Radeon 8060S gates its fused attention kernels behind an experimental flag, and the default path materialises the 1024 by 6 by 93 by 93 attention matrices. With the flag set, the same attention measures 1.0 ms per call against 19.2 ms, and the environment, the sampling over 10,000 actions and every copy turn out to be rounding errors next to the network.
What was measured and rejected
The brief listed six candidate fixes; measurement kept one.
- torch.compile of the forward reached 60.9 ms against 86.1 ms eager with the flag, a further 1.42x, with a 36.6 s cold compile. It was rejected on correctness grounds: the compiled graph returns NaN rows for scattered batch entries on real data, on 44 to 99 of 202 steps across every attention backend, both input dtypes, an encoder-only graph and with split reductions disabled, while the eager network is clean on every batch tested. A per-step NaN guard with eager fallback caught every case, but a quarter of the steps then paid for two forwards and the win was gone. This is a bug in this inductor build for gfx1151, recorded here so the next attempt starts from evidence.
- Gumbel-max sampling on the device measured 2.9 ms per step against 2.8 ms for multinomial once both run eagerly; the fusion that would have made it faster needs torch.compile. The loop keeps multinomial.
- More environments do not help: the compiled forward served 16.8k env forwards per second at batch 1,024, 15.8k at 2,048 and 13.4k at 4,096. The GPU is saturated at the preset's batch size.
- Pinned-memory copies, uint8 planes and Rust thread tuning target stages that together cost 4.4 ms of a 225 ms step.
The curves match
Ten iterations from seed 0 on both paths, with seed 1 on the old path as the noise yardstick. The plot shows collection speed; the table shows the largest per-iteration difference between the fast run and the reference run against the largest difference between the two reference seeds.
Collection steps per second over ten iterations
- reference, seed 0
- reference, seed 1
- fast, seed 0
| Series | iteration | environment steps per second |
|---|---|---|
| reference, seed 0 | 1 | 3026 |
| reference, seed 0 | 2 | 4352 |
| reference, seed 0 | 3 | 4317 |
| reference, seed 0 | 4 | 4383 |
| reference, seed 0 | 5 | 4373 |
| reference, seed 0 | 6 | 4350 |
| reference, seed 0 | 7 | 4362 |
| reference, seed 0 | 8 | 4377 |
| reference, seed 0 | 9 | 4377 |
| reference, seed 0 | 10 | 4395 |
| reference, seed 1 | 1 | 4535 |
| reference, seed 1 | 2 | 4510 |
| reference, seed 1 | 3 | 4371 |
| reference, seed 1 | 4 | 4368 |
| reference, seed 1 | 5 | 4403 |
| reference, seed 1 | 6 | 4403 |
| reference, seed 1 | 7 | 3620 |
| reference, seed 1 | 8 | 4368 |
| reference, seed 1 | 9 | 4394 |
| reference, seed 1 | 10 | 4437 |
| fast, seed 0 | 1 | 9971 |
| fast, seed 0 | 2 | 10146 |
| fast, seed 0 | 3 | 10252 |
| fast, seed 0 | 4 | 10356 |
| fast, seed 0 | 5 | 10206 |
| fast, seed 0 | 6 | 10176 |
| fast, seed 0 | 7 | 10103 |
| fast, seed 0 | 8 | 9998 |
| fast, seed 0 | 9 | 10039 |
| fast, seed 0 | 10 | 10252 |
| Metric | fast minus reference, seed 0 | seed 1 minus seed 0 | verdict |
|---|---|---|---|
| policy loss | 0.0088 | 0.0103 | within noise |
| entropy | 0.042 | 0.031 | slightly outside |
| kept transitions | 13,991 | 10,333 | outside, early iterations |
| games per iteration | 72 | 165 | within noise |
| mean plies | 18.0 | 87.9 | within noise |
Bit-exact equality was never expected across attention kernels: the worst logit difference between the two backends on real batches is 0.0078, one to two bf16 ulps, but one different sampled action changes the rest of a trajectory. The kept counter is the most sensitive metric because the advantage filter's floor at 0.01 turns small value differences into set-membership differences; the runs rejoin by iteration eight (36,980 kept fast against 39,055 and 36,958 reference). The metrics that carry the learning signal, policy loss, entropy, games and plies, track inside or at the edge of the two-seed envelope, and iteration 1 is indistinguishable (entropy 3.030 on both paths, mean plies 86 on both).
The environment share
The world-model study's premise was that environment steps are nearly free here. Measured: env.step, env.observe and env.legal_mask, including their host-device copies, took 2.0% of collection wall time before the fix and take 5.4% after. The engine alone steps the same 1,024 games at 0.6 to 0.85 million steps per second with no network in the loop, two orders of magnitude above what the training loop asks of it. Imagination cannot pay for itself on this machine; the premise holds with margin.
What remains
The forward pass is still 89% of the step and runs at about 8.5 TFLOP/s of effective bf16 compute, memory-bound on activations. The paths that would go further are the ones the evidence rejected (a working inductor build for this GPU, or hand-fused kernels), or a smaller or cheaper network, which is a learning decision rather than an engineering one. The scaled run should plan on about 10,000 collection steps per second per GPU process, and the update phase now costs roughly as much wall time as collection, so it is the next profiling target.
Reproduction
Registration experiments/5dc645d7-0c8a-4491-9de6-f5180a396310.toml.
Commands, from the research directory with the virtualenv from
just setup:
PYTHONPATH=training .venv/bin/python scripts/profile_collect.py --path reference --out runs/profile-reference.json
PYTHONPATH=training .venv/bin/python scripts/profile_collect.py --path fast --out runs/profile-fast.json
PYTHONPATH=training ARMIES_COLLECT_PATH=reference .venv/bin/python -m armies_train train --preset small --iterations 10 --out runs/throughput-ref-seed0 --seed 0
PYTHONPATH=training .venv/bin/python -m armies_train train --preset small --iterations 10 --out runs/throughput-fast-seed0 --seed 0
python3 scripts/build_throughput_assets.pyEach GPU job ran under flock runs/gpu.lock. The record carries the
SHA-256 of every raw output; the chart assets are built by the script
named above.