Repository navigation
perf(v1): accept packed CSR sampling masks from prime-rl's /generate - #2778
Merged
Merged
Conversation
This was referenced Oct 6, 2026
faresobeid
marked this pull request as ready for review
October 7, 2026 14:51
Contributor
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a narrowly scoped compatibility enhancement that decodes prime-rl’s packed sampling-mask payload into the existing representation while preserving the list-based path. The behavior is covered by a focused equivalence test and does not affect schemas, deployment, security, or other production workflows. No code changes detected at You can add or adjust custom eligibility rules. Learn more. |
faresobeid
force-pushed
the
perf/packed-sampling-mask
branch
2 times, most recently
from
October 10, 2026 15:21
cef65c3 to
dc361d8
Compare
This was referenced Oct 11, 2026
faresobeid
force-pushed
the
perf/packed-sampling-mask
branch
from
October 14, 2026 14:26
ec0bbed to
8f1a6ce
Compare
mikasenghaas
approved these changes
Oct 14, 2026
faresobeid
added a commit
to PrimeIntellect-ai/prime-rl
that referenced
this pull request
Oct 14, 2026
…r GC (#3900) ## Problem Under RL load (GLM-4.5-Air, 1 trainer + 1 inference node, ~130-300 concurrent 16k-token requests), the vLLM API server spends 25% (top_p 1.0) to 42-43% (top_p 0.97 + sampling replay) of its wall time in gen-2 GC, with single pauses up to 64 s. During a pause the server answers nothing (/health, /metrics, completions, weight-update calls). With sampling replay this cost -30% effective generation throughput and +50% step time, and with default router health checks 25-83% of rollouts failed. Cause: tens of millions of GC-tracked per-token objects alive at once in the API-server process: `Logprob` objects and dicts per token plus decoded strings (output processor), one pydantic object per token in `_create_tokens_logprobs`, one Python list per token for the sampling mask, and the JSON encoding of all of it. Every gen-2 pass walks them all. ## Change `/inference/v1/generate` (prime-rl's `PrimeRlServingTokens`) now builds no per-token objects: - **Packed payloads**, encoded like `routed_experts` (one `encode_array` helper for all three; `routed_experts.py` is removed). `routed_experts` adds `start`, anchored at the last forwarded token: `len(prompt) + len(completion) - 1 - len(rows)`. Without P/D this equals `routed_experts_prompt_start`; on a P/D decode instance (#3830) the rows begin at its first forward. - `choice.completion_logprobs = {data, shape, dtype}`: base64 float32, the sampled token's logprob per completion token (clamped at -9999 like upstream). `choice.logprobs` is no longer set, so `/generate` no longer returns `top_logprobs` (whatever `sampling_params.logprobs` asks for). renderers asks for `logprobs = 1` and only reads the sampled logprob, so nothing in our stack uses them. - `choice.sampling_mask = {"ids": {...}, "counts": {...}}`: CSR base64 int32 arrays instead of `list[list[int]]`. - **Flat logprobs, no detokenization**: non-streaming `/generate` requests get `flat_logprobs = True` (vLLM's `FlatLogprobs`: a few lists per request instead of a dict + `Logprob` objects per token) unless they ask for prompt logprobs, and `detokenize = False` unless they set stop strings (`/generate` returns token ids only, so the text and the per-token decoded logprob strings were never used). - **Masks stay numpy in the output processor** (`monkey_patch_packed_sampling_masks`, non-streaming requests only): keep only the per-step id arrays (untracked by GC) instead of a tuple per token, and hand the serving layer a `PackedSamplingMask(ids, counts)` instead of `list[list[int]]`. - **API-server GC**: after app init, `gc.collect(); gc.freeze()` (startup objects leave GC) and `gc.set_threshold(50_000, 20, 100)` (default `(700, 10, 10)`). - Streaming `/generate` and the OpenAI endpoints are unchanged. Clients: PrimeIntellect-ai/renderers#174 reads `completion_logprobs` (same checks as the per-token path); PrimeIntellect-ai/verifiers#2778 decodes the packed mask. Both keep the old forms for stock vLLM. ## Pins - `deps/renderers` 41f9290 -> ca029b5 (renderers main, the merge commit of renderers#174). - `deps/verifiers` 2cb0a6d -> e888a71 (verifiers main, the merge commit of verifiers#2778). Landing order: renderers#174 -> verifiers#2778 -> this PR, with the prime-rl re-pins to renderers/verifiers main in the same commit as the server change. An old client against a new server gets no `logprobs.content`. ## Measurement One node, GLM-4.5-Air TP8, 130 concurrent closed-loop requests (Polaris prompts, top_p 0.97 / top_k 512, max 16k tokens, ~12.5k tokens/response), 15 min per arm, before (main) vs after (this branch): | arm | gen tok/s | done reqs | errors | gen2 GC % wall | all GC % wall | gen2 count | max GC pause s | health p50/p99/max s | resp MB mean | client parse ms mean | tokens/resp | |:------------------|------------:|------------:|---------:|-----------------:|----------------:|-------------:|-----------------:|:-----------------------|---------------:|-----------------------:|--------------:| | base-nomask | 5497 | 512 | 0 | 11.8 | 13 | 164 | 1.87 | 0.003/1.34/21.7 | 1.71 | 28.5 | 12490 | | fix-nomask | 5601 | 516 | 0 | 0 | 0 | 0 | 0 | 0.003/0.01/0.3 | 0.12 | 1 | 12619 | | base-mask | 5453 | 508 | 0 | 23.8 | 25.8 | 111 | 4.27 | 0.003/4.37/30.1 | 1.83 | 31.4 | 12479 | | fix-mask | 5446 | 504 | 0 | 0 | 0.9 | 0 | 0.99 | 0.003/0.07/1.8 | 0.31 | 1.4 | 12582 | | fix-mask-nogctune | 5480 | 512 | 0 | 5.1 | 5.7 | 31 | 2.63 | 0.003/1.94/2.8 | 0.31 | 1.3 | 12500 | The closed-loop tok/s does not move, because the engine core keeps generating during API-server pauses. The RL cost came from what the pauses block (health checks, dispatch, responses, weight updates), measured here by /health latency. Ablation: packing alone takes gen-2 GC from 23.8% to 5.1% of wall time; gc.freeze + thresholds take it to zero gen-2 passes. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the `/generate` response contract and patches vLLM internals; clients must understand packed fields, though streaming and OpenAI routes are unchanged. > > **Overview** > Under heavy RL concurrency, `/inference/v1/generate` no longer builds millions of per-token Python objects in the API server. **Non-streaming** responses now return **packed, base64 numpy payloads** for `routed_experts`, new `completion_logprobs` (sampled-token logprob per position), and CSR `sampling_mask` (`ids` + `counts`), with serialization consolidated in `serving_tokens` and `routed_experts.py` removed. > > The handler enables **flat logprobs** and disables **detokenization** when safe (no prompt logprobs / no stop strings), so upstream stops materializing `Logprob` dicts and decoded strings. A vLLM **output-processor monkey patch** keeps sampling-mask chunks as numpy until finish for `FINAL_ONLY` requests. > > After app startup, the API server runs **`gc.collect()` / `gc.freeze()`** and raises GC thresholds to cut gen-2 pauses. Unit tests cover packing and `post_process`; **`renderers`** is pinned to a release that reads the new fields (paired client PRs). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a6f0da5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
samsja
added a commit
to PrimeIntellect-ai/prime-rl
that referenced
this pull request
Oct 15, 2026
…nvs to latest main (#4056) Bumps the **renderers** and **prime-envs** submodules to latest main and moves the first-party pins to their released floors (**verifiers 0.4.0**, **renderers 0.1.12**). Re-landed on current main: the previous version of this PR conflicted with the v0.10.0 release. | dep | from | to | |---|---|---| | verifiers | `e888a71` (main's pin) | unchanged; pyproject floor -> released `>=0.4.0` | | renderers | `ca029b5` | `5d870d8` (main) | | prime-envs | `89dcc7f` | `91d7d15` (main) | **Why verifiers stays at main's pin instead of the `v0.4.0` tag:** main's pin is v0.4.0+12: it carries [#2778](PrimeIntellect-ai/verifiers#2778) (packed CSR sampling-mask decoding), which the packed per-token `/generate` payloads from #3900 require. Pinning the tag exactly would break router-replay runs: the tag's `SamplingMask.from_sampling_mask` only accepts the one-list-per-token form, while `/generate` emits packed `{ids, counts}`. So this PR holds verifiers where main has it (the 0.4.0 line) and only raises the floor to the released `0.4.0` — no new verifiers commits; the unreleased refactors past main (task workspace deployments #2833, agentic judging in the selected workspace #2832, verification into the rollout lifecycle #2830, Runtime view split from provider backends #2827) stay out. What comes in: - **renderers** (1 commit): GLM-5.3 Flash renderer with image support (#191) - **prime-envs** (9 commits): HLE variants and ALE CLI evaluation adapter (#979), require verifiers 0.4.0 across all environments (#983), fix lean memory for isolated proof grading (#986), fix nl2repobench offline VM image selection (#985), fix lab Prime image namespace (#984), fix terminal-bench-2 repaired QEMU task images (#974), fix swe staged task inputs for Harbor 0.24 (#975), remove pi-terminal-bench-2 (#973), speed up pull request smoke tests (#976) **Workspace change:** `deps/prime-envs/environments/office/ale_cli` is excluded from the workspace. It requires `numpy>=2.4.4` (via the agents-last-exam `tasks` package), and vLLM 0.31.0 requires `numpy<2.4` via `mistral-common`, so it can't share the lock (same treatment as tau2/apex). The lock changes only first-party packages: env versions move (deep-swe 0.1.2, hle 0.3.1, swe-sweep 0.1.1, terminal-bench-2 0.1.1; pi-terminal-bench-2 removed) and envs align on `verifiers>=0.4.0` with explicit `datasets>=3.0.0`. No third-party version moved. Validated locally: `uv lock` resolves and `uv lock --check` is clean; CPU unit tests pass against the bumped sources (`uv run pytest tests/unit -m "not gpu"`: **827 passed, 1 skipped, 236 deselected**). Not yet run: GPU/integration CI and a sandboxed RL smoke run. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Wide verifiers 0.4 rollout and workspace exclusion affect every env in the shared lock; upstream changes to verification and task workspaces may change RL/eval behavior even though prime-rl source is mostly unchanged. > > **Overview** > Bumps first-party training stack deps to released floors: **`verifiers[harbor]>=0.4.0`** and **`renderers[multimodal]>=0.1.12`** (dropping dev pre-release pins), with **`uv.lock`** refreshed so workspace env packages match. > > **Workspace:** **`deps/prime-envs/environments/office/ale_cli`** is added to **`tool.uv.workspace` `exclude`** because its numpy floor conflicts with vLLM’s numpy cap—same pattern as tau/apex envs. > > **Lock alignment:** Editable prime-envs now require **`verifiers>=0.4.0`**; many dataset-driven envs also declare explicit **`datasets>=3.0.0`**. **`pi-terminal-bench-2`** drops out of the workspace/lock; a few env package versions tick up (e.g. **deep-swe**, **hle**, **swe-sweep**, **terminal-bench-2**). No third-party PyPI version churn beyond first-party metadata in the lock. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b9750ab. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
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.
SamplingMask.from_sampling_masknow also accepts prime-rl's packed CSR form.prime-rl's
/inference/v1/generateserver now returns sampling masks as{"ids": {data, shape, dtype}, "counts": {data, shape, dtype}}(base64 int32 arrays) instead oflist[list[int]]. The one-list-per-token form was a large part of the vLLM API server's GC load and JSON size under RL traffic (see the prime-rl PR). Here it also skips building one Python list per token when the client parses the response.idsandcountsstraight into the existing int32 arrays (_decode_packed, which feat(v1): optional sampler logprobs on SamplingMask #2756 reuses for its packed logprobs).Companions: renderers PrimeIntellect-ai/renderers#174 / prime-rl PrimeIntellect-ai/prime-rl#3900. Land renderers -> verifiers -> prime-rl; the prime-rl submodule pins must move to the merged commits.
Validation
tests/v1/test_trace.py tests/v1/test_graph.py: pass (one new test: packed and list inputs give the same arrays).ruff check/ruff format(0.16.0) on the changed files: clean.Note
Low Risk
Additive parsing path with existing list behavior preserved; main caveat is packed
shapeis not validated during decode.Overview
Extends
SamplingMask.from_sampling_maskso verifiers can ingest prime-rl’s packed CSR sampling masks from/generate, not only vLLM’s per-tokenlist[list[int]]form. When the input is a dict withidsandcounts, each value is decoded via a new_decode_packedhelper (base64data+dtype→numpyint32 arrays) instead of concatenating Python lists per token.List-based input is unchanged. A unit test asserts packed and list representations yield the same
idsandcountsarrays.Reviewed by Cursor Bugbot for commit 8f1a6ce. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Accept packed CSR sampling masks in
SamplingMask.from_sampling_maskSamplingMask.from_sampling_maskin types.py now accepts either the existing list-of-lists input or a dict with packedidsandcountsarrays, matching prime-rl's/generateformat_decode_packedhelper that base64-decodes the packeddata, interprets it with the packed dtype, and casts to the requested dtypeidsandcountsarraysshapemetadata in_decode_packedis ignored; the arrays are reconstructed only fromcountsandidsMacroscope summarized 8f1a6ce.