[Kernel] Extend Marlin thread-tile padding to MoE (WNA16 + FP8/MXFP8) (#45703)

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

Michael Goin committed Jun 23, 2026 at 12:45 UTC 37a682d392330da665690ee0a77c9d0a875f315f
7 files changed +573 -29
tests/kernels/quantization/test_marlin_tile_padding.py
+389
@@ -5,6 +5,8 @@
5 Run `pytest tests/kernels/quantization/test_marlin_tile_padding.py`.
6 """
7
8 +from types import SimpleNamespace
9 +
10 import pytest
11 import torch
12
@@ -14,6 +16,7 @@ from vllm.model_executor.layers.quantization.utils.marlin_utils import (
16 apply_gptq_marlin_linear,
17 marlin_make_empty_g_idx,
18 marlin_make_workspace_new,
19 + marlin_moe_padded_intermediate,
20 marlin_pad_qweight,
21 marlin_pad_scales,
22 marlin_padded_nk,
@@ -115,6 +118,74 @@ def test_marlin_pad_helpers_shapes():
118 assert padded.shape == (1, padded_n)
119
120
121 +# Rank-local MoE intermediate sizes. group<=0 / 32 with a non-multiple-of-64
122 +# size is where tile padding triggers; 64/128 are already tile-aligned.
123 +MOE_INTERMEDIATE_SIZES = [64, 96, 100, 176, 192, 256, 2816]
124 +
125 +
126 +@pytest.mark.parametrize("intermediate", MOE_INTERMEDIATE_SIZES)
127 +@pytest.mark.parametrize("group_size", [-1, 32, 64, 128])
128 +def test_marlin_moe_padded_intermediate(intermediate, group_size):
129 + # The MoE gate only admits shapes where the group does not straddle the
130 + # boundary, i.e. group divides the intermediate size.
131 + if group_size > 0 and intermediate % group_size != 0:
132 + pytest.skip("group straddles the boundary; rejected by the MoE gate")
133 +
134 + padded = marlin_moe_padded_intermediate(intermediate, group_size)
135 + assert padded >= intermediate
136 + # Valid MoE thread tile: gate-up n = 2*intermediate % 128, down k % 64.
137 + assert (2 * padded) % 128 == 0
138 + assert padded % 64 == 0
139 + if group_size > 0:
140 + assert padded % group_size == 0
141 +
142 + # Minimal: no smaller valid intermediate exists.
143 + for cand in range(intermediate, padded):
144 + if (
145 + (2 * cand) % 128 == 0
146 + and cand % 64 == 0
147 + and (group_size <= 0 or cand % group_size == 0)
148 + ):
149 + pytest.fail(f"{cand} beats {padded}")
150 +
151 + # Already-tile-aligned sizes pass through unchanged (zero hot-path cost).
152 + if intermediate % 64 == 0:
153 + assert padded == intermediate
154 +
155 +
156 +def test_marlin_moe_pad_helpers_shapes():
157 + from vllm.model_executor.layers.fused_moe.oracle.int_wna16 import (
158 + _pad_rows,
159 + _pad_w13_bias,
160 + _pad_w13_shard_cols,
161 + )
162 +
163 + E, rows, N, padded_N = 2, 8, 96, 128
164 +
165 + # w13 stores the two gate/up shards along the last dim; padding each shard
166 + # must preserve the loaded values and zero the padded columns.
167 + w13 = torch.arange(E * rows * 2 * N).reshape(E, rows, 2 * N).float()
168 + padded = _pad_w13_shard_cols(w13, N, padded_N)
169 + assert padded.shape == (E, rows, 2 * padded_N)
170 + shards = padded.view(E, rows, 2, padded_N)
171 + orig = w13.view(E, rows, 2, N)
172 + assert torch.equal(shards[..., :N], orig)
173 + assert shards[..., N:].abs().sum() == 0
174 +
175 + # w2 stores the intermediate dim in the rows.
176 + w2 = torch.ones(E, N // 32, 16)
177 + padded = _pad_rows(w2, padded_N // 32)
178 + assert padded.shape == (E, padded_N // 32, 16)
179 + assert padded[:, N // 32 :, :].abs().sum() == 0
180 +
181 + bias = torch.arange(E * 2 * N).reshape(E, 2 * N).float()
182 + padded = _pad_w13_bias(bias, N, padded_N)
183 + assert padded.shape == (E, 2 * padded_N)
184 + bias_shards = padded.view(E, 2, padded_N)
185 + assert torch.equal(bias_shards[..., :N], bias.view(E, 2, N))
186 + assert bias_shards[..., N:].abs().sum() == 0
187 +
188 +
189 def _gpu_marlin_unsupported() -> bool:
190 return not (
191 current_platform.is_cuda() and current_platform.has_device_capability(80)
@@ -468,3 +539,321 @@ def test_check_marlin_supports_layer_allow_tile_padding():
539 # A group straddling the TP shard cannot be fixed by padding
540 layer = _FakeLinear(4608, 4672, input_size=18688)
541 assert not check_marlin_supports_layer(layer, 128, allow_tile_padding=True)
542 +
543 +
544 +@pytest.mark.skipif(
545 + _gpu_marlin_unsupported(),
546 + reason="Marlin is not supported on this GPU type.",
547 +)
548 +@pytest.mark.parametrize("group_size", [-1, 32])
549 +@pytest.mark.parametrize("shape", [(96, 256, 8), (160, 512, 4)])
550 +def test_gptq_marlin_moe_padded_round_trip(shape, group_size):
551 + """Pad a tile-misaligned MoE intermediate the way the WNA16 Marlin MoE prep
552 + does, run the real repack + fused_marlin_moe, and check against the
553 + dequantized reference. Symmetric int4's quantized zero decodes to -8, so the
554 + padded region only stays out of the output via the zero-padded scales.
555 + """
556 + from tests.kernels.utils import torch_experts
557 + from vllm.config import VllmConfig, set_current_vllm_config
558 + from vllm.model_executor.layers.fused_moe import fused_topk
559 + from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
560 + fused_marlin_moe,
561 + )
562 + from vllm.model_executor.layers.fused_moe.oracle.int_wna16 import (
563 + _pad_rows,
564 + _pad_w13_shard_cols,
565 + )
566 + from vllm.model_executor.layers.quantization.utils.marlin_utils import (
567 + marlin_moe_padded_intermediate,
568 + marlin_moe_permute_scales,
569 + )
570 +
571 + n, k, e = shape
572 + topk, m = 2, 33
573 + padded_n = marlin_moe_padded_intermediate(n, group_size)
574 + assert padded_n != n, "test should exercise padding"
575 +
576 + dtype = torch.float16
577 + device = torch.device("cuda")
578 + quant_type = scalar_types.uint4b8
579 + bits = quant_type.size_bits
580 + pack = 32 // bits
581 +
582 + a = torch.randn((m, k), device=device, dtype=dtype) / 10
583 + w1 = torch.randn((e, 2 * n, k), device=device, dtype=dtype) / k**0.5
584 + w2 = torch.randn((e, k, n), device=device, dtype=dtype) / n**0.5
585 +
586 + def quant(w, size_k, size_n):
587 + # w is (size_n, size_k); gptq expects (size_k, size_n).
588 + ref, q_w, s, _, _ = gptq_quantize_weights(
589 + w.T, quant_type, group_size, act_order=False
590 + )
591 + return ref, gptq_pack(q_w, bits, size_k, size_n), s
592 +
593 + w13_qw, w13_s, w13_ref = [], [], []
594 + w2_qw, w2_s, w2_ref = [], [], []
595 + for i in range(e):
596 + ref, qw, s = quant(w1[i], k, 2 * n)
597 + w13_ref.append(ref.T) # (2n, k)
598 + w13_qw.append(qw)
599 + w13_s.append(s)
600 + ref, qw, s = quant(w2[i], n, k)
601 + w2_ref.append(ref.T) # (k, n)
602 + w2_qw.append(qw)
603 + w2_s.append(s)
604 +
605 + w13_qweight = torch.stack(w13_qw)
606 + w2_qweight = torch.stack(w2_qw)
607 + w13_scales = torch.stack(w13_s)
608 + w2_scales = torch.stack(w2_s)
609 + w1_ref = torch.stack(w13_ref) # (e, 2n, k)
610 + w2_ref = torch.stack(w2_ref) # (e, k, n)
611 +
612 + # Pad the intermediate via the production helpers.
613 + w13_qweight = _pad_w13_shard_cols(w13_qweight, n, padded_n)
614 + w2_qweight = _pad_rows(w2_qweight, padded_n // pack)
615 + w13_scales = _pad_w13_shard_cols(w13_scales, n, padded_n)
616 + if group_size > 0:
617 + w2_scales = _pad_rows(w2_scales, padded_n // group_size)
618 +
619 + sort_idx = torch.empty((e, 0), dtype=torch.int32, device=device)
620 + marlin_w13 = ops.gptq_marlin_moe_repack(
621 + w13_qweight, sort_idx, w13_qweight.shape[1] * pack, w13_qweight.shape[2], bits
622 + )
623 + marlin_w2 = ops.gptq_marlin_moe_repack(
624 + w2_qweight, sort_idx, w2_qweight.shape[1] * pack, w2_qweight.shape[2], bits
625 + )
626 + group_or_pack = group_size if group_size != -1 else pack
627 + marlin_w13_s = marlin_moe_permute_scales(
628 + s=w13_scales, size_k=n, size_n=w13_scales.shape[2], group_size=group_size
629 + )
630 + marlin_w2_s = marlin_moe_permute_scales(
631 + s=w2_scales,
632 + size_k=w2_scales.shape[1] * group_or_pack,
633 + size_n=w2_scales.shape[2],
634 + group_size=group_size,
635 + )
636 +
637 + score = torch.randn((m, e), device=device, dtype=dtype)
638 + topk_weights, topk_ids, _ = fused_topk(a, score, topk, False)
639 +
640 + marlin_out = fused_marlin_moe(
641 + a,
642 + marlin_w13,
643 + marlin_w2,
644 + None,
645 + None,
646 + marlin_w13_s,
647 + marlin_w2_s,
648 + topk_weights,
649 + topk_ids,
650 + quant_type_id=quant_type.id,
651 + global_num_experts=e,
652 + is_k_full=True,
653 + )
654 + with set_current_vllm_config(VllmConfig()):
655 + ref = torch_experts(
656 + a,
657 + w1_ref,
658 + w2_ref,
659 + topk_weight=topk_weights,
660 + topk_ids=topk_ids,
661 + global_num_experts=e,
662 + )
663 +
664 + torch.testing.assert_close(marlin_out, ref, atol=5e-2, rtol=0)
665 +
666 +
667 +def test_check_moe_marlin_supports_layer_padding():
668 + from vllm.model_executor.layers.quantization.utils.marlin_utils import (
669 + check_moe_marlin_supports_layer,
670 + )
671 +
672 + def make_layer(hidden, intermediate):
673 + layer = SimpleNamespace()
674 + layer.hidden_size = hidden
675 + layer.apply_router_weight_on_input = False
676 + layer.moe_config = SimpleNamespace(
677 + intermediate_size_per_partition_unpadded=intermediate
678 + )
679 + return layer
680 +
681 + # group=32 with intermediate % 64 != 0: rejected strictly, accepted w/ padding
682 + layer = make_layer(4096, 96)
683 + assert not check_moe_marlin_supports_layer(layer, 32)
684 + assert check_moe_marlin_supports_layer(layer, 32, allow_tile_padding=True)
685 + # channelwise misaligned intermediate is paddable
686 + assert check_moe_marlin_supports_layer(layer, -1, allow_tile_padding=True)
687 +
688 + # A group straddling the boundary cannot be fixed by padding
689 + layer = make_layer(4096, 176)
690 + assert not check_moe_marlin_supports_layer(layer, 128, allow_tile_padding=True)
691 +
692 + # hidden_size is the MoE I/O extent and is never padded
693 + layer = make_layer(4090, 128)
694 + assert not check_moe_marlin_supports_layer(layer, 64, allow_tile_padding=True)
695 +
696 +
697 +@pytest.mark.skipif(
698 + _gpu_marlin_unsupported() or not is_fp8_marlin_supported(),
699 + reason="FP8 Marlin is not supported on this GPU type.",
700 +)
701 +@pytest.mark.parametrize("quant", ["channel", "tensor"])
702 +@pytest.mark.parametrize("shape", [(96, 256, 8), (160, 512, 4)])
703 +def test_fp8_marlin_moe_padded_round_trip(shape, quant):
704 + """FP8 weight-only MoE: pad a tile-misaligned intermediate and check the
705 + real prepare + fused_marlin_moe against the dequantized reference."""
706 + from tests.kernels.utils import torch_experts
707 + from vllm.config import VllmConfig, set_current_vllm_config
708 + from vllm.model_executor.layers.fused_moe import fused_topk
709 + from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
710 + fused_marlin_moe,
711 + )
712 + from vllm.model_executor.layers.quantization.utils.marlin_utils import (
713 + marlin_moe_intermediate_size,
714 + marlin_moe_padded_intermediate,
715 + )
716 + from vllm.model_executor.layers.quantization.utils.marlin_utils_fp8 import (
717 + prepare_fp8_moe_layer_for_marlin,
718 + )
719 +
720 + n, k, e = shape
721 + topk, m = 2, 33
722 + fp8 = torch.float8_e4m3fn
723 + dtype = torch.bfloat16
724 + device = torch.device("cuda")
725 + padded_n = marlin_moe_padded_intermediate(n, -1)
726 + assert padded_n != n
727 +
728 + def q(w): # (out, in) -> fp8 weight, scale, dequant reference
729 + dim = None if quant == "tensor" else 1
730 + s = (w.abs().amax(dim, keepdim=dim is not None) / 448.0).clamp(min=1e-8)
731 + wq = (w / s).clamp(-448, 448).to(fp8)
732 + ref = wq.to(dtype) * s.to(dtype)
733 + s = s.reshape(1) if quant == "tensor" else s.squeeze(1)
734 + return wq, s, ref
735 +
736 + a = torch.randn((m, k), device=device, dtype=dtype) / 10
737 + w1 = torch.randn((e, 2 * n, k), device=device, dtype=dtype) / k**0.5
738 + w2 = torch.randn((e, k, n), device=device, dtype=dtype) / n**0.5
739 + w13_q, w13_s, w1_ref = zip(*(q(w1[i]) for i in range(e)))
740 + w2_q, w2_s, w2_ref = zip(*(q(w2[i]) for i in range(e)))
741 +
742 + w13_weight, w2_weight = torch.stack(w13_q), torch.stack(w2_q)
743 + layer = SimpleNamespace(
744 + num_experts=e,
745 + hidden_size=k,
746 + intermediate_size_per_partition=n,
747 + orig_dtype=dtype,
748 + w13_weight=w13_weight,
749 + )
750 + pw13, pw2, ps13, ps2 = prepare_fp8_moe_layer_for_marlin(
751 + layer, w13_weight, w2_weight, torch.stack(w13_s), torch.stack(w2_s)
752 + )
753 + assert marlin_moe_intermediate_size(pw13, pw2) == padded_n
754 +
755 + score = torch.randn((m, e), device=device, dtype=dtype)
756 + topk_weights, topk_ids, _ = fused_topk(a, score, topk, False)
757 + out = fused_marlin_moe(
758 + a,
759 + pw13,
760 + pw2,
761 + None,
762 + None,
763 + ps13,
764 + ps2,
765 + topk_weights,
766 + topk_ids,
767 + quant_type_id=scalar_types.float8_e4m3fn.id,
768 + global_num_experts=e,
769 + is_k_full=True,
770 + workspace=layer.workspace,
771 + )
772 + with set_current_vllm_config(VllmConfig()):
773 + ref = torch_experts(
774 + a,
775 + torch.stack(w1_ref),
776 + torch.stack(w2_ref),
777 + topk_weight=topk_weights,
778 + topk_ids=topk_ids,
779 + global_num_experts=e,
780 + )
781 + torch.testing.assert_close(out, ref, atol=8e-2, rtol=0)
782 +
783 +
784 +@pytest.mark.skipif(
785 + _gpu_marlin_unsupported() or not is_fp8_marlin_supported(),
786 + reason="FP8 Marlin is not supported on this GPU type.",
787 +)
788 +@pytest.mark.parametrize("shape", [(96, 256, 8), (160, 512, 4)])
789 +def test_mxfp8_marlin_moe_padded_round_trip(shape):
790 + """MXFP8 weight-only MoE round-trip at a tile-misaligned intermediate, with
791 + unit e8m0 scales so the reference is the exact fp8 dequant."""
792 + from tests.kernels.utils import torch_experts
793 + from vllm.config import VllmConfig, set_current_vllm_config
794 + from vllm.model_executor.layers.fused_moe import fused_topk
795 + from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
796 + fused_marlin_moe,
797 + )
798 + from vllm.model_executor.layers.quantization.utils.marlin_utils import (
799 + marlin_moe_intermediate_size,
800 + marlin_moe_padded_intermediate,
801 + )
802 + from vllm.model_executor.layers.quantization.utils.marlin_utils_fp8 import (
803 + prepare_mxfp8_moe_layer_for_marlin,
804 + )
805 +
806 + n, k, e = shape
807 + topk, m, gs, e8m0_one = 2, 33, 32, 127
808 + fp8 = torch.float8_e4m3fn
809 + dtype = torch.bfloat16
810 + device = torch.device("cuda")
811 + padded_n = marlin_moe_padded_intermediate(n, gs)
812 + assert padded_n != n
813 +
814 + a = torch.randn((m, k), device=device, dtype=dtype) / 10
815 + w13_weight = torch.randn((e, 2 * n, k), device=device, dtype=dtype) / k**0.5
816 + w2_weight = torch.randn((e, k, n), device=device, dtype=dtype) / n**0.5
817 + w13_weight = w13_weight.clamp(-448, 448).to(fp8)
818 + w2_weight = w2_weight.clamp(-448, 448).to(fp8)
819 + w13_scale = torch.full(
820 + (e, 2 * n, k // gs), e8m0_one, dtype=torch.uint8, device=device
821 + )
822 + w2_scale = torch.full((e, k, n // gs), e8m0_one, dtype=torch.uint8, device=device)
823 +
824 + layer = SimpleNamespace(
825 + num_experts=e, hidden_size=k, intermediate_size_per_partition=n
826 + )
827 + with set_current_vllm_config(VllmConfig()):
828 + pw13, pw2, ps13, ps2 = prepare_mxfp8_moe_layer_for_marlin(
829 + layer, w13_weight, w2_weight, w13_scale, w2_scale
830 + )
831 + assert marlin_moe_intermediate_size(pw13, pw2) == padded_n
832 +
833 + score = torch.randn((m, e), device=device, dtype=dtype)
834 + topk_weights, topk_ids, _ = fused_topk(a, score, topk, False)
835 + out = fused_marlin_moe(
836 + a,
837 + pw13,
838 + pw2,
839 + None,
840 + None,
841 + ps13,
842 + ps2,
843 + topk_weights,
844 + topk_ids,
845 + quant_type_id=scalar_types.float8_e4m3fn.id,
846 + global_num_experts=e,
847 + is_k_full=True,
848 + workspace=layer.workspace,
849 + )
850 + with set_current_vllm_config(VllmConfig()):
851 + ref = torch_experts(
852 + a,
853 + w13_weight.to(dtype),
854 + w2_weight.to(dtype),
855 + topk_weight=topk_weights,
856 + topk_ids=topk_ids,
857 + global_num_experts=e,
858 + )
859 + torch.testing.assert_close(out, ref, atol=8e-2, rtol=0)
vllm/model_executor/layers/fused_moe/oracle/int_wna16.py
+71
@@ -29,6 +29,7 @@ from vllm.model_executor.layers.fused_moe.experts.trtllm_mxint4_moe import (
29 from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
30 from vllm.model_executor.layers.quantization.utils.marlin_utils import (
31 marlin_act_int8_process_scales,
32 + marlin_moe_padded_intermediate,
33 marlin_moe_permute_scales,
34 marlin_permute_bias,
35 moe_awq_to_marlin_zero_points,
@@ -361,6 +362,34 @@ def _process_weights_flashinfer(
362 )
363
364
365 +def _pad_w13_shard_cols(x: torch.Tensor, unit: int, padded_unit: int) -> torch.Tensor:
366 + """Zero-pad each of the two gate/up shards of a ``(E, rows, 2 * unit)``
367 + tensor along its last dim, from ``unit`` to ``padded_unit`` columns."""
368 + if padded_unit == unit:
369 + return x
370 + e, rows, _ = x.shape
371 + x = x.view(e, rows, 2, unit)
372 + x = torch.nn.functional.pad(x, (0, padded_unit - unit))
373 + return x.reshape(e, rows, 2 * padded_unit).contiguous()
374 +
375 +
376 +def _pad_rows(x: torch.Tensor, padded_rows: int) -> torch.Tensor:
377 + """Zero-pad a ``(E, rows, cols)`` tensor to ``padded_rows`` rows."""
378 + if padded_rows == x.size(1):
379 + return x
380 + return torch.nn.functional.pad(x, (0, 0, 0, padded_rows - x.size(1)))
381 +
382 +
383 +def _pad_w13_bias(bias: torch.Tensor, n: int, padded_n: int) -> torch.Tensor:
384 + """Zero-pad each gate/up shard of a ``(E, 2 * n)`` bias to ``padded_n``."""
385 + if padded_n == n:
386 + return bias
387 + e = bias.size(0)
388 + bias = bias.view(e, 2, n)
389 + bias = torch.nn.functional.pad(bias, (0, padded_n - n))
390 + return bias.reshape(e, 2 * padded_n).contiguous()
391 +
392 +
393 def _process_weights_marlin(
394 layer: torch.nn.Module,
395 input_dtype: torch.dtype | None,
@@ -431,6 +460,29 @@ def _process_weights_marlin(
460 marlin_w13_scales = w13_scales
461 marlin_w2_scales = w2_scales
462
463 + # --- Pad the intermediate size to a valid Marlin thread tile ---
464 + # GPTQ packs along K: w13's N is in the (shard) columns, w2's N in the rows.
465 + # Act-order keeps the strict shape and is never padded.
466 + N = layer.intermediate_size_per_partition
467 + padded_N = marlin_moe_padded_intermediate(N, group_size)
468 + if padded_N != N:
469 + assert actorder != "group", (
470 + "Marlin MoE thread-tile padding is unsupported with act-order"
471 + )
472 + marlin_w13_qweight = _pad_w13_shard_cols(marlin_w13_qweight, N, padded_N)
473 + marlin_w2_qweight = _pad_rows(marlin_w2_qweight, padded_N // pack_factor)
474 + marlin_w13_scales = _pad_w13_shard_cols(marlin_w13_scales, N, padded_N)
475 + if group_size > 0:
476 + marlin_w2_scales = _pad_rows(marlin_w2_scales, padded_N // group_size)
477 + if w13_qzeros is not None:
478 + w13_qzeros = _pad_w13_shard_cols(
479 + w13_qzeros, N // pack_factor, padded_N // pack_factor
480 + )
481 + if w2_qzeros is not None and group_size > 0:
482 + w2_qzeros = _pad_rows(w2_qzeros, padded_N // group_size)
483 + if w13_bias is not None:
484 + w13_bias = _pad_w13_bias(w13_bias, N, padded_N)
485 +
486 # --- Process act_order (g_idx) ---
487 if actorder == "group":
488 num_experts = w13_g_idx.shape[0]
@@ -608,6 +660,25 @@ def _process_awq_weights_marlin(
660 w13_scales = w13_scales.data * 512
661 w2_scales = w2_scales.data * 512
662
663 + # --- Pad the intermediate size to a valid Marlin thread tile ---
664 + # AWQ packs along N: w13's N is in the (shard) columns, w2's N in the rows.
665 + N = layer.intermediate_size_per_partition
666 + padded_N = marlin_moe_padded_intermediate(N, group_size)
667 + if padded_N != N:
668 + w13_qweight = _pad_w13_shard_cols(
669 + w13_qweight, N // pack_factor, padded_N // pack_factor
670 + )
671 + w2_qweight = _pad_rows(w2_qweight, padded_N)
672 + w13_scales = _pad_w13_shard_cols(w13_scales, N, padded_N)
673 + w13_qzeros = _pad_w13_shard_cols(
674 + w13_qzeros, N // pack_factor, padded_N // pack_factor
675 + )
676 + if group_size > 0:
677 + w2_scales = _pad_rows(w2_scales, padded_N // group_size)
678 + w2_qzeros = _pad_rows(w2_qzeros, padded_N // group_size)
679 + if w13_bias is not None:
680 + w13_bias = _pad_w13_bias(w13_bias, N, padded_N)
681 +
682 w13_g_idx_sort_indices = torch.nn.Parameter(
683 torch.empty((num_experts, 0), dtype=torch.int32, device=device),
684 requires_grad=False,
vllm/model_executor/layers/quantization/auto_awq.py
+3 -1
@@ -339,7 +339,9 @@ class AutoAWQConfig(QuantizationConfig):
339 ):
340 return UnquantizedFusedMoEMethod(layer.moe_config)
341
342 - if not check_moe_marlin_supports_layer(layer, self.group_size):
342 + if not check_moe_marlin_supports_layer(
343 + layer, self.group_size, allow_tile_padding=True
344 + ):
345 logger.warning_once(
346 f"Layer '{prefix}' is not supported by AutoAWQMoEMarlin. "
347 "Falling back to Moe WNA16 kernels."
vllm/model_executor/layers/quantization/auto_gptq.py
+3 -1
@@ -243,7 +243,9 @@ class AutoGPTQConfig(QuantizationConfig):
243 if isinstance(layer, RoutedExperts):
244 from vllm.model_executor.layers.quantization.moe_wna16 import MoeWNA16Config
245
246 - if not check_moe_marlin_supports_layer(layer, self.group_size):
246 + if not check_moe_marlin_supports_layer(
247 + layer, self.group_size, allow_tile_padding=not self.desc_act
248 + ):
249 logger.warning_once(
250 f"Layer '{prefix}' is not supported by GPTQMoeMarlin. "
251 "Falling back to Moe WNA16 kernels."
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe.py
+9 -6
@@ -96,15 +96,18 @@ class CompressedTensorsMoEMethod(FusedMoEMethodBase):
96 )
97
98 # Prefer to use the MarlinMoE kernel when it is supported.
99 + is_actorder = (
100 + weight_quant.strategy == QuantizationStrategy.GROUP
101 + and weight_quant.actorder
102 + in (ActivationOrdering.GROUP, ActivationOrdering.DYNAMIC)
103 + )
104 if (
100 - not check_moe_marlin_supports_layer(layer, group_size)
105 + not check_moe_marlin_supports_layer(
106 + layer, group_size, allow_tile_padding=not is_actorder
107 + )
108 or current_platform.is_rocm()
109 ):
103 - if (
104 - weight_quant.strategy == QuantizationStrategy.GROUP
105 - and weight_quant.actorder
106 - in (ActivationOrdering.GROUP, ActivationOrdering.DYNAMIC)
107 - ):
110 + if is_actorder:
111 raise ValueError(
112 "WNA16MoE is not supported with actorder=group/dynamic."
113 )
vllm/model_executor/layers/quantization/utils/marlin_utils.py
+43 -10
@@ -330,12 +330,43 @@ def check_marlin_supports_layer(
330 )[0]
331
332
333 -def check_moe_marlin_supports_layer(layer: RoutedExperts, group_size: int) -> bool:
333 +def marlin_moe_padded_intermediate(intermediate_size: int, group_size: int = -1) -> int:
334 + """Smallest MoE intermediate size satisfying the Marlin MoE thread tiles.
335 +
336 + The kernel needs gate-up ``2 * intermediate % 128 == 0`` and down
337 + ``intermediate % 64 == 0``, i.e. ``intermediate % 64 == 0``. A misaligned
338 + size is zero-padded to the next valid tile at weight prep, kept a multiple
339 + of ``group_size`` so the group count stays integral. The padded region never
340 + reaches the MoE output: w13's padded output channels are zeroed by the
341 + zero-padded scales, so the padded inputs to w2 are zero.
342 + """
343 + group = group_size if group_size > 0 else 1
344 + padded = round_up(intermediate_size, math.lcm(64, group))
345 + if padded != intermediate_size:
346 + logger.warning_once(
347 + "Marlin requires thread-tile padding for the MoE intermediate size "
348 + "of some layers in this model. Padded experts pad/slice activations "
349 + "on every forward; performance may be degraded."
350 + )
351 + return padded
352 +
353 +
354 +def check_moe_marlin_supports_layer(
355 + layer: RoutedExperts, group_size: int, allow_tile_padding: bool = False
356 +) -> bool:
357 + """Whether the fused MoE Marlin kernel supports ``layer``.
358 +
359 + Callers without act-order may pass ``allow_tile_padding=True``: a
360 + tile-misaligned intermediate size is then zero-padded to a valid thread
361 + tile at weight prep (see marlin_moe_padded_intermediate), so only a group
362 + straddling the padded boundary stays unsupported. hidden_size is the MoE
363 + I/O extent and is never padded. Act-order keeps the strict shape.
364 + """
365 if current_platform.is_rocm():
366 return False
367 hidden_size = layer.hidden_size
337 - # Note: The layer has not performed rounding on intermediate_size's at this
338 - # point. Use the unpadded size which won't change.
368 + # The layer has not rounded intermediate_size yet; use the stable unpadded
369 + # size. gate-up needs n=2*intermediate % 128, down needs k=intermediate % 64.
370 intermediate_size_per_partition = (
371 layer.moe_config.intermediate_size_per_partition_unpadded
372 )
@@ -343,13 +374,15 @@ def check_moe_marlin_supports_layer(layer: RoutedExperts, group_size: int) -> bo
374 # apply_router_weight_on_input is not supported for moe marlin
375 supports_router_weight = not layer.apply_router_weight_on_input
376
346 - # gate-up: (n, k) = (intermediate_size_per_partition * 2, hidden_size)
347 - # down: (n, k) = (hidden_size, intermediate_size_per_partition)
348 - # moe marlin requires n % 128 == 0 and k % 64 == 0
349 - supports_shape = (
350 - hidden_size % 128 == 0
351 - and intermediate_size_per_partition % max(64, group_size) == 0
352 - )
377 + if allow_tile_padding:
378 + supports_shape = hidden_size % 128 == 0 and (
379 + group_size <= 0 or intermediate_size_per_partition % group_size == 0
380 + )
381 + else:
382 + supports_shape = (
383 + hidden_size % 128 == 0
384 + and intermediate_size_per_partition % max(64, group_size) == 0
385 + )
386 supports_group_size = group_size in [-1, 32, 64, 128]
387 return supports_shape and supports_group_size and supports_router_weight
388
vllm/model_executor/layers/quantization/utils/marlin_utils_fp8.py
+55 -11
@@ -10,6 +10,7 @@ from vllm.model_executor.layers.quantization.utils.marlin_utils import (
10 USE_FP32_REDUCE_DEFAULT,
11 get_marlin_input_dtype,
12 marlin_make_workspace_new,
13 + marlin_moe_padded_intermediate,
14 marlin_pad_dim,
15 marlin_pad_qweight,
16 marlin_pad_scales,
@@ -216,6 +217,25 @@ def prepare_fp8_layer_for_marlin(
217 replace_parameter(layer, "bias", bias)
218
219
220 +def _moe_pad_shard_rows(x: torch.Tensor, n: int, padded_n: int) -> torch.Tensor:
221 + """Zero-pad each gate/up shard of a ``(E, 2 * n, ...)`` tensor to padded_n
222 + rows. FP8 zero decodes to 0.0, so the padded rows contribute nothing."""
223 + if padded_n == n:
224 + return x
225 + e = x.size(0)
226 + rest = x.shape[2:]
227 + x = x.view(e, 2, n, *rest)
228 + x = torch.nn.functional.pad(x, (0, 0) * len(rest) + (0, padded_n - n))
229 + return x.reshape(e, 2 * padded_n, *rest)
230 +
231 +
232 +def _moe_pad_last(x: torch.Tensor, n: int, padded_n: int) -> torch.Tensor:
233 + """Zero-pad the last dim of a ``(E, ..., n)`` tensor to padded_n."""
234 + if padded_n == n:
235 + return x
236 + return torch.nn.functional.pad(x, (0, padded_n - n))
237 +
238 +
239 def prepare_fp8_moe_layer_for_marlin(
240 layer: torch.nn.Module,
241 w13_weight: torch.Tensor,
@@ -246,6 +266,15 @@ def prepare_fp8_moe_layer_for_marlin(
266 n = layer.intermediate_size_per_partition
267 w13_n = w13_weight.size(1)
268 weight_block_size = getattr(layer, "weight_block_size", None)
269 + group_size = -1 if weight_block_size is None else weight_block_size[1]
270 +
271 + # Pad a tile-misaligned intermediate size to a valid Marlin thread tile.
272 + # FP8 zero decodes to 0.0, so padded weights drop out; the converted scales
273 + # are padded to match below (the padded values are irrelevant).
274 + padded_n = marlin_moe_padded_intermediate(n, group_size)
275 + if padded_n != n:
276 + w13_weight = _moe_pad_shard_rows(w13_weight, n, padded_n)
277 + w2_weight = _moe_pad_last(w2_weight, n, padded_n)
278
279 # WORKSPACE
280 device = layer.w13_weight.device
@@ -258,13 +287,7 @@ def prepare_fp8_moe_layer_for_marlin(
287 # Repack weights to marlin format
288 def repack_weight(name: str, weight: torch.Tensor) -> torch.Tensor:
289 tensor_list = []
261 - if "w13" in name:
262 - size_n, size_k = w13_n, k
263 - else:
264 - size_n, size_k = k, n
265 -
266 - assert weight.shape == (e, size_n, size_k)
267 -
290 + size_n, size_k = weight.size(1), weight.size(2)
291 for i in range(e):
292 qweight = pack_fp8_to_int32(weight[i], size_k_first=False)
293 qweight = qweight.T.contiguous()
@@ -280,9 +303,7 @@ def prepare_fp8_moe_layer_for_marlin(
303 w2_weight = repack_weight("w2", w2_weight)
304
305 # WEIGHT SCALES
283 - # Permute scales
284 - group_size = -1 if weight_block_size is None else weight_block_size[1]
285 -
306 + # Permute scales (convert at the original size, then pad to the tile).
307 def permute_scales(scales: torch.Tensor, name: str) -> torch.Tensor:
308 scales = scales.to(layer.orig_dtype)
309 tensor_list = []
@@ -320,6 +341,20 @@ def prepare_fp8_moe_layer_for_marlin(
341 # size_n may not divisible by block_size[0]
342 scales = scales[..., :size_n].contiguous()
343
344 + # Pad the converted (E, G, size_n) scales to the padded thread tile.
345 + if padded_n != n:
346 + if "w13" in name:
347 + g = scales.size(1)
348 + scales = scales.view(e, g, 2, n)
349 + scales = torch.nn.functional.pad(scales, (0, padded_n - n))
350 + scales = scales.reshape(e, g, 2 * padded_n)
351 + size_n = 2 * padded_n
352 + else:
353 + if group_size > 0:
354 + pad_groups = (padded_n - n) // group_size
355 + scales = torch.nn.functional.pad(scales, (0, 0, 0, pad_groups))
356 + size_k = padded_n
357 +
358 for i in range(e):
359 marlin_scales = marlin_permute_scales(
360 s=scales[i], size_k=size_k, size_n=size_n, group_size=group_size
@@ -497,10 +532,19 @@ def prepare_mxfp8_moe_layer_for_marlin(
532 """
533 group_size = 32
534 e = w13.shape[0]
500 - w13_n = w13.shape[1]
535 k = w13.shape[2]
536 n = w2.shape[2]
537
538 + # Pad a tile-misaligned intermediate size to a valid Marlin thread tile.
539 + padded_n = marlin_moe_padded_intermediate(n, group_size)
540 + if padded_n != n:
541 + w13 = _moe_pad_shard_rows(w13, n, padded_n)
542 + w13_scale = _moe_pad_shard_rows(w13_scale, n, padded_n)
543 + w2 = _moe_pad_last(w2, n, padded_n)
544 + w2_scale = _moe_pad_last(w2_scale, n // group_size, padded_n // group_size)
545 + n = padded_n
546 + w13_n = w13.shape[1]
547 +
548 device = w13.device
549 param_dtype = torch.get_default_dtype()
550 perm = torch.empty(0, dtype=torch.int, device=device)