[ROCm][Bugfix] Fix AITER preshuffled FP8 block-scale kernel (#56433)
Signed-off-by: Matthew Wong <Matthew.Wong2@amd.com>
Matt committed
Sep 11, 2026 at 04:44 UTC
b4da4d17ae0c1fb44f7358585f04fdba7e4e1c91
5 files changed
+120
-36
vllm/model_executor/kernels/linear/scaled_mm/aiter.py
+49
-7
@@ -2,6 +2,8 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5
+from typing import ClassVar
6
+
7
import torch
8
9
from vllm import _custom_ops as ops
@@ -462,6 +464,12 @@ class AiterFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
464
class AiterPreshuffledFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
465
"""Aiter FP8 block-scaled GEMM using a pre-shuffled (bpreshuffle) weight."""
466
467
+ # gemm_a8w8_blockscale_bpreshuffle reads the activation scale column-major.
468
+ wants_transposed_act_scale: ClassVar[bool] = True
469
+
470
+ # process_weights_after_loading shuffles layer.weight to layout (16, 16).
471
+ preshuffles_weight: ClassVar[bool] = True
472
+
473
@classmethod
474
def is_supported(
475
cls, compute_capability: int | None = None
@@ -519,9 +527,25 @@ class AiterPreshuffledFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
527
528
return True, None
529
530
+ @staticmethod
531
+ def _reads_weight_directly(layer: torch.nn.Module) -> bool:
532
+ """True when something other than apply_weights consumes layer.weight.
533
+
534
+ Such a weight must stay in the plain layout: ``is_bmm`` marks a stack
535
+ of matrices (wo_a), ``skip_weight_relayout`` marks MLA's kv_b_proj.
536
+ Both are stamped after construction, so can_implement cannot see them.
537
+ """
538
+ return bool(
539
+ getattr(layer, "is_bmm", False)
540
+ or getattr(layer, "skip_weight_relayout", False)
541
+ )
542
+
543
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
544
super().process_weights_after_loading(layer)
545
546
+ if self._reads_weight_directly(layer):
547
+ return
548
+
549
params = FP8BlockParams.from_layer(layer)
550
if params.weight_scale_inv is not None:
551
ws, attr = params.weight_scale_inv, params.WEIGHT_SCALE_INV
@@ -565,11 +589,25 @@ class AiterPreshuffledFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
589
else params.weight_scale_inv
590
)
591
592
+ # Left unshuffled above; kv_b_proj still reaches here from MLA's
593
+ # prefill-context path.
594
+ plain = self._reads_weight_directly(layer)
595
+
596
x_2d = x.view(-1, x.shape[-1])
569
- A, As = rocm_aiter_ops.group_fp8_quant(x_2d, transpose_scale=True)
570
- output = rocm_aiter_ops.gemm_a8w8_blockscale_bpreshuffle(
571
- A, params.weight, As, Bs, output_dtype=self.config.out_dtype
572
- )
597
+ A, As = rocm_aiter_ops.group_fp8_quant(x_2d, transpose_scale=not plain)
598
+ if plain:
599
+ output = rocm_aiter_ops.gemm_a8w8_blockscale(
600
+ A,
601
+ params.weight,
602
+ As,
603
+ Bs,
604
+ list(self.weight_group_shape),
605
+ output_dtype=self.config.out_dtype,
606
+ )
607
+ else:
608
+ output = rocm_aiter_ops.gemm_a8w8_blockscale_bpreshuffle(
609
+ A, params.weight, As, Bs, output_dtype=self.config.out_dtype
610
+ )
611
if bias is not None:
612
output = output + bias
613
return output.view(*x.shape[:-1], params.weight.shape[0])
@@ -581,7 +619,11 @@ class AiterPreshuffledFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
619
As: torch.Tensor,
620
Bs: torch.Tensor,
621
) -> torch.Tensor:
584
- raise NotImplementedError(
585
- "AiterPreshuffledFp8BlockScaledMMKernel overrides apply_weights and "
586
- "does not use apply_block_scaled_mm."
622
+ """Block-scaled GEMM for callers that pre-quantize their activations.
623
+
624
+ ``As`` must be column-major; a row-major one will not raise, it just
625
+ returns wrong numbers for M > 1.
626
+ """
627
+ return rocm_aiter_ops.gemm_a8w8_blockscale_bpreshuffle(
628
+ A, B, As, Bs, output_dtype=self.config.out_dtype
629
)
vllm/model_executor/layers/attention/mla_attention.py
+3
-4
@@ -564,10 +564,9 @@ class MLAAttention(nn.Module, AttentionLayerBase):
564
)
565
self.q_pad_num_heads = getattr(self.impl, "q_pad_num_heads", None)
566
self.is_amx_bmm_enabled = getattr(self.impl, "uses_amx_bmm", False)
567
- # AMX reads kv_b_proj's weight directly and never calls it live; the
568
- # reference CPU MLA backend calls it but isn't perf-critical. Skip
569
- # the packed-kernel dispatch either way.
570
- kv_b_proj._cpu_skip_gemm_dispatch = True
567
+ # MLA reads this weight directly to build W_UK/W_UV, so a backend must
568
+ # not relayout it at load time.
569
+ kv_b_proj.skip_weight_relayout = True
570
self.use_direct_call = not current_platform.opaque_attention_op()
571
572
vllm_config = get_current_vllm_config()
vllm/model_executor/layers/linear.py
+3
-3
@@ -204,9 +204,9 @@ class UnquantizedLinearMethod(LinearMethodBase):
204
205
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
206
if current_platform.is_cpu():
207
- # MLA's kv_b_proj (see `_cpu_skip_gemm_dispatch`): not
208
- # perf-critical, so skip packing and use a plain fallback.
209
- if getattr(layer, "_cpu_skip_gemm_dispatch", False):
207
+ # MLA's kv_b_proj (see `skip_weight_relayout`): not perf-critical,
208
+ # so skip packing and use a plain fallback.
209
+ if getattr(layer, "skip_weight_relayout", False):
210
layer.cpu_linear = torch.nn.functional.linear
211
return
212
vllm/models/deepseek_v4/amd/model.py
+11
-6
@@ -71,7 +71,10 @@ from vllm.model_executor.models.utils import (
71
make_layers,
72
maybe_prefix,
73
)
74
-from vllm.models.deepseek_v4.amd.rocm import DeepseekV4ROCMAiterMLAAttention
74
+from vllm.models.deepseek_v4.amd.rocm import (
75
+ DeepseekV4ROCMAiterMLAAttention,
76
+ weight_already_preshuffled,
77
+)
78
from vllm.platforms import current_platform
79
from vllm.platforms.rocm import on_gfx950
80
from vllm.sequence import IntermediateTensors
@@ -147,11 +150,13 @@ class DeepseekV4MLP(nn.Module):
150
return
151
if ws.dtype == torch.float8_e8m0fnu:
152
ws = _upcast_e8m0_to_fp32(ws).contiguous()
150
- replace_parameter(
151
- self.gate_up_proj,
152
- "weight",
153
- rocm_aiter_ops.shuffle_weight(w.data, layout=(16, 16)),
154
- )
153
+ # Skip if the linear's kernel already shuffled it.
154
+ if not weight_already_preshuffled(self.gate_up_proj):
155
+ replace_parameter(
156
+ self.gate_up_proj,
157
+ "weight",
158
+ rocm_aiter_ops.shuffle_weight(w.data, layout=(16, 16)),
159
+ )
160
self._gateup_scale = ws
161
162
def forward(self, x):
vllm/models/deepseek_v4/amd/rocm.py
+54
-16
@@ -61,6 +61,16 @@ def _build_indptr_from_lengths(lengths: torch.Tensor) -> torch.Tensor:
61
return indptr
62
63
64
+def weight_already_preshuffled(linear: torch.nn.Module) -> bool:
65
+ """True when the linear's kernel already B-preshuffled ``weight``.
66
+
67
+ The hand-shuffles below (fused_wqa_wkv, wo_b, gate_up_proj) must be skipped
68
+ for those, since shuffle_weight is a permutation rather than an involution.
69
+ """
70
+ kernel = getattr(getattr(linear, "quant_method", None), "fp8_linear", None)
71
+ return bool(getattr(kernel, "preshuffles_weight", False))
72
+
73
+
74
def apply_pre_quantized_block_scaled_mm(
75
linear: torch.nn.Module,
76
x_fp8: torch.Tensor,
@@ -551,12 +561,13 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
561
return None
562
if ws.dtype == torch.float8_e8m0fnu:
563
ws = _upcast_e8m0_to_fp32(ws).contiguous()
554
- # Shuffle the weight in place (single weight, no unshuffled copy).
555
- replace_parameter(
556
- linear,
557
- "weight",
558
- rocm_aiter_ops.shuffle_weight(w.data, layout=(16, 16)),
559
- )
564
+ # Skip if the linear's kernel already shuffled it.
565
+ if not weight_already_preshuffled(linear):
566
+ replace_parameter(
567
+ linear,
568
+ "weight",
569
+ rocm_aiter_ops.shuffle_weight(w.data, layout=(16, 16)),
570
+ )
571
return ws
572
573
self._wqa_wkv_scale = _prep(self.fused_wqa_wkv)
@@ -654,16 +665,26 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
665
return qr_kv, kv_score, indexer_kv_score, indexer_weights
666
667
@functools.cached_property
657
- def _wq_b_uses_aiter_block_scaled(self) -> bool:
658
- """True when both wq_b GEMMs run the aiter block-scaled fp8 kernel.
668
+ def _wq_b_act_scale_transpose(self) -> bool | None:
669
+ """Activation-scale byte order every wq_b consumer agrees on, else None.
670
+
671
+ None means "do not take the fused norm+quant path". Otherwise the value
672
+ is the ``transpose_scale`` the producer must pass so that the scale it
673
+ writes matches what the selected GEMM reads.
674
675
Cached: the linear kernels and the aiter env gates are fixed once
676
the model is built, so this is evaluated at the first forward
677
only.
678
664
- The fused norm+quant path is only valid if the quant and GEMM it
665
- replaces are exactly the aiter ones; otherwise fall back to the
666
- shared path.
679
+ Two conditions, both necessary:
680
+
681
+ * The fused norm+quant path is only valid if the quant and GEMM it
682
+ replaces are exactly the aiter ones; otherwise fall back to the
683
+ shared path.
684
+ * One qr_scale feeds both self.wq_b and self.indexer.wq_b, which have
685
+ different (N, K) and so can resolve to kernels wanting opposite byte
686
+ orders. A single producer cannot serve both, so refuse the fast path
687
+ when they disagree rather than guessing.
688
"""
689
from vllm._aiter_ops import rocm_aiter_ops
690
from vllm.model_executor.kernels.linear.scaled_mm import (
@@ -671,16 +692,31 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
692
)
693
694
if not rocm_aiter_ops.is_linear_fp8_enabled():
674
- return False
695
+ return None
696
697
linears = [self.wq_b]
698
if self.indexer is not None:
699
linears.append(self.indexer.wq_b)
700
+ layouts: set[bool] = set()
701
for linear in linears:
702
kernel = getattr(getattr(linear, "quant_method", None), "fp8_linear", None)
703
if not isinstance(kernel, Fp8BlockScaledMMLinearKernel):
682
- return False
683
- return True
704
+ return None
705
+ layouts.add(bool(getattr(kernel, "wants_transposed_act_scale", False)))
706
+ if len(layouts) != 1:
707
+ logger.warning_once(
708
+ "DeepSeek-V4 wq_b consumers disagree on activation-scale layout; "
709
+ "disabling the fused q/kv norm+quant path.",
710
+ scope="global",
711
+ )
712
+ return None
713
+ transpose_scale = layouts.pop()
714
+ logger.debug_once(
715
+ "DeepSeek-V4 wq_b: emitting %s-major activation scales",
716
+ "column" if transpose_scale else "row",
717
+ scope="global",
718
+ )
719
+ return transpose_scale
720
721
def _split_qkv_and_norm(
722
self, qr_kv: torch.Tensor
@@ -697,11 +733,12 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
733
when the aiter linear path is not active.
734
"""
735
qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1)
736
+ transpose_scale = self._wq_b_act_scale_transpose
737
if not (
738
qr.dim() == 2
739
and qr.shape[0] > 0
740
and self.q_lora_rank % 128 == 0
704
- and self._wq_b_uses_aiter_block_scaled
741
+ and transpose_scale is not None
742
):
743
return super()._split_qkv_and_norm(qr_kv)
744
@@ -715,7 +752,8 @@ class DeepseekV4ROCMAiterMLAAttention(DeepseekV4Attention):
752
kv_weight=self.kv_norm.weight.data,
753
kv_epsilon=self.eps,
754
group_size=128,
718
- transpose_scale=False,
755
+ # Emit the byte order the wq_b GEMMs read.
756
+ transpose_scale=transpose_scale,
757
)
758
759
def _o_proj(self, o: torch.Tensor, positions: torch.Tensor) -> torch.Tensor: