[Quant] Use canonical N-first weight format for CT WNA16 MoE (#52798)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: HDCharles <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/users/emails#list-email-addresses-for-the-authenticated-user","status":"404"}> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: OpenAI Codex <noreply@openai.com>

HDCharles committed Sep 28, 2026 at 13:22 UTC c01368fd64eaecc0c7242e4d933ec80fe8e047f5
6 files changed +223 -210
tests/kernels/quantization/test_int4_emulation_moe.py
+43 -8
@@ -240,6 +240,29 @@ def _make_awq_moe_weights(E, K, N, group_size, asym=False):
240 )
241
242
243 +def _canonicalize_gptq_moe_inputs(
244 + w13: torch.Tensor,
245 + w2: torch.Tensor,
246 + w13_scale: torch.Tensor,
247 + w2_scale: torch.Tensor,
248 + w13_qzeros: torch.Tensor | None,
249 + w2_qzeros: torch.Tensor | None,
250 +):
251 + """Convert GPTQ K-first tensors to the oracle's canonical N-first layout."""
252 +
253 + def transpose(value: torch.Tensor | None) -> torch.Tensor | None:
254 + return value.transpose(1, 2).contiguous() if value is not None else None
255 +
256 + return (
257 + w13.transpose(1, 2).contiguous(),
258 + w2.transpose(1, 2).contiguous(),
259 + w13_scale.transpose(1, 2).contiguous(),
260 + w2_scale.transpose(1, 2).contiguous(),
261 + transpose(w13_qzeros),
262 + transpose(w2_qzeros),
263 + )
264 +
265 +
266 def _run_emulation_forward(
267 experts, w13_bf16, w2_bf16, hidden_states, topk_weights, topk_ids, E, K, N
268 ):
@@ -478,7 +501,8 @@ def test_gptq_process_weights_shapes_and_values(E, K, N, group_size, asym):
501 w13, w13s, w13z, w13_ref, w2, w2s, w2z, w2_ref = _make_gptq_moe_weights(
502 E, K, N, group_size, asym
503 )
481 - result = _process_weights_emulation_gptq(w13, w2, w13s, w2s, w13z, w2z)
504 + gptq_inputs = _canonicalize_gptq_moe_inputs(w13, w2, w13s, w2s, w13z, w2z)
505 + result = _process_weights_emulation_gptq(*gptq_inputs)
506 w13_out, w2_out = result[0], result[1]
507
508 assert w13_out.shape == (E, 2 * N, K)
@@ -561,7 +585,7 @@ def test_gptq_awq_process_weights_agree(E, K, N, group_size, asym):
585 g2s_list.append(s2)
586 a2s_list.append(s2)
587
564 - gptq_res = _process_weights_emulation_gptq(
588 + gptq_inputs = _canonicalize_gptq_moe_inputs(
589 torch.stack(g13_list),
590 torch.stack(g2_list),
591 torch.stack(g13s_list),
@@ -569,6 +593,7 @@ def test_gptq_awq_process_weights_agree(E, K, N, group_size, asym):
593 torch.stack(g13z_list) if asym else None,
594 torch.stack(g2z_list) if asym else None,
595 )
596 + gptq_res = _process_weights_emulation_gptq(*gptq_inputs)
597 awq_res = _process_weights_emulation_awq(
598 torch.stack(a13_list),
599 torch.stack(a2_list),
@@ -615,7 +640,7 @@ def test_gptq_vs_awq_forward_agree(E, K, N, top_k, group_size, num_tokens):
640 g2s_list.append(s2)
641 a2s_list.append(s2.clone())
642
618 - gptq_res = _process_weights_emulation_gptq(
643 + gptq_inputs = _canonicalize_gptq_moe_inputs(
644 torch.stack(g13_list),
645 torch.stack(g2_list),
646 torch.stack(g13s_list),
@@ -623,6 +648,7 @@ def test_gptq_vs_awq_forward_agree(E, K, N, top_k, group_size, num_tokens):
648 None,
649 None,
650 )
651 + gptq_res = _process_weights_emulation_gptq(*gptq_inputs)
652 awq_res = _process_weights_emulation_awq(
653 torch.stack(a13_list),
654 torch.stack(a2_list),
@@ -750,7 +776,7 @@ def test_ep_output_matches_no_ep(E, K, N, top_k, group_size, num_tokens, ep_size
776 if fmt == "gptq"
777 else _process_weights_emulation_awq
778 )
753 - res = process_fn(
779 + process_inputs = (
780 torch.stack(packed13_list),
781 torch.stack(packed2_list),
782 torch.stack(scales13_list),
@@ -758,6 +784,9 @@ def test_ep_output_matches_no_ep(E, K, N, top_k, group_size, num_tokens, ep_size
784 None,
785 None,
786 )
787 + if fmt == "gptq":
788 + process_inputs = _canonicalize_gptq_moe_inputs(*process_inputs)
789 + res = process_fn(*process_inputs)
790 w13_all, w2_all = res[0], res[1] # [E, 2N, K], [E, K, N]
791
792 hidden_states = torch.randn(num_tokens, K, dtype=torch.bfloat16, device=device)
@@ -855,7 +884,7 @@ def test_ep_gptq_awq_agree(E, K, N, top_k, group_size, num_tokens, ep_size):
884 g2s_list.append(s2)
885 a2s_list.append(s2.clone())
886
858 - gptq_res = _process_weights_emulation_gptq(
887 + gptq_inputs = _canonicalize_gptq_moe_inputs(
888 torch.stack(g13_list),
889 torch.stack(g2_list),
890 torch.stack(g13s_list),
@@ -863,6 +892,7 @@ def test_ep_gptq_awq_agree(E, K, N, top_k, group_size, num_tokens, ep_size):
892 None,
893 None,
894 )
895 + gptq_res = _process_weights_emulation_gptq(*gptq_inputs)
896 awq_res = _process_weights_emulation_awq(
897 torch.stack(a13_list),
898 torch.stack(a2_list),
@@ -954,7 +984,7 @@ def test_ep_partial_rank_no_active_experts(
984 scales13_list.append(s13)
985 scales2_list.append(s2)
986
957 - res = _process_weights_emulation_gptq(
987 + gptq_inputs = _canonicalize_gptq_moe_inputs(
988 torch.stack(packed13_list),
989 torch.stack(packed2_list),
990 torch.stack(scales13_list),
@@ -962,6 +992,7 @@ def test_ep_partial_rank_no_active_experts(
992 None,
993 None,
994 )
995 + res = _process_weights_emulation_gptq(*gptq_inputs)
996 w13_all, w2_all = res[0], res[1]
997
998 # Force topk_ids to only use experts in [0, num_local) — rank 0's slice
@@ -1029,7 +1060,7 @@ def test_ep_sum_equals_full_forward(E, K, N, top_k, group_size, num_tokens, ep_s
1060 scales13_list.append(s13)
1061 scales2_list.append(s2)
1062
1032 - res = _process_weights_emulation_gptq(
1063 + gptq_inputs = _canonicalize_gptq_moe_inputs(
1064 torch.stack(packed13_list),
1065 torch.stack(packed2_list),
1066 torch.stack(scales13_list),
@@ -1037,6 +1068,7 @@ def test_ep_sum_equals_full_forward(E, K, N, top_k, group_size, num_tokens, ep_s
1068 None,
1069 None,
1070 )
1071 + res = _process_weights_emulation_gptq(*gptq_inputs)
1072 w13_all, w2_all = res[0], res[1]
1073
1074 # Fix routing so every token uses exactly 2 consecutive experts (round-robin)
@@ -1143,7 +1175,7 @@ def test_emulation_output_close_to_bf16_reference(
1175 if fmt == "gptq"
1176 else _process_weights_emulation_awq
1177 )
1146 - res = process_fn(
1178 + process_inputs = (
1179 torch.stack(packed13_list),
1180 torch.stack(packed2_list),
1181 torch.stack(scales13_list),
@@ -1151,6 +1183,9 @@ def test_emulation_output_close_to_bf16_reference(
1183 None,
1184 None,
1185 )
1186 + if fmt == "gptq":
1187 + process_inputs = _canonicalize_gptq_moe_inputs(*process_inputs)
1188 + res = process_fn(*process_inputs)
1189 w13_bf16, w2_bf16 = res[0], res[1]
1190
1191 dummy_scale = torch.ones(1, dtype=torch.float16, device=device)
tests/quantization/test_moe_wna16.py
+44 -9
@@ -149,7 +149,11 @@ def test_wna16_oracle_rejects_incompatible_quant_structures(
149 assert expected in reason
150
151
152 -def test_compressed_tensors_weights_are_transposed_for_triton():
152 +def test_compressed_tensors_nfirst_weights_pass_through_for_triton():
153 + """CT provides N-first [E, N, K_packed] int32; Triton expects N-first uint8.
154 +
155 + No transpose needed — just a view from int32 to uint8.
156 + """
157 quant_config = QuantizationArgs(
158 num_bits=4,
159 type=QuantizationType.INT,
@@ -158,10 +162,41 @@ def test_compressed_tensors_weights_are_transposed_for_triton():
162 dynamic=False,
163 group_size=32,
164 )
161 - w13 = torch.arange(16, dtype=torch.int32).reshape(1, 2, 8)
162 - w2 = torch.arange(12, dtype=torch.int32).reshape(1, 2, 6)
163 - w13_scale = torch.arange(32, dtype=torch.float16).reshape(1, 4, 8)
164 - w2_scale = torch.arange(18, dtype=torch.float16).reshape(1, 3, 6)
165 + # N-first layout: [E, N, K_packed]
166 + w13 = torch.arange(16, dtype=torch.int32).reshape(1, 8, 2)
167 + w2 = torch.arange(12, dtype=torch.int32).reshape(1, 6, 2)
168 + w13_scale = torch.arange(32, dtype=torch.float16).reshape(1, 8, 4)
169 + w2_scale = torch.arange(18, dtype=torch.float16).reshape(1, 6, 3)
170 +
171 + converted = convert_to_wna16_moe_kernel_format(
172 + backend=WNA16MoEBackend.TRITON,
173 + layer=torch.nn.Module(),
174 + quant_config=quant_config,
175 + input_dtype=None,
176 + w13=w13,
177 + w2=w2,
178 + w13_scale=w13_scale,
179 + w2_scale=w2_scale,
180 + )
181 +
182 + assert converted is not None
183 + assert torch.equal(converted[0], w13.contiguous().view(torch.uint8))
184 + assert torch.equal(converted[1], w2.contiguous().view(torch.uint8))
185 + assert torch.equal(converted[2], w13_scale)
186 + assert torch.equal(converted[3], w2_scale)
187 +
188 +
189 +def test_gptq_nfirst_weights_pass_through_for_triton():
190 + """GPTQ frontend normalizes to N-first before calling oracle.
191 +
192 + Verifies the oracle handles N-first GPTQ inputs the same way as CT.
193 + """
194 + quant_config = AutoGPTQConfig(4, 128, False, True, False, {}, {})
195 + # N-first layout (after GPTQ frontend transpose): [E, N, K_packed]
196 + w13 = torch.arange(16, dtype=torch.int32).reshape(1, 8, 2)
197 + w2 = torch.arange(12, dtype=torch.int32).reshape(1, 6, 2)
198 + w13_scale = torch.arange(32, dtype=torch.float16).reshape(1, 8, 4)
199 + w2_scale = torch.arange(18, dtype=torch.float16).reshape(1, 6, 3)
200
201 converted = convert_to_wna16_moe_kernel_format(
202 backend=WNA16MoEBackend.TRITON,
@@ -175,10 +210,10 @@ def test_compressed_tensors_weights_are_transposed_for_triton():
210 )
211
212 assert converted is not None
178 - assert torch.equal(converted[0], w13.transpose(1, 2).contiguous().view(torch.uint8))
179 - assert torch.equal(converted[1], w2.transpose(1, 2).contiguous().view(torch.uint8))
180 - assert torch.equal(converted[2], w13_scale.transpose(1, 2).contiguous())
181 - assert torch.equal(converted[3], w2_scale.transpose(1, 2).contiguous())
213 + assert torch.equal(converted[0], w13.contiguous().view(torch.uint8))
214 + assert torch.equal(converted[1], w2.contiguous().view(torch.uint8))
215 + assert torch.equal(converted[2], w13_scale)
216 + assert torch.equal(converted[3], w2_scale)
217
218
219 def test_moe_wna16_setup_forwards_selected_backend(monkeypatch):
vllm/model_executor/layers/fused_moe/oracle/int_wna16.py
+88 -103
@@ -567,13 +567,26 @@ def _process_weights_marlin(
567 """Standard Marlin weight post-processing shared by MARLIN and
568 BATCHED_MARLIN backends.
569
570 + Inputs arrive in canonical N-first layout ``[E, N, K_packed]``.
571 +
572 Steps
573 -----
572 - 1. Optional FP8 preprocessing of packed weights / scales.
573 - 2. Repack weights via ``gptq_marlin_moe_repack``.
574 - 3. Permute scales (and optionally extract INT8 global scales).
575 - 4. Permute bias tensors.
574 + 1. Transpose canonical N-first to K-first for Marlin.
575 + 2. Optional FP8 preprocessing of packed weights / scales.
576 + 3. Repack weights via ``gptq_marlin_moe_repack``.
577 + 4. Permute scales (and optionally extract INT8 global scales).
578 + 5. Permute bias tensors.
579 """
580 + # Canonical N-first → K-first for Marlin.
581 + w13_qweight = w13_qweight.transpose(1, 2).contiguous()
582 + w2_qweight = w2_qweight.transpose(1, 2).contiguous()
583 + w13_scales = w13_scales.transpose(1, 2).contiguous()
584 + w2_scales = w2_scales.transpose(1, 2).contiguous()
585 + if w13_qzeros is not None:
586 + w13_qzeros = w13_qzeros.transpose(1, 2).contiguous()
587 + if w2_qzeros is not None:
588 + w2_qzeros = w2_qzeros.transpose(1, 2).contiguous()
589 +
590 is_a_8bit = input_dtype is not None and input_dtype.itemsize == 1
591
592 marlin_w13_qweight: torch.Tensor
@@ -934,7 +947,12 @@ def _process_weights_cpu(
947 torch.Tensor | None, # w13_bias
948 torch.Tensor | None, # w2_bias
949 ]:
937 - """CPU INT4 W4A16 weight post-processing."""
950 + """CPU INT4 W4A16 weight post-processing.
951 +
952 + Non-AWQ inputs arrive in canonical N-first layout and are transposed
953 + to K-first internally. AWQ uses a different packing axis and arrives
954 + in its native format.
955 + """
956 from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
957 prepare_int4_moe_layer_for_cpu,
958 )
@@ -945,15 +963,9 @@ def _process_weights_cpu(
963 AutoGPTQConfig,
964 )
965
948 - # Detect packing format.
949 - # AWQ: qweight is [E, K, 2*N//8] (packed along output/N dim).
950 - # GPTQ: qweight is [E, K//8, 2*N] (packed along input/K dim).
951 - # compressed-tensors: qweight is [E, K//8, 2*N] (packed along input/K dim).
966 if isinstance(quant_config, AutoAWQConfig):
953 - # AWQ: K is stored unpacked in dim 1.
967 cpu_quant_algo = ops.CPUQuantAlgo.AWQ
968 elif isinstance(quant_config, (AutoGPTQConfig, QuantizationArgs)):
956 - # GPTQ / compressed-tensors: K//8 is stored packed in dim 1.
969 if isinstance(quant_config, AutoGPTQConfig) and quant_config.desc_act:
970 raise NotImplementedError(
971 "CPU WNA16 MoE backend does not support GPTQ with "
@@ -961,6 +973,15 @@ def _process_weights_cpu(
973 "reordering support."
974 )
975 cpu_quant_algo = ops.CPUQuantAlgo.GPTQ
976 + # Canonical N-first → K-first for CPU kernel.
977 + w13 = w13.transpose(1, 2).contiguous()
978 + w2 = w2.transpose(1, 2).contiguous()
979 + w13_scale = w13_scale.transpose(1, 2).contiguous()
980 + w2_scale = w2_scale.transpose(1, 2).contiguous()
981 + if w13_qzeros is not None:
982 + w13_qzeros = w13_qzeros.transpose(1, 2).contiguous()
983 + if w2_qzeros is not None:
984 + w2_qzeros = w2_qzeros.transpose(1, 2).contiguous()
985 else:
986 raise TypeError(
987 "CPU WNA16 MoE backend requires AutoAWQConfig, AutoGPTQConfig "
@@ -1030,41 +1051,28 @@ def _process_weights_xpu(
1051 torch.Tensor | None, # w13_bias
1052 torch.Tensor | None, # w2_bias
1053 ]:
1033 - """Repack GPTQ-format INT4 MoE weights into the layout
1034 - `vllm_xpu_kernels.fused_moe_interface.xpu_fused_moe(is_int4=True)` expects:
1035 -
1036 - w13: [E, 2*N, K] int4 (uint8 storage [E, 2*N, K // 2])
1037 - w13_scales: [E, 2*N, K // group_size] params_dtype
1038 - w2: [E, K, N] int4 (uint8 storage [E, K, N // 2])
1039 - w2_scales: [E, K, N // group_size] params_dtype
1040 -
1041 - Input GPTQ layout from MoERunner.weight_loader:
1042 - w13: [E, K // 8, 2*N] int32 (8 nibbles per int32 along the input dim)
1043 - w13_scales: [E, K // group_size, 2*N] params_dtype
1044 - w2: [E, N // 8, K] int32
1045 - w2_scales: [E, N // group_size, K] params_dtype
1046 -
1047 - Transpose dim 1 ↔ dim 2 then view int32 → uint8 to recover sequential
1048 - int4-packed bytes along the input dim. Each packed int32 holds 8 nibbles
1049 - `(n7<<28)|(n6<<24)|...|(n1<<4)|n0` in ascending K order; on a
1050 - little-endian host the int32→uint8 view exposes them as bytes
1051 - `[n1<<4|n0, n3<<4|n2, n5<<4|n4, n7<<4|n6]`, i.e. two nibbles per byte
1052 - with the lower nibble = lower input-K index. xpu_fused_moe(is_int4=True)
1053 - expects this convention; on a big-endian host the byte order reverses
1054 - and the kernel would silently miscompute, so we hard-fail.
1054 + """Repack INT4 MoE weights into the layout
1055 + `vllm_xpu_kernels.fused_moe_interface.xpu_fused_moe(is_int4=True)` expects.
1056 +
1057 + Inputs arrive in canonical N-first layout ``[E, N, K_packed]`` int32.
1058 + The int32 → uint8 view recovers sequential int4-packed bytes along
1059 + the input dim. Each packed int32 holds 8 nibbles in ascending K order;
1060 + on a little-endian host the view exposes them as two nibbles per byte
1061 + with the lower nibble = lower input-K index. xpu_fused_moe(is_int4=True)
1062 + expects this convention; big-endian hosts are unsupported.
1063 """
1064 del layer, quant_config # unused — kept for parity with the marlin helper
1065
1066 if sys.byteorder != "little":
1067 raise NotImplementedError(
1060 - "_process_weights_xpu requires a little-endian host: the GPTQ "
1068 + "_process_weights_xpu requires a little-endian host: the "
1069 "int32 → uint8 nibble repack relies on LE byte ordering."
1070 )
1071
1064 - w13_xpu = w13_qweight.transpose(1, 2).contiguous().view(torch.uint8)
1065 - w2_xpu = w2_qweight.transpose(1, 2).contiguous().view(torch.uint8)
1066 - w13_scales_xpu = w13_scales.transpose(1, 2).contiguous()
1067 - w2_scales_xpu = w2_scales.transpose(1, 2).contiguous()
1072 + w13_xpu = w13_qweight.contiguous().view(torch.uint8)
1073 + w2_xpu = w2_qweight.contiguous().view(torch.uint8)
1074 + w13_scales_xpu = w13_scales.contiguous()
1075 + w2_scales_xpu = w2_scales.contiguous()
1076
1077 return (
1078 w13_xpu,
@@ -1315,16 +1323,23 @@ def _process_weights_emulation_gptq(
1323 ) -> tuple:
1324 """Dequantize int4 weights to BF16 for the emulation backend.
1325
1318 - Inputs are in GPTQ packed format:
1319 - w13: [E, K//8, 2*N] int32 (gate+up proj stacked on dim 2)
1320 - w2: [E, N//8, K] int32
1321 - w13_scale: [E, K//gs, 2*N] float16
1322 - w2_scale: [E, N//gs, K] float16
1326 + Inputs arrive in canonical N-first layout and are transposed to
1327 + K-first internally for the GPTQ dequantization routines.
1328
1329 Outputs (what TritonExperts expects):
1330 w13_out: [E, 2*N, K] bfloat16
1331 w2_out: [E, K, N] bfloat16
1332 """
1333 + # Canonical N-first → K-first for dequantization.
1334 + w13 = w13.transpose(1, 2).contiguous()
1335 + w2 = w2.transpose(1, 2).contiguous()
1336 + w13_scale = w13_scale.transpose(1, 2).contiguous()
1337 + w2_scale = w2_scale.transpose(1, 2).contiguous()
1338 + if w13_qzeros is not None:
1339 + w13_qzeros = w13_qzeros.transpose(1, 2).contiguous()
1340 + if w2_qzeros is not None:
1341 + w2_qzeros = w2_qzeros.transpose(1, 2).contiguous()
1342 +
1343 # w13: packed along K (dim 1), output cols are 2*N (dim 2)
1344 # transpose_output=True yields [E, 2*N, K]
1345 w13_bf16 = _unpack_and_dequant_int4_gptq(
@@ -1435,6 +1450,10 @@ def convert_to_wna16_moe_kernel_format(
1450 ):
1451 """Dispatch weight post-processing to the appropriate per-backend handler.
1452
1453 + All non-AWQ frontends must supply weights and scales in N-first
1454 + canonical layout ``[E, N_out, K_packed]``. AWQ uses a different
1455 + packing axis and is handled by dedicated per-backend helpers.
1456 +
1457 To add a new backend, implement a ``_process_weights_<name>`` helper and
1458 add a branch here. Backends that rewrite the layer's parameters in place
1459 (e.g. Humming) return ``None``; the caller then skips the param scatter.
@@ -1626,67 +1645,33 @@ def convert_to_wna16_moe_kernel_format(
1645 w2_qzeros,
1646 )
1647 elif backend == WNA16MoEBackend.TRITON:
1629 - # Two possible input layouts depending on the quantization source:
1630 - #
1631 - # MoeWNA16 (uint8): (E, N_out, K // bit8_pack) — N-first
1632 - # → just view as uint8 (no-op)
1633 - #
1634 - # AutoGPTQ/compressed-tensors (int32, K-first):
1635 - # (E, K // pack32, N_out)
1636 - # → transpose to N-first, then view as uint8 to get
1637 - # (E, N_out, K // bit8_pack) [int32 = 4 bytes → 4 uint8s]
1638 - # Scales: (E, K // gs, N_out) → transpose → (E, N_out, K // gs)
1639 - from vllm.model_executor.layers.quantization.auto_gptq import (
1640 - AutoGPTQConfig,
1641 - )
1648 + # All inputs arrive in canonical N-first format; view as uint8.
1649 + w13_uint8 = w13.contiguous().view(torch.uint8)
1650 + w2_uint8 = w2.contiguous().view(torch.uint8)
1651 +
1652 + # Repack N-first int32 zero-points to uint8:
1653 + # [E, N//pf, K//gs] int32 → [E, N*4//pf, K//gs] uint8
1654 + if w13_qzeros is not None and w13_qzeros.dtype == torch.int32:
1655 + E13, Np13, Kg13 = w13_qzeros.shape
1656 + w13_qzeros = (
1657 + w13_qzeros.contiguous()
1658 + .view(torch.uint8)
1659 + .reshape(E13, Np13, Kg13, 4)
1660 + .permute(0, 1, 3, 2)
1661 + .reshape(E13, Np13 * 4, Kg13)
1662 + .contiguous()
1663 + )
1664 + if w2_qzeros is not None and w2_qzeros.dtype == torch.int32:
1665 + E2, Np2, Kg2 = w2_qzeros.shape
1666 + w2_qzeros = (
1667 + w2_qzeros.contiguous()
1668 + .view(torch.uint8)
1669 + .reshape(E2, Np2, Kg2, 4)
1670 + .permute(0, 1, 3, 2)
1671 + .reshape(E2, Np2 * 4, Kg2)
1672 + .contiguous()
1673 + )
1674
1643 - if isinstance(quant_config, (AutoGPTQConfig, QuantizationArgs)):
1644 - # These integrations build in K-first format even when the Triton
1645 - # backend is selected. Transpose to N-first first.
1646 - w13_uint8 = w13.transpose(1, 2).contiguous().view(torch.uint8)
1647 - w2_uint8 = w2.transpose(1, 2).contiguous().view(torch.uint8)
1648 - w13_scale = w13_scale.transpose(1, 2).contiguous()
1649 - w2_scale = w2_scale.transpose(1, 2).contiguous()
1650 - # Zero points from compressed-tensors checkpoints are K-first int32
1651 - # with 8 int4 ZPs packed per element: shape (E, K//gs, N//8).
1652 - # fused_moe_kernel_gptq_awq expects N-first uint8 with 2 int4 ZPs
1653 - # per byte: shape (E, N//2, K//gs), indexed as
1654 - # (offs_bn // 2) * stride_bzn + offs_k_group * stride_bzk.
1655 - # Conversion steps:
1656 - # (E, K//gs, N//8) int32
1657 - # → transpose(1,2) → (E, N//8, K//gs) int32
1658 - # → view(uint8) → (E, N//8, K//gs*4) [each int32 → 4 bytes]
1659 - # → reshape(…,4) → (E, N//8, K//gs, 4) [isolate byte index]
1660 - # → permute(0,1,3,2) → (E, N//8, 4, K//gs) [byte index before K]
1661 - # → reshape → (E, N//2, K//gs) [kernel expected layout]
1662 - # After this, element [e, offs_bn//2, k_group] is the uint8 byte
1663 - # holding the two int4 ZPs for output channels offs_bn and offs_bn+1.
1664 - if w13_qzeros is not None:
1665 - E13, Kg13, Np13 = w13_qzeros.shape
1666 - w13_qzeros = (
1667 - w13_qzeros.transpose(1, 2)
1668 - .contiguous()
1669 - .view(torch.uint8)
1670 - .reshape(E13, Np13, Kg13, 4)
1671 - .permute(0, 1, 3, 2)
1672 - .reshape(E13, Np13 * 4, Kg13)
1673 - .contiguous()
1674 - )
1675 - if w2_qzeros is not None:
1676 - E2, Kg2, Np2 = w2_qzeros.shape
1677 - w2_qzeros = (
1678 - w2_qzeros.transpose(1, 2)
1679 - .contiguous()
1680 - .view(torch.uint8)
1681 - .reshape(E2, Np2, Kg2, 4)
1682 - .permute(0, 1, 3, 2)
1683 - .reshape(E2, Np2 * 4, Kg2)
1684 - .contiguous()
1685 - )
1686 - else:
1687 - # MoeWNA16 uses N-first uint8 weights and scales.
1688 - w13_uint8 = w13.view(torch.uint8)
1689 - w2_uint8 = w2.view(torch.uint8)
1675 return (
1676 w13_uint8,
1677 w2_uint8,
vllm/model_executor/layers/fused_moe/routed_experts.py
+1 -1
@@ -676,7 +676,7 @@ class RoutedExperts(PluggableLayer):
676 # Hereafter, `expert_id` is local physical id
677
678 # is_transposed: if the dim to shard the weight
679 - # should be flipped. Required by GPTQ, compressed-tensors
679 + # should be flipped. Required by GPTQ/AWQ (K-first format).
680 # should be whatever dimension intermediate_size_per_partition is
681 is_transposed = getattr(param, "is_transposed", False)
682
vllm/model_executor/layers/quantization/auto_gptq.py
+18 -6
@@ -626,19 +626,31 @@ class AutoGPTQMoEMethod(FusedMoEMethodBase):
626 layer.register_parameter("w2_bias", None)
627 w2_bias = None
628
629 + # Normalize GPTQ K-first layout to canonical N-first format.
630 + w13 = layer.w13_qweight.data.transpose(1, 2).contiguous()
631 + w2 = layer.w2_qweight.data.transpose(1, 2).contiguous()
632 + w13_scale = layer.w13_scales.data.transpose(1, 2).contiguous()
633 + w2_scale = layer.w2_scales.data.transpose(1, 2).contiguous()
634 + w13_qzeros = getattr(layer, "w13_qzeros", None)
635 + w2_qzeros = getattr(layer, "w2_qzeros", None)
636 + if w13_qzeros is not None:
637 + w13_qzeros = w13_qzeros.data.transpose(1, 2).contiguous()
638 + if w2_qzeros is not None:
639 + w2_qzeros = w2_qzeros.data.transpose(1, 2).contiguous()
640 +
641 converted = convert_to_wna16_moe_kernel_format(
642 backend=self.wna16_moe_backend,
643 layer=layer,
644 quant_config=self.quant_config,
645 input_dtype=self.input_dtype,
634 - w13=layer.w13_qweight,
635 - w2=layer.w2_qweight,
636 - w13_scale=layer.w13_scales,
637 - w2_scale=layer.w2_scales,
646 + w13=w13,
647 + w2=w2,
648 + w13_scale=w13_scale,
649 + w2_scale=w2_scale,
650 w13_bias=w13_bias,
651 w2_bias=w2_bias,
640 - w13_qzeros=getattr(layer, "w13_qzeros", None),
641 - w2_qzeros=getattr(layer, "w2_qzeros", None),
652 + w13_qzeros=w13_qzeros,
653 + w2_qzeros=w2_qzeros,
654 )
655
656 if converted is None:
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_wna16.py
+29 -83
@@ -112,10 +112,10 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod):
112 WNA16MoEBackend.MARLIN,
113 WNA16MoEBackend.BATCHED_MARLIN,
114 ]
115 - self.is_transposed = self.wna16_backend not in (
116 - WNA16MoEBackend.FLASHINFER_TRTLLM,
117 - WNA16MoEBackend.HUMMING,
118 - )
115 + # CT checkpoints always load in N-first format [E, N, K_packed].
116 + # Backend-specific transposition is handled in
117 + # convert_to_wna16_moe_kernel_format.
118 + self.is_transposed = False
119
120 if self.is_marlin:
121 assert check_moe_marlin_supports_config(
@@ -141,80 +141,31 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod):
141 num_groups_w2: int | None = None,
142 num_groups_w13: int | None = None,
143 ) -> tuple[int, int, int]:
144 - """Get the shape of the weight based on the weight name, number of experts
145 - hidden size, intermediate size per partition, number of groups for w2,
146 - and number of groups for w13. Pass in num_groups_w2 and num_groups_w13
147 - for weight scales/zero_points.
144 + """Return N-first buffer shape for the given weight/scale/zp tensor.
145 +
146 + All CT MoE weights are allocated in the checkpoint's native N-first
147 + layout ``[E, N, K_packed]``. Backend-specific transposition (e.g. to
148 + Marlin's K-first layout) is deferred to
149 + ``convert_to_wna16_moe_kernel_format``.
150 """
149 - if weight_name in ("w13_scale", "w13_zp"):
150 - assert num_groups_w13 is not None, (
151 - "num_groups_w13 must be provided for weight scales/zero_points"
152 - )
153 - if weight_name in ("w2_scale", "w2_zp"):
154 - assert num_groups_w2 is not None, (
155 - "num_groups_w2 must be provided for weight scales/zero_points"
156 - )
151 w13_num_shards = 2 if self.moe.is_act_and_mul else 1
158 - shape_map: dict[str, dict[str, tuple[int, int | None, int | None]]] = {
159 - "w13_weight": {
160 - "Flashinfer": (
161 - num_experts,
162 - w13_num_shards * intermediate_size_per_partition,
163 - self._packed_dim(hidden_size),
164 - ),
165 - "Marlin": (
166 - num_experts,
167 - self._packed_dim(hidden_size),
168 - w13_num_shards * intermediate_size_per_partition,
169 - ),
170 - },
171 - "w13_scale": {
172 - "Flashinfer": (
173 - num_experts,
174 - w13_num_shards * intermediate_size_per_partition,
175 - num_groups_w13,
176 - ),
177 - "Marlin": (
178 - num_experts,
179 - num_groups_w13,
180 - w13_num_shards * intermediate_size_per_partition,
181 - ),
182 - },
183 - "w13_zp": {
184 - "Marlin": (
185 - num_experts,
186 - num_groups_w13,
187 - self._packed_dim(w13_num_shards * intermediate_size_per_partition),
188 - ),
189 - },
190 - "w2_weight": {
191 - "Flashinfer": (
192 - num_experts,
193 - hidden_size,
194 - self._packed_dim(intermediate_size_per_partition),
195 - ),
196 - "Marlin": (
197 - num_experts,
198 - self._packed_dim(intermediate_size_per_partition),
199 - hidden_size,
200 - ),
201 - },
202 - "w2_scale": {
203 - "Flashinfer": (num_experts, hidden_size, num_groups_w2),
204 - "Marlin": (num_experts, num_groups_w2, hidden_size),
205 - },
206 - "w2_zp": {
207 - "Marlin": (
208 - num_experts,
209 - num_groups_w2,
210 - self._packed_dim(hidden_size),
211 - ),
212 - },
152 + w13_n = w13_num_shards * intermediate_size_per_partition
153 + pf = self.packed_factor
154 + shape_map = {
155 + "w13_weight": (num_experts, w13_n, hidden_size // pf),
156 + "w13_scale": (num_experts, w13_n, num_groups_w13),
157 + "w13_zp": (num_experts, w13_n // pf, num_groups_w13),
158 + "w2_weight": (
159 + num_experts,
160 + hidden_size,
161 + intermediate_size_per_partition // pf,
162 + ),
163 + "w2_scale": (num_experts, hidden_size, num_groups_w2),
164 + "w2_zp": (num_experts, hidden_size // pf, num_groups_w2),
165 }
214 - backend_key = "Marlin" if self.is_transposed else "Flashinfer"
215 - shape = shape_map[weight_name][backend_key]
216 - assert shape[1] is not None and shape[2] is not None
217 - return shape[0], shape[1], shape[2]
166 + shape = shape_map[weight_name]
167 + assert isinstance(shape, tuple) and all(isinstance(d, int) for d in shape)
168 + return shape # type: ignore[return-value]
169
170 def create_weights(
171 self,
@@ -225,11 +176,8 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod):
176 params_dtype: torch.dtype,
177 **extra_weight_attrs,
178 ):
228 - # Will transpose the loaded weight along the
229 - # intermediate and hidden dim sizes. Will
230 - # shard for TP along the transposed dims
179 extra_weight_attrs.update(
232 - {"is_transposed": self.is_transposed, "quant_method": self.strategy}
180 + {"is_transposed": False, "quant_method": self.strategy}
181 )
182
183 w13_weight = torch.nn.Parameter(
@@ -440,12 +388,10 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod):
388 replace_parameter(layer, "w13_weight_scale", w13_scales)
389 replace_parameter(layer, "w2_weight_scale", w2_scales)
390
443 - # CPU fused_experts_cpu and the RDNA3 HIP kernel require zero points
444 - # even for symmetric quant (the oracle synthesizes them).
391 + # CPU fused_experts_cpu requires zero points even for symmetric quant.
392 # EMULATION bakes ZP into the dequantized bf16 weights — ZP is None.
393 if (
447 - not self.symmetric
448 - or self.wna16_backend in (WNA16MoEBackend.CPU, WNA16MoEBackend.RDNA3)
394 + not self.symmetric or self.wna16_backend == WNA16MoEBackend.CPU
395 ) and self.wna16_backend != WNA16MoEBackend.EMULATION:
396 assert w13_qzeros is not None and w2_qzeros is not None
397 replace_parameter(layer, "w13_weight_zero_point", w13_qzeros)