Repository navigation
[ROCm][Bugfix] Stop declaring the MXFP4 MoE intermediate padding to AITER - #61816
Open
PerryZhang01 wants to merge 1 commit into
Open
PerryZhang01 wants to merge 1 commit into
PerryZhang01 wants to merge 1 commit into
Conversation
PerryZhang01
requested review from
AndreasKaratzas,
DarkLight1337,
tjtanaa and
ywang96
as code owners
October 15, 2026 11:02
PerryZhang01
force-pushed
the
fix/aiter-mxfp4-moe-skip-intermediate-pad
branch
3 times, most recently
from
October 15, 2026 12:50
d2e9729 to
e720183
Compare
…ITER AITER returns NaN from the a8w4 MoE (FP8 activations x MXFP4 weights) at decode-sized batches when a non-zero intermediate_pad is declared. Several scratch buffers on that path are allocated with torch.empty, sized for the padded sorted length, and read where the producing kernel never wrote; in FP8/E8M0 a garbage 0xFF byte is NaN, so a whole token row comes back NaN. Details and a standalone repro: ROCm/aiter#6565 The declaration is redundant for MXFP4 anyway. create_weights allocates w13/w2 and their scales with torch.zeros at the padded intermediate size and the checkpoint fills only the real rows, so the padding trims itself: stage1's padded N rows compute x @ 0 = 0, and stage2's padded K columns contribute anything @ 0 = 0. hidden_pad is deliberately left untouched: the activation columns it covers are not guaranteed to be zero, so dropping it would not be equivalent. This is a workaround; revert it once the AITER fix (ROCm/aiter#6568) is merged and pinned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: perzhang <perzhang@amd.com>
PerryZhang01
force-pushed
the
fix/aiter-mxfp4-moe-skip-intermediate-pad
branch
from
October 15, 2026 12:53
e720183 to
2da58ad
Compare
5 of 6 tasks
Contributor
|
Thanks for the fix! Meet the same problem when testing CI: buildkite #95188. |
This branch has not been deployed
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
The AMD CI job
dsv41-mono-decode-numerics-amdgoes red intermittently with(e.g. https://buildkite.com/vllm/ci/builds/94971). The NaN comes from AITER, on
vLLM's ordinary MoE path, not from the mono kernels the test compares against.
AITER's a8w4 MoE (FP8 activations x MXFP4 weights) allocates several scratch
buffers with
torch.empty, sized for the padded sorted length, and reads themwhere the producing kernel never wrote. In FP8/E8M0 a garbage
0xFFbyte isNaN, so a whole token row of the output comes back NaN. It only happens with a
non-zero
intermediate_pad, at decode-sized batches, after larger calls haverun in the same process — which is why it shows up as a flaky decode failure.
Root cause, a standalone repro and a fix are filed upstream:
This change
Stop declaring
intermediate_padto AITER on the MXFP4 path. It is redundantthere, and dropping it keeps the call off the buggy AITER path.
Why it is redundant — the padded region is zero at the point AITER sees it, and
stays zero through every step between allocation and the kernel call:
torch.zerosforw13/w2, their scales andw13_bias/w2_bias, at the padded intermediate sizeMxfp4MoEMethod.create_weightsRoutedExperts._narrow_expert_data_for_padding, used by_load_w13/_load_w2right beforeexpert_data.copy_()view+permute(assertsN % 16 == 0,K % 32 == 0, no re-padding), so zeros stay zerosaiter.ops.shuffle.shuffle_weight0x7F(E8M0 = 1.0) before copying, so it is never garbageaiter.utility.fp4_utils.e8m0_shuffleSo for the padded columns the kernel computes
x @ 0 = 0(gate and up are both zero there, and the bias is zero,so the activation output is exactly
silu(0) * 0 = 0)0 @ 0 = 0Exact zeros also mean the padded columns cannot disturb the stage-2 activation
quantization:
amaxover an MX group is unaffected by zeros, whether or not agroup straddles real and padded columns.
Two things are deliberately left alone:
hidden_pad— the activation columns it covers are not guaranteed to bezero, so dropping it would not be equivalent.
activation == MoEActivation.SITU, the a16w4 FlyDSL port used byKimi K3) — that kernel consumes the pad. The surrounding code already skips
the rounding for it ("SITU's A16W4 FlyDSL kernel pads per gate/up half; pass
through unrounded"), and on that path AITER keys its own scratch
initialization on the value:
_alloc = torch.zeros if inter_dim_pad > 0 else torch.emptyinaiter/ops/flydsl/moe_kernels.py. Zeroing the declarationthere would remove an initialization that is currently happening, which is
the opposite of what this PR is for.
This is a workaround and should be reverted once the AITER fix is merged and
pinned. The surrounding code already carries a similar TODO about AITER padding
behaviour.
Test Result
MI355X (gfx950), ROCm 7.2.3, stock AITER 0.1.25 — i.e. without the
AITER-side fix — running
tests/models/test_dsv41_mono_layers.pyunmodified,each run covering TP2 and TP4:
14 runs, 28 cells, no NaN. Before the change the same test failed intermittently
— locally about 2 runs in 6 — so 14 clean runs is about 0.3% likely if the
change did nothing.
Overhead: none measurable. Timing the same shape with and without the
declaration (median over 6 rounds, per
fused_moe_2stagescall):All within run-to-run noise (~4%).
Scope and limits
model_dim=5120, per-rankintermediate 576 padded to 640, E=384, topk=6). Other MXFP4 models on this
path (e.g. gpt-oss with INTERLEAVE) are covered by the same zero-fill
argument but were not run here, so reviewers may prefer a narrower gate.
the table. It is not confirmed by a run against a real MXFP4 checkpoint that
actually has padding: the test that exercises this path uses a random-weight
loader which also zero-fills the padding, and every MXFP4 checkpoint we have
locally has
moe_intermediate_size = 2048, which is already aligned per rank(pad = 0). DeepSeek-V4.1-Flash (2304) is the one that pads, and the local copy
of it is FP8 rather than MXFP4.
intermediate_pad == 0and are unaffected either way.VLLM_ROCM_USE_AITER_MOE_A4W4_DSV4opt-in is a sub-case ofuse_mxfp4_w4a16, so it is covered by this change as well. The samezero-fill argument applies to it, but it runs different kernels and was not
exercised here.