Skip to content

platoon 0.4.0

Choose a tag to compare

@ApGa ApGa released this 25 Oct 07:41
· 37 commits to main since this release
b46d0fc

Training can choose its tasks by their pass rates and train only on the groups that carry signal: task samplers per
environment, an adaptive pass_rate sampler, an API for task samplers of your own, and group admission with refill, all
experimental. Rollout permits now come back as each rollout ends, so the next group starts on the permits that a group's
finished rollouts give back: that is the new default, train.permit_release: rollout (group trains as 0.3 did), and
in process a permit also waits for its rollout's background teardowns. Rollouts start sooner: platoon train stages
harness toolchains and preloads the rollout workers' modules before the first rollout, and a limit on rollouts starting
at once is new and experimental. train.task_order, which 0.3.0 renamed train.task_sampler, is removed. A group now
cancels its other rollouts when one of them raises, and the warning about a model service that sends no routing ids
comes once, not for each rollout. The TextCraft extension's release for platoon 0.4 is platoon-textcraft 0.3.0.

New features

  • Task samplers you choose per environment, and an adaptive one (experimental). Each environment entry may set
    task_sampler and task_sampler_options: under train.task_sampler: weighted (a mixture's default), the run hands
    each slot to an entry by weight, as before, and the entry's sampler chooses which of its tasks fills it. uniform,
    the default, draws the entry's tasks in the seeded passes it always has; pass_rate chooses the task whose next
    group is most likely to mix passes and failures, so that more groups train, from the per-task statistics: Thompson
    sampling on each task's pass rate, with a fifth of the draws going to the tasks drawn least recently and at most
    three draws of a task per pool-sized window. train.task_sampler: pass_rate chooses over every task of the run
    instead. Each step logs what it drew and why, the informative share it predicted and the one it got, how
    concentrated its draws are, and how long it took to decide (train/sampler/...). It is experimental: it becomes the
    recommendation only after a GPU comparison with uniform shows more informative groups at an unchanged held-out
    pass@1
    (Choose tasks by their pass rates).
  • Write your own task sampler (experimental). A sampler is a small class, registered with
    @task_sampler(name, options=...) from an entry's package, or named as module:Class, at the run level or an
    entry's, with typed options (train.task_sampler_options, or the entry's task_sampler_options) checked against its
    schema before the run starts (core's samplers' already when the config loads) and recorded in lock.json. A sampler
    that reads no statistics draws when the order reaches it, as the built-ins always have; one that reads them decides
    when its group can start, among the tasks whose label has room, reproducibly from its seed and its decision count.
    The API and the per-task statistics types it reads are in the new platoon.experimental module, and
    platoon.testing.check_task_sampler checks a sampler against the contract
    (Write a task sampler).
  • Train only on the groups that carry signal, with refill (experimental). train.group_admission: informative (or
    an environment's own group_admission) decides when each training group settles whether it counts toward the batch: a
    group whose workflow says it trains nothing (its rollouts scored alike, or it carried no datums) is rejected, and one
    whose rollouts scored alike frees its place at once, so that another group starts while the trainer works (one without
    datums, which can end at once, keeps its place until a step takes it). A step trains the first batch_size admitted
    groups that are not stale, and stops waiting once it has counted refill.max_groups_per_step (3.0) times batch_size
    groups, training what it admitted. group_admission_options: {band: [lo, hi]} also rejects groups whose pass rate is
    outside the band. all, the default, admits every group, as before. A step then trains up to several times the
    informative groups, and only those, at more rollouts a step: up to the cap's multiple of the groups it counts, and
    where the trainer bounds the run, the groups that start while it trains, which the horizon and the rollout permits
    bound, not the cap; each step logs what it admitted and rejected, and why (train/admission/...). Under a loss whose
    objective is not zero at zero advantages, a rejected group's advantage-independent terms go too, and loading warns;
    beside a rule, num_minibatches must be 1. Custom rules register with @group_admission or are named as
    module:Class; the API is in platoon.experimental, and platoon.testing.check_group_admission checks a rule. It is
    experimental: it becomes the recommendation only after a GPU comparison (Train only on groups that carry
    signal
    , Write a group admission
    rule
    ).
  • Rollout permits that come back as each rollout ends, by default. A training rollout returns its permit
    (train.max_concurrent_rollouts) as soon as it ends rather than when its group's workflow returns
    (train.permit_release: rollout, the default), so the next group starts on the permits a group's finished rollouts
    give back instead of waiting for its slowest one; on long agentic tasks a quarter to a third of the budget sat idle
    that way. GroupRolloutWorkflow runs each member on its group's lease, so a group starts on its first free permit
    and its members as the rest free (rolling starts), each member's straggler deadline shifted by its own start delay; a
    workflow that does not say it runs its rollouts on the lease gets each group's permits whole, as before. Groups are
    drawn, decided, credited, admitted and resumed as before, a partly finished group counts whole toward the horizon,
    and a run at max_staleness: 0 with the default budget trains the same datums in the same order. More groups are
    open at once, so more long groups can go stale: the run warns when they do, and raising max_staleness by one is the
    documented answer. The default changes ahead of a GPU comparison of the two settings; train.permit_release: group
    keeps the behavior of earlier releases, event for event (RFC 0020;
    Rollout permits; what changes for a config that sets nothing is
    under Changes, below).
  • Run a workflow's own rollouts on the lease (experimental). platoon.experimental exports rollout_lease() and
    RolloutLease: a workflow runs each rollout in async with rollout_lease().rollout(i):, whose permit comes back
    when the block exits, and declares runs_rollouts_on_lease = True for rolling starts. In a training draw a block for
    an index outside the draw's size raises IndexError, and a second block for an index RuntimeError, under either
    setting; outside a training draw, as in an evaluation, the lease is a no-op (Run rollouts on the
    lease
    ).
  • See what keeps groups from starting. Every training run logs, each step, the share of the step in which the
    dispatcher could not start a group and why (train/dispatch/blocked_share/{permits,horizon,labels,plan,sampler}),
    the permits held (rollout_client/permits_held_mean), the groups waiting for a worker-startup slot under process
    isolation (rollout_client/startup_gate_waiting_mean), and, where a straggler policy applies, what it censors
    (train/batch/censoring/*, and each label's: the share of rollouts it cancelled, and over mixed groups the pass
    rate of each group's slowest valid rollout against the others'). It warns, once per start of training, when more
    than 2% of the groups the last 10 steps consumed went stale (so from the tenth step on), naming what held the
    dispatcher back and the fixes, and when more of GroupRolloutWorkflow's rollouts ran at once than
    max_concurrent_rollouts.
  • Rollouts start without the work that each used to do again, or wait for. Two costs fell on a rollout's start.
    A CLI harness's first rollouts on a node waited while one of them copied the harness's toolchain to node-local disk
    (about 36,000 files and 800 MB for OpenHands), at every start and every resume in a new job: platoon train now
    stages its toolchains while it opens the training service and saves at launch, and starts no rollout, training's
    or an evaluation's, before that is done (platoon eval while it lists its tasks), so that no rollout waits for the
    copy and the first ones wait only for what of it outlasts the launch save; it waits ten minutes at most, and not
    past a shutdown request. And each rollout worker imported, after its fork, what its environment and harness import
    when they open, about 3,700 modules for an in-process ORS environment played by CodeAct, and parsed the
    environment's task file again: the workers' fork server now does that once, before it forks them, from what the
    config names (the environment's module and the openreward SDK, each function harness's and program's module, the
    registry lookups, an in-process environment's task lists for the run's splits), and starts as soon as the training
    client is open. It keeps only what leaves it fork-safe: a module or a build that starts a thread or a process,
    leaves a file, socket or event loop open, or seeds a numpy generator from the OS is left out with a warning that
    says why, and the workers import it themselves. train.workflow_config.worker_preload adds modules that an
    environment imports only when it opens. In CPU benchmarks of TextCraft-Synth played by CodeAct with Qwen3-4B's
    renderer, a forked worker reached its agent in 0.03 s instead of 7-9 s, 25-30 s and 48-56 s with 32, 128 and 256
    workers starting at once, with 149 file system calls on the shared filesystem instead of 17,134 and 30 MB of memory
    of its own instead of 290 MB. In CPU emulations of an OpenHands first wave whose toolchain copy took 77-90 s, the
    first sandbox could open 8-17 s sooner after the training client opened, and 46-53 s sooner behind a 38 s launch
    save (Rollouts are slow to start).
  • A limit on rollouts starting at once (experimental). train.max_starting_rollouts: N lets at most N rollouts
    set up their workers, sandboxes and harnesses at once, training's and evaluation's together, first come, first
    served: a GroupRolloutWorkflow rollout takes a start slot once it holds its permit and gives it back as its first
    model call is issued, as it ends, or after worker_startup_budget_seconds + 300 s without a call. It is for
    start-ups that contend for a shared capacity, such as sandboxes that each extract a root filesystem; off by default
    (null), it changes nothing. It shortens no one-time start-up work (the fork server's import, a harness toolchain's
    copy to each node, the modules each worker imports), and with any value a run's whole first wave reaches the model
    later. Each step logs the rollouts starting and waiting to start, their waits and why slots came back
    (rollout_client/start_limit/...), and the run warns when slots come back on their timer, or mostly only at their
    rollouts' end (Rollout start-ups).

Changes

  • train.task_sampler takes registered names and module:Class. An unknown name is still refused when the config
    loads, in the same report as the file's other problems, with a longer message that begins as before, unless an
    entry names a package that could register it; it is then refused before the run starts.
  • A full checkpoint's tasks/dispatch-*.json is format 2: it adds the row each outstanding draw drew, the count and
    state of each task sampler, the run's and each environment's under weighted (and auto for a mixture), and the
    slots waiting for an environment's adaptive sampler. A resume reads platoon 0.3.0's format 1 as before, and goes on
    exactly where its run stood; platoon 0.3.0 cannot resume exactly from a checkpoint that 0.4.0 wrote (it draws from
    the checkpoint's batch, as from a checkpoint of 0.2)
    (Checkpoints and resume).
  • The per-task statistics record which task sampler drew each group, and why: GroupRecord.sampler names the
    sampler that chose the task (the run's sampler as the config names it, or an entry's as <sampler>@<entry>, such as
    uniform@<entry> for an environment's uniform under weighted), with the draw's reason and score, and so do
    the draw events of tasks/events.jsonl, every one of them.
  • Every training run's lock.json records its task samplers, the run's and each entry's it asks, as components of
    kind: task_sampler with their options, defaults included; and every run logs what its samplers drew since the last
    step, by reason, and how long their decisions took (train/sampler/draws, train/sampler/draws/<reason>,
    train/sampler/decision_ms, and an entry's under train/sampler/environment/<entry>/).
  • The per-task statistics take in settled groups in the order they were drawn, at each step's metrics, each full
    checkpoint and each adaptive decision, not in the order they completed, so their values no longer depend on
    completion order within a step.
  • The per-task statistics have a sixth fate, rejected, a group admission rule's: platoon tasks show prints it
    after lost, and platoon tasks export has a fate_rejected column after fate_lost, so the five columns after it
    (group_size to newest_version) move one place right. A rejection's fate event in tasks/events.jsonl names the
    rule and its reason (rule, admission).
  • GroupRolloutWorkflow declares gives_verdicts = True; a workflow class of your own that gives each group a
    verdict declares it too, so that group_admission: informative loads beside it.
  • A full checkpoint of a run under a group admission rule records in its row of checkpoints.jsonl where the order
    stood (next_position): a resume that cannot read the checkpoint's state of the draws draws on from there, since
    such a run's steps draw more positions than their batch.
  • GroupRolloutWorkflow declares runs_rollouts_on_lease = True and runs each member in a block of its group's
    lease; a subclass that overrides arun_episode, _run_group_rollouts or _run_group_member, itself or through a
    mixin, reads as False unless its own class body sets it. Under permit_release: group the blocks hold nothing,
    but a training group of a subclass that runs a member index twice, as a retry of the whole group in
    arun_episode does, now fails with RuntimeError under either setting: a retry stays inside the member's block.
    A member's rollout sees no lease, so a group that it runs in turn takes no permits of its own and runs as before.
  • Breaking: rollout permits come back as each rollout ends, unless train.permit_release: group. The default of
    train.permit_release is rollout, so train.max_concurrent_rollouts counts the rollouts running, no longer the
    whole groups it holds. Where the permits bound a run, more groups run at once and start sooner, steps get shorter, and
    more long groups can be dropped as stale: when more than 2% of the groups the last 10 steps consumed went stale, the
    run warns and advises max_staleness + 1. There its rollouts are requested in another order, some on a newer policy,
    and a restart redoes more of them. In process, a rollout's permit also waits, at most 60 s, for the sandbox and env
    session teardowns that platoon finishes in the background, so that no more sandboxes are alive at once than the budget
    (under group such a teardown can still outlive its group, as before). A run whose permits never bind, as at
    max_staleness: 0 with the default budget, trains as before. Each start of training under rollout says so in a line
    of its log (Each rollout returns its permit as it ends (train.permit_release: rollout) ...). train.permit_release: group restores the behavior of earlier releases (The previous
    behavior
    ).
  • Under train.permit_release: rollout, rollout_client/rollouts_in_flight_configured is max_concurrent_rollouts
    itself, so concurrency_achieved_fraction reads as the permits' busy share, and groups_in_flight can exceed the
    budget over the group size. The straggler settings' documentation calls them a deadline against runaway rollouts,
    not a throughput lever.
  • Under train.permit_release: rollout, an in-process rollout's permit comes back once the sandbox and env session
    teardowns that platoon finishes in the background have ended too, such as one that a straggler cut interrupts or an
    ORS environment's that waits for its code to end, so that no more sandboxes are alive at once than
    max_concurrent_rollouts. The permit waits for them at most 60 s, and the run warns of a teardown that runs longer;
    a cancellation ends the wait and no teardown, and a rollout that had ended keeps its outcome. Under group nothing
    waits for them, as before (Rollout permits).
  • A staged harness toolchain has a stamp beside it, .<copy>.staged, that records the tree hash it was checked
    against and the jobs that checked it. Where the copy outlives its job (PLATOON_TOOLCHAIN_CACHE on node-local disk
    that persists), the first staging of each later job hashes the copy again and keeps it when it matches its
    manifest; before, a kept copy was used unchecked. One that no longer matches is copied again beside it, and the old
    copy is set aside whole (.<copy>.<hex>.stale), never removed, since rollouts of another job may still run from it.
    A copy staged by an earlier platoon is checked once. A cache that platoon cannot write to (read-only, another
    user's, a full disk) records no check, so its kept copy is used unchecked, as before, with a warning.
    platoon.harness.toolchains.prestage stages toolchains ahead of rollouts.
  • The rollout workers' fork server takes 4-8 s longer to start and holds 150-230 MB more memory, which the workers it
    forks share (14-20 s against 10-13 s, and 890-930 MB against 670-740 MB, in CPU benchmarks of TextCraft-Synth played
    by CodeAct with Qwen3-4B's renderer), once per run and while the trainer saves at launch; each worker holds far less
    of its own.
    Its first log line counts the modules it imported for the environments beyond the worker's stack (imported the worker's stack and 9 module(s) the environments open with). An environment variable that one of those imports
    sets is set in each worker too. Under worker_start_method: exec, rollout_isolation: inprocess and
    worker_python_env_isolation nothing is imported ahead, as before, and a worker_preload set there is warned
    about.

Fixes

  • A GroupRolloutWorkflow group without a straggler policy no longer leaves its other rollouts running when one of
    them raises (as a subclass's own arun_episode_single can): it cancels them and raises once they have unwound. They
    ran on after the group had failed and its rollout permits had come back, so more rollouts could run at once than
    train.max_concurrent_rollouts, on work that no step trains. A cancelled group likewise returns only once all its
    rollouts have unwound, not once the first has.
  • A resume under train.task_sampler: uniform draws again exactly the row it had drawn; it could take the listing's
    form of the row, whose nested values differ in their empty keys.
  • A rollout worker that is cancelled, as by a straggler cut or a stop, no longer leaves its readiness and diagnostics
    files in node-local scratch, where they stayed until a later run found them a day old.
  • A model service that sends no routing ids is warned about once, not for each rollout. With
    rollout_isolation: process, the trainer warned about every rollout whose model calls did not all get a routing
    id, which a server with rollout routing replay (R3) sends with each sampled sequence. With a service that sends
    none, as a dense model's does, the trainer logged a warning for each rollout: 18,125 in one 70-step run. Now it
    warns once for each start of training, about training and about evaluation rollouts each, that the service sends no
    routing ids: expected for a dense model, but no rollout routing replay for a mixture-of-experts model. It still
    warns about each rollout that got routing ids for some of its model calls but not all, or for none after the service
    has sent some, and it reports the first rollout to get any after rollouts without them, naming the first few of
    those. The metrics train/rollout_worker_routing_id_coverage and eval/rollout_worker_routing_id_coverage are
    logged as before.

Removed

  • Breaking: train.task_order is gone. Platoon 0.3.0 renamed it train.task_sampler and deprecated the old name
    for removal in 0.4.0. A config or an override that still sets it fails as a configuration error (exit 78), with
    train.task_order: unknown key; platoon 0.4.0 removed it; use train.task_sampler; so does a config copied from
    platoon 0.2.0's config show --defaults, which spells task_order: auto, even beside a task_sampler, where 0.3.0
    ignored it. Set train.task_sampler instead: it takes the old name's values, auto and weighted, which draw as
    they did (Train on several environments).

Install and upgrade

pip install -U 'platoon[tinker,ors]>=0.4,<0.5'
# with the TextCraft extension, name it too:
pip install -U 'platoon[tinker,ors]>=0.4,<0.5' 'platoon-textcraft[platoon]'
  • To upgrade a project that pins one minor release, move its range to 0.4, such as platoon[tinker,ors]>=0.4,<0.5.
    Platoon 0.4 refuses the components of an installed extension whose requirement on platoon excludes it, such as
    platoon>=0.3,<0.4, with an error that names both versions, until a release of the extension admits 0.4;
    meanwhile, PLATOON_IGNORE_EXTENSION_REQUIREMENTS=1 loads them anyway
    (Compatibility and provenance).
  • A config that sets train.task_order no longer loads: use train.task_sampler, which takes its values
    (platoon config validate names the key).
  • train.permit_release now defaults to rollout. Where the permits bound a run, more groups run at once and
    steps get shorter, and more long groups can be dropped as stale: if the run warns, raise train.max_staleness by
    one. train.permit_release: group trains as 0.3 did
    (The previous behavior).
  • The TextCraft extension's release for platoon 0.4 is platoon-textcraft 0.3.0, whose platoon extra requires
    platoon[ors]>=0.4,<0.5. Its code and data are those of 0.2.0. The release for platoon 0.3 stays 0.2.0: there, keep
    the extension within 0.2 (platoon-textcraft>=0.2,<0.3), since platoon 0.3 refuses the components of 0.3.0, also
    when it is installed without its extra. An upgrade of platoon alone leaves 0.2.0 installed, which pip does not flag
    and platoon 0.4 refuses: upgrade the two together, as above
    (The TextCraft extension).