[Bugfix] Fix stale HPC QK-norm weights after weight refit - #56706
Merged
ywang96 merged 2 commits intoSep 15, 2026
Merged
Conversation
HpcRopeNorm kept float32 copies of q_norm/k_norm weights that were filled only by its process_weights_after_loading(). That hook is dispatched solely by the HpcModule loop in model_loader/utils.py, which runs at cold start; no reload path (reload_weights, NCCL, IPC, Sharded RDT) reaches it, so after an RL weight refit the fused kernel kept consuming pre-refit norm weights. Fix: build q_norm/k_norm in float32 when HPC is active (RMSNorm takes a dtype kwarg) and have the kernel read them directly. With no derived copy there is nothing to refresh, on any load path. Reading the fallback norms directly also drops the old implicit "fallback_qnorm is not None" guard, so the invariant is now enforced where it belongs: HpcRopeNorm.__init__ raises ValueError when QK-Norm is enabled without both fallback norms, and _forward_impl narrows explicitly. HpcIHCPre/Head bound aliases to owner weights in the same hook. Those aliases already survived refits (layerwise reload copies into and re-registers the original tensors), so this is cleanup rather than a fix: their forwards now read the owner attributes directly. With no HpcModule needing the hook, the three overrides, the base-class no-op and the dedicated loader loop are deleted. Known limitation, deliberately out of scope: sharded-state artifacts saved by an older build contain the retired qnorm_weight/knorm_weight keys and ShardedStateLoader will reject them; regenerate the artifact. Signed-off-by: aoshen02 <aoshen524@gmail.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen02@users.noreply.github.com>
Collaborator
Author
|
@thisjiang Hi could you take a look? |
Collaborator
Author
|
/ci run |
|
✅ Triggered Buildkite CI #88695 for commit |
Contributor
Thanks! I'll test 'sm90 + HY3 + HPC_ATTN refit' on Hopper GPU and share the results once ready. |
Collaborator
Author
Thank you! |
Collaborator
Author
|
/ci run |
|
✅ Triggered Buildkite CI #88800 for commit |
Contributor
|
Hi~I verified 'sm90 + HY3 + HPC_ATTN refit' on Hopper GPU and confirmed the results are correct. Thanks for your fix! |
ywang96
approved these changes
Sep 15, 2026
ItsRoy69
pushed a commit
to ItsRoy69/vllm
that referenced
this pull request
Sep 15, 2026
…ct#56706) Signed-off-by: aoshen02 <aoshen02@users.noreply.github.com>
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.
Purpose
HpcRopeNormkept float32 mirror Parameters (qnorm_weight/knorm_weight) of theq_norm/k_normweights. They were filled byprocess_weights_after_loading(), and thathook was dispatched only by the generic
HpcModuleloop inmodel_executor/model_loader/utils.py, which runs at cold start.No reload path reaches that loop:
base_loader.load_modelutils.process_weights_after_loadingweight_cache/ipc_loaderGPUModelRunner.reload_weightsinitialize/finalize_layerwise_reloadwake_upafter sleeplayerwise.pyonly dispatchesprocess_weights_after_loadingfor layers carrying aQuantizeMethodBaseand for deferred attention layers.HpcRopeNormis neither, so after anRL weight refit the fused kernel consumed pre-refit QK-norm weights — silently, with no
error and no warning.
Fix
Build
q_norm/k_normin float32 when the HPC path is active, and have the kernel readthem directly. No derived copy means nothing to refresh, on any load path. The checkpoint is
untouched (still bf16) —
default_weight_loadercasts onparam.data.copy_().Cleanup bundled in (not a fix)
HpcIHCPre/HpcIHCHeadbound owner weight aliases in the same hook. Those aliases dosurvive refits (verified empirically, see Test Result), so their forwards now read the owner
attributes directly purely to remove the last reason for the hook to exist. With no
HpcModuleneeding it, the three overrides, the base no-op and the loader loop are deleted(4
process_weights_after_loadingmounting points → 3).The two failure modes are different and it matters:
HpcIHCPre/HpcIHCHeadHpcRopeNormcopy_().float()duplicateAn alias follows its owner because layerwise reload's
_copy_and_restore_kernel_tensorscopies in place and
_place_kernel_tensorsre-registers the original Parameter objects.A duplicate does not.
Test Plan
New test
test_hpc_rope_norm_kernel_sees_refit_norm_weightsdrives the production_forward_implwith a recordinghpcstub and asserts the kernel receives the refit Q andK weights, float32, same storage.
End-to-end on GB200 (sm100) with Tencent hpc-ops built from source, HY4
(
HYV4ForCausalLM,enable_ihc: true,hc_mult: 4,hidden_size: 6144), three arms —base,base2(same code asbase, to establish the run-to-run noise floor),pr— plus adeliberately perturbed arm as a positive control.
Test Result
Unit —
tests/model_executor/model_loader/test_reload.py -m "not slow_test", run insidevllm/vllm-openai:nightly(aarch64, GB200):test_hpc_rope_norm_kernel_sees_refit_norm_weightsFAILS without the change and PASSES withit — it is the new test, and it does catch the bug.
The one remaining failure,
test_kv_scale_reload, fails identically with and without thechange; it is pre-existing in that image and unrelated.
Applying only this PR's
utils.pydeletion (theHpcModuleloop + its import) on top of anotherwise untouched tree gives byte-identical results to not applying it, confirming the
removed mounting point is inert for every non-HPC path.
Lint —
pre-commit run --files <changed>: all hooks pass. (mypy initially flaggedrope_norm.py:398-399 Item "None" of "Any | None" has no attribute "weight"— the same latentcrash the removed
fallback_qnorm is not Noneguard used to prevent. Fixed by validating theinvariant in
__init__with an explicitValueErrorand narrowing at the use site.)End-to-end, HY4 on GB200 / sm100 — the HPC gate fires:
HpcIHCPre: 8,HpcIHCPost: 8,HpcIHCHead: 1,gated_mla_layers: 4, identical across arms, so the changed code really isexecuted.
process_weights_after_loadingmounting points, read off the live model:Exactly the two HPC hooks are removed; the other mounting points are untouched.
Output equivalence — greedy, fixed prompts, token ids compared one by one:
hc_fn.weight * 1.01on the changed line)The positive control is what makes the 8/8 meaningful: the same test does detect a
perturbation on the very line this PR touches.
Throughput, batch 256 × 512 output tokens, 7 reps, median:
In eager run 2 the same code (
base2) moved +2.47%, so the apparent gain tracks armordering (warm-up), not the change. No measurable difference in either mode, which matches the
mechanism — a handful of extra attribute lookups per forward.
Refit-alias validity,
reload_weightson thebasearm (the claim that the iHC cleanup isnot a fix):
Confirms the alias half is genuinely cleanup.
GSM8K on the full 78-layer model — the 4-layer slice I first used has no
discriminating power (every arm, including a deliberately sabotaged one, scores 0.000 with
invalid_rate=0.985), so I pulled the fulltencent/Hy4-preview(131 shards, 1.5 TB bf16) andre-ran on 4 nodes / 16 GPUs, TP=4 x PP=4 with expert parallelism, via
distributed_executor_backend="external_launcher":fused_modulesHpcIHCPre: 156, HpcIHCPost: 156, HpcIHCHead: 1gated_mla_layers156 = 78 layers x 2 iHC boundaries per layer, so the changed path is exercised on every layer.
invalid_ratedrops from 0.985 on the slice to 0.012 here, i.e. the model is actuallyanswering and the score is meaningful. Accuracy is identical between the arms.
(0.566 is the raw-completion score without a chat template; it is used only for the A/B
comparison, not as a claim about the model's ceiling.)
Not verified
sm90 + HY3 +
HPC_ATTNrefit.HpcAttentionBackend.supports_compute_capabilityiscapability == DeviceCapability(9, 0), so the actual fix (HpcRopeNorm,hy_v3.py) cannot beexercised on sm100 at all — my cluster is GB200 only. What I verified end-to-end is the iHC
cleanup half plus the mounting-point removal. I'd appreciate the Tencent hpc-ops authors (or
anyone with H100/H800) running an sm90 refit-consistency check; the script must fail on
main, otherwise it isn't exercising the bug.Reproducing the hpc-ops build
hpc-ops ships no wheel (no GitHub release, nothing on PyPI), and building it inside
vllm/vllm-openai:nightlyneeds three environment fixes unrelated to this PR:--no-index.git, andhpc-ops/setup.py:get_version()shells out to it for a short hash.nvidia-*-cu13wheels, socusparse.his not under/usr/local/cuda/include; torch'sATen/cuda/CUDAContextLight.hfails to include it.export CPATH=<site-packages>/nvidia/cu13/include.Known limitation
Sharded-state artifacts saved by an older build contain the retired
hpc_rope_norm.qnorm_weight/knorm_weightkeys;ShardedStateLoaderindexesstate_dictby checkpoint key and raises
KeyError. Regenerate the artifact.Worth noting this swaps the direction of an existing incompatibility rather than introducing
one: on
main, loading a sharded artifact saved without HPC into an HPC-enabled buildalready fails, because those two keys stay in
state_dictand hitraise ValueError(f"Missing keys {tuple(state_dict)} in loaded state!"). I chose not to touchthe shared loader from an HPC PR — happy to add a narrow tolerance here or as a prerequisite PR
if maintainers prefer.
Follow-ups deliberately left out of this PR
HpcRopeNorm.__init__could also reject a non-float32fallback_qnorm.weight. The fp32contract now lives with the caller (
hy_v3.py), andhy_v3is the only in-tree caller, butnothing checks it for the next model that wires up
HPC_ATTN.isinstance(x, HpcModule)anymore, and the base class is reducedto
support()returningTrueplus apassforward()— both overridden by everysubclass. It is arguably dead and could be dropped.
Not a duplicate: no open PR addresses HPC QK-norm staleness across weight reload
(searched
vllm-project/vllmforHpcRopeNorm,hpc qk norm refit,process_weights_after_loading refit; the only nearby open PR, #47443, changes HPC attentiondecode split-K selection). RFC #54477 is the umbrella for selective weight reload in RL.
AI assistance was used for this change. Every changed line was reviewed by me, and I ran the
lint, unit and end-to-end runs reported above.