feat(weight-transfer): add NIXL + ModelExpress synchronization#3060
Conversation
4de7d09 to
3969547
Compare
24eea1d to
771a362
Compare
| """GPU memory utilization. Forwarded as ``--gpu-memory-utilization``.""" | ||
|
|
||
| quantization: Literal["mxfp8", "fp8_per_block"] | None = None | ||
| quantization: str | None = None |
samsja
left a comment
There was a problem hiding this comment.
can we add docs on:
- the new conversion format for moe
- the way nixl.py file work and record the transformation
I will review the pr in depth, doing step by step
samsja
left a comment
There was a problem hiding this comment.
how much did we tested rl run ? I am worried about the potential change in bf16/fp32 for some of the layers, I know that before it was not super clearly definw who was doing what in dtype
samsja
left a comment
There was a problem hiding this comment.
can we rename the ht_to_tt function to hf_to_prl ? kinda not tt anymore no ?
There was a problem hiding this comment.
would it make sens to have this in a prime_rl.broadcast.nixl submodule. next to fileystem and nccl ideally
| ops: list[ConvOp] = [ | ||
| Rename(f"{p}.gate.weight", f"{p}.router.gate.weight"), | ||
| Rename(f"{p}.gate.e_score_correction_bias", f"{p}.expert_bias"), | ||
| routed_experts_op( |
There was a problem hiding this comment.
why is this snake case when the rest is pascal case?
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 4484cb1. Configure here.
The merge of #3060 kept this branch's on_version_pending/on_new_version bodies and dropped main's ModelExpress status updates — the READY signal the NIXL trainer waits on before each broadcast and the INITIALIZING reset after it. Post-startup NIXL runs would hang in the handshake. Both signals are back, ordered as on main (READY pre-update, INITIALIZING post-update) around this branch's staleness/wake logic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…#3050) * fix(orchestrator): enforce target lag strictly at ship time The dispatch gate only stops new rollouts; on tiny fast envs the sink keeps forming batches from buffered rollouts, so the orchestrator ships past TARGET_LAG, finishes, and tears down the watcher while the trainer still has pending NCCL broadcasts. Those broadcasts then block forever in the STABLE->NCCL_READY handshake (no watcher to trigger the inference receive) and the launcher never exits. hold_for_target_lag blocks shipping batch N until the trainer has published v{N-1-TARGET_LAG}, bounding in-queue staleness and keeping the orchestrator alive for every broadcast the trainer will make. Under NCCL the last published version is max_steps-2, exactly what the final batch requires, so the hold never waits on an unpublishable version. Also logs a per-batch staleness decomposition (pre-queue = versions elapsed during generation, in-queue = versions spent waiting in the sink) under staleness/*. Fixes RES-1077 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * remove staleness metrics from ship path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(orchestrator): step-based staleness with demand-driven dispatch Measure a rollout's off-policy lag against the batch that consumes it — batch N trains on policy v{N-1}, so lag = (N-1) - generation version, queue time included. The old off_policy_steps counter only counted weight updates during generation, so a rollout that finished fast but sat buffered across steps trained arbitrarily stale while reporting 0. - max_off_policy_steps now bounds consumption lag: in-flight groups are cancelled pre-pause (cancel_stale_train_groups) and buffered sink survivors are swept whenever the cutoff moves (ship, weight update). Groups share one generation version, so stale groups drop whole. - Dispatch is demand-driven: the orchestrator injects train_demand (batch shortfall minus buffered + partial-group + in-transit rollouts), re-evaluated per scheduled group, replacing the lead-based dispatch gate and the #2990 final-batch NCCL exemption it needed. out_q backlog counts as coverage so ship-holds don't over-provision. - Max Off-Policy in logs is now the honest consumption lag, stamped at ship (reverse_text: steady-state max 3 vs a dishonest 0 before). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review pass: bool dispatch contract, oversampling-aware demand, inline single-use helpers - train_demand -> train_needed: the dispatcher only thresholds the value, and the magnitude isn't even unit-stable across batch modes, so the injected contract is a bool predicate. - Demand target is max(batch_size, max_inflight_rollouts): explicit over-provisioning (oversampling_factor > 1 or a raised max_inflight) keeps racing batch stragglers instead of being silently capped at one batch; capacities below the batch keep a batch_size target so the batch can still complete. - Reject max_off_policy_steps=0 with NCCL broadcast + max_steps: the final batch necessarily trains one version off-policy (the trainer never broadcasts the last versions), so a zero budget would hang the run collecting the final batch. - Inline single-call helpers (hold_for_target_lag, stale_cutoff, the dispatcher lag gauges) into their call sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * require max_off_policy_steps >= 1 at the field The last NCCL batch necessarily trains one version off-policy (the trainer never broadcasts the final versions), so a zero budget can never be met and would hang the run collecting the final batch. A field floor covers this without a conditional validator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix CI regressions: partial-group dispatch deadlock + serialized weight applies Two regressions surfaced by the PR's GPU CI run: - alphabet_sort/rl_opd deadlocked at 126/128 (+3 buffered, 0 inflight): demand hit zero mid-group, so an open group's remaining rollouts were never scheduled — its buffered members count as batch coverage but the group can never finalize. Already-open groups now keep scheduling regardless of demand (try_schedule allow_fresh); only fresh groups are demand-gated. - reverse_text timed out (~26s/step vs ~19s on main): the ship hold waited on the applied version, serializing every inference weight reload into the ship path. Filesystem broadcast decouples publish from apply and the trainer never blocks on the watcher there, so the hold now releases on the published version (tracked in on_version_pending). NCCL keeps waiting for the apply — the trainer blocks in the receive handshake until then, so releasing on publish could strand its final handshake at teardown. Verified: reverse_text e2e clean on both broadcast types (filesystem 6-7s steps, NCCL exits clean), 485 unit tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * raise multi-run integration timeout to 600s Resumed orchestrators now pace to the shared trainer — a batch only ships once the trainer has produced the required policy version — so with three runs round-robining one trainer, beta_resume's tail (steps 21-25) can legitimately exceed the old 300s wait while its versions queue behind the other runs. Matches the 600s budget of the other RL integration tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * retract stale-dropped rollouts from the payload-size estimate drop_stale now mirrors what entering pending_batch added to _payload_total/_payload_count, keeping the token-mode demand estimate consistent with the buffered set. Also expand the allow_fresh comment with the group-finalization invariant it protects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * upload integration run logs as artifacts on failure The multi-run test's failures only surface a sampled tail of one orchestrator's output; the copied per-run orchestrator/trainer/inference logs are what's needed to debug them (currently: beta_resume wedging on a policy version the shared trainer never publishes after resume). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * keep CI test outputs for the failure-artifact upload The output_dir fixture rmtree'd the whole outputs dir at session teardown, so the upload step found nothing. Skip the cleanup under CI (the workspace is discarded with the job) and match log files directly instead of a directory glob. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * dispatch lookahead: cover TARGET_LAG extra batches when freshness allows Exact-one-batch demand synchronized dispatch into one 128-rollout wave per batch: each ship opened demand 0->128 at once, the wave stampeded the env server together (MCP timeouts + backoff retries in CI alphabet_sort), and inference idled ~60s per cycle while the whole wave sat in env round-trips — tripling the step cadence vs main (artifact logs: vLLM alternating 30s busy / 60s '0 running, 0 waiting'). With up to TARGET_LAG batches of lookahead — allowed only while a rollout landing that far ahead still fits max_off_policy_steps — steady-state dispatch is continuous (each arrival frees demand for the next start), keeping the engine fed through env-phase gaps. Costs one version of staleness on the lookahead batch (reverse_text plateau 3->4, within the default cap of 8). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * raise reverse_text mismatch-KL budget to 0.02 Mismatch KL compares generation-time inference logprobs against the training weights, so it includes async training's policy-version gap on top of trainer/inference numerics. With strict pacing + dispatch lookahead the steady-state consumption lag is 2-3 versions (visible and bounded, vs unmeasured before), which puts the last-steps average at ~0.011-0.012 against the old 0.01 budget calibrated for main's profile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * raise alphabet_sort and multi-run integration budgets Artifacts from the failed runs show no wedges — every process was mid-progress at the deadline: alphabet_sort cycles at ~70s on slow vm runners (33s on fast ones; trainer MFU varies 2-6% across the pool) and the multi-run trainer was mid-micro-step serving three runs round-robin. With strict pacing the orchestrators ride the trainer's pace instead of finishing early on stale buffers, so wall time tracks the slowest component. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * raise multi-run inner milestone waits to 600s The wait_for_log/wait_for_file 300s defaults were calibrated for orchestrators that sprint ahead of the shared trainer on buffered batches; with strict pacing three orchestrators ride the trainer's actual speed, so every internal milestone (alpha steps, checkpoint STABLE markers, resume completion) lands slower and a different 300s wait tripped on each CI round. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * count group-scored partial arrivals as batch coverage buffered_count hides group-scoring envs' partial groups for the pipeline log's honesty, but demand must count them — otherwise members sitting between arrival and group completion invite bounded over-dispatch. Also document why a held batch intentionally does not count as coverage (dispatch-ahead keeps inference busy through trainer-bound holds; staleness stays bounded by lookahead + cutoff). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * guard token-export STABLE markers against deleted run dirs The multi-run trainer crashed with FileNotFoundError touching run_gamma/token_exports/step_20/STABLE: the multi-run integration test rmtree's a run's dir once its orchestrator finishes, while the trainer is still flushing that run's tail steps. The broadcast paths already skip deleted runs on FileNotFoundError; mark_stable now does the same. This was the root cause of the recurring multi_run CI failure: the crashed trainer never published the resumed run's next version, so beta_resume's ship-hold waited forever ('failed with code None'). On main the same crash risk exists but the orchestrator never waited on the trainer, so nothing surfaced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * recalibrate multi-run early-step reward floor for strict pacing Batch N's data now comes from v{N-2}..v{N-4} instead of main's effectively-fresher buffer profile, so reward-at-step-7 reads 1-2 policy versions staler (observed 0.14-0.20 across four CI rounds vs the 0.2 floor). The final-reward thresholds still enforce learning quality; the early floor drops to 0.1 — comfortably above a broken run's flat ~0.05-0.08. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * raise rl_sft integration budget to 900s Same class as the other RL integration budgets: the trainer was SIGTERM'd at step 14 with the orchestrator already finished — killed by the 600s deadline steps from completion on a slow runner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * lower multi-run final reward floor to 0.6 With strict pacing the reward-vs-step-index curve sits 1-2 policy versions lower throughout; gamma — the most trainer-contended of the five runs — landed at 0.627 while its siblings cleared 0.69-0.75. A broken run still reads ~0.1-0.3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * bound dispatch freshness at DISPATCH_LAG instead of the drop cap Root cause of the reward-at-step gap vs main: the old lead gate doubled as the dispatch freshness bound (rollouts only started at version >= step-2, landing lag <= 2). The demand rewrite left freshness gated only by max_off_policy_steps (8) plus a deliberately-ahead lookahead batch, so mean consumption lag ran 2-4 versions vs main's 1-2 — visible as Max Off-Policy 3-4, mismatch KL ~2x main's, and reward-at-step lower by 1-2 versions of learning. Dispatch now pauses when new rollouts would train more than DISPATCH_LAG = TARGET_LAG+1 versions behind their generation policy, with the lookahead scaled inside that budget — the tightest bound that still keeps inference busy during ship-holds. Landing lag returns to <= 2 (spill 3); local e2e: Max Off-Policy plateau 4 -> 3, mismatch KL mean ~0.012 -> ~0.008. max_off_policy_steps returns to being purely the drop safety net. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: retrigger GPU tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * tolerate trainer tail-flush when deleting run dirs in multi-run test The fixture's rmtree races the shared trainer's token-export writes, which mkdir the run dir back mid-deletion (OSError: Directory not empty). Retry until the flush for the finished run drains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * restore NIXL ModelExpress handshake signals in the version hooks The merge of #3060 kept this branch's on_version_pending/on_new_version bodies and dropped main's ModelExpress status updates — the READY signal the NIXL trainer waits on before each broadcast and the INITIALIZING reset after it. Post-startup NIXL runs would hang in the handshake. Both signals are back, ordered as on main (READY pre-update, INITIALIZING post-update) around this branch's staleness/wake logic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * reduce to the minimal ship-hold fix + staleness docs Strip the demand-driven dispatch, consumption-lag drop rework, and test recalibrations accumulated on this branch back to main's behavior, and keep only what the spec (docs/staleness.md) strictly requires: - ADVANCE rule: batch N ships only once the trainer has published v{N-1-TARGET_LAG}, so the orchestrator cannot finish and tear down the weight watcher while the trainer still has in-memory broadcast handshakes pending (RES-1077). Main's dispatch gate (START) and in-flight drop aging (DIE) are untouched, so generation freshness and throughput are unchanged in both pacing regimes — the hold only moves where a finished batch waits, never when the trainer consumes it. - policy.version advances at publish confirmation (inference applies immediately and pauses during the update, so published and applied are indistinguishable to consumers), and WeightWatcher.stop() drains an in-flight apply before cancelling — teardown can never strand the trainer mid-handshake. - Keep the trainer-side token-export guard, the multi-run test's deletion-race helper, and the CI failure-artifact pipeline from the earlier iterations; drop everything else. Verified on the original repro (reverse_text, fast env, NCCL): 10/10 batches, clean trainer exit, 2 holds at the buffer-fed tail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fold staleness spec into docs/training.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * drop the docs section for now, trim the hold comment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * never delete the test output dir; clean at run start instead The output_dir fixture deleted the whole workspace at session teardown, racing the failure-artifact upload (and hiding logs from post-mortems generally). Keep it unconditionally and make fresh starts responsible for their own hygiene: alphabet_sort and reverse_text_moe now pass --clean-output-dir like the other rl-launcher tests. Resume runs are unaffected — validate_output_dir returns before cleaning when resuming. The multi-run orchestrators manage per-run dirs the test creates (and the standalone orchestrator entrypoint has no clean flag). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * stamp true consumption staleness on shipped rollouts Batch N trains on policy v{N-1}, so a rollout generated from v{k} is (N-1)-k versions off-policy — queue time included. The dispatcher's in-flight counter only sees weight updates during generation, which undercounts by the queue-wait aging and the dispatch-time baseline gap; the ship-time stamp overwrites it so Max Off-Policy in logs and the rollout records report the real number. The counter itself (and the max_off_policy_steps drop rule it feeds) is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * use the deletion-retry helper for alpha's run dir too Alpha is SIGTERM'd mid-run, so the trainer is actively flushing its token-export tail when the dir is deleted — the recreate race is most likely at exactly this site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Summary
Adds the initial production path for direct trainer-to-vLLM weight synchronization over NIXL and ModelExpress:
trainer-native weights -> declarative trainer-to-HF graph -> typed BF16/FP32 wire arenas -> recorded HF-to-vLLM loader graph -> final vLLM kernel tensorsThis composes the trainer conversion graph from #2797 with vLLM loader recording from #2779. #2598 was used only as a reference for the ModelExpress transport shape.
Key properties:
prime_rl.nixl_weight_transferCorrectness and failure handling
Validation
Ruff 0.13.0 check, Ruff format check, and
git diff --checkpass after rebasing onto currentmain. CPU and GPU workflows are delegated to PR CI.Existing Qwen3.5-MoE, Qwen3.5-MoE VLM, and Nemotron-H tests use the instance conversion API that owns the model config.
WhiteFiber Qwen3-MoE E2E
Validated
Qwen3-30B-A3B-Instruct-2507innixl-modelexpress-nvl72-e2e:max_off_policy_steps=1NIXL was 2.02196x faster for end-to-end rank-0 synchronization.
The NCCL-minus-NIXL mean KL difference was
+0.0058004, with an approximate 95% interval of[-0.0009297, +0.0125306]; the reverse-text workload did not resolve a backend-related KL regression.The NIXL run had zero rollout errors and zero pod restarts. NCCL had three expected stale-rollout cancellations at off-policy level 1 due to slower synchronization, with no transfer or inference failures.
W&B:
Current scope
Note
High Risk
Large new distributed weight-sync path on the critical RL training loop (trainer, orchestrator, vLLM workers) with NIXL/UCX and ModelExpress coordination; conversion refactors touch every custom model family used for broadcast.
Overview
Introduces
weight_broadcast.type = "nixl"as a third in-memory transport (with NCCL and filesystem), wired through shared RL/trainer/orchestrator/inference configs,modelexpress==0.3.0as a core dependency, and trainerNIXLWeightBroadcastplus vLLMNIXLWeightUpdateWorkerfor UCX/NIXL pulls coordinated by ModelExpress (session_id, gRPC port, READY/INITIALIZING rendezvous).The orchestrator
WeightWatchercan advance policy versions from ModelExpress status instead of waiting on filesystemSTABLEmarkers; startup and per-step sync paths treat NIXL like NCCL for dispatch gating and worker-count auto-setup. LoRA remains filesystem-only for in-memory transports.Model weight conversion is refactored: imperative per-family converters become
conversion_opschains (Rename,Stack,routed_experts_op, etc.) with instanceconvert_to_hf/convert_to_primeonPreTrainedModelPrimeRL, pluskeep_in_fp32_for_weight_transferhooks where wire dtype must stay FP32.Smaller changes: drop the optional
modelexpressuv extra from Docker/entrypoint sync, relax inferencequantizationtyping and remove themxfp8SM100 config validator, and passsession_idthrough/init_broadcasterfor NCCL compatibility.Reviewed by Cursor Bugbot for commit 4484cb1. Bugbot is set up for automated code reviews on this repo. Configure here.