[ROCm][DSv4.1][Perf] Dequantize the MXFP8 weight once when dot_scaled cannot be used - #56560
Conversation
2278773 to
dc886b4
Compare
|
Please fix before merge:
|
Thanks @ChuanLi1101 for the comments. All three comments addressed in 1. The unaligned fallback. You are right that the two paths keyed off different things, and that the apply path stopped being safe on its own. The per-step dequant is back as a third branch, so an FP8 weight with an unaligned K never reaches 2. The test. 3. The header. Corrected. The header now describes what the code does. |
|
Looks good overall. One small test gap:
|
Thanks @ChuanLi1101 for the comments, the production shapes have been added in ceab01d: Dropped K=2080 at the same time: 58 passed on gfx950. |
|
Hi @JohnQinAMD, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
|
✅ @JohnQinAMD, CI is now available for this PR.
|
|
Hi @JohnQinAMD, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
eadd03e to
b08f5ae
Compare
…be used RocmDotScaledMxfp8LinearKernel needs K % 128 == 0 for tl.dot_scaled. Where that does not hold it falls back to dequantizing the weight inside apply_weights, so the same constant BF16 tensor is rebuilt on every forward. Convert it once in process_weights_after_loading instead and keep BF16, which is what EmulationMxfp8LinearKernel already does; apply_weights then dispatches on the weight's element size. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: JohnQinAMD <yanyuan.qin@amd.com>
Dequantizing the weight at load keyed the load path off K % 128 but the apply path off the weight's element size. The two agree only once process_weights_after_loading has run; a weight that is still FP8 with an unaligned K then reaches dot_scaled, whose tiling requires K % 128 == 0. Restores the per-step dequant as a third branch, so the dispatch is safe on its own again, and names the alignment the two paths have to share. The header described a fallback through the kernel selector for K % 128 != 0, which can_implement has never done -- it returns True unconditionally and the selector is never shown K. It now says what the code does. Adds the unaligned-K counterpart of test_mxfp8_linear_emulation_bf16_at_load: the load-time BF16 weight matches a per-step dequant, and an unaligned weight that is still FP8 does not reach dot_scaled. Without the restored branch the unaligned case fails. Signed-off-by: JohnQinAMD <yanyuan.qin@amd.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The regression test parametrized synthetic K=2080/2048. The shapes this actually fires on are the DeepSeek-V4.1 shared expert's `down_proj`, `Linear(moe_intermediate_size=2304 -> hidden_size=5120)`: it is row-parallel, so TP shards K to 576 at TP4 and 288 at TP8, neither a multiple of 128. Both are now covered directly. K=2080 is dropped -- it shares 288's residue mod 128 and tested nothing 288 does not -- and K=2048 stays as the aligned control that has to keep reaching `dot_scaled`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: JohnQinAMD <yanyuan.qin@amd.com>
mypy rejects `calls.append(1) or real(*a, **k)` with [func-returns-value]: `list.append` returns None, and a None-returning call may not be used as a value. The lambda is correct at runtime -- append is falsy, so `or` always evaluates the real call -- but it fails the mypy pre-commit hook, which runs for 3.10, 3.11, 3.12 and 3.13. Splitting it into a named function keeps the behaviour identical and types cleanly. Verified with the repo's own hooks on both files this PR touches: ruff-check, ruff-format and mypy for all four Python versions pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: JohnQinAMD <yanyuan.qin@amd.com>
b08f5ae to
7487778
Compare
|
/ci run |
|
✅ Triggered Buildkite CI #88951 for commit |
… cannot be used (vllm-project#56560) Signed-off-by: JohnQinAMD <yanyuan.qin@amd.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Purpose
RocmDotScaledMxfp8LinearKernelneedsK % 128 == 0fortl.dot_scaled. Where that does not hold,apply_weightsdequantizes the weight instead:layer.weightandlayer.weight_scaleare constant after loading, so that rebuilds the same tensor on every forward. This converts it once inprocess_weights_after_loadingand keeps BF16 — whatEmulationMxfp8LinearKernelalready does underVLLM_MXFP8_EMULATION_DEQUANT_AT_LOAD— and hasapply_weightsdispatch on the weight's element size.Why it is worth a patch
dequant_mxfp8_to_bf16is six pointwise kernels, and its FP32 intermediate is 4x the FP8 source and round-trips through HBM twice, so a 2.95 MB weight costs 56 MB of traffic.On DeepSeek-V4.1-Flash it fires on one linear per layer, and which one depends on TP:
moe_intermediate_sizeis 2304, and the shared expert'sdown_projis row-parallel, so its K is sharded whilegate_up's stays at 5120.K % 128TP1 and TP2 never see this; TP4 and TP8 pay it on all 40 layers — 2.24 GB of HBM traffic per decode token to produce 236 MB of weights that never change.
Test Plan
4x MI355X (gfx950), ROCm 7.2.3, TP4, on
vllm/vllm-openai-rocm:nightly-eed1f3d0c6043bd494424a22443ee198dd56f657. Server as the recipe composes it —--tokenizer-mode deepseek_v41 --tensor-parallel-size 4 --gpu-memory-utilization 0.9 --moe-backend aiter_triton_mxfp4_bf16,VLLM_ROCM_USE_AITER=1,VLLM_ROCM_USE_AITER_MOE=1— withcudagraph_mode=FULL_DECODE_ONLYpinned, and a fresh server per arm.vllm bench serve --backend vllm --model /model \ --served-model-name DeepSeek-V4.1-Flash --dataset-name random --ignore-eos \ --random-input-len 8192 --random-output-len 1024 \ --max-concurrency {1,8} --num-prompts {8,24}The board drifts between processes, so the unpatched arm was run both first and last.
Test Result
The two unpatched arms are 0.28% apart at c=1 and 0.08% at c=8, so the effect is 16-29x the drift over that span.
A decode profile confirms the mechanism rather than inferring it from the timing:
exp2_kernel_vectorized4andCUDAFunctorOnSelf_add<float>go from 2600 calls each to exactly zero, andbfloat16_copy_kernel_cudafrom 2665 to 65 (the remainder is an unrelated once-per-step collapse). Weight memory rises 0.11 GiB per rank, which is the 236 MB of BF16 predicted from the weight shapes less the 118 MB of FP8 it replaces.