[MiniMax-M3.1] Fix CI: lazy CuTe DSL imports, CUDA 13.0 skips, test configs

- Import the M3 CuTe DSL ops (fused MoE finalize, QKV+indexer epilogue)
  where they are used: a module-level import made every import of
  vllm.models.minimax_m3 require cutlass, which the ROCm and CPU images
  do not install.
- Skip the NVFP4 prefill tests when MSA cannot build the Q8KV4 prefill
  (CUDA < 13.4), with MSA's reason.
- Keep the ROCm-only amd_specialized_decode tests out of the B200
  MiniMax-M3 job.
- Compare FLASHINFER_TS with FA4 within one bf16 ulp instead of bit-exact.
- Give the M3 embed-sharing fixture a moe_backend and a TP=1 group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
This commit is contained in:
Yongye ZhuandClaude Opus 5.5 committed 2026-10-27 21:10:22 +00:00
1 parent 2bd0080ba8
commit 80b23fc460
6 files changed
+45 -17

No files matched your search

+1 -1
View File
@@ -282,7 +282,7 @@ steps:
- vllm/utils/flashinfer.py
- tests/models/minimax_m3/
commands:
- pytest -v -s models/minimax_m3
- pytest -v -s models/minimax_m3 --ignore=models/minimax_m3/amd_specialized_decode
# Catch-all for test files at the tests/kernels root. This job collects
# the whole root so new files are wired by default.
@@ -1105,6 +1105,14 @@ def _make_prefill_metadata(
return metadata, topk
def _skip_without_q8kv4_prefill(num_q_heads: int, num_kv_heads: int) -> None:
blocker = sparse_attention_msa_module._q8kv4_prefill_blocker(
num_q_heads, num_kv_heads, BLOCK_SIZE, TOPK
)
if blocker is not None:
pytest.skip(blocker)
@pytest.mark.parametrize(
("num_q_heads", "num_kv_heads"),
[pytest.param(64, 4, id="tp1"), pytest.param(16, 1, id="tp4")],
@@ -1117,6 +1125,7 @@ def test_msa_prefill_nvfp4_matches_triton_on_dequantized_cache(
including chunked prefill over cached context, with the fp8 q the fused
insert writes (the only q it writes on this path), on the CUTLASS Q8KV4
kernel."""
_skip_without_q8kv4_prefill(num_q_heads, num_kv_heads)
torch.manual_seed(0)
q_lens = [300, 128, 77]
seq_lens_list = [300, 640, 1100]
@@ -1187,6 +1196,7 @@ def test_msa_prefill_nvfp4_uses_the_builders_workspace(
grown by the call, and the output equals the per-call-allocation path's."""
from vllm.third_party.fmha_sm100.prefill_q8kv4 import PrefillWorkspace
_skip_without_q8kv4_prefill(num_q_heads, num_kv_heads)
torch.manual_seed(0)
q_lens = [300, 128, 77]
seq_lens_list = [300, 640, 1100]
+4 -2
View File
@@ -196,8 +196,10 @@ def test_dense_plan_matches_flash_attention_non_causal():
out=out,
)
# One page of KV reduces in the same order in both kernels.
torch.testing.assert_close(out.reshape(b * sq, hq, HEAD_DIM), ref, rtol=0, atol=0)
# Within one bf16 ulp: the kernels round differently on some SM100 parts.
torch.testing.assert_close(
out.reshape(b * sq, hq, HEAD_DIM), ref, rtol=1e-2, atol=2e-3
)
def test_metadata_tensors_keep_their_addresses_across_builds():
@@ -100,7 +100,9 @@ def minimax_pp_stage(monkeypatch):
monkeypatch.setattr(minimax_m3, "get_pp_group", lambda: group)
monkeypatch.setattr(parallel_state, "get_pp_group", lambda: group)
monkeypatch.setattr(parallel_state, "model_parallel_is_initialized", lambda: True)
monkeypatch.setattr(minimax_m3, "supports_fused_moe_finalize", lambda _: False)
monkeypatch.setattr(
minimax_m3.MiniMaxM3Model, "_init_fused_moe_finalize", lambda *_: None
)
monkeypatch.setattr(
minimax_m3,
"VocabParallelEmbedding",
@@ -160,7 +160,7 @@ def m3_config():
),
quant_config=None,
cache_config=None,
kernel_config=SimpleNamespace(monokernel=False),
kernel_config=SimpleNamespace(monokernel=False, moe_backend="auto"),
)
@@ -267,6 +267,10 @@ def test_m3_target_allocates_embedding_on_required_stage(
)
monkeypatch.setattr(model, "get_pp_group", pp)
monkeypatch.setattr("vllm.distributed.parallel_state.get_pp_group", pp)
monkeypatch.setattr(
"vllm.distributed.parallel_state.get_tp_group",
lambda: SimpleNamespace(world_size=1),
)
monkeypatch.setattr(model, "VocabParallelEmbedding", lambda *_, **kw: _embed())
monkeypatch.setattr(model, "make_layers", lambda *_, **kw: (0, 0, nn.ModuleList()))
monkeypatch.setattr(model, "MiniMAXGemmaRMSNorm", lambda *_, **kw: nn.Identity())
+22 -12
View File
@@ -128,15 +128,6 @@ from vllm.models.minimax_m3.common.sparse_attention import (
select_main_backend_and_impl_cls,
)
from vllm.models.minimax_m3.common.vision_tower import MiniMaxVLVisionModel
from vllm.models.minimax_m3.nvidia.ops.finalize_allreduce_norm import (
finalize_allreduce_gemma_rms_norm_mxfp8,
init_fused_moe_finalize,
supports_fused_moe_finalize,
)
from vllm.models.minimax_m3.nvidia.ops.qkv_index_fused_epilogue import (
minimax_m3_qkv_index_fused_epilogue,
register_qkv_index_fused_epilogue_warmup,
)
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.platforms import current_platform
from vllm.sequence import IntermediateTensors
@@ -906,6 +897,10 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
self.use_qkv_fused_epilogue = _supports_qkv_fused_epilogue(self)
if self.use_qkv_fused_epilogue:
from vllm.models.minimax_m3.nvidia.ops.qkv_index_fused_epilogue import (
register_qkv_index_fused_epilogue_warmup,
)
logger.info_once("MiniMax-M3: QKV+indexer GEMM with fused epilogue enabled")
block_size = vllm_config.cache_config.block_size
# The query is E4M3-only when CUTLASS decode serves the layer.
@@ -945,6 +940,10 @@ class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
) -> None:
"""qkv_proj + QK-norm/RoPE + KV/index-cache insert as one MXFP8 kernel,
writing q (bf16 or E4M3, as allocated) and index_q."""
from vllm.models.minimax_m3.nvidia.ops.qkv_index_fused_epilogue import (
minimax_m3_qkv_index_fused_epilogue,
)
weight = self.qkv_proj.weight
if weight.shape[1] != self.hidden_size:
weight = weight.t() # the CuTe-DSL kernel keeps a [K, N] view
@@ -1207,6 +1206,10 @@ class MiniMaxM3DecoderLayer(nn.Module):
# The preceding MoE left a small batch unfinalized: finalize,
# all-reduce, norm and MXFP8-quantize the qkv_proj input at once.
assert residual is not None
from vllm.models.minimax_m3.nvidia.ops.finalize_allreduce_norm import (
finalize_allreduce_gemma_rms_norm_mxfp8,
)
return finalize_allreduce_gemma_rms_norm_mxfp8(
hidden_states,
residual,
@@ -1331,10 +1334,10 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
layer.fuse_input_allreduce = idx > 0 and prev_defers
prev_defers = layer.ffn_all_reduce_deferred
self.fuse_final_norm_allreduce = prev_defers
if supports_fused_moe_finalize(vllm_config):
self._init_fused_moe_finalize()
if current_platform.is_cuda():
self._init_fused_moe_finalize(vllm_config)
def _init_fused_moe_finalize(self) -> None:
def _init_fused_moe_finalize(self, vllm_config: VllmConfig) -> None:
"""Let small batches leave a MoE's top-k finalize to the next layer's
input norm, which already fuses the TP all-reduce, and have it quantize
the normed rows to the MXFP8 its qkv_proj reads.
@@ -1344,6 +1347,13 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
in BF16, so the last layer keeps its own finalize. Builds a collective
kernel: every TP rank decides alike.
"""
from vllm.models.minimax_m3.nvidia.ops.finalize_allreduce_norm import (
init_fused_moe_finalize,
supports_fused_moe_finalize,
)
if not supports_fused_moe_finalize(vllm_config):
return
layers = self.layers[self.start_layer : self.end_layer]
deferring = [
layer.block_sparse_moe