WM-Audit is a preregistered, single-machine CPU audit suite that disentangles world-model perceptual fidelity (next-frame MSE / SSIM / NLL, plus fixed-filter stand-ins† for LPIPS / rollout FVD) from control utility (does the model let a planner actually solve the task?).
Everyone is building bigger world models; almost no one is checking whether the ones we have actually control. We measured it.
World models are trained, selected, and reported on pixel fidelity (MSE / NLL / FVD). But planners consume counterfactual action effects and reward predictions. Do the diagnostics we report predict closed-loop control?
We run a battery of diagnostics on each world model, measure the real closed-loop return, then correlate every diagnostic with the return across seed/capacity — with percentile-bootstrap 95% CIs and Holm-corrected p-values, all preregistered before seeing data.
| Diagnostic | What it tests | |
|---|---|---|
| Perceptual | pixel_mse, pixel_ssim, pixel_nll, pixel_lpips†, rollout_fvd† |
next-frame / rollout image fidelity |
| Control | return_ece, counterfactual_tau |
imagined-vs-real return calibration; (s,a1,a2) action ordering |
| Uncertainty | uncertainty_rho, uncertainty_ece |
does ensemble dispersion track the true error? |
†
pixel_lpips/rollout_fvdare fixed-filter stand-ins, not learned perceptual backbones. They are computed by a deterministic, dependency-free bank of seeded random convolution filters (wm_audit.diagnostics.perceptual.FixedFilterFeatures) standing in for the pretrained VGG/AlexNet trunk behind real LPIPS and the feature distribution behind real FVD. The numbers are real and reproducible (no mock), but they are not publication-grade learned LPIPS/FVD — do not over-read them as such. Swap in the reallpipstrunk for main-result numbers.
counterfactual_tau is operationalized as the global pooled Kendall-τb: at
each sampled state we step two candidate actions in the oracle env and through
the model (one-step rewards), then pool all (state, action) triples across
states into one two-sample series and compute a single tie-aware Kendall-τb
between oracle and imagined rewards. It is not a per-state sign-agreement
rate / fraction of states where the two actions are ordered correctly.
Ensemble convention (consistent). The pixel and uncertainty diagnostics
already averaged per-member values across the ensemble. The control battery
(return_ece, counterfactual_tau) is now on the same footing: each is computed
per member on the identical segments/states and averaged across members, rather
than silently using only the lead member.
Which rows enter which test. H1/H2 ask which diagnostics predict closed-loop
return across model capacity, so their oriented-rank composites and every
Spearman correlation are computed on the float baseline rows only
([r for r in rows if not r['compression']]). Compression-tagged rows (e.g.
INT8) are the same already-trained ensemble under a deployment perturbation — a
near-duplicate of a float baseline, not a new capacity tier — so they belong only
to H3. They are still shown in the leaderboard Rows table, but never enter the
H1/H2 correlation pool. H3 pairs each INT8 row with its float baseline on the
exact (hidden, embed, seed) triple, and measures degradation on a fixed
reference ladder locked to the float baseline set: each compressed row's
diagnostic values are inserted into the fixed float reference distribution
(competition/insertion rank), so adding or removing another compression lever
cannot shift a shared pair's degradation.
Modeling scope: recurrent state is reset every batch (no learned temporal
dependency). During training (wm_audit/train/train_wm.py) the GRU hidden
state is re-initialized to zeros at every training step
(h = model.init_state(batch_size)); transitions are never unrolled into
sequences, so the GRU's recurrent carry-over is never trained. Despite the
RSSM/GRU wiring, the stand-in model is effectively a per-transition feedforward
mapping. This pilot's H1/H2/H3 conclusions describe only this single-step
stand-in pipeline and must NOT be extrapolated to a genuine recurrent world
model with learned temporal dynamics.
pixel_nll uses one scalar global logvar (weak approximation). The
probabilistic head (wm_audit/models/dreamer_lite/rssm.py) keeps a single
shared logvar parameter for every pixel instead of a per-pixel/per-channel
variance head; gaussian_nll therefore reports a homoscedastic, all-pixels-
shared NLL. It is a coarse fidelity signal for cross-config comparison, not a
full heteroscedastic per-pixel likelihood.
- H1 — pixel fidelity weakly predicts closed-loop return:
|ρ(pixel, R_real)| < 0.4. (A falsification is still published as a bounded result.) - H2 — control-relevant diagnostics strongly predict return:
ρ > 0.6. - H3 — compression (INT8 / prune / distill) degrades calibration and uncertainty alignment faster than raw pixel fidelity.
- ✅ Implemented and CI-tested: stats core (Spearman / Kendall / bootstrap / Holm), the diagnostic battery, a deterministic built-in env, replay buffer, a tiny CPU RSSM, CEM-MPC close-loop, and INT8 quantization parity.
- 🧪 Three runs executed: a CPU reduced pilot (2026-10-13), a locked-scale
pixel_navMAIN (2026-11-07), and the locked-scaledm_control.cartpole_swingupMAIN (2026-11-13). The second MAIN executes the preregistered 200k train steps × 100k collect steps × 5 seeds × 3 capacity tiers × INT8 lever (bootstrap=2000) as five seed shards on GitHub-hostedubuntu-latestrunners (MuJoCo 4.0.1 viadm_control,MUJOCO_GL=osmesa, no GPU), n=15 float rows. The locked 2-of-2 2-env MAIN is therefore executed, not projected.pixel_navverdicts: H1 threshold met — weak / tentative only, |ρ(pixel_composite,R)|=0.021<0.4 on n=15, 95% CI [−0.540, 0.622], p_raw=0.939, Holm p=1.000 → not statistically significant, NOT an established finding; H2 contradicted (ρ(control_composite,R)=−0.102); H3 reverses (INT8 pixel degradation 0.780 > control 0.422, n_pairs=15).cartpole_swingupverdicts: H1 rule met again at ρ=+0.130, CI [−0.477, +0.685], p_raw=0.645 (equally uninformative); H2 contradicted again at ρ=+0.301 vs the 0.6 threshold — the strongest single control signal isreturn_eceat −0.525 (raw p=0.044, Holm p=0.443); H3 reverses again (pixel degradation 1.580 > control 1.367). Both environments agree on H1/H2 and both contradict the preregistered H3 direction. ⚠️ Two of the nine diagnostics are undefined incartpole_swingupand are reported asnull, not as numbers.counterfactual_tauis NaN in all 30 rows (one 10 ms step barely moves the pole, so the oracle rewards over 40 random action pairs are numerically identical and the τb denominator vanishes);uncertainty_rhois NaN in 6/30 rows, all at capacity(16,16), where the ensemble collapses onto identical predictions. So H2'scontrol_compositemixes 2 of 3 diagnostics in 12 rows and 1 of 3 in three rows — the H2 number above is a degraded composite, not the strict three-diagnostic one the preregistration names.- 🐞 A NaN-ranking defect in the aggregation was found while reading that data
and fixed before publishing (commit on
ci/cartpole-full):rankdataused to park non-finite values at one end of the ladder, which manufactured ρ=−0.043 for an all-NaN diagnostic, dragged H2 to +0.308, and scored unmeasurable compressed rows as the worst value on the fixed ladder — reporting control degradation 4.311 and making H3 look supported. Correlations now drop non-finite pairs and reportn/n_dropped, composites leave an unmeasurable diagnostic out of that row's mean (composite_coveragein the JSON), and the artifact is written withnullinstead of the RFC-invalidNaNliteral. Pinned by tests intests/test_prereg_decisions.py. The buggy numbers are recorded in the write-up so the size of the artifact is on the record. ⚠️ Outcome resolution, stated numerically: the 15cartpole_swingupfloat rows carry 9 distinctR_realvalues and 2 of 5 seeds show no capacity-dependent variation at all (one configuration returns 158.8 against 4.0–25.3 elsewhere). Rank correlations over that axis have fewer effective levels than rows, which bounds what the H1/H2 intervals can mean. The pipeline emits this asoutcome_resolutionin the JSON and prints a warning when ties dominate.- 🧪 Reduced pilot kept for reproducibility (
leaderboard/results_main.json): envpixel_nav, 3 seeds × 3 capacities (n=9 float rows),train_steps=12000, H1 threshold met at ρ=−0.317 but CI [−0.947, 0.554] crosses zero (p_adj=1.000), H2 not supported (ρ=0.26), H3 not supported (n_pairs=9). - 📄 Write-up:
docs/workshop_null_result.md— a workshop-style report covering both environments (abstract, preregistered design, full 11-statistic tables, the degenerate-diagnostic disclosure, threats to validity, and the exact reproduction commands).
1. The H1 "support" is a threshold-rule label, not an established finding.
Both runs fire the preregistered threshold rule, and in neither case is that
evidence significant. On the locked-scale pixel_nav run (n=15),
|ρ(pixel_composite, R_real)| = 0.021 < 0.4, but the percentile-bootstrap 95% CI
[−0.540, 0.622] crosses zero, p_raw = 0.939, Holm-adj p = 1.000. On the reduced
pilot (n=9) the same rule fires at |ρ| = 0.317 < 0.4 with CI [−0.947, 0.554],
p_raw = 0.406, p_adj = 1.000. At both sample sizes the interval is equally
compatible with a moderate positive and a moderate negative correlation. Treat
this as a tentative / weak exploratory signal — never as "we found that pixel
fidelity predicts control".
2. The diagnostic battery is NOT independent evidence. On the reduced pilot
(n=9) five of the eleven diagnostics — pixel_mse, pixel_nll,
pixel_lpips†, rollout_fvd†, uncertainty_ece — produce identical
Spearman ρ = 0.3167 and identical p_raw = 0.4064 (same CI [−0.554, 0.947]):
their 9-element rank vectors are exactly collinear. On the locked-scale pixel_nav
run (n=15) the collinear group shrinks to two diagnostics (pixel_mse,
pixel_nll, ρ=0.061, p_raw=0.830, CI [−0.562, 0.612]) — the capacity ladder now
separates some metrics — but the battery still carries fewer independent signals
than its 11 rows suggest, and multi-diagnostic agreement must not be counted as
cross-validation. The remaining driver is the stand-in model design (a
scalar-logvar NLL head and a fixed, pre-defined filter bank leave little
independent variation between pixel metrics), not sample size alone.
3. Already-disclosed scopes that further bound every verdict: single-step
feedforward stand-in (GRU state reset every batch — no learned temporal
dynamics); scalar homoscedastic pixel_nll; fixed-filter stand-ins for LPIPS /
FVD (not learned backbones); two small environments only — pixel_nav and
cartpole_swingup. dm_control could not be installed in the original CPU
container (MuJoCo/EGL unavailable there) nor on Windows (labmaze needs
bazel), so the second environment was run on GitHub-hosted runners instead;
that is documented in configs/audit/main_cartpole_full.yaml, and the runner
measurements are in docs/cartpole_runner_probe.md.
The locked-scale pixel_nav run in leaderboard/results_main_pixelnav_200k.json
is CPU, train_steps=200000, 5 seeds (prereg values met, n=15 after INT8
exclusion); the reduced pilot in results_main.json is CPU,
train_steps=12000, 3 seeds. Nothing here is extrapolated to a recurrent world
model or to tasks larger than these two.
The committed numbers in leaderboard/results_main.json come from a CPU-only,
single-machine reduced pilot. They were produced in the exact environment
below; see requirements-lock.txt for the machine-readable pins.
| Component | Pinned version |
|---|---|
| OS | Windows 11 (64-bit, x86-64) |
| Python | CPython 3.13.x (64-bit) |
| torch | 2.14.1+cpu (CUDA disabled; torch.cuda.is_available() == False) |
| numpy | 2.5.4 |
| scipy | 1.18.1 (installed, but not imported by the audit - stats are hand-rolled) |
| pyyaml | 6.0.3 |
To reproduce that environment (this is the CPU-only reproduction path: the whole audit runs on CPU, and the locked torch build has CUDA disabled):
python -m venv .venv
.\.venv\Scripts\activate
pip install -r requirements-lock.txt # exact pins; torch==2.14.1+cpu comes from the PyTorch CPU index
pip install -e .Where the torch wheel comes from.
torch==2.14.1+cpuis a local version that is not published on the default PyPI index; it only exists on the PyTorch CPU index (https://download.pytorch.org/whl/cpu). The lock file itself declares--extra-index-url https://download.pytorch.org/whl/cpuat its top, so the singlepip install -r requirements-lock.txtcommand above automatically fetches the +cpu wheel from that PyTorch CPU index and every other package from PyPI. No extra flags are required - a clean machine running exactly the commands above resolves the whole lock. (Without that in-file index declaration pip fails with "No matching distribution found" for torch, because the +cpu wheel is absent from plain PyPI.)
Honest bit-reproducibility caveat. The rows are seeded and the stats pipeline is deterministic, but torch CPU BLAS kernels, the BLAS vendor, and OS/architecture can still shift the last digits of floating-point results across machines, torch builds, or numpy versions. We therefore do not claim bit-for-bit equality on a different OS / BLAS / torch build. What we do claim is robustness of the conclusions: at n=9 the preregistered verdicts are threshold / sign decisions (|ρ|=0.32<0.4 meets the H1 threshold; ρ=0.26<0.6 rejects H2; INT8 control-deg 0.35 ≤ pixel-deg 0.43 rejects H3), and none of these flip on sub-permille float reordering. Note that "meets the H1 threshold" is a mechanical label only — it carries no statistical significance at n=9 (CI crosses zero, Holm-adj p=1.0; see Limitations). The lock pins the exact tested build so a re-run in this environment matches the printed precision.
python -m venv .venv
.\.venv\Scripts\activate
pip install -r requirements-lock.txt # exact CPU pins; torch +cpu wheel via the in-lock PyTorch CPU index (see Environment above)
pip install -e ".[dev]"
pytest # 78 tests + CI smoke (must stay green)
python -m experiments.run_audit # == --mode smoke: n=6 CI placeholder (CPU, ~6-7 min)
# default output: gitignored .tmp/results_smoke.json;
# it NEVER overwrites the tracked leaderboard/results_smoke.json.
# This is NOT the committed n=9 pilot - see
# "Reproducing the n=9 reduced pilot" below.
python experiments\make_leaderboard.py # render leaderboard/LEADERBOARD.mdA bare python -m experiments.run_audit (equivalent to --mode smoke, n=6)
writes to the gitignored .tmp/ scratch path by default (see
experiments/run_audit.py); the committed leaderboard/results_smoke.json is
preserved history and is only rewritten when you pass an explicit --out.
There are two distinct modes - do not confuse them:
| Mode | What it is | n (float rows into H1/H2) | Seeds | train_steps | bootstrap |
|---|---|---|---|---|---|
--mode smoke |
CI / quick sanity placeholder | n=6 | 2 | 150 | 200 |
--mode main |
the committed reduced pilot (leaderboard/results_main.json) |
n=9 | 3 | 12000 | 2000 |
--mode main + --config configs/audit/main_pixel_nav_full.yaml |
locked-scale pixel_nav MAIN (leaderboard/results_main_pixelnav_200k.json); run with --jobs 4 |
n=15 | 5 | 200000 | 2000 |
same flags + --config configs/audit/main_cartpole_full.yaml |
locked-scale cartpole_swingup MAIN (leaderboard/results_main_cartpole_200k.json); five seed shards on a runner, merged with --resume |
n=15 | 5 | 200000 | 2000 |
To regenerate the reduced pilot numbers (CPU only):
# from the repo root, with the locked venv activated:
python -m experiments.run_audit --mode main --out leaderboard/results_main.json
python experiments\make_leaderboard.py # re-render leaderboard/LEADERBOARD.md from the JSONTo regenerate the locked-scale pixel_nav MAIN (Linux CPU, ~2 h on 4 cores):
python -m experiments.run_audit --mode main \
--config configs/audit/main_pixel_nav_full.yaml --jobs 4 \
--out leaderboard/results_main_pixelnav_200k.json
python experiments/make_leaderboard.py--jobs N runs the (seed, capacity) grid on N worker processes, each with
torch pinned to one thread. Row order is preserved, so the grid and the
correlation block are the same either way — but the per-row floats are not
bit-identical: thread count changes floating-point accumulation order, and a
serial-versus-parallel probe on the smoke grid measured a worst relative
difference of ~1.5e-7 across the diagnostics. Closed-loop R_real happened to
come out bit-identical on that grid because the CEM argmax never moved; a
different grid could flip an argmax and move it further. tests/test_parallel_consistency.py
pins both halves of this: R_real must match exactly, every other diagnostic
within 1e-5 relative, and the resulting rho within 1e-9.
- Wall-clock (pilot, estimate): no timer was logged during the
2026-10-13 pilot. From the recorded CPU throughput of ~236 member-steps/s and
the pilot's 9 configs × 3 ensemble members × 12000 train steps (~324k
member-steps) plus data collection, CEM closed-loop rollouts, and a
2000-resample bootstrap, expect on the order of ~25-35 minutes on a
comparable CPU. (For reference,
--mode smokemeasured ~396 s / ~6.6 min on this machine.) Treat both numbers as estimates, not guarantees. - Wall-clock (
pixel_navmain, measured): the locked-scale run (15 configs, 4 worker processes) wrote its artifact roughly ~2 h after launch on the 4-core container (workspace clone at 03:06 → results file at 05:07; the container rotated the process log, so the exact launch timestamp is unavailable). The serial equivalent is roughly 15-18 h, which is why--jobs 4exists. A single-config probe (10k collect, 5k train) took 149 s. - Expected output (
leaderboard/results_main.json, pilot): 18 rows = 9 float baselines + 9 INT8 pairs across 3 seeds × 3 capacities; the H1/H2 correlations are computed on the n=9 float rows only (INT8 rows enter H3 only). Verdicts - H1 threshold met: weak / tentative exploratory signal only (|ρ(pixel_composite, R_real)| = 0.317 < 0.4, but n=9, CI [−0.947, 0.554] crosses zero, p_raw=0.406, Holm-adj p=1.000 → not significant), H2 not supported (ρ(control_composite, R_real) = 0.259 < 0.6), H3 not supported (INT8 control-composite degradation 0.352 ≤ pixel-composite degradation 0.433, n_pairs=9). Re-running inside the locked environment reproduces these values to printed precision; see the bit-reproducibility caveat under Environment. - Expected output (
results_main_pixelnav_200k.json, main subset): 30 rows = 15 float + 15 INT8 pairs across 5 seeds × 3 capacities; H1/H2 on the n=15 float rows only. Verdicts - H1 threshold met but weak (ρ=0.021, CI [−0.540, 0.622], p_adj=1.000), H2 contradicted (ρ(control_composite) = −0.102), H3 reverses (pixel-composite degradation 0.780 > control-composite degradation 0.422, n_pairs=15).
The built-in pixel_nav env makes the whole chain run on CPU without
dm_control. Heavy envs (dm_control.cartpole_swingup, Atari) live behind
optional extras.
configs/ preregistration.yaml + env / wm / audit configs
wm_audit/
stats/ Spearman, Kendall tau-b, bootstrap CI, Holm (numpy, tested)
diagnostics/ perceptual / control / uncertainty / fidelity_utility
envs/ unified reset/step interface; built-in pixel_nav (+ optional dm_control)
data/ replay buffer + intervention perturbations
models/ dreamer_lite RSSM (CPU)
train/ collection + ensemble training
policy/ CEM-MPC close-loop
deploy/ INT8 quantization parity (RQ3 axis)
experiments/ run_audit.py (entry), make_leaderboard.py
notebooks/ 10min_audit.ipynb
tests/ unit + smoke (CI must pass)
leaderboard/ placeholder (LEADERBOARD.md)
Two tests are deliberately wired so that a broken implementation turns them red:
test_calibration.py::test_offset_increases_ece_monotonically (ECE must grow
with systematic offset) and test_spearman_bootstrap.py::test_bootstrap_ci_covers_known_truth.
We verified both go red when the underlying math is broken.
WM-Audit is a measurement layer on top of the following world models and
recent audit-adjacent work. (IDs verified against arXiv abstract pages; the
numbers in this repo come from three locked/reduced runs — the 2026-10-13 CPU
reduced pilot, the 2026-11-07 locked-scale pixel_nav main, and the 2026-11-13
locked-scale cartpole_swingup main, both at 200k steps × 5 seeds × 3
capacities — plus a downgraded CI smoke placeholder. cartpole_swingup is the
second of the two preregistered environments; it ran on GitHub-hosted runners
because dm_control was not installable in the CPUs this repo targets.)
- DreamerV3 — D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap. Mastering Diverse Domains through World Models. arXiv:2301.04104.
- TD-MPC2 — N. Hansen, H. Su, X. Wang. TD-MPC2: Scalable, Robust World Models for Continuous Control. arXiv:2310.16828.
- Validate the Dream — C. Oefinger et al. Validate the Dream Before You Trust Its Verdict: Admissibility for World-Model Simulators. arXiv:2607.07196. (Venue/workshop affiliation not independently verified; cited by arXiv ID only.)
- Biased Dreams — J. Berger, B. Frauenknecht, S. Trimpe, B. Leibe. Biased Dreams: Limitations to Epistemic Uncertainty Quantification in Latent Dynamics Models. arXiv:2604.25416. (Venue affiliation not independently verified; cited by arXiv ID only.)
- stable-worldmodel — L. Maes et al., incl. Y. LeCun, R. Balestriero. stable-worldmodel: A Platform for Reproducible World Modeling Research and Evaluation. arXiv:2605.21800.
MIT — see LICENSE.
