Skip to content

feat(nemo-gym): train on rollouts from external agent harnesses - #3407

Draft
ananthsub wants to merge 9 commits into
NVIDIA-NeMo:mainfrom
ananthsub:ananthsub/token-id-capture-rebased
Draft

feat(nemo-gym): train on rollouts from external agent harnesses#3407
ananthsub wants to merge 9 commits into
NVIDIA-NeMo:mainfrom
ananthsub:ananthsub/token-id-capture-rebased

Conversation

@ananthsub

@ananthsub ananthsub commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Trains on rollouts from an agent harness that drives its own model calls and returns no token IDs.

Gym captures the exact token IDs and log probabilities inside its model server and rebuilds each rollout into a trainable Responses payload. NeMo-RL assigns rollout identity, constructs the TokenSource inside its rollout actor, consumes the rebuilt result, and retires the frozen capture only after the rollout consumer accepts it.

Depends on the Gym stack: NVIDIA-NeMo/Gym#2190, #2124, #2125, #2126, #2341, #2180, #2181, and #2349.

Control and data flow

sequenceDiagram
    autonumber
    participant RL as NemoGym actor
    participant G as Gym run_examples
    participant H as Agent harness
    participant M as Gym model-server worker
    participant K as TokenSink
    participant S as TokenSource
    participant T as Trainer consumer

    RL->>RL: Run token_source_factory inside actor
    RL->>RL: Assign _ng_rollout_id
    RL->>G: Dispatch selected rows
    G->>H: Run agent
    H->>M: Model calls under /training-token-capture/{rollout_id}
    M->>K: Append TokenEntry records
    G-->>RL: Return rollout without token metadata
    RL->>S: Freeze and read snapshot
    RL->>RL: Rebuild response and preserve original media items
    RL-->>T: Yield trainable rollout
    T-->>RL: Request next rollout
    RL->>S: Conditionally retire accepted snapshot
Loading

Integration behavior

  • Capture selection comes from Gym's resolved static agent configuration. token_id_capture.all_agents: true opts every agent into capture for training-only runs.
  • _build_token_capture_source uses a local TokenCaptureStore when token_id_capture.dir is configured.
  • A training framework passes token_source_factory to spinup_nemo_gym_actor. Ray serializes the factory with the actor constructor, and the factory runs inside the actor process and virtual environment.
  • The factory must return Gym's TokenSource protocol. It may wrap a TransferQueue handle or any other framework-owned transport client.
  • Gym model-server workers construct the corresponding configured TokenSink and LineageStore proxies in their own processes. Their adapter package must be installed in the model-server environment.
  • The source, sink, and lineage proxies do not share Python objects or virtual environments. They communicate through the framework-owned transport.
  • Frozen records remain available while the rollout is yielded. Conditional retirement runs only after the consumer requests the next item, so failed or abandoned consumption does not delete evidence prematurely.
  • shutdown() closes the actor-owned source before stopping Gym servers.
  • Full UUID rollout IDs avoid collisions across actors and restarts.
  • Multimodal postprocessing uses the original response output, while training tokens come from the rebuilt output.
  • GRPO grouping keeps dataset prompt identity instead of grouping on the external harness's captured first-call prompt.

Source construction

The default file adapter is selected with token_id_capture.dir. A framework data plane injects an actor-local factory instead of naming a source class in Gym configuration.

source_factory = TransferQueueTokenSourceFactory(queue_handle)
actor = spinup_nemo_gym_actor(
    ...,
    token_source_factory=source_factory,
)

The source must implement Gym's TokenSource protocol: freeze, conditional drop, and idempotent close. The model-server side uses the corresponding TokenSink and LineageStore protocols supplied by the framework.

Metrics

The integration reports token_capture/rebuilt_fraction, rollouts_rebuilt, rollouts_unbuilt, calls_per_rollout_mean, chains_per_rollout_mean, delivered_fraction_mean, quarantined_fraction_mean, empty_generation_calls, parent_link_failures, masked_rollouts, and incomplete_rollouts.

@copy-pr-bot

copy-pr-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Comment thread nemo_rl/environments/nemo_gym.py
@ananthsub
ananthsub force-pushed the ananthsub/token-id-capture-rebased branch from a6c65af to de2d246 Compare August 18, 2026 16:57
@copy-pr-bot

copy-pr-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

ananthsub and others added 9 commits August 27, 2026 09:30
An agent harness we do not control, such as the Claude Code CLI, returns a
transcript with no token ids. Training on its rollouts needs the exact ids and
log probabilities the policy sampled, so Gym captures them at the model server
and rebuilds each rollout's model calls into one contiguous Responses payload.
This is the NeMo-RL side of that: correlate the rollouts, read the rebuilt
response back, and report what the rebuild kept.

In NemoGymEnvironment:

- Stamp each rollout with a correlation id before dispatch, so the harness's
  model calls can be attributed to the rollout that produced them. Ids are
  derived per shard, so two actors cannot mint the same one.
- After each row is yielded, replace response.output with the rebuilt items when
  the producing agent opted into capture. Agents that already carry token ids
  inline are left alone: replacing them with a reconstruction would silently
  train on the reconstruction wherever the two differ.
- Accumulate the per-rollout capture metrics across the stream and emit them
  with the timing metrics, since the generator has no end-of-loop.
- Retire a rollout's records once they have been read.

Failure handling follows the same rule throughout: one malformed capture must
degrade its own sample, never the batch. A failed rebuild keeps its records as
evidence and masks the sample rather than training on a partial rollout, and the
call is wrapped because the builder cannot contain a corrupt file or an
unreadable directory.

The recipes pin skip_tokenizer_init=false. Gym serves model calls over vLLM's
OpenAI-compatible HTTP server, which needs a tokenizer to apply the chat
template; without one every model call fails and the harness generates nothing.
configure_generation_config already defaults this to false when
expose_http_server is set, but setup_nemo_gym_config sets expose_http_server
after that default is resolved, so the check never sees it.

Four recipes ship: _smoke (0.5B, 2 steps, the minimum end-to-end check), _tools
(3B, adds a hermes tool parser so the harness actually calls tools), _supply
(3B, the same plus prefix supply), and an unsuffixed config for longer runs. The
two without a tool parser produce single-turn rollouts by design.

Depends on the Gym token-capture stack beginning at NVIDIA-NeMo/Gym#2124.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Gym exposes the per-record step its own rollout collection runs, so the copy of
it here goes away: the build, the metrics, the mask verdict and the retention all
live on that side and the two can no longer drift.

The step takes the finished record and a TokenSource, so nothing here reads Gym's
config. Today that source is the colocated file store; a data plane of our own
passes its own source instead and nothing else in this path changes.

Whether a rollout needs token ids attached is decided from the rollout, by
whether its output items already carry them. The local check for the producing
agent's opt-in flag is gone with it, which is what makes a batch mixing native
agents and external harnesses work: each rollout gets what it needs, and a native
agent that later moves onto capture starts being rebuilt with no change here.
That check could not have worked anyway, since it needed Gym's resolved server
configs and this actor only ever holds the env block.

Rollout identity moves to Gym's dedicated _ng_rollout_id key. It was synthesized
by writing a per-actor random base into the task index and a counter into the
rollout index, overloading fields that mean "which dataset row" everywhere else.
The id is unchanged in shape and still unique per actor and across steps.

mask_sample is read from the top of the rollout record as well as from the
instance config. Gym's token capture uses the former, because rollout collection
does not own the agent's instance config. An agent that judges its own rollout
unusable still sets the latter, and either one masks.

The example configs move to the nested token_id_capture block.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
The harness builds its own requests and never reads the row, so its calls reach the
engine with no sampling params and create_chat_completion refuses them. Set
sampling_overrides on that server, interpolated from policy.generation so the value has
one source.

Scoped to the blackbox run configs rather than the shared Gym training config: a native
agent's request already carries the right params from the row, and replacing them would
discard a validation profile the framework chose deliberately.

Also drops the two per-parameter publication keys, which were read through defaults that
happened to equal the configured values, so a run could not tell a wired pin from an
unwired one.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Adopt Gym's finalized capture lifecycle and pluggable source contract, preserve rollout grouping and multimodal inputs, and make the nightly smoke test reliably exercise optimizer updates.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Exclude local validation recipes and remove deployment-specific filesystem path checks from the production integration.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Let the framework construct its TokenSource inside the rollout consumer actor while preserving the default file-backed source.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Keep NeMo-RL responsible only for actor-local source construction now that Gym rejects configured framework sources at its schema boundary.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Carry source prompt identities through dynamic sampling so harness-specific prompt changes cannot collapse GRPO advantages into singleton groups.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Pin the declared Gym dependency stack and adapt rebased GRPO and rollout tests to the current async generation interfaces.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
@ananthsub
ananthsub force-pushed the ananthsub/token-id-capture-rebased branch from 8335e15 to 93f23d3 Compare August 27, 2026 22:27
@github-actions

Copy link
Copy Markdown

✅ Submodule Fast-Forward Check Results

Check based on commit: 93f23d3 (PR #3407 from ananthsub/token-id-capture-rebased)

✅ Submodules that are properly updated:

Gym: ✅ PR branch is ahead of main branch (fast-forward)

All submodule changes look good! ✨

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.

2 participants