BackwardCurriculum
BackwardCurriculum(configuration: BackwardCurriculumConfiguration) -> NoneA one-way receding front over a single archived route's segments (Salimans & Chen, done strictly).
Midpoint restarts are confined to the route's LAST route_length - front segments. The front moves backward only once the policy has mastered the current depth, so the tail is optimised before the prefix is ever touched -- dynamic programming by curriculum ordering. Because the policy carries a learned tail forward, improvements compound without needing archive banking.
Why this replaces the utility-blend frontier: that blend (win_proximity x (1 - local_progress)) was measured inert on cavern 15 -- Midpoint_Utility_Min/Max did not move between 7M and 98.7M steps while local_progress struggling-cell counts went backwards (86 -> 120 of 226). It spreads restarts across every depth at once, so PPO optimises a moving target: the tail keeps changing while it works on the prefix. A front is guaranteed to advance or to stall visibly at a known segment.
Restart states come directly from the archived route's path in the repository, which the archive pins for as long as the route is archived. An earlier version instead filtered the segment store's cell buckets for the route's own trajectory_id; that population is fixed at the route's segment count on the day it is ingested, is never replenished (every later episode gets a new id), and is ground away by max_per_key priority retention. It emptied within 100k steps on two consecutive cavern-15 runs, collapsing midpoint restarts to 0.1-0.4% of episodes with no error anywhere. Sourcing from the path removes that failure mode by construction rather than by tuning a filter.
Advancement is on a PLATEAU, not an absolute win rate. An absolute bar cannot work: the front recedes but the tail grows, so 80% means a 25-action tail at front 37 and a ~300-action tail at front 2, which needs ~99% per segment. The same number therefore becomes unreachable at some depth in every game, and the curriculum is forced through a wall it was never going to clear -- measured on cavern 15, where the collapse simply moved (front 2 on one run, front 24 on the next). S&C's premise is that each step back adds a small increment, so what is measured here is whether the increment still costs anything: the front holds while the win rate improves and moves when it stops.
Three rules keep it honest:
- A floor, not a target. A plateau below
minimum_advance_rateis a wall, not mastery, and the curriculum STOPS rather than advancing. Handing a policy that cannot solve the shorter problem a longer one is how every earlier run died. A visible stall at a known segment is the design goal. - A retreat rail. If a depth fails outright the front steps back toward the exit. One-way was right against the 2026-06-19 oscillation, but one-way plus forcing guarantees destruction: at 0% the returns are uniform and there is no gradient back. Firing only on catastrophic failure is a different thing from oscillating around a threshold, and
max_advance_attemptsbounds it. - No step-based patience. It existed to escape restart starvation, which direct path sourcing made impossible, and its only remaining effect was to force advances into walls.
Tail lengths at each depth are compared against the reference route's own remaining actions, which is the direct expression of "better than, or equal to, the trajectory" and the signal for whether a depth still has anything left to give.
Initialize the curriculum.
Parameters:
configuration(BackwardCurriculumConfiguration) – The curriculum's tuning parameters.
Properties
action_granular
action_granular: boolWhether rungs are single actions (per-action minted restarts) rather than marker segments.
advances
advances: intHow many times the front has receded.
at_start
at_start: boolWhether the front has receded past the first segment, so episodes now run from the true start.
A restart resumes from the END of its segment, so a front of 0 still skips the route's opening segment. The front therefore recedes one step further, to -1, at which point the curriculum stops supplying restart states and the episode begins where the game does. That final phase is the natural place for reset jitter: the policy has a robust route by then, and jitter widens the set of opening conditions it holds under.
best_tail_delta
best_tail_delta: int | NoneBest tail length at the current front minus the reference route's own tail from there.
Negative beats the reference, zero matches it, None means no win yet at this depth.
best_window_rate
best_window_rate: float | NoneThe best win rate achieved at the current depth, or None before the first full window.
enabled
enabled: boolWhether the curriculum is active.
episodes_at_front
episodes_at_front: intHow many qualifying episodes have run at the current front.
front
front: int | NoneThe current front segment index, -1 once it has receded to the true start, or None before a route has been seen.
front_rung_success_rate
front_rung_success_rate: float | NoneThe success rate over the window for episodes that restarted AT the front rung, or None when empty.
Rising = the front is being learned (be patient); flat near zero while episodes pile up = a wall. Distinct from window_win_rate, which under Salimans & Chen averages the whole sampled band.
over_budget_penalty
over_budget_penalty: floatThe reward applied to an episode that exceeds its action budget.
pulse_active
pulse_active: boolWhether a pulse is currently teaching or fading at the front.
pulse_weight
pulse_weight: floatThe just-in-time pulse's current BC loss weight (0 when idle or disabled).
pulses_fired
pulses_fired: intThe number of pulses fired across the run.
retreats
retreats: intHow many times a depth failed outright and the front stepped back toward the exit.
salimans_chen
salimans_chen: boolWhether the front follows the Salimans & Chen rule (band ahead of the front, success >= rho advances).
stalled
stalled: boolWhether the curriculum has hit a wall and stopped advancing.
A visible stall at a known segment is the whole point of a strict front, and is far more useful than forcing through: it preserves a working policy and localises the difficulty exactly.
starved
starved: intHow many times the front's filter yielded no candidate, so the episode fell back to a true start.
total_tail_delta
total_tail_delta: intCumulative actions saved against the reference across all mastered depths.
window_win_rate
window_win_rate: float | NoneThe win rate over the current front's outcome window, or None until the window is FULL.
A partial window is not a rate: after an advance or a resume the first few votes read as wild swings (one win in three is 33%) that mean nothing, so nothing is reported until window episodes have voted at this depth.
Methods
action_budget
action_budget(segment_index: int) -> int | NoneReturn the action budget for a restart at segment_index, or None when uncapped.
Scaled by the reference route's remaining actions from that segment, so the budget is tight where the problem is small and loosens automatically as the front recedes and the policy is asked to play more of the route.
Parameters:
segment_index(int) – The segment the episode restarts from.
Returns:
int | None– The budget in actions, orNonewhen the cap is disabled or the route is unknown.
load_state_dict
load_state_dict(state: dict[str, Any]) -> NoneRestore the curriculum from a checkpoint.
Parameters:
state(dict[str, Any]) – A previously serialised curriculum state.
observe_route
observe_route(route_length: int, remaining_actions: list[int]) -> NoneRegister the archived route's shape, seeding the front on first sight.
The front starts initial_offset segments back from the exit so the first depth is a short, winnable tail rather than the terminal segment alone.
Parameters:
route_length(int) – The archived route's segment count.remaining_actions(list[int]) – Per segment, the reference route's action count from the END of that segment (where a restart resumes) to the win. Used to score tails as better than, equal to, or worse than the reference.
record_outcome
record_outcome(reference: TrajectorySliceReference, *, won: bool, tail_actions: int | None = None) -> NoneRecord one midpoint episode's outcome, moving the front when this depth has given what it can.
Episodes count only when they start within credit_window segments of the front -- normally the front itself. Restarts are drawn from a wider sample_window for the difficulty gradient, but a shallower, easier restart must never vote on the front's competence.
Once the window is full the win rate is tracked, but no judgement is made until the depth has had min_episodes_at_front episodes: a depth the policy has never seen reads 0% because it has not learned it yet. After that, three outcomes are possible -- the depth is failing outright and the front retreats; the rate is still improving and the front holds; or it has plateaued, so the depth has given what it can and the front either advances or declares a wall.
Parameters:
reference(TrajectorySliceReference) – The slice reference the episode restarted from.won(bool) – Whether the episode ended in a win.tail_actions(int | None) – Actions taken from the restart to the win, when it won.
record_restart_served
record_restart_served() -> NoneNote that a curriculum restart was served, ending any starvation streak.
record_starved
record_starved() -> NoneRecord that the front admitted no sampleable candidate.
Starvation is silent by construction -- the store simply returns no sample and the episode becomes a true start ; it must be counted AND announced. On cavern 15 an unmetered starvation switched the entire midpoint mechanism off for 9.6 hours before anyone noticed; on 2026-08-13 a metered but unannounced one ran 43k episodes into a reset storm. Tiered so it cannot swamp the console: the first starvation warns, then every STARVATION_WARNING_INTERVAL at WARNING, escalating to ERROR once STARVATION_ERROR_THRESHOLD is passed. A single starvation is legitimate (the gap between a route displacement and its re-mint), so this never throws.
sample_segment_index
sample_segment_index(rng: random.Random) -> int | NoneDraw the route segment to restart from, uniformly over the front band.
Drawn from sample_window segments, which is a difficulty gradient: the shallower draws are short, winnable tails the policy can bootstrap from. This is deliberately WIDER than the band that votes on mastery -- see credit_window for why letting the easy draws vote is what made the front advance on competence it did not have.
Parameters:
rng(Random) – The selector's random source.
Returns:
int | None– A segment index, orNonebefore a route has been seen.
state_dict
state_dict() -> dict[str, Any]Serialise the curriculum for checkpointing.