[Refactor][ROCm] Migrate the RDNA3 W4A16 MoE to the oracle/experts pa… (#55522)
Signed-off-by: JartX <sagformas@epdcenter.es>
JartX committed
Sep 12, 2026 at 15:20 UTC
986e2f870abc8f87a1830af7d335961b3e4234b2
10 files changed
+453
-377
tests/kernels/quantization/test_rdna3_compile_guards.py
+68
-59
@@ -34,6 +34,13 @@ from vllm.platforms import current_platform
34
if not current_platform.is_rocm():
35
pytest.skip("RDNA3 compile-guard tests are ROCm-only", allow_module_level=True)
36
37
+from vllm.model_executor.layers.quantization.utils.quant_utils import ( # noqa: E402
38
+ kInt4Static,
39
+ kInt4Static32,
40
+ kInt4Static32Asym,
41
+ kInt4StaticAsym,
42
+ kInt8Static,
43
+)
44
from vllm.platforms.rocm import on_gfx1100 # noqa: E402
45
46
gfx1100_only = pytest.mark.skipif(
@@ -166,15 +173,13 @@ def test_op_absent_on_non_gfx1100(op_name):
173
174
@not_gfx1100
175
def test_rocm_moe_not_supported_on_non_gfx1100():
169
- """rocm_moe_rdna.is_supported() must return False on non-gfx1100 hardware."""
170
- from vllm.model_executor.layers.quantization.compressed_tensors.compressed_tensors_moe import ( # noqa: E501
171
- rocm_moe_rdna,
176
+ """The RDNA3 MoE experts must not be selectable on non-gfx1100 hardware."""
177
+ from vllm.model_executor.layers.fused_moe.experts.rdna3_moe import (
178
+ Rdna3WNA16Experts,
179
)
180
174
- wq = type("WQ", (), {"num_bits": 4})()
175
- assert rocm_moe_rdna.is_supported(wq) is False, (
176
- "rocm_moe_rdna.is_supported() returned True on non-gfx1100 — "
177
- "dispatch guard is broken"
181
+ assert Rdna3WNA16Experts._supports_current_device() is False, (
182
+ "Rdna3WNA16Experts reported support on non-gfx1100 — dispatch guard is broken"
183
)
184
185
@@ -359,54 +364,58 @@ class TestCustomOpsGuards:
364
# ============================================================================
365
366
362
-class _FakeWeightQuant:
363
- """Minimal stand-in for a weight quantization config."""
364
-
365
- def __init__(self, num_bits):
366
- self.num_bits = num_bits
367
-
368
-
367
class TestMoEDispatchMocked:
368
"""Mock on_gfx1100() to False and verify RDNA3 MoE is unreachable."""
369
372
- def test_is_supported_false_when_mocked_cdna(self):
373
- """rocm_moe_rdna.is_supported() must return False when not on gfx1100."""
374
- from vllm.model_executor.layers.quantization.compressed_tensors.compressed_tensors_moe import ( # noqa: E501
375
- rocm_moe_rdna,
370
+ def test_kernel_unavailable_when_mocked_cdna(self):
371
+ """The device gate must reject when not on gfx1100."""
372
+ from vllm.model_executor.layers.fused_moe.experts.rdna3_moe import (
373
+ rdna3_moe_kernel_available,
374
)
375
376
with patch("vllm.platforms.rocm.on_gfx1100", return_value=False):
379
- assert rocm_moe_rdna.is_supported(_FakeWeightQuant(num_bits=4)) is False
377
+ assert rdna3_moe_kernel_available() is False
378
381
- @pytest.mark.parametrize("num_bits", [2, 3, 8, 16])
382
- def test_is_supported_rejects_non_w4(self, num_bits):
383
- """is_supported() rejects non-4-bit even before checking arch."""
384
- from vllm.model_executor.layers.quantization.compressed_tensors.compressed_tensors_moe import ( # noqa: E501
385
- rocm_moe_rdna,
379
+ @pytest.mark.parametrize(
380
+ "weight_key",
381
+ [kInt8Static, kInt4StaticAsym, kInt4Static32Asym, None],
382
+ )
383
+ def test_quant_scheme_rejects_non_symmetric_int4(self, weight_key):
384
+ """Only symmetric int4 weight-only schemes reach the kernel."""
385
+ from vllm.model_executor.layers.fused_moe.experts.rdna3_moe import (
386
+ Rdna3WNA16Experts,
387
)
388
388
- assert rocm_moe_rdna.is_supported(_FakeWeightQuant(num_bits=num_bits)) is False
389
+ assert Rdna3WNA16Experts._supports_quant_scheme(weight_key, None) is False
390
390
- def test_is_supported_false_when_op_missing(self):
391
- """is_supported() returns False when the C++ op doesn't exist."""
392
- from vllm.model_executor.layers.quantization.compressed_tensors.compressed_tensors_moe import ( # noqa: E501
393
- rocm_moe_rdna,
391
+ @pytest.mark.parametrize("weight_key", [kInt4Static, kInt4Static32])
392
+ def test_quant_scheme_accepts_symmetric_int4(self, weight_key):
393
+ from vllm.model_executor.layers.fused_moe.experts.rdna3_moe import (
394
+ Rdna3WNA16Experts,
395
+ )
396
+
397
+ assert Rdna3WNA16Experts._supports_quant_scheme(weight_key, None) is True
398
+
399
+ def test_kernel_unavailable_when_op_missing(self):
400
+ """The device gate returns False when the C++ op doesn't exist."""
401
+ from vllm.model_executor.layers.fused_moe.experts.rdna3_moe import (
402
+ rdna3_moe_kernel_available,
403
)
404
405
fake_rocm_c = type("FakeRocmC", (), {"gptq_gemm_rdna3": None})()
406
with patch.object(torch, "ops", create=True) as mock_ops:
407
mock_ops._rocm_C = fake_rocm_c
399
- assert rocm_moe_rdna.is_supported(_FakeWeightQuant(num_bits=4)) is False
408
+ assert rdna3_moe_kernel_available() is False
409
401
- def test_is_supported_false_when_rocm_c_absent(self):
402
- """is_supported() returns False when _rocm_C doesn't exist at all."""
403
- from vllm.model_executor.layers.quantization.compressed_tensors.compressed_tensors_moe import ( # noqa: E501
404
- rocm_moe_rdna,
410
+ def test_kernel_unavailable_when_rocm_c_absent(self):
411
+ """The device gate returns False when _rocm_C doesn't exist at all."""
412
+ from vllm.model_executor.layers.fused_moe.experts.rdna3_moe import (
413
+ rdna3_moe_kernel_available,
414
)
415
416
fake_ops = type("FakeOps", (), {})()
417
with patch.object(torch, "ops", fake_ops):
409
- assert rocm_moe_rdna.is_supported(_FakeWeightQuant(num_bits=4)) is False
418
+ assert rdna3_moe_kernel_available() is False
419
420
421
class TestDenseKernelSelectionMocked:
@@ -471,30 +480,30 @@ class TestDenseKernelSelectionMocked:
480
)
481
482
474
-class TestCompressedTensorsMoEDispatchGuard:
475
- """Verify compressed_tensors_moe.py only enters rocm_moe_rdna under is_rocm()."""
483
+class TestWNA16OracleWiring:
484
+ """The RDNA3 backend must reach the kernel only through the oracle."""
485
477
- def test_rocm_guard_in_dispatch_source(self):
478
- """The rocm_moe_rdna import and call must be inside an is_rocm() check."""
479
- src = _read_pkg_source_or_skip(
480
- "model_executor",
481
- "layers",
482
- "quantization",
483
- "compressed_tensors",
484
- "compressed_tensors_moe",
485
- "compressed_tensors_moe.py",
486
+ def test_backend_maps_to_rdna3_experts(self):
487
+ from vllm.model_executor.layers.fused_moe.experts.rdna3_moe import (
488
+ Rdna3WNA16Experts,
489
+ )
490
+ from vllm.model_executor.layers.fused_moe.oracle.int_wna16 import (
491
+ WNA16MoEBackend,
492
+ backend_to_kernel_cls,
493
+ map_wna16_backend,
494
)
487
- lines = src.splitlines()
495
489
- for i, line in enumerate(lines, 1):
490
- stripped = line.strip()
491
- if "rocm_moe" in stripped and not stripped.startswith("#"):
492
- found_guard = False
493
- for j in range(i - 1, max(0, i - 15), -1):
494
- if "is_rocm()" in lines[j - 1]:
495
- found_guard = True
496
- break
497
- assert found_guard, (
498
- f"L{i}: rocm_moe_rdna reference not protected by "
499
- f"is_rocm() guard: {stripped}"
500
- )
496
+ assert backend_to_kernel_cls(WNA16MoEBackend.RDNA3) == [Rdna3WNA16Experts]
497
+ assert map_wna16_backend("rdna3") == WNA16MoEBackend.RDNA3
498
+
499
+ def test_backend_is_offered_before_the_triton_fallback(self):
500
+ """Priority order: the native kernel outranks Triton when supported."""
501
+ from vllm.model_executor.layers.fused_moe.oracle.int_wna16 import (
502
+ WNA16MoEBackend,
503
+ _get_priority_backends,
504
+ )
505
+
506
+ backends = _get_priority_backends()
507
+ assert backends.index(WNA16MoEBackend.RDNA3) < backends.index(
508
+ WNA16MoEBackend.TRITON
509
+ )
tests/quantization/test_moe_wna16.py
+34
@@ -93,6 +93,40 @@ def test_moe_wna16_accepts_channelwise_gptq_activation_order():
93
False,
94
"MoeWNA16 checkpoint layout",
95
),
96
+ (
97
+ WNA16MoEBackend.RDNA3,
98
+ AutoGPTQConfig(4, 128, False, True, False, {}, {}),
99
+ False,
100
+ False,
101
+ "compressed-tensors",
102
+ ),
103
+ (
104
+ WNA16MoEBackend.RDNA3,
105
+ QuantizationArgs(
106
+ num_bits=4,
107
+ type=QuantizationType.INT,
108
+ strategy=QuantizationStrategy.GROUP,
109
+ symmetric=False,
110
+ dynamic=False,
111
+ group_size=128,
112
+ ),
113
+ True,
114
+ False,
115
+ "asymmetric",
116
+ ),
117
+ (
118
+ WNA16MoEBackend.RDNA3,
119
+ QuantizationArgs(
120
+ num_bits=4,
121
+ type=QuantizationType.INT,
122
+ strategy=QuantizationStrategy.CHANNEL,
123
+ symmetric=True,
124
+ dynamic=False,
125
+ ),
126
+ False,
127
+ False,
128
+ "group-wise scales",
129
+ ),
130
],
131
)
132
def test_wna16_oracle_rejects_incompatible_quant_structures(
vllm/config/kernel.py
+2
@@ -141,6 +141,7 @@ MoEBackend = Literal[
141
"flydsl",
142
"hpc",
143
"emulation",
144
+ "rdna3",
145
]
146
147
# Backends that run the mega-MoE model path through the flashinfer moe_ep
@@ -262,6 +263,7 @@ class KernelConfig:
263
- "aiter_triton_mxfp4_bf16": Use the AITER Triton MXFP4 W4A16
264
(moe_gemm_a16w4) MoE kernel (ROCm gfx942/gfx950/gfx1250)
265
- "flydsl": Use AMD FlyDSL kernels (ROCm only)
266
+ - "rdna3": Use the fused RDNA3 W4A16 HIP kernel (ROCm gfx1100 only)
267
- "hpc": Use HPC kernels (FP8 and Hopper only)
268
- "emulation": use BF16/FP16 GEMM, dequantizing weights and
269
running QDQ on activations.
vllm/model_executor/layers/fused_moe/experts/rdna3_moe.py
new
+235
@@ -0,0 +1,235 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Fused MoE W4A16 experts on the RDNA3 (gfx1100) HIP kernel.
4
+
5
+``moe_gptq_gemm_rdna3`` is a single HIP kernel launch per GEMM that handles
6
+expert routing + W4A16 dequant + dot product with atomic output accumulation.
7
+
8
+Weight format (per expert, same as the dense RDNA3 W4A16 kernel):
9
+ - Packed int32 ``[E, K/8, N]`` with exllama shuffle
10
+ - Scales ``[E, groups, N]`` in activation dtype
11
+ - Zero points ``[E, groups, N/8]`` packed int32 (synthesized, symmetric only)
12
+"""
13
+
14
+import torch
15
+
16
+import vllm._custom_ops as ops
17
+import vllm.model_executor.layers.fused_moe.modular_kernel as mk
18
+from vllm.model_executor.layers.fused_moe.activation import (
19
+ MoEActivation,
20
+ apply_moe_activation_supported,
21
+)
22
+from vllm.model_executor.layers.fused_moe.config import (
23
+ FusedMoEConfig,
24
+ FusedMoEParallelConfig,
25
+ FusedMoEQuantConfig,
26
+)
27
+from vllm.model_executor.layers.fused_moe.moe_align_block_size import (
28
+ moe_align_block_size,
29
+)
30
+from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
31
+ TopKWeightAndReduceNoOP,
32
+)
33
+from vllm.model_executor.layers.quantization.utils.quant_utils import (
34
+ QuantKey,
35
+ kInt4Static,
36
+ kInt4Static32,
37
+)
38
+from vllm.platforms import current_platform
39
+
40
+
41
+def rdna3_moe_kernel_available() -> bool:
42
+ """Whether the fused RDNA3 MoE HIP kernel is built into this binary."""
43
+ if not current_platform.is_rocm():
44
+ return False
45
+
46
+ from vllm.platforms.rocm import on_gfx1100
47
+
48
+ return (
49
+ on_gfx1100()
50
+ and hasattr(torch.ops, "_rocm_C")
51
+ and hasattr(torch.ops._rocm_C, "moe_gptq_gemm_rdna3")
52
+ )
53
+
54
+
55
+class Rdna3WNA16Experts(mk.FusedMoEExpertsModular):
56
+ """W4A16 experts backed by ``moe_gptq_gemm_rdna3`` (gfx1100).
57
+
58
+ Both GEMMs accumulate atomically, so their destinations are zeroed first.
59
+ The second GEMM is given ``output_topk``, which makes it reduce over the
60
+ top-k dimension while it accumulates — the ``moe_sum`` launch and the
61
+ ``[M * top_k, K]`` intermediate are fused away, hence
62
+ ``TopKWeightAndReduceNoOP``.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ moe_config: FusedMoEConfig,
68
+ quant_config: FusedMoEQuantConfig,
69
+ max_num_tokens: int | None = None,
70
+ num_dispatchers: int | None = None,
71
+ ):
72
+ assert quant_config.use_int4_w4a16, "Supports only int4_w4a16"
73
+ super().__init__(
74
+ moe_config=moe_config,
75
+ quant_config=quant_config,
76
+ max_num_tokens=max_num_tokens,
77
+ num_dispatchers=num_dispatchers,
78
+ )
79
+ self._empty_topk_weights: torch.Tensor | None = None
80
+
81
+ @staticmethod
82
+ def activation_format() -> mk.FusedMoEActivationFormat:
83
+ return mk.FusedMoEActivationFormat.Standard
84
+
85
+ def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
86
+ return TopKWeightAndReduceNoOP()
87
+
88
+ @staticmethod
89
+ def _supports_current_device() -> bool:
90
+ return rdna3_moe_kernel_available()
91
+
92
+ @staticmethod
93
+ def _supports_no_act_and_mul() -> bool:
94
+ return True
95
+
96
+ @staticmethod
97
+ def _supports_quant_scheme(
98
+ weight_key: QuantKey | None,
99
+ activation_key: QuantKey | None,
100
+ ) -> bool:
101
+ # Symmetric int4 weights only: the kernel consumes synthesized zero
102
+ # points and has no path for checkpoint zero points.
103
+ return activation_key is None and weight_key in (kInt4Static, kInt4Static32)
104
+
105
+ @staticmethod
106
+ def _supports_activation(activation: MoEActivation) -> bool:
107
+ return apply_moe_activation_supported(activation)
108
+
109
+ @staticmethod
110
+ def _supports_parallel_config(moe_parallel_config: FusedMoEParallelConfig) -> bool:
111
+ return not (
112
+ moe_parallel_config.use_fi_nvl_two_sided_kernels
113
+ or moe_parallel_config.use_fi_nvl_one_sided_kernels
114
+ )
115
+
116
+ def moe_problem_size(
117
+ self,
118
+ a1: torch.Tensor,
119
+ w1: torch.Tensor,
120
+ w2: torch.Tensor,
121
+ topk_ids: torch.Tensor,
122
+ ) -> tuple[int, int, int, int, int]:
123
+ # Weights are packed K-first: w1 is [E, K // 8, w13_shards * N] and
124
+ # w2 is [E, N // 8, K], so neither N nor K can be read off a trailing
125
+ # dimension the way the base implementation does.
126
+ assert w1.dim() == 3 and w2.dim() == 3
127
+ assert a1.dim() == 2
128
+ assert topk_ids.size(0) == a1.size(0), f"{topk_ids.size(0)} != {a1.size(0)}"
129
+ return w1.size(0), a1.size(0), w2.size(1) * 8, a1.size(-1), topk_ids.size(1)
130
+
131
+ def workspace_shapes(
132
+ self,
133
+ M: int,
134
+ N: int,
135
+ K: int,
136
+ topk: int,
137
+ global_num_experts: int,
138
+ local_num_experts: int,
139
+ expert_tokens_meta: mk.ExpertTokensMetadata | None,
140
+ activation: MoEActivation,
141
+ ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
142
+ # The modular kernel provisions the output buffer out of workspace13,
143
+ # so both GEMM-1 buffers have to live in workspace2 — the second GEMM
144
+ # reads the activation output while accumulating into the output.
145
+ gate_up = N if not activation.is_gated else 2 * N
146
+ return ((M, K), (M * topk * (gate_up + N),), (M, K))
147
+
148
+ def apply(
149
+ self,
150
+ output: torch.Tensor,
151
+ hidden_states: torch.Tensor,
152
+ w1: torch.Tensor,
153
+ w2: torch.Tensor,
154
+ topk_weights: torch.Tensor,
155
+ topk_ids: torch.Tensor,
156
+ activation: MoEActivation,
157
+ global_num_experts: int,
158
+ expert_map: torch.Tensor | None,
159
+ a1q_scale: torch.Tensor | None,
160
+ a2_scale: torch.Tensor | None,
161
+ workspace13: torch.Tensor,
162
+ workspace2: torch.Tensor,
163
+ expert_tokens_meta: mk.ExpertTokensMetadata | None,
164
+ apply_router_weight_on_input: bool,
165
+ ) -> None:
166
+ assert self.w1_scale is not None and self.w2_scale is not None
167
+ assert self.w1_zp is not None and self.w2_zp is not None
168
+
169
+ E, M, N, K, top_k = self.moe_problem_size(hidden_states, w1, w2, topk_ids)
170
+ if global_num_experts == -1:
171
+ global_num_experts = E
172
+
173
+ gate_up = w1.size(2)
174
+ act_n = self.adjust_N_for_activation(gate_up, activation)
175
+ rows = M * top_k
176
+
177
+ scratch = workspace2.view(-1)
178
+ gate_up_out = scratch[: rows * gate_up].view(rows, gate_up)
179
+ act_out = scratch[rows * gate_up : rows * (gate_up + act_n)].view(rows, act_n)
180
+
181
+ # BLOCK_SIZE_M=1 for decode (no padding waste), 4 for prefill.
182
+ block_size_m = 1 if M <= 4 else 4
183
+ sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
184
+ topk_ids,
185
+ block_size_m,
186
+ global_num_experts,
187
+ expert_map,
188
+ )
189
+
190
+ if (
191
+ self._empty_topk_weights is None
192
+ or self._empty_topk_weights.device != hidden_states.device
193
+ ):
194
+ self._empty_topk_weights = torch.empty(
195
+ 0, dtype=torch.float32, device=hidden_states.device
196
+ )
197
+ topk_weights_f32 = topk_weights.reshape(-1).float()
198
+ no_topk_weights = self._empty_topk_weights
199
+
200
+ gate_up_out.zero_()
201
+ ops.moe_gptq_gemm_rdna3(
202
+ hidden_states,
203
+ gate_up_out,
204
+ w1,
205
+ self.w1_scale,
206
+ self.w1_zp,
207
+ topk_weights_f32 if apply_router_weight_on_input else no_topk_weights,
208
+ sorted_token_ids,
209
+ expert_ids,
210
+ num_tokens_post_padded,
211
+ top_k,
212
+ block_size_m,
213
+ apply_router_weight_on_input,
214
+ )
215
+
216
+ self.activation(activation, act_out, gate_up_out)
217
+
218
+ # output_topk=top_k makes the kernel accumulate into out[token_id],
219
+ # fusing the top-k reduction into the atomic write-back.
220
+ output.zero_()
221
+ ops.moe_gptq_gemm_rdna3(
222
+ act_out,
223
+ output,
224
+ w2,
225
+ self.w2_scale,
226
+ self.w2_zp,
227
+ no_topk_weights if apply_router_weight_on_input else topk_weights_f32,
228
+ sorted_token_ids,
229
+ expert_ids,
230
+ num_tokens_post_padded,
231
+ 1,
232
+ block_size_m,
233
+ not apply_router_weight_on_input,
234
+ output_topk=top_k,
235
+ )
vllm/model_executor/layers/fused_moe/oracle/int_wna16.py
+110
@@ -7,6 +7,7 @@ from typing import Any
7
import torch
8
from compressed_tensors.quantization import (
9
QuantizationArgs,
10
+ QuantizationStrategy,
11
)
12
13
import vllm._custom_ops as ops
@@ -41,8 +42,10 @@ from vllm.model_executor.layers.quantization.utils.marlin_utils import (
42
)
43
from vllm.model_executor.layers.quantization.utils.quant_utils import (
44
QuantKey,
45
+ pack_quantized_values_into_int32,
46
)
47
from vllm.platforms import current_platform
48
+from vllm.scalar_type import scalar_types
49
50
logger = init_logger(__name__)
51
@@ -56,6 +59,7 @@ class WNA16MoEBackend(Enum):
59
TRITON = "TRITON"
60
XPU = "XPU"
61
EMULATION = "EMULATION"
62
+ RDNA3 = "RDNA3"
63
64
65
def backend_to_kernel_cls(
@@ -100,6 +104,12 @@ def backend_to_kernel_cls(
104
)
105
106
return [Int4EmulationTritonExperts]
107
+ elif backend == WNA16MoEBackend.RDNA3:
108
+ from vllm.model_executor.layers.fused_moe.experts.rdna3_moe import (
109
+ Rdna3WNA16Experts,
110
+ )
111
+
112
+ return [Rdna3WNA16Experts]
113
else:
114
raise ValueError(f"Unknown WNA16 MoE backend: {backend.value}")
115
@@ -114,6 +124,8 @@ def _get_priority_backends() -> list[WNA16MoEBackend]:
124
return [WNA16MoEBackend.XPU]
125
126
return [
127
+ # Native HIP kernel, gated on gfx1100 by _supports_current_device().
128
+ WNA16MoEBackend.RDNA3,
129
WNA16MoEBackend.FLASHINFER_TRTLLM,
130
WNA16MoEBackend.MARLIN,
131
WNA16MoEBackend.BATCHED_MARLIN,
@@ -134,6 +146,18 @@ def _backend_incompatibility_reason(
146
if backend == WNA16MoEBackend.FLASHINFER_TRTLLM and (may_have_zp or may_have_bias):
147
return "zero points and bias are not supported"
148
149
+ if backend == WNA16MoEBackend.RDNA3:
150
+ if not isinstance(quant_config, QuantizationArgs):
151
+ return "only compressed-tensors checkpoints are supported"
152
+ if may_have_zp:
153
+ return "asymmetric checkpoints are not supported"
154
+ if may_have_bias:
155
+ return "expert bias is not supported"
156
+ if quant_config.actorder == "group":
157
+ return "group activation ordering is not supported"
158
+ if quant_config.strategy != QuantizationStrategy.GROUP:
159
+ return "only group-wise scales are supported"
160
+
161
from vllm.model_executor.layers.quantization.auto_awq import AutoAWQConfig
162
from vllm.model_executor.layers.quantization.auto_gptq import AutoGPTQConfig
163
from vllm.model_executor.layers.quantization.moe_wna16 import MoeWNA16Config
@@ -190,6 +214,7 @@ def map_wna16_backend(runner_backend: MoEBackend) -> WNA16MoEBackend:
214
"humming": WNA16MoEBackend.HUMMING,
215
"flashinfer_trtllm": WNA16MoEBackend.FLASHINFER_TRTLLM,
216
"emulation": WNA16MoEBackend.EMULATION,
217
+ "rdna3": WNA16MoEBackend.RDNA3,
218
}
219
if backend := mapping.get(runner_backend):
220
return backend
@@ -372,6 +397,9 @@ def make_wna16_moe_kernel(
397
from vllm.model_executor.layers.fused_moe.experts.int4_emulation_moe import (
398
Int4EmulationTritonExperts,
399
)
400
+ from vllm.model_executor.layers.fused_moe.experts.rdna3_moe import (
401
+ Rdna3WNA16Experts,
402
+ )
403
from vllm.model_executor.layers.fused_moe.experts.xpu_moe import (
404
XPUExpertsWNA16,
405
)
@@ -387,6 +415,7 @@ def make_wna16_moe_kernel(
415
XPUExpertsWNA16,
416
CPUExpertsInt4,
417
Int4EmulationTritonExperts,
418
+ Rdna3WNA16Experts,
419
)
420
if backend == WNA16MoEBackend.HUMMING:
421
allowed_experts += tuple(backend_to_kernel_cls(WNA16MoEBackend.HUMMING))
@@ -811,6 +840,78 @@ def _process_awq_weights_marlin(
840
)
841
842
843
+def _synthesize_rdna3_qzeros(
844
+ groups: int, out_features: int, device: torch.device
845
+) -> torch.Tensor:
846
+ """Create the packed zero-point tensor for symmetric quantization.
847
+
848
+ GPTQv1 +1 quirk: the kernel adds 1 to the stored zeros, so encode
849
+ (bias - 1) = 7 for uint4b8 (bias=8).
850
+ """
851
+ zeros = torch.full(
852
+ (groups, out_features),
853
+ scalar_types.uint4b8.bias - 1,
854
+ dtype=torch.int32,
855
+ device=device,
856
+ )
857
+ return pack_quantized_values_into_int32(zeros, scalar_types.uint4b8, packed_dim=1)
858
+
859
+
860
+def _process_weights_rdna3(
861
+ w13: torch.Tensor,
862
+ w2: torch.Tensor,
863
+ w13_scale: torch.Tensor,
864
+ w2_scale: torch.Tensor,
865
+ group_size: int,
866
+) -> tuple[
867
+ torch.Tensor, # w13_qweight
868
+ torch.Tensor, # w2_qweight
869
+ torch.Tensor, # w13_scales
870
+ torch.Tensor, # w2_scales
871
+ torch.Tensor | None, # w13_qzeros
872
+ torch.Tensor | None, # w2_qzeros
873
+ torch.Tensor | None, # w13_input_global_scale
874
+ torch.Tensor | None, # w2_input_global_scale
875
+ torch.Tensor | None, # w13_bias
876
+ torch.Tensor | None, # w2_bias
877
+]:
878
+ """RDNA3 (gfx1100) W4A16 weight post-processing.
879
+
880
+ Interleaves the packed nibbles per expert (the exllama shuffle the dense
881
+ RDNA3 kernel also uses) and synthesizes the symmetric zero points that
882
+ ``moe_gptq_gemm_rdna3`` dequantizes with. The packed layout
883
+ ``[E, K // 8, N]`` and the ``[E, groups, N]`` scales are already what the
884
+ kernel wants, so neither is repacked.
885
+ """
886
+ device = w13.device
887
+ num_experts = w13.size(0)
888
+
889
+ for e in range(num_experts):
890
+ w13_e = w13[e].contiguous()
891
+ ops.gptq_shuffle(w13_e, 4)
892
+ w13[e] = w13_e
893
+ w2_e = w2[e].contiguous()
894
+ ops.gptq_shuffle(w2_e, 4)
895
+ w2[e] = w2_e
896
+
897
+ def _qzeros(w: torch.Tensor) -> torch.Tensor:
898
+ qz = _synthesize_rdna3_qzeros((w.size(1) * 8) // group_size, w.size(2), device)
899
+ return qz.unsqueeze(0).expand(num_experts, -1, -1).contiguous()
900
+
901
+ return (
902
+ w13,
903
+ w2,
904
+ w13_scale.contiguous(),
905
+ w2_scale.contiguous(),
906
+ _qzeros(w13),
907
+ _qzeros(w2),
908
+ None, # w13_input_global_scale
909
+ None, # w2_input_global_scale
910
+ None, # w13_bias
911
+ None, # w2_bias
912
+ )
913
+
914
+
915
def _process_weights_cpu(
916
quant_config: QuantizationConfig | QuantizationArgs | None,
917
w13: torch.Tensor,
@@ -1432,6 +1533,15 @@ def convert_to_wna16_moe_kernel_format(
1533
w13_bias,
1534
w2_bias,
1535
)
1536
+ elif backend == WNA16MoEBackend.RDNA3:
1537
+ assert isinstance(quant_config, QuantizationArgs)
1538
+ return _process_weights_rdna3(
1539
+ w13,
1540
+ w2,
1541
+ w13_scale,
1542
+ w2_scale,
1543
+ quant_config.group_size,
1544
+ )
1545
elif backend == WNA16MoEBackend.CPU:
1546
return _process_weights_cpu(
1547
quant_config,
vllm/model_executor/layers/fused_moe/routed_experts.py
-1
@@ -617,7 +617,6 @@ class RoutedExperts(PluggableLayer):
617
# against known CompressionFormat enum values that have this quality
618
if quant_method_name in (
619
"CompressedTensorsWNA16MoEMethod",
620
- "CompressedTensorsWNA16RDNA3MoEMethod",
620
"CompressedTensorsW4A16FlydslMoEMethod",
621
):
622
if is_transposed:
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe.py
-7
@@ -88,14 +88,7 @@ class CompressedTensorsMoEMethod(FusedMoEMethodBase):
88
f"and bits: {weight_quant.num_bits}"
89
)
90
91
- # Native ROCm HIP kernels (RDNA3, etc.)
91
if current_platform.is_rocm():
93
- from . import rocm_moe_rdna
94
-
95
- if rocm_moe_rdna.is_supported(weight_quant):
96
- return rocm_moe_rdna.make_method(
97
- weight_quant, input_quant, layer.moe_config
98
- )
92
from vllm.platforms.rocm import on_gfx950
93
94
vllm_config = get_current_vllm_config()
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_wna16.py
+4
-2
@@ -443,10 +443,12 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod):
443
replace_parameter(layer, "w13_weight_scale", w13_scales)
444
replace_parameter(layer, "w2_weight_scale", w2_scales)
445
446
- # CPU fused_experts_cpu requires zero points even for symmetric quant.
446
+ # CPU fused_experts_cpu and the RDNA3 HIP kernel require zero points
447
+ # even for symmetric quant (the oracle synthesizes them).
448
# EMULATION bakes ZP into the dequantized bf16 weights — ZP is None.
449
if (
449
- not self.symmetric or self.wna16_backend == WNA16MoEBackend.CPU
450
+ not self.symmetric
451
+ or self.wna16_backend in (WNA16MoEBackend.CPU, WNA16MoEBackend.RDNA3)
452
) and self.wna16_backend != WNA16MoEBackend.EMULATION:
453
assert w13_qzeros is not None and w2_qzeros is not None
454
replace_parameter(layer, "w13_weight_zero_point", w13_qzeros)
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_wna16_rdna3.py
deleted
-260
@@ -1,260 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""CompressedTensors MoE W4A16 using the fused RDNA3 (gfx1100) HIP kernel.
4
-
5
-Uses ``moe_gptq_gemm_rdna3`` — a single HIP kernel launch per GEMM that
6
-handles expert routing + W4A16 dequant + dot product with atomic output.
7
-
8
-Weight format (per expert, same as dense RDNA3 W4A16):
9
- - Packed int32 ``[E, K/8, N]`` with exllama shuffle
10
- - Scales ``[E, groups, N]`` in activation dtype
11
- - Zero points ``[E, groups, N/8]`` packed int32 (synthesized)
12
-"""
13
-
14
-import torch
15
-
16
-from vllm import _custom_ops as ops
17
-from vllm.logger import init_logger
18
-from vllm.model_executor.layers.fused_moe import (
19
- RoutedExperts,
20
- SharedExperts,
21
-)
22
-from vllm.model_executor.layers.fused_moe.activation import (
23
- MoEActivation,
24
- apply_moe_activation,
25
-)
26
-from vllm.model_executor.layers.fused_moe.moe_align_block_size import (
27
- moe_align_block_size,
28
-)
29
-from vllm.model_executor.layers.quantization.compressed_tensors.compressed_tensors_moe.compressed_tensors_moe_wna16 import ( # noqa: E501
30
- CompressedTensorsWNA16MoEMethod,
31
-)
32
-from vllm.model_executor.layers.quantization.utils.quant_utils import (
33
- pack_quantized_values_into_int32,
34
-)
35
-from vllm.scalar_type import scalar_types
36
-
37
-logger = init_logger(__name__)
38
-
39
-
40
-def _synthesize_qzeros(
41
- groups: int, out_features: int, device: torch.device
42
-) -> torch.Tensor:
43
- """Create packed zero-point tensor for symmetric quant.
44
-
45
- GPTQv1 +1 quirk: kernel adds 1 to stored zeros, so encode
46
- (bias - 1) = 7 for uint4b8 (bias=8).
47
- """
48
- zeros = torch.full(
49
- (groups, out_features),
50
- scalar_types.uint4b8.bias - 1,
51
- dtype=torch.int32,
52
- device=device,
53
- )
54
- return pack_quantized_values_into_int32(zeros, scalar_types.uint4b8, packed_dim=1)
55
-
56
-
57
-class CompressedTensorsWNA16RDNA3MoEMethod(CompressedTensorsWNA16MoEMethod):
58
- """W4A16 MoE using the fused RDNA3 HIP kernel (moe_gptq_gemm_rdna3).
59
-
60
- Weights are in RDNA3 format (shuffled int32 [E, K/8, N]),
61
- NOT Triton format (transposed uint8). apply() dispatches through
62
- the fused HIP kernel directly.
63
- """
64
-
65
- def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
66
- device = layer.w13_weight_packed.device
67
- num_experts = layer.w13_weight_packed.shape[0]
68
-
69
- # Shuffle weights in-place per expert (exllama nibble interleave)
70
- for e in range(num_experts):
71
- w13_e = layer.w13_weight_packed.data[e].contiguous()
72
- ops.gptq_shuffle(w13_e, 4)
73
- layer.w13_weight_packed.data[e] = w13_e
74
- w2_e = layer.w2_weight_packed.data[e].contiguous()
75
- ops.gptq_shuffle(w2_e, 4)
76
- layer.w2_weight_packed.data[e] = w2_e
77
-
78
- # Keep scales as [E, groups, N] in activation dtype
79
- act_dtype = layer.w13_weight_scale.dtype
80
- layer.w13_weight_scale = torch.nn.Parameter(
81
- layer.w13_weight_scale.to(dtype=act_dtype).contiguous(),
82
- requires_grad=False,
83
- )
84
- layer.w2_weight_scale = torch.nn.Parameter(
85
- layer.w2_weight_scale.to(dtype=act_dtype).contiguous(),
86
- requires_grad=False,
87
- )
88
-
89
- # Synthesize packed zero points: [E, groups, N/8] int32
90
- w13_groups = (layer.w13_weight_packed.shape[1] * 8) // self.group_size
91
- w13_N = layer.w13_weight_packed.shape[2]
92
- w2_groups = (layer.w2_weight_packed.shape[1] * 8) // self.group_size
93
- w2_N = layer.w2_weight_packed.shape[2]
94
-
95
- w13_qz = _synthesize_qzeros(w13_groups, w13_N, device)
96
- w2_qz = _synthesize_qzeros(w2_groups, w2_N, device)
97
- layer.w13_qzeros = torch.nn.Parameter(
98
- w13_qz.unsqueeze(0).expand(num_experts, -1, -1).contiguous(),
99
- requires_grad=False,
100
- )
101
- layer.w2_qzeros = torch.nn.Parameter(
102
- w2_qz.unsqueeze(0).expand(num_experts, -1, -1).contiguous(),
103
- requires_grad=False,
104
- )
105
-
106
- # Pre-allocate reusable buffers for decode (sizes based on top_k=8)
107
- N_gate_up = w13_N
108
- hidden_size = w2_N
109
- intermediate = N_gate_up // 2 # gated activation
110
- # Max tokens we expect in decode; prefill will re-allocate if needed
111
- max_decode_tokens = 16
112
- top_k = 8 # conservative default
113
- buf_size = max_decode_tokens * top_k
114
- layer.rdna3_w1_buf = torch.zeros(
115
- buf_size, N_gate_up, dtype=act_dtype, device=device
116
- )
117
- layer.rdna3_act_buf = torch.empty(
118
- buf_size, intermediate, dtype=act_dtype, device=device
119
- )
120
- layer.rdna3_out_buf = torch.zeros(
121
- max_decode_tokens, hidden_size, dtype=act_dtype, device=device
122
- )
123
- layer.rdna3_empty_tw = torch.empty(0, device=device)
124
-
125
- def apply(
126
- self,
127
- layer: RoutedExperts,
128
- x: torch.Tensor,
129
- topk_weights: torch.Tensor,
130
- topk_ids: torch.Tensor,
131
- shared_experts: SharedExperts | None,
132
- shared_experts_input: torch.Tensor | None,
133
- ) -> torch.Tensor:
134
- activation = (
135
- layer.activation
136
- if isinstance(layer.activation, MoEActivation)
137
- else MoEActivation.from_str(layer.activation)
138
- )
139
- return _rdna3_fused_moe(
140
- x,
141
- topk_weights,
142
- topk_ids,
143
- layer=layer,
144
- activation=activation,
145
- apply_router_weight_on_input=(layer.apply_router_weight_on_input),
146
- global_num_experts=layer.global_num_experts,
147
- expert_map=layer.expert_map,
148
- )
149
-
150
-
151
-def _rdna3_fused_moe(
152
- hidden_states: torch.Tensor,
153
- topk_weights: torch.Tensor,
154
- topk_ids: torch.Tensor,
155
- layer: RoutedExperts,
156
- activation: MoEActivation,
157
- apply_router_weight_on_input: bool,
158
- global_num_experts: int,
159
- expert_map: torch.Tensor | None,
160
-) -> torch.Tensor:
161
- """Fused MoE forward using the RDNA3 W4A16 HIP kernel.
162
-
163
- Optimizations vs naive dispatch:
164
- - BLOCK_SIZE_M=1 for decode (no padding waste, bf16 fast path)
165
- - Pre-allocated buffers (no torch.zeros per call)
166
- - Inline token sorting for small M (skip moe_align_block_size)
167
- - moe_sum fused into output accumulation
168
- """
169
- num_tokens = hidden_states.shape[0]
170
- top_k = topk_ids.shape[1]
171
- total_tokens = num_tokens * top_k
172
- N_gate_up = layer.w13_weight_packed.shape[2]
173
- hidden_size = layer.w2_weight_packed.shape[2]
174
- dtype = hidden_states.dtype
175
- device = hidden_states.device
176
-
177
- intermediate_size = N_gate_up // 2 if activation.is_gated else N_gate_up
178
-
179
- if global_num_experts <= 0:
180
- global_num_experts = layer.w13_weight_packed.shape[0]
181
-
182
- # BLOCK_SIZE_M=1 for decode (small M), 4 for prefill
183
- block_size_m = 1 if num_tokens <= 4 else 4
184
-
185
- # --- Token routing ---
186
- sorted_token_ids, expert_ids, num_tokens_post_padded = moe_align_block_size(
187
- topk_ids,
188
- block_size_m,
189
- global_num_experts,
190
- expert_map,
191
- )
192
-
193
- # --- Reuse pre-allocated buffers when possible ---
194
- if total_tokens <= layer.rdna3_w1_buf.shape[0]:
195
- w1_out = layer.rdna3_w1_buf[:total_tokens]
196
- w1_out.zero_()
197
- act_out = layer.rdna3_act_buf[:total_tokens]
198
- else:
199
- w1_out = torch.zeros(
200
- total_tokens,
201
- N_gate_up,
202
- dtype=dtype,
203
- device=device,
204
- )
205
- act_out = torch.empty(
206
- total_tokens,
207
- intermediate_size,
208
- dtype=dtype,
209
- device=device,
210
- )
211
-
212
- # --- topk weights (pre-cast to float32 for kernel) ---
213
- topk_w_float = topk_weights.view(-1).float()
214
- empty_tw = layer.rdna3_empty_tw
215
-
216
- # --- w1 GEMM: [M, K] -> [M*top_k, N_gate_up] ---
217
- ops.moe_gptq_gemm_rdna3(
218
- hidden_states,
219
- w1_out,
220
- layer.w13_weight_packed,
221
- layer.w13_weight_scale,
222
- layer.w13_qzeros,
223
- topk_w_float if apply_router_weight_on_input else empty_tw,
224
- sorted_token_ids,
225
- expert_ids,
226
- num_tokens_post_padded,
227
- top_k,
228
- block_size_m,
229
- apply_router_weight_on_input,
230
- )
231
-
232
- # --- Activation (silu_and_mul etc.) ---
233
- apply_moe_activation(activation, act_out, w1_out)
234
-
235
- # --- w2 GEMM: [M*top_k, intermediate] -> [M, hidden] (fused reduce) ---
236
- # output_topk=top_k: kernel writes to out[token_id / top_k] directly,
237
- # fusing moe_sum into the atomic accumulation — saves one kernel launch
238
- # and the w2_out intermediate buffer.
239
- out = torch.zeros(
240
- num_tokens,
241
- hidden_size,
242
- dtype=dtype,
243
- device=device,
244
- )
245
- ops.moe_gptq_gemm_rdna3(
246
- act_out,
247
- out,
248
- layer.w2_weight_packed,
249
- layer.w2_weight_scale,
250
- layer.w2_qzeros,
251
- topk_w_float if not apply_router_weight_on_input else empty_tw,
252
- sorted_token_ids,
253
- expert_ids,
254
- num_tokens_post_padded,
255
- 1,
256
- block_size_m,
257
- not apply_router_weight_on_input,
258
- output_topk=top_k,
259
- )
260
- return out
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/rocm_moe_rdna.py
deleted
-48
@@ -1,48 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""ROCm MoE kernel dispatcher.
4
-
5
-Selects architecture-specific native HIP MoE kernels in priority order.
6
-Falls back to the Triton WNA16 path when no native kernel is available.
7
-"""
8
-
9
-import torch
10
-
11
-from vllm.logger import init_logger
12
-
13
-logger = init_logger(__name__)
14
-
15
-
16
-def is_supported(weight_quant) -> bool:
17
- """Check if a native ROCm MoE kernel is available for this config."""
18
- if weight_quant.num_bits != 4:
19
- return False
20
-
21
- from vllm.platforms.rocm import on_gfx1100
22
-
23
- # RDNA3 (gfx1100). Future: add RDNA4 (gfx12x), CDNA (gfx94x), etc.
24
- return (
25
- on_gfx1100()
26
- and hasattr(torch.ops, "_rocm_C")
27
- and hasattr(torch.ops._rocm_C, "moe_gptq_gemm_rdna3")
28
- )
29
-
30
-
31
-def make_method(weight_quant, input_quant, moe_config):
32
- """Create the native ROCm MoE method. Call only after is_supported()."""
33
- from vllm.platforms.rocm import on_gfx1100
34
-
35
- if on_gfx1100():
36
- from .compressed_tensors_moe_wna16_rdna3 import (
37
- CompressedTensorsWNA16RDNA3MoEMethod,
38
- )
39
-
40
- logger.info_once(
41
- "Using CompressedTensorsWNA16RDNA3MoEMethod (native RDNA3 HIP kernel)"
42
- )
43
- return CompressedTensorsWNA16RDNA3MoEMethod(
44
- weight_quant, input_quant, moe_config
45
- )
46
-
47
- # Future: RDNA4, CDNA, etc.
48
- raise RuntimeError("is_supported() returned True but no kernel matched")