Repository navigation
feat: pass router-replay ids and sampling masks by handle on multi-node runs - #3902
Open
faresobeid wants to merge 33 commits into
Open
faresobeid wants to merge 33 commits into
faresobeid wants to merge 33 commits into
Conversation
This was referenced Oct 6, 2026
faresobeid
force-pushed
the
feat/payload-segments
branch
from
October 6, 2026 14:53
1bcca41 to
d0aff47
Compare
This was referenced Oct 6, 2026
faresobeid
force-pushed
the
feat/payload-segments
branch
from
October 6, 2026 17:29
d0aff47 to
28baf5a
Compare
faresobeid
force-pushed
the
perf/packed-generate-gc
branch
from
October 7, 2026 00:39
355d9b2 to
df49fe2
Compare
faresobeid
force-pushed
the
feat/payload-segments
branch
2 times, most recently
from
October 7, 2026 13:59
cfc1d59 to
93ea816
Compare
faresobeid
force-pushed
the
perf/packed-generate-gc
branch
from
October 8, 2026 14:35
8760073 to
601f185
Compare
faresobeid
force-pushed
the
feat/payload-segments
branch
from
October 8, 2026 14:39
93ea816 to
f97b76a
Compare
faresobeid
force-pushed
the
perf/packed-generate-gc
branch
from
October 8, 2026 19:16
601f185 to
c5f8b5a
Compare
faresobeid
force-pushed
the
feat/payload-segments
branch
2 times, most recently
from
October 9, 2026 23:48
5ed3500 to
c2f9290
Compare
faresobeid
force-pushed
the
perf/packed-generate-gc
branch
from
October 9, 2026 23:48
c5f8b5a to
bb77d68
Compare
faresobeid
force-pushed
the
feat/payload-segments
branch
from
October 10, 2026 15:11
5a862b4 to
123233f
Compare
…r GC Return completion logprobs and sampling masks as base64 numpy arrays (like routed_experts), keep masks as numpy in the output processor, request flat logprobs and skip detokenization on /generate, and gc.freeze() plus higher GC thresholds in the API server. Bumps renderers/verifiers to the companion branches that parse the packed fields.
Streaming (DELTA) and cumulative outputs keep vLLM's chunk list and SamplingMask form; only FINAL_ONLY requests, which PrimeRlServingTokens reads, get the packed CSR arrays. Document that each mask chunk is one position.
faresobeid
force-pushed
the
feat/payload-segments
branch
from
October 10, 2026 15:21
123233f to
a0690a6
Compare
faresobeid
force-pushed
the
perf/packed-generate-gc
branch
from
October 10, 2026 15:21
bb77d68 to
b920328
Compare
This was referenced Oct 10, 2026
Open
# Conflicts: # src/prime_rl/inference/vllm/serving_tokens.py # src/prime_rl/orchestrator/orchestrator.py # tests/unit/inference/test_serving_tokens.py
# Conflicts: # deps/renderers # packages/prime-rl-configs/src/prime_rl/configs/rl.py # src/prime_rl/orchestrator/orchestrator.py # src/prime_rl/orchestrator/train_sink.py # src/prime_rl/orchestrator/types.py
faresobeid
added a commit
that referenced
this pull request
Oct 17, 2026
…-replay (#3970) into feat/payload-resume-filter
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 72319a0. Configure here.
# Conflicts: # src/prime_rl/inference/vllm/serving_tokens.py
…train work The sweep removed v<k> for k below min_fresh_version(step + 1), which assumes no rollout dispatched earlier can still ship. Rollouts that outlive max_off_policy_steps (no in-flight stale cancel) would lose their earlier turns' files and crash the trainer on read. Bound it by the oldest group start in flight, in the sink or in an unread batch, and run it in the main loop between items so no finished rollout is between holders.
…gregated P/D The prefill and decode engines each write their rows to a payload file and the PD router concatenates their segments (router#60), so rl no longer keeps P/D runs inline.
# Conflicts: # src/prime_rl/orchestrator/batch.py
mikasenghaas
pushed a commit
that referenced
this pull request
Oct 21, 2026
) Router replay (`enable_return_routed_experts`) costs a lot of generation throughput on vLLM 0.32. vLLM's AuxOutput store and worker code are unchanged since 0.31, where the numbers below were measured. In the v031 defaults experiment (GLM-4.5-Air TP8, about 400 running requests), arm C vs arm B was −25% generation. Most of the cost is CPU work in vLLM's AuxOutput path on the output worker (TP0) and in EngineCore, not GPU work. This PR adds three vLLM patches in `patches.py`. Routing rows are unchanged. ## Where the time goes GLM-4.5-Air TP8 on H200, one node, `/inference/v1/generate`, closed loop with 400 concurrent requests, max 16k tokens, sampling masks on. Diagnostics: per-function timers, per-thread CPU, GC callbacks, and stack samples in worker TP0 and EngineCore. 1. **Store eviction on the output worker** (largest cost). vLLM sizes `BlockObjectStore` to the KV cache, so under load it is full and every `put` evicts. `_evict_to_fit` scans the LRU from the front and skips keys referenced by running requests. Those keys pile up at the front, so each `put` rescans ~300k keys: 9 ms per `put`, 46 puts/s, on the writer thread. It holds the GIL against the thread that launches the next step, and `execute_model` goes from 2.7 to 13.8 ms per call. 2. **One array per request per step.** The output worker returns `dict[request_id, AuxRequestOutput]`. Each step pickles one small array per running request to EngineCore, each as its own out-of-band buffer. EngineCore rebuilds them one object at a time. The cost grows with requests per step, and the churn drives GC. 3. **Engine GC.** vLLM freezes the startup heap of EngineCore and of the workers but keeps the default thresholds. ## Fix - `monkey_patch_aux_output_store` - Before upstream `_evict_to_fit` runs on a full store, it moves the referenced keys at the front of the LRU to the end. A referenced key cannot be evicted, and release moves it to the end anyway, so the victims are identical to upstream. - The output worker builds the store at 2× vLLM's size. At 100% KV the referenced rows alone can exceed a store of exactly KV size. `put` then raises `BlockObjectStoreError` in the writer thread and the engine dies. The condition does not depend on LRU order, so upstream raises on the same sequence. Before this PR the engine was too slow to fill the KV cache in this harness. With only the eviction fix, it reached 100% KV and died. - `monkey_patch_packed_aux_outputs` + `PackedAuxOutputs`: the worker sends one object per step (id→index dict, token starts, offsets, one rows array). It pickles as three byte strings and answers the scheduler's `in` / `[]` lookups. - `monkey_patch_engine_gc`: EngineCore keeps vLLM's freeze and gets `set_threshold(50_000, 20, 100)`. The workers get `gc.collect()` + `gc.freeze()` + the same thresholds after `compile_or_warm_up_model`. ## Results (gen tok/s, steady window 240–900 s) Earlier runs (node A, 400 concurrent, 16k): | arm | gen tok/s | tok/s per request | KV usage | |---|---|---|---| | router replay off, main | 9,356 | 23.5 | 0.72 | | router replay on, main | 5,724 (−39%) | 14.4 | 0.85 | | on, GC + packing (no store fix) | 8,250 | 20.9 | 0.76 | | on, store eviction only (1× size) | crashed at 100% KV | | | | on, all three (1× size) | 9,412 | 23.8 | 0.72 | | off, all three | 9,596 | 24.1 | 0.72 | Ablation of the branch as pushed (node B, same node for every row): | arm | load | gen tok/s | tok/s per request | running | KV mean / peak | |---|---|---|---|---|---| | all three | 400 conc, 16k | **9,679** | 24.6 | 393 | 0.71 / 0.98 | | store + GC (no packing) | 400 conc, 16k | 9,493 (−1.9%) | 24.3 | 390 | 0.72 | | store only | 400 conc, 16k | 8,309 (−14%) | 21.2 | 392 | 0.73 / 1.00, no error | | all three | 1000 conc, 2k, `max_num_seqs=1024` | **9,328** | 9.6 | 970 | 0.25 | | store + GC (no packing) | 1000 conc, 2k, `max_num_seqs=1024` | 9,078 (−2.7%) | 9.4 | 971 | 0.25 | - **Store only**: EngineCore gen-2 GC takes 10.7% of wall time. With GC tuning, EngineCore and the workers run 0 collections in the window. - **Packing**: worker TP0 `MessageQueue.enqueue` takes 0.32 vs 0.94 ms per call at 400 running, and 0.46 vs 2.85 ms at 970 running (2 calls per step). EngineCore's unpickle is hidden inside `dequeue`, so the bench below gives its size. - **Scaling of packing** (CPU bench of the per-step path; pickle + unpickle per step, ms): | requests per step | 400 | 1000 | 2000 | |---|---|---|---| | per-request dict | 6.1 | 18.7 | 31.7 | | packed | 0.13 | 0.37 | 0.8 | The cost is the same for 46 or 78 MoE layers × top-8: it is per request, not per byte. Both halves sit on the step's critical path: the worker main thread, then the EngineCore loop. So packing matters more as engines run more requests per step, which large sparse MoE engines with DP/EP do. At 1000 requests it is already ~2.7% end to end on this model. - Store headroom cost: host RAM per engine ≤ 2 × KV tokens × MoE layers × top-k × itemsize, on the output worker only. The arena is an anonymous mmap, so only used slots cost RSS. - Here: 4.37M tokens × 46 × 8 × 1 B = 1.6 GB, so up to 3.2 GB. - DeepSeek-V3-class model (58 × 8, uint8), 4M KV tokens per engine: 1.9 GB → 3.7 GB. - 78 × 8 at 8M tokens: 5 GB → 10 GB per engine. - With more than 256 experts the rows are uint16, which doubles these numbers. ## Checks - **Routing rows.** 24 sequential `top_k=1` requests (12 prompts × 300/1000 tokens) on node B: token ids and `routed_experts` bytes are bitwise identical between main and this branch (24/24). Earlier, router replay off gave identical tokens (24/24). - **Store eviction.** Compared with upstream on random op sequences (300 seeds × 300 puts): same stored keys, same order of unreferenced keys (the only order that affects eviction), same errors. Bench with 450k slots and 320k live keys: 27.7 → 0.40 ms per step. - **Unit tests.** `tests/unit/orchestrator`, `tests/unit/test_configs.py`, `tests/unit/inference` with `-m "not gpu"`: 381 passed (vLLM 0.32 venv). `ruff check` and `ruff format --check` are clean. ## Notes - Upstream-worthy: the eviction scan, store capacity at full KV, and packed per-step aux outputs. - Upstream: vllm-project/vllm#62044 (store fix; closed without merge, so the store patch stays), #58328 (open; the store grows past 2x KV instead of raising), #61086 (open; per-step R3 packing), #62086 (open draft; GC thresholds). - **Open question (API server, not this PR).** When ~100 length-capped 16k requests finish together, the API server loop stalls for ~25 s and clients see `ReadError`s (~110 per 400-concurrent arm). Each response carries about 8 MB of routed-experts JSON. By-handle payloads (#3902) avoid this path. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Monkey-patches vLLM engine, worker, and API-server internals and changes generate response shapes for logprobs and sampling masks; mitigated by unit tests and documented parity checks for routing rows and eviction. > > **Overview** > Cuts CPU/GC cost on the **router-replay** path and on **`/inference/v1/generate`** under heavy RL concurrency by avoiding per-token Python objects and tuning garbage collection. > > **Engine / aux-output path:** Registers four vLLM monkey patches in `patches.py`. Routed experts per scheduler step are sent as a single **`PackedAuxOutputs`** blob (replacing per-request dict pickling). The aux-output **`BlockObjectStore`** gets a 2× capacity headroom and an eviction tweak so referenced LRU keys are moved to the end before upstream eviction (avoids multi-ms rescans at full KV). **EngineCore** and GPU workers **freeze** the post-warmup heap and use higher **`gc` thresholds**; the API server does the same after startup. > > **Generate handler:** **`PrimeRlServingTokens`** strips heavy fields early via **`_PackedOutputs`**, then returns **base64-packed numpy** for `routed_experts`, **`completion_logprobs`**, and CSR **`sampling_mask`** (`{ids, counts}`). Non-streaming requests use **flat logprobs**, and **detokenize** is disabled when there are no stop strings. A separate patch keeps **FINAL_ONLY** sampling masks as **`PackedSamplingMask`** in the output processor. > > **Tests / deps:** Adds **`PackedAuxOutputs`** pickle and store-eviction parity tests; updates serving-token unit tests. Bumps **`renderers`** and drops **`pytest-cov`** from the lockfile. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e1afb9c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
mikasenghaas
pushed a commit
that referenced
this pull request
Oct 21, 2026
) Router replay (`enable_return_routed_experts`) costs a lot of generation throughput on vLLM 0.32. vLLM's AuxOutput store and worker code are unchanged since 0.31, where the numbers below were measured. In the v031 defaults experiment (GLM-4.5-Air TP8, about 400 running requests), arm C vs arm B was −25% generation. Most of the cost is CPU work in vLLM's AuxOutput path on the output worker (TP0) and in EngineCore, not GPU work. This PR adds three vLLM patches in `patches.py`. Routing rows are unchanged. ## Where the time goes GLM-4.5-Air TP8 on H200, one node, `/inference/v1/generate`, closed loop with 400 concurrent requests, max 16k tokens, sampling masks on. Diagnostics: per-function timers, per-thread CPU, GC callbacks, and stack samples in worker TP0 and EngineCore. 1. **Store eviction on the output worker** (largest cost). vLLM sizes `BlockObjectStore` to the KV cache, so under load it is full and every `put` evicts. `_evict_to_fit` scans the LRU from the front and skips keys referenced by running requests. Those keys pile up at the front, so each `put` rescans ~300k keys: 9 ms per `put`, 46 puts/s, on the writer thread. It holds the GIL against the thread that launches the next step, and `execute_model` goes from 2.7 to 13.8 ms per call. 2. **One array per request per step.** The output worker returns `dict[request_id, AuxRequestOutput]`. Each step pickles one small array per running request to EngineCore, each as its own out-of-band buffer. EngineCore rebuilds them one object at a time. The cost grows with requests per step, and the churn drives GC. 3. **Engine GC.** vLLM freezes the startup heap of EngineCore and of the workers but keeps the default thresholds. ## Fix - `monkey_patch_aux_output_store` - Before upstream `_evict_to_fit` runs on a full store, it moves the referenced keys at the front of the LRU to the end. A referenced key cannot be evicted, and release moves it to the end anyway, so the victims are identical to upstream. - The output worker builds the store at 2× vLLM's size. At 100% KV the referenced rows alone can exceed a store of exactly KV size. `put` then raises `BlockObjectStoreError` in the writer thread and the engine dies. The condition does not depend on LRU order, so upstream raises on the same sequence. Before this PR the engine was too slow to fill the KV cache in this harness. With only the eviction fix, it reached 100% KV and died. - `monkey_patch_packed_aux_outputs` + `PackedAuxOutputs`: the worker sends one object per step (id→index dict, token starts, offsets, one rows array). It pickles as three byte strings and answers the scheduler's `in` / `[]` lookups. - `monkey_patch_engine_gc`: EngineCore keeps vLLM's freeze and gets `set_threshold(50_000, 20, 100)`. The workers get `gc.collect()` + `gc.freeze()` + the same thresholds after `compile_or_warm_up_model`. ## Results (gen tok/s, steady window 240–900 s) Earlier runs (node A, 400 concurrent, 16k): | arm | gen tok/s | tok/s per request | KV usage | |---|---|---|---| | router replay off, main | 9,356 | 23.5 | 0.72 | | router replay on, main | 5,724 (−39%) | 14.4 | 0.85 | | on, GC + packing (no store fix) | 8,250 | 20.9 | 0.76 | | on, store eviction only (1× size) | crashed at 100% KV | | | | on, all three (1× size) | 9,412 | 23.8 | 0.72 | | off, all three | 9,596 | 24.1 | 0.72 | Ablation of the branch as pushed (node B, same node for every row): | arm | load | gen tok/s | tok/s per request | running | KV mean / peak | |---|---|---|---|---|---| | all three | 400 conc, 16k | **9,679** | 24.6 | 393 | 0.71 / 0.98 | | store + GC (no packing) | 400 conc, 16k | 9,493 (−1.9%) | 24.3 | 390 | 0.72 | | store only | 400 conc, 16k | 8,309 (−14%) | 21.2 | 392 | 0.73 / 1.00, no error | | all three | 1000 conc, 2k, `max_num_seqs=1024` | **9,328** | 9.6 | 970 | 0.25 | | store + GC (no packing) | 1000 conc, 2k, `max_num_seqs=1024` | 9,078 (−2.7%) | 9.4 | 971 | 0.25 | - **Store only**: EngineCore gen-2 GC takes 10.7% of wall time. With GC tuning, EngineCore and the workers run 0 collections in the window. - **Packing**: worker TP0 `MessageQueue.enqueue` takes 0.32 vs 0.94 ms per call at 400 running, and 0.46 vs 2.85 ms at 970 running (2 calls per step). EngineCore's unpickle is hidden inside `dequeue`, so the bench below gives its size. - **Scaling of packing** (CPU bench of the per-step path; pickle + unpickle per step, ms): | requests per step | 400 | 1000 | 2000 | |---|---|---|---| | per-request dict | 6.1 | 18.7 | 31.7 | | packed | 0.13 | 0.37 | 0.8 | The cost is the same for 46 or 78 MoE layers × top-8: it is per request, not per byte. Both halves sit on the step's critical path: the worker main thread, then the EngineCore loop. So packing matters more as engines run more requests per step, which large sparse MoE engines with DP/EP do. At 1000 requests it is already ~2.7% end to end on this model. - Store headroom cost: host RAM per engine ≤ 2 × KV tokens × MoE layers × top-k × itemsize, on the output worker only. The arena is an anonymous mmap, so only used slots cost RSS. - Here: 4.37M tokens × 46 × 8 × 1 B = 1.6 GB, so up to 3.2 GB. - DeepSeek-V3-class model (58 × 8, uint8), 4M KV tokens per engine: 1.9 GB → 3.7 GB. - 78 × 8 at 8M tokens: 5 GB → 10 GB per engine. - With more than 256 experts the rows are uint16, which doubles these numbers. ## Checks - **Routing rows.** 24 sequential `top_k=1` requests (12 prompts × 300/1000 tokens) on node B: token ids and `routed_experts` bytes are bitwise identical between main and this branch (24/24). Earlier, router replay off gave identical tokens (24/24). - **Store eviction.** Compared with upstream on random op sequences (300 seeds × 300 puts): same stored keys, same order of unreferenced keys (the only order that affects eviction), same errors. Bench with 450k slots and 320k live keys: 27.7 → 0.40 ms per step. - **Unit tests.** `tests/unit/orchestrator`, `tests/unit/test_configs.py`, `tests/unit/inference` with `-m "not gpu"`: 381 passed (vLLM 0.32 venv). `ruff check` and `ruff format --check` are clean. ## Notes - Upstream-worthy: the eviction scan, store capacity at full KV, and packed per-step aux outputs. - Upstream: vllm-project/vllm#62044 (store fix; closed without merge, so the store patch stays), #58328 (open; the store grows past 2x KV instead of raising), #61086 (open; per-step R3 packing), #62086 (open draft; GC thresholds). - **Open question (API server, not this PR).** When ~100 length-capped 16k requests finish together, the API server loop stalls for ~25 s and clients see `ReadError`s (~110 per 400-concurrent arm). Each response carries about 8 MB of routed-experts JSON. By-handle payloads (#3902) avoid this path. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Monkey-patches vLLM engine, worker, and API-server internals and changes generate response shapes for logprobs and sampling masks; mitigated by unit tests and documented parity checks for routing rows and eviction. > > **Overview** > Cuts CPU/GC cost on the **router-replay** path and on **`/inference/v1/generate`** under heavy RL concurrency by avoiding per-token Python objects and tuning garbage collection. > > **Engine / aux-output path:** Registers four vLLM monkey patches in `patches.py`. Routed experts per scheduler step are sent as a single **`PackedAuxOutputs`** blob (replacing per-request dict pickling). The aux-output **`BlockObjectStore`** gets a 2× capacity headroom and an eviction tweak so referenced LRU keys are moved to the end before upstream eviction (avoids multi-ms rescans at full KV). **EngineCore** and GPU workers **freeze** the post-warmup heap and use higher **`gc` thresholds**; the API server does the same after startup. > > **Generate handler:** **`PrimeRlServingTokens`** strips heavy fields early via **`_PackedOutputs`**, then returns **base64-packed numpy** for `routed_experts`, **`completion_logprobs`**, and CSR **`sampling_mask`** (`{ids, counts}`). Non-streaming requests use **flat logprobs**, and **detokenize** is disabled when there are no stop strings. A separate patch keeps **FINAL_ONLY** sampling masks as **`PackedSamplingMask`** in the output processor. > > **Tests / deps:** Adds **`PackedAuxOutputs`** pickle and store-eviction parity tests; updates serving-token unit tests. Bumps **`renderers`** and drops **`pytest-cov`** from the lockfile. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e1afb9c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> (cherry picked from commit 78efef1)
# Conflicts: # deps/renderers # deps/verifiers # src/prime_rl/inference/vllm/serving_tokens.py # src/prime_rl/orchestrator/batch.py # src/prime_rl/orchestrator/trajectories.py # src/prime_rl/trainer/rl/data.py # src/prime_rl/trainer/rl/train.py # src/prime_rl/transports/batch/types.py # tests/unit/inference/test_serving_tokens.py # uv.lock
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.

Router-replay ids and sampling masks are per-token arrays, so they grow with batch × context × layers × top-k. Inline, they travel as base64 through the API server, env servers and orchestrator, then once per CP rank over ZMQ. On multi-node runs (including disaggregated P/D) this PR passes them by handle: the inference server writes them to a file on the shared filesystem and only small segment descriptors travel. Single-node, eval and frozen-source rollouts stay inline.
What
orchestrator.payload_root.rlsets it to<run dir>/payloadsformulti_nodedeployments with router replay or sampling-mask capture, and defaultstrainer.data.num_workers = 1.extra_args.payload_dir = <payload_root>/v<k>(k= dispatch policy version).serving_tokens.pywrites routing rows (uint8/uint16, as vLLM picks) and the CSR sampling mask to one fsynced file per request and returnspayloadsegments (PayloadSegment(field, file, offset, pos, rows, dtype, shape)) instead. Mask ids are bit-packed atceil(log2(vocab_size))bits (18 for 150k vocabularies), with per-row start indices so any row range can be read.routed_layers(hf_config)lists the layers with a learned router. Dense MLP layers (first_k_dense_replace,mlp_layer_types,num_dense_layers,mlp_only_layers), Nemotron-H Mamba/attention layers and DeepSeek-V4 hash layers are left out. The server applies it to routing rows (by handle and inline). The trainer scatters the rows back with zero rows for the other layers, which is what vLLM leaves there. The DeepSeek-V4 trainer model routes hash layers by token id instead of replaying them.payload_dirand each writes its own file. The router merges the two segment lists (router#60): prefill's routing, then decode's clipped at prefill's coverage; prefill's 1-token mask segment is dropped.preadthreads; stale or short reads retry, then raise.v<k>older than every live rollout and older thanstep - max_off_policy_steps. A fresh run wipes the root; a resumed run keeps it (fix(orchestrator): drop restored work that points at deleted by-handle payload files #4044 drops restored work whose files are gone).Cost
Rows per token (routed layers only): Qwen3-30B-A3B 384 B, GLM-4.5-Air 360 B (−2.2 %), GLM-5.3 600 B (−3.8 %), DeepSeek-V4-Flash 240 B (−7 %), Nemotron-3-Nano 138 B (−56 %). Masks about 4 B per completion token + 2.25 B per kept id, −35 to −39 % vs 4 B per kept id on GPU runs below.
Step time on GLM-4.5-Air was unchanged: at that size the gain is memory. Large-scale step time and shared-filesystem throughput have not been measured yet.
Correctness (GPU, Qwen3-30B-A3B TP1,
VLLM_BATCH_INVARIANT=1, router replay + masks)Tests
pytest -m "not gpu" tests/unit/orchestrator tests/unit/test_configs.py tests/unit/inference tests/unit/transports tests/unit/train/rl: 410 passed; router#60cargo test --lib routed_experts: 6 passed; ruff clean. Docs:docs/inference.md§ Payloads by handle.Before merge
deps/verifiers(now 410e84e8) anddeps/renderers(now 4163c89).vllm-routerpin (v0.2.3). Without it, P/D turns carry only decode's routing and the orchestrator drops it with a warning.sampling_mask_logprobsby handle too.Upstream: vllm-project/vllm#57968 (open) — Mooncake keys-only R3, an alternative design for the routing half (not a dependency).
Note
Medium Risk
Touches the full train data path (inference → orchestrator → trainer) and shared-filesystem retention; mistakes could drop or misread routing/mask data and bias RL, though extensive unit tests cover parity with inline mode.
Overview
Multi-node router replay and sampling-mask capture now spill per-token payloads to a shared filesystem instead of shipping huge inline arrays through HTTP, env servers, orchestrator, and ZMQ.
The
rlentrypoint setsorchestrator.payload_root(typically<run dir>/payloads) when deployment ismulti_nodeand routed experts or sampling masks are enabled, and defaultstrainer.data.num_workers = 1so payload reads can overlap training. Train rollouts passextra_args.payload_dir = <root>/v<policy version>; vLLMserving_tokensfsyncs one file per request and returnspayloadsegment descriptors. A newtransports/payloadlayer handles CSR bit-packed masks,routed_layers-only routing rows, clip/pack in the orchestrator, CP-shardedpreadin the trainer, and lifecycle GC (per-batch delete after policy apply, immediate cleanup on drops, version sweeps).Inline payloads remain for single-node, eval, and clients without
payload_dir. Docs add Payloads by handle.Reviewed by Cursor Bugbot for commit d1f029e. Bugbot is set up for automated code reviews on this repo. Configure here.