Repository navigation
[Bugfix] Fuse GLM-5.3 KDA q|k|v convs into one conv1d parameter - #60991
Conversation
…ight Glm5NextLinearAttention runs one causal conv over q|k|v and cached the concatenated weight on first forward, so an RL refit that rewrote the three ColumnParallelLinear conv weights left the kernels reading the old values (vllm-project#55087). Invalidating that cache is not enough: with the default FULL_AND_PIECEWISE cudagraphs the KDA forward is captured inside the FULL decode graphs, which keep the merged weight's address, so a rebuilt tensor never reaches decode replays. Allocate the merged [3*C, 1, K] buffer up front and make the q/k/v conv weights views into it. Every write path (initial load, load_weights from a weight-transfer engine, the layerwise reload_weights, kernel-format copy_) then lands in the weight the kernels read, at an address that never changes. The buffer is a non-persistent parameter-alias buffer, which the layerwise reload already skips and restores. Co-authored-by: Jyotirmoy Roy <jyotirmoyroy649@gmail.com> Co-authored-by: Mustafa YILDIRIM <2341474+mustafayildirim@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Jared Wen <w13431838023@gmail.com>
There was a problem hiding this comment.
🟡 Changes recommended
IPC cache copy mode independently replaces the aliased tensors, reintroducing stale convolution weights after refits.
1 open finding
What changed in this PR
Fixes stale GLM-5.3 KDA convolution weights during refits by sharing storage between q/k/v parameters and the merged kernel buffer.
Changes:
- Allocates and aliases the merged convolution buffer during initialization.
- Adds unit and CUDA-graph refit regression tests.
| File | Description |
|---|---|
vllm/models/glm5next/common/kda.py |
Introduces shared q/k/v convolution storage. |
tests/models/glm5next/test_kda_conv_weight_refit.py |
Tests aliasing, reloads, and graph-visible storage stability. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| self.register_buffer( | ||
| "_merged_conv_weight", | ||
| _merge_conv_weights(self.q_conv1d, self.k_conv1d, self.v_conv1d), | ||
| persistent=False, |
|
Nice find on the FULL-graph address problem. Making the merged weight live at a fixed address is the right goal. I think the aliasing approach can still go stale, though, in two load paths where the q/k/v params stop sharing storage with
Suggestion: fuse the three convs into one param, the way Kimi-Linear's KDA ( # kda.py __init__: replaces q/k/v_conv1d, the unsqueezes, and the cached merge
self.conv1d = ColumnParallelLinear(
input_size=self.conv_size,
output_size=3 * projection_size,
bias=False,
params_dtype=torch.float32,
prefix=f"{prefix}.conv1d",
)
self.conv1d.weight.data = self.conv1d.weight.data.unsqueeze(1)
delattr(self.conv1d.weight, "weight_loader")
set_weight_attrs(
self.conv1d.weight,
{"weight_loader": _make_fused_conv1d_weight_loader(
[projection_size] * 3, self.tp_size, self.tp_rank)},
)
# _forward
conv_weights = self.conv1d.weight.view(
self.conv1d.weight.size(0), self.conv1d.weight.size(-1)
)
conv_bias = self.conv1d.bias
# model.py stacked_params_mapping
(".conv1d", ".q_conv1d", 0),
(".conv1d", ".k_conv1d", 1),
(".conv1d", ".v_conv1d", 2),
From reading the code, this should:
Trade-offs:
Your refit test should carry over, with Caveat: I haven't tested any of this. It's from reading the code, so please treat it as a suggestion, not a verified fix. This review/comment was written by an AI agent (Claude Code) on my behalf. |
Replace the q/k/v conv weights aliased into a non-persistent merged buffer with a single [3 * C_local, 1, K] conv1d parameter. The checkpoint's q_conv1d/k_conv1d/v_conv1d weights load into it as shards 0/1/2 through stacked_params_mapping and the fused conv1d weight loader already used by the Kimi and OLMo KDA/GDN layers. Parameter views did not survive every weight path. The UVA and prefetch CPU offloaders replace each parameter's .data after the layer is built, and the IPC weight cache clones every entry on its own, so the q/k/v weights stopped being views of the buffer the kernels read and cpu_offload_gb produced wrong tokens. A single parameter is the tensor all of these paths write, so loads, refits and offload reach the kernels at a fixed address and FULL cudagraph decode stays correct after a refit (vllm-project#55087). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jared Wen <w13431838023@gmail.com>
c72e86f to
92ba789
Compare
|
The fused param change looks good to me. One concern about the new e2e test: 1. A 2. Keep the e2e shape, but with dummy weights. If you'd rather keep the pruned-checkpoint e2e, I'd move it into the This review/comment was written by an AI agent (Claude Code) on my behalf. |
The pruned-checkpoint e2e test downloaded 19 GB and started four engines in the kernel-level glm5next unit step. Check the regression directly instead: a decode step through `_forward` with the kernels stubbed must hand the conv kernel `conv1d.weight` itself before and after a refit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jared Wen <w13431838023@gmail.com>
hao-aaron
left a comment
There was a problem hiding this comment.
LGTM, thanks for the fused param and the _forward guard.
For the record, the same bug pattern exists in the GLM-5.3 indexer: Indexer._wp_fp32 (vllm/models/glm5next/common/attention.py) is an fp32 copy of the weights_proj rows of the fused wk_weights_proj, built lazily on the first forward and never refreshed, so after a refit the head gate keeps using the old weights (and FULL cudagraphs hold its address). It exists because wk_weights_proj is one bf16 param but the gate is computed in fp32. A structural fix along the lines of this PR would be to unfuse it and make weights_proj its own fp32 parameter loaded directly by load_weights. Not for this PR, just noting it here so it's tracked.
This review/comment was written by an AI agent (Claude Code) on my behalf.
|
✅ @JaredforReal, CI is now available for this PR.
|
|
/ci run |
|
✅ Triggered Buildkite CI #97525 for commit ℹ️ This PR is 10 commits behind upstream |
…-project#60991) Signed-off-by: Jared Wen <w13431838023@gmail.com> Co-authored-by: Jyotirmoy Roy <jyotirmoyroy649@gmail.com> Co-authored-by: Mustafa YILDIRIM <2341474+mustafayildirim@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Christopher Mackenzie <christopher.mackenzie.software@gmail.com>
Main fused the q, k, v short convs into one [3P, 1, K] conv1d parameter (vllm-project#60991), so the MonoKernel's KDA weight packing failed at engine start with 'no attribute q_conv1d'. Its packed layout is that parameter's, so read it directly. Signed-off-by: Limpus, David <dlimpus@amd.com>
… fused conv1d vllm-project#60991 fused the q/k/v short convs into one conv1d parameter; read its [3P, 1, 4] per-rank weight as the [3P, 4] tap tensor instead of the removed q_conv1d/k_conv1d/v_conv1d. Signed-off-by: Sriram Kumar <skishore@amd.com>

Overview
Glm5NextLinearAttentioncached the merged q|k|v conv weight on the first forward, so an RL refit left the conv kernels reading stale values (#55087). This PR replaces the threeq/k/v_conv1dmodules with a singleconv1dparameter of shape[3 * C_local, 1, K], which the kernels read directly. Checkpointq_conv1d/k_conv1d/v_conv1dweights load into it as shards 0/1/2 throughstacked_params_mapping, using the_make_fused_conv1d_weight_loaderalready used by Kimi-Linear KDA and OLMo-Hybrid GDN. There is no second tensor to keep in sync, so every write path lands in the tensor the kernels read, at a fixed address.Alternative fixes for the same issue: #56298 and #57638 (see Details; their authors are co-authors on the commit). The fused-parameter design was suggested by hao-aaron in review and replaces this PR's first approach (parameter views into a merged buffer), which broke under weight offloading and
ipc_cachecopy mode.Claims
load_weights/reload_weights(checkpoint or kernel format), GLM-5.3 KDA decode uses the refitted conv weights, in eager mode and with the defaultFULL_AND_PIECEWISEcudagraphs.--cpu-offload-gb(with and without refit) and with--load-format ipc_cachein bothcopyandzero_copymodes.[q_r | k_r | v_r], the same as the oldtorch.cat. Direct-load outputs are identical tomain. One copy of the conv weights is dropped.Validation
Hardware: GB300 (SM100), TP1, 4-layer pruned GLM-5.3-Flash at full width (
JaredforReal/GLM-5.3-Flash-4L).conv1dsub-module): checkpoint q/k/v shards (2D and 3D) land in q|k|v order;load_weights-style refit and a realinitialize_layerwise_reload/finalize_layerwise_reloadcycle update the weight without moving it. 22 passed withtest_kda_recurrent.py.test_refit_reaches_cudagraph_decode[cpu_offload_gb=0|10](pruned model, default cudagraphs, one engine per case): direct load -> reference tokens;reload_weightsof the checkpoint with the conv weights x2 -> output changes; checkpoint as-is -> == reference; kernel-formatreload_weights(is_checkpoint_format=False)onconv1d.weightx2 / x0.5 -> changes / == reference; the conv weight address is identical after all four refits. 2 passed (~5 min each, mostly startup). Onmainthe first refit step fails ([Bug]: GLM-5.3 KDA keeps stale merged convolution weights after refit #55087).pre-commit runon the changed files: passed (ruff, mypy, SPDX, test tethering).CI note: the e2e test is the first in
tests/models/glm5nextto use the pruned checkpoint (19 GB download, needs SM90/SM100, skipped elsewhere). It runs in the existingglm5next-unit-testsstep; the two offload cases take about 10 min together on GB300, mostly engine startup. #57537 adds a dedicated step for pruned-checkpoint e2e tests that this test could move to once it lands.Full refit flow, 3 prompts x 24 greedy tokens, compared with
maindirect load:reload_weights()load_weightspartial update / kernel-format refitcpu_offload_gb=10cpu_offload_gb=10After the reload, every language-model parameter and buffer checksum equals a fresh load.
--load-format ipc_cache(avllm preloaddaemon per GPU, default cudagraphs). After each refit the check compares the conv weight the kernel reads against the checkpoint × scale (max abs diff), then compares the output:Checkpoint-format refits are not supported on an
ipc_cache-loaded model onmaineither, so they're not compared here._apply_entriesre-registers every parameter as a plainnn.Parameter(399/399 loseweight_loader), soload_weightsof any stacked weight raisesAttributeError(verified onmainwithself_attn.q_proj), and the layerwisereload_weightsfails withCannot copy out of meta tensor. Onmainthe three conv weights happened to fall through todefault_weight_loader; with this PR they're stacked like the other projections. Making checkpoint-format refit work afteripc_cacheis a separate issue.Details
Root cause. The KDA layer runs one causal conv over the concatenated q|k|v channels. The merged weight was built lazily on the first forward with
torch.catand cached, so later writes toq/k/v_conv1d.weight(RL refit through the weight-transfer engines,reload_weights, or kernel-formatcopy_) never reached it.Why not invalidate the cache (#56298, #57638). Both PRs drop the cached tensor on refit and rebuild it on the next forward. That works with
enforce_eager=True, but with the defaultFULL_AND_PIECEWISEcudagraphs the KDA forward is captured inside the FULL decode graphs, which keep the old tensor's address, so decode replays never see the rebuilt weight.Why one parameter instead of views into a merged buffer. Views only hold while nothing replaces a parameter's storage. The UVA and prefetch CPU offloaders set each parameter's
.dataright after the layer is built, and theipc_cacheloader in copy mode clones every entry separately; either way the three weights stop aliasing the tensor the kernels read. With a single parameter, those paths, the layerwise reload, and kernel-formatcopy_all write the tensor the kernels use. This matches how Kimi-Linear and OLMo-Hybrid already store their fused short convs.Trade-offs.
q/k/v_conv1d.weighttoconv1d.weight. Checkpoint-format updates keep the HF names and go through the stacked mapping. Kernel-format writers need the fused name, and GLM-5.3 artifacts saved with vLLM parameter names (sharded_state, vLLM-serialized tensorizer) before this change will not load.q_conv1d/_merged_conv_weightand will need a small rebase.Not covered here. The TP2 cudagraph reload drift from the FlashInfer TRT-LLM FP8 MoE
gemm1_clamp_limittensor and the DSA indexer's lazily cached_wp_fp32are separate issues, left for follow-ups.AI assistance (Claude Code) was used for the investigation and the draft of the change; every line was reviewed and the runs above were executed by the submitter.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
🤖 Generated with Claude Code