Repository navigation
feat: keep training arrays with the env servers - #4196
Merged
Merged
Conversation
mikasenghaas
force-pushed
the
feat/payload-refs
branch
from
October 21, 2026 15:02
810a7cf to
f00fa85
Compare
mikasenghaas
force-pushed
the
feat/payload-refs
branch
from
October 21, 2026 15:46
ba7172d to
8c9ba1e
Compare
mikasenghaas
force-pushed
the
feat/payload-refs
branch
from
October 22, 2026 12:24
a71044a to
bc3d0f0
Compare
mikasenghaas
added a commit
that referenced
this pull request
Oct 22, 2026
…#4176, squashed) Squash of #4176 (feat/icepop-sc-sampling-replay @ daff5a2) onto rl-v1/prl, merged with the payload references of #4196 and the batch-load perf commit: - SamplingMask keeps memoryview ids/counts (zero-copy decode) and gains memoryview logprobs. - The trainer's head assembly merges the top-logprob and sampling-mask heads, keeping the row-copy fast path. - The train sink accepts sampler logprobs inline or kept by an env server (payload reference). - The env-server payload hook encodes the mask and head with _encode_sampler_evidence, and the trainer's payload assembly carries the mask's logprobs column. Co-Authored-By: Mika Senghaas <mika@primeintellect.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Router-replay rows, the score-centering top-k head and sampling masks travel today from the env servers to the orchestrator inline, and the orchestrator packs them into the trainer batch, so the orchestrator node holds every buffered episode's arrays. With `[payloads]`, each env-server worker keeps a finished train episode's per-branch arrays and serves them over HTTP; the orchestrator packs only references, and each trainer rank fetches its micro batches' arrays before the step and assembles them exactly as the packer does inline. Arrays are freed by the policy version stamped on each dispatch, and capped per worker. Micro batches whose arrays cannot be fetched are dropped from the loss on every rank that trains them. On multi-node runs the env servers now always run on the inference nodes, round robin over sources, instead of next to the orchestrator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once every rank has fetched a step's arrays (after the world all-reduce that agrees on failed fetches), one rank per data rank deletes them from the env servers. Drop the per-worker size cap, which threw away arrays a pending batch still needed; the version sweep stays only for arrays no batch ever fetches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every array an env server keeps is deleted exactly once, by whoever holds it last: the trainer after every rank fetched it, the train sink when it drops a sample or trace without shipping it (filtered traces and groups, untrained branches, pruned and stale samples, shutdown), and the env server when the episode never reached the orchestrator. This replaces the version sweep, so dispatches no longer stamp policy versions for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spreading sources round robin left a single env's memory on one node. Every inference node now runs a replica of every launcher-managed env server, each publishing its address to <name>.<node>.address, and the orchestrator routes each run to the replica with the fewest runs in flight. The rl launcher sets env_server_replicas to the number of inference nodes on multi-node runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop the [payloads] block: train env servers launched by rl always keep their episodes' training arrays, and the trainer always fetches and frees them. The launcher marks train sources with the internal keep_training_arrays env-server field; eval servers and servers run by hand still send arrays inline. Also drop deployment.orchestrator_on_inference. Env servers now run on the inference nodes and the orchestrator holds only references, so moving the orchestrator off the trainer node no longer frees memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mikasenghaas
force-pushed
the
feat/payload-refs
branch
from
October 23, 2026 14:05
e19ad6b to
495d1e1
Compare
Adapt references to main's micro-batch format: micro batches now carry raw buffers, a padding count and sampling masks with sampler logprobs. - The env-server hook and trace_to_samples share one branch encoder (encode_branch_arrays), so kept arrays match what main packs inline. - The trainer assembles fetched arrays with the packer's own helpers (pack_token_arrays, pad_token_arrays), so mask logprobs and padding follow the inline path. - The train sink deletes the arrays of samples that staleness masking voids, and accepts kept mask logprobs as sampler heads. Also fix review findings in the same code: - A micro batch whose fetch failed keeps the model's routing shape (carried on the reference) and repeats real rows, not zeros. - Samples with and without routing never share a micro batch. - The payload server is created once and advertises the bind host. - A failing keep frees what it stored; any fetch error drops the micro batch instead of crashing the step. - Env-server workers no longer import torch through this module. - Replica address files are indexed on every multi-node run, so a run with one inference node starts; the dead orchestrator-side env-server block is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ef6d678. Configure here.
- Truncation copies the whole reference, so a truncated sample's failed fetch keeps the routing shape instead of crashing the step. - References record fields per branch, so the sink's sampling-mask and top-logprob guards hold for each branch. - The payload server binds the env server's own host and defaults to loopback, so local runs need no resolvable hostname and expose nothing beyond what the env server does. - References live under a namespaced info key, so an env's own info["payload"] cannot collide with them. - A batch held back at max_steps releases its arrays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Env servers no longer stream training arrays (verifiers drops them from every delta), so references are the only path to the trainer: - TrainingSample carries a reference, never arrays, and the packer stops slicing and concatenating them. Micro batches list one reference per sequence, so samples with and without arrays share micro batches; the trainer gives the ones without empty rows. - The encoders and array helpers move to transports/payload.py, shared by the env-server hook and the trainer's assembly and free of torch. - References name the fields they hold; padding dummies keep only routing, so the trainer stops refilling their masks and heads. - The sink's mask and head guards check references only and tell a user-run train env server to set keep_training_arrays. - The sampling-support metric, which read streamed masks, is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mikasenghaas
commented
Oct 26, 2026
Drop orchestrator_on_inference from main's new GLM-5.3 Flash SWE example: this branch removes the field, and the orchestrator no longer holds training arrays, so it fits next to the trainer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review: drop keep_training_arrays, so every env server launched with prime-rl's env-server entrypoint keeps its episodes' arrays, and the eval sink deletes those of eval episodes since eval never trains on them. Keep deployment.orchestrator_on_inference as on main, and leave docs/inference.md unchanged. Pin verifiers to the late-cancel fixes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The dispatcher deletes the arrays of an episode that returned but is never emitted: a claimed task that had already finished, or a run cancelled during session cleanup. - An env server bound to a wildcard address binds the payload server to all interfaces and advertises a routable address, not the hostname's (often 127.0.1.1). - A branch whose arrays fail to encode is skipped with a warning instead of failing the episode; a train run that needs them still stops at the train sink's guard. - The payload server accepts a deep backlog, and fetches retry before a micro batch is dropped from the loss. - Packing concatenates each array once (b"".join) instead of growing bytes per sample, and assembly checks each reference's token count. - Pin verifiers to the pool late-cancel fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mikasenghaas
added a commit
that referenced
this pull request
Oct 27, 2026
Env.run takes the group's sampling, and the sink reads each env's sampling at the work's step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deps/verifiers moves to verifiers#2934 merged with verifiers main (5e36059); deps/renderers back to main's b6ab8c7 (the previous merge of main had set it to dc0b492). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mikasenghaas
added a commit
to PrimeIntellect-ai/verifiers
that referenced
this pull request
Oct 28, 2026
## Summary - **Training arrays never stream.** Nodes' per-token training arrays (`TRAINING_ARRAY_FIELDS`: routing rows, sampling masks, top-k heads) are left out of every delta, so they never cross the env server → client hop. The routing-row repairs, which only patched rows already streamed, are gone on both sides. - **`set_episode_hook(hook)`** (`verifiers.v1.serve`) is the only way the arrays leave a worker. It installs an `EpisodeHook` with two methods: - `keep(episode, sampling)` runs off the event loop on each finished episode, before its final flush. A host keeps the arrays itself and leaves references in `trace.info`; what it writes to the traces streams with the final flush. - `discard(episode)` frees what `keep` kept for an episode that was not delivered. - **Strict contract:** every episode passed to `keep` is either delivered in its run's successful reply or handed to `discard`, exactly once and never while `keep` still runs. This covers a `keep` that raises, a cancel that lands during or after `keep` (shutdown included), a reply that fails to encode or send, and a client that cancels the run after the reply left, including while it assembles or validates the reply: the client always sends a cancel, the pool broker forwards a cancel to the worker that replied within `LATE_CANCEL_SECONDS`, and that worker discards the kept episode. The host can then free what it keeps without any sweep. Used by PrimeIntellect-ai/prime-rl#4196: its train env-server workers keep each trainable branch's arrays and serve them to the trainer, so the orchestrator never holds them. ## Breaking - **Deltas carry no training arrays.** A client that read `routed_experts`, `sampling_mask` or `top_logprobs` from streamed nodes now gets `None`. Migration: keep the arrays on the server with `set_episode_hook` and fetch them from there. - **`routing_repairs` deltas are gone.** `EpisodeAssembly` no longer applies them, and no server sends them. ## Tests `tests/unit/serve/test_delta.py` (an episode streams and reassembles without its training arrays) and `tests/unit/serve/test_episode_hook.py` (a failed, abandoned or finished `keep`, a cancel before or after the reply leaves, and a cancel during the client's final decode each discard exactly once; an expired delivery is never discarded) and `tests/unit/serve/test_pool_cancel.py` (the broker routes a late cancel to the worker that replied); full `tests/unit`: 239 passed. prime-rl#4196 covers `keep` / `discard` end to end. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **High Risk** > Breaking wire semantics for training arrays and cancel/discard lifecycle; incorrect hook or cancel handling could leak GPU memory or drop data trainers depend on. > > **Overview** > **Per-token training arrays no longer cross the env-server wire.** Streamed node deltas now omit `routed_experts`, `sampling_mask`, and `top_logprobs` (`TRAINING_ARRAY_FIELDS`), and the **`routing_repairs`** mechanism is removed from both `DeltaStreamer` and `EpisodeAssembly`. Clients that assemble `WireEpisode` from deltas should expect those fields to be **`None`**. > > **`set_episode_hook`** is the supported way for a host to retain arrays on the worker: **`keep(episode, sampling)`** runs in a background thread after rollout finishes (before the final delta flush); **`discard(episode)`** runs when delivery fails or the client abandons the run. The server tracks kept episodes through reply send and a **`LATE_CANCEL_SECONDS`** window so a late cancel can still trigger **`discard`**. > > **Client and pool behavior** is tightened so abandonment is signaled reliably: **`EnvClient.run`** reuses a stable **`request_id`** and fires **`_cancel_run`** if decode/validation fails after the reply, or on request cancellation; the pool broker forwards cancels to the worker that still holds an in-flight run **or** recently replied within the late-cancel window. > > Tests now assert reassembly **without** training arrays instead of routing-row repair/parametrized dtypes. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1cee4be. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- Macroscope's pull request summary starts here --> <!-- Macroscope will only edit the content between these invisible markers, and the markers themselves will not be visible in the GitHub rendered markdown. --> <!-- If you delete either of the start / end markers from your PR's description, Macroscope will append its summary at the bottom of the description. --> > [!NOTE] > ### Let host keep finished episodes' training arrays via episode hooks > - Adds `EpisodeHook` with `keep(episode, sampling)` and `discard(episode)`, plus `set_episode_hook` to configure it. When a run finishes, the server runs `keep` in a worker thread before the final delta flush and tracks the kept episode for cleanup. > - Streamed node deltas now omit the `routed_experts`, `sampling_mask`, and `top_logprobs` training arrays (see `TRAINING_ARRAY_FIELDS`), and `routing_repairs` delta logic is removed from both streaming and assembly ([delta.py](verifiers/v1/serve/delta.py)). > - The client and pool support late cancellation: `EnvClient` can cancel a run after its reply arrived, and `EnvServerPool` forwards such cancels to the worker that handled the request within `LATE_CANCEL_SECONDS` ([client.py](verifiers/v1/serve/client.py), [pool.py](verifiers/v1/serve/pool.py)). > - Cancellation and failed/unsendable replies now discard hook-kept episode data. > - Risk: reassembled streamed nodes no longer contain `routed_experts` (the updated test asserts it is `None`); consumers relying on the old streaming repair path in [test_delta.py](tests/unit/serve/test_delta.py) behavior will need the hook to obtain training arrays. > > <!-- Macroscope's review summary starts here --> > > <sup><a href="https://app.macroscope.com">Macroscope</a> summarized 1cee4be.</sup> > <!-- Macroscope's review summary ends here --> > <!-- Macroscope's pull request summary ends here --> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Keep main's per-token staleness stream alongside the payload references: samples and micro batches carry both, and padding extends staleness while the trainer pads the fetched arrays. Point deps/verifiers at the merged verifiers#2934 (60290d436). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hallerite
previously approved these changes
Oct 28, 2026
- Stop the run when no micro batch's arrays could be fetched anywhere in a step, instead of training on nothing; log dropped micro batches across all data ranks, not only the logging rank's. - Fetch and delete payloads without HTTP proxies: payload URLs are node-local, and a proxy set for downloads breaks them. - Keep a branch's routing rows when its sampler evidence fails to encode; only the mask and head are dropped, with a warning. - Check that assembled routing data matches its shape, so a short blob cannot shift later samples' rows. - Delete payloads from one shared pool of daemon threads with a 10 s timeout, so DELETEs queued at exit never hold the process up. - Start the payload server on first use, so pool brokers open none. - Release only claimed tasks that are done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Router-replay rows, the score-centering top-k head and sampling masks travel today from the env servers to the orchestrator inline, and the orchestrator packs them into the trainer batch. At high concurrency with long contexts, the orchestrator node then holds terabytes of per-token arrays that only the trainer reads. Now they never leave the env servers inline: the train env servers keep them, and references are the only way they reach the trainer. There is no config and no inline path:
transports/payload.py). vLLM still returns them inline, so the env server keeps full control over the response. When a train episode finishes, each env-server worker (verifiers'set_episode_hook,PayloadHook.keep) encodes every trainable branch's arrays (encode_branch_arrays), keeps the blob, serves it over HTTP (GET <url>/<id>, a stdlib threaded server per worker on the env server's bind host, loopback by default,DELETEto free). It leaves the URL and, per branch, the blob id, the kept fields and the routing shape intrace.info["prime_rl_payload"]. Env-server workers import no torch through this path. Episode streams never carry the arrays (verifiers#2934).PayloadSlice(url, id, kept token count, fields); truncation shortens the count. A micro batch lists one reference per sequence, so samples with and without arrays pack together; samples with and without routing do not (orchestrator/trajectories.py,orchestrator/batch.py). Padding dummies reference only routing rows.pack_token_arrays,pad_token_arrays: cut each to its sample, concatenate in packing order, pad; a sample without arrays gets empty rows), so the tensor conversion is unchanged (trainer/rl/data.py,transports/payload.py).DELETE <url>/<id>), by whoever holds it last: the trainer, once every rank fetched a step's arrays (after the world all-reduce that agrees on failed fetches, one rank per data rank deletes); the orchestrator, for whatever it receives but never ships: the train sink for filtered and dropped traces and groups, untrained branches, pruned samples, samples that staleness masking voids, and batches and queues discarded at shutdown; the eval sink for every eval episode; the dispatcher for episodes that returned after their group was dropped; the env server, for episodes it kept that never reached the orchestrator, including runs the client abandoned after the reply (PayloadHook.discard, called by verifiers' delivery contract).payload/dropped_micro_batches(summed over data ranks). Its routing keeps the model's shape and repeats real rows, so the forward pass still routes like real data.env-serverentrypoint always installs the hook, with no config. Eval never trains, soEvalSink.adddeletes the arrays of every eval episode it receives, in the orchestrator and in the standaloneevalentrypoint (orchestrator/eval_sink.py).envs/<split>/<name>.<node>.address. The orchestrator connects to all replicas and sends each run to the replica with the fewest runs in flight (orchestrator/envs.py). So env-server memory and CPU spread over all inference nodes, even for a single env. The launcher setsorchestrator.env_server_replicasto the number of inference nodes.Builds on PrimeIntellect-ai/verifiers#2934 (merged): episode streams without training arrays, and
set_episode_hookwith its keep/discard contract.deps/verifierspoints at its merge commit on verifiersmain(60290d436).Breaking
logs/envs/<split>/<name>.<node>.log. No option restores the old placement. To pin a source's env server elsewhere, run it yourself and point the source at it withserve.address.env-serverentrypoint. Episode streams no longer carry training arrays, so a train source served any other way (serve.addresspointing at a plain verifiers server) delivers none. Runs that need sampling masks or top-k heads stop with an error saying so.serve.max_concurrent, the worker pool and sandbox limits of a launcher-managed env server now apply to each replica, so a source's total rises with the number of inference nodes. Migration: divide a limit that must hold run-wide by the number of inference nodes.payload/dropped_micro_batches).sampling_support/sizemetric is removed. Traces reach the orchestrator without sampling masks, so nothing feeds it.Verification
Qwen3-30B-A3B-Thinking on
i3_mathwith router replay (48 layers × 8 experts, uint8) and IcePop score centering (top-128 head), batch 128 × group 8, up to 1,024 rollouts in flight, 16k-token completions, H200.Inline (
main) vs by reference, 1 trainer + 1 inference node each, same config otherwise, 10 steps, measured on an earlier revision of this branch:Per-step reward varies by ±0.07 at this batch size, so the reward gap is within about two standard errors; the trainer sees byte-identical arrays (unit test below) and the mismatch KL is unchanged.
Env-server replicas, 2 trainer + 4 inference nodes, 27 steps each. one env server vs one replica per inference node:
The replica run continued to its 60-step limit and exited cleanly. Each replica's memory peaks early (≤ 25.9 GiB) and stays flat at ~22–23 GiB, so eager deletes keep up. Over 60 steps: no dropped micro batches, payload fetch 0.67 s mean, orchestrator peak RSS 3.3 GiB, reward mean 0.417. In both runs, most env-server memory is the math runtime's per-episode subprocesses and the pool workers; the kept arrays are about 10 GiB in total.
Transport, measured on an earlier revision of this branch: deltas into the orchestrator 8.8 GiB → 120 MiB per step, trainer batch 2.6 GiB → 78 MiB per step.
Tests:
tests/unit/transports/test_payload.py: real verifiers traces go through the env-server hook,trace_to_samples, packing (truncation, padding, multi-sample bins) and the HTTP fetch, and each fetched micro batch holds exactly its branches' encoded arrays in packing order. Samples with and without routing never share a micro batch. Explicit heads, masks with sampler logprobs, plain masks and a sample without arrays survive truncation, mixed packing, padding and the trainer's tensor conversion. Also covered: the routing of a micro batch whose fetch failed (truncated sample included), akeepthat fails midway, a branch whose arrays don't encode, the bind host (wildcard included), and the store anddiscard.tests/unit/orchestrator/test_train_sink_payloads.py: the train sink deletes exactly the arrays of filtered, dropped, stale, pruned and discarded samples and leaves only the shipped ones; the eval sink deletes those of eval episodes; the dispatcher deletes those of episodes that returned after their group was dropped.tests/unit/orchestrator/test_env_replicas.py: least-loaded routing over replicas.pytest -m "not gpu" tests/unitafter mergingmain: 1003 passed; after the last fixes, the affected suites (orchestrator, transports, trainer, configs, eval) pass 498.🤖 Generated with Claude Code
Note
High Risk
Changes the training data path (HTTP payload fetch, strict delete accounting, micro-batch drop on fetch failure) and multi-node env-server placement; misconfiguration or network issues can silently drop training signal or fail steps.
Overview
Training arrays (router replay, sampling masks, top-k heads) are kept on env servers and passed by reference instead of riding inline through the orchestrator and rollout transport. The
env-serverentrypoint installsPayloadHook, which encodes branch arrays over HTTP and stampstrace.info["prime_rl_payload"]; samples carryPayloadSlice, the packer threadspayloadsthrough micro batches, and trainer ranks fetch blobs before each step then release them after a DP-wide sync. Dropped or never-shipped arrays are deleted from train/eval sinks, the dispatcher on cancel, and when fetches fail micro batches are zeroed out of the loss (withpayload/dropped_micro_batches).Multi-node runs spread env-server load:
orchestrator.env_server_replicas(auto-set to inference node count) launches one env server per inference node; the orchestrator connects to all replicas and routes episodes least-loaded first. SLURM templates move env-server startup off the orchestrator node; address files use<name>.<replica>.address.Orchestrator batching no longer inlines slice/pack logic for routed experts and masks—that moves to
transports/payload.py. Live monitor deltas keep full delta dicts; thesampling_support/sizemetric is removed.Reviewed by Cursor Bugbot for commit d65bfc0. Bugbot is set up for automated code reviews on this repo. Configure here.