Repository navigation
feat(rl): one paired GPU capture and one support gather for exact score centering (follow-up to #4176) - #4232
Open
faresobeid wants to merge 29 commits into
Open
faresobeid wants to merge 29 commits into
faresobeid wants to merge 29 commits into
Conversation
- Truncated train sampling without top_k now defaults to top_k = 128; explicit top_k is still capped at 512. - When a request records a sampling mask, the inference server drops filtered (-inf) candidates from compact logprobs, so each row carries only the recorded support instead of top_k sentinel-padded entries. - Masked tokens' sampler logprobs move onto the transport SamplingMask (new optional `logprobs`), so the support ids cross to the trainer once. TopLogprobs only carries heads of untruncated distributions, and the trainer builds the centering head from both. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ng-replay # Conflicts: # src/prime_rl/inference/patches.py
) Truncated training should not force eval requests on the same vLLM engine to use a positive temperature and bounded top-k. This PR targets `feat/icepop-sc-sampling-replay` (#4176), not main. - Policy-training requests opt in to mask capture with `sampling_params.extra_args.prl_replay`. Validation retains the positive-temperature/top-k requirements only for opted-in requests. Other request validation still runs against a private model-config view; the shared engine config is never toggled. - The V2 sampler tracks capture per request slot. Mixed batches zero capture-only committed-token counts for eval rows, so the capture kernel skips those rows before reading logits. Only replay rows set the compact support width; eval-only batches allocate no sampling masks. Normal sampled-token counts and logits are unchanged. Empty eval supports are discarded before response assembly. - Online eval retains independent top-p/top-k/greedy settings, disables logprobs, and explicitly disables an omitted top-k instead of inheriting it from the model generation config. Its existing chat endpoint strips router payloads. Training's renderer client keeps its required logprobs. - The parent's top-p-before-top-k ordering continues to apply to both train and eval on the capture-enabled engine, independently of per-request capture. Limitations: request-scoped capture currently rejects speculative decoding because its separate rejection sampler is not adapted here. Router instrumentation and internal router transport remain engine-wide; this removes eval response payloads, not that model-side overhead. This is a vLLM 0.32 compatibility patch using private APIs, pending native per-request support. Validation: - Seven CPU/GPU sampler tests passed on the devbox, including training alongside unbounded eval supports and an eval-only capture bypass. - Eight focused config tests passed, including resolved-config round trips and independent top-p-only/greedy eval settings. - Broader local config run: 210 passed; nine failed because optional tasksets or `psutil` were not installed (39 example cases deselected). - Real vLLM 0.32 V2 engine smoke test on an RTX PRO 6000 with a tiny dummy-weight Llama: mixed, eval-only, training-only, and reordered batches. This is an inference wiring test, not a full training or throughput benchmark. - Ruff and `git diff --check` pass.
…replay request, one support gather
hallerite
force-pushed
the
feat/icepop-sc-sampling-replay
branch
3 times, most recently
from
October 24, 2026 14:36
04ef127 to
dbeb8ce
Compare
# Conflicts: # docs/inference.md # packages/prime-rl-configs/src/prime_rl/configs/inference.py # packages/prime-rl-configs/src/prime_rl/configs/orchestrator.py # src/prime_rl/inference/vllm/paired_capture.py # src/prime_rl/inference/vllm/serving_tokens.py # src/prime_rl/orchestrator/batch.py # src/prime_rl/orchestrator/metrics.py # src/prime_rl/orchestrator/trajectories.py # src/prime_rl/trainer/models/layers/lm_head.py # src/prime_rl/trainer/rl/loss.py # src/prime_rl/trainer/rl/train.py # tests/unit/orchestrator/test_batch.py # tests/unit/orchestrator/test_metrics.py # tests/unit/train/rl/test_loss.py
faresobeid
marked this pull request as ready for review
October 24, 2026 18:33
… the GPU Untruncated score-centered requests no longer use vLLM's logprob return. The patched sampler writes the sampled token plus the top ids (vLLM's logprob layout) or K i.i.d. draws from the processed distribution (extra_args.prl_sampler_draws) into the packed sampling-mask transport. The server returns them as compact logprobs. Removes compact_logprobs.py and the logprobs_mode/max_logprobs setup; score centering enables enable_return_sampling_mask instead. Bumps renderers for sampler-draw rows (PrimeIntellect-ai/renderers#202).
samsja
pushed a commit
that referenced
this pull request
Oct 26, 2026
…sks on (#4267) With `enable_return_sampling_mask` on, vLLM generation throughput dropped by about 45% (Qwen3-1.7B, H200). The cost was not the mask transport. It came from GPU->CPU syncs inside the sampler step. Each sync makes the engine thread wait for the current forward pass, which removes vLLM's async-scheduling overlap. py-spy on the EngineCore showed the busy loop 100% busy, with about 30% of it spent waiting in `sampling_order.py:29` (`k.max().item()`). The sampler kernels themselves cost about 1 ms per step. Syncs removed: - `apply_top_p_then_top_k` (on by default through `top_p_before_top_k`, so it ran for every top-k/top-p request on a mask server, replayed or not): `k.max().item()` and the tie-fallback `.any().item()`. The width now comes from the host `top_k` array, set in a `Sampler._sample_random` wrapper. Candidates are ranked by an int64 `(score, token id)` key, so ties at the boundary resolve to the reference's stable order without a data-dependent fallback. The result is bit-identical to `reference_top_p_then_top_k`, including bf16-rounded logits with boundary ties. The old fast path fell back to the full sort on those logits. - `capture_selected_supports` and `pack_support_logprobs`: `torch.as_tensor(numpy, device=cuda)` (a pageable H2D copy that syncs) becomes a pinned `non_blocking` copy. Sampling semantics, masks, and logprobs are unchanged. There are no loss changes. ## Numbers Qwen3-1.7B, one H200 per server, 512 concurrent x 1024 tokens (`min_tokens` = `max_tokens`), T=1, seeded; replay/top-k requests use top_k=128, top_p=0.95. tok/s, two runs each. The columns come from separate jobs. Untruncated requests have no syncs on either branch, so that row shows the run-to-run noise (about 10%): | request | main, masks off | main, masks on | this PR, masks on | |---|---|---|---| | untruncated, sampled logprob | 15.8-16.7k | 15.5-16.3k | 17.3-18.4k | | top-k/top-p, no replay | 14.3-17.6k | 9.0-10.1k | 15.4-15.5k | | replay | - | 8.2-9.6k | 14.4-14.9k | On #4232's code, which deletes `sampling_order.py`, the same pinned copies plus the matching fixes in `pack_sampler_rows` give: - untruncated + 16 draws: 9.3-10.6k -> 17.1-17.4k (-6% vs untruncated) - replay: 8.6-9.2k -> 14.4-16.9k The rest of the replay gap on main is GPU time in the top-p-first sampler: 4.1 ms vs vLLM's Triton top-k/top-p at 0.9 ms for 512 rows. `apply_top_p_then_top_k` microbench, 512 x 151936 bf16-rounded logits: old 9.5 ms (tie fallback to the full sort), new 4.1 ms, reference 8.4 ms. With `VLLM_GPU_SYNC_CHECK=warn`, vLLM reports no syncs in `execute_model`/`sample_tokens` for replay, top-k, and draws requests. ## Overlap - #4232 (feat/sc-exact-support) deletes `sampling_order.py` and has the same `as_tensor` syncs in `request_replay.py`/`paired_capture.py`, plus four more in `pack_sampler_rows` (numpy-indexed CUDA tensors and `as_tensor` copies). If #4232 lands first, only the two pinned-copy lines here still apply, together with the `pack_sampler_rows` fixes on that branch. - #4213 (metrics) does not touch inference. ## Validation - `pytest tests/unit/orchestrator tests/unit/test_configs.py tests/unit/inference -m "not gpu"`: 445 passed, 1 skipped. `ruff check` and `ruff format --check` are clean. - GPU (H200): `pytest tests/unit/inference/test_sampling_order.py`: 14 passed. - Bench scripts: `/home/fares/scratch/smt/bench` (client.py, val.sbatch). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes core top-k/top-p sampling and mask packing on every masked request; behavior is intended to be bit-identical to the reference, but the paths are performance-critical inference code. > > **Overview** > Restores vLLM throughput when **sampling masks** are enabled by eliminating **GPU→CPU syncs** in the hot sampling and mask-capture paths, without changing sampling semantics or logprobs. > > **`apply_top_p_then_top_k`** no longer calls `k.max().item()` or a tie-check `.any().item()`. Batch max top-k is supplied from host numpy via a **`ContextVar`**, set in a **`Sampler._sample_random` wrapper** during `patch_top_p_then_top_k`. The bounded path ranks candidates with a composite **(score, token id)** key so boundary ties match the reference stable order without a data-dependent fallback to full sort. > > **`capture_selected_supports`** and **`pack_support_logprobs`** replace syncing `torch.as_tensor(numpy, device=cuda)` with **pinned, non-blocking** host→device copies for boolean masks. > > Tests extend the patch test to preserve **`_sample_random`** under monkeypatch. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1aee656. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
# Conflicts: # src/prime_rl/inference/vllm/sampling_order.py # tests/unit/inference/test_sampling_order.py
…/topp-ragged-capture # Conflicts: # tests/unit/inference/test_serving_tokens.py
samsja
added a commit
that referenced
this pull request
Oct 27, 2026
## Summary With IcePop score centering on, `validate_score_centering_data` ran on the CPU, on the training loop's main thread, for every micro batch: - boolean masking of the `[seq, 128]` head ids and values; - a full sort of the ids to find duplicates; - `exp`, sums and support checks. The loop waits on the GPU at the end of every micro batch, so this time added straight to step time. It took ~2.4 s per 262k-token micro batch at `OMP_NUM_THREADS=1`. This PR runs the same checks on the GPU, on the copies the step already makes, with no host sync, and checks the result once per step. ## Changes - `score_centering_data_errors` runs the six checks per token on the step's GPU tensors. It returns one flag per check without syncing. - `raise_score_centering_data_errors` syncs once, after the last micro batch and before gradient scaling, clipping and `optimizer.step()`. It raises the same message the CPU validation raised for the first failing micro batch. - `topk_valid` is computed on the GPU instead of the CPU. - `validate_score_centering_data` keeps its signature. It reuses the same checks, and the trainer still calls it on the CPU only when top logprobs are missing. **Behavior change:** an invalid micro batch now fails the step after the forward/backward instead of before its forward. Gradients from bad sampler evidence are still never applied. In between, out-of-vocabulary head ids can't index out of bounds: the LM head maps them through clamped, range-masked local indices. ## Benchmarks Validation alone, on a synthetic micro batch (262,144 tokens, 128-wide head, vocab 129,280): | | per micro batch | |---|---| | CPU, `OMP_NUM_THREADS=1` (before) | 2,424 ms | | CPU, `OMP_NUM_THREADS=16` (before) | 351 ms | | GPU (this PR) | ~2 ms of kernels, 0.36 ms on the host, ~450 MiB of temporaries | End to end, on an 8-layer DSv4-Flash slice: 1×8 B300, CP=8, 4 micro batches per rank, forward/backward averaged over steps 8–10: | | s / micro batch | |---|---| | score centering on, before | 5.65 | | score centering on, this PR | 1.84 | | score centering off | 1.66 | The remaining ~0.18 s per micro batch is the score-centering loss path itself. ## Testing - `tests/unit/orchestrator/test_batch.py` checks that the GPU path raises the same error as the CPU validation for each corruption case where top logprobs are present, on CPU and CUDA. On GPU, `test_batch`, `tests/unit/train/rl/test_loss.py` and `test_fused_lm_head` pass. ruff check and format pass. - The old and new validation raise identical messages on 9 corruption types, on clean data, and on a corrupted row outside the loss mask. #4232 doesn't touch this validation. The two PRs only conflict textually, in `train.py`'s top-k block. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Training-step timing and failure timing change for IcePop score centering; invalid batches still abort before `optimizer.step()` but after backward. Logic must stay equivalent to the prior CPU checks on RL tokens only. > > **Overview** > Moves IcePop **score-centering** sampler-evidence checks off the CPU micro-batch hot path and onto **GPU tensors**, deferring a **single host sync** until after all micro-batches finish forward/backward. > > **`data.py`** factors the six RL-token checks into `score_centering_data_errors` (per-check flags, no sync) and `raise_score_centering_data_errors` (one sync, same `ValueError` messages via `SCORE_CENTERING_DATA_ERRORS`). `validate_score_centering_data` still runs on CPU when top logprobs are missing. > > **`train.py`** drops per-micro-batch CPU `validate_score_centering_data`; with logprobs present it appends GPU flags each micro-batch and calls `raise_score_centering_data_errors` before gradient scaling/`optimizer.step()`. `topk_valid` is computed on CUDA with the other top-k tensors. > > **Behavior:** bad sampler data now fails the step **after** that micro-batch’s backward (gradients still not applied); missing-logprob batches still fail immediately on CPU. > > **Tests** assert the deferred GPU path raises the same errors as CPU validation (CPU/CUDA). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9c23f38. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # src/prime_rl/trainer/rl/train.py
…ts with logprobs = 0
…ords sampler logprobs
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ed83b67. Configure here.
# Conflicts: # src/prime_rl/orchestrator/trajectories.py # tests/unit/orchestrator/test_batch.py
…flowed the capture capacity
This branch has not been deployed
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.

Every score-centered or replayed request now captures the sampler's probabilities on the GPU, in vLLM's sampling step, with no host sync. vLLM returns no logprobs for these requests. Net +584 / −770 on current main.
What changes
prl_replay) requests get each kept id with its processed logprob. Untruncated requests get the sampled token plus the top ids, or withextra_args.prl_sampler_draws = K, K i.i.d. draws from q (torch.multinomialon the global generator, so the draws are independent of the sampled token). All host copies are pinned and non-blocking, so vLLM's async scheduling keeps overlapping.(logprob << 32) | idinto one flat device buffer. No top-k cap, no full-vocab bitmask, no injectedtop_k. The buffer size isinference.sampling_support_capacity(default 16M entries, 128 MB). A step whose supports do not fit fails those requests with an explicit error; nothing is truncated. The server logs the max per-step total every minute.LossInputstakestrainer_support_logprobs,sampler_support_logprobs,support_validandsampler_tail_mass."samples": true.top_p_before_top_k/sampling_order.py(the p→k reorder; exact centering doesn't need it, since mask and logprobs come from the same processed logits), and the vLLM logprob-return path for compact requests (compact_logprobs.py,logprobs_mode/max_logprobssetup).TRAIN_TOP_K_DEFAULTinjection and the top-k bound are gone too.Why
The old path computed logprobs in vLLM with a raised
max_logprobs, synced GPU→CPU in capture (which stalled async scheduling), and padded supports to a fixed width, so top-p alone needed an injectedtop_kcap.Numbers
Qwen3-1.7B (1×H200, 512 concurrent) and GLM-4.5-Air (TP 4, 256 concurrent), T=1, top_p 0.95, 1024 forced tokens, tok/s.
replay= mask capture;sc= replay + sampler logprobs.scis lower because the API server serializes full-vocab rows at forced lengths (mean 174 kept per token); on GLM it is −6%.processed_logprobsto ≤ 8.3e-7 on identical seeded sequences.Validation
pytest tests/unit/orchestrator tests/unit/test_configs.py tests/unit/inference tests/unit/train -m "not gpu": 660 passed.ruff check,ruff format --check,uv lock --check: clean.test_serving_tokens(capture equals the masked set, logprobs equal log_softmax over it, sampler rows merge, overflow fails),test_loss,test_fused_lm_head,test_nemotron_h_kl: 53 passed.Before merge
deps/renderersto its merge commit.#4233 is stacked on this branch.
Note
High Risk
Changes the inference–trainer contract for truncated RL (logprobs, masks, capacity failures) and score-centered IcePop; misconfiguration or overflow now fails requests instead of silently bounding supports.
Overview
Sampling replay and IcePop score centering now get the sampler’s exact kept set and processed logprobs from GPU capture in vLLM’s sampling step, instead of raising
max_logprobsand shipping logprobs through vLLM’s normal path. Replay and score-centered requests setlogprobs = Noneon the server; supports are packed as CSR rows(logprob << 32) | idvia Triton over the already masked processed logits, so top-p alone works without an injectedtop_kcap or fixed-width padding.Removed the
top_p_before_top_kengine patch (sampling_order.py), standalonecompact_logprobsserialization for generic compact requests, orchestratorTRAIN_TOP_K_DEFAULT/ 512 bound and mixed top-k validation, and RL auto-setup that forcedprocessed_logprobs+max_logprobs. Addedinference.sampling_support_capacity(default 16M entries); steps that exceed capacity fail explicitly rather than truncating.The fused LM head uses one support gather for replay normalization, support logprobs, and backward; IcePop centering inputs are renamed to
*_support_*withsampler_tail_massinstead of a “complete support” flag. Mask capture auto-enables when train sampling is truncated or records sampler logprobs (score centering). Optionalprl_sampler_rows/prl_sampler_drawsreturns GPU-packed compact logprobs for untruncated score centering (including i.i.d. draws).Reviewed by Cursor Bugbot for commit 606eaad. Bugbot is set up for automated code reviews on this repo. Configure here.