The brax-PPO student, the 14 curricula (conf/mode/), PAIRED, and the
measured throughput.
Entry point scripts/brax_ued.py; code in src/training/ (student.py,
iteration.py = one outer iteration, curricula.py = the modes below,
train.py = the loop and logging).
just train mode=<option> name=<run> [overrides] (conf/train.yaml;
every field: configuration.md).
brax_ued.py uses brax's PPO pieces with the mujoco_playground recipe
(networks, running obs normalizer, compute_ppo_loss, GAE, 20-step unrolls,
4 epochs x 32 minibatches, discount 0.97; the ppo and network config
fields, defaults = this recipe) and drives FunctionalEnv itself,
because ppo.train fixes each env's model for the whole run and cannot swap
levels.
- Outer iteration:
num_laneslevels (at most the arena'stiles², 256) written into tiles 0..num_lanes-1 of the shared arena,envs_per_laneenvs reset onto each tile (plus a fresh physics DR draw), steppedlevel_stepsin 20-step unrolls; when training, a PPO update follows every unroll. Lane regret = mean positive GAE advantage over the iteration.tile_leak_frac: envs that reached a neighbouring tile (tiled arena). - Physics backend:
backend(defaultauto: Wojtek warp on a GPU, jax on CPU; the Playground robots always jax, robots). Compare runs within one backend (Throughput). mode=dr: fresh uniform levels at full difficulty, train every iteration.mode=dr_easy: DR over ACCEL's easy generator (control points withingenerator_scaleof flat) — isolates the curriculum from "train easy".mode=dr_mixed: uniform levels scaled by a per-level difficulty drawn from[0, 1].mode=terrain_cl: the game-inspired terrain curriculum of Rudin et al. 2022 (legged_gym / Isaac Labterrain_levels), per lane because allenvs_per_laneenvs of a lane share its tile (training/terrain_cl.py).- Column: each lane draws one uniform level (own noise seed) at the start of the run and keeps it.
- Row
rin0..10: the lane runs its column shrunk towardflatbyd = r / 10(LevelSpace.shrink, therandom/d<d>formula). Initial rows uniform in0..5(legged_gymmax_init_terrain_level). - Every iteration trains (as DR); afterwards each lane moves: up one row
when >= 50 % of its envs succeeded (no fall, >=
min_travelm, astrain_success), else down one row when its mean travel < 0.5 x its mean commanded distance; clipped at row 0. A lane promoted past row 10 goes to a uniformly random row. - Commanded distance (
iteration.commanded_motion): the displacement of a robot tracking the iteration's commands exactly, body-frame(vx, vy)integrated along the heading thewzcommands turn, over every step (resets included). A constant straight command gives legged_gym's|command_xy| x episode_length_s. Travel is a displacement, so a path length (sum |command_xy| x dt) would demote perfect trackers: with the redrawn turning commands a perfect tracker's mean displacement is ~0.43 of it on Spot, ~0.56 on Go1/Go2. - Logged:
terrain_cl/row_mean,/row_p10,/row_p90(rows the iteration ran),/promote_frac,/demote_frac,/top_reset_frac, plus the level composition.
mode=accel: replay iterations (replay_prob, once the buffer holdsnum_laneslevels) train on rank+staleness-prioritized buffer levels, refresh their scores and queue their mutations; explore iterations do not train and score the queued children plus fresh easy levels (control points withingenerator_scaleof flat). The buffer keeps thebuffer_sizehighest-regret levels.- ACCEL curriculum switches (
mode.*; defaults reproduce plain ACCEL):mode.mutation=grow— after the local edits, scale the level's deviation from flat byU(1, grow_max)(default 1.3). Plain symmetric edits smooth as often as they roughen, so plain ACCEL's buffer stays near the easy generator's roughness (~0.03 m ongrid).mode.score=learnability— level scorep(1-p)(SFL),p= fraction of the lane's envs that never fell and ended >=min_travelm out, instead of positive value loss.mode.score=lp— learning progress, signed (training/progress.py), after LP-ACRL (Li et al. 2026, arXiv 2601.17428:LP = R_c(ζ) - R_c-1(ζ), the change in expected episodic reward between consecutive stages), adapted to ACCEL's buffer:Rper visit of a level = mean over itsenvs_per_laneenvs of the undiscounted return (full task reward) of the env's first episode on the level: reward summed from the level's first step through its first termination, else over allH= resolvedlevel_stepssteps (Spot 780, Wojtek 300).lane_episode_return,LaneTotalsintraining/iteration.py.- A terminated env (fall, divergence, Wojtek's
no_progresscut) auto-resets onto its spawn on the same tile and keeps stepping for the rest of the level; those steps still train PPO and still count inlane_returns(PAIRED regret), but not inR.Hstays below the env's 1000-step episode length, so no truncation inside a level. - Score =
R(this visit) - R(previous visit of the level): negative on forgetting. A level's first visit scores 0; LP is defined from its second visit on. A mutated child is a new level, so its first visit also scores 0: it does not inherit its parent'sR, since a child-vs-parent difference mixes the change in difficulty the mutation made with the student's learning progress. - The buffer stores each level's last
R(every buffered mode tracks it; onlylpscores by it). Sampling, replay, mutation and the generator are plain ACCEL's. Negative scores rank last and are evicted first; at the start the buffer fills with 0-score fresh levels (eviction only pastbuffer_size) and replays them.
mode.generator=mixed— explore iterations draw fresh levels from thedr_mixeddistribution instead of the easy generator.- Logged:
buffer_roughness_mean/_p90,buffer_score_roughness_spearman(rank correlation of level score vs roughness),children_roughness,train_success,learnability_mean. - Mode options
mode=accel_grow(grow),mode=accel_sfl(grow + learnability) andmode=accel_lp(plain ACCEL +lp).level_space=gridcomposes 16 edits for the twogrowmodes (8 otherwise);paramedits 2 parameters either way.
mode=paired/paired_uni/paired_bi/paired_evo: PAIRED and its variants, two students; see PAIRED.mode=plr: robust PLR (PLR⊥) - ACCEL's buffer, rank/staleness replay and score refresh, but no mutation; explore iterations score fresh uniform (DR) levels.mode=sfl: PLR⊥ scored by learnabilityp(1-p)instead of positive value loss — an approximation of SFL (Rutherford et al. 2024), not the original: the buffer persists with rank/staleness replay and explore iterations do not train, where SFL periodically rebuilds a top-K buffer from a large random pool and trains on a buffer/random mix.robot=go1|barkour|spot|go2: a Playground quadruped instead of Wojtek, see Playground robots.- Eval every
eval_everyiterations: deterministic policy oneval_levelsfresh uniform levels (2 episodes each, in chunks of at mosttiles²levels) at difficulties 0.25/0.5/0.75/1.0 —eval/d<d>/success_rate(no fall and >=min_travelm from spawn), fall rate, travel, forward vs commanded speed. - Curriculum composition (
level_space=paramonly), every iteration over the levels the student ran on (replayed, explored or PAIRED-generated):level/<param>= mean physical value (ParamSpace.physical, robot units),level/slope_deg_abs/level/step_height_m_abs= mean |value| of the signed parameters,level/genome_dist= mean |genome -flat|. - Compute accounting, every iteration:
sim_steps(every training env step simulated: PAIRED's antagonist and ACCEL/PLR explore iterations included, eval rollouts not),student_train_steps(env steps whose data fed a student PPO update),elapsed_s(wall clock since launch, compile included). The run summary ends withfinal/elapsed_h,final/sim_steps,final/student_train_steps(paper_tables.pycompute table). - SLURM:
slurm/brax_ued.sbatch, job interface in slurm.md; assets must be built beforehand since the array's tasks start together. - The arena must match the run's
arenaconfig (hfield.arena.check_arena:pad_radius, height scale, level grid, tiles, gap); the error names thejust build-accel-arena arena...command for it. Training and the builder read the samearenagroup, so the default needs no override.
Two brax students (protagonist = the checkpointed deliverable, antagonist), each with its own params, optimizer and running obs normalizer. PAIRED (Dennis et al. 2020) and the variants of Stabilizing UED with a Learned Adversary (Mediratta et al. 2023).
mode= |
level source | students | adversary |
|---|---|---|---|
paired |
learned adversary | both train every iteration | trained on regret |
paired_uni |
as paired |
+ UniBC | as paired |
paired_bi |
as paired |
+ BiBC | as paired |
paired_evo |
ACCEL's buffer / mutation | train on replay iterations | none |
mode.bc is a field, so the presets are just paired + bc; BC on
PAIRED+Evo is mode=paired_evo mode.bc=uni|bi. mode.bc on a one-student
curriculum (dr*, accel, plr) fails at startup.
- PAIRED regret (
training/paired.py:paired_regret), per level over itsenvs_per_laneepisodes:max(antag max return - prot mean return, 0). Both students run the same levels in separate rollouts (own reset keys and physics DR draws), protagonist first. - Adversary (
mode=paired*):training/adversary.py+training/generator.py, latent seed -> sigmoid-squashed Gaussian over the genome (param: the 8 parameters, a random seed appended),num_laneslevels per iteration; reward = PAIRED regret; single-step PPO, 5 epochs, clip 0.2, batch-mean baseline,gen_entropy_coef(0.1, HiEnt on the teacher),gen_learning_rate. - Student HiEnt:
mode.student_entropy_coefis brax'sentropy_costfor every student of every mode; default0.01= the mujoco_playground recipe. HiEnt (Mediratta et al. 2023) = raising it on both PAIRED students (e.g.mode.student_entropy_coef=0.05). - UniBC / BiBC (paper Sec. 4.3): adds
bc_coef * KL(pi_teacher || pi_student)to the student's PPO loss, teacher stop-gradient, on the student's own minibatch observations.uni: protagonist <- antagonist.bi: also antagonist <- protagonist, the protagonist as this iteration's updates left it (it trains first).- Teacher = the other student's policy params and its own normalizer
(
Student.as_teacher), fixed during the student's iteration. - KL (
training/paired.py:policy_kl): brax'sNormalTanhDistributionturns the policy logits intoloc,softplus(raw) + 0.001; both students push the Gaussian through the same tanh, and KL is invariant under a shared bijection, so the closed-form diagonal-Gaussian KL of the base distributions is the KL of the squashed policies. - Applied when
it % bc_every == 0(bc_coefs);bc_coefreaches the jitted update as a traced scalar (0 on other iterations), so no recompile. A student that never distils (none; the antagonist underuni) gets no BC term at all, somode=pairedis plain PAIRED.
- PAIRED+Evo (
mode=paired_evo):accel_iterationunchanged, withscore_levels->run_pair, so the level score is PAIRED regret over both students instead of positive value loss (mode.scoremust staypvl).- Explore iterations: queued children plus fresh levels from ACCEL's
explore generator (
mode.generator), rolled out by both students without updates, scored, added to the buffer. - Replay iterations (
replay_prob): rank+staleness-sampled buffer levels, both students train (HiEnt +mode.bc), scores refreshed, mutations (mode.mutation) queued and scored on the next explore iteration (ACCEL's order).
- Explore iterations: queued children plus fresh levels from ACCEL's
explore generator (
- Logged (all four):
paired_regret_mean,prot_return,antag_return,antag_forward_vel,prot_bc_kl/antag_bc_kl(mean KL(other student || this one) over the iteration's observations, after each update; logged undernonetoo, as a policy-distance diagnostic),bc_coef(the protagonist's weight this iteration, 0 when off or not training);gen_lossonpaired*; the ACCEL buffer stats onpaired_evo. Student metrics without a prefix are the protagonist's. - Cost: every iteration simulates both students, explore iterations of
paired_evoincluded:sim_steps= 2 xenv_steps;env_steps/num_timestepsandstudent_train_stepscount protagonist steps. BC adds one teacher policy forward per minibatch and one KL forward per unroll (physics is ~98% of an iteration). Each student jits its own update, so compile time roughly doubles (CPU smoke, 2 x 16 envs: ~2.5 min to the first logged iteration).
Wojtek, one H100, 8192 envs (256 lanes x 32), 300-step levels;
just profile (scripts/profile_brax_ued.py, diagnostics/throughput.py)
measures it.
| backend | env steps/s | training iteration | evaluate |
final eval |
|---|---|---|---|---|
| warp | 263k | ~10.8 s (2.46M steps) = ~227k steps/s | 17-23 s | ~8.5 min |
| jax | 12.9k | ~9.7k steps/s end to end | 106 s | ~55 min |
- Physics is ~98 % of an iteration on jax; DR draw, PPO update, regret and level rendering together < 5 %.
- 32M steps of
mode=accelon warp: ~5 min wall clock, ~2 min of it compilation. A 300M-step run: ~0.5 GPU-h (PAIRED variants simulate two students: ~1 GPU-h). - Playground robots run on jax: Go1 ~410k env steps/s on flat levels and
~335k in
mode=accelon one H100, so a 1B-step run takes ~1 GPU-h. - Whole-device GPU memory on warp: ~16 GB (JAX preallocation off).
- Warp is not jax: one checkpoint scores 1.5-3 points higher success on terrain under warp (flat unchanged) and stands 7-11 mm higher on every tile. Train, eval and compare within one backend.