Skip to content

[RL][Model Loader] Direct weight reload: write checkpoint tensors into the live model - #58382

Open
aoshen02 wants to merge 6 commits into
vllm-project:mainfrom
aoshen02:wu/pr-a-upstream
Open

aoshen02 wants to merge 6 commits into
vllm-project:mainfrom
aoshen02:wu/pr-a-upstream

Conversation

@aoshen02

@aoshen02 aoshen02 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #57733 and #62407 (merge those first); mypy/CI fail until they land. This PR's diff is only the 8 direct-reload files. They import helpers that #57733 adds (finalize_model_weights, discard_weight_parts, stash_weight_part) and rely on #62407's kept parameter objects.

Validated as the stack #57733 + #62407 + this PR + E1 (aoshen02#179) + derived-tensor in place (aoshen02#180), with same-weight direct reloads on GB200, NUPD=3. GSM8K is measured before and after, and every tensor is fingerprinted against cold start:

Model Job GSM8K pre → post Tensors changed
DeepSeek-V4.1 40010 0.909 → 0.904 0/1709
MiniMax-M3.1 40013 0.951 → 0.951 0/3148
Muse Spark NVFP4 40042 0.839 → 0.838 0/3011
gpt-oss-120b TRTLLM MXFP4 (NUPD=2) 39950 0.938 → 0.936 0/796

Problem

RL weight sync reloads the checkpoint into a serving engine. The layerwise reload moves every layer to meta, materializes it again, loads the checkpoint and reruns post-load processing per layer. That costs a second copy of each layer at a time, and its correctness depends on per-layer completeness bookkeeping, so partial or reordered updates are hard to get right.

Fix

VLLM_RELOAD_MODE=direct (registered in vllm/envs.py; the default stays layerwise) writes checkpoint tensors straight into the live model and reruns cold-start post-load processing once, at the startup addresses:

  • At cold start, before process_weights_after_loading (PWAL), record each tensor's checkpoint layout.
  • begin_direct_reload, called from initialize_layerwise_reload, views every tensor whose layout PWAL changed back to its checkpoint layout, on the same storage. The loaders then write the checkpoint tensors in place.
  • finish_direct_reload, called from finalize_layerwise_reload, clears the one-shot "already processed" guards and reruns PWAL for every module, then the model-level hook, as cold start does.
  • replace_parameter keeps a parameter's address when PWAL changes its layout: under direct reload, the new tensor is written into a view of the existing storage when it fits.
  • Refused at cold start: draft_lm_head_quantization. The quantized draft lm_head would be written into the target lm_head it shares.
  • Refused at begin, before anything is modified:
    • no layout was recorded (also the case for pre-processed IPC weight-cache weights, already in runtime layout);
    • a storage is too small for its checkpoint layout.
  • sharded_rdt refuses direct mode at init, because its replay plan is baked by a layerwise (meta) dry run.

Delivery contract. An update must deliver every checkpoint tensor of each module whose PWAL changes the tensor's layout or is not idempotent. Tensors without such processing, for example a frozen embedding table, may be left out and keep their bytes. Nothing in this PR checks the contract: until the completeness follow-up merges, a violation is not detected.

PWAL must keep every checkpoint tensor registered under its checkpoint name. This builds on #62407 (retire/alias instead of delete/rename).

Known limitations. Non-default MoE backends whose PWAL reallocates derived constants on a rerun are not yet refilled in place: TRTLLM FP8 per-tensor, FlashInfer CUTLASS NVFP4/MXFP4, CuteDSL, and compressed-tensors FP8 constants. They are fixed in the derived-tensor follow-up. Online quantization is supported by a later follow-up.

Tests

Dependencies / merge order

#57733 → #62407 → this PR. The follow-ups stack on this one: completeness check → moved-address/host-float check → side-tensor detector. The derived-tensor in-place PR, the NCCL empty_cache fix and the loader-bypass fix are independent.

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify

mergify Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Documentation preview: https://vllm--58382.org.readthedocs.build/en/58382/

@mergify mergify Bot added the documentation Improvements or additions to documentation label Sep 23, 2026
…o live parameters

Add WeightTransferConfig.reload_mode ("layerwise" | "direct", default
layerwise) for the nccl and ipc weight-transfer engines. Layerwise reload moves
each layer to meta, loads a temporary copy, re-runs post-load processing and
copies back into the captured storage. For a model whose post-load step leaves
the parameters as the checkpoint has them, that detour is pure overhead.

In direct mode start_reload records the address and layout of every parameter
and buffer, model.load_weights runs unchanged so each weight_loader copy_s into
the live storage, and finish_reload checks that nothing moved; a failure leaves
the model undefined and refuses further updates. There is no rollback and no
abort: a failed reload means the engine must be restarted.

start_reload / finish_reload exist because a second mode has to be dispatched
somewhere, and because finish has to complete whichever mode start began: the
mode is latched on the model rather than re-read from a config that may have
changed in between. Only the two engines that have a mode go through them;
every other caller keeps naming the layerwise functions directly.

Signed-off-by: Ao Shen <aoshen524@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Signed-off-by: aoshen02 <aoshen@inferact.ai>
aoshen02 and others added 4 commits October 27, 2026 08:35
Signed-off-by: aoshen <aoshen@inferact.ai>
…o the live model

Replace the earlier direct-reload implementation with the reworked one.

At cold start, post-load processing records each tensor's checkpoint
layout. begin_direct_reload views every re-laid-out tensor back to that
layout on its own storage, the weight loaders write the update in place,
and finish_direct_reload clears the one-shot post-load guards and reruns
process_weights_after_loading. Addresses stay where CUDA graphs captured
them.

direct mode refuses draft_lm_head_quantization (speculative config and
begin) and sharded RDT transfers.

The ipc/nccl engine, weight-transfer config and docs changes of the
earlier version are dropped; the PR now touches only the direct reload
files. It depends on vllm-project#57733 and vllm-project#62407 (replace_parameter /
retire_parameter, kept parameter objects).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>
begin_direct_reload already refuses draft_lm_head_quantization in the
worker that reloads; drop the SpeculativeConfig check. Share the layout
span computation between direct.py and _view_in_place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: aoshen <aoshen524@gmail.com>
…tart

- Refuse draft_lm_head_quantization when the layout is recorded (cold
  start) instead of at the first update.
- Record nothing for pre-processed (IPC weight-cache) weights: they are
  already in runtime layout, so a later begin refuses.
- Reuse _extent for the layout span; compute it on a meta tensor before
  set_, which may grow a too-small CPU storage instead of raising.
- Remove unused test scaffolding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: aoshen <aoshen524@gmail.com>
@aoshen02 aoshen02 changed the title [RL][Model Loader] Direct weight reload: write checkpoint weights into live parameters [RL][Model Loader] Direct weight reload: write checkpoint tensors into the live model Oct 27, 2026
hao-aaron added a commit to aoshen02/vllm that referenced this pull request Oct 27, 2026
Multi-part checkpoint weights no longer park parts across load_weights
calls. finalize_model_weights keeps only the model-level post-load hook.

- Molmo/Molmo2: wte.embedding and wte.new_embedding are written straight
  into their rows of this rank's embed_tokens shard, in any order or call.
- Llama4 DP vision: q/k/v load into their rows of the replicated qkv_proj
  through the stacked mapping; _consolidate_qkv_weights is gone.
- The FP8 multi-part cases (indexer wk dequant for DeepSeek-V2/V3.2,
  dots3, GLM5-next; GLM5-next FP8 MLA projections; MiMo-V2 fused qkv) and
  the Jina-v3 LoRA merge go back to main's handling; they move to
  checkpoint-layout parameters on top of direct reload (vllm-project#58382).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeFSSrpw7yTpLz2C2sHDKA
Signed-off-by: Aaron Hao <aaronhao@inferact.ai>
vllm-project#57733 no longer parks multi-part weights across load_weights calls, so
there is nothing to discard at reload start and no parked part to test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeFSSrpw7yTpLz2C2sHDKA
Signed-off-by: Aaron Hao <aaronhao@inferact.ai>

@hao-aaron hao-aaron left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through this against the RFC tracker. Leaving out anything F2/F4/F5/F7/F8 or PR 5 already covers. A few things I think belong here though:

1. Refuse online quant in this PR. F8 adds support, but until then nothing stops it, and finish_direct_reload pops _already_called_process_weights_after_loading, which is the only thing keeping online FP8 PWAL (online/fp8.py) from re-quantizing already-converted bytes. Same shape as the draft_lm_head_quantization refusal; a test next to that one would be nice.

2. Replaced param objects are silently treated as derived. _checkpoint_views skips anything without _ckpt_layout, so if cold-start PWAL registers a new Parameter under a checkpoint name, begin skips it and the loader writes checkpoint bytes into the runtime layout (a same-numel transpose passes default_weight_loader). F4 snapshots at first begin, after this already happened, so it won't catch it. Could we record the names in record_checkpoint_layout and raise in begin_direct_reload if one now points at an object with no recorded layout? That makes the #62407 contract enforced instead of assumed.

3. Failure state. test_retry_after_aborted_update_matches_cold_start says the worker refuses to run a partly updated model, but I don't see that anywhere. Either add a minimal in-progress/failed flag that begin/finish set, or fix the comment and add it to Known limitations.

Smaller:

  • Dispatching direct mode from inside initialize_layerwise_reload / finalize_layerwise_reload reads oddly, and the env is checked separately at begin and finish. A small start_reload / finish_reload that stores the mode on the model would be cleaner (#63599 has one in reload/lifecycle.py if you want to borrow it).
  • finish_direct_reload searches for a "cuda" param; the engines already have self.device, could just pass it through.
  • Popping the guard on every module also hits MoERunnerInterface, which sets it in __init__ as a hack. Probably harmless, but limiting it to modules with a quant_method seems safer.
  • _view_in_place bounds writes by storage.nbytes() rather than the tensor's own range, so it assumes nothing else lives in that storage. Fine if F4/F7 add an overlap check, just want to make sure it's tracked.
  • Add VLLM_RELOAD_MODE to the ignored set in compile_factors so toggling it doesn't invalidate the compile cache.

This review/comment was written by an AI agent (Claude Code) on my behalf.

@mergify

mergify Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @aoshen02.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants