Repository navigation
[Model Loader] Run the model-level post-load hook once at the end of every load and reload - #64357
Merged
Merged
Conversation
… hook load_weights must not assume all weights arrive in one call. RL weight updates call it once per bucket (verl: 512MB buckets, tensor by tensor; the NCCL engine: once per tensor), so a weight and its scale, or the halves of a merged tensor, can land in different calls, and post-load work inside load_weights ran on partial weights, once per bucket. One finish step, finalize_model_weights(model), now runs after every full load and every reload: model_loader/utils.py::process_weights_after_loading (cold load, including dummy loads) and finalize_layerwise_reload (reload_weights and the NCCL/IPC/M2N/sharded-RDT finish_weight_update). It raises on multi-tensor weights whose parts never all arrived, then calls the model's process_weights_after_loading once. No per-model opt-ins. This mirrors SGLang's single post-processing at end_weight_update. Cross-call pairing: stash_weight_part(module, key, part, tensor) parks a part on the module (cloned, since transfer buckets reuse their buffers) and returns all parts once complete. It replaces every call-local pairing buffer, which dropped or raised on a pair split across calls: the FP8 indexer wk dequant (DeepSeek-V2/V3.2 nvidia+amd model and MTP, deepseek_mtp, HY-V4 amd/nvidia model and nvidia MTP, dots3), GLM5-next indexer wk and BF16-kept MLA projections (model and MTP), MiMo-V2 and MiMo-V2 MTP fp8 qkv (their own persisted dicts), the molmo/molmo2 split embedding, the Llama4 DP-vision q/k/v concat and the Jina-v3 LoRA merge. Moved to process_weights_after_loading (out of load_weights): - DFlash family fused context-KV buffers (Qwen3 DFlash, Qwen3/Gemma4 DSpark, LiLiCorr, Laguna). The fused raw-weight path is used only when the projections are unquantized and not CPU-repacked (_reads_raw_weights); otherwise each projection runs through its quant method. - BERT MLM tied decoder copy; DeepSeek-V4/V4.1 mHC broadcast; LiLiCorr inference buffers (a dummy load now builds them); the OpenPanguModel sweep; Gemma4 MTP lm-head cache invalidation and suppress-token index. - DeepSeek-V4 XPU target/MTP/DSpark finalize_mega_moe_weights (the lazy call in MegaMoE forward() is gone); MiniCPM-V/O resampler device move (a dummy load crashed); OpenPangu text/MTP sweeps (VL delegates); DeepSeek-V4.1 NVIDIA Engram page-cache drop and THP collapse. StaticSinkAttention.update_sink_kv clears sink_populated so a re-run hook rewrites the cached sink KV. No opt-in handshakes: the DeepSeek-V4/V4.1 VL wrappers drop finalizes_weights_during_load / _weights_finalized and the language-model- first regrouping and always delegate the hook; stream_language_model_first is removed. No load_weights calls the model hook any more. Decisions on one call's loaded set run only for a complete load (is_mtp_completeness_check_enabled, which the weight-transfer engines turn off): AutoWeightsLoader's skipped-alias guard, DSpark confidence-head drops, Qwen3 DSpark, Eagle3, AliceAI and LiLiCorr checkpoint checks, the Phi-3-V embedding alias, the NemotronVL seq-cls head init, and the MTP layer checks that were still ungated. TensorizerLoader: a raw (not vLLM-serialized) checkpoint loads through BaseModelLoader.load_model instead of a private helper that skipped per-layer quant processing; block-FP8 checkpoints crashed in their first forward. Same change and test as vllm-project#55318. Co-authored-by: nicole-lihui <44200120+nicole-lihui@users.noreply.github.com> Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai> # Conflicts: # .buildkite/test_areas/models_basic.yaml # tests/models/test_deepseek_v4_vl_stream_weights.py # vllm/model_executor/models/mimo_v2.py # vllm/models/deepseek_v41/amd/vl_model.py
- Leave the raw tensorizer path, the MiniCPM-V/O resampler move and the DeepSeek-V4 XPU lazy finalize in forward() as on main; none is needed for loads split across calls (the tensorizer change is vllm-project#55318). - torchao reload and the expert-substitution error path run inside load_weights, so they restore layers only (finalize_layerwise_processing) and leave the model hook to the load finish. - dots3 vision: the hook now re-runs at every reload finish; compile once. - Drop the OpenPangu delegate test; one-line docstrings. - Fix a mis-indented CI path from the main merge. Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once tied, `embed_tokens` was the canonical name of the shared embedding, so the checkpoint's `model.embed_tokens.weight` was skipped as its alias and every reload left the embedding stale. Re-register the tie last so the language model's name stays canonical. Also update the MiMo-V2 split-qkv test to the shared pending-parts store. Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai> # Conflicts: # .buildkite/test_areas/models_basic.yaml
… reload Model Runner V2 records the DFlash context-KV step in its FULL CUDA graph, so reallocating the fused buffers on a drafter reload left the graph reading the old weights. After a W0->W1 update the acceptance rate fell from 0.310 to 0.013 on Qwen3-4B + DFlash-b16 (GB300). Copy the rebuilt tensors into the existing ones when the shape is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
LiLiCorr scores candidates inside the DFlash2 FULL CUDA graph, so its fused edge weights and factor splits must keep their addresses across a reload, like the DFlash context-KV buffers. Extend `_rebuild_in_place` to tuples of tensors and use it for `materialize_inference_buffers`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
…tream Signed-off-by: aoshen02 <aoshen@inferact.ai>
…remaining models Bring the PR in line with the direct-reload branch it is split from. - Weight parts: `discard_weight_parts` drops parts parked by an update that never finished when the next reload starts; a parked part carries an event recorded on the stream that parked it, so pairing across the packed NCCL receive streams is safe. `finalize_model_weights` checks the parts, then runs the model-level hook. - DeepSeek V3.2 / DeepSeek MTP: the FP8 indexer wk pair goes through `stash_weight_part` (it keeps vllm-project#59223's cross-call pairing and adds the discard and parts check). - Tensorizer runs the loader hook; MiniCPM-O/V, Phi-3-V, DeepSeek V4 XPU, Muse Spark (AMD model, DSpark) build their caches in `process_weights_after_loading`; BERT MLM derives the decoder only when the checkpoint omits it. - DFlash/LiLiCorr: drop `_rebuild_in_place` and its test; refilling the derived buffers in place moves to the follow-up that adds `set_cached_tensor`. The dots3 vision compile guard moves to the dots3 vision follow-up. - Tests: weight parts (discard, streams, BERT decoder), packed tensor, tensorizer, expert substitution, OpenPangu post-load; CI lists the merged `test_deepseek_v4_vl.py` instead of the removed `test_deepseek_v4_vl_stream_weights.py`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
…g rerunnable Weight reloads rerun process_weights_after_loading. dots3 NOTE vision: - the FP8 vision MoE returned early once its fused buffers existed, so a reload left them built from the startup experts; it also deleted the checkpoint experts, so a reload had nowhere to write them. Rebuild the fused experts on every call, refill the buffers in place, and keep the checkpoint experts registered. - compile_block_modules replaced attn/norm modules with torch.compile wrappers, renaming their parameters under `_orig_mod` (a reload could not find `visual.blocks.N.attn.*`) and wrapping them again on the second call. Compile the modules in place with `Module.compile`, once. Static sink attention (OpenPangu): `update_sink_kv` only reset `sink_populated`; the population runs in the `maybe_populate_sink` op, recorded as a no-op when CUDA graphs were captured, so graph replay never wrote the new sink K/V into the KV cache. Write them into the KV cache at once when it exists (every reload); cold start keeps the lazy population in forward, which runs during graph warmup before capture. Tested on the real attention class, before and after `bind_kv_cache`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
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>
…exer-wk revert Follow-up to 5abd769 (review): - `_try_load_fp8_indexer_wk` went back to main (a dict buffer), but deepseek_mtp, HY-V4 (amd/nvidia model, nvidia mtp) and two GLM5-next tests in test_quark.py still passed the module. Revert those to main too. - Molmo/Molmo2 wrote each vocab half straight into `embed_tokens.weight.data`, bypassing the parameter's weight loader. Layerwise reload restores a layer on meta and wraps its loaders to buffer and count what arrives, so a direct write landed on the meta tensor and counted nothing: the layer would be reported as failed to load and keep its old embedding. The half now goes through `param.weight_loader(param, weight, shard_id)`, an offset-aware loader installed on `embed_tokens.weight` at construction (whole-weight loads still use the default vocab-parallel loader). Molmo2 shares the helpers with Molmo. - A checkpoint with only one half is rejected again, except under `disable_mtp_completeness_check` (partial updates, where the other half may come in a later call). 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>
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>
Split the other models out into a follow-up PR. Revert to main: Molmo, Molmo2, Llama4 DP vision, BERT MLM, Phi-3-V, OpenPangu (and StaticSinkAttention), MiniCPM-V/O, Gemma4 MTP, Laguna DFlash, LiLiCorr, Eagle3 (Llama, Qwen3), NemotronVL, AliceAI and Bailing V3 MTP, with their tests. Qwen3 DFlash/DSpark stay: the DeepSeek-V4/V4.1 and Muse Spark DSpark drafters build on them. Gemma4 DSpark stays too, since it inherits their post-load hook and its fused K build must handle processed weights. Signed-off-by: Aaron Hao <hao-aaron@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With mm_encoder_tp_mode="data" the vision qkv_proj is replicated, and load_weights gathered q, k and v and concatenated them once all three had arrived. A weight update that sends them in separate load_weights calls hit torch.cat on None. Give the replicated qkv_proj weight and bias a loader that writes each part into its rows, and route q/k/v through the normal stacked mapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SeFSSrpw7yTpLz2C2sHDKA Signed-off-by: Aaron Hao <aaronhao@inferact.ai>
hao-aaron
requested review from
AndreasKaratzas,
DarkLight1337,
dllehr-amd,
hongxiayang,
vadiklyutiy and
zyongye
as code owners
October 28, 2026 20:04
Contributor
|
/ci run |
|
✅ Triggered Buildkite CI #98695 for commit |
hao-aaron
added a commit
to hao-aaron/vllm
that referenced
this pull request
Oct 30, 2026
… in place The V2 speculator captures the context K/V step in its FULL CUDA graph. Beam's DFlash2 drafter rebuilt its stacked K/V and K-norm weights, and Kimi-K3's DSpark drafter its stacked KV-norm weights and KV scales, as new tensors on every load, so after a draft weight update the graph read freed memory (Beam: draft acceptance 7.03 -> 4.89). Refill them in place. Originally aoshen02's 1d14daf on vllm-project#64357, moved here. Co-authored-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Aaron Hao <hao-aaron@users.noreply.github.com>
Contributor
|
/ci retry |
|
✅ Queued 2 failed job(s) for retry in Buildkite CI #98695. |
ywang96
approved these changes
Oct 30, 2026
Collaborator
Author
|
/ci retry |
|
✅ Triggered Buildkite CI #98826 for commit |
khluu
disabled auto-merge
October 30, 2026 22:52
5 of 6 tasks
gcanlin
added a commit
to gcanlin/vllm
that referenced
this pull request
Nov 2, 2026
vllm-project#64357 runs the model's process_weights_after_loading at the end of every reload, and vllm-project#64336's direct mode reruns it too, so the block-FP8 shards a reload writes are requantized in place. Remove the refusal added until that landed. Test both modes through start_reload / finish_reload. Signed-off-by: Canlin <canlinguosdu@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 tasks done
aoshen02
added a commit
to aoshen02/vllm
that referenced
this pull request
Nov 2, 2026
The earlier revision (run the model-level hook on every load and reload) landed in reworked form as vllm-project#64357. This PR now carries the follow-up: one post-load hook signature, with the loader running every module's hook. Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 tasks done
gau-nernst
pushed a commit
to yeh-sudo/vllm
that referenced
this pull request
Nov 2, 2026
…g hooks vllm-project#64357 and vllm-project#63440 each added process_weights_after_loading to MuseSpark1xForCausalLM, and the second definition replaced the first. On ROCm that skipped materialize_residual_path_constants, so the monokernel refused to attach, and mypy fails on main with no-redef. Run both steps in one hook: materialize the constants first, then attach the monokernel, which needs the folded router weights. Co-authored-by: Claude Signed-off-by: Alex-Wengg <hanweng9@gmail.com> Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>
zydo
added a commit
to zydo/vllm
that referenced
this pull request
Nov 2, 2026
vllm-project#64357 and vllm-project#63440 each added a process_weights_after_loading to MuseSpark1xForCausalLM, so the second definition silently replaced the first and broke pre-commit on main (ruff F811, mypy no-redef). Run both steps in one method: materialize the residual-path constants first, then attach the monokernel, which snapshots the final weights. Signed-off-by: zydo <zydo.git@gmail.com>
6 tasks done
Member
|
/ci run all |
|
❌ CI commands require an open PR. |
6 tasks done
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.
Thanks to Tony Wang (@new-TonyWang) for design and review contributions.
PR 1 of the direct weight reload stack (RFC #54477). It replaces #57733. The code is the same, plus the Llama4 DP-vision fix that #57733 dropped when it was narrowed.
Problem
RL weight updates do not load a model in one
load_weightscall. The weight-transfer engines (NCCL, IPC, M2N, sharded RDT) andreload_weightscall it once per bucket, or once per tensor, betweeninitialize_layerwise_reloadandfinalize_layerwise_reload. On main, three things go wrong with that:finalize_layerwise_reloadonly finishes the layers. The model'sprocess_weights_after_loading()runs only from the cold-load path. So any state a model derives from its weights in that hook, such as fused buffers or broadcast copies, keeps the old weights' values after an update.load_weightsinstead. It then runs once per bucket, on partial weights. Under layerwise reload it runs while the layers are still on meta, which fails withCannot copy out of meta tensor. This affects DeepSeek-V4/V4.1, the DFlash/DSpark drafters and Muse Spark.confidence_headtoNone.Fix
finalize_model_weights(model)calls the model'sprocess_weights_after_loading(). It is called once at the end of a cold load (as before, after per-layer quant processing) and once at the end of everyfinalize_layerwise_reload. That coversreload_weightsand the NCCL, IPC, M2N and sharded-RDT engines.finish_weight_updatenow callsfinalize_model_weightsonce.finalize_layerwise_reload, so it still skips the hook, as on main.finalize_layerwise_processing, so they do not run the model hook early.load_weightsinto the hook. Eachload_weightsbelow now only loads, and the hook runs at the end:finalize_mega_moe_weightsis a no-op).finalize_mega_moe_weights. The lazy call in MegaMoEforward()is removed, since dummy loads run the hook too._build_fused_kv_buffers..weightdirectly is used only when the projections are unquantized and not CPU-repacked (_reads_raw_weights). Otherwise each projection runs through its quant method.qkv_proj.weightdirectly, now after quant processing, so a quantized or CPU-repacked Muse Spark drafter would read kernel-layout weights.load_weights, so its own post-load step runs and a reload refills it in place.materialize_residual_path_constants.load_weightsfinalization saw all its weights (stream_language_model_first,finalizes_weights_during_load,_weights_finalized). That is no longer needed and is removed.is_mtp_completeness_check_enabled(), which the weight-transfer engines turn off during an update:confidence_headdrop in Qwen3, Gemma4 and DeepSeek-V4/V4.1 (NVIDIA/AMD).AutoWeightsLoader._check_skipped_aliases.compile_block_moduleswould wrap the blocks in anothertorch.compileeach time. It now compiles in place withModule.compile(parameter names keep no_orig_mod) and only once. The vision MoE is unchanged from main: its hook fuses the experts into FP8 once, deletes the checkpoint experts and returns early afterwards, so a reload leaves the vision experts as they are.mm_encoder_tp_mode="data").ReplicatedLinear, andload_weightsgatheredq_proj/k_proj/v_projand calledtorch.catonce all three had arrived. An update that sends them in separate calls failed ontorch.catofNone.QKVParallelLinear/RowParallelLinearwithdisable_tp=Truein DP mode, so q/k/v go through the normal stacked mapping and each part is written into its rows as it arrives._consolidate_qkv_weightsis removed._load_model_serialized_cpu, which ran the model hook but skipped per-layerprocess_weights_after_loading. It now goes throughBaseModelLoader.load_model.Not in this PR
Weights combined from several checkpoint tensors inside
load_weights. A weight plus its scale, or a fused FP8 qkv, still need every part in the same call, as on main:wkdequant;These are later PRs in the stack (PR 6).
Drafters that still finish inside
load_weights, unchanged from main:materialize_inference_buffers.mix.Lcheck,fold_from_raw_and the fused buffers. It also inherits the new DFlash hook, so at cold start its fused KV buffers are built twice, the second time after quant processing.A bucketed update of these drafters behaves as on main. LiLiCorr and Laguna are in the F9 follow-up.
Hooks that are not safe to rerun. Every model-level hook reachable from a top-level model was checked for a second run on reloaded weights. These are not fixed here:
fuse_shared_expert, the first run setsshared_experts = None(finalize_mega_moe_weights), so the second run raisesAttributeErrorinload_shared_expert.finalize_weightsreturns early (stale experts), andprepare_attn_preshuffle/prepare_gateup_preshuffleshuffle in place again, so layers that the update did not resend are shuffled twice.finalize_weightsreturns early after droppingw13_weight.finalize_weightsreturns early once the weights are transformed. The XPU runtime currently rejects mega-MoE, so the hook is a no-op._w_foldand Muse Spark's residualalpha/betaare re-created by the hook, so a captured CUDA graph keeps reading the old ones.load_weights, so neither is worse than main._context_kv_norm_weights, are also re-created. The V2 speculator's FULL CUDA graph reads the context K/V buffers, so after a reload it reads freed memory and draft acceptance drops (Beam DFlash2 TP4, split reload: 7.03 → 4.89; MiniMax-M3.1 MXFP8 DSpark TP4: 5.70 → 4.07). Outputs stay correct because the target verifies the drafts. Writing these buffers back in place is PR 5 (refill_derived); with it, acceptance after the reload matches the cold start.Tests
tests/model_executor/model_loader/test_reload.py::TestModelHookOnSplitLoad: the model hook runs once, after all weights, for a layerwise reload split into two buckets, a partial layerwise update, and a sparse NCCL update in two chunks.tests/models/test_qwen3_dflash_mxfp8_context_kv.py: the MXFP8 context projection matches the QKV projection, following the real load order (post-load steps, then the context buffers).test_expert_substitution.py: updated for the Tensorizer path.tests/models/test_deepseek_v4_vl.py: replacestest_deepseek_v4_vl_stream_weights.pyandtest_deepseek_v4_vl_rocm.py. It checks that the VL wrappers leave finalization to the hook.test_tensorizer.py::test_deserialized_hf_weights_are_ready_for_forward.dc45a5f552(this PR's merge base) with this branch at4df1d673doverlaid:test_reload.py::TestModelHookOnSplitLoad2,test_qwen3_dflash_mxfp8_context_kv.py6,test_deepseek_v4_vl.py3,test_expert_substitution.py13.tests/v1/spec_decode/test_lilicorr.py: 33 passed after7e0898bdc(its coverage test builds the model without__init__and needed_context_kv_proj).test_tensorizer.py::test_deserialized_hf_weights_are_ready_for_forwardwas not run here: the venv has notensorizerpackage.mm_encoder_tp_mode="data", prefix caching off.load_weightscalls in v, k, q order, throughinitialize_layerwise_reload/finalize_layerwise_reload.expected Tensor as element 0 ... got NoneType(thetorch.catin_consolidate_qkv_weights).0403d305a, nightlydbcf568821with this branch overlaid:Cold start with the DSpark drafter (slurm 40659): TP=4, 7 speculative tokens, GSM8K 1319 questions, 5-shot, greedy.
Weight update through NCCL (slurm 40658), with the sharded-trainer benchmark:
fsdp=2, layerwise reload, 2 syncs of a checkpoint in which every tensor changes.Cannot copy out of meta tensor(the post-load step insideload_weights).gemm1_clamp_limit,tid2eid, the RoPE cache andtopk_indices_buffer. These are rebuilt by layerwise reload's per-layer processing, not by this PR's hook; keeping their addresses is the job of direct reload ([RL][Model Loader] Direct weight reload: write checkpoint tensors into the live model #58382).🤖 Generated with Claude Code
https://claude.ai/code/session_01SeFSSrpw7yTpLz2C2sHDKA