feat: top-p/top-k train sampling with native sampling replay - #3431
Draft
mikasenghaas wants to merge 1 commit into
Draft
feat: top-p/top-k train sampling with native sampling replay#3431mikasenghaas wants to merge 1 commit into
mikasenghaas wants to merge 1 commit into
Conversation
mikasenghaas
force-pushed
the
chore/vllm-0.28
branch
from
August 29, 2026 04:03
14a0bbf to
77c9264
Compare
Truncated train sampling (top_p < 1, top_k) renormalizes the rollout distribution over the surviving kept set; rollout logprobs reflect that (processed_logprobs) while the trainer normalizes over the full vocab, biasing every importance ratio. Record the kept set at sampling time and renormalize trainer logprobs over the same set (DeepSeek V3.2's Keep Sampling Mask, arXiv:2512.02556 3.1). Same user API as #3235: [orchestrator.train.sampling] top_p/top_k, no replay flags. Truncating policy sampling auto-enables inference.enable_return_sampling_mask, bounds top_k to 512 (trainer mask tensors pad to the largest kept set), and rejects opd/opsd and temperature 0. Unlike #3235 the capture is vLLM's native --return-sampling-mask (>= 0.28, V2 model runner) instead of custom engine patches: the /generate response carries sampling_mask natively, renderers parse it (PrimeIntellect-ai/renderers#144) and verifiers carry it as KeptTokens arrays (PrimeIntellect-ai/verifiers#2460). Capture is engine-wide: vLLM rejects requests with temperature <= 0 or top_k <= 0 while it is on, and it is incompatible with router replay (V1-only).
mikasenghaas
force-pushed
the
feat/native-sampling-replay
branch
from
August 29, 2026 05:10
a34da34 to
5a657f6
Compare
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
Builds on vLLM 0.28.0 from #3430. Supersedes #3235 — same feature and same user API, but the capture side is vLLM's native sampling-mask support (vllm#49577, released in 0.28.0) instead of prime-rl's engine/IPC monkey patches.
Adds top-p and top-k sampling support for train rollouts (both were hardcoded off). Truncated sampling renormalizes the rollout distribution over the surviving "kept set" of tokens; rollout logprobs already reflect that (
logprobs_mode = "processed_logprobs"), but the trainer normalizes over the full vocabulary — so every importance ratio is biased and runs with truncated sampling collapse. This PR records the kept set at sampling time and renormalizes trainer logprobs over the same set: DeepSeek V3.2's "Keep Sampling Mask" (arXiv:2512.02556 §3.1).Usage
That's the whole config — there are no replay flags. Truncated train sampling (
top_p < 1and/ortop_k) implies sampling replay end to end:top_krespected if set, else defaulted to 512; values above 512 are rejected — the trainer pads each micro batch's masks to the largest kept set, so the bound caps trainer memory). Truncation knobs must be the typed fields — smuggling them viaextra_bodyis rejected, as istemperature = 0. Frozen-source envs are exempt.inference.enable_return_sampling_maskis auto-set and persisted into per-node configs; it maps to vLLM's--return-sampling-maskand forces the V2 model runner. Hand-setting is only for standalone-launched servers.opd/opsdare rejected at config time (reference logprobs are full-vocab prefill scores).How it works
Inference — no prime-rl patches. vLLM's
--return-sampling-maskrecordstorch.isfinite(processed_logits)after top-k/top-p/min-p filtering and returns it on/inference/v1/generatechoices assampling_mask: list[list[int]](one list of surviving vocab ids per completion token). The stock tokens endpoint already emits the field, soPrimeRlServingTokensis untouched.Native-capture constraints, surfaced as config validation / documented behavior:
VLLM_USE_V2_MODEL_RUNNER=1under the flag, and the flag is rejected together withenable_return_routed_experts(router replay is V1-only).temperature <= 0or without an effectivetop_k > 0. Therlentrypoint warns when eval sources share the engine — eval sampling must settop_k(the model's generation config often supplies one) and a non-zero temperature.Transport (renderers → verifiers → orchestrator → trainer):
sampling_maskon thegenerateresult (drops the base64kept_tokenssplice from the feat: top-p/top-k train sampling with sampling replay #3235 lineage; no server emits it anymore).KeptTokens.from_sampling_maskconverts to flat int32ids/countsarrays; graph attribution validates alignment;Branch.kept_tokensunchanged.KeptTokens {ids, counts}(int32 bytes, CSR-style) onTrainingSample/MicroBatch, appended last to keep the positional wire layout stable; packed/truncated/padded alongside the other per-token streams; tensorized as[1, seq, max_kept]with-1padding.Trainer (unchanged from #3235):
logprob = logits[label]/T - logsumexp(logits[kept]/T)in both the chunked fused LM head (backward restricted to kept ids) and the vanilla path. Positions without a mask use full-vocab logprobs.Verification
uv run ruff check/ruff format --check;uv run pytest tests/unit/test_configs.py tests/unit/train tests/unit/orchestrator tests/unit/inferenceminustests/unit/train/models: 300 passed, 4 skipped, 1 failed (test_qwen3_vl_e2e, pre-existing fix: token_id-formatted logprob tokens in the qwen3-vl fake engine #3161; thetests/unit/train/modelsbackward tests crash identically on the base branch on this box).End-to-end on reverse-text (Qwen3-0.6B-Reverse-Text-SFT, 20 steps, 1 trainer + 1 inference GPU, filesystem rollout transport for payload inspection), all runs from this branch:
return_sampling_mask; all 162 orch→trainer micro batches carrykept_tokens = None(wire layout unchanged).top_p = 0.95(auto top_k = 512, with warning): reward 0.23 → 0.85, 0% rollout errors, mismatch KL 0.0006–0.0090 — inside the baseline's band, i.e. replay is exact. Engine logsreturn_sampling_mask: True+ V2 model runner. All 162 micro batches carry masks; per loss-masked token: mean kept 11.3, median 2, p99 169, max 388 ≤ 512, 48.4% singletons, 100.00% mask coverage; sampled token contained in its own kept set at 58,570/58,570 checked positions.top_p = 0.95, top_k = 20: reward 0.21 → 0.87, 0% rollout errors, mismatch KL 0.0006–0.0126. All 162 micro batches carry masks; mean kept 3.7, median 2, p99 17, max 19 ≤ 20, 100.00% coverage; membership 60,854/60,854.W&B:
reverse-text/reverse-text-native-replay-{baseline,topp095,topp095-topk20}. Payload sizes were checked by decoding everyrollouts/step_*/rank_*.bin:countsalways aligns withinput_idsandsum(counts) == len(ids)in every micro batch; the mask streams add 40% (top_k 512) / 22% (top_k 20) to reverse-text's tiny batches — the share shrinks with real context lengths since prompt tokens carry no masks.🤖 Generated with Claude Code