ReMDM Planner β Craftax Classic artefacts
Anonymous release accompanying Return-Weighted ELBO Fine-Tuning Degrades Masked Diffusion Planners (under double-blind review). It contains the code, the trained checkpoints and the pre-computed results behind every Craftax Classic number in the paper.
Anonymity notice. This repository is an anonymised mirror prepared for
double-blind review. Author names, institutional affiliations, experiment-tracking
identifiers and absolute filesystem paths have been removed or replaced with
placeholders. Cluster hardware is referred to only as GPU-24GB and GPU-H200.
What is here
src/ Craftax_Baselines/ configs/ experiments/ scripts/ tests/
main.py pyproject.toml uv.lock Dockerfile LICENSE demo_craftax.ipynb
checkpoints/
offline/Craftax-Classic-Symbolic-v1-Offline-Diffusion-BC-100M/ Offline BC planner
online/Craftax-Classic-Symbolic-v1-Online-Diffusion-DAgger-100M/ DAgger planner (the
checkpoint every
ablation fine-tunes)
ppo_agents/ PPO-RNN experts
experiments/rl_finetuning/outputs/craftax_classic_ablations/ The published 25-condition suite
figures/ 113 pre-computed PNGs
tables/ main_results.csv, group_summary.csv, hypothesis_verdict.csv,
achievement_summary.csv, gradient_analysis.csv, significance_test.txt,
gdelta.csv, results.tex, ...
gdelta/ gdelta_{seed0,seed1,seed2,aggregate}.json <- the return-term measurement
results.json, diagnosis.md
experiments/rl_finetuning/outputs/review_*/ Five review arms, outside the 25-condition suite
review_anchor_baseline_rl/ same-stack anchor for the four below
review_run1_bc_all/ unweighted control (every weight fixed at one)
review_run2_advclip_lr_matched/ advantage_clip at a matched effective step
review_run4_baseline_lr1e-4/ learning-rate sweep
review_run4_baseline_lr1e-5/
results/inference/ Single-seed evaluations through the inpainting sampler (see below),
plus PPO-RNN expert evaluations
results/paper_figures/ The 11 manuscript figures as vector PDF
Headline numbers (Craftax Classic)
| Score | |
|---|---|
| Pretrained (DAgger) checkpoint | 11.81 |
| Baseline return-weighted ELBO fine-tuning, 500 iters | 8.22 Β± 0.14 |
| Best of 25 conditions (LoRA) | 11.63 Β± 0.03 |
| Worst of 25 conditions (normalised advantages) | 3.73 Β± 0.52 |
No condition of the 25 recovers the checkpoint it started from. Group means are 9.74 (A), 7.07 (B), 10.53 (C) and 7.97 (D).
The review arms
These are separate runs on a different host and are quoted against each other, not
against the table above: the suite does not reproduce across hosts, and the same
checkpoint and configuration score the pretrained planner at 11.9754 here against
11.8084 above, a consistent offset of 1.41%. baseline_rl was therefore re-run as a
same-stack anchor.
| Arm | lr | Score |
|---|---|---|
baseline_rl β same-stack anchor |
3e-4 | 8.3395 Β± 0.1468 |
bc_all β uniform weights, all rollout windows |
3e-4 | 4.7348 Β± 0.1869 |
advantage_clip β matched effective step |
3.223e-4 | 4.9246 Β± 0.2573 |
baseline_rl |
1e-4 | 10.1486 Β± 0.1811 |
baseline_rl |
1e-5 | 10.8104 Β± 0.1024 |
Removing the return weighting while keeping the rollouts costs more than the weighting itself (β3.60 against the anchor). Lowering the learning rate by a factor of thirty recovers 68% of the degradation without removing it β 10.8104 still sits 1.1650 below the same-stack checkpoint, more than ten times the seed sd at that rate. Matching the effective step does not close the advantage-clipping gap.
The MiniHack counterparts of all three arms were run and are reported in the paper, but their run directories are not part of either release β only the Craftax Classic ones are published here. The MiniHack numbers are quoted in that repo's notebook.
The return term
experiments/rl_finetuning/outputs/craftax_classic_ablations/gdelta/ holds the direct
measurement of the return term g_Ξ΄ at the pretrained checkpoint, over 3 rollout seeds
Γ 8 noise draws on 1,024-window batches. Reproduce it on a CPU with
run_ablations.py --measure-gdelta. The Eq.-4 correctness residual is 4.8 Γ 10β»β·.
Two evaluation paths β read this before comparing numbers
Every number in the paper comes from the ablation harness
(experiments/rl_finetuning/ablations/training.py::build_eval_fn):
sample_plan with no locked prefix, 50 denoising steps, 8 actions executed per
plan, scored as returned_episode_returns.
main.py --mode inference can also take a different path,
sample_plan_inpainting, which freezes every executed action as an inpainting prefix
and replans every step. It is a different planner at evaluation time and scores far
lower on the same weights (3.26 against 11.81 for the DAgger checkpoint). The
single-seed JSONs under results/inference/ were produced by that path and are kept as
an ablation on the planning-as-inpainting design choice β they are not paper numbers.
configs/defaults.yaml now sets inference_sampler: sample_plan, so the two paths
agree by default.
demo_craftax.ipynb evaluates through the harness path (Cell 5) and runs the
inpainting path only as a clearly labelled ablation (Cell 5b).
Quick start
from huggingface_hub import snapshot_download
path = snapshot_download(repo_id="AnonMLuser/remdm-planner-craftax", local_dir="remdm-planner-craftax")
Then open demo_craftax.ipynb, or see the project documentation below.
Citation
@inproceedings{anonymous2026returnweighted,
title = {Return-Weighted ELBO Fine-Tuning Degrades Masked Diffusion Planners},
author = {Anonymous},
booktitle = {Submitted to NeurIPS 2026},
year = {2026},
note = {Under double-blind review}
}
ReMDM Planner for Craftax
JAX implementation of ReMDM (Remasking Discrete Diffusion Model) for action-sequence planning in Craftax, a JAX-accelerated, procedurally generated open-world survival game. A bidirectional transformer generates plan_horizon-length action plans by iteratively denoising masked token sequences, conditioned on the current symbolic observation. Trained under a pre-trained PPO expert, either offline (behavioural cloning on live rollouts) or online (DAgger).
The sibling repository remdm-planner-minihack implements the same method in PyTorch on MiniHack. Both repos share the same CLI, config layout and README structure; commands transfer between them by swapping the repo name and benchmark-specific values.
Method
The planner starts from a fully-masked action sequence and iteratively unmasks tokens over T denoising steps; ReMDM extends MDLM with remasking strategies that let committed tokens be re-predicted, improving plan coherence.
Two independent training pipelines are compared head-to-head, both fed by one pre-trained PPO expert checkpoint: --mode offline behaviour-clones from live expert rollouts, --mode online runs DAgger from scratch against expert labels. Either output is scored with --mode inference.
Setup
Prerequisites: Python 3.12+, uv. Linux GPU use needs NVIDIA driver >= 580 for CUDA 13, or >= 525 with --extra cuda12. CUDA and cuDNN come from the pip wheels, so no OS-level toolkit is required; if module load cuda/13.x is in your shell profile, unset LD_LIBRARY_PATH, which otherwise shadows the wheel libraries.
git clone https://github.com/ANONYMOUS/remdm-planners.git
cd remdm-planner-craftax
# Or, if already cloned without submodules:
git submodule update --init --recursive
# Default: CPU-only JAX (macOS, or Linux without a GPU).
# Installs the dev group (pytest) too.
uv sync
# Linux GPU, CUDA 13 (driver >= 580)
uv sync --extra cuda13
# Linux GPU, CUDA 12 fallback (driver >= 525, or Maxwell/Pascal cards)
uv sync --extra cuda12
Extras: cuda13 and cuda12 are mutually exclusive and Linux-only. JAX ships GPU support only through these extras, so a GPU node needs one explicitly.
Repo layout
remdm-planner-craftax/
βββ Craftax_Baselines/ Git submodule β PPO expert training and env wrappers
βββ configs/ Experiment configs (defaults.yaml + presets, see Configuration)
βββ src/ Model, diffusion, planner pipelines
βββ experiments/
β βββ rl_finetuning/ RL fine-tuning ablation suite (run_ablations.py)
βββ scripts/ Param counter, PPO evaluator, paper figures, HF upload, provisioning
βββ tests/ Smoke suite β uv run pytest
βββ checkpoints/ Gitignored β offline/, online/, ppo_agents/ (see Checkpoints)
βββ results/ Gitignored, created on demand β inference/ eval JSONs and
β paper_figures/ manuscript PDFs, both published (see Checkpoints)
βββ demo_craftax.ipynb Demo notebook
βββ main.py CLI entry point
βββ pyproject.toml uv project β deps, cuda12/cuda13 extras, dev group
Quickstart
Full DAgger pipeline (rollout, expert labelling, gradient updates, validation) under configs/smoke.yaml, ~25 s on CPU. The expert is randomly initialised unless --ppo-checkpoint is given, so this runs on a clean clone with no downloads. Watch mean step reward, loss and all metrics finite; returns stay at 0.000, since no episode terminates in so short a run.
python main.py --mode smoke
Training
Two independent training methods; neither depends on the other. An offline BC checkpoint can warm-start DAgger via --checkpoint, but this was not used for the paper results. All training modes need a PPO expert checkpoint.
Stage 1 β Train the PPO expert (submodule)
cd Craftax_Baselines
python ppo_rnn.py --env_name Craftax-Classic-Symbolic-v1 \
--total_timesteps 1000000000 --save_policy --use_wandb
cd ..
(ppo_rnd.py for Random Network Distillation.) Released experts are on the HF Hub, see Checkpoints.
Offline BC
Rolls out the PPO agent live at each update.
python main.py --mode offline --ppo-checkpoint /path/to/ppo_checkpoint
Online DAgger
Trained from scratch. Per iteration a mixed expert/learner policy rolls out, the expert labels every visited state, and the model trains on the aggregated buffer.
python main.py --mode online --ppo-checkpoint /path/to/ppo_checkpoint
# Optional: warm-start from a pre-trained offline checkpoint
python main.py --mode online --ppo-checkpoint /path/to/ppo_checkpoint \
--checkpoint /path/to/offline_checkpoint
With save_policy: true (default) and W&B on, training uploads two artifacts, either consumable via --checkpoint wandb:β¦: {env_name}-policy (final) and {env_name}-policy-best (highest validation return).
Collect trajectories to disk
Rolls out the PPO checkpoint and saves (obs, actions, rewards, dones) as .npz, for inspection; --mode offline does not consume it (it rolls out live).
python main.py --mode collect --ppo-checkpoint /path/to/ppo_checkpoint \
--data data/trajectories.npz \
--override collect_num_steps=1000000 --override collect_num_envs=128
Resuming a training run
# Same shape for --mode online. --resume also accepts a wandb: artifact reference.
python main.py --mode offline --ppo-checkpoint /path/to/ppo_checkpoint \
--resume /path/to/completed_offline_checkpoint \
--override offline_total_timesteps=200000000
The DAgger replay buffer is not persisted; it refills within a few iterations. The cosine LR schedule spans the full num_updates, offset so the LR resumes where it stopped. resume_step and resume_wandb_run_id come from the metadata sidecar; without one, pass --resume-step (or --resume-wandb-run-id) explicitly.
--resume restores the optimiser state, so it needs a checkpoint written by the current AdamW chain; an older one fails loudly and there is no compatibility path. Use --checkpoint instead β parameters only, warm-starting a fresh run.
Evaluation from a checkpoint
python main.py --mode inference --checkpoint /path/to/checkpoint --output results/inference/eval.json
Prints steps per second, per-achievement unlock counts, and two returns that must not be quoted against one another; --output also writes both as JSON:
| Reported | JSON key | Meaning |
|---|---|---|
| Mean return, completed episodes | mean_return_completed_episodes (with n_completed_episodes) |
Mean over every episode that terminated inside the rollout β the returned_episode_returns statistic the ablation tables and the paper report |
| Mean return, first life only | mean_return_first_life, and mean_score for backwards compatibility |
Strict single-life return: the first episode of each env only. A harsher statistic |
By default this replans from scratch every eval_replan (8) steps, conditioned only on the current observation β the same sampler and cadence as build_eval_fn in the ablation harness, so it is the protocol behind the published numbers. Length and width come from eval_steps / eval_num_envs.
--override inference_sampler=inpainting switches to the historical-inpainting sampler, which replans every step with the executed actions locked as a fixed prefix, leaving fewer free positions the further into a window it gets. It is kept as an ablation on the planning-as-inpainting design choice and scores far lower on the same weights; no published number comes from it.
Write eval JSONs into results/inference/ (created for you): scripts/hf_upload.py publishes every JSON it finds there.
Match the config to the checkpoint. The model is built from the config, not the checkpoint, and a mismatch raises at restore. Every released diffusion checkpoint carries the defaults.yaml architecture, so evaluate with the matching final_* config, which also sets the right env_name and recipe values:
python main.py --mode inference \
--config configs/final_craftax_classic_gpu_24gb.yaml \
--checkpoint checkpoints/online/Craftax-Classic-Symbolic-v1-Online-Diffusion-DAgger-100M
Any checkpoint flag (--checkpoint, --ppo-checkpoint, --resume) accepts a W&B artifact reference prefixed wandb:; the artifact downloads automatically (location: wandb_download_dir, default ./artifacts/).
python main.py --mode inference \
--checkpoint wandb:my-team/remdm-planner-craftax/Craftax-Classic-Symbolic-v1-policy:latest
Baselines and ablations
RL baselines
PPO baselines (the expert family: ppo, ppo_rnn, ppo_rnd) train in the Craftax_Baselines submodule, see Training. Evaluate an expert with scripts/eval_ppo_expert.py:
uv run python scripts/eval_ppo_expert.py \
--path checkpoints/ppo_agents/Craftax-Classic-Symbolic-v1-PPO_RNN-1000M \
--env-name Craftax-Classic-Symbolic-v1
Method ablations (named configs)
Each paper experiment is a named config; pass it via --config:
python main.py --mode online --ppo-checkpoint <ppo> --config configs/classic_exp_a_beta_fix.yaml
python main.py --mode online --ppo-checkpoint <ppo> --config configs/classic_exp_b_beta_big_model.yaml
python main.py --mode online --ppo-checkpoint <ppo> --config configs/classic_exp_c_full_recipe.yaml
python main.py --mode online --ppo-checkpoint <ppo> --config configs/classic_exp_d_850K_model.yaml
RL fine-tuning ablation suite
26 registered ablations (same names as in the minihack repo). See experiments/README.md.
python experiments/rl_finetuning/run_ablations.py --list
python experiments/rl_finetuning/run_ablations.py \
--checkpoint $PRETRAINED_CKPT --all
python experiments/rl_finetuning/run_ablations.py \
--checkpoint wandb:my-team/remdm-planner-craftax/Craftax-Classic-Symbolic-v1-policy-best:latest \
--ablations baseline_rl kl_penalty --fast
The same entry point measures the return term of the gradient decomposition at the pretrained checkpoint, with no training and no accelerator:
python experiments/rl_finetuning/run_ablations.py --measure-gdelta --gdelta-seeds 0 1 2 \
--checkpoint $PRETRAINED_CKPT --results-path $RUN/results.json --output-dir $RUN
Configuration
One YAML config holds the experiment; the CLI holds the run.
Precedence, lowest to highest: configs/defaults.yaml < --config preset < --override and run flags. Exactly two config layers β a preset never inherits from another preset.
- Config files (
configs/*.yaml): hyperparameters, model and method settings, ablation definitions. - Run flags:
--seed,--checkpoint,--ppo-checkpoint,--data,--output,--resume*,--jit/--no-jit(disable JIT for debugging). --override KEY=VALUE(repeatable): keys are validated againstdefaults.yamland cast to the key's type, so a typo is an error, not a silent no-op.
defaults.yaml is the final Craftax Classic recipe, not a neutral baseline. Run main.py with no --config and you get the paper's Classic DAgger run: a 384-dim, 6-layer model over 100M env frames.
Presets hold only deltas. A key belongs in a preset only if its value differs from defaults.yaml; restating one silently pins the preset when the recipe later moves. tests/test_config.py enforces this.
Schedule keys are denominated in env frames, not update steps. Six settings β
lr_warmup_frames,offline_total_timesteps,online_total_timesteps,dagger_beta_final,dagger_buffer_cycles,val_interval_framesβ declare the hardware-invariant quantity;resolve_num_updates()andresolve_scaled_hyperparams()derive the update-step forms the runners consume (num_updates,LR_WARMUP_STEPS,DAGGER_BETA_DECAY,DAGGER_BUFFER_MAX,VAL_INTERVAL) from them at load. Set the frame-denominated key; the derived ones are outputs, not inputs.
python main.py --mode offline --ppo-checkpoint <ppo> --no-jit \
--override lr=1e-4 --override plan_horizon=64 --override num_envs=4
| Preset | Purpose |
|---|---|
configs/defaults.yaml |
The final Craftax Classic recipe, and what every other preset layers onto |
configs/smoke.yaml |
--mode smoke overrides (see the sizing invariants commented in the file) |
configs/{classic,craftax}_exp_a_beta_fix.yaml |
DAgger β beta decay fix only (isolates data quality) |
configs/{classic,craftax}_exp_b_beta_big_model.yaml |
DAgger β beta fix + larger transformer |
configs/{classic,craftax}_exp_c_full_recipe.yaml |
DAgger β beta + big model + training dynamics |
configs/classic_exp_d_{100K,250K,850K,3M}_model.yaml |
Craftax Classic model-size scaling sweep |
configs/craftax_exp_d_{500K,1M,3M,7M}_model.yaml |
Full Craftax model-size scaling sweep |
configs/final_craftax_classic_{gpu_h200,gpu_24gb}.yaml |
Final Classic DAgger β num_envs and seed only; the recipe is defaults.yaml |
configs/final_craftax_{gpu_h200,gpu_24gb}.yaml |
Final Full Craftax DAgger β the 8 keys where Full Craftax departs from the Classic recipe, plus num_envs and seed |
Within each family the two machine configs differ only in num_envs and seed, guarded by test_cluster_siblings_differ_only_in_num_envs_and_seed rather than by the loader. A Full Craftax hyperparameter change must be made in both final_craftax_* files β with no inheritance those 8 keys are duplicated verbatim in each; a Classic one belongs in defaults.yaml.
Key hyperparameters are documented inline in configs/defaults.yaml; the appendix tabulates the load-bearing ones. Ablation-suite hyperparameters live in experiments/rl_finetuning/configs/, loaded by run_ablations.py, not main.py.
Checkpoints
With save_policy: true (the default), training saves Orbax checkpoints to policies (final) and policies_best (highest validation return) β under wandb.run.dir with W&B on, uploaded as {env_name}-policy and {env_name}-policy-best, and under {checkpoint_dir}/{mode}/{run_name}/ with W&B off, so a run never discards its weights. Diffusion checkpoints carry a resume_metadata.json sidecar recording the producing run's config, which is what --resume reads; PPO checkpoints carry config.yaml and wandb-summary.json.
Pass the checkpoint directory, not the step subdirectory β CheckpointManager resolves the latest step itself. Offline checkpoints save at the resolved env-frame budget: 99,942,400 for the Classic recipe at 512 envs (1525 updates Γ 512 Γ 128).
checkpoints/ is gitignored; released weights live on the Hub at AnonMLuser/remdm-planner-craftax, mirroring the layout below.
| Checkpoint directory | Environment | Role | Trained for |
|---|---|---|---|
checkpoints/offline/Craftax-Classic-Symbolic-v1-Offline-Diffusion-BC-100M |
Craftax Classic | Offline BC planner | 1e8 env frames |
checkpoints/online/Craftax-Classic-Symbolic-v1-Online-Diffusion-DAgger-100M |
Craftax Classic | Online DAgger planner | 1e8 env frames |
checkpoints/ppo_agents/Craftax-Classic-Symbolic-v1-PPO_RNN-1000M |
Craftax Classic | PPO-RNN expert | 1e9 env frames |
checkpoints/ppo_agents/Craftax-Symbolic-v1-PPO_RNN-1000M |
Full Craftax | PPO-RNN expert | 1e9 env frames |
Full-Craftax diffusion planner checkpoints are not released: no full-Craftax training run has completed; the released Full Craftax expert can still supervise a new run:
python main.py --mode online \
--config configs/final_craftax_gpu_24gb.yaml \
--ppo-checkpoint checkpoints/ppo_agents/Craftax-Symbolic-v1-PPO_RNN-1000M
# All four (~470 MB); narrow the --include glob for a single checkpoint.
uv run hf download AnonMLuser/remdm-planner-craftax --include "checkpoints/**" --local-dir .
Keep the --include. The Hub repo carries its own README.md (the generated model card), LICENSE and .gitattributes; dropping the glob and pulling into --local-dir . overwrites this repository's copies of all three. To fetch everything, add --exclude "README.md" "LICENSE" ".gitattributes", or use a separate --local-dir. Publishing is safe either way β hf_upload.py stages LICENSE and the demo README.md from git, not the working tree.
Experiment outputs
Ablation figures, tables and results.json are regenerated output, so
experiments/rl_finetuning/outputs/ and results/inference/ are gitignored. Obtain
them either way:
# Fetch the published run (figures, tables, results.json, diagnosis.md)
uv run hf download AnonMLuser/remdm-planner-craftax \
--include "experiments/rl_finetuning/outputs/**" --local-dir .
# Or regenerate from a checkpoint; writes to outputs/{run_id}/
python experiments/rl_finetuning/run_ablations.py --checkpoint $PRETRAINED_CKPT --all
scripts/hf_upload_demo.py reads outputs/craftax_classic_ablations/{figures,tables}
from the working copy, so fetch or regenerate first. demo_craftax.ipynb needs no local
copy β it reads them through its own snapshot_download.
Paper figures
Each manuscript figure puts Craftax Classic and MiniHack side by side, so they are built
by scripts/paper_figures.py rather than by the single-environment
experiments/rl_finetuning/analysis/plots.py. It reads both repositories'
results.json and emits vector PDF at NeurIPS column width:
uv run python scripts/paper_figures.py \
--minihack-results ../remdm-planner-minihack/experiments/rl_finetuning/outputs/minihack_ablations/results.json \
--outdir results/paper_figures
The MiniHack path defaults to that sibling checkout. Pass --emit-tex-macros to
run_ablations.py to also write tables/results.tex, one \newcommand per headline
quantity, so the manuscript cites generated numbers instead of retyping them. Macros from
this repository are prefixed rw and the sibling suite's mh, so both files can be
\input together.
Publishing to the Hub
scripts/hf_upload.py rediscovers and uploads four things, each keeping its repo-relative path: checkpoints/, every experiments/rl_finetuning/outputs/<run>/ holding a results.json (with diagnosis.md, tables/, figures/, gdelta/), the eval JSONs in results/inference/, and the manuscript figure PDFs in results/paper_figures/. It drops W&B and hub config keys, shortens absolute paths and regenerates the model card.
HF_TOKEN=hf_xxx uv run python scripts/hf_upload.py --repo-id <ANON_HF_REPO_ID> --dry-run
--dry-run prints the staged tree and card without uploading; drop it to upload. Also --inference-results <FILE|DIR> ... (eval JSONs kept elsewhere), --private, --yes.
Checkpoint discovery expects the released layout, checkpoints/<role>/<name>/<step>/. A training run writes elsewhere, so copy its wandb.run.dir/policies directory to checkpoints/{offline,online}/<name> first, or nothing is staged. checkpoints/hf/ is skipped β that is where a Hub download lands, and publishing from it would nest already-published artefacts under checkpoints/hf/checkpoints/....
Results, citation, licence
Results tables and the full method description are in Return-Weighted ELBO Fine-Tuning Degrades Masked Diffusion Planners (under submission); demo_craftax.ipynb reproduces the headline evaluation. Citation to be added on publication. Licence: MIT, see LICENSE.
Appendix: benchmark-specific detail
Environments
| Environment | Achievements | Actions | Notes |
|---|---|---|---|
Craftax-Classic-Symbolic-v1 |
22 | 17 | Crafter ported to JAX |
Craftax-Symbolic-v1 |
65 | 43 | + NetHack mechanics, 9 floors |
Set via the env_name config key.
Remasking strategies
Selected by remask_strategy, on top of the three-phase loop controlled by use_loop, t_on and t_off.
| Strategy | Formula | Description |
|---|---|---|
rescale |
sigma = eta * sigma_max |
Scales maximum remasking probability proportionally |
cap |
sigma = min(eta, sigma_max) |
Caps remasking at a fixed rate |
conf |
sigma = softmax(-psi) * eta * sigma_max over committed tokens |
Low-confidence tokens are remasked preferentially (psi = decode probability at last unmask) |
Key hyperparameters
configs/defaults.yaml is authoritative and commented inline. Tabulated here are the
keys that change a result, carry a hazard, or are named elsewhere in this README.
Environment. env_name selects the benchmark: Craftax-Classic-Symbolic-v1
(default) or Craftax-Symbolic-v1 for Full Craftax.
Diffusion model
| Parameter | Default | Description |
|---|---|---|
plan_horizon |
32 | Action plan length H |
diffusion_steps / diffusion_steps_eval |
15 / 10 | Denoising steps T at training and at inference |
diffusion_schedule |
cosine |
Noise schedule: cosine or linear |
remask_strategy |
rescale |
Remasking strategy: rescale, cap, or conf |
train_sigma |
0.0 | Per-token remasking correction during training (0 = standard MDLM) |
label_smoothing |
0.0 | Cross-entropy label smoothing epsilon (0 = exact ELBO) |
eta |
0.5 | Remasking strength |
use_loop |
true |
Three-phase loop remasking (Algorithm 3) |
t_on / t_off |
0.7 / 0.3 | Time window boundaries for loop remasking |
temperature |
0.5 | Softmax temperature for token sampling |
top_p |
0.95 | Nucleus sampling threshold |
Transformer architecture. d_model 384, n_heads 8, n_layers 6, d_ff 768,
obs_encoder_layers 2, obs_encoder_width 768, dropout_rate 0.1 β the shape every
released checkpoint carries. A checkpoint restores only against a matching config.
Offline training
| Parameter | Default | Description |
|---|---|---|
offline_total_timesteps |
1e8 | Env-frame budget. Derives num_updates as offline_total_timesteps // (num_envs * num_steps). |
num_envs / num_steps |
1024 / 128 | Parallel environments, and env steps per update; their product is fpu |
num_minibatches / update_epochs |
8 / 8 | Gradient minibatches per epoch, and epochs per update |
num_repeats |
1 | Independent training seeds (vmapped) |
lr |
3e-4 | AdamW learning rate (cosine-decayed to 10% over all gradient steps) |
weight_decay |
0.0 | Decoupled AdamW decay for core training; 0.0 is Adam exactly (the ablation suite keeps 1e-4) |
lr_warmup_frames |
1.6384e6 | Env-frame linear warm-up budget (0 = disabled). Derives LR_WARMUP_STEPS in gradient steps. |
max_grad_norm |
1.0 | Global gradient clipping norm |
return_weight_cap |
5.0 | Clip ceiling for per-window return weights (lower clip fixed at 0.1) |
val_interval_frames |
1e6 | Env-frames between validation rollouts. Derives VAL_INTERVAL in update steps. |
Online DAgger training
| Parameter | Default | Description |
|---|---|---|
online_total_timesteps |
1e8 | Env-frame budget. Derives num_updates. |
dagger_beta_init |
1.0 | Initial expert mixing probability beta_1 |
dagger_beta_final |
0.344 | Target final mixing ratio. Derives the per-update decay beta_i = beta_init * decay^i. |
dagger_buffer_cycles |
1.90735 | Replay-buffer capacity in update cycles of history. Derives DAGGER_BUFFER_MAX in samples. |
dagger_train_passes |
null |
Passes per update over the buffer; null = 1 (matches offline BC per-update gradient work) |
dagger_expert_deterministic |
true |
Argmax expert (fixed s -> a* map) vs categorical sampling |
Data collection / inference
| Parameter | Default | Description |
|---|---|---|
collect_num_steps / collect_num_envs |
1e7 / 128 | Steps to collect, and envs collecting them |
ppo_model_type |
ppo_rnn |
PPO architecture: ppo, ppo_rnn, or ppo_rnd |
eval_steps / eval_num_envs |
10000 / 32 | Evaluation length and width (independent of num_envs) |
inference_sampler |
sample_plan |
sample_plan (the published protocol) or inpainting |
eval_replan |
8 | Env steps executed per plan under sample_plan |
Checkpointing / logging
| Parameter | Default | Description |
|---|---|---|
save_policy |
true |
Save final checkpoint and upload as W&B artifact |
checkpoint_dir |
checkpoints |
Where checkpoints land when W&B is off |
seed |
null |
RNG seed (random if null; per-run: --seed) |
jax_compilation_cache_dir |
null |
Persistent XLA compilation cache; null = off. See below |
The resume_*, use_wandb and wandb_* keys mirror the run flags documented under
Configuration.
Persistent compilation cache
The whole training run is one jax.jit, so every process pays one large
compilation before any work happens, and multi-seed runs, resumed runs and the
ablation suite each repeat it. jax_compilation_cache_dir makes the second and
later runs of the same graph skip it. The cache is keyed on the lowered HLO, so
a hit is bit-identical to a miss. Point it at local disk, not an NFS home:
python main.py --mode online --ppo-checkpoint <ppo> \
--config configs/final_craftax_classic_gpu_24gb.yaml \
--override jax_compilation_cache_dir=/var/tmp/$USER/jax-cache
Environment wrappers
From Craftax_Baselines/wrappers.py (submodule):
| Wrapper | Purpose |
|---|---|
LogWrapper |
Tracks episode returns and lengths; adds stats to the info dict |
AutoResetEnvWrapper |
Automatically resets episodes on done |
BatchEnvWrapper |
Vmaps reset and step over num_envs environments |
OptimisticResetVecEnvWrapper |
Batched resets with reduced overhead; enable via use_optimistic_resets |
Stack (identical for training and inference): env -> LogWrapper -> AutoResetEnvWrapper -> BatchEnvWrapper.
Testing
uv run pytest
A CPU-only suite, 14 modules. Tiny synthetic data and a shrunken model throughout β no real checkpoints, datasets or network calls, and nothing written outside tmp_path. conftest.py forces JAX_PLATFORMS=cpu and disables W&B; there are no custom markers.
| File | Covers |
|---|---|
test_smoke_src.py, test_smoke_experiments.py |
that things run: imports, model from the real config, a gradient step, checkpoint round-trip, samplers, resolvers, every CLI entry point, and all 26 ablations' losses and optimizers |
test_spec_*.py, test_method_spec*.py |
that things are correct: each canonical statement of the parent workspace's the spec *.md pinned against the implementation |
test_config.py, test_recipe_values.py |
the preset, delta-only, cluster-sibling and poolability rules, and the shipped recipe values |
test_gdelta.py, test_tex_macros.py |
the --measure-gdelta decomposition, and the --emit-tex-macros output: definitions only, uniquely named, letters only |
test_gpu_agreement.py |
CPU/GPU agreement, skipped without a device |
Implementation notes
| Topic | Note |
|---|---|
| JAX purity | make_train_offline_diffusion / make_train_online_dagger are fully JIT-compatible; env construction and checkpoint I/O sit outside jax.jit. |
| Offline data | --mode offline rolls out PPO live. --mode collect saves an .npz for inspection only β re-feeding it to --mode offline is unsupported. |
| Episode-boundary masking | A window at (e, t) is valid only if dones[e, t+1:t+H-1] are all False. |
| Return weighting | Valid windows are weighted by cumulative reward, normalised by the batch mean, clipped to [0.1, return_weight_cap], and applied as per-sample multipliers before loss reduction. |
| LR schedule | Cosine decay lr -> lr * 0.1 over all gradient steps. lr_warmup_frames prepends linear warm-up, converted as (frames // fpu) * update_epochs * num_minibatches (* dagger_train_passes online). |
| DAgger sizing | dagger_sizing() in src/planners/common.py is the single source of truth for samples_per_update, buffer capacity and n_train_passes. |
| DAgger aggregation | Ross et al. (2011). A circular buffer accumulates (obs, expert_plan) across iterations, with a sliding stride so every visited state contributes a label; the expert receives correct done flags so its RNN state resets at episode boundaries. |
| Loss weight clipping | The MDLM SUBS weight -alpha'(t) / (1 - alpha_t) is clipped to 1000 for stability as alpha_t -> 1. |
| Denoising indexing | Reverse scan runs step_idx = 0 -> T-1, mapping to t = (T - step_idx) / T (high to low noise). |
| Validation and best checkpoint | Every val_interval updates, at val_diffusion_steps / val_replan_every / val_steps. The highest-return parameters are kept alongside the live ones and uploaded as {env_name}-policy-best. |
| W&B namespaces | Centralised in src/planners/logging.py: diffusion/, train/, env/, val/, dagger/. train/sps only in modes with live env interaction. |
| PPO experts | Training lives entirely in Craftax_Baselines/; planner modes only consume checkpoints. Released PPO checkpoints were saved on GPU and fail to restore on a CPU-only machine. |