Repository navigation
Conversation
Adopt upstream vLLM 0.30.0 and regenerate the lockfile with the optional Speculators dependencies. Update speculative-decoding examples for the source-group configuration schema.
Work around a vLLM 0.30.0 FlashInfer cached-startup illegal memory access while preserving Torch compilation and CUDA graphs. Document the setting for exported-model inference.
nxphi47
marked this pull request as ready for review
October 5, 2026 01:03
This was referenced Oct 10, 2026
samsja
added a commit
that referenced
this pull request
Oct 10, 2026
Replaces the three switches `model.fsdp_cpu_offload`, `model.optim_cpu_offload` and `model.full_offload` with one field: ```toml [model] offload = "optimizer" # default; also "full", "none" ``` This applies to both the RL trainer and SFT. The `fsdp` switch is not migrated: the mode is removed outright (rationale below). The `[model.full_offload] numa_bind` knob is dropped: no recipe, deployment, template or doc sets it. Full offload now always pins each rank's CPUs to its GPU's NUMA node. That was already the default. Ranks whose GPU reports no NUMA node still skip binding with a warning. On hosts without GPU sysfs topology (`/sys/bus/pci/devices/<bus>/numa_node`), binding is no longer optional: full offload fails there instead of being switched off. Why: the three switches could only be used one at a time, enforced by `cpu_offload_mutual_exclusion`. Because `optim_cpu_offload` defaulted to `true`, enabling full offload also meant writing `optim_cpu_offload = false`, otherwise validation failed (see the old `configs/advanced/deepseek-v4-flash/sft.toml` comment). With one value the conflicting combinations cannot be expressed, so that validator is deleted. The default behaviour does not change: optimizer-state offload stays on. Why drop `fsdp`: the mode was the stock torch FSDP2 `CPUOffloadPolicy` with no in-tree config, example, or test user; Muon rejected it and it broke NCCL weight broadcast. It was kept only for long-context runs whose fp32 masters/moments no longer fit on GPU (the reason it was restored in #3052); the `full` mode added in #3234 keeps FP32 masters, moments, and gradients in host RAM, covering that memory pressure instead. Gone with it: the `CPUOffloadPolicy` selection in `setup_fsdp`, CPU-side model init in `setup_model`/`load_dcp_from_hf`, `_move_buffers_to_cuda`, and the fsdp leg of the Gloo special case. Changes: - `ModelConfig.offload: Literal["none", "optimizer", "full"] = "optimizer"` replaces the three fields. `OptimizerInBackwardOffloadConfig` (and with it `numa_bind`) and its bool-normalizing `BeforeValidator` are gone. - Validators in `TrainerConfig` and `SFTConfig`: - full offload accepts only AdamW or SignSGD and disables clipping, now reading `offload == "full"`; - the `Muon optimizer does not support FSDP CPU offload` validators are deleted with the mode. - Runtime: - `setup_optimizer(..., offload=...)` replaces `cpu_offload` + `full_offload_config`, and its "both enabled" check is gone. - `FullCPUOffloadOptimizer` drops its unused `offload_config` argument. - `setup_full_cpu_optimizer_offload()` takes no arguments and always binds. - `model.py`: FSDP always uses the default `OffloadPolicy()`; CPU-side model init in `setup_model`/`load_dcp_from_hf` and `_move_buffers_to_cuda` are gone. - Tomls: `glm-4.5-air/swe-budget.toml` and `ci/integration/reverse-text-sft/full-offload-resume.toml` → `offload = "full"`; `glm-5.3/sft/h200/16k-12-node.toml` → `offload = "none"`; `glm-5.3/sft/h200/base.toml` → `offload = "optimizer"`. Also updated `docs/scaling.md`, the `deepseek-v4-flash/sft.toml` comment, and `skills/training/start-run/SKILL.md`, which documented the removed fields. Breaking: the old keys now fail with `extra_forbidden`, and `offload = "fsdp"` fails the `Literal`. There is no shim. Interaction with #3849 (one trainer-common config base): #3849 moves the full-offload and Muon/FSDP validators from `TrainerConfig`/`SFTConfig` onto `BaseTrainerConfig`. This PR edits the full-offload validator's condition and deletes the Muon/FSDP one, so the conflict is textual only: keep #3849's single copy of the full-offload validators with `self.model.offload == "full"` and drop its Muon/FSDP copy. Whichever lands second takes a small rebase. Other open PRs that set or read the old keys need a rebase: - tomls: #3883, #3879, #3766, #3765, #3729, #3629, #3627, #3616, #3544 - code: #3885 (`optim_cpu_offload`), #3739 (`full_offload` validators), #3724, #3719, #3699, #3610, #3487 (`fsdp_cpu_offload` / `enable_gloo`) Test plan - `pytest tests/unit/orchestrator tests/unit/test_configs.py tests/unit/train/test_cpu_adam.py tests/unit/train/test_state_offload.py -m "not gpu"`: 336 passed, 1 skipped (Qwen3-VL snapshot not cached on this host), 4 deselected (GPU; this host has no GPU). The existing offload config tests are updated to the new field. `test_load_configs` covers the 4 edited tomls. - `cli(TrainerConfig|SFTConfig, ...)`: `--model.offload full|none` and the default (`optimizer`) resolve as expected; `optim_cpu_offload` and `offload = "fsdp"` are rejected. - `import prime_rl.trainer.rl.train, prime_rl.trainer.sft.train` works. - `ruff check` and `ruff format --check` on the changed Python files: clean. - Not run: GPU or multi-node training with `offload = "full"`. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Breaking TOML/API surface touches distributed training memory paths (FSDP offload, optimizer-in-backward, Gloo); behavior should match prior modes when mapped correctly, but mis-migrated configs will fail at parse time. > > **Overview** > **Breaking config change:** `model.fsdp_cpu_offload`, `model.optim_cpu_offload`, and `model.full_offload` are replaced by a single **`model.offload`** setting (`"optimizer"` default, or `"full"`, `"fsdp"`, `"none"`). RL and SFT share the same field; example TOMLs and scaling docs are updated accordingly. > > The mutual-exclusion validator between the old flags is removed because only one mode can be expressed. **`[model.full_offload]` / `numa_bind` is dropped** — `"full"` always runs NUMA binding and CPU optimizer thread setup via parameterless `setup_full_cpu_optimizer_offload()`. Runtime paths (`setup_optimizer`, FSDP CPU offload in `model.py`, Gloo when offload is `"full"` or `"fsdp"`) key off the new enum; `FullCPUOffloadOptimizer` no longer takes an offload config object. > > Default training behavior is unchanged: optimizer-state CPU offload remains the default (`"optimizer"`). Old TOML keys are not aliased and should fail validation. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 35da73a. 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: sami jaghouar <sami.jaghouar@gmail.com>
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.
RL changes the target policy while a fixed speculative draft continues proposing from its original distribution. PrimeRL also needs to update both models together: reloading only the policy leaves the draft stale, and an unsafe draft reload can overwrite shared target weights or leave CUDA graphs pointing at obsolete tensors.
This PR adds an optional path to train a draft from the live policy's detached features, synchronize both models into inference, and save/resume/export them together. It also exposes upstream standalone draft training through PrimeRL's TOML CLI. Validation covers Qwen3-4B and a separately trained DSpark for Qwen3-30B-A3B-Thinking-2507; the model-specific evidence and gaps are listed below.
Accuracy update: two completed default-vLLM0.30 Avg@16 evaluations now give pooled Avg@32: 609/960 (63.44%) ordinary decoding versus 603/960 (62.81%) DSpark, a difference of −0.625 percentage points, paired-question bootstrap 95% interval [−3.44, +2.08]. The first set showed a −3.96-point drop; the independent-seed replication reversed direction to +2.71 points. The initial drop did not replicate, and the combined result is consistent with sampling variation. This does not prove equivalence: the interval still permits a 3.44-point drop and no noninferiority margin was specified. Exact target-weight checks, scored greedy tests and complete-serving distribution checks found no verification bypass. Optional batch-invariant execution eliminates the tested greedy numerical divergence; its separate completed Avg@16 scores 295/480 ordinary versus 318/480 DSpark (+4.79 points, interval [+1.88, +7.92]). This exploratory diagnostic is not pooled with default results or adopted in the RL overlay.
Branch/runtime status: the last main merge incorporates
d5f29c072in667d0acf5;1279ebe6fadds the scoped example fusion setting;710dd6e20fixes the subsequently identified DSpark replica compilation-cache defect. The branch uses upstream vLLM 0.30.0, retains main's dependency and submodule pins, and adapts the example configs to the current source-group schema. The full benchmark and training matrix below was originally run with 0.29.0; post-merge validation is reported separately.Router fix and 30B validation update
Long-response math runs exposed slow backend health responses: the router could mark a still-generating vLLM worker unhealthy and fail rollouts with HTTP 503. Commit
31520387baddsinference.router.health_check_timeout_secs, constrained to a positive integer, with the existing five-second default preserved. Both the local inference launcher and the shared SLURM router template pass it tovllm-router --health-check-timeout-secs. This is a six-line implementation change across three files, not only an example TOML change, and does not modify vLLM sources.The full 30B workload uses 60 seconds. A side-by-side health-probe check returned HTTP 200 in all 106 ready samples with 60 seconds, versus 16 HTTP 503 responses from the original five-second router. This mitigates false worker exclusions; it does not establish the underlying API event-loop latency cause, and it delays detection of a truly unresponsive backend.
The independent serving-speed fix is an upstream configuration override,
max_cudagraph_capture_size=1024, for 128 requests and seven draft tokens: target verification schedules up to128 × (7 + 1) = 1024positions. The old cap of 512 forced eager fallback for those batches. The draft's own 896-position batch also exceeded 512. No verifier or acceptance rule changed.For the tested 30B TP4 configuration, compose this after the base RL config and the overlay selecting the separately trained compatible draft:
The router and graph values above are explicit workload overrides, not new global defaults. Backend selection remains vLLM's default
auto(MNNVL on these H200s). A larger concurrency ceiling needs corresponding graph and KV-cache capacity.Latest validation: on 2026-10-05, 207 existing configuration tests passed on an allocated compute node; Ruff lint/format and Git whitespace checks passed. Direct checks verify the default of 5, the override of 60, rejection of nonpositive values, the local command argument, and SLURM template rendering with Bash syntax validation. The complete repository suite was not rerun for these six lines. Earlier supervised tests and CI results below retain their original scope.
Upstream vLLM or a custom fork?
The runtime is the official upstream vLLM release wheel. This PR does not introduce a vLLM fork, custom wheel, or edits to installed vLLM sources.
speculative_configvllm-project/speculators, pinned toffcccb8; siblinghs-connectorsuses the same revisiontorch.compile(dynamic=False)before invoking the upstream trainer; no installed source is editedCache fix validation (
710dd6e20): two configuration regression cases pass, including a run with no GPUs exposed. Ruff and diff checks pass. CPU CI passed 678 tests (1 skipped, 191 deselected); slim installation, Ruff and CodeQL also passed. GPU CI remains skipped for this draft; the GPU checks here were supervised compute-node runs. Cold and same-cache TP2×DP2 serving completed 96 requests /32,768 tokens, including 8,192 scored greedy tokens, all target argmax, with zero request failures or preemptions. Both DP engines served requests, and complete per-engine counters agree with responses. Cached logs show all four distinct draft AOT paths. Backend selection remainsauto→MNNVL, fusion is disabled, and AOT/compilation/CUDA graphs remain enabled. These short functional checks do not replace the matched TP1 throughput result. Artifacts:dspark-dp-cache-validation.json,startup-audit-dp-index-{cold,cached}.log, and rawstartup-dp-index-*responses/counters.The integration calls vLLM's layerwise reload helpers and an internal per-engine RPC API. It also invokes the draft's fused-KV buffer rebuild hook after reload finalization. These are version-sensitive integration points, so using upstream wheels does not imply compatibility with every vLLM release.
The initial fusion workaround commit changes only
examples/specdecode/dspark.toml,examples/specdecode/rl.toml, and their documentation. It disables the graph-level all-reduce/RMSNorm fusion pass; ordinary all-reduce can independently select FlashInfer MNNVL, and cached startup still failed with this flag disabled. Disabling AOT serialization also failed because the regular compilation cache uses the same replica index.The follow-up investigation identified a separate upstream configuration bug. Independent dense-model data-parallel replicas keep distinct
data_parallel_indexvalues after their process-group rank resets to zero. DSpark'sreplace(parallel_config, ...)reconstructs thatinit=Falsefield from the zero rank. Both replicas then read/write the same draft compilation paths, whose generated functions contain device-specific CUDA guards. Metadata captured target→draft tensor-device transitions cuda:0→cuda:2 and cuda:3→cuda:1 in two failing workers. The target compilation paths were correctly separated. The new PrimeRL plugin wrapper preserves the original cache index after upstream construction. It is a runtime vLLM compatibility patch, not a fork, custom wheel, kernel replacement or verification change. Use a fresh cache root to avoid artifacts already produced with colliding indices; cache cleanup is not automatic. This DP2 failure cannot explain the separate TP1 accuracy comparisons.The benchmarks use the default
autobackend selection and default AOT settings, with fusion disabled. A diagnostic explicittrtllmoverride was stopped during compilation before requests and is not adopted.autoselected MNNVL on the tested H200s; its kernel nametrtllm_mnnvl_allreduce_fusionrefers to FlashInfer communication inside vLLM, not the TensorRT-LLM inference engine. Compilation and CUDA graphs remain enabled. An eager-only diagnostic passed but is not adopted.The optional
specdecodedependency group installs Speculators and its connector.datasets>=4.7.0avoids the oldVideoReaderimport failure encountered with the installed torchvision version. The UV override replaces an upstream build-machinefile://connector dependency with the pinned Git source;tool.uv.package=trueensures the project CLI entrypoint is installed.Features shipped
uv run specdecode @ ...trains from saved target hidden states using upstream Speculatorstrainer.speculator; draft joins the policy FSDP mesh and optimizer, with detached targets and an optional separate learning ratespeculator/directory; FP8 target export keeps the draft in BF16Online standalone training here means on-demand feature extraction; joint RL training directly reuses the current trainer forward pass.
Design and code structure
flowchart LR P[FSDP policy forward] -->|detached layer features| D[Upstream draft model and loss on FSDP] P --> O[Shared optimizer and scheduler] D --> O O --> C[Joint DCP checkpoint] O --> W[Policy and speculator weight namespaces] W --> T[Filesystem or NCCL transport] T --> V[PrimeRL vLLM worker extension] V --> I[Upstream target and draft inference] C --> E[HF target export plus speculator directory]configs/specdecode.py, trainer/RL configsspecdecode/train.py,specdecode/worker.pyspecdecode/training.pytrainer/rl/train.py,trainer/optimutils/weights.py,transports/weights/*speculator.namespace and omit reconstructed verifier-only tensors when appropriateinference/vllm/worker/*,inference/vllm/server.pyorchestrator/inference_metrics.py, checkpoint convertersThe joint adapter captures the checkpoint-requested policy layer features and final pre-normalization hidden state. Features are detached, so draft loss does not backpropagate through the policy. It aligns token targets, masks anchors that cross packed-document boundaries, and gathers context-parallel features before the draft forward. Each CP rank therefore processes the full draft sequence.
Frozen verifier embedding/head/norm projections are refreshed from the live policy before training and before publishing updated weights. The optional draft learning rate uses the existing scheduler. Policy RL loss formulas and the
optimization_dtype/reduce_dtypesettings are unchanged.Reload correctness requires more than copying parameter values:
GET /weight_fingerprint?target=model|draftgathers hashes, shapes, nonzero counts and addresses from every DP/TP worker. This is an expensive idle-worker diagnostic, not a training hot-path operation.Example commands and configs
Run model workloads inside compute allocations. Use a one-GPU allocation for an inference server or standalone trainer, separate GPUs for online extraction/training, and six GPUs for the joint RL example. The AIME config uses the Prime runtime and requires its normal credentials. Do not resync dependencies while workers are running.
Inference and AIME evaluation
inference.tomlpins the target and serves router/backend ports 18200/18201.dspark.tomlpins the draft and requests seven speculative tokens.aime25.tomlselects all 30 questions ×8 completions, temperature 0.6, a 31,000-token completion limit and concurrency 16. Point--client.base-urlat the compute host if the evaluator runs elsewhere.Offline and online standalone training
First prepare tokenized target responses with upstream
speculators prepare-data, containinginput_ids,loss_maskandseq_len. Put the dataset atoutputs/specdecode/data, or overridetrain.data.data_path. Training data is an input to the example and is not bundled in the PR.These are alternative training modes; use distinct
--output-dirvalues for separate experiments. The file backend normally reads<data_path>/hidden_states; set[train.backend].hidden_states_pathfor another location. The example extraction layers match this DSpark checkpoint: 1, 9, 17, 25, 33 and final pre-normalization layer 36. The default standalone recipe runs four updates;--dry-runvalidates options without loading models. Upstream manages standalone checkpoint saving and resume undertrain.trainer.save_path.Frozen draft training and joint RL
The joint recipe uses 4096-token sequences, 3072-token completions, batch size 8/group size 2,
i3_math, and three updates. The key optional section is:Set
freeze_backbone = trueto train only the draft. The checked-in frozen smoke is synthetic; the stronger real-batch validation below used the upstream offline loader's first two batches.Export and serve a trained checkpoint
For inference, set
vllm.modelto the exported root andvllm.speculative_config.modelto itsspeculator/directory, withmethod = "dspark"andnum_speculative_tokens = 7. The examples setvllm.compilation_config.pass_config.fuse_allreduce_rms = false; this does not guarantee successful multi-GPU startup on 0.30.0. A filesystem worker also accepts a standalone draft checkpoint throughPOST /update_weights?target=draftwith{"weight_dir":"/path/to/draft"}between/pauseand/resume. KeepVLLM_SERVER_DEV_MODEdisabled: vLLM's development update route has a different request schema and conflicts with this endpoint.More usage detail: docs/specdecode.md. All eight recipes are under examples/specdecode.
Results and validation performed
The original validation matrix used H200 GPUs on one compute node, upstream vLLM 0.29.0, Torch 2.13.0+cu130 and Transformers 5.6.2. The implementation was exercised before being recorded in commit
3b6c27c9e, on basef962e9c10. Post-merge checks use upstream vLLM 0.30.0 and main's dependency versions. Original 0.29 results are retained separately from the new 0.30 measurements below.Post-merge validation: vLLM 0.30.0
The regenerated lockfile installs successfully with
uv sync --all-extras --all-packages --locked. All 235 focused tests passed, and Ruff lint/format passed all 24 changed Python files. After the standalone worker fix in55a279fa1, GitHub CPU CI also passed 676 tests (1 skipped, 191 deselected); Ruff, slim-config installation and all CodeQL checks passed. GPU integration CI remains skipped on the draft PR; the GPU checks below were run manually in compute allocations. The merge retains main's vLLM/router/CUTLASS versions and verifier/renderer submodule pins.Supervised CP2 + TP2×DP2 joint RL completed updates 1–2 and resumed checkpoint 2→3 with NCCL policy/draft synchronization. Draft gradient norms were 0.5447, 1.0958 and 0.3678; losses were 0.417862, 0.417463 and 0.339368. All three checkpoints saved. The first restart failed during FlashInfer initialization; a retry with the scoped example setting passed, but later startup failures showed that setting is insufficient as a workaround. All eight specdecode config cases were rechecked and passed after the setting changed.
The BF16 export exactly matches all 398 target +62 draft checkpoint tensors. Fresh loading exactly matches the final NCCL policy/draft hashes on all four workers. Draft-only zero/restore preserves the target and captured addresses, restores every owned tensor and gives identical greedy probe outputs. Combined policy+draft filesystem reload also restores all four workers exactly, including addresses. Initial run, resume.
Accuracy audit: the installed upstream standard rejection kernel passed 655,360 draws across point-mass, adversarial, matching, masked-target and temperature-0.6 proposal distributions, within a preset six-binomial-standard-error tolerance plus 2/N. Masked target tokens never appeared; matching proposals accepted 100%. All 4,096 deliberately wrong greedy proposals were replaced by the target argmax. Code inspection confirms target-ratio acceptance and corrected residual sampling; synthetic acceptance and adaptive verification are disabled in the follow-up evaluation. These checks do not establish end-to-end accuracy equivalence. Completed full-length AIME30×16 and greedy-prefix results are reported below.
Matched default-settings TP1 benchmark: neither 0.30 repeat reproduced 5K+. The warmed repeat delivered 4,937.36 output tokens/s, versus 5,087.80 on 0.29 (−2.96%). The first run, including first-request sampling-kernel JIT, delivered 4,564.84 tokens/s (−10.28%). Both results are retained:
The two 0.30 runs use the same live server without changed settings; complete engine counter deltas match trace token totals exactly. The pinned model/draft, TP1/DP1, 32K context, concurrency 16, sampling and PrimeRL eval recipe match the original, with fusion disabled and no backend/AOT override. The new runs use another physical H200 because the original node is occupied. The merged-main software stack also changes dependencies and the PrimeRL wrapper (including
enable_scale_outAPI registration). These limited stochastic repeats do not establish a statistically significant or vLLM-only regression. The original TP1 engine already resolved fusion disabled, so the difference cannot be attributed to turning off that pass. Detailed artifacts:dspark-030-default-aime25/summary.jsonanddspark-030-default-warm-aime25/summary.jsonunderoutputs/specdecode-dev.Multi-GPU fusion performance, preliminary: on the same four H200s/checkpoint (TP2×DP2, concurrency16), three60-request×2048-token rounds gave medians 13,915.48tok/s enabled versus 13,863.50 disabled: 0.37% lower with fusion disabled. Ranges were13,906.05–14,163.14 and13,742.83–13,960.74 respectively; no preemptions. This difference is small relative to round variation. The final disabled cached restart failed in
vllm.all_reduce → FlashInferAllReduce.all_reduce → trtllm_mnnvl_allreduce_fusion, so that phase has no throughput result. Longer600-request rounds were cancelled before requests to prioritize the matched default-settings TP1 benchmark. These short-output four-GPU rates are not directly comparable to full-AIME TP1 rates.A further installed-kernel test passed 131,072 three-token sequences with prefix-dependent target and adversarial proposal distributions: all64 joint probabilities met the preset tolerance, exercising rejection at each depth and all-accepted bonus sampling. The documentation now explains the rejection guarantee, seed limitations, Avg@16 versus pass@16, and data preparation for a target without a pretrained draft.
Expanded accuracy check: both default-settings AIME Avg@16 sets completed at the full 31K completion limit. The first uses request seeds
1729 + 1009*replicate + question_index; the independent replication uses25000 + 1009*replicate + question_index. Each set has 480 completions per mode. All settings, questions and scoring are otherwise unchanged. Paired-question bootstrap intervals use 100,000 draws and seed7331.The first negative result is retained, but it did not replicate. Both pooled modes pass25/30 questions at least once; truncations total126 ordinary /129 DSpark. All1,920 completions succeeded without preemptions. Raw records, per-question counts and bootstrap results are in
aime16-030-*andaccuracy-030-{analysis,seed25000-analysis,avg32-analysis}.json. No persistent regression or accuracy equivalence has been demonstrated.A separate full-length Avg@16 diagnostic with
VLLM_BATCH_INVARIANT=1completed using the original seeds: 295/480 (61.4583%) ordinary versus 318/480 (66.2500%) DSpark, a difference of +4.7917 points, paired-question bootstrap 95% interval [+1.8750, +7.9167]. Pass@16 is 26/30 versus 25/30, with 65 versus 60 truncations. All scored records completed and no preemptions occurred. This is an exploratory diagnostic, reported separately from default results and the matched throughput benchmark. It does not establish an accuracy improvement from batch invariance or DSpark. The same nominal seed does not couple random draws across execution modes; numerical execution changes proposal/acceptance paths and subsequent draws. Artifact:accuracy-030-batch-invariant-analysis.json.The batch-invariant ordinary arm was deliberately moved to a longer allocation after 46 completed records because its original allocation could not be extended. Those records and seeds were preserved; unfinished requests were intentionally cancelled and retried under the same config on another H200 in the same node, where the remaining 434 records completed. Its complete timing is invalid, so no speed ratio is reported. Neither default-settings replication arm nor the batch-invariant DSpark arm was restarted by this move. All diagnostic servers and allocations have been released.
A separate greedy test on all30 questions, capped at4,096 tokens, found 0/30 bitwise-identical complete prefixes, with first differences after12–1,263 tokens. Temperature zero rules out sampling randomness as that difference's explanation. All 290 inspected target tensors match exactly. An isolated scored follow-up found all49,152 DSpark tokens and49,152 ordinary tokens were target argmax tokens, with no verification failure observed. At first divergences, the top-two score gap had median0.02250, range0.00050–0.16278. Ordinary decoding itself changed on5/6 control prompts when batch size changed. Fresh identical-prefix one-token probes matched29/30; the remaining case flipped two close target scores. This supports numerical execution differences, without proving their effect on stochastic correctness. Artifacts:
greedy-030-comparison.json,accuracy-030-target-equality.json,greedy-030-scored/analysis.json. Read-only fingerprint diagnostics overlapped Avg@16 timing; they did not overlap the completed30×8 throughput benchmark.Complete DSpark counters give 53.74% draft acceptance and mean acceptance length 4.7617. The direct HTTP Avg@16 harness differs from the matched30×8 benchmark above; their throughput values must not be substituted for each other.
Expanded long-run validation: trained a fresh five-layer DSpark for
Qwen/Qwen3-30B-A3B-Thinking-2507. Corpus generation and preparation completed: 8,192 uniquei3_mathquestions, 142,140,970 completion tokens and 143,171,059 total tokens. All 8,192 requests succeeded, with zero errors, aborts or preemptions; 1,164 length-limited responses are retained. Normalized exact AIME matches are excluded. The validated question-disjoint split is 7,372 training / 820 validation, with sequences 819–31,673 tokens.The proper run completed all five epochs / 5,864 updates, with held-out validation and model/optimizer/scheduler checkpoints for every epoch. The selected epoch-5 checkpoint is
checkpoints/4; all 62 tensors are finite. All 5,864 W&B training rows have finite losses, with zero failed records. Completed standalone W&B run.These are teacher-forced validation estimates, not live serving acceptance. The selected trained draft fed the real math RL run with the supplied model and training/evaluation sources, 400 steps, batch 64/group 8, and four trainer/four inference H200 GPUs. Diagnostic checkpoints did not feed that run.
Full 30B RL completed: corrected job 1894 ran from 2026-10-01 03:56:57 to 2026-10-02 16:23:27 UTC and exited successfully. The trainer completed update 400 and its joint policy/draft broadcast at 16:16:01 UTC, then saved the final checkpoint. Checkpoints exist at updates 100, 200, 300 and 400. The file-metric audit finds all 400 completed updates with finite recorded policy/draft losses and timings. The run was not error-free: of 78,142 indexed training episodes, 2,792 failed (889 provider-error and 1,903 harness-error stop conditions); 156 of 5,040 indexed evaluation episodes failed. Root-cause attribution of those later failures and their impact on final scores remain unaudited. This validates the full real-rollout training/broadcast/checkpoint lifecycle at 32K with CP4/EP4 and native CPU optimizer offload; resuming a 30B RL checkpoint and a complete final-quality/error audit are still pending. Completed RL W&B run.
Matched 30B serving benchmark, same four H200s and frozen policy/draft, vLLM 0.30 with fusion off:
The fixed configuration improves complete-batch speed by 65.4% and sustained speed by 57.1% over ordinary decoding in this benchmark. It uses 256 held-out math prompts, concurrency 128, natural EOS up to 31K output tokens, and the production token/logprob workload. Zero request errors or preemptions were recorded. This direct-backend benchmark excludes the router and does not measure an entire RL training run.
Observed early RL speed: W&B completion timestamps for updates 2–20 give 14.20 updates/hour for corrected spec versus 8.08 for
mathcispo(1.76×). Trainer forward/backward averages 204 versus 198 seconds, while residual batch waits average 37 versus 237 seconds. The corresponding serving rates are about 10.68K versus 6.74K output tokens/s (1.59×). These are observational windows, not an isolated speculation effect: losses, policies, nodes and runtime versions differ, and the historical CISPO window includes failed rollouts. A later wall-hour snapshot gives 15 versus 12 completed updates (1.25×) at different policy ages. Neither ratio is claimed as the whole-run speedup.Forward/backward overlaps generation; its share of trainer elapsed time does not measure inference idle time.
off_policy/meanmeasures rollout version age, whileTARGET_LAG=1separately gates new admission. In the inspected window, the off-policy limit of 8 was not reached, existing requests continued generating while admission was gated, and acknowledged weight-sync pauses averaged about five seconds in both runs. For updates/hour, filter W&B history to rows containingtime/forward_backward, then usestepand_timestamp; unfilteredtime/stepalso contains orchestrator records.The corrected run's initial AIME evaluation was 199/240 (82.92%), pass@8 28/30, all at policy v0, versus CISPO's 193/240. The paired-question difference is +2.5 points, 95% interval [−0.42, +6.25], consistent with sampling variation rather than proof of equivalence. In-flight requests retain KV caches across live weight updates; fixed-policy speculative-sampling correctness does not by itself prove distribution equivalence across such updates. Earlier graph-corrected attempts encountered false router health exclusions and a separate host-memory/node failure; they are excluded from healthy end-to-end speed claims.
Standalone performance bug fixed: activation-checkpoint recomputation can call upstream FlexAttention outside the compiled model, reaching Torch2.13's eager fallback. The PrimeRL standalone worker now compiles that attention function explicitly before invoking the upstream trainer. An isolated H20032K/512-anchor attention forward/backward check reduced median time from371.87ms to4.72ms and peak allocated memory from102.10GiB to0.312GiB; outputs and Q/K/V gradients passed the predeclared
rtol=0.03, atol=0.005comparison. These are one-attention measurements, not whole-model speedups or vLLM serving results. The fix passed212 existing configuration/draft tests, Ruff, a real-feature one-GPU train/checkpoint/resume from steps2→4, and four-GPU train/checkpoint at32K. All62 checkpoint tensors were finite;39 changed on resume, and target-owned projections were unchanged. No eager-attention warning occurred. The eight-record diagnostic dataset (repeated for the four-GPU test) establishes runtime compatibility, not held-out learning quality. The completed five-epoch job used its original upstream launcher; its learning results do not measure the subsequent PrimeRL worker compilation fix.A separate 30B trainer configuration check completed two updates at32K with CP4/EP4, compilation, activation offloading and native full CPU optimizer offload, using a fresh compatible draft and two synthetic packed microbatches per update. Losses were finite; peak GPU memory42.73GiB. No real rollout, weight synchronization or checkpoint saving was exercised by this check. This earlier trainer-only check is separate from the subsequently completed standalone training and full RL run above.
The 30B standalone interface also passed on eight real generated responses: two updates, held-out validation, and model/optimizer/scheduler checkpoint saving. Its diagnostic checkpoint then passed cold and cached TP4/DP1 serving startup with default backend selection and AOT enabled, fusion disabled. Both phases completed 16 requests × 512 greedy tokens; all 16,384 tokens matched target argmax, with no request failures or preemptions. Logs confirm automatic MNNVL selection and cached AOT loading on all four workers. This clears that tested 30B topology, not the separate 4B TP2×DP2 failure or real RL weight synchronization. The diagnostic weights did not feed the full RL run.
Original full AIME 2025: 30 questions ×8 samples, vLLM 0.29.0
DSpark achieved 3.572× baseline throughput. Baseline and normal DSpark ran sequentially on the same physical GPU; zero-draft used another H200 on the same node. Throughput is completed output tokens divided by first dispatch to last arrival, including evaluation overhead and excluding server startup/compilation. All three runs had zero preemptions. These are single-run end-to-end measurements.
Accuracy equivalence is not established. Normal DSpark's paired question-bootstrap difference from baseline was −3.75 percentage points, 95% interval [−8.75,+0.42]. Zero-draft's difference was −0.42 points, interval [−5.42,+4.17]. Matching pass@8 or overlapping intervals is not proof of noninferiority.
Original exact weight and training checks, vLLM 0.29.0
Joint draft gradient norms at updates 1–4 were 0.3504, 0.5466, 0.3903, 0.4580. Steady update 2/3 forward/backward took 4.626/2.975s, or 4811/5374 training tokens/s. The short run validates working gradients and synchronization, not a learning trend. CPU-offload validation was a trainer component test with synthetic batches; the complete RL run used the GPU optimizer.
Matched held-out validation used one fixed 512-token batch, seed 42, and eight anchors:
The small dataset contained 10 chunks /8420 tokens from two capped target responses. This comparison establishes functional compatibility, not improved draft quality or generalization.
W&B: joint updates 1–3, resumed update 4. Raw trace/checkpoint artifacts, fingerprint JSONs, helper scripts and the detailed report are retained locally under
outputs/specdecode-dev/and are not included in this PR. The original validation jobs finished and their compute allocations were released.Automated checks executed
235 focused cases passed after merging main: 207 config cases, 8 weight utilities, 5 draft-mask/optimizer cases, 5 inference-ratio cases and 10 existing client cases. The original pre-merge run passed 228 cases. Ruff lint/format passed all 24 changed Python files. PR diff whitespace checks pass. The complete repository suite was not run.
GitHub checks also passed: CPU tests and slim installation (676 passed, 1 skipped, 191 deselected), Ruff, and CodeQL. GPU CI was skipped for this draft; GPU evidence comes from the supervised compute-node runs described above.
To repeat the focused checks in a compute environment:
The follow-up
VLLM_BATCH_INVARIANT=1diagnostic produced30/30 identical1,536-token greedy prefixes across ordinary and DSpark modes, all six256-token batch controls identical across concurrency16/6/1, and30/30 identical next-token replays with exactly equal shared top-five log probabilities. All49,152 tokens per mode followed the target argmax. This supports a numerical explanation for the default greedy discrepancy; it does not prove full stochastic accuracy equivalence. The optional mode is not adopted in the benchmark or RL configuration. Both independent default-settings Avg@16 sets and their pooled Avg@32 are reported above.An independent audit of all960 scored answers reproduces all310/291 correct results using direct integer comparison; the13 fractional answers are all unequal to their gold answer. No scoring discrepancy was found (
accuracy-030-scorer-audit.json).A complete-serving short-sequence sampling test passed under default vLLM0.30 settings with fusion disabled:32,768 four-token requests (131,072 output tokens), two sampling recipes, and every one of256 enumerated outcomes within the predeclared six-standard-error plus2/N tolerance. No zero-reference-probability outputs, request errors or preemptions occurred. Counters confirm20,442 accepted draft tokens across the two DSpark cases. The test uses a constrained four-digit vocabulary and mode-specific reference probabilities; it detects substantial short-sequence sampling bias but does not prove long-context accuracy equivalence. A completed batch-invariant repeat adds32,768 requests/131,072 tokens, passes the same checks, and makes all measured reference probabilities exactly equal across modes. It remains a diagnostic. Artifacts:
serving-distribution-030/analysis.jsonand raw reference/sample records.The proper30B data preparation also now converts raw assistant-token masks into DSpark next-token prediction masks and excludes seven tail positions per response before packing, matching the joint RL adapter. The corrected mask matches the adapter exactly on12 real packed records, including four31K responses (
prediction-mask-check.json). Commitb8d59eb97documents this custom-data contract;git diff --checkpassed. This custom-data mismatch was caught before full draft optimization; raw generated tokens/responses and the supplied RL model/train/eval choices are unchanged. The earlier eight-record checkpoint remains a compatibility probe only.Pending gaps / before this is ready to merge
specdecodeis separate from the SFT entrypoint.max_stepsdoes not stop its outer epoch loop at the pinned revision: a 30B preflight configured for two updates continued into later epochs. Bounded examples use one epoch; the proper five-epoch run has no step cap. No upstream fix is included in this PR.