feat: top-p/top-k train sampling with sampling replay - #3235
Draft
mikasenghaas wants to merge 3 commits into
Draft
feat: top-p/top-k train sampling with sampling replay#3235mikasenghaas wants to merge 3 commits into
mikasenghaas wants to merge 3 commits into
Conversation
Squash of feat/top-p-mask-replay (PR #2979) onto current main, adapting to the vllm pass-through inference config, multi-tenant removal, and the v0 env compat drop. Co-authored-by: fares <fares@primeintellect.ai> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nothing between sampler and API process pairs logprob ids and values column-wise, so the -inf float filler rows were pure IPC overhead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The enable flag rides vLLM's additional_config as enable_return_sampling_mask (named after the in-flight native vLLM flag, vllm-project/vllm#49577), snapshotted at Sampler.__init__ like the fp32 patches. The API-process patches are data-driven off the separator id and install unconditionally. The capture width is a fixed constant; the orchestrator rejects train-sampling top_k above it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Supersedes #2979 (same feature, re-cut from current
main; see What changed vs #2979).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; our 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 makes truncation safe by recording the kept set at sampling time and renormalizing trainer logprobs over the same set: DeepSeek V3.2's "Keep Sampling Mask" (arXiv:2512.02556 §3.1), also described in Cognition's SWE-1.7 post as "sampling distribution replay".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 — see below), so kept sets are never larger than the capture width. Truncation knobs must be the typed fields — smuggling them viaextra_bodyis rejected. Frozen-source envs are exempt (external endpoints, no importance ratios).inference.enable_return_sampling_mask(bool, named after vLLM's in-flight native flag) turns on capture at a fixed width of 512; the orchestrator rejects train-samplingtop_k > 512so no kept set ever overflows — replay is exact at every position. The flag is auto-set and persisted into per-node configs; hand-setting is only for standalone-launched servers.opd/opsd(reference logprobs are full-vocab prefill scores), and the gibberish/repetition filters (removed from the default lists, rejected if explicitly configured — their full-softmax thresholds misfire when singleton kept sets read as probability 1.0).How it works
Inference (
src/prime_rl/inference/vllm/kept_tokens.py, monkey patches over the stock vLLM 0.26 wheel):-1-separated extension on the id tensor (ids only — nothing between sampler and API process pairs ids and logprob values column-wise), at a fixed device-side width (no host syncs). An API-process patch splits the extension back off before vLLM builds logprob dicts — chat/eval consumers see byte-identical logprobs — and/inference/v1/generatereturns base64{ids, counts}per choice, likerouted_experts. Kept sets are decode-only, so PD-disaggregated serving needs no router changes.additional_configasenable_return_sampling_mask, snapshotted atSampler.__init__where vLLM guarantees a config context (the fp32_lm_head mechanism). The API-process patches are data-driven off the separator id and install unconditionally — rows without extensions pass through untouched.logprobs_modeoverrides,VLLM_USE_V2_MODEL_RUNNER=1(prime-rl pins the V1 runner anyway).Upstream path: vLLM is adding native support with the same semantics and constraints — vllm-project/vllm#49577
enable_return_sampling_mask, near-merge, earliest release ~0.28 (built for the V2 model runner). Once it ships in a release we pin, the two engine patches here reduce to therouted_experts-style API-layer glue (KeptTokensCapture+ serializer). On released vLLM the only patch-free alternative today is requestinglogprobs = top_kper token, which ships k ids+floats per position through vLLM's per-position logprob-dict machinery — orders of magnitude more transport and API-process work than this extension (~32 B/token measured).Trainer (data-driven — replays masks whenever the batch carries them):
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 (context tokens, non-policy samples) use full-vocab logprobs.Transport:
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.What changed vs #2979
main, adapted to the vLLM 0.26 bump, the pass-through[inference.vllm]config, the multi-tenant removal, and the v0 env-compat drop — which also retires feat: top-p/top-k train sampling with sampling replay #2979's known gap (kept tokens were v1-only; v0 envs no longer exist).-inffloat filler rows were pure IPC overhead (halves the extension's engine→API traffic).enable_return_sampling_mask) with a hardcoded capture width, transported viaadditional_configinstead of env vars; the orchestrator rejectstop_k > 512instead of deriving a width.Paired dep PRs (already merged and pinned)
kept_tokensfrom/generateresponses (base64 splice fast path).KeptTokensdataclass:TurnTokenscarrier → graph attribution →Branch.kept_tokens.Both are ancestors of main's current submodule pins — this PR does not touch submodules.
Verification
Checks on this branch:
uv run ruff check/ruff format --check,uv lock --checkuv run pytest tests/unit/test_configs.py tests/unit/inference/(120 passed)uv run pytest tests/unit/train/ tests/unit/orchestrator/(165 passed; the one failure,test_qwen3_vl_e2e.py, fails identically onmain— pre-existing, fix: token_id-formatted logprob tokens in the qwen3-vl fake engine #3161)top_p 0.97on reverse-text resolves totop_k = 512(with warning) andinference.toml: kept_tokens = 512.selective_log_softmax_with_keptand the fused_SequenceChunkedLogProbEntropyFn(forward + backward) match a dense masked-renormalization reference (float32 error ≤ 5e-7; misaligned-mask fallback; singleton kept set → logprob 0, exactly zero grad).End-to-end on reverse-text (Qwen3-0.6B-Reverse-Text-SFT, 20 steps, 1 trainer + 1 inference GPU), both runs from this branch:
mismatch_kl0.0007–0.0153. Traces carrytop_p = 1.0; capture stays off (nocapture ENABLEDengine log), logprobs unaffected.top_p 0.97(replay): reward 0.19 → 0.75, 0% rollout errors,mismatch_klbounded 0.0006–0.0143, entropy healthy. Traces carrytop_p = 0.97/top_k = 512; the engine logsKept-set sampling-mask capture ENABLED for this Sampler instance (cap=512)(viaadditional_config); and since the orchestrator raises on any truncating sample without masks, completing 20/20 steps means every trainable sample shipped its kept sets. W&B:reverse-text/reverse-text-{baseline,topp0.97}-pr3235.top_k = 1024with truncation fails config validation with the fixed-capture-width error.Prior validation on #2979 (same logic; CPU tests were out-of-band, GPU runs on H200):
mismatch_kl0.0003–0.0004 flat from step 1 to 200 — below an untruncated control's noise floor; train reward 0.49 → 0.68; AIME2024 eval 0.1875 → 0.2458; entropy flat; replay ≈5% MFU vs control.top_k = 512never binds, 100.00% mask coverage on sampled tokens, ~32 B/token on the wire.🤖 Generated with Claude Code