Pre-registration: parallel sim rollouts — N env workers, one batched policy
Registered 2026-08-12 10:1xZ (work session; real date -u at
write: 10:10). Owner-approved 08:44Z, owner-sequenced 09:32Z: “Once
I relinquish the GPU, remember to do sim-parallel-rollouts before
any other experiments.” The CPU scaffold landed this session
(commit 1e4e16f, check.py 710 green); this note registers the GPU
leg that runs FIRST on release — an infrastructure oracle, not a
behavioral experiment.
Plain words
Evaluating a robot policy in our simulator is slow because we run one episode at a time: the big GPU does a tiny bit of thinking per step and then sits idle while the simulator draws the next pictures. The fix is standard: run many simulator copies at once in separate worker processes and have them share the one policy, which thinks about all of their pictures in a single batch. A 100-episode evaluation should drop from ~1.5 hours to ~20 minutes. The catch: our evaluations are only trustworthy because every episode is exactly reproducible. So before this faster path is allowed to produce any registered number, it must prove — on real episodes — that it gives bit-for-bit the same rows as the slow path it replaces. That proof is what this note registers.
What landed (CPU, commit 1e4e16f)
sim/rollout_sim_parallel.py: spawn worker processes each own a fullSO101Sim(own EGL context — MuJoCo’s EGL display is per-process global state); the parent holds the single checkpoint copy and serves batchedpolicy.predictcalls.- Deterministic lockstep rounds: each round collects exactly one
predict request per still-active worker in worker-index order,
answers all with one batched forward. Batch membership is a pure
function of (seed partition, worker count, policy outputs) — never
of wall-clock timing. Seeds partition round-robin
(
seeds[w::N]). - Noise is untouched by construction: rows carry the sequential
driver’s identity triple (
repo_id="sim/eval100",episode_index=seed,frame_index=replan), and stable-key flow noise is invariant to batch composition (oracle-pinned intests/test_stable_noise.py). - Both drivers now share one episode loop (
run_episode_loop), and 5 CPU-tier oracles (tests/test_sim_parallel_rollouts.py) pin harness equivalence with a fake sim whose dynamics depend on the commanded actions: rows bit-equal to the sequential loop’s (minuslatency_ms), per-seed predict counts, lockstep batch trace, hold-arm worker-local path, error propagation.
The open question the GPU smoke answers
Batching N observations changes the GEMM shapes inside the decode;
floating-point reduction order may move with them, and heun-10
feeds any last-bit drift back through chaotic contact physics —
so batched-vs-batch-1 bit-identity is an empirical question, not a
provable one. It is exactly what
fontaine/scripts/sim_parallel_oracle.py measures.
Registered protocol (on GPU release, before any other experiment)
- Arm:
er60k(er_60k/step_060000, heun-10, bf16, policy seed 0) — the banked reference decode. Seeds 0–5, replans 30 × horizon 30,post_backendauto (torch compositor) on both paths. - Run 1 (oracle at 2 workers):
sim_parallel_oracle.py --num-seeds 6 --workers 2— sequential driver, then parallel, same seeds; compare every row field exceptlatency_ms. - Run 2 (oracle at the target worker count): same at
--workers 8. GREEN is only transferable to a registered eval run at the SAME worker count and decode settings; re-run the oracle when either changes. - Decision rule (frozen):
- GREEN both runs (all fields bit-identical): the parallel path may substitute for the sequential driver in registered evals at the validated settings. The sim100 rerun (still owner-hold) would use it.
- FAIL: the parallel path produces NO registered numbers as-is.
Fallback (registered now): it may be used for arm-vs-arm
paired comparisons only if every arm AND every baseline row in
that comparison is regenerated under the identical parallel
schedule (same worker count — no mixing with banked sequential
rows), via a per-use amendment reporting the observed divergence
(the oracle prints per-field max |diff|). Diagnosis knob:
re-run at
--post-backend numpyto separate compositor-context effects from decode-batching effects.
- Throughput read (record-only, only if GREEN): one 20-seed er60k arm at 8 workers — prices the rerun afternoon (target from the queue item: 100-seed arm in ~20–30 min vs ~1.5 h sequential; spot20 measured ~5.4 min/episode render-bound at 3-process contention, ~94 ms/tick compositor solo).
- Budget: worker VRAM ≈ 0.5–1 GiB CUDA context each at
post_backendauto (fine next to the policy on 80 GiB); 26 CPU cores bound the useful worker count well above 8. Gate ≤ 1 GPU-h for oracle runs + throughput read combined. - Launch mechanics:
run_detached.shunit + babysit registry entry at launch, per the charter; results post same session.
No behavioral claims ride on this. Its deliverable is a yes/no gate fact (may the fast path produce registered numbers?) plus a measured throughput number that prices every sim eval after it.