Skip to content

feat: adapt orchestrator to pull-based env server (lazy/infinite tasksets)#2882

Draft
mikasenghaas wants to merge 15 commits into
mainfrom
feat/orchestrator-lazy-tasksets
Draft

feat: adapt orchestrator to pull-based env server (lazy/infinite tasksets)#2882
mikasenghaas wants to merge 15 commits into
mainfrom
feat/orchestrator-lazy-tasksets

Conversation

@mikasenghaas

@mikasenghaas mikasenghaas commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

Adapt the orchestrator to the pull-based v1 env server. Companion to verifiers PR #1829, which replaces the index-addressed rollout RPC with sample() / run_rollout(task) / run_group(task, n) and moves task scheduling (shuffle + epoch) into the server. The orchestrator no longer addresses tasks by index — it samples one task per group, then runs it.

  • Dispatcher samples once per group, then fans out at rollout granularity. schedule_group_rollout calls env.sample() once for the group (stored on GroupState.task) so all group_size rollouts share one task. A group-scored env then runs the whole group as one indivisible run_group(task, group_size); a non-group env fans out group_size independent run_rollout(task) calls, each holding a single permit. This is the key consolidation: a group is the aggregation unit, but a non-group env's concurrency is controlled per rollout — permits free one rollout at a time, so max_concurrent stays saturated instead of idling on a straggler-bound whole-group pull.
  • Env gains sample() / run_rollout(client, task, ...) / run_group(client, task, group_size, ...) wrapping the new client RPCs; the task is carried as a vf.WireTask (not a dict).
  • TrainSource collapses to a ratio-weighted env pickernext_example returns just {env_name}. The index range, per-env shuffle, and epoch cursors are gone; the server owns all of that now.
  • EvalSource/EvalEnv examples become bare slots (a count); GroupState.task_idx is a -1 placeholder until the first sample() returns (it only labels error markers — successful rollouts carry the real idx on their own Trace).
  • Env.num_tasks is int | None (None ⟺ infinite), used only to bound a finite eval.
  • Shuffle is configured via --<env>.taskset.shuffle (a vf.ShuffleConfig), not a top-level TrainEnvConfig.shuffle bool (removed). Train envs default it on (a before-validator injects a ShuffleConfig when unset); eval envs leave it unset → deterministic. The legacy bridge gets the ShuffleConfig from the taskset too. (The env server's broker owns the task cursor, so a multi-worker pool serves one coherent shuffled stream — see Remove reload_weights_on_start dead code #1829.)

Requires the verifiers pin to include #1829 (the pull RPCs). This is not backward-compatible with the old index-addressed server — the two land together.

Breaking

  • Default training weighting is now equal (...env[].ratio defaults to 1.0): previously, with no ratio set, envs were sampled proportional to dataset size; now every env defaults to ratio = 1.0, so they are sampled in equal parts (1:1:1). Configs still parse (ratio stays optional), but multi-env runs that relied on the implicit size weighting will sample differently — set an explicit ratio per env to restore the old mix. The all-or-nothing ratio validator and the infinite-needs-ratio error are removed. This changes training dynamics for existing multi-env configs.
  • Rollouts are pulled, not addressed: the dispatcher sample()s a task then runs it; neither run_rollout nor run_group takes a task_idx. Custom code that drove rollouts by index must sample() first. Requires the verifiers pin to include Remove reload_weights_on_start dead code #1829.
  • TrainEnvConfig.shuffle (top-level bool) is removed. Configure shuffling via --<env>.taskset.shuffle (a ShuffleConfig); training still shuffles by default (a before-validator injects a ShuffleConfig when unset). To pin a seed, set --<env>.taskset.shuffle.seed N.

Known follow-up

  • Subset eval drifts across steps. The server cursor advances between eval steps, so a subset eval (num_examples < taskset size) repeated across steps sees a rotating subset, unlike the old fixed 0..n-1. Full-set eval is unaffected (each step pulls one reshuffled epoch = the same set). A clean fix is a per-eval-step cursor reset (a reset RPC) — not in this PR.

Verification

  • Reverse-text e2e (this machine, 2× RTX PRO 6000). v1 reverse_text trained 20 steps (reward 0.20 → 0.78, 0 errors); the v0 path (legacy bridge) ran after a WireTask(prompt=None) fix to the bridge's sample(). A 2-worker static-pool run produced exactly 8 distinct tasks × 16 rollouts per step (clean groups, no duplication, 0 errors), confirming per-rollout fan-out + single-cursor pool routing.
  • Source unit checks. TrainSource: equal-weight default ([1.0, 1.0], not size-proportional), explicit-ratio override, {env_name}-only examples, and permit gating (cost = group_size for group-scored envs, 1 otherwise — matching how the dispatcher actually opens each kind of group).

🤖 Generated with Claude Code

…ask count)

The env server no longer reports a task count for lazily-served (generator,
possibly unbounded) tasksets — `info().num_tasks` is now `int | None`. Adapt the
orchestrator to drive purely by `task_idx`:

- `Env.num_tasks` is `int | None`; `EvalEnv` requires `num_examples` to bound a
  lazily-served eval (can't enumerate an unknown/infinite count).
- `TrainSource` keeps the finite path unchanged (enumerate the index range,
  shuffle, reshuffle per epoch, weight by size) and adds an unbounded path:
  `num_tasks is None` → stream a monotonically increasing `task_idx` (the
  taskset's own generator supplies the variety). Unbounded envs have no size to
  weight by, so they require an explicit `ratio`.

Backward-compatible: against a server that still reports a count, every env is
bounded and behavior is unchanged. Pairs with verifiers PR #1829 (the env-server
API change); the verifiers pin bump lands when that merges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mikasenghaas and others added 10 commits June 25, 2026 19:51
Mirrors the verifiers companion (#1829): the server reports num_tasks=None only for an
UNBOUNDED taskset (finite ones, list or generator, report a count), so the orchestrator's
wording follows suit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1.0)

Train env sampling weight is now each env's `ratio`, defaulting to 1.0 (equal parts,
1:1:1). Drops the previous dataset-size-proportional default (and the all-or-nothing
ratio validator + the unbounded-needs-ratio error — every env now has a ratio). An
unbounded env no longer needs an explicit ratio.

BREAKING: changes default training dynamics — multi-env runs that relied on implicit
dataset-size weighting now sample envs equally; set explicit `ratio`s to restore a mix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eduling

The orchestrator no longer addresses tasks by index. It pulls: it picks which env
(weighted by `ratio`), and the env server hands out the next task (shuffle + epoch
owned server-side, per verifiers#1829). The served task's idx rides back on each Trace.

- `Env.run_rollout` removed; everything goes through `Env.run_group(group_size)` (a
  single rollout is just group_size=1). A group is one indivisible `run_group` pull —
  so a non-group-scoring env's group is now one worker's `group_size` rollouts of one
  pulled task (same as group-scoring envs already work), not individual round-robined
  rollouts.
- `TrainSource` collapses to a `ratio`-weighted env picker (drops the index range,
  per-env shuffle, epoch cursors — the server does all that now).
- `EvalEnv` examples become bare slots (count only); `GroupState.task_idx` is a -1
  placeholder (known only once a Trace returns; labels error markers).
- The orchestrator passes `shuffle` through to the spawned server (the v0 bridge too).

Requires the verifiers pin to include #1829 (task-less RPC); not backward-compatible
with the old index-addressed server.

Known follow-up: a *subset* eval (num_examples < taskset size) repeated across eval
steps drifts (the server cursor advances between steps), unlike the old fixed 0..n-1.
Full-set eval is unaffected (each step is one reshuffled epoch = same set). Fix needs a
per-eval-step cursor reset (a `reset` RPC).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The verifiers env-server default is shuffle=False (deterministic). Training wants
shuffled, epoch-reshuffled data, so TrainEnvConfig overrides shuffle=True; eval envs
inherit the deterministic default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A group samples its task once (sample()), then a non-group env fans out group_size
individual run_rollout(task) calls — one permit each, freed on completion, so a slow
rollout never holds the whole group's permits and inflight stays at max_concurrent.
Group-scored envs run the whole group via run_group(task, n). Restores per-rollout
concurrency (lost in the run_group-per-group model) without addressing tasks by index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… crashing the loop

Review fixes:
- TrainSource/EvalSource open-cost was group_size for ALL envs (stale from the
  run_group-per-group iteration). A non-group env opens with one run_rollout (1 permit)
  and fills the rest per-rollout, so the source cost is now group_size only for
  group-scored envs, else 1 — fixes a non-group eval env with per-env group_size >
  max_inflight_rollouts never being scheduled (silent eval hang).
- Guard env.sample() in schedule_group_rollout: it runs in the scheduling path (outside
  the inflight-task error handling), so an unguarded error killed the dispatch loop. Now
  caught -> log + retry next tick.
- Fix stale GroupState.task comment (task is sampled for both group and non-group envs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mikasenghaas mikasenghaas changed the title feat: adapt orchestrator to index-addressed env server (lazy/unbounded tasksets) feat: adapt orchestrator to pull-based env server (lazy/infinite tasksets) Jun 26, 2026
mikasenghaas and others added 4 commits June 26, 2026 01:41
Drops TrainEnvConfig.shuffle (top-level bool); training defaults taskset.shuffle to a
ShuffleConfig (on) via a before-validator, configured with --<env>.taskset.shuffle. Eval
envs leave it unset (deterministic). Legacy bridge gets the ShuffleConfig from taskset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The orchestrator now sets taskset.shuffle (a ShuffleConfig); the verifiers pin must
include it. Points at the feat/dynamic-tasksets HEAD — the two land together.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feature-detect ShuffleConfig on the installed verifiers before injecting the train default,
so the slim configs package (which can resolve an older published verifiers) still parses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ifiers with ShuffleConfig

- Drop the before-validator feature-detection; require a verifiers floor (>=0.1.15.dev412)
  that has ShuffleConfig, and default the train shuffle in a plain after-validator.
- Trim dispatcher comments + the redundant group-liveness guard around the sample() warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant