Skip to content

feat(token-id-capture): chain a rollout's calls into one response - #2125

Merged
ananthsub merged 5 commits into
mainfrom
ananthsub/tokidcap/builder
Aug 21, 2026
Merged

feat(token-id-capture): chain a rollout's calls into one response#2125
ananthsub merged 5 commits into
mainfrom
ananthsub/tokidcap/builder

Conversation

@ananthsub

@ananthsub ananthsub commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Builds one trainable Responses trajectory from a rollout's unordered token-capture records.

Reconstruction flow

flowchart TD
    E[Unordered TokenEntry records] --> Z[Exclude calls with no generated tokens]
    Z --> O[Order calls by prompt length]
    O --> P[Find the earlier call whose complete tokens are the longest strict prompt prefix]
    P --> A{Several candidates share the longest prefix?}
    A -->|yes| Q[Quarantine the ambiguous call]
    A -->|no| L[Attach the inferred predecessor]
    Q --> F[Build roots and chains]
    L --> F
    F --> S{Exactly one root and one trainable chain?}
    S -->|no| M[mask_sample = true]
    S -->|yes| R[Project contiguous Responses output]
    R --> C[Assert prefix continuity and report metrics]
Loading

The predecessor relationship is inferred only during reconstruction: call B follows call A when A's complete token sequence is a strict prefix of B's prompt.

Summary

  • Chains calls through strict longest-prefix inference and quarantines indistinguishable candidates instead of guessing.
  • Excludes empty generations so a filtered call cannot become the predecessor of its retry.
  • Fails closed for incomplete capture, no trainable generation, multiple roots or chains, unresolved records, or unsafe projection.
  • Reports roots, chains, quarantine, delivered-token fraction, empty generations, and parent-link failures.
  • Keeps the low-level per_request builder for multi-trajectory consumers, while single-response delivery rejects it explicitly.
  • Covers trajectories_from_source success, incomplete state, source failures, and unsupported single-response modes.

Depends on #2124. Consumed by #2126.

@copy-pr-bot

copy-pr-bot Bot commented Jul 23, 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.

@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.

cmunley1
cmunley1 previously approved these changes Aug 6, 2026
Comment thread nemo_gym/token_id_capture/consumer.py Outdated
@ananthsub

Copy link
Copy Markdown
Contributor Author

/claude review

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

SHIP WITH CARE — trajectory builder + consumer for stitching captured token records into contiguous training trajectories. The correctness-critical parts hold up well.

What I checked hardest (this feeds RLHF; wrong tokens/masks corrupt training silently):

  • Masking errs safe. Every uncertain condition — unresolved final-call retries, >1 root, incomplete capture, log-prob/token mismatch, ambiguous parents — resolves to mask_sample=True or quarantine rather than a guess. _assemble wraps build+project+assert in a try/except that degrades one rollout instead of killing a batch, which matches the stated caller (rollout-collection / trainer step loops).
  • Projection invariant is self-checked. assert_prefix_contiguity runs on every assembled response, so a stitching bug fails the rollout rather than shipping a discontiguous prompt/generation sequence. Usage counts read from token-bearing items, so a leading content-only item won't KeyError or skew totals.
  • Async correctness is clean. No httpx, no ray.get. Sync co-located path uses the new seal_now; the trainer path awaits source.seal. The _sealseal_now rename has no external callers and seal() still delegates to it correctly.
  • clear_token_captures_for_rollouts correctly addresses the append-mode id-reuse hazard (rerun of a deterministic rollout id would otherwise stitch two attempts into one chain).

Two non-blocking items posted inline:

  • NOTE (consumer.py): per_request builder produces roots=0, so mask_sample masks 100% of its output — the strategy is effectively unusable through this consumer. Default prefix_merging unaffected.
  • NOTE (consumer.py): trajectories_from_source and clear_token_captures_for_rollouts are new public API with no direct tests; the trainer-transport path has seal/incomplete handling a builder-only test doesn't cover.

Neither is a merge blocker. No BLOCKER or RISK findings — masking-on-uncertainty and the contiguity assertion make the failure mode "dropped sample," not "corrupted score."

Comment thread nemo_gym/token_id_capture/consumer.py
@ananthsub

Copy link
Copy Markdown
Contributor Author

/claude review

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Review incomplete: the PR revision changed during review. The captured base ref (f480877826ff9dbda89a44411ca608a5443ab9b3) no longer matches the current base (6aca3bf814baf9fc839609dd6577d886450dd4c7), so check-pr-revision.sh fails the pre-publish revision check. To avoid posting findings against a stale diff, no inline comments were posted. Please re-run the review against the current head.

@ananthsub

Copy link
Copy Markdown
Contributor Author

/claude review

generated_tokens_captured: int = 0
generated_tokens_delivered: int = 0
# Only one chain is delivered per rollout.
# Sub-agent branches and post-compaction chains are dropped.

@cmunley1 cmunley1 Aug 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should support returning the full tree / forest, or at least segments in a list with broadcasted reward.

Also would like to have a optional filter for certain prompts like title calls

Comment thread nemo_gym/token_id_capture/consumer.py Outdated
if not snapshot.entries:
built = _failed_build(rollout_id, builder, "capture contains no token records")
else:
built = _assemble(rollout_id, list(snapshot.entries), builder, model)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be asyncio.to_thread(_assemble, ...)

Comment thread nemo_gym/token_id_capture/builder.py Outdated
Identical cumulative sequences are ambiguous.
The caller quarantines an ambiguous subtree.
"""
matches = [n for n in candidates if _is_prefix(n.cumulative, prompt)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With rollouts with 100s to 1000 turns, _infer_parent scans every prior node and compares full cumulative prefixes, which becomes O(turns³) as context grows, and the recursive walk() below might hit Python’s recursion limit 1k turn scale.

Should we make traversal iterative? Would a trie help here? We should test 1000 turn

cmunley1
cmunley1 previously approved these changes Aug 20, 2026
cmunley1
cmunley1 previously approved these changes Aug 21, 2026
cmunley1
cmunley1 previously approved these changes Aug 21, 2026
A rollout's captured calls arrive unordered and have to be stitched back into the
sequence the policy actually sampled. Call N+1's prompt begins with call N's
prompt plus its generation, so the calls chain by token prefix, with the tokens
between them (tool output, a new user turn) kept as interstitial context. That
needs no cooperation from the harness, which is the point: the harness is opaque.

When a rollout has more than one root, the delivered chain is the one whose root
completed first. capture_tokens is awaited inside the model server's response
path, so a call's record is durable before its response reaches the harness and
the next call has not been made yet; for a sequential harness, completion order is
dispatch order. Ordering on the record's created_at rather than on file order
matters at num_workers > 1, where several processes append and their interleaving
is lock order.

Two shapes are not handled, both involving a second agent. An auxiliary call the
harness makes on its own account can complete before the agent's first turn and be
selected instead. Parallel sub-agents overlap, so completion order stops meaning
dispatch order, and nothing in a record says which agent made a call. Both are
follow-up work; until then a rollout that split reports chains > 1 and a
delivered_fraction below 1 rather than failing quietly.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Seal capture snapshots before reconstruction and reject empty, ambiguous, multi-root, or multi-trajectory projections so uncertain policy output cannot receive rollout reward.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Keep the multi-trajectory builder available as a low-level primitive while making the single-response consumer fail clearly and safely. Cover external source freezing and its failure states directly.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Describe frozen snapshot input, fail-closed masking, and single-response delivery with short standalone comments and docstrings.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sla:review-overdue Review response is over the one-business-day SLA

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants