@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 774e552397
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 2, 04:45 UTC

[compressed-tensors] Asymmetric support for MoE WNA16 marlin (#44025)

Signed-off-by: Brian Dellabetta <bdellabe@redhat.com>

Brian Dellabetta committed Jun 2, 2026 at 11:51 UTC 774e5523971beee46d0744f15fe141548969240f
5 files changed +126 -12
vllm/model_executor/layers/fused_moe/experts/marlin_moe.py
+4
@@ -45,6 +45,8 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
45 kFp8StaticTensorSym,
46 kInt4Static,
47 kInt4Static32,
48 + kInt4Static32Asym,
49 + kInt4StaticAsym,
50 kInt8Static,
51 kMxfp4Static,
52 kMxfp8Static,
@@ -611,6 +613,8 @@ class MarlinExpertsBase(mk.FusedMoEExpertsModular):
613 kInt4Static,
614 kInt8Static,
615 kInt4Static32,
616 + kInt4StaticAsym,
617 + kInt4Static32Asym,
618 ]
619 return weight_key in SUPPORTED_W
620
vllm/model_executor/layers/fused_moe/oracle/int_wna16.py
+18
@@ -32,6 +32,7 @@ from vllm.model_executor.layers.quantization.utils.marlin_utils import (
32 marlin_moe_permute_scales,
33 marlin_permute_bias,
34 moe_awq_to_marlin_zero_points,
35 + moe_packed_to_marlin_zero_points,
36 )
37 from vllm.model_executor.layers.quantization.utils.quant_utils import (
38 QuantKey,
@@ -496,6 +497,23 @@ def _process_weights_marlin(
497 marlin_w2_scales
498 )
499
500 + # --- Permute zero points ---
501 + if w13_qzeros is not None and w2_qzeros is not None:
502 + w13_qzeros = moe_packed_to_marlin_zero_points(
503 + w13_qzeros,
504 + size_k=w13_qzeros.shape[1],
505 + size_n=w13_qzeros.shape[2] * pack_factor,
506 + num_bits=num_bits,
507 + is_a_8bit=is_a_8bit,
508 + )
509 + w2_qzeros = moe_packed_to_marlin_zero_points(
510 + w2_qzeros,
511 + size_k=w2_qzeros.shape[1],
512 + size_n=w2_qzeros.shape[2] * pack_factor,
513 + num_bits=num_bits,
514 + is_a_8bit=is_a_8bit,
515 + )
516 +
517 # --- Permute bias ---
518 if w13_bias is not None:
519 w13_bias_out = marlin_permute_bias(w13_bias)
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_wna16_marlin.py
+73 -12
@@ -29,6 +29,7 @@ from vllm.model_executor.layers.quantization.compressed_tensors.compressed_tenso
29 )
30 from vllm.model_executor.layers.quantization.compressed_tensors.schemes.compressed_tensors_wNa16 import ( # noqa
31 WNA16_SUPPORTED_TYPES_MAP,
32 + WNA16_ZP_SUPPORTED_TYPES_MAP,
33 )
34 from vllm.model_executor.layers.quantization.utils.marlin_utils import (
35 get_marlin_input_dtype,
@@ -56,9 +57,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
57 super().__init__(moe)
58 self.weight_quant = weight_quant
59 self.input_quant = input_quant
59 - assert weight_quant.symmetric, (
60 - "Only symmetric quantization is supported for MoE"
61 - )
60 + self.symmetric = weight_quant.symmetric
61 # Extract properties from weight_quant
62 self.num_bits = weight_quant.num_bits
63 self.packed_factor = 32 // weight_quant.num_bits
@@ -66,7 +65,12 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
65 self.group_size = weight_quant.group_size
66 self.actorder = weight_quant.actorder
67
69 - self.quant_type = WNA16_SUPPORTED_TYPES_MAP[self.num_bits]
68 + self.quant_type = (
69 + WNA16_SUPPORTED_TYPES_MAP[self.num_bits]
70 + if self.symmetric
71 + else WNA16_ZP_SUPPORTED_TYPES_MAP[self.num_bits]
72 + )
73 +
74 self.marlin_input_dtype = get_marlin_input_dtype(layer_name)
75
76 if self.num_bits == 4:
@@ -82,7 +86,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
86 "CompressedTensorsWNA16MarlinMoEMethod only supports int4 and int8 now."
87 )
88
85 - weight_key = QuantKey(self.quant_type, scale)
89 + weight_key = QuantKey(self.quant_type, scale, symmetric=self.symmetric)
90
91 # Select WNA16 MoE backend via oracle.
92 self.wna16_backend, self.experts_cls = select_wna16_moe_backend(
@@ -103,15 +107,15 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
107 Get the shape of the weight based on the weight name, number of experts
108 hidden size, intermediate size per partition, number of groups for w2,
109 and number of groups for w13. Pass in num_groups_w2 and num_groups_w13
106 - for weight scales.
110 + for weight scales/zero_points.
111 """
108 - if weight_name == "w13_scale":
112 + if weight_name in ("w13_scale", "w13_zp"):
113 assert num_groups_w13 is not None, (
110 - "num_groups_w13 must be provided for weight scales"
114 + "num_groups_w13 must be provided for weight scales/zero_points"
115 )
112 - if weight_name == "w2_scale":
116 + if weight_name in ("w2_scale", "w2_zp"):
117 assert num_groups_w2 is not None, (
114 - "num_groups_w2 must be provided for weight scales"
118 + "num_groups_w2 must be provided for weight scales/zero_points"
119 )
120 w13_num_shards = 2 if self.moe.is_act_and_mul else 1
121 is_flashinfer = self.wna16_backend == WNA16MoEBackend.FLASHINFER_TRTLLM
@@ -140,6 +144,15 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
144 w13_num_shards * intermediate_size_per_partition,
145 ),
146 },
147 + "w13_zp": {
148 + "Marlin": (
149 + num_experts,
150 + num_groups_w13,
151 + w13_num_shards
152 + * intermediate_size_per_partition
153 + // self.packed_factor,
154 + ),
155 + },
156 "w2_weight": {
157 "Flashinfer": (
158 num_experts,
@@ -156,6 +169,13 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
169 "Flashinfer": (num_experts, hidden_size, num_groups_w2),
170 "Marlin": (num_experts, num_groups_w2, hidden_size),
171 },
172 + "w2_zp": {
173 + "Marlin": (
174 + num_experts,
175 + num_groups_w2,
176 + hidden_size // self.packed_factor,
177 + ),
178 + },
179 }
180 backend_key = "Flashinfer" if is_flashinfer else "Marlin"
181 return shape_map[weight_name][backend_key]
@@ -263,6 +283,39 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
283 set_weight_attrs(w2_scale, extra_weight_attrs)
284 set_weight_attrs(w2_scale, {"load_full_w2": load_full_w2})
285
286 + if not self.symmetric:
287 + w13_zp = torch.nn.Parameter(
288 + torch.zeros(
289 + *self.get_weight_shape(
290 + "w13_zp",
291 + num_experts,
292 + hidden_size,
293 + intermediate_size_per_partition,
294 + num_groups_w13=num_groups_w13,
295 + ),
296 + dtype=torch.int32,
297 + ),
298 + requires_grad=False,
299 + )
300 + layer.register_parameter("w13_weight_zero_point", w13_zp)
301 + set_weight_attrs(w13_zp, extra_weight_attrs)
302 +
303 + w2_zp = torch.nn.Parameter(
304 + torch.zeros(
305 + *self.get_weight_shape(
306 + "w2_zp",
307 + num_experts,
308 + hidden_size,
309 + intermediate_size_per_partition,
310 + num_groups_w2=num_groups_w2,
311 + ),
312 + dtype=torch.int32,
313 + ),
314 + requires_grad=False,
315 + )
316 + layer.register_parameter("w2_weight_zero_point", w2_zp)
317 + set_weight_attrs(w2_zp, extra_weight_attrs)
318 +
319 w2_weight_shape = torch.nn.Parameter(
320 torch.empty(num_experts, 2), requires_grad=False
321 )
@@ -334,8 +387,8 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
387 w2_g_idx_processed,
388 w13_g_idx_sort_indices,
389 w2_g_idx_sort_indices,
337 - _, # w13_qzeros
338 - _, # w2_qzeros
390 + w13_qzeros,
391 + w2_qzeros,
392 w13_input_global_scale,
393 w2_input_global_scale,
394 _, # w13_bias
@@ -351,6 +404,8 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
404 w2_scale=layer.w2_weight_scale,
405 w13_g_idx=layer.w13_weight_g_idx,
406 w2_g_idx=layer.w2_weight_g_idx,
407 + w13_qzeros=getattr(layer, "w13_weight_zero_point", None),
408 + w2_qzeros=getattr(layer, "w2_weight_zero_point", None),
409 )
410
411 # Replace common parameters
@@ -359,6 +414,10 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
414 replace_parameter(layer, "w13_weight_scale", w13_scales)
415 replace_parameter(layer, "w2_weight_scale", w2_scales)
416
417 + if not self.symmetric:
418 + replace_parameter(layer, "w13_weight_zero_point", w13_qzeros)
419 + replace_parameter(layer, "w2_weight_zero_point", w2_qzeros)
420 +
421 # Marlin-specific parameters (not needed for Flashinfer)
422 if not is_flashinfer:
423 replace_parameter(layer, "w13_weight_g_idx", w13_g_idx_processed)
@@ -417,6 +476,8 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
476 w2_scale=layer.w2_weight_scale,
477 group_size=self.group_size,
478 num_bits=self.num_bits,
479 + w1_zp=getattr(layer, "w13_weight_zero_point", None),
480 + w2_zp=getattr(layer, "w2_weight_zero_point", None),
481 )
482
483 def apply_monolithic(
vllm/model_executor/layers/quantization/utils/marlin_utils.py
+24
@@ -429,6 +429,30 @@ def maybe_warn_marlin_atomic_add(device, dtype):
429 )
430
431
432 +def moe_packed_to_marlin_zero_points(
433 + q_zp_packed: torch.Tensor,
434 + size_k: int,
435 + size_n: int,
436 + num_bits: int,
437 + is_a_8bit: bool = False,
438 +):
439 + """Convert compressed-tensors packed zero points to Marlin format.
440 +
441 + Unlike AWQ, compressed-tensors uses standard bit packing without
442 + interleaving, so we just unpack and apply Marlin permutation directly.
443 + """
444 + num_experts = q_zp_packed.shape[0]
445 + output = torch.empty(
446 + (num_experts, q_zp_packed.shape[1], q_zp_packed.shape[2]),
447 + device=q_zp_packed.device,
448 + dtype=q_zp_packed.dtype,
449 + )
450 + for e in range(num_experts):
451 + q_zp = unpack_cols(q_zp_packed[e], num_bits, size_k, size_n)
452 + output[e] = marlin_zero_points(q_zp, size_k, size_n, num_bits, is_a_8bit)
453 + return output
454 +
455 +
456 def maybe_warn_marlin_atomic_add_env():
457 if torch.compiler.is_dynamo_compiling():
458 return
vllm/model_executor/layers/quantization/utils/quant_utils.py
+7
@@ -181,6 +181,13 @@ kInt8Static = QuantKey(INT8_DTYPE, scale=kInt8StaticGroupScale, symmetric=True)
181 kInt4Static32GroupScale = ScaleDesc(torch.float16, True, GroupShape(1, 32))
182 kInt4Static32 = QuantKey(INT4_DTYPE, scale=kInt4Static32GroupScale, symmetric=True)
183
184 +kInt4StaticAsym = QuantKey(
185 + scalar_types.uint4, scale=kInt4StaticGroupScale, symmetric=False
186 +)
187 +kInt4Static32Asym = QuantKey(
188 + scalar_types.uint4, scale=kInt4Static32GroupScale, symmetric=False
189 +)
190 +
191 kInt8StaticChannelSym = QuantKey(torch.int8, kStaticChannelScale, symmetric=True)
192 kInt8DynamicTokenSym = QuantKey(torch.int8, kDynamicTokenScale, symmetric=True)
193