feat: top-p/top-k train sampling with kept-set mask replay#2979
Draft
faresobeid wants to merge 13 commits into
Draft
feat: top-p/top-k train sampling with kept-set mask replay#2979faresobeid wants to merge 13 commits into
faresobeid wants to merge 13 commits into
Conversation
…r renorm)
Sampling with truncation (top_p < 1) renormalizes the rollout
distribution over the surviving kept set; a trainer normalizing over
the full vocab then computes biased importance ratios and runs collapse
(DeepSeek V3.2 'Keep Sampling Mask' arXiv:2512.02556 §3.1, Cognition
SWE-1.7 'sampling distribution replay').
Inference: capture the kept-set token ids in the sampler (finite
entries of the processed logprobs — logprobs_mode=processed_logprobs
already forces the mask-materializing native sampling path) and ride
them through the existing logprobs pipeline as a -1-separated row
extension, so no fixed msgspec/dataclass schema needs new fields
across the worker->engine->API process boundaries. An API-side patch
splits the extension off before vLLM builds logprob dicts (stock
logprobs stay byte-identical for chat/eval consumers) and attaches it
to the finished CompletionOutput; /inference/v1/generate serializes it
as base64 {ids, counts} per choice, mirroring routed_experts. Kept
sets are decode-only, so the PD router passes them through unchanged.
Trainer: enable_sampling_mask_replay renormalizes logprobs over the
kept set — logprob = logits[label] - logsumexp(logits[kept]) — in both
the chunked fused LM head (online kept logsumexp + backward softmax
restricted to kept ids) and the vanilla path. Positions without a mask
(context tokens, kept sets above inference.kept_tokens_max) fall back
to full-vocab logprobs; the bias is bounded by -log(top_p). Singleton
kept sets give logprob 0 and zero gradient. Entropy stays full-vocab.
Config: orchestrator.train.sampling.top_p (was hardcoded 1.0),
inference.enable_return_kept_tokens/kept_tokens_max,
trainer.enable_sampling_mask_replay with auto-setup + a loud warning
for truncated sampling without replay.
Pairs with kept-token commits in deps/renderers (parse + strip fast
path) and deps/verifiers (TurnTokens -> graph -> Branch.kept_tokens).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- fused backward: mask non-kept logits to -inf before exp — a non-kept logit above the kept-set logZ overflowed exp() to inf and poisoned grads with inf * 0 = NaN via the indicator multiply (reproduced) - sampler capture: fixed cap+1 extension width chosen device-side, with overflow detected via the extra column — removes the per-step host syncs (.item()/.any()) that stalled the engine loop; cap default 2048 -> 512 to bound the per-step D2H and trainer padding - fail fast instead of running silently biased: reject speculative decoding and logprobs_mode overrides at server start (both bypass or invalidate the capture), reject VLLM_USE_V2_MODEL_RUNNER=1 in the patch, reject softcapped (Gemma) lm_heads at trainer start, and raise in the trainer when replay is on but an rl-bearing micro batch carries no kept sets (pure-ce and dummy batches legitimately don't) - warn on truncation smuggled via extra_body (top_k/min_p/top_p) and on opd/opsd + replay (ref logprobs stay full-vocab prefill scores) - kept_tokens_max: Field(ge=1); transport: kept_tokens moved to the end of TrainingSample/MicroBatch so the positional (array_like) layout of earlier fields stays stable across versions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fused fn: collect kept logits into a persistent [tokens, K] buffer with one logsumexp at the end instead of the online accumulator (deletes the -inf-guarded helper), hoist the int64 cast to once per token chunk, use an int8 indicator + in-place masked_fill_ in backward (drops ~200MiB of fp32 temporaries per inner iteration), save kept_tokens/replay as None instead of sentinel empties, and share the replay-eligibility predicate (kept_replay_mask) with the vanilla path so the two can't drift. Sampler patch: mirror the stock forward signature instead of *args index arithmetic; stop masking extension logprob values nobody reads (one full(-inf) block, no per-step device-scalar tensors). Processor patch: always track kept rows from the first call, killing the position counter and mid-request backfill; split rows with 2D slices at the uniform separator column instead of per-row nonzero + np.stack. Trainer wiring: gate the kept H2D copy on the replay flag, run the missing-masks check on the CPU-side tensors (no per-micro-batch device sync), express the kept shift through shift_tensor_left (generalized with pad_value via full_like), null kept_tokens on dummy batches, and drop the no-op _copy_kept_tokens. Configs: TrainSamplingConfig.truncates_distribution() owns the extra_body sentinel knowledge; the RL validator just calls it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Truncated train sampling without replay has no legitimate use (every source — DeepSeek V3.2 §3.1, Cognition SWE-1.7, Miles' ablation — reports collapse, and slime hard-requires masks when top_p != 1), and the top_p field is new in this branch so no existing config depends on the naive combination. The rl entrypoint now flips trainer.enable_sampling_mask_replay (and with it inference.enable_return_kept_tokens) whenever any train sampling config truncates; an explicit enable_sampling_mask_replay = false opts out with a loud warning (naive-top-p baselines). The inference-flag override warning now only fires when the user explicitly set it false. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y exact top_k at sampling time bounds every kept set to k, so replay never overflows into the full-vocab fallback — but only if the capture cap covers it. A cap below k would silently overflow every truncated position, and the fallback's -log(top_p) bias bound doesn't hold when top-k does the trimming (with top_p=1 it's log of the top-k mass, unbounded). Add orchestrator.train.sampling.top_k (emitted top-level, whitelisted through verifiers; extra_body still works), include it in truncates_distribution, and have the rl entrypoint raise inference.kept_tokens_max to the configured top_k with a warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When sampling-mask replay is on, every truncating train sampling config gets a top-k bound (defaulted to 512 when only top-p/min-p truncate — rarely binding at typical top_p, so the sampling policy is essentially unchanged; explicit top_k wins). Kept sets are then bounded by k and inference.kept_tokens_max is derived as the largest configured top_k, so the overflow fallback is unreachable for policy tokens and replay is exact everywhere. kept_tokens_max is no longer meant to be set by hand (only standalone-launched servers need it, to cover their clients' top_k); an explicit value below the derived bound is bumped with a warning, a larger one is respected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- top_k rides extra_body as the single carrier (mirroring EvalSamplingConfig), with the field overriding any smuggled value — previously it was emitted top-level while extra_body kept the stamped -1 sentinel, leaving precedence to per-client merge order. effective_top_k becomes a precedence read and truncates_distribution composes with it, so top-k sentinel decoding lives in one place. - auto_setup_sampling_mask_replay: per-env sampling configs are authoritative (group values are merged into envs by resolve_env_defaults; the group only stands in when no envs exist) — this also fixes a false positive where group-level truncation with every env overriding to top_p=1 enabled replay that no rollout would feed, crashing the trainer's missing-masks check. Flatten the accreted branches (compute truncating/unbounded lists once, drop the no-op branch), derive kept_tokens_max unconditionally (warn when overriding an explicit value — matching what the docstring already claimed), and document all four actions in the validator docstring. - warn_mask_replay_with_ref_kl: envs inherit the top-level algo before RL validators (inherit_env_algorithms), so check env algos directly — drops a false positive when every env overrides an opd top-level algo. - Single source for the coupled 512s: DEFAULT_KEPT_TOKENS_MAX in inference.py is both the standalone capture-width default and the injected replay top-k. Field docstrings trimmed to field-local facts pointing at docs/inference.md, which now also mentions the injection warning users will see. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Truncated train sampling now requires sampling-mask replay outright — an explicit enable_sampling_mask_replay=false with truncation is a config error instead of a warned opt-out (the naive combination has no use beyond reproducing a known collapse). opd/opsd with replay is rejected: reference logprobs are full-vocab prefill scores and would mix normalizations in the ref_kl term. The gibberish/repetition filters read rollout logprobs against full-softmax thresholds that misfire under renormalization (singleton kept sets read as probability 1.0): the default-list instances are removed automatically, explicitly configured ones are rejected; zero_advantage is unaffected. Also cover env-less configs in the algo check (top-level algo fallback, mirroring the sampling-config fallback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nfig Four decisions that collapse the config policy from ~160 lines across four files to ~60 in two: 1. trainer.enable_sampling_mask_replay deleted. Replay is data-driven: the trainer replays kept masks whenever a batch carries them, like every other per-token stream (temperatures, rl_weights, mm_kwargs). The missing-masks guard moves to the orchestrator, which knows sampling was truncated: TrainEnv.requires_kept_masks (truncating + policy-sourced) is enforced per sample at group finalization. Gemma softcapped heads fail on their existing first-batch assert. 2. Truncation policy lives on OrchestratorConfig (it reads only orchestrator state): top-k defaulting, opd/opsd rejection, and logprob-filter pruning in one setup_truncated_sampling validator — scoped to policy-sourced envs, since frozen sources sample on external endpoints and never train importance ratios. RLConfig keeps only the cross-config wiring: derive the capture width. 3. inference.enable_return_kept_tokens + kept_tokens_max merge into one field: kept_tokens (None = capture off, int = width). Enable-without- width and width-without-enable are no longer representable. 4. Truncation knobs in train extra_body are rejected (top_p/top_k/min_p are typed fields now, incl. new min_p); the sentinel-decoding helper effective_top_k and the two-carrier precedence reasoning disappear. resolve_env_config's own post-validation sentinel stamping is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop implementation internals (capture transport, width mechanics, FlashInfer note, bias bounds) and the stale limitations paragraph; keep what a user needs — why, the config, what gets rejected, the standalone-server note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop reviewer-facing narration accreted across the feature's passes — repeated citations, comparisons to other implementations, multi-line hazard essays — keeping one-to-two-line comments only where the code can't show the constraint (separator alignment, pre-exp -inf masking, wire-layout stability, sentinel stamping order). Bumps the verifiers pointer for the same trim there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… not keys The SLURM path re-parses the resolved config, which carries the disabled top_k=-1/min_p=0.0 sentinels resolve_env_config stamps into extra_body post-validation; rejecting on key presence broke the round-trip. Reject only values that actually truncate. 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.
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 < 1,top_k, ormin_p > 0) implies replay end to end:top_krespected if set, else defaulted to 512, which rarely binds at typical top_p), so kept sets are never larger than k. Truncation knobs must be the typed fields — smuggling them viaextra_bodyis rejected. Frozen-source envs are exempt (external endpoints, no importance ratios).inference.kept_tokens(None= capture off, int = capture width) is derived as the largest configured top_k, so no kept set ever overflows — replay is exact at every position. It's only set by hand for standalone-launched inference 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.24 wheel — same pattern as router replay; no upstream support exists):-1-separated row extension, 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.logprobs_modeoverrides,VLLM_USE_V2_MODEL_RUNNER=1.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.Paired dep branches
Submodule pointers in this PR pin:
PrimeIntellect-ai/renderersfeat/kept-tokens-parse— parsekept_tokensfrom/generateresponses (base64 splice fast path).PrimeIntellect-ai/verifiersfeat/kept-tokens-branch—TurnTokens→ graph attribution →Branch.kept_tokens.PRs for both still need to be opened.
Verification
All CPU-level, against the real installed vLLM 0.24:
Sampler.forwardproduces kept sets matching an independent implementation of vLLM's top-p semantics; the returned sampled-token logprob equals the renormalized reference; greedy/untruncated steps stay position-aligned.inference.kept_tokens(including explicit-value override),extra_bodytruncation rejection, opd/filter guards, frozen-source exemption, standalone-server warning.End-to-end GPU validation (H200,
top_p 0.97 / top_k 512):mismatch_klbounded 0.0005–0.0094, entropy rising, 0% errors.hendrycks-sanity/topp0.97-topk512-replay):mismatch_kl0.0003–0.0004 at every step, flat from step 1 to 200 — below an untruncated control run's noise floor (0.00042–0.00046), since singleton kept sets contribute exactly zero mismatch. Train reward 0.49 → 0.68; AIME2024 eval 0.1875 → 0.2458; entropy flat (no collapse); DPPO trust-region masking 0.0000 in both runs; replay costs ≈5% MFU vs control.top_k=512never binds, 100.00% mask coverage on sampled tokens, ~32 B/token on the wire (~40% of the trainer payload, trivial in absolute bandwidth).Known gap from the ablation: legacy v0 envs are not wired for kept tokens — truncated sampling on a v0 env fails loudly at the first group (the orchestrator's missing-mask guard). Needs the v0
ResponseTokenspath wired for parity (routed_experts supports v0) or documented v1-only support.🤖 Generated with Claude Code