Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
59b65ec
[None][feat] Add W4A16 per-channel weight-only support for Cutlass fu…
Dorijan10 Aug 27, 2026
3d0c78a
[None][test] Add non-gated W4A16 accuracy coverage and register W4A16…
Dorijan10 Aug 28, 2026
6002ec4
[None][fix] Derive the default unpadded hidden size after the per-cha…
Dorijan10 Aug 28, 2026
6d411d7
[None][chore] Correct W4A16 comment references and add missing docstr…
Dorijan10 Aug 28, 2026
41bc35b
[None][chore] Annotate the new W4A16 functions and reject unsupported…
Dorijan10 Aug 28, 2026
ead5547
[None][chore] Raise ValueError for unsupported W4A16 activation dtypes
Dorijan10 Aug 28, 2026
02320c3
Merge upstream/main into w4a16-moe-per-channel
Dorijan10 Sep 2, 2026
de766ac
[None][fix] Correct the profiler group size and guard use_woq_per_cha…
Dorijan10 Sep 2, 2026
abdf57d
[None][chore] Reject the fused gate-up scale layout and mark w3_weigh…
Dorijan10 Sep 2, 2026
7b7805a
Merge upstream/main into w4a16-moe-per-channel
Dorijan10 Sep 3, 2026
612897f
[None][test] Run the W4A16 multi-GPU module tests on H100 and B200
Dorijan10 Sep 3, 2026
1268530
[None][fix] Import W4A16WoqPerChannelFusedMoEMethod from the relocate…
Dorijan10 Sep 15, 2026
9f8c86d
[None][fix] Validate the packed column count in the W4A16 alignment c…
Dorijan10 Sep 15, 2026
a42bcce
[None][chore] Share one implementation between the per-channel weight…
Dorijan10 Sep 15, 2026
2d7f402
Merge upstream/main into w4a16-moe-per-channel
Dorijan10 Sep 16, 2026
cdf98bb
[None][fix] Restore blank-line separation at the merge seam in test_m…
Dorijan10 Sep 17, 2026
80cdabe
Merge upstream/main into w4a16-moe-per-channel
Dorijan10 Sep 18, 2026
9a9258b
Merge upstream/main into w4a16-moe-per-channel
Dorijan10 Sep 21, 2026
94bc901
Merge upstream/main into w4a16-moe-per-channel
Dorijan10 Sep 22, 2026
8321bd8
Merge upstream/main into w4a16-moe-per-channel
Dorijan10 Sep 24, 2026
22f544f
Merge upstream/main into w4a16-moe-per-channel
Dorijan10 Oct 21, 2026
631df61
Merge upstream/main into w4a16-moe-per-channel
Dorijan10 Oct 26, 2026
e9fe34e
Merge upstream/main into w4a16-moe-per-channel
Dorijan10 Nov 6, 2026
b77e6a8
[None][doc] Add the w4a16 CUTLASS leaf to the MoE developer guide
Dorijan10 Nov 6, 2026
4e1db5b
[None][test] Run the multi-GPU W4A16 MoE test on DGX H100 only
Dorijan10 Nov 6, 2026
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
57 changes: 35 additions & 22 deletions cpp/tensorrt_llm/thop/moe/cutlass/moeOp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -333,15 +333,15 @@ class FusedMoeRunner : public EagerMoeWorkspaceOwner
}

FusedMoeRunner(c10::ScalarType activation_dtype, c10::ScalarType weight_dtype, c10::ScalarType output_dtype,
bool use_deepseek_fp8_block_scale, bool use_w4_group_scaling, bool use_int8_woq_per_channel,
bool use_deepseek_fp8_block_scale, bool use_w4_group_scaling, bool use_woq_per_channel,
bool use_mxfp8_act_scaling, bool use_mxfp8_weight_scaling, bool use_fused_finalize)
{
mActivationDtype = activation_dtype;
mWeightDtype = weight_dtype;
mOutputDtype = output_dtype;
mUseDeepSeekFP8BlockScaling = use_deepseek_fp8_block_scale;
mUseW4GroupScaling = use_w4_group_scaling;
mUseINT8WoqPerChannel = use_int8_woq_per_channel;
mUseWoqPerChannel = use_woq_per_channel;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the flag is no longer INT8-specific by name, nothing ties use_woq_per_channel to an integer weight dtype, yet it unconditionally swaps the hidden/inter interpretation in runMoe, runMoeMinLantency and runGemmProfile. A caller passing it with, say, an FP8 weight dtype would get silently transposed dimensions with no error. Worth a constructor check next to the use_mxfp8_weight_scaling one: TORCH_CHECK(!mUseWoqPerChannel || isIntWeightOnlyQuant(), ...).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, added in de766ac next to the use_mxfp8_weight_scaling check:
TORCH_CHECK(!mUseWoqPerChannel || isIntWeightOnlyQuant(), "use_woq_per_channel requires an INT8 or INT4 weight dtype.");. After the rename the flag name was the only thing implying an integer weight dtype, and it swaps the hidden/inter interpretation in all three call sites.

mUseMxfp8ActScaling = use_mxfp8_act_scaling;
mUseMxfp8WeightScaling = use_mxfp8_weight_scaling;
mUseFusedFinalize = use_fused_finalize;
Expand All @@ -360,6 +360,14 @@ class FusedMoeRunner : public EagerMoeWorkspaceOwner
&& mWeightDtype == c10::ScalarType::Float8_e4m3fn),
"use_mxfp8_weight_scaling requires both activation and weight dtypes to be Float8_e4m3fn.");

// The per-channel weight-only path reinterprets fc2's dimensions as
// [num_experts, inter_size, hidden_size] and, for INT4, treats the trailing
// dim as packed two-per-byte. That is only meaningful for integer
// weight-only quantization, so reject other weight dtypes here rather
// than silently transposing them downstream.
TORCH_CHECK(
!mUseWoqPerChannel || isIntWeightOnlyQuant(), "use_woq_per_channel requires an INT8 or INT4 weight dtype.");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

// keep consistent with cpp/tensorrt_llm/plugins/mixtureOfExperts/mixtureOfExpertsPlugin.cpp
if (mActivationDtype == c10::ScalarType::Half && mWeightDtype == c10::ScalarType::Half)
{
Expand Down Expand Up @@ -636,9 +644,9 @@ class FusedMoeRunner : public EagerMoeWorkspaceOwner
int64_t num_rows = input.sizes()[0];
int64_t hidden_size = fc2_expert_weights.sizes()[1];
int64_t inter_size = fc2_expert_weights.sizes()[2] * mInnerDimMultiplier;
if (mUseINT8WoqPerChannel)
if (mUseWoqPerChannel)
{
// Note: The weight shape for INT8 weight only quantization is different, e.g., fc2_expert_weights:
// Note: The weight shape for per-channel weight-only quantization is different, e.g., fc2_expert_weights:
// [num_experts, inter_size, hidden_size]
hidden_size = fc2_expert_weights.sizes()[2] * mInnerDimMultiplier;
inter_size = fc2_expert_weights.sizes()[1];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -714,20 +722,20 @@ class FusedMoeRunner : public EagerMoeWorkspaceOwner

// Validate the fc1/fc2 inter-size relationship now that the activation type (gated vs
// non-gated) is finalized: the swiglu_alpha/beta/limit handling above can promote the
// activation to SwigluBias, which is gated. INT8-woq uses a transposed weight layout, so
// activation to SwigluBias, which is gated. Woq uses a transposed weight layout, so
// its fc1/fc2 dim ordering differs from the non-woq path. Gated activations (Swiglu/Geglu)
// require fc1's intermediate dim to be 2x fc2's (one half each for gate and up); non-gated
// activations (Relu2/Identity/ReLU/SiLU/Gelu, e.g. Nemotron-H) require them to be equal.
if (mUseINT8WoqPerChannel)
if (mUseWoqPerChannel)
{
if (isGatedActivation(base_activation_type))
{
TORCH_CHECK(fc1_expert_weights.sizes()[2] == fc2_expert_weights.sizes()[1] * mInnerDimMultiplier * 2,
TORCH_CHECK(fc1_expert_weights.sizes()[2] * mInnerDimMultiplier == fc2_expert_weights.sizes()[1] * 2,
"fc1_expert_weights inter size must be 2 times fc2_expert_weights inter size.");
}
else
{
TORCH_CHECK(fc1_expert_weights.sizes()[2] == fc2_expert_weights.sizes()[1] * mInnerDimMultiplier,
TORCH_CHECK(fc1_expert_weights.sizes()[2] * mInnerDimMultiplier == fc2_expert_weights.sizes()[1],
"fc1_expert_weights inter size must be equal to fc2_expert_weights inter size.");
}
}
Expand Down Expand Up @@ -985,9 +993,9 @@ class FusedMoeRunner : public EagerMoeWorkspaceOwner
int64_t num_rows = input.sizes()[0];
int64_t hidden_size = fc2_expert_weights.sizes()[1];
int64_t inter_size = fc2_expert_weights.sizes()[2] * mInnerDimMultiplier;
if (mUseINT8WoqPerChannel)
if (mUseWoqPerChannel)
{
// Note: The weight shape for INT8 weight only quantization is different, e.g., fc2_expert_weights:
// Note: The weight shape for per-channel weight-only quantization is different, e.g., fc2_expert_weights:
// [num_experts, inter_size, hidden_size]
hidden_size = fc2_expert_weights.sizes()[2] * mInnerDimMultiplier;
inter_size = fc2_expert_weights.sizes()[1];
Expand Down Expand Up @@ -1043,20 +1051,20 @@ class FusedMoeRunner : public EagerMoeWorkspaceOwner
swiglu_clamp_after_silu, mActivationParamBuffers, capture_buffers);

// Validate the fc1/fc2 inter-size relationship now that the activation type (gated vs
// non-gated) is finalized. INT8-woq uses a transposed weight layout, so its fc1/fc2 dim
// non-gated) is finalized. Woq uses a transposed weight layout, so its fc1/fc2 dim
// ordering differs from the non-woq path; both mirror the gated/non-gated split used in
// runMoe(). Gated activations (Swiglu/Geglu) require fc1's intermediate dim to be 2x fc2's;
// non-gated (Relu2/Identity/ReLU/SiLU/Gelu, e.g. Nemotron-H) require them to be equal.
if (mUseINT8WoqPerChannel)
if (mUseWoqPerChannel)
{
if (isGatedActivation(base_activation_type))
{
TORCH_CHECK(fc1_expert_weights.sizes()[2] == fc2_expert_weights.sizes()[1] * mInnerDimMultiplier * 2,
TORCH_CHECK(fc1_expert_weights.sizes()[2] * mInnerDimMultiplier == fc2_expert_weights.sizes()[1] * 2,
"fc1_expert_weights inter size must be 2 times fc2_expert_weights inter size.");
}
else
{
TORCH_CHECK(fc1_expert_weights.sizes()[2] == fc2_expert_weights.sizes()[1] * mInnerDimMultiplier,
TORCH_CHECK(fc1_expert_weights.sizes()[2] * mInnerDimMultiplier == fc2_expert_weights.sizes()[1],
"fc1_expert_weights inter size must be equal to fc2_expert_weights inter size.");
}
}
Expand Down Expand Up @@ -1173,9 +1181,9 @@ class FusedMoeRunner : public EagerMoeWorkspaceOwner
int64_t const num_rows = input.sizes()[0];
int64_t hidden_size = fc2_expert_weights.sizes()[1];
int64_t inter_size = fc2_expert_weights.sizes()[2] * mInnerDimMultiplier;
if (mUseINT8WoqPerChannel)
if (mUseWoqPerChannel)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runGemmProfile derives group_size from isInt4Quant() alone (L1001-1005), without consulting mUseWoqPerChannel. Plain per-channel W4A16 now reaches this path with mInnerDimMultiplier = 2, so group_size becomes 128: GemmProfilerBackend then classifies it as is_int_groupwise_w_quant (moe_kernels.cu L5199-5202), sizes the scale workspace by hidden_size / mGroupSize, and builds QuantParams::GroupWise instead of QuantParams::Int (L5467). Since the GEMM picks its scale pointer at runtime on groupwise.group_size > 0 (L3835/3864/3964), the profiling run feeds groupwise scales and group_size = 128 into a runner instantiated with use_w4_groupwise = false, i.e. a configuration the real runMoe never uses. Should this be isInt4Quant() && !mUseWoqPerChannel?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in de766ac. Until this change isInt4Quant() implied mUseW4GroupScaling, since the only INT4 caller was WInt4AFP8FusedMoEMethod and fused_moe_cutlass.py sets use_w4_group_scaling=self.has_w4afp8 or self.has_w4a16_mxfp4, so per-channel W4A16 is the first INT4 config without group scaling and the first to reach this. I went with isInt4Quant() && mUseW4GroupScaling rather than !mUseWoqPerChannel, since that positive form is already the predicate used for the runner selection in the constructor and in getQuantParams(), so the profiler and runtime agree by construction instead of through two separately spelled rules. Could do it your way as well, if you think it is better.

{
// Note: The weight shape for INT8 weight only quantization is different, e.g., fc2_expert_weights:
// Note: The weight shape for per-channel weight-only quantization is different, e.g., fc2_expert_weights:
// [num_experts, inter_size, hidden_size]
hidden_size = fc2_expert_weights.sizes()[2] * mInnerDimMultiplier;
inter_size = fc2_expert_weights.sizes()[1];
Expand All @@ -1185,8 +1193,13 @@ class FusedMoeRunner : public EagerMoeWorkspaceOwner
std::nullopt, enable_alltoall,
/*has_input_sf=*/false, /*swizzled_input_sf=*/true, /*use_dynamic_fc2_scale=*/false);
#endif
int64_t const group_size_
= isInt4Quant() ? TmaWarpSpecializedGroupedGemmInput::INT4GroupwiseParams::int4_group_size : -1;
// Only group-scaled INT4 (W4A8_AWQ) carries a group size. Plain per-channel
// W4A16 must profile with -1 so the profiler builds QuantParams::Int, matching
// getQuantParams() and the runner chosen in the constructor; deriving this from
// isInt4Quant() alone would profile a groupwise configuration runMoe never uses.
int64_t const group_size_ = (isInt4Quant() && mUseW4GroupScaling)
? TmaWarpSpecializedGroupedGemmInput::INT4GroupwiseParams::int4_group_size
: -1;
int64_t const group_size = isWFP4A16Quant()
? TmaWarpSpecializedGroupedGemmInput::INT4GroupwiseParams::wfp4a16_group_size
: group_size_;
Expand Down Expand Up @@ -1273,7 +1286,7 @@ class FusedMoeRunner : public EagerMoeWorkspaceOwner

bool mUseDeepSeekFP8BlockScaling = false;
bool mUseW4GroupScaling = false;
bool mUseINT8WoqPerChannel = false;
bool mUseWoqPerChannel = false;
bool mUseMxfp8ActScaling = false;
bool mUseFusedFinalize = true;
bool mUseMxfp8WeightScaling = false;
Expand Down Expand Up @@ -2579,10 +2592,10 @@ class FusedMoeRunner : public EagerMoeWorkspaceOwner
else if (isIntWeightOnlyQuant())
{
TORCH_CHECK(quant_scales.has_value(), "Expecting quant scales for weight only quantization");
if (mUseINT8WoqPerChannel)
if (mUseWoqPerChannel)
{
TORCH_CHECK(
quant_scales.value().size() == 2, "Expecting 2 quant scales for INT8 weight only quantization");
TORCH_CHECK(quant_scales.value().size() == 2,
"Expecting 2 quant scales for per-channel weight only quantization");
auto& fc1_weight_scales = quant_scales.value()[0];
auto& fc2_weight_scales = quant_scales.value()[1];
return kernels::QuantParams::Int(static_cast<float const*>(fc1_weight_scales.data_ptr()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def __call__(
enable_alltoall: bool = False,
use_deepseek_fp8_block_scale: bool = False,
use_w4_group_scaling: bool = False,
use_int8_woq_per_channel: bool = False,
use_woq_per_channel: bool = False,
use_mxfp8_act_scaling: bool = False,
min_latency_mode: bool = False,
use_fused_finalize: bool = True,
Expand Down Expand Up @@ -194,7 +194,7 @@ def __call__(
enable_alltoall=enable_alltoall,
use_deepseek_fp8_block_scale=use_deepseek_fp8_block_scale,
use_w4_group_scaling=use_w4_group_scaling,
use_int8_woq_per_channel=use_int8_woq_per_channel,
use_woq_per_channel=use_woq_per_channel,
use_mxfp8_act_scaling=use_mxfp8_act_scaling,
min_latency_mode=min_latency_mode,
use_fused_finalize=use_fused_finalize,
Expand Down
32 changes: 20 additions & 12 deletions tensorrt_llm/_torch/custom_ops/torch_custom_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def __init__(
cluster_rank: int,
use_deepseek_fp8_block_scale: bool,
use_w4_group_scaling: bool,
use_int8_woq_per_channel: bool,
use_woq_per_channel: bool,
use_mxfp8_act_scaling: bool,
min_latency_mode: bool,
use_fused_finalize: bool,
Expand All @@ -139,7 +139,7 @@ def __init__(
self.enable_alltoall = False
self.use_deepseek_fp8_block_scale = use_deepseek_fp8_block_scale
self.use_w4_group_scaling = use_w4_group_scaling
self.use_int8_woq_per_channel = use_int8_woq_per_channel
self.use_woq_per_channel = use_woq_per_channel
self.use_mxfp8_act_scaling = use_mxfp8_act_scaling
self.use_mxfp8_weight_scaling = use_mxfp8_weight_scaling
self.min_latency_mode = min_latency_mode
Expand All @@ -149,15 +149,15 @@ def __init__(

instance_key = (x_dtype, weight_dtype, output_dtype,
use_deepseek_fp8_block_scale, use_w4_group_scaling,
use_int8_woq_per_channel, use_mxfp8_act_scaling,
use_woq_per_channel, use_mxfp8_act_scaling,
use_mxfp8_weight_scaling)

if instance_key not in MoERunner.runner_dict:
MoERunner.runner_dict[
instance_key] = torch.classes.trtllm.FusedMoeRunner(
x_dtype, weight_dtype, output_dtype,
use_deepseek_fp8_block_scale, use_w4_group_scaling,
use_int8_woq_per_channel, use_mxfp8_act_scaling,
use_woq_per_channel, use_mxfp8_act_scaling,
use_mxfp8_weight_scaling, use_fused_finalize)
self.fused_moe_runner = MoERunner.runner_dict[instance_key]

Expand Down Expand Up @@ -187,7 +187,7 @@ def unique_id(self):
self.enable_alltoall,
self.use_deepseek_fp8_block_scale,
self.use_w4_group_scaling,
self.use_int8_woq_per_channel,
self.use_woq_per_channel,
self.use_mxfp8_act_scaling,
self.min_latency_mode,
self.use_fused_finalize,
Expand Down Expand Up @@ -253,7 +253,7 @@ def fused_moe(
enable_alltoall: bool = False,
use_deepseek_fp8_block_scale: bool = False,
use_w4_group_scaling: bool = False,
use_int8_woq_per_channel: bool = False,
use_woq_per_channel: bool = False,
use_mxfp8_act_scaling: bool = False,
min_latency_mode: bool = False,
use_fused_finalize: bool = True,
Expand Down Expand Up @@ -317,7 +317,7 @@ def fused_moe(
cluster_rank=cluster_rank,
use_deepseek_fp8_block_scale=use_deepseek_fp8_block_scale,
use_w4_group_scaling=use_w4_group_scaling,
use_int8_woq_per_channel=use_int8_woq_per_channel,
use_woq_per_channel=use_woq_per_channel,
use_mxfp8_act_scaling=use_mxfp8_act_scaling,
min_latency_mode=min_latency_mode,
use_fused_finalize=use_fused_finalize,
Expand Down Expand Up @@ -437,7 +437,7 @@ def _(input: torch.Tensor,
enable_alltoall: bool = False,
use_deepseek_fp8_block_scale: bool = False,
use_w4_group_scaling: bool = False,
use_int8_woq_per_channel: bool = False,
use_woq_per_channel: bool = False,
use_mxfp8_act_scaling: bool = False,
min_latency_mode: bool = False,
use_fused_finalize: bool = True,
Expand Down Expand Up @@ -468,10 +468,18 @@ def _(input: torch.Tensor,
token_to_slot: Optional[torch.Tensor] = None,
swiglu_clamp_after_silu: bool = False):
seq_len = input.shape[0]
if use_int8_woq_per_channel:
# Note: The weight shape for INT8 weight only quantization is different, i.e.,
# fc2_expert_weights: [num_experts, inter_size, hidden_size]
hidden_size = fc2_expert_weights.shape[2]
if use_woq_per_channel:
# Note: The weight shape for per-channel weight-only quantization is
# dim-swapped, i.e. fc2_expert_weights: [num_experts, inter_size, hidden_size].
#
# Sub-byte weights are packed along that trailing hidden dim, so the stored
# extent is hidden_size / elements_per_byte and must be scaled back up to
# recover the logical hidden size. This mirrors the real op, which applies
# mInnerDimMultiplier; without it the fake kernel reports
# half the hidden size for INT4 and shape inference silently disagrees with
# the kernel under torch.compile.
inner_dim_multiplier = 2 if fc2_expert_weights.dtype == torch.quint4x2 else 1
hidden_size = fc2_expert_weights.shape[2] * inner_dim_multiplier
else:
hidden_size = fc2_expert_weights.shape[1]

Expand Down
8 changes: 5 additions & 3 deletions tensorrt_llm/_torch/moe/fused_moe/MOE_DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ Still on old path (standalone, with embedded communication):

| File | Backend | Hardware | Scenario | Scheduler |
|------|---------|----------|----------|-----------|
| `cutlass/` (+ the `fused_moe_cutlass.py` alias module) | the abstract `CutlassFusedMoEBase` and its twelve leaves (`CUTLASS_LEAVES`): one per quantization format, two for FP8 block scales (see [CUTLASS leaves](#cutlass-leaves-fused_moecutlass)) | SM80+ | High throughput, the widest quant support; the fallback family | `EXTERNAL_COMM` |
| `cutlass/` (+ the `fused_moe_cutlass.py` alias module) | the abstract `CutlassFusedMoEBase` and its thirteen leaves (`CUTLASS_LEAVES`): one per quantization format, two for FP8 block scales (see [CUTLASS leaves](#cutlass-leaves-fused_moecutlass)) | SM80+ | High throughput, the widest quant support; the fallback family | `EXTERNAL_COMM` |
| `trtllm_gen/` (+ the `fused_moe_trtllm_gen.py` alias module) | the abstract `TrtllmGenFusedMoEBase` and its leaves (see [TRTLLM-Gen leaves](#trtllm-gen-leaves-fused_moetrtllm_gen)) | SM100/SM103/SM107 | Min-latency and high-throughput on Blackwell/Rubin; also serves unquantized BF16 through FlashInfer, on a gate of its own (`§` under [Quantization Support](#quantization-support)) | `EXTERNAL_COMM` |
| `fused_moe_deepgemm.py` | `DeepgemmCudaFp8BlockScalesImpl` (aliased as `DeepGemmFusedMoE`) | SM100/SM103/SM107 | FP8 Block Scales on Blackwell/Rubin | `EXTERNAL_COMM` |
| `cutedsl/dense_gemm.py` (+ the `fused_moe_densegemm.py` alias module) | `TrtllmCutedslDenseGemmNvfp4Impl` (aliased as `DenseGEMMFusedMoE`) | SM100/SM103 | NVFP4 min-latency; CuTe DSL dense GEMM packs all experts into one matrix (vs Cutlass per-expert scatter), efficient for small token counts | `EXTERNAL_COMM` |
Expand Down Expand Up @@ -337,6 +337,7 @@ parent of every leaf) and re-exports `raise_moe_lora_multichunk_unsupported`.
| `w4a8_mxfp4_fp8` | `TrtllmCutlassW4a8Mxfp4Fp8Impl` | `trtllm.cutlass.grouped_gemm.w4a8_mxfp4_fp8` | Static per-tensor e4m3 input |
| `w4a16_mxfp4` | `TrtllmCutlassW4a16Mxfp4Impl` | `trtllm.cutlass.grouped_gemm.w4a16_mxfp4` | Rounds hidden and intermediate up to 128 at construction; `run_moe` pads the activation to match |
| `w4a8_awq` | `TrtllmCutlassW4a8AwqImpl` | `trtllm.cutlass.grouped_gemm.w4a8_awq` | — |
| `w4a16` | `TrtllmCutlassW4a16Impl` | `trtllm.cutlass.grouped_gemm.w4a16` | — |
| `w8a16` | `TrtllmCutlassW8a16Impl` | `trtllm.cutlass.grouped_gemm.w8a16` | — |

- **A leaf declares; the base derives.** A leaf states its `descriptor`, its
Expand Down Expand Up @@ -726,6 +727,7 @@ Each backend's `can_implement(p, d)` classmethod declares what it supports. Sour
| W4A8 MXFP4 MXFP8 | Y (SM100/103/107/120/121) | Y (SM100/103/107) | N | N | N | Y (SM100/103/107, requires `hidden_size % 512 == 0`) | N | N | N | N |
| W8A8 MXFP8 MXFP8 | Y (SM100/103/107) | N | N | N | N | N | N | N | N | N |
| W4A8 AWQ | Y (SM89/90) | N | N | N | N | N | N | N | N | N |
| W4A16 | Y (SM80+) | N | N | N | N | N | N | N | N | N |
| W8A16 | Y (SM80+) | N | N | N | N | N | N | N | N | N |
| INT4 WoQ (W4AFP8) | N | N | N | N | N | N | N | N | N | N |

Expand Down Expand Up @@ -934,8 +936,8 @@ bias plus filled `alpha` / `beta` / `limit`, summarized as
TRTLLM-Gen and CUTLASS leaves admit it per format through
`supports_gptoss_style` (on CUTLASS: unquantized, MXFP8, NVFP4 and the three
MXFP4 formats). The kernel takes the three constants for every format; what
limits the set is the weight methods. W8A16 and W4A8_AWQ keep a bias shape that
is wrong for their transposed layouts, and NVFP4 sets
limits the set is the weight methods. W8A16, W4A16 and W4A8_AWQ keep a bias
shape that is wrong for their transposed layouts, and NVFP4 sets
`rejects_gptoss_expert_bias` because its weight pad assumes 2-D, so MiniMax-M3
(no expert bias) is in and a 1-D gpt-oss bias is out. Each way of getting the
set wrong has a test that catches it:
Expand Down
3 changes: 3 additions & 0 deletions tensorrt_llm/_torch/moe/fused_moe/cutlass/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from .w4a8_awq import TrtllmCutlassW4a8AwqImpl
from .w4a8_mxfp4_fp8 import TrtllmCutlassW4a8Mxfp4Fp8Impl
from .w4a8_mxfp4_mxfp8 import TrtllmCutlassW4a8Mxfp4Mxfp8Impl
from .w4a16 import TrtllmCutlassW4a16Impl
from .w4a16_mxfp4 import TrtllmCutlassW4a16Mxfp4Impl
from .w4a16_nvfp4 import TrtllmCutlassW4a16Nvfp4Impl
from .w8a16 import TrtllmCutlassW8a16Impl
Expand All @@ -51,6 +52,7 @@
TrtllmCutlassW4a8Mxfp4Mxfp8Impl,
TrtllmCutlassW4a16Mxfp4Impl,
TrtllmCutlassW4a8AwqImpl,
TrtllmCutlassW4a16Impl,
TrtllmCutlassW8a16Impl,
TrtllmCutlassFp8Impl,
TrtllmCutlassUnquantizedImpl,
Expand Down Expand Up @@ -125,6 +127,7 @@ def cutlass_leaf(
"TrtllmCutlassW4a8AwqImpl",
"TrtllmCutlassW4a8Mxfp4Fp8Impl",
"TrtllmCutlassW4a8Mxfp4Mxfp8Impl",
"TrtllmCutlassW4a16Impl",
"TrtllmCutlassW4a16Mxfp4Impl",
"TrtllmCutlassW4a16Nvfp4Impl",
"TrtllmCutlassW8a16Impl",
Expand Down
Loading
Loading