[MoE Refactor] Migrate MoeWNA16Method quantization method over to using the new MK oracle scheme. (#44120)

Signed-off-by: Bill Nell <bnell@redhat.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: mgoin <mgoin64@gmail.com>

bnellnm committed Jul 21, 2026 at 22:20 UTC ec59c1579f582ac1f8f9ac5871490138f9a84b14
11 files changed +805 -195
tests/quantization/test_auto_gptq.py
+79
@@ -5,13 +5,17 @@
5 Run `pytest tests/quantization/test_auto_gptq.py -v -s`.
6 """
7
8 +from types import SimpleNamespace
9 +
10 import pytest
11 import torch
12
13 from tests.quantization.utils import is_quant_method_supported
14 +from vllm.model_executor.layers.fused_moe import RoutedExperts
15 from vllm.model_executor.layers.quantization.auto_gptq import (
16 AutoGPTQConfig,
17 AutoGPTQLinearMethod,
18 + AutoGPTQMoEMethod,
19 )
20
21 PROMPT = "On the surface of Mars, we found"
@@ -54,3 +58,78 @@ def test_auto_gptq_quantization_method(vllm_runner, model_id: str, monkeypatch):
58 def test_auto_gptq_config_get_name():
59 """Test that AutoGPTQConfig.get_name() returns 'auto_gptq'."""
60 assert AutoGPTQConfig.get_name() == "auto_gptq"
61 +
62 +
63 +def test_auto_gptq_moe_creates_zero_initialized_expert_biases():
64 + method = object.__new__(AutoGPTQMoEMethod)
65 + method.quant_config = AutoGPTQConfig(4, 128, False, True, False, {}, {})
66 + method.input_dtype = None
67 + method.experts_cls = None
68 + layer = torch.nn.Module()
69 +
70 + method.create_weights(
71 + layer=layer,
72 + num_experts=2,
73 + hidden_size=8,
74 + intermediate_size_per_partition=4,
75 + params_dtype=torch.float16,
76 + intermediate_size_full=4,
77 + weight_loader=lambda *args, **kwargs: None,
78 + )
79 +
80 + assert layer.w13_bias.shape == (2, 8)
81 + assert layer.w2_bias.shape == (2, 8)
82 + assert torch.count_nonzero(layer.w13_bias) == 0
83 + assert torch.count_nonzero(layer.w2_bias) == 0
84 +
85 +
86 +def test_routed_experts_loads_per_expert_biases():
87 + class Loader:
88 + quant_config = None
89 + quant_method = object()
90 + moe_config = SimpleNamespace(
91 + is_act_and_mul=True,
92 + tp_rank=0,
93 + moe_parallel_config=SimpleNamespace(tp_size=1),
94 + )
95 + _get_hidden_dim = staticmethod(RoutedExperts._get_hidden_dim)
96 + _narrow_expert_data_for_padding = staticmethod(
97 + RoutedExperts._narrow_expert_data_for_padding
98 + )
99 + _load_w13 = RoutedExperts._load_w13
100 + _loaded_expert_biases = set()
101 +
102 + @staticmethod
103 + def _map_global_expert_id_to_local_expert_id(expert_id):
104 + return expert_id
105 +
106 + loader = Loader()
107 + w13_bias = torch.nn.Parameter(torch.zeros(1, 8), requires_grad=False)
108 + w2_bias = torch.nn.Parameter(torch.zeros(1, 4), requires_grad=False)
109 +
110 + for shard_id, loaded in (
111 + ("w1", torch.tensor([1.0, 2.0, 3.0, 4.0])),
112 + ("w3", torch.tensor([5.0, 6.0, 7.0, 8.0])),
113 + ):
114 + assert RoutedExperts.weight_loader(
115 + loader,
116 + w13_bias,
117 + loaded,
118 + weight_name="model.layers.0.mlp.experts.w13_bias",
119 + shard_id=shard_id,
120 + expert_id=0,
121 + return_success=True,
122 + )
123 +
124 + assert RoutedExperts.weight_loader(
125 + loader,
126 + w2_bias,
127 + torch.tensor([9.0, 10.0, 11.0, 12.0]),
128 + weight_name="model.layers.0.mlp.experts.w2_bias",
129 + shard_id="w2",
130 + expert_id=0,
131 + return_success=True,
132 + )
133 + assert torch.equal(w13_bias, torch.arange(1, 9, dtype=torch.float32).reshape(1, 8))
134 + assert torch.equal(w2_bias, torch.arange(9, 13, dtype=torch.float32).reshape(1, 4))
135 + assert loader._loaded_expert_biases == {"w13_bias", "w2_bias"}
tests/quantization/test_moe_wna16.py
+179 -30
@@ -5,45 +5,194 @@ from types import SimpleNamespace
5
6 import pytest
7 import torch
8 +from compressed_tensors.quantization import (
9 + ActivationOrdering,
10 + QuantizationArgs,
11 + QuantizationStrategy,
12 + QuantizationType,
13 +)
14
9 -from vllm.model_executor.layers.fused_moe.activation import MoEActivation
10 -from vllm.model_executor.layers.quantization.moe_wna16 import MoeWNA16Method
11 -from vllm.platforms import current_platform
15 +from vllm.model_executor.layers.fused_moe.oracle.int_wna16 import (
16 + WNA16MoEBackend,
17 + _backend_incompatibility_reason,
18 + _convert_moe_wna16_humming_tensors,
19 + convert_to_wna16_moe_kernel_format,
20 + map_wna16_backend,
21 +)
22 +from vllm.model_executor.layers.quantization import moe_wna16
23 +from vllm.model_executor.layers.quantization.auto_awq import AutoAWQConfig
24 +from vllm.model_executor.layers.quantization.auto_gptq import AutoGPTQConfig
25 +from vllm.model_executor.layers.quantization.moe_wna16 import (
26 + MoeWNA16Config,
27 + MoeWNA16Method,
28 +)
29
30
14 -@pytest.mark.skipif(not current_platform.is_cuda(), reason="Only test on CUDA")
15 -def test_moe_wna16_apply_passes_layer_activation(monkeypatch):
16 - captured_kwargs = {}
31 +def test_map_wna16_backend_supports_triton():
32 + assert map_wna16_backend("triton") == WNA16MoEBackend.TRITON
33
18 - def fake_fused_experts(*args, **kwargs):
19 - captured_kwargs.update(kwargs)
20 - return torch.empty(1, 2)
34
22 - monkeypatch.setattr(
23 - "vllm.model_executor.layers.fused_moe.fused_experts",
24 - fake_fused_experts,
35 +@pytest.mark.parametrize(
36 + ("backend", "quant_config", "may_have_zp", "may_have_bias", "expected"),
37 + [
38 + (
39 + WNA16MoEBackend.TRITON,
40 + AutoAWQConfig(4, 128, True, False),
41 + True,
42 + False,
43 + "AutoAWQ weight layout",
44 + ),
45 + (
46 + WNA16MoEBackend.TRITON,
47 + AutoGPTQConfig(4, 128, True, True, False, {}, {}),
48 + False,
49 + False,
50 + "activation ordering",
51 + ),
52 + (
53 + WNA16MoEBackend.TRITON,
54 + QuantizationArgs(
55 + num_bits=4,
56 + type=QuantizationType.INT,
57 + strategy=QuantizationStrategy.GROUP,
58 + symmetric=True,
59 + dynamic=False,
60 + group_size=128,
61 + actorder=ActivationOrdering.GROUP,
62 + ),
63 + False,
64 + False,
65 + "activation ordering",
66 + ),
67 + (
68 + WNA16MoEBackend.TRITON,
69 + AutoGPTQConfig(4, 128, False, True, False, {}, {}),
70 + False,
71 + True,
72 + "bias",
73 + ),
74 + (
75 + WNA16MoEBackend.MARLIN,
76 + MoeWNA16Config(
77 + linear_quant_method="gptq",
78 + weight_bits=4,
79 + group_size=128,
80 + has_zp=False,
81 + lm_head_quantized=False,
82 + modules_to_not_convert=None,
83 + full_config={},
84 + ),
85 + False,
86 + False,
87 + "MoeWNA16 checkpoint layout",
88 + ),
89 + ],
90 +)
91 +def test_wna16_oracle_rejects_incompatible_quant_structures(
92 + backend, quant_config, may_have_zp, may_have_bias, expected
93 +):
94 + reason = _backend_incompatibility_reason(
95 + backend=backend,
96 + quant_config=quant_config,
97 + may_have_zp=may_have_zp,
98 + may_have_bias=may_have_bias,
99 + )
100 +
101 + assert reason is not None
102 + assert expected in reason
103 +
104 +
105 +def test_compressed_tensors_weights_are_transposed_for_triton():
106 + quant_config = QuantizationArgs(
107 + num_bits=4,
108 + type=QuantizationType.INT,
109 + strategy=QuantizationStrategy.GROUP,
110 + symmetric=True,
111 + dynamic=False,
112 + group_size=32,
113 )
114 + w13 = torch.arange(16, dtype=torch.int32).reshape(1, 2, 8)
115 + w2 = torch.arange(12, dtype=torch.int32).reshape(1, 2, 6)
116 + w13_scale = torch.arange(32, dtype=torch.float16).reshape(1, 4, 8)
117 + w2_scale = torch.arange(18, dtype=torch.float16).reshape(1, 3, 6)
118
119 + converted = convert_to_wna16_moe_kernel_format(
120 + backend=WNA16MoEBackend.TRITON,
121 + layer=torch.nn.Module(),
122 + quant_config=quant_config,
123 + input_dtype=None,
124 + w13=w13,
125 + w2=w2,
126 + w13_scale=w13_scale,
127 + w2_scale=w2_scale,
128 + )
129 +
130 + assert converted is not None
131 + assert torch.equal(converted[0], w13.transpose(1, 2).contiguous().view(torch.uint8))
132 + assert torch.equal(converted[1], w2.transpose(1, 2).contiguous().view(torch.uint8))
133 + assert torch.equal(converted[2], w13_scale.transpose(1, 2).contiguous())
134 + assert torch.equal(converted[3], w2_scale.transpose(1, 2).contiguous())
135 +
136 +
137 +def test_moe_wna16_setup_forwards_selected_backend(monkeypatch):
138 method = object.__new__(MoeWNA16Method)
28 - method.moe = SimpleNamespace(disable_inplace=False)
29 - method.moe_quant_config = object()
30 - layer = SimpleNamespace(
31 - w13_qweight=torch.empty(1, 2),
32 - w2_qweight=torch.empty(1, 2),
33 - activation=MoEActivation.GELU_TANH,
34 - apply_router_weight_on_input=False,
35 - global_num_experts=1,
36 - expert_map=None,
139 + method.experts_cls = object
140 + method.wna16_backend = WNA16MoEBackend.HUMMING
141 + method.moe = object()
142 + quant_config = object()
143 + method.get_fused_moe_quant_config = lambda layer: quant_config
144 + layer = SimpleNamespace(_expert_routing_tables=lambda: (None, None, None))
145 + captured = {}
146 + kernel = object()
147 +
148 + def fake_make_wna16_moe_kernel(**kwargs):
149 + captured.update(kwargs)
150 + return kernel
151 +
152 + monkeypatch.setattr(moe_wna16, "make_wna16_moe_kernel", fake_make_wna16_moe_kernel)
153 +
154 + method._setup_kernel(layer)
155 +
156 + assert method.moe_kernel is kernel
157 + assert captured["backend"] == WNA16MoEBackend.HUMMING
158 + assert captured["layer"] is layer
159 +
160 +
161 +def test_moe_wna16_humming_adapter_repacks_uint8_tensors():
162 + qweight = torch.arange(32, dtype=torch.uint8).reshape(1, 4, 8)
163 + scales = torch.arange(16, dtype=torch.float16).reshape(1, 4, 4)
164 + qzeros = torch.arange(16, dtype=torch.uint8).reshape(1, 8, 2)
165 +
166 + converted = _convert_moe_wna16_humming_tensors(
167 + {"qweight": qweight, "scales": scales, "qzeros": qzeros},
168 + has_zero_point=True,
169 )
170
39 - output = method.apply(
40 - layer,
41 - x=torch.empty(1, 2),
42 - topk_weights=torch.empty(1, 1),
43 - topk_ids=torch.empty(1, 1, dtype=torch.int32),
44 - shared_experts=None,
45 - shared_experts_input=None,
171 + assert torch.equal(converted["weight"], qweight.view(torch.int32))
172 + assert converted["weight"].shape == (1, 4, 2)
173 + assert torch.equal(converted["weight_scale"], scales)
174 + expected_qzeros = (
175 + qzeros.transpose(-1, -2)
176 + .contiguous()
177 + .view(torch.int32)
178 + .transpose(-1, -2)
179 + .contiguous()
180 + )
181 + assert torch.equal(converted["zero_point"], expected_qzeros)
182 + assert converted["zero_point"].shape == (1, 2, 2)
183 +
184 +
185 +def test_moe_wna16_uses_humming_quant_config(monkeypatch):
186 + from vllm.model_executor.layers.quantization.utils import humming_utils
187 +
188 + method = object.__new__(MoeWNA16Method)
189 + method.wna16_backend = WNA16MoEBackend.HUMMING
190 + layer = object()
191 + quant_config = object()
192 + monkeypatch.setattr(
193 + humming_utils,
194 + "get_humming_moe_quant_config",
195 + lambda actual_layer: quant_config if actual_layer is layer else None,
196 )
197
48 - assert output.shape == (1, 2)
49 - assert captured_kwargs["activation"] is MoEActivation.GELU_TANH
198 + assert method.get_fused_moe_quant_config(layer) is quant_config
vllm/model_executor/layers/fused_moe/experts/triton_moe.py
+30 -20
@@ -43,7 +43,10 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
43 kFp8Static128BlockSym,
44 kFp8StaticChannelSym,
45 kFp8StaticTensorSym,
46 + kInt4Static,
47 + kInt4Static32,
48 kInt8DynamicTokenSym,
49 + kInt8Static,
50 kInt8StaticChannelSym,
51 )
52 from vllm.platforms import current_platform
@@ -542,40 +545,45 @@ class TritonExperts(LoRAExpertsMixin, mk.FusedMoEExpertsModular):
545 class TritonWNA16Experts(TritonExperts):
546 @staticmethod
547 def _supports_current_device() -> bool:
545 - raise NotImplementedError(
546 - "TritonWNA16Experts is not yet used by an Oracle. "
547 - "This method should not be called."
548 - )
548 + return current_platform.is_cuda_alike() or current_platform.is_xpu()
549
550 @staticmethod
551 def _supports_no_act_and_mul() -> bool:
552 - raise NotImplementedError(
553 - "TritonWNA16Experts is not yet used by an Oracle. "
554 - "This method should not be called."
555 - )
552 + return True
553
554 @staticmethod
555 def _supports_quant_scheme(
556 weight_key: QuantKey | None,
557 activation_key: QuantKey | None,
558 ) -> bool:
562 - raise NotImplementedError(
563 - "TritonWNA16Experts is not yet used by an Oracle. "
564 - "This method should not be called."
565 - )
559 + SUPPORTED_W = [
560 + kInt4Static,
561 + kInt8Static,
562 + kInt4Static32,
563 + # other group sizes?
564 + ]
565 + return weight_key in SUPPORTED_W
566
567 @staticmethod
568 def _supports_activation(activation: MoEActivation) -> bool:
569 - raise NotImplementedError(
570 - "TritonWNA16Experts is not yet used by an Oracle. "
571 - "This method should not be called."
572 - )
569 + return activation in [
570 + MoEActivation.SILU,
571 + MoEActivation.GELU,
572 + MoEActivation.GELU_TANH,
573 + MoEActivation.SWIGLUOAI,
574 + MoEActivation.SWIGLUSTEP,
575 + MoEActivation.SILU_NO_MUL,
576 + MoEActivation.GELU_NO_MUL,
577 + MoEActivation.GELU_TANH_NO_MUL,
578 + MoEActivation.RELU2_NO_MUL,
579 + ]
580
581 @staticmethod
582 def _supports_parallel_config(moe_parallel_config: FusedMoEParallelConfig) -> bool:
576 - raise NotImplementedError(
577 - "TritonWNA16Experts is not yet used by an Oracle. "
578 - "This method should not be called."
583 + # Why?
584 + return not (
585 + moe_parallel_config.use_fi_nvl_two_sided_kernels
586 + or moe_parallel_config.use_fi_nvl_one_sided_kernels
587 )
588
589 def apply(
@@ -598,7 +606,9 @@ class TritonWNA16Experts(TritonExperts):
606 ):
607 # Check constraints.
608 if self.quant_config.use_int4_w4a16:
601 - assert hidden_states.size(-1) // 2 == w1.size(2), "Hidden size mismatch"
609 + assert hidden_states.size(-1) // 2 == w1.size(2), (
610 + f"Hidden size mismatch {hidden_states.size(-1) // 2} == {w1.size(2)}"
611 + )
612 else:
613 assert hidden_states.size(-1) == w1.size(2), (
614 f"Hidden size mismatch {hidden_states.size(-1)} != {w1.size(2)}"
vllm/model_executor/layers/fused_moe/fused_moe.py
+6 -1
@@ -31,6 +31,7 @@ from vllm.model_executor.layers.fused_moe.utils import (
31 )
32 from vllm.platforms import current_platform
33 from vllm.triton_utils import tl, triton
34 +from vllm.utils.math_utils import next_power_of_2
35 from vllm.utils.platform_utils import get_device_name_as_file_name
36 from vllm.utils.torch_utils import direct_register_custom_op
37
@@ -1298,7 +1299,11 @@ def get_default_config(
1299 bit = 4 if dtype == "int4_w4a16" else 8
1300 use_moe_wna16_cuda = should_moe_wna16_use_cuda(M * topk, block_shape[1], E, bit)
1301 if use_moe_wna16_cuda:
1301 - config = {"BLOCK_SIZE_M": min(16, M), "SPLIT_K": 1}
1302 + config = {
1303 + "BLOCK_SIZE_M": min(16, next_power_of_2(M)),
1304 + "GROUP_SIZE_M": 1,
1305 + "SPLIT_K": 1,
1306 + }
1307 elif M <= 20:
1308 config = {"BLOCK_SIZE_M": 16, "GROUP_SIZE_M": 1, "SPLIT_K": 1}
1309 elif M <= 40:
vllm/model_executor/layers/fused_moe/oracle/int_wna16.py
+231 -71
@@ -24,6 +24,9 @@ from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
24 MarlinExperts,
25 MarlinExpertsBase,
26 )
27 +from vllm.model_executor.layers.fused_moe.experts.triton_moe import (
28 + TritonWNA16Experts,
29 +)
30 from vllm.model_executor.layers.fused_moe.experts.trtllm_mxint4_moe import (
31 TrtLlmMxint4ExpertsMonolithic,
32 )
@@ -50,6 +53,7 @@ class WNA16MoEBackend(Enum):
53 HUMMING = "HUMMING"
54 CPU = "CPU"
55 FLASHINFER_TRTLLM = "FLASHINFER_TRTLLM"
56 + TRITON = "TRITON"
57 XPU = "XPU"
58 EMULATION = "EMULATION"
59
@@ -76,6 +80,8 @@ def backend_to_kernel_cls(
80 return [BatchedMarlinExperts]
81 elif backend == WNA16MoEBackend.FLASHINFER_TRTLLM:
82 return [TrtLlmMxint4ExpertsMonolithic]
83 + elif backend == WNA16MoEBackend.TRITON:
84 + return [TritonWNA16Experts]
85 elif backend == WNA16MoEBackend.XPU:
86 from vllm.model_executor.layers.fused_moe.experts.xpu_moe import (
87 XPUExpertsWNA16,
@@ -107,19 +113,56 @@ def _get_priority_backends() -> list[WNA16MoEBackend]:
113 if current_platform.is_xpu():
114 return [WNA16MoEBackend.XPU]
115
110 - _AVAILABLE_BACKENDS = [
116 + return [
117 WNA16MoEBackend.FLASHINFER_TRTLLM,
118 WNA16MoEBackend.MARLIN,
119 WNA16MoEBackend.BATCHED_MARLIN,
120 + WNA16MoEBackend.TRITON,
121 WNA16MoEBackend.HUMMING,
122 WNA16MoEBackend.EMULATION,
123 ]
117 - return _AVAILABLE_BACKENDS
124 +
125 +
126 +def _backend_incompatibility_reason(
127 + backend: WNA16MoEBackend,
128 + quant_config: QuantizationConfig | QuantizationArgs,
129 + may_have_zp: bool,
130 + may_have_bias: bool,
131 +) -> str | None:
132 + if backend == WNA16MoEBackend.FLASHINFER_TRTLLM and (may_have_zp or may_have_bias):
133 + return "zero points and bias are not supported"
134 +
135 + from vllm.model_executor.layers.quantization.auto_awq import AutoAWQConfig
136 + from vllm.model_executor.layers.quantization.auto_gptq import AutoGPTQConfig
137 + from vllm.model_executor.layers.quantization.moe_wna16 import MoeWNA16Config
138 +
139 + if backend == WNA16MoEBackend.TRITON:
140 + if may_have_bias:
141 + return "expert bias is not supported"
142 + if isinstance(quant_config, AutoAWQConfig):
143 + return "the AutoAWQ weight layout is not supported"
144 + if isinstance(quant_config, AutoGPTQConfig) and quant_config.desc_act:
145 + return "GPTQ activation ordering is not supported"
146 + if (
147 + isinstance(quant_config, QuantizationArgs)
148 + and quant_config.actorder == "group"
149 + ):
150 + return "group activation ordering is not supported"
151 +
152 + if isinstance(quant_config, MoeWNA16Config) and backend in (
153 + WNA16MoEBackend.MARLIN,
154 + WNA16MoEBackend.BATCHED_MARLIN,
155 + WNA16MoEBackend.EMULATION,
156 + ):
157 + return "the MoeWNA16 checkpoint layout is not supported"
158 +
159 + return None
160
161
162 def map_wna16_backend(runner_backend: MoEBackend) -> WNA16MoEBackend:
163 """Map user's MoEBackend to WNA16MoEBackend."""
164 mapping = {
165 + "triton": WNA16MoEBackend.TRITON,
166 "marlin": WNA16MoEBackend.MARLIN,
167 "humming": WNA16MoEBackend.HUMMING,
168 "flashinfer_trtllm": WNA16MoEBackend.FLASHINFER_TRTLLM,
@@ -136,6 +179,9 @@ def map_wna16_backend(runner_backend: MoEBackend) -> WNA16MoEBackend:
179 def select_wna16_moe_backend(
180 config: FusedMoEConfig,
181 weight_key: QuantKey,
182 + quant_config: QuantizationConfig | QuantizationArgs,
183 + may_have_zp: bool,
184 + may_have_bias: bool,
185 ) -> tuple[WNA16MoEBackend, type[mk.FusedMoEExperts]]:
186 """Select the WNA16 MoE backend.
187
@@ -143,6 +189,9 @@ def select_wna16_moe_backend(
189 config: the shared ``FusedMoEConfig`` for this layer.
190 weight_key: The QuantKey describing the weight quantization.
191 Must have int4 or int8 type.
192 + quant_config: Quantization structure and checkpoint format description.
193 + may_have_zp: Whether the integration can provide weight zero points.
194 + may_have_bias: Whether the integration can provide expert bias.
195
196 Returns:
197 A tuple of (``WNA16MoEBackend``, experts class or ``None``).
@@ -189,6 +238,11 @@ def select_wna16_moe_backend(
238 runner_backend = config.moe_backend
239 if runner_backend != "auto":
240 requested_backend = map_wna16_backend(runner_backend)
241 + reason = _backend_incompatibility_reason(
242 + requested_backend, quant_config, may_have_zp, may_have_bias
243 + )
244 + if reason is not None:
245 + raise ValueError(_make_log_unsupported(requested_backend, reason))
246 return _return_or_raise(
247 requested_backend, config, weight_key, None, activation_format
248 )
@@ -197,6 +251,12 @@ def select_wna16_moe_backend(
251 AVAILABLE_BACKENDS = _get_priority_backends()
252
253 for backend in AVAILABLE_BACKENDS:
254 + reason = _backend_incompatibility_reason(
255 + backend, quant_config, may_have_zp, may_have_bias
256 + )
257 + if reason is not None:
258 + logger.debug_once(_make_log_unsupported(backend, reason), scope="local")
259 + continue
260 activation_key = None # always BF16 activation for WNA16 MoE
261 for k_cls in backend_to_kernel_cls(backend):
262 supported, reason = k_cls.is_supported_config(
@@ -294,6 +354,7 @@ def make_wna16_moe_kernel(
354 allowed_experts: tuple[type[mk.FusedMoEExperts], ...] = (
355 MarlinExperts,
356 BatchedMarlinExperts,
357 + TritonWNA16Experts,
358 TrtLlmMxint4ExpertsMonolithic,
359 XPUExpertsWNA16,
360 CPUExpertsInt4,
@@ -315,6 +376,7 @@ def make_wna16_moe_kernel(
376 assert prepare_finalize is not None
377
378 logger.info_once("Using %s", prepare_finalize.__class__.__name__, scope="local")
379 + logger.info_once("Using %s", experts_cls.__name__, scope="local")
380
381 extra_args: dict[str, Any] = {}
382 if backend == WNA16MoEBackend.HUMMING:
@@ -329,35 +391,17 @@ def make_wna16_moe_kernel(
391 "is_k_full": is_k_full,
392 }
393
332 - if experts_cls is XPUExpertsWNA16:
333 - assert (
334 - prepare_finalize.activation_format == mk.FusedMoEActivationFormat.Standard
335 - ), (
336 - "XPUExpertsWNA16 only supports the Standard activation format; "
337 - "xpu_fused_moe(is_int4=True) does not implement BatchedExperts."
338 - )
339 - experts: mk.FusedMoEExperts = XPUExpertsWNA16(
340 - moe_config=moe_config,
341 - quant_config=moe_quant_config,
342 - )
343 - elif (
344 - prepare_finalize.activation_format == mk.FusedMoEActivationFormat.BatchedExperts
345 - ):
394 + if prepare_finalize.activation_format == mk.FusedMoEActivationFormat.BatchedExperts:
395 max_num_tokens = prepare_finalize.max_num_tokens_per_rank()
396 assert max_num_tokens is not None
348 - experts = experts_cls(
349 - max_num_tokens=max_num_tokens,
350 - num_dispatchers=prepare_finalize.num_dispatchers(),
351 - moe_config=moe_config,
352 - quant_config=moe_quant_config,
353 - **extra_args,
354 - )
355 - else:
356 - experts = experts_cls(
357 - moe_config=moe_config,
358 - quant_config=moe_quant_config,
359 - **extra_args,
360 - )
397 + extra_args["max_num_tokens"] = max_num_tokens
398 + extra_args["num_dispatchers"] = prepare_finalize.num_dispatchers()
399 +
400 + experts = experts_cls(
401 + moe_config=moe_config,
402 + quant_config=moe_quant_config,
403 + **extra_args,
404 + )
405
406 return mk.FusedMoEKernel(
407 prepare_finalize,
@@ -1034,11 +1078,68 @@ def _humming_wna16_weight_schema(
1078 "sym": quant_config.is_sym,
1079 }
1080 raise TypeError(
1037 - "Humming WNA16 MoE requires AutoAWQConfig or AutoGPTQConfig, "
1081 + "Humming WNA16 checkpoint schema requires AutoAWQConfig or "
1082 + "AutoGPTQConfig, "
1083 f"got {type(quant_config).__name__}."
1084 )
1085
1086
1087 +def _convert_moe_wna16_humming_tensors(
1088 + tensors: dict[str, torch.Tensor], has_zero_point: bool
1089 +) -> dict[str, torch.Tensor]:
1090 + """Convert MoeWNA16's N-first uint8 packing to Humming's int32 packing."""
1091 + if sys.byteorder != "little":
1092 + raise NotImplementedError(
1093 + "MoeWNA16 to Humming conversion requires a little-endian host."
1094 + )
1095 +
1096 + output = {
1097 + "weight": tensors["qweight"].contiguous().view(torch.int32),
1098 + "weight_scale": tensors["scales"],
1099 + }
1100 + if has_zero_point:
1101 + qzeros = tensors["qzeros"]
1102 + output["zero_point"] = (
1103 + qzeros.transpose(-1, -2)
1104 + .contiguous()
1105 + .view(torch.int32)
1106 + .transpose(-1, -2)
1107 + .contiguous()
1108 + )
1109 + return output
1110 +
1111 +
1112 +class _MoeWNA16HummingWeightSchema:
1113 + """Adapter from MoeWNA16's generic packed layout to Humming's layout."""
1114 +
1115 + def __init__(self, bits: int, group_size: int, has_zero_point: bool) -> None:
1116 + self.bits = bits
1117 + self.group_size = group_size
1118 + self.has_zero_point = has_zero_point
1119 +
1120 + def convert_humming(
1121 + self,
1122 + tensors: dict[str, torch.Tensor],
1123 + shape_n_stacks: list[int],
1124 + shape_k_stacks: list[int],
1125 + param_dtype: torch.dtype,
1126 + num_experts: int | None = None,
1127 + ) -> tuple[Any, dict[str, torch.Tensor]]:
1128 + del shape_n_stacks, shape_k_stacks, num_experts
1129 + from vllm.utils.humming import HummingWeightSchema, dtypes
1130 +
1131 + output = _convert_moe_wna16_humming_tensors(
1132 + tensors, has_zero_point=self.has_zero_point
1133 + )
1134 + output["weight_scale"] = output["weight_scale"].to(param_dtype)
1135 + schema = HummingWeightSchema(
1136 + b_dtype=dtypes.DataType.from_str(f"uint{self.bits}"),
1137 + weight_scale_group_size=self.group_size,
1138 + has_zero_point=self.has_zero_point,
1139 + )
1140 + return schema, output
1141 +
1142 +
1143 def _unpack_and_dequant_int4_gptq(
1144 w_int32: torch.Tensor,
1145 scale: torch.Tensor,
@@ -1323,13 +1424,27 @@ def convert_to_wna16_moe_kernel_format(
1424 input_dtype: optional activation dtype, usually should be 16 bit.
1425 """
1426 if backend == WNA16MoEBackend.HUMMING:
1427 + from vllm.model_executor.layers.quantization.moe_wna16 import MoeWNA16Config
1428 from vllm.model_executor.layers.quantization.utils.humming_utils import (
1429 convert_to_humming_moe_kernel_format,
1430 )
1431
1330 - convert_to_humming_moe_kernel_format(
1331 - layer, quant_config=_humming_wna16_weight_schema(quant_config)
1332 - )
1432 + if isinstance(quant_config, MoeWNA16Config):
1433 + from vllm.utils.humming import HummingInputSchema
1434 +
1435 + convert_to_humming_moe_kernel_format(
1436 + layer,
1437 + weight_schema=_MoeWNA16HummingWeightSchema(
1438 + bits=quant_config.weight_bits,
1439 + group_size=layer.group_size,
1440 + has_zero_point=quant_config.has_zp,
1441 + ),
1442 + input_schema=HummingInputSchema(),
1443 + )
1444 + else:
1445 + convert_to_humming_moe_kernel_format(
1446 + layer, quant_config=_humming_wna16_weight_schema(quant_config)
1447 + )
1448 return None
1449
1450 if backend in (
@@ -1344,16 +1459,32 @@ def convert_to_wna16_moe_kernel_format(
1459 )
1460
1461 if isinstance(quant_config, AutoAWQConfig):
1347 - if w13_qzeros is None or w2_qzeros is None:
1348 - raise ValueError("AWQ Marlin MoE requires zero-point tensors.")
1349 -
1350 - weight_bits = quant_config.weight_bits
1462 + num_bits = quant_config.weight_bits
1463 pack_factor = quant_config.pack_factor
1464 group_size = quant_config.group_size
1465 + elif isinstance(quant_config, AutoGPTQConfig):
1466 + num_bits = quant_config.quant_type.size_bits
1467 + pack_factor = quant_config.pack_factor
1468 + group_size = quant_config.group_size
1469 + actorder = "group" if quant_config.desc_act else None
1470 + elif isinstance(quant_config, QuantizationArgs):
1471 + num_bits = quant_config.num_bits
1472 + pack_factor = 32 // quant_config.num_bits
1473 + group_size = quant_config.group_size
1474 + actorder = quant_config.actorder
1475 + else:
1476 + raise TypeError(
1477 + "Marlin WNA16 MoE backend requires AutoGPTQConfig, AutoAWQConfig or "
1478 + f"QuantizationArgs, got {type(quant_config).__name__}."
1479 + )
1480 +
1481 + if isinstance(quant_config, AutoAWQConfig):
1482 + if w13_qzeros is None or w2_qzeros is None:
1483 + raise ValueError("AWQ Marlin MoE requires zero-point tensors.")
1484
1485 return _process_awq_weights_marlin(
1486 layer,
1356 - weight_bits,
1487 + num_bits,
1488 pack_factor,
1489 group_size,
1490 input_dtype,
@@ -1366,41 +1497,28 @@ def convert_to_wna16_moe_kernel_format(
1497 w13_bias,
1498 w2_bias,
1499 )
1369 - elif isinstance(quant_config, AutoGPTQConfig):
1370 - num_bits = quant_config.quant_type.size_bits
1371 - pack_factor = quant_config.pack_factor
1372 - group_size = quant_config.group_size
1373 - actorder = "group" if quant_config.desc_act else None
1374 - elif isinstance(quant_config, QuantizationArgs):
1375 - num_bits = quant_config.num_bits
1376 - pack_factor = 32 // quant_config.num_bits
1377 - group_size = quant_config.group_size
1378 - actorder = quant_config.actorder
1500 else:
1380 - raise TypeError(
1381 - "Marlin WNA16 MoE backend requires AutoAWQConfig, AutoGPTQConfig or "
1382 - f"QuantizationArgs, got {type(quant_config).__name__}."
1501 + if w13_g_idx is None or w2_g_idx is None:
1502 + raise ValueError("GPTQ Marlin MoE requires g_idx tensors.")
1503 +
1504 + return _process_weights_marlin(
1505 + layer,
1506 + input_dtype,
1507 + num_bits,
1508 + pack_factor,
1509 + group_size,
1510 + actorder,
1511 + w13,
1512 + w2,
1513 + w13_scale,
1514 + w2_scale,
1515 + w13_g_idx,
1516 + w2_g_idx,
1517 + w13_qzeros,
1518 + w2_qzeros,
1519 + w13_bias,
1520 + w2_bias,
1521 )
1384 - if w13_g_idx is None or w2_g_idx is None:
1385 - raise ValueError("GPTQ Marlin MoE requires g_idx tensors.")
1386 - return _process_weights_marlin(
1387 - layer,
1388 - input_dtype,
1389 - num_bits,
1390 - pack_factor,
1391 - group_size,
1392 - actorder,
1393 - w13,
1394 - w2,
1395 - w13_scale,
1396 - w2_scale,
1397 - w13_g_idx,
1398 - w2_g_idx,
1399 - w13_qzeros,
1400 - w2_qzeros,
1401 - w13_bias,
1402 - w2_bias,
1403 - )
1522 elif backend == WNA16MoEBackend.CPU:
1523 return _process_weights_cpu(
1524 quant_config,
@@ -1482,5 +1600,47 @@ def convert_to_wna16_moe_kernel_format(
1600 w13_qzeros,
1601 w2_qzeros,
1602 )
1603 + elif backend == WNA16MoEBackend.TRITON:
1604 + # Two possible input layouts depending on the quantization source:
1605 + #
1606 + # MoeWNA16 (uint8): (E, N_out, K // bit8_pack) — N-first
1607 + # → just view as uint8 (no-op)
1608 + #
1609 + # AutoGPTQ/compressed-tensors (int32, K-first):
1610 + # (E, K // pack32, N_out)
1611 + # → transpose to N-first, then view as uint8 to get
1612 + # (E, N_out, K // bit8_pack) [int32 = 4 bytes → 4 uint8s]
1613 + # Scales: (E, K // gs, N_out) → transpose → (E, N_out, K // gs)
1614 + from vllm.model_executor.layers.quantization.auto_gptq import (
1615 + AutoGPTQConfig,
1616 + )
1617 +
1618 + if isinstance(quant_config, (AutoGPTQConfig, QuantizationArgs)):
1619 + # These integrations build in K-first format even when the Triton
1620 + # backend is selected. Transpose to N-first first.
1621 + w13_uint8 = w13.transpose(1, 2).contiguous().view(torch.uint8)
1622 + w2_uint8 = w2.transpose(1, 2).contiguous().view(torch.uint8)
1623 + w13_scale = w13_scale.transpose(1, 2).contiguous()
1624 + w2_scale = w2_scale.transpose(1, 2).contiguous()
1625 + else:
1626 + # MoeWNA16 uses N-first uint8 weights and scales.
1627 + w13_uint8 = w13.view(torch.uint8)
1628 + w2_uint8 = w2.view(torch.uint8)
1629 + return (
1630 + w13_uint8,
1631 + w2_uint8,
1632 + w13_scale,
1633 + w2_scale,
1634 + None,
1635 + None,
1636 + None,
1637 + None,
1638 + w13_qzeros,
1639 + w2_qzeros,
1640 + None,
1641 + None,
1642 + w13_bias,
1643 + w2_bias,
1644 + )
1645 else:
1646 raise ValueError(f"Unsupported wna16 MoE backend: {backend.value}")
vllm/model_executor/layers/fused_moe/routed_experts.py
+20
@@ -114,6 +114,7 @@ class RoutedExperts(PluggableLayer):
114 self.e_score_correction_bias = e_score_correction_bias
115 self.apply_router_weight_on_input = apply_router_weight_on_input
116 # End random parameters
117 + self._loaded_expert_biases: set[str] = set()
118
119 self.quant_method = self._get_quant_method(
120 self.layer_name,
@@ -691,6 +692,25 @@ class RoutedExperts(PluggableLayer):
692
693 expert_data = param.data if full_load else param.data[expert_id]
694
695 + if "bias" in weight_name:
696 + self._loaded_expert_biases.add(weight_name.rsplit(".", 1)[-1])
697 + if shard_id == "w2":
698 + expert_data = self._narrow_expert_data_for_padding(
699 + expert_data,
700 + loaded_weight,
701 + hidden_dim=0,
702 + )
703 + expert_data.copy_(loaded_weight)
704 + else:
705 + self._load_w13(
706 + shard_id=shard_id,
707 + shard_dim=0,
708 + loaded_weight=loaded_weight,
709 + expert_data=expert_data,
710 + tp_rank=self.moe_config.tp_rank,
711 + )
712 + return True if return_success else None
713 +
714 # Case input scale: input_scale loading is only supported for fp8
715 if "input_scale" in weight_name:
716 # this is needed for compressed-tensors only
vllm/model_executor/layers/quantization/auto_awq.py
+3
@@ -560,6 +560,9 @@ class AutoAWQMoEMethod(FusedMoEMethodBase):
560 self.wna16_moe_backend, self.experts_cls = select_wna16_moe_backend(
561 moe,
562 kInt4Static,
563 + quant_config=self.quant_config,
564 + may_have_zp=self.quant_config.zero_point,
565 + may_have_bias=True,
566 )
567
568 def create_weights(
vllm/model_executor/layers/quantization/auto_gptq.py
+60 -46
@@ -489,6 +489,9 @@ class AutoGPTQMoEMethod(FusedMoEMethodBase):
489 self.wna16_moe_backend, self.experts_cls = select_wna16_moe_backend(
490 moe,
491 weight_key,
492 + quant_config=self.quant_config,
493 + may_have_zp=True,
494 + may_have_bias=True,
495 )
496
497 def create_weights(
@@ -577,25 +580,25 @@ class AutoGPTQMoEMethod(FusedMoEMethodBase):
580 set_weight_attrs(w2_scales, extra_weight_attrs)
581 # don't shard the w2 scales when running act order
582 set_weight_attrs(w2_scales, {"load_full_w2": self.quant_config.desc_act})
580 - # up_proj scales
583 + # up_proj zero points
584 w13_qzeros = torch.nn.Parameter(
585 torch.empty(
586 num_experts,
587 scales_size13,
588 2 * intermediate_size_per_partition // self.quant_config.pack_factor,
586 - dtype=params_dtype,
589 + dtype=torch.int32,
590 ),
591 requires_grad=False,
592 )
593 layer.register_parameter("w13_qzeros", w13_qzeros)
594 set_weight_attrs(w13_qzeros, extra_weight_attrs)
592 - # down_proj scales
595 + # down_proj zero points
596 w2_qzeros = torch.nn.Parameter(
597 torch.empty(
598 num_experts,
599 scales_size2,
600 hidden_size // self.quant_config.pack_factor,
598 - dtype=params_dtype,
601 + dtype=torch.int32,
602 ),
603 requires_grad=False,
604 )
@@ -644,6 +647,26 @@ class AutoGPTQMoEMethod(FusedMoEMethodBase):
647 layer.register_parameter("w2_g_idx_sort_indices", w2_g_idx_sort_indices)
648 set_weight_attrs(w2_g_idx_sort_indices, extra_weight_attrs)
649
650 + # Some GPTQ checkpoints contain expert biases even when the model
651 + # architecture does not declare them. Zero initialization keeps
652 + # checkpoints without biases equivalent to the bias-free path.
653 + w13_bias = torch.nn.Parameter(
654 + torch.zeros(
655 + num_experts,
656 + 2 * intermediate_size_per_partition,
657 + dtype=params_dtype,
658 + ),
659 + requires_grad=False,
660 + )
661 + layer.register_parameter("w13_bias", w13_bias)
662 + set_weight_attrs(w13_bias, extra_weight_attrs)
663 + w2_bias = torch.nn.Parameter(
664 + torch.zeros(num_experts, hidden_size, dtype=params_dtype),
665 + requires_grad=False,
666 + )
667 + layer.register_parameter("w2_bias", w2_bias)
668 + set_weight_attrs(w2_bias, extra_weight_attrs)
669 +
670 if self.experts_cls is not None and issubclass(
671 self.experts_cls, FusedMoEExpertsModular
672 ):
@@ -651,12 +674,31 @@ class AutoGPTQMoEMethod(FusedMoEMethodBase):
674 layer.workspace = marlin_make_workspace_new(device, 4)
675
676 def process_weights_after_loading(self, layer: RoutedExperts) -> None:
677 + def replace_or_register(name: str, val: torch.Tensor | None):
678 + if val is None:
679 + return
680 +
681 + if hasattr(layer, name):
682 + replace_parameter(layer, name, val)
683 + else:
684 + layer.register_parameter(
685 + name, torch.nn.Parameter(val, requires_grad=False)
686 + )
687 +
688 is_a_8bit = self.input_dtype is not None and self.input_dtype.itemsize == 1
689
656 - if is_a_8bit:
657 - assert self.quant_config.quant_type.size_bits == 8, (
658 - "W8A8-INT8 is not supported by marlin kernel."
659 - )
690 + assert not is_a_8bit or self.quant_config.quant_type.size_bits == 8, (
691 + "W8A8-INT8 is not supported by marlin kernel."
692 + )
693 +
694 + w13_bias = getattr(layer, "w13_bias", None)
695 + if "w13_bias" not in layer._loaded_expert_biases:
696 + layer.register_parameter("w13_bias", None)
697 + w13_bias = None
698 + w2_bias = getattr(layer, "w2_bias", None)
699 + if "w2_bias" not in layer._loaded_expert_biases:
700 + layer.register_parameter("w2_bias", None)
701 + w2_bias = None
702
703 converted = convert_to_wna16_moe_kernel_format(
704 backend=self.wna16_moe_backend,
@@ -669,8 +711,10 @@ class AutoGPTQMoEMethod(FusedMoEMethodBase):
711 w2_scale=layer.w2_scales,
712 w13_g_idx=layer.w13_g_idx,
713 w2_g_idx=layer.w2_g_idx,
672 - w13_bias=getattr(layer, "w13_bias", None),
673 - w2_bias=getattr(layer, "w2_bias", None),
714 + w13_bias=w13_bias,
715 + w2_bias=w2_bias,
716 + w13_qzeros=getattr(layer, "w13_qzeros", None),
717 + w2_qzeros=getattr(layer, "w2_qzeros", None),
718 )
719
720 if converted is None:
@@ -703,42 +747,12 @@ class AutoGPTQMoEMethod(FusedMoEMethodBase):
747 replace_parameter(layer, "w2_g_idx", w2_g_idx)
748 replace_parameter(layer, "w13_g_idx_sort_indices", w13_g_idx_sort_indices)
749 replace_parameter(layer, "w2_g_idx_sort_indices", w2_g_idx_sort_indices)
706 - if w13_qzeros is not None:
707 - replace_parameter(layer, "w13_qzeros", w13_qzeros)
708 - if w2_qzeros is not None:
709 - replace_parameter(layer, "w2_qzeros", w2_qzeros)
710 - if w13_input_global_scale is not None:
711 - if hasattr(layer, "w13_input_global_scale"):
712 - replace_parameter(
713 - layer, "w13_input_global_scale", w13_input_global_scale
714 - )
715 - else:
716 - layer.register_parameter(
717 - "w13_input_global_scale",
718 - torch.nn.Parameter(w13_input_global_scale, requires_grad=False),
719 - )
720 - if w2_input_global_scale is not None:
721 - if hasattr(layer, "w2_input_global_scale"):
722 - replace_parameter(layer, "w2_input_global_scale", w2_input_global_scale)
723 - else:
724 - layer.register_parameter(
725 - "w2_input_global_scale",
726 - torch.nn.Parameter(w2_input_global_scale, requires_grad=False),
727 - )
728 - if w13_bias is not None:
729 - if hasattr(layer, "w13_bias"):
730 - replace_parameter(layer, "w13_bias", w13_bias)
731 - else:
732 - layer.register_parameter(
733 - "w13_bias", torch.nn.Parameter(w13_bias, requires_grad=False)
734 - )
735 - if w2_bias is not None:
736 - if hasattr(layer, "w2_bias"):
737 - replace_parameter(layer, "w2_bias", w2_bias)
738 - else:
739 - layer.register_parameter(
740 - "w2_bias", torch.nn.Parameter(w2_bias, requires_grad=False)
741 - )
750 + replace_or_register("w13_input_global_scale", w13_input_global_scale)
751 + replace_or_register("w2_input_global_scale", w2_input_global_scale)
752 + replace_or_register("w13_bias", w13_bias)
753 + replace_or_register("w2_bias", w2_bias)
754 + replace_or_register("w13_qzeros", w13_qzeros)
755 + replace_or_register("w2_qzeros", w2_qzeros)
756
757 # The modular kernel reads w13_weight/w2_weight; marlin keeps *_qweight.
758 layer.w13_weight = layer.w13_qweight
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_wna16_marlin.py
+14 -7
@@ -92,7 +92,15 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
92 self.wna16_backend, self.experts_cls = select_wna16_moe_backend(
93 config=self.moe,
94 weight_key=weight_key,
95 + quant_config=self.weight_quant,
96 + may_have_zp=not self.symmetric,
97 + may_have_bias=False,
98 )
99 + self.is_marlin = self.wna16_backend in [
100 + WNA16MoEBackend.MARLIN,
101 + WNA16MoEBackend.BATCHED_MARLIN,
102 + ]
103 + self.is_transposed = self.wna16_backend != WNA16MoEBackend.FLASHINFER_TRTLLM
104
105 def get_weight_shape(
106 self,
@@ -118,7 +126,6 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
126 "num_groups_w2 must be provided for weight scales/zero_points"
127 )
128 w13_num_shards = 2 if self.moe.is_act_and_mul else 1
121 - is_flashinfer = self.wna16_backend == WNA16MoEBackend.FLASHINFER_TRTLLM
129 shape_map = {
130 "w13_weight": {
131 "Flashinfer": (
@@ -177,7 +184,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
184 ),
185 },
186 }
180 - backend_key = "Flashinfer" if is_flashinfer else "Marlin"
187 + backend_key = "Marlin" if self.is_transposed else "Flashinfer"
188 return shape_map[weight_name][backend_key]
189
190 @staticmethod
@@ -217,9 +224,8 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
224 # Will transpose the loaded weight along the
225 # intermediate and hidden dim sizes. Will
226 # shard for TP along the transposed dims
220 - is_transposed = self.wna16_backend != WNA16MoEBackend.FLASHINFER_TRTLLM
227 extra_weight_attrs.update(
222 - {"is_transposed": is_transposed, "quant_method": self.strategy}
228 + {"is_transposed": self.is_transposed, "quant_method": self.strategy}
229 )
230
231 w13_weight = torch.nn.Parameter(
@@ -415,7 +421,6 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
421
422 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
423 # Process weights using the shared oracle infrastructure
418 - is_flashinfer = self.wna16_backend == WNA16MoEBackend.FLASHINFER_TRTLLM
424 converted = convert_to_wna16_moe_kernel_format(
425 backend=self.wna16_backend,
426 layer=layer,
@@ -430,6 +435,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
435 w13_qzeros=getattr(layer, "w13_weight_zero_point", None),
436 w2_qzeros=getattr(layer, "w2_weight_zero_point", None),
437 )
438 +
439 if converted is None:
440 # In-place backends (e.g. Humming) are not wired through this
441 # marlin-only method; fail clearly rather than unpacking None.
@@ -437,6 +443,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
443 f"{type(self).__name__} does not support the "
444 f"{self.wna16_backend.value} MoE backend."
445 )
446 +
447 (
448 w13_qweight,
449 w2_qweight,
@@ -466,7 +473,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
473 replace_parameter(layer, "w2_weight_zero_point", w2_qzeros)
474
475 # Marlin-specific parameters (not needed for Flashinfer)
469 - if not is_flashinfer:
476 + if self.is_marlin:
477 if w13_g_idx_processed is not None:
478 replace_parameter(layer, "w13_weight_g_idx", w13_g_idx_processed)
479 if w2_g_idx_processed is not None:
@@ -510,7 +517,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
517
518 # Add Marlin-specific arguments
519 marlin_args: dict[str, Any] = {}
513 - if not is_flashinfer:
520 + if self.is_marlin:
521 marlin_args = {
522 "w13_g_idx": layer.w13_weight_g_idx,
523 "w2_g_idx": layer.w2_weight_g_idx,
vllm/model_executor/layers/quantization/moe_wna16.py
+166 -19
@@ -15,8 +15,13 @@ from vllm.model_executor.layers.fused_moe import (
15 )
16 from vllm.model_executor.layers.fused_moe.config import (
17 FusedMoEQuantConfig,
18 - int4_w4a16_moe_quant_config,
19 - int8_w8a16_moe_quant_config,
18 +)
19 +from vllm.model_executor.layers.fused_moe.oracle.int_wna16 import (
20 + WNA16MoEBackend,
21 + convert_to_wna16_moe_kernel_format,
22 + make_wna16_moe_kernel,
23 + make_wna16_moe_quant_config,
24 + select_wna16_moe_backend,
25 )
26 from vllm.model_executor.layers.fused_moe.unquantized_fused_moe_method import (
27 UnquantizedFusedMoEMethod,
@@ -27,7 +32,15 @@ from vllm.model_executor.layers.quantization.base_config import (
32 QuantizationConfig,
33 QuantizeMethodBase,
34 )
30 -from vllm.model_executor.utils import set_weight_attrs
35 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
36 + INT4_DTYPE,
37 + INT8_DTYPE,
38 + QuantKey,
39 + kInt4Static32GroupScale,
40 + kInt4StaticGroupScale,
41 + kInt8StaticGroupScale,
42 +)
43 +from vllm.model_executor.utils import replace_parameter, set_weight_attrs
44 from vllm.platforms import current_platform
45
46
@@ -199,6 +212,34 @@ class MoeWNA16Method(FusedMoEMethodBase):
212 super().__init__(moe)
213 self.quant_config = quant_config
214
215 + num_bits = self.quant_config.weight_bits
216 + group_size = self.quant_config.group_size
217 +
218 + if num_bits == 4:
219 + quant_type = INT4_DTYPE
220 + if group_size == 32:
221 + scale = kInt4Static32GroupScale
222 + else:
223 + scale = kInt4StaticGroupScale
224 + elif num_bits == 8:
225 + assert group_size == -1
226 + quant_type = INT8_DTYPE
227 + scale = kInt8StaticGroupScale
228 + else:
229 + raise ValueError("MoeWNA16Method only supports int4 and int8 now.")
230 +
231 + weight_key = QuantKey(quant_type, scale)
232 +
233 + # Select WNA16 MoE backend via oracle.
234 + # handle ZP?
235 + self.wna16_backend, self.experts_cls = select_wna16_moe_backend(
236 + config=self.moe,
237 + weight_key=weight_key,
238 + quant_config=self.quant_config,
239 + may_have_zp=self.quant_config.has_zp,
240 + may_have_bias=False,
241 + )
242 +
243 def create_weights(
244 self,
245 layer: RoutedExperts,
@@ -322,23 +363,104 @@ class MoeWNA16Method(FusedMoEMethodBase):
363 def get_fused_moe_quant_config(
364 self, layer: RoutedExperts
365 ) -> FusedMoEQuantConfig | None:
325 - weight_bits = self.quant_config.weight_bits
326 - has_zp = self.quant_config.has_zp
327 - assert weight_bits == 4 or weight_bits == 8
328 - config_builder = (
329 - int4_w4a16_moe_quant_config
330 - if weight_bits == 4
331 - else int8_w8a16_moe_quant_config
332 - )
366 + if self.wna16_backend == WNA16MoEBackend.HUMMING:
367 + from vllm.model_executor.layers.quantization.utils.humming_utils import (
368 + get_humming_moe_quant_config,
369 + )
370 +
371 + return get_humming_moe_quant_config(layer)
372
334 - return config_builder(
373 + has_zp = self.quant_config.has_zp
374 + return make_wna16_moe_quant_config(
375 w1_scale=layer.w13_scales,
376 w2_scale=layer.w2_scales,
377 w1_zp=layer.w13_qzeros if has_zp else None,
378 w2_zp=layer.w2_qzeros if has_zp else None,
339 - block_shape=[0, layer.group_size],
379 + group_size=layer.group_size,
380 + num_bits=self.quant_config.weight_bits,
381 )
382
383 + def _setup_kernel(self, layer: RoutedExperts):
384 + assert self.experts_cls is not None
385 + self.moe_quant_config = self.get_fused_moe_quant_config(layer)
386 + assert self.moe_quant_config is not None
387 + self.moe_kernel = make_wna16_moe_kernel(
388 + moe_quant_config=self.moe_quant_config,
389 + moe_config=self.moe,
390 + experts_cls=self.experts_cls,
391 + backend=self.wna16_backend,
392 + layer=layer,
393 + routing_tables=layer._expert_routing_tables(),
394 + )
395 +
396 + def process_weights_after_loading(self, layer: RoutedExperts) -> None:
397 + has_zp = self.quant_config.has_zp
398 + converted = convert_to_wna16_moe_kernel_format(
399 + backend=self.wna16_backend,
400 + layer=layer,
401 + quant_config=self.quant_config,
402 + input_dtype=None,
403 + w13=layer.w13_qweight,
404 + w2=layer.w2_qweight,
405 + w13_scale=layer.w13_scales,
406 + w2_scale=layer.w2_scales,
407 + w13_g_idx=None,
408 + w2_g_idx=None,
409 + w13_qzeros=layer.w13_qzeros if has_zp else None,
410 + w2_qzeros=layer.w2_qzeros if has_zp else None,
411 + )
412 +
413 + if converted is None:
414 + # Backend rewrote the layer's params in place (e.g. Humming).
415 + self._setup_kernel(layer)
416 + return
417 +
418 + (
419 + w13_qweight,
420 + w2_qweight,
421 + w13_scales,
422 + w2_scales,
423 + _,
424 + _,
425 + _,
426 + _,
427 + w13_qzeros,
428 + w2_qzeros,
429 + w13_input_global_scale,
430 + w2_input_global_scale,
431 + _, # w13_bias
432 + _, # w2_bias
433 + ) = converted
434 +
435 + # Replace common parameters
436 + replace_parameter(layer, "w13_qweight", w13_qweight)
437 + replace_parameter(layer, "w2_qweight", w2_qweight)
438 + replace_parameter(layer, "w13_scales", w13_scales)
439 + replace_parameter(layer, "w2_scales", w2_scales)
440 + layer.w13_weight = layer.w13_qweight
441 + layer.w2_weight = layer.w2_qweight
442 +
443 + if has_zp:
444 + assert w13_qzeros is not None and w2_qzeros is not None
445 + replace_parameter(layer, "w13_qzeros", w13_qzeros)
446 + replace_parameter(layer, "w2_qzeros", w2_qzeros)
447 +
448 + # Marlin-specific parameters (not needed for Flashinfer)
449 + if self.wna16_backend != WNA16MoEBackend.FLASHINFER_TRTLLM:
450 + # Register input global scales if present
451 + if w13_input_global_scale is not None:
452 + layer.register_parameter(
453 + "w13_input_global_scale",
454 + torch.nn.Parameter(w13_input_global_scale, requires_grad=False),
455 + )
456 + if w2_input_global_scale is not None:
457 + layer.register_parameter(
458 + "w2_input_global_scale",
459 + torch.nn.Parameter(w2_input_global_scale, requires_grad=False),
460 + )
461 +
462 + self._setup_kernel(layer)
463 +
464 def apply(
465 self,
466 layer: RoutedExperts,
@@ -348,19 +470,44 @@ class MoeWNA16Method(FusedMoEMethodBase):
470 shared_experts: SharedExperts | None,
471 shared_experts_input: torch.Tensor | None,
472 ) -> torch.Tensor:
351 - from vllm.model_executor.layers.fused_moe import fused_experts
352 -
353 - return fused_experts(
473 + assert not self.is_monolithic
474 + assert self.moe_kernel is not None
475 + return self.moe_kernel.apply(
476 x,
355 - layer.w13_qweight,
356 - layer.w2_qweight,
477 + layer.w13_weight,
478 + layer.w2_weight,
479 topk_weights=topk_weights,
480 topk_ids=topk_ids,
481 activation=layer.activation,
482 + global_num_experts=layer.global_num_experts,
483 + expert_map=layer.expert_map,
484 apply_router_weight_on_input=layer.apply_router_weight_on_input,
485 + shared_experts=shared_experts,
486 + shared_experts_input=shared_experts_input,
487 + )
488 +
489 + def apply_monolithic(
490 + self,
491 + layer: RoutedExperts,
492 + x: torch.Tensor,
493 + router_logits: torch.Tensor,
494 + input_ids: torch.Tensor | None = None,
495 + ) -> torch.Tensor:
496 + assert self.is_monolithic
497 + assert self.moe_kernel is not None
498 + return self.moe_kernel.apply_monolithic(
499 + x,
500 + layer.w13_weight,
501 + layer.w2_weight,
502 + router_logits,
503 + activation=layer.activation,
504 global_num_experts=layer.global_num_experts,
505 expert_map=layer.expert_map,
363 - quant_config=self.moe_quant_config,
506 + apply_router_weight_on_input=layer.apply_router_weight_on_input,
507 + num_expert_group=layer.num_expert_group,
508 + topk_group=layer.topk_group,
509 + e_score_correction_bias=layer.e_score_correction_bias,
510 + routed_scaling_factor=layer.routed_scaling_factor,
511 )
512
513 @staticmethod
vllm/model_executor/layers/quantization/utils/gptq_utils.py
+17 -1
@@ -3,7 +3,7 @@
3 from collections.abc import Mapping
4 from copy import deepcopy
5 from types import MappingProxyType
6 -from typing import TYPE_CHECKING
6 +from typing import TYPE_CHECKING, Any
7
8 import regex as re
9 import torch
@@ -69,6 +69,20 @@ def get_dynamic_override(
69 return default_value
70
71
72 +def flatten_list(lst: list[Any]) -> list[Any]:
73 + output = []
74 +
75 + def _flatten(lst: list[Any]):
76 + for i in lst:
77 + if isinstance(i, list):
78 + _flatten(i)
79 + else:
80 + output.append(i)
81 +
82 + _flatten(lst)
83 + return output
84 +
85 +
86 def is_layer_gptq_quantized(
87 prefix: str,
88 quantized_layers: list[str],
@@ -83,6 +97,8 @@ def is_layer_gptq_quantized(
97
98 proj_name = prefix.split(".")[-1]
99
100 + quantized_layers = flatten_list(quantized_layers)
101 +
102 # Fused layers like gate_up_proj or qkv_proj will not be fused
103 # in the safetensors checkpoint. So, we convert the name
104 # from the fused version to unfused + check to make sure that