feat(token-id-capture): capture training tokens from external harnesses - #2124
Conversation
|
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. |
32b555f to
d0a5092
Compare
d0a5092 to
767c488
Compare
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>
|
/claude review |
|
SHIP WITH CARE Training-token capture: a new leaf package ( What I checked and where it holds up:
Watch items (not blockers):
Public API is additive only (new optional kwargs with defaults, new per-agent |
|
/claude review |
|
SHIP WITH CARE — training-token capture infrastructure. Correct-by-construction on the failure paths that matter for RLHF, but one write-path efficiency concern and a couple of NOTEs. The design is sound and the review-critical property — an incomplete rollout must never look complete — is defended in depth: RISK (inline, store.py:151): NOTE — request-body buffering under token-capture-only mode. When NOTE — No blockers. The correctness-critical paths for training data integrity are well-handled. |
|
/claude review |
|
Review incomplete: the PR head advanced during review (analysis ran against |
|
/claude review |
An agent harness that drives its own model calls hands back a transcript with no
token ids, because the wire formats it speaks have no field for them. RL trains on
token ids, and re-tokenizing the returned text gives a sequence that differs from
what the policy sampled by an unmeasured amount.
The ids still exist inside the model server, for the moment before it converts the
response to the harness's dialect and synthesizes a stream. Capture takes them
there, keyed to the rollout that produced them, and writes one TokenEntry per
model call.
Calls are correlated to a rollout by the /ng-rollout/<id> path prefix already on
main for evaluation capture, so this adds no second correlation scheme. The
agent-side gate now serves both consumers, and a per-agent token_id_capture flag
scopes which agents participate; native agents leave it off because they carry
token ids on their own response items.
The capture key is derived from a run request's task and rollout indices, which
assumes each dispatch gets a distinct pair. A caller that restarts numbering per
dispatch produces a repeated id, so two dispatches share one key and their calls
stitch into one trajectory. An explicit _ng_rollout_id on the run body replaces
the derivation, with the attempt suffix still applied on top. An id that would not
survive the path segment is refused rather than rewritten, and the id pattern is
defined once so the body check and the middleware cannot disagree.
Settings live in one `token_id_capture` block rather than as flat keys, and it
names where records go:
token_id_capture:
enabled: true
dir: /tmp/ng_tokcap
sink: my_pkg.sinks:MyDataPlaneSink
`sink` is constructed once per server process at app startup. That matters at
num_workers > 1: uvicorn is handed an app string and workers=N and spawns those
workers, re-importing the app module rather than inheriting the launcher's memory,
so a sink installed programmatically by a launcher does not exist in any worker.
Measured, capture then falls back to the file store, or writes nothing at all when
no directory is set, and logs no error either way. install_token_sink remains for
programmatic use under the same constraint. The validator refuses combinations
that would silently capture nothing: settings with `enabled: false`, a sink beside
a directory, an unknown key, and a sink that cannot report a lost call.
TokenSink and TokenSource are protocols in a module that imports no web framework,
cluster runtime or tensor library, so an inference worker can write into its own
data plane without pulling in the server stack.
Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
…no token ids capture_tokens returned quietly when a response carried no token ids, so a rollout that lost a call looked identical to a complete one. The builder reads the gap between one call's tokens and the next call's prompt as tool output, which closes the chain over the hole: the missing call's generated tokens are delivered inside the next prompt at mask 0, and tokens the policy sampled train as if the environment had written them. Mark the rollout instead, on the same path an exception already takes. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Separate rollout correlation from capture intent and expose durable paired transport contracts so external frameworks can integrate without silent partial training data. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Use the current FastAPI lifespan API so configured capture transports are closed without breaking server startup. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Make agent selection explicit without changing evaluation defaults, and avoid rescanning growing token payloads on every durable write. Rename snapshot and URL contracts so their lifecycle and training purpose are clear. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Keep comments and docstrings aligned with static agent selection, the training-specific route marker, and frozen source snapshots. Use short standalone sentences throughout the capture path. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Keep remaining protocol and storage comments to one complete thought per line. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Keep a frozen state tombstone after conditional drop so a late writer from the retired attempt cannot recreate its records. Explicit pre-dispatch cleanup starts the next attempt. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Remove framework source construction from Gym configuration so consumers create and inject sources in their own process. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Keep token_id_capture out of server discovery so env prefetch does not treat run-wide capture settings as a server. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Name the request context separately from its sink, remove speculative compatibility prose, and keep Claude Code capture opt-in rather than enabled by default. Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Records the exact prompt token IDs, generated token IDs, and generation log probabilities for model calls made by an external agent harness.
Control and data flow
sequenceDiagram participant RC as Rollout collection participant AS as Agent server participant H as External harness participant MW as Model middleware participant MS as Model route participant TS as TokenSink RC->>AS: /run with rollout identity AS->>AS: select static agent capture or all_agents AS->>H: model URL with /ng-rollout/id/training-token-capture H->>MW: model request MW->>MW: mint model_call_id and CaptureContext MW->>MS: normalized model request MS->>MS: assemble exact token arrays MS->>TS: await put(TokenEntry) alt capture fails or arrays are incomplete MS->>TS: await mark_incomplete(rollout_id, model_call_id) end MS-->>H: dialect response or streamCapture happens before dialect conversion or stream synthesis can discard token fields. The write is awaited before the model response returns.
Summary
all_agentsselects every configured agent for a training run./training-token-capturepath, independently of rollout correlation and evaluation observability.TokenSinkandTokenSourcecontracts with durable incomplete state, atomic frozen snapshots, lifecycle hooks, and versioned conditional retirement.Gym imports no training-framework data plane. A framework configures sink and lineage proxy factories in Gym model-server workers and constructs its source in the rollout-consumer process.
Stack base. Followed by #2125.