Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions tensorrt_llm/_torch/models/modeling_minimaxm3.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
# and flash SDPA does not accept attn_mask.
_DENSE_SDPA_BACKENDS = [SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH]


# ---------------------------------------------------------------------------
# Config normalization helpers
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1802,6 +1803,10 @@ def __init__(self, model_config: "ModelConfig[PretrainedConfig]"):
for layer_idx in range(config.num_hidden_layers)
]
)
# The executor owns the authoritative CUDA-graph configuration. Mark
# this model as eligible here and let the executor enable the automatic
# FlashInfer MXFP8 path only when its decode graph runner is active.
self._use_flashinfer_mxfp8_decode_graph_default = True
# Final norm is a plain (non-Gemma) RMSNorm for the same reason as the
# layer-boundary norms (see MiniMaxM3DecoderLayer.__init__): it doubles
# as the last layer's next_layer_layernorm, so the last MoE/MLP output
Expand Down
147 changes: 132 additions & 15 deletions tensorrt_llm/_torch/modules/linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import math
import os
from abc import ABC, abstractmethod
from collections.abc import Iterator
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
from typing import ClassVar, Dict, List, Optional, Union

Expand Down Expand Up @@ -3033,28 +3036,121 @@ def _mxfp8_cutlass_op_available() -> bool:
) and torch.cuda.get_device_capability()[0] >= 10


_FLASHINFER_MXFP8_AUTOTUNE_ACTIVE = ContextVar(
"flashinfer_mxfp8_autotune_active", default=False)
_FLASHINFER_MXFP8_DECODE_GRAPH_CAPTURE_ACTIVE = ContextVar(
"flashinfer_mxfp8_decode_graph_capture_active", default=False)


@contextmanager
def flashinfer_mxfp8_autotune() -> Iterator[None]:
"""Tune FlashInfer MXFP8 tactics while enabling auto-dispatched calls."""
from flashinfer import autotune

token = _FLASHINFER_MXFP8_AUTOTUNE_ACTIVE.set(True)
try:
with autotune():
yield
finally:
_FLASHINFER_MXFP8_AUTOTUNE_ACTIVE.reset(token)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


@contextmanager
def flashinfer_mxfp8_decode_graph_capture() -> Iterator[None]:
"""Enable auto-dispatched FlashInfer calls only for decode graph capture."""
token = _FLASHINFER_MXFP8_DECODE_GRAPH_CAPTURE_ACTIVE.set(True)
try:
yield
finally:
_FLASHINFER_MXFP8_DECODE_GRAPH_CAPTURE_ACTIVE.reset(token)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


class MXFP8LinearMethod(LinearMethodBase):
"""MXFP8 weights (e4m3 + UE8M0 1x32) x dynamic MXFP8 activations (W8A8).

Two execution paths share a common loader:
Three execution paths share a common loader:
- Reference (no CUTLASS op compiled): dequantize the weight to compute
dtype and run F.linear. Slow but correct -- used to seed M1 tests and
as a portable fallback.
- CUTLASS (Blackwell sm100/103 + mxfp8_mxfp8_gemm op present): dynamic
MXFP8 activation quantize + block-scaled e4m3xe4m3 GEMM.

The path is selected at create_weights time via _mxfp8_cutlass_op_available;
the weight_scale tensor's layout matches the chosen path (2D [O,K/32] for
the reference, 1D padded swizzled for CUTLASS) so apply() stays branchless.
- FlashInfer: reuse the CUTLASS-layout activations, weights, and scales
with ``mm_mxfp8``. MiniMax-M3 enables this path automatically only
while tuning or capturing decode CUDA graphs; eager execution remains
on the native TensorRT-LLM op.

``TRTLLM_MXFP8_GEMM_BACKEND`` can explicitly select ``trtllm``,
``flashinfer``, or ``auto``. The reference layout is 2D [O,K/32]; both
compiled backends consume the same 1D padded swizzled scale layout.
"""
BLOCK_SIZE = 32
# Swizzled-SF layout padding (matches W4A8MXFP4FP8: rows->128, cols/SFblock->4).
_SF_ROW_PAD = 128
_SF_COL_PAD = 4

def __init__(self):
def __init__(self) -> None:
super().__init__()
self.use_cutlass = _mxfp8_cutlass_op_available()
self.backend = os.environ.get("TRTLLM_MXFP8_GEMM_BACKEND", "trtllm")
if self.backend not in ("trtllm", "flashinfer", "auto"):
raise ValueError("TRTLLM_MXFP8_GEMM_BACKEND must be 'trtllm', "
f"'flashinfer', or 'auto', got {self.backend!r}")
self._flashinfer_mxfp8 = None
self._flashinfer_autotuned = False
if self.backend == "flashinfer":
self._load_flashinfer(required=True)
elif self.backend == "auto" and not self._load_flashinfer(
required=False):
self.backend = "trtllm"

@property
def uses_flashinfer(self) -> bool:
return self.backend in ("flashinfer", "auto")

@property
def needs_flashinfer_autotune(self) -> bool:
return self.uses_flashinfer and self._flashinfer_mxfp8 is not None

def _load_flashinfer(self, *, required: bool) -> bool:
if not self.use_cutlass:
if required:
raise RuntimeError(
"FlashInfer MXFP8 GEMM requires the TensorRT-LLM MXFP8 "
"quantization ops on Blackwell")
return False
try:
from flashinfer import autotune, mm_mxfp8
if not callable(autotune):
raise ImportError("flashinfer.autotune is unavailable")
except ImportError as error:
if required:
raise RuntimeError(
"TRTLLM_MXFP8_GEMM_BACKEND=flashinfer requires the "
"pinned flashinfer-python package") from error
logger.warning_once(
"FlashInfer MXFP8 is unavailable; using the native "
"TensorRT-LLM GEMM backend.",
key="flashinfer_mxfp8_unavailable")
return False
self._flashinfer_mxfp8 = mm_mxfp8
return True

def enable_flashinfer_auto(self) -> bool:
"""Enable graph-only FlashInfer dispatch unless the user overrode it."""
if "TRTLLM_MXFP8_GEMM_BACKEND" in os.environ:
return self.backend == "auto"
if not self._load_flashinfer(required=False):
return False
self.backend = "auto"
return True

def mark_flashinfer_autotuned(self) -> None:
self._flashinfer_autotuned = True

def disable_flashinfer_auto(self) -> None:
if self.backend == "auto":
self.backend = "trtllm"
self._flashinfer_autotuned = False

@classmethod
def _swizzled_scale_size(cls, out_features: int, in_features: int) -> int:
Expand Down Expand Up @@ -3100,15 +3196,36 @@ def apply(self, module: Linear, input: torch.Tensor,
# the CUTLASS block-scaled e4m3xe4m3 GEMM.
act_e4m3, act_sf = torch.ops.trtllm.mxfp8_quantize(
input.contiguous(), True)
# globalScale is the alpha multiplier; pure MXFP8xMXFP8 uses 1.0.
global_scale = torch.ones([1],
dtype=torch.float32,
device=input.device)
output = torch.ops.trtllm.mxfp8_mxfp8_gemm(act_e4m3, act_sf,
module.weight,
module.weight_scale,
global_scale,
module.dtype)
use_flashinfer = self.backend == "flashinfer" or (
self.backend == "auto" and
(_FLASHINFER_MXFP8_AUTOTUNE_ACTIVE.get() or
(self._flashinfer_autotuned
and _FLASHINFER_MXFP8_DECODE_GRAPH_CAPTURE_ACTIVE.get())))
if use_flashinfer:
flashinfer_mxfp8 = self._flashinfer_mxfp8
assert flashinfer_mxfp8 is not None
output = flashinfer_mxfp8(
act_e4m3,
module.weight.t(),
act_sf,
module.weight_scale,
out_dtype=module.dtype,
use_8x4_sf_layout=False,
backend="cutlass",
)
else:
# globalScale is the alpha multiplier; pure MXFP8xMXFP8 uses 1.0.
global_scale = torch.ones([1],
dtype=torch.float32,
device=input.device)
output = torch.ops.trtllm.mxfp8_mxfp8_gemm(
act_e4m3,
act_sf,
module.weight,
module.weight_scale,
global_scale,
module.dtype,
)
if bias is not None:
output = output + bias
else:
Expand Down
88 changes: 77 additions & 11 deletions tensorrt_llm/_torch/pyexecutor/model_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1574,10 +1574,33 @@ def _release_megamoe_profiling_scratch():

def _run_autotuner_warmup(self, resource_manager: ResourceManager):
"""Runs forward passes to populate the autotuner cache."""
if not self.llm_args.enable_autotuner:
from ..modules.linear import (MXFP8LinearMethod,
flashinfer_mxfp8_autotune)

enable_trtllm_autotuner = self.llm_args.enable_autotuner
use_mxfp8_flashinfer_graph_default = (
self.cuda_graph_runner.enabled
and "TRTLLM_MXFP8_GEMM_BACKEND" not in os.environ and any(
getattr(module, "_use_flashinfer_mxfp8_decode_graph_default",
False) for module in self.model.modules()))
flashinfer_mxfp8_methods = []
for module in self.model.modules():
quant_method = getattr(module, "quant_method", None)
if not isinstance(quant_method, MXFP8LinearMethod):
continue
if use_mxfp8_flashinfer_graph_default:
quant_method.enable_flashinfer_auto()
if quant_method.needs_flashinfer_autotune:
flashinfer_mxfp8_methods.append(quant_method)
enable_flashinfer_mxfp8_autotuner = bool(flashinfer_mxfp8_methods)

if not enable_trtllm_autotuner and not enable_flashinfer_mxfp8_autotuner:
return
AutoTuner.get().setup_distributed_state(self.mapping, self.dist)
logger.info("Running autotuner warmup...")
if enable_trtllm_autotuner:
AutoTuner.get().setup_distributed_state(self.mapping, self.dist)
logger.info(
f"Running autotuner warmup (TRT-LLM={enable_trtllm_autotuner}, "
f"FlashInfer MXFP8={enable_flashinfer_mxfp8_autotuner})...")
kv_cache_manager = resource_manager.get_resource_manager(
self.kv_cache_manager_key)
token_num_upper_bound = min(self.max_num_tokens,
Expand All @@ -1593,8 +1616,15 @@ def _run_autotuner_warmup(self, resource_manager: ResourceManager):
warmup_configs.append((1 + self.max_total_draft_tokens, 1))

cache_path = os.environ.get("TLLM_AUTOTUNER_CACHE_PATH", None)
with self.no_cuda_graph(), autotune(cache_path=cache_path):
ran_forward = False
trtllm_autotune_context = (autotune(
cache_path=cache_path) if enable_trtllm_autotuner else
contextlib.nullcontext())
flashinfer_autotune_context = (flashinfer_mxfp8_autotune()
if enable_flashinfer_mxfp8_autotuner else
contextlib.nullcontext())
ran_forward = False
with self.no_cuda_graph(
), trtllm_autotune_context, flashinfer_autotune_context:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
for num_tokens, num_gen_requests in warmup_configs:
warmup_request = self._create_warmup_request(
resource_manager, num_tokens, num_gen_requests)
Expand All @@ -1620,7 +1650,7 @@ def _run_autotuner_warmup(self, resource_manager: ResourceManager):
torch.cuda.synchronize()
ran_forward = True

if ran_forward:
if ran_forward and enable_trtllm_autotuner:
# pp_recv in AutoTuner choose_one will never be called if there is no tuning op during the forward pass.
# So we need to make an extra call to consume the previous rank's pp_send to guarantee that the previous rank's pp_send is released.
AutoTuner.get().cache_pp_recv()
Expand All @@ -1629,10 +1659,28 @@ def _run_autotuner_warmup(self, resource_manager: ResourceManager):
# Clean the pp flag to avoid deadlock with synchronous send/recv
AutoTuner.get().clean_pp_flag()

logger.info(
f"[Autotuner] Cache size after warmup is {len(AutoTuner.get().profiling_cache)}"
)
AutoTuner.get().print_profiling_cache()
if enable_flashinfer_mxfp8_autotuner:
if ran_forward:
for method in flashinfer_mxfp8_methods:
method.mark_flashinfer_autotuned()
else:
forced_flashinfer = any(method.backend == "flashinfer"
for method in flashinfer_mxfp8_methods)
for method in flashinfer_mxfp8_methods:
method.disable_flashinfer_auto()
if forced_flashinfer:
raise RuntimeError(
"FlashInfer MXFP8 was explicitly requested but its autotuner "
"warmup forward could not run")
logger.warning(
"FlashInfer MXFP8 autotuning could not run; using the native "
"TensorRT-LLM GEMM backend.")

if enable_trtllm_autotuner:
logger.info(
f"[Autotuner] Cache size after warmup is {len(AutoTuner.get().profiling_cache)}"
)
AutoTuner.get().print_profiling_cache()

self._release_megamoe_profiling_scratch()

Expand Down Expand Up @@ -1888,7 +1936,25 @@ def _run_cuda_graph_warmup(self, resource_manager: ResourceManager):
or self._torch_compile_piecewise_cuda_graph):
return

self._capture_generation_cuda_graphs(resource_manager)
from ..modules.linear import (MXFP8LinearMethod,
flashinfer_mxfp8_autotune,
flashinfer_mxfp8_decode_graph_capture)

# The automatic MiniMax-M3 MXFP8 selection is decode-graph-only.
# Tune every generation graph shape during the warmup-only pass. Keep
# piecewise context/prefill graph capture on the native backend.
flashinfer_methods = [
quant_method for module in self.model.modules()
if isinstance((quant_method := getattr(module, "quant_method", None)
), MXFP8LinearMethod)
and quant_method.needs_flashinfer_autotune
]
flashinfer_autotune_context = (
flashinfer_mxfp8_autotune() if self.cuda_graph_runner.is_warmup_only
and flashinfer_methods else contextlib.nullcontext())
with flashinfer_autotune_context, flashinfer_mxfp8_decode_graph_capture(
):
self._capture_generation_cuda_graphs(resource_manager)
# Piecewise graphs have separate capture machinery and do not use the
# whole-model attention workspace. Capture them only on the second pass.
if not self.cuda_graph_runner.is_warmup_only:
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_lists/test-db/l0_b200.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ l0_b200:
- unittest/_torch/modules/test_fp4_num_tokens_slice.py
- unittest/_torch/modules/test_awq_quantization.py
- unittest/_torch/modules/test_triton_linear.py
- unittest/_torch/modules/test_mxfp8_linear.py
- unittest/_torch/modules/test_group_rmn_norm.py
- unittest/_torch/modules/test_rotary_embedding.py
- unittest/_torch/modules/mamba
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_lists/test-db/l0_b300.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ l0_b300:
- unittest/_torch/modules/test_fused_activation_quant.py
- unittest/_torch/modules/test_awq_quantization.py
- unittest/_torch/modules/test_triton_linear.py
- unittest/_torch/modules/test_mxfp8_linear.py
- unittest/_torch/modules/test_group_rmn_norm.py
- unittest/_torch/modules/test_rotary_embedding.py
- unittest/_torch/modules/mamba
Expand Down
Loading
Loading