Skip to content

feat(vllm-model): on-policy sampling pin via sampling_overrides - #2127

Closed
ananthsub wants to merge 5 commits into
ananthsub/tokidcap/deliveryfrom
ananthsub/tokidcap/sampling-pin
Closed

feat(vllm-model): on-policy sampling pin via sampling_overrides#2127
ananthsub wants to merge 5 commits into
ananthsub/tokidcap/deliveryfrom
ananthsub/tokidcap/sampling-pin

Conversation

@ananthsub

@ananthsub ananthsub commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

On-policy sampling pin in the vLLM model server

Fourth PR in the stack (base: #2126).

Problem

An external agent picks its own sampling settings when it calls the model. During training the generation worker checks that requests use the trainer's sampling settings, so a temperature or top_p the agent chose that doesn't match will either fail the run or bias the recorded rollout.

What it adds

  • sampling_overrides in vllm_model: set temperature and top_p on every chat request before generation, replacing whatever the agent sent.
  • The values come from plain config keys (policy_generation_temperature, policy_generation_top_p) with on-policy defaults. Gym doesn't know anything about the trainer; the trainer writes its settings into those keys (see the matching NeMo-RL change) and Gym just reads them.

Notes for review

  • vllm_model never references NeMo-RL. Any training framework can drive this by filling in those config keys.
  • This change stands on its own; it's grouped with the stack because it's part of making external-agent training work.

Tests

No isolated unit test — the vllm_model server test runs in its own environment. Verified in the 2-GPU Megatron GRPO run: sampling matched the trainer and the run stayed on-policy (generation KL ~0.04).

Stack

  1. feat(token-id-capture): capture training tokens from external harnesses #2124 capture core
  2. feat(token-id-capture): chain a rollout's calls into one response #2125 trajectory builder and consumer
  3. feat(token-id-capture): deliver rebuilt trajectories safely #2126 delivery, per-agent scoping, cleanup
  4. feat(vllm-model): on-policy sampling pin via sampling_overrides #2127 on-policy sampling pin (this PR)
  5. feat(token-id-capture): Claude Code external-harness example #2128 Claude Code example

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

@ananthsub ananthsub changed the title ananthsub/tokidcap/sampling pin feat(vllm-model): on-policy sampling pin via sampling_overrides Jul 23, 2026
@ananthsub
ananthsub marked this pull request as ready for review July 23, 2026 16:18
@github-actions github-actions Bot added the sla:triage-overdue Review assignment is over the one-business-day SLA label Jul 24, 2026
@ananthsub
ananthsub force-pushed the ananthsub/tokidcap/delivery branch from 74a48d6 to 71de7c5 Compare July 29, 2026 06:55
@ananthsub
ananthsub force-pushed the ananthsub/tokidcap/sampling-pin branch from 4a07f8d to 774339d Compare July 29, 2026 06:55
@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.

ananthsub and others added 5 commits July 29, 2026 00:10
Claude Code makes model calls that are not part of the rollout: it generates a
conversation title and probes quota. They reach the model server on the same
rollout-prefixed URL and get captured, and because they are genuine policy
output -- real token ids, real log probs -- nothing downstream can tell they do
not belong. Training on them optimizes the policy to write conversation titles
under the rollout's reward.

The record now keeps what the *harness* asked for (requested_model, has_tools),
read off the parsed request body at the handler rather than by touching the body
again in middleware. That is the signal, because a harness asks for a small
model for these calls even though the server serves one model.

Classification uses two signals and needs no harness-specific code in the core:
an optional explicit pattern list for deployments that know their harness, and
self-calibration -- whichever model generated the most tokens in a rollout is
the policy model, and calls asking for a different one are side calls. Records
written before this field existed carry an empty requested_model and are all
kept, so nothing changes for them.

Excluded calls are reported (side_calls_excluded) rather than silently dropped,
and a rollout whose calls were *all* side calls is masked instead of yielding an
empty trajectory.

This is the second half of the title-call problem. The first was structural: a
short side call became the main chain and the real rollout was dropped, fixed by
selecting on generated-token mass. Even with the right chain selected, the side
call would still have been stitched in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
The builder infers lineage after the fact, by finding the earlier call whose
token sequence is the longest prefix of this call's prompt. That cannot do two
things. It cannot resolve a retry: capture records a call once the response is
assembled, including one the client never received, so a retry leaves two
records with the same prompt and different generations, and both are equally
valid children. And it cannot run before the call, which is where supplying the
engine an exact prefix has to happen.

This resolves the parent at request time using only what the harness already
sends. A harness must echo the conversation to continue it, so the assistant
turns in a request are the ones we produced; hashing them in order identifies
the call that produced the last one. Nothing is added to the wire and nothing
depends on the harness preserving a field we invented.

Tool-call arguments are canonicalized (sorted-key JSON) for comparison only,
because harnesses re-serialize them between turns -- compact one turn, pretty
the next. Without that every tool-using turn would miss. The model's original
argument string is what stays in the record.

The index is a map keyed by call, not a running cursor, so it is a tree: two
sub-agents branching from one parent both resolve to that parent and both would
get the same prefix. A cursor would hand the second branch a prefix containing
the first branch's generation, which the splice would apply without complaint.
Entries are added and never mutated, so concurrent sub-agents cannot corrupt
each other's lineage.

Three cases deliberately return no parent rather than a guess: a new or
rewritten conversation, an ambiguous match (two recorded calls with
byte-identical output), and an evicted rollout. Each falls back to prefix
inference, which is what happens today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Without this the engine builds every prompt by re-rendering the conversation
through the chat template. Re-tokenizing an assistant turn can produce a
different split than the one sampled, and for a reasoning model the template
drops earlier thinking entirely. Either way the new prompt does not extend the
previous prompt-plus-generation, the builder cannot chain, and only the first
call is trained on.

Supplying the parent's cumulative ids as required_prefix_token_ids makes
NeMo-RL's splice keep them verbatim and append only the newly rendered tail.

The harness does not send this field and could not. Gym constructs the outbound
engine request itself, so it is injected in
_preprocess_chat_completion_create_params next to the sampling pin. The inbound
request supplies identity; the outbound request supplies payload.

The splice applies whatever it is given without checking that it belongs to this
conversation, so supply fires only on a unique, verified parent and otherwise
forwards the request untouched. Each record carries prefix_supplied and the
server tracks a supplied/eligible ratio, so a run can be audited afterwards
rather than inferred from whether chains happen to be contiguous.

Off by default; it needs a backend that honours the field.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
The route that serves a rollout's raw training tokens is registered on the same
app the harness calls to generate. That is acceptable inside a trusted cluster.
It is not acceptable once the harness runs in a sandbox whose only egress is
this server, because the harness could read its own training data -- or another
rollout's.

token_id_capture_read_token requires a bearer token on the route, compared in
constant time. When it is unset the route stays open and warns once, so existing
deployments keep working and the gap is visible rather than silent.

This is the one piece of sandboxing that has to land before the sandbox work,
not with it. Everything else about capture is unaffected by sandboxing: capture
happens in the model server, outside the sandbox, and the sandbox only ever sees
text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
Add a framework-agnostic sampling pin to vllm_model: sampling_overrides forces
temperature/top_p on every chat request, read from generic policy_generation_*
keys with on-policy defaults. This keeps an external harness's rollouts
on-policy for training without Gym holding any framework-specific knowledge —
the training framework publishes its sampling into the generic keys.

Signed-off-by: Ananth Subramaniam <ansubramania@nvidia.com>
@ananthsub
ananthsub force-pushed the ananthsub/tokidcap/delivery branch from 71de7c5 to 5c317c1 Compare July 29, 2026 07:12
@ananthsub
ananthsub force-pushed the ananthsub/tokidcap/sampling-pin branch from 774339d to 7342d4e Compare July 29, 2026 07:12
@ananthsub
ananthsub marked this pull request as draft July 29, 2026 07:12
@github-actions github-actions Bot removed the sla:triage-overdue Review assignment is over the one-business-day SLA label Jul 29, 2026
@ananthsub

Copy link
Copy Markdown
Contributor Author

Superseded: recreating this PR with the correct stack base (ananthsub/tokidcap/read-route) so it contains only its own commit. Four commits (side calls, parent index, prefix supply, read-route auth) were inserted into the stack after this PR was opened, and GitHub does not permit changing the base branch of a PR that is part of a stack. No review comments to carry over.

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.

1 participant