OGBench cube MLP world models (diffusion-free control)
Deterministic MLP dynamics models trained on OGBench cube-single and
cube-double. They exist as a control: a diffusion-free baseline that
measures what the denoising process in a diffusion world model actually buys.
They are not proposed as the better model β see the results below.
Each model regresses one step of dynamics,
(state_t, action_t) -> state_{t+1}
as a [256, 256, 256] SiLU MLP over the shared normalized state space. It
predicts the normalized state delta from a zero-initialized output head,
so an untrained model sits at the identity transition. Sampling is a single
forward pass per step, against 20 denoising passes per step for the diffusion
model it is compared with.
Headline result
One-step accuracy does not predict rollout quality. The MLP is better than the diffusion model at one-step prediction on both tasks and worse over a 100-step rollout on both tasks.
Validation normalized MSE after four exhaustive epochs. The diffusion control is the heterogeneous Diffusion Forcing transformer trained on identical data under an identical schedule.
| task | model | one-step NMSE | 100-step rollout NMSE |
|---|---|---|---|
| cube-single | MLP (this repo) | 0.000135 | 0.00735 |
| cube-single | Diffusion Forcing | 0.000553 | 0.00163 |
| cube-double | MLP (this repo) | 0.0000487 | 0.01628 * |
| cube-double | Diffusion Forcing | 0.000657 | 0.00435 |
* best checkpoint, epoch 2. The final epoch reaches 2.82 β see below.
The MLP is 4.1x (cube-single) and 13.5x (cube-double) better at one step, and 4.5x and 3.7x worse over 100 steps.
The failure mode
cube-double shows it sharply. Across four epochs its one-step error falls
monotonically while its rollout error blows up in the final epoch:
| epoch | one-step NMSE | rollout NMSE |
|---|---|---|
| 1 | 0.0000898 | 0.02135 |
| 2 | 0.0000647 | 0.01628 |
| 3 | 0.0000525 | 0.02174 |
| 4 | 0.0000487 | 2.8221 |
A one-step regressor with a small consistent bias compounds that bias over 100 autoregressive steps. The denoising step of a diffusion model projects each predicted state back toward the data manifold; a deterministic MLP has nothing that does this, so it drifts.
Two practical consequences:
- Do not select or early-stop these models on one-step error. It improves while the thing you care about degrades.
- Use
world_model_best.pt, notworld_model_final.pt, forcube-double. Best is selected on rollout NMSE. Forcube-singlethe two are the same checkpoint (epoch 4).
Files
cube-single/world_model_best.pt # epoch 4, rollout NMSE 0.00735
cube-single/world_model_final.pt # identical checkpoint
cube-single/metrics.jsonl # full per-step training + per-epoch validation
cube-double/world_model_best.pt # epoch 2, rollout NMSE 0.01628 <- use this
cube-double/world_model_final.pt # epoch 4, rollout NMSE 2.8221 <- diverged
cube-double/metrics.jsonl
Each .pt is a plain torch.save payload:
| key | contents |
|---|---|
checkpoint_format |
ogbench.deterministic_mlp.export.v1 |
model_config |
the DeterministicMlpDynamicsConfig fields |
model |
EMA weights (147,550 parameters for cube-single, 152,185 for cube-double) |
schema |
canonical state schema |
environment_names, trajectory_metadata, metrics, step |
provenance |
The format string is deliberate: diffusion exports use
ogbench.heterogeneous_df.export.v2, and consumers dispatch on it, so a model
of a different family must not borrow that label.
Inference β read this before evaluating
The network outputs a state delta that is already in normalized space. Add it to the normalized current state, then unnormalize. Getting this wrong does not fail loudly; it silently produces errors 1,500x too large.
import torch
p = torch.load("cube-single/world_model_best.pt", map_location="cpu")
assert p["checkpoint_format"] == "ogbench.deterministic_mlp.export.v1"
w = p["model"]
s_off, s_scale = w["state_normalizer.offset"], w["state_normalizer.scale"]
a_off, a_scale = w["action_normalizer.offset"], w["action_normalizer.scale"]
net = torch.nn.Sequential( # layers 0/2/4/6 are Linear
torch.nn.Linear(33, 256), torch.nn.SiLU(),
torch.nn.Linear(256, 256), torch.nn.SiLU(),
torch.nn.Linear(256, 256), torch.nn.SiLU(),
torch.nn.Linear(256, 28),
)
net.load_state_dict({k[len("network."):]: v
for k, v in w.items() if k.startswith("network.")})
net.eval()
def step(state, action): # both raw, batch-first
x = (state - s_off) / s_scale # normalize state
u = (action - a_off) / a_scale # normalize action
delta = net(torch.cat([x, u], dim=-1)) # ALREADY normalized-space
return (x + delta) * s_scale + s_off # add, then unnormalize
Three ways to get this wrong, with the error each produces on
cube-single-play-v0-val (all 100,000 in-episode transitions):
| interpretation | normalized one-step MSE |
|---|---|
x + delta, then unnormalize (correct) |
0.000171 |
| treat output as the absolute normalized next state | 0.256 |
| normalize the output again before adding | 983 |
add output to the raw state (normalize(s + out)) |
0.0814 |
The second row is the common one: because the output head is zero-initialized
and deltas are small (mean absolute value 0.0272, matching the true mean
absolute normalized step of 0.0272), reading the output as an absolute state
predicts approximately the midpoint of the normalization range, giving an
error of about mean(x^2) = 0.256.
Equivalently, load the weights into DeterministicMlpDynamicsModel and call
rollout, which returns raw (unnormalized) future states and ignores the
diffusion noise arguments it accepts for interface parity.
Sanity check
Against the trivial "predict no change" baseline on the same 100,000 transitions, under the metric's own normalization:
| normalized one-step MSE | |
|---|---|
| predict no change | 0.00545 |
| this model | 0.000171 |
The model is 31.9x better than predicting no change. If you measure otherwise, the inference convention above is the first thing to check.
Note the 0.000171 here is over every validation transition, while the 0.000135 in the table above is what training logged from its sampled validation batches (4 batches of 8 windows). The all-transitions figure is the more reliable estimate.
What the normalization is
normalize(v) = (v - offset) / scale, with
offset = (min + max) / 2
scale = max(max - min, 1e-2) / 2
where min/max are per-dimension min and max over the training split.
It is a half-range, not a standard deviation and not a delta scale. On
cube-single the mean per-dimension scale is 1.53 against a mean per-dimension
std of 0.595, so (scale/std)^2 averages about 52 β an NMSE quoted against
std is roughly 52x larger than one quoted against this normalizer.
Training
| data | OGBench cube-single-play-v0.npz / cube-double-play-v0.npz and their -val.npz splits, read directly |
| observation construction | none β the state is the raw observations row, unmodified and unreordered (28 dims cube-single, 37 cube-double). No goal conditioning, no reordering, no derived features. actions and terminals are the only other fields read. |
| schedule | 4 exhaustive epochs, every window seen exactly once per epoch |
| updates | 56,316 (identical to the diffusion control) |
| global batch | 64, one rank |
| window | context 1, prediction horizon 100, stride 1 |
| optimizer | AdamW, lr 1e-4, weight decay 1e-5, grad clip 10.0, EMA 0.999 |
| precision | bfloat16 autocast |
| hardware | one NVIDIA H200 |
| state / action dims | cube-single 28/5, cube-double 37/5 |
Every element of the protocol other than the model is shared with the diffusion control, and both see the same windows for the same number of gradient updates, so the comparison isolates the model.
Training cost is the other half of the comparison: this model sustains roughly 300 updates/second against the diffusion model's 0.33 seconds per update β about two orders of magnitude cheaper per update, minutes rather than hours for the full schedule.
Caveats
- Single seed per task. The
cube-doubledivergence is one run; treat its magnitude as illustrative rather than precisely estimated. - Validation metrics are dataset-based rollout error, not task success. No downstream policy-improvement results are included here.
- Trained only on
cube-singleandcube-double.