Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
26a85c3
fix: recognize Triton 3.6 MoE configs
yeahdongcn Sep 13, 2026
100b5f2
feat: model Nemotron non-gated MoE tuning semantics
yeahdongcn Sep 9, 2026
f90fe33
fix: implement non-gated relu2 MoE activation in tuner
yeahdongcn Sep 9, 2026
8e4089d
feat: enable BF16 Nemotron MoE tuning
yeahdongcn Sep 9, 2026
d78fdb7
fix: use full Nemotron non-gated MoE projection width
yeahdongcn Sep 9, 2026
58672a8
fix: make Nemotron MoE tuner return combined outputs
yeahdongcn Sep 9, 2026
ec67ada
test: gate Nemotron MoE pipeline semantics
yeahdongcn Sep 9, 2026
1737c4f
test: cover topk MoE combine semantics
yeahdongcn Sep 9, 2026
ddc5757
fix: complete MoE activation and reduction semantics
yeahdongcn Sep 9, 2026
5dd124f
test: cover all routed expert rows
yeahdongcn Sep 9, 2026
8182e68
test: model routed weight in fake down kernel
yeahdongcn Sep 9, 2026
754ca7f
test: cover python-side routed weighting
yeahdongcn Sep 9, 2026
8528e3c
fix: provide standalone MoE alignment fallback
yeahdongcn Sep 9, 2026
4475402
fix: normalize torchada fused MoE split config
yeahdongcn Sep 9, 2026
f0eb59e
fix: cache MoE routing metadata across graph capture
yeahdongcn Sep 9, 2026
aa5ac5e
feat: add Nemotron E128 N1856 S5000 MoE config
yeahdongcn Sep 9, 2026
758ea5a
fix: keep MoE routing graph safe and tune stable inputs
yeahdongcn Sep 9, 2026
cef6de3
fix: make MoE tuner graph and reference gates meaningful
yeahdongcn Sep 9, 2026
8152a58
feat: tune Nemotron non-gated MoE shapes
yeahdongcn Sep 13, 2026
9c78582
test: correct non-gated Nemotron projection width
yeahdongcn Sep 13, 2026
143fb4a
fix: keep runtime resolver version scope unchanged
yeahdongcn Sep 13, 2026
c37ddfb
perf: add Nemotron Triton 3.6 S5000 configs
yeahdongcn Sep 13, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"1": {
"BLOCK_SIZE_M": 32,
"BLOCK_SIZE_N": 64,
"BLOCK_SIZE_K": 64,
"GROUP_SIZE_M": 32,
"num_warps": 4,
"num_stages": 1
},
"8": {
"BLOCK_SIZE_M": 64,
"BLOCK_SIZE_N": 32,
"BLOCK_SIZE_K": 64,
"GROUP_SIZE_M": 64,
"num_warps": 4,
"num_stages": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"triton_version": "3.6.0",
"64": {
"BLOCK_SIZE_M": 8,
"num_warps": 8
},
"512": {
"BLOCK_SIZE_M": 8,
"num_warps": 4
}
}
55 changes: 46 additions & 9 deletions src/torchada/triton/autotune/fused_moe/tune_moe.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ class ModelEntry:
shard_intermediate_size: int = 0
topk: int = 0
num_fused_shared_experts: int = 0
activation: str = "silu"
is_gated: bool = True
dtype: torch.dtype = torch.float16
block_shape: Optional[Tuple[int, int]] = None

Expand Down Expand Up @@ -167,6 +169,8 @@ def unique_key(self) -> Tuple:
self.shard_intermediate_size,
self.topk,
self.num_fused_shared_experts,
self.activation,
self.is_gated,
str(self.dtype),
self.use_fp8,
self.use_int8,
Expand All @@ -188,7 +192,7 @@ def validate_and_log_entries(entries: List[ModelEntry]) -> None:
for i, e in enumerate(entries):
logger.info(
"[%d] model=%s tp=%d ep=%d experts=%d hidden=%d "
"intermediate=%d topk=%d shared=%d dtype=%s block=%s",
"intermediate=%d topk=%d shared=%d activation=%s gated=%s dtype=%s block=%s",
i,
e.path,
e.tp_size,
Expand All @@ -198,6 +202,8 @@ def validate_and_log_entries(entries: List[ModelEntry]) -> None:
e.shard_intermediate_size,
e.topk,
e.num_fused_shared_experts,
e.activation,
e.is_gated,
e.dtype_str,
e.block_shape,
)
Expand Down Expand Up @@ -316,6 +322,8 @@ def benchmark_config(
per_channel_quant: bool,
block_shape: List[int] = None,
num_fused_shared_experts: int = 0,
activation: str = "silu",
is_gated: bool = True,
num_iters: int = 100,
) -> float:
"""Run the fused MoE kernel and return latency in microseconds."""
Expand All @@ -324,6 +332,10 @@ def benchmark_config(
init_dtype = torch.float16 if use_fp8_w8a8 else dtype
num_routed_experts = num_experts - num_fused_shared_experts
assert num_routed_experts > 0
# ``shard_intermediate_size`` is the first GEMM output width (w1.shape[1]).
# Gated models use half of it as the second GEMM input width because w1
# contains gate and up projections; non-gated models use the full width.
w2_input_size = shard_intermediate_size // 2 if is_gated else shard_intermediate_size
x = torch.randn(num_tokens, hidden_size, dtype=dtype, device=device)

# Create random weights based on quantization type
Expand All @@ -338,7 +350,7 @@ def benchmark_config(
w2 = torch.randint(
-127,
127,
(num_experts, hidden_size, shard_intermediate_size // 2),
(num_experts, hidden_size, w2_input_size),
dtype=torch.int8,
device=device,
)
Expand All @@ -353,7 +365,7 @@ def benchmark_config(
w2 = torch.randint(
0,
255,
(num_experts, hidden_size, shard_intermediate_size // 4),
(num_experts, hidden_size, w2_input_size // 2),
dtype=torch.uint8,
device=device,
)
Expand All @@ -368,7 +380,7 @@ def benchmark_config(
w2 = torch.randn(
num_experts,
hidden_size,
shard_intermediate_size // 2,
w2_input_size,
dtype=init_dtype,
device=device,
)
Expand All @@ -385,7 +397,7 @@ def benchmark_config(
w1_scale = w2_scale = a1_scale = a2_scale = None
if use_int8_w8a16:
w1_scale = torch.randn(
(num_experts, 2 * shard_intermediate_size),
(num_experts, shard_intermediate_size),
dtype=torch.float32,
device=device,
)
Expand All @@ -396,7 +408,7 @@ def benchmark_config(
n_tiles_w1 = (shard_intermediate_size + block_n - 1) // block_n
n_tiles_w2 = (hidden_size + block_n - 1) // block_n
k_tiles_w1 = (hidden_size + block_k - 1) // block_k
k_tiles_w2 = (shard_intermediate_size // 2 + block_k - 1) // block_k
k_tiles_w2 = (w2_input_size + block_k - 1) // block_k
w1_scale = torch.randn(
(num_experts, n_tiles_w1, k_tiles_w1),
dtype=torch.bfloat16,
Expand All @@ -423,7 +435,7 @@ def benchmark_config(
n_tiles_w1 = (shard_intermediate_size + block_n - 1) // block_n
n_tiles_w2 = (hidden_size + block_n - 1) // block_n
k_tiles_w1 = (hidden_size + block_k - 1) // block_k
k_tiles_w2 = (shard_intermediate_size // 2 + block_k - 1) // block_k
k_tiles_w2 = (w2_input_size + block_k - 1) // block_k
w1_scale = torch.rand(
(num_experts, n_tiles_w1, k_tiles_w1),
dtype=torch.float32,
Expand Down Expand Up @@ -462,6 +474,8 @@ def run():
top_k=topk,
num_fused_shared_experts=num_fused_shared_experts,
inplace=True,
activation=activation,
is_gated=is_gated,
)
with override_config(config):
fused_moe(
Expand Down Expand Up @@ -548,6 +562,8 @@ def build_model_entries(args: argparse.Namespace) -> List[ModelEntry]:
entry.shard_intermediate_size = params["shard_intermediate_size"]
entry.topk = params["topk"]
entry.num_fused_shared_experts = params.get("num_fused_shared_experts", 0)
entry.activation = params.get("activation", "silu")
entry.is_gated = params.get("is_gated", True)
entry.dtype_str = _resolve_dtype_str(entry.dtype_str, params)
entry.dtype = _resolve_torch_dtype(entry.dtype_str, params)
entry.block_shape = tuple(params["block_shape"]) if params["block_shape"] else None
Expand Down Expand Up @@ -603,6 +619,8 @@ def build_model_entries(args: argparse.Namespace) -> List[ModelEntry]:
entry.shard_intermediate_size = params["shard_intermediate_size"]
entry.topk = params["topk"]
entry.num_fused_shared_experts = params.get("num_fused_shared_experts", 0)
entry.activation = params.get("activation", "silu")
entry.is_gated = params.get("is_gated", True)
entry.dtype_str = _resolve_dtype_str(entry.dtype_str, params)
entry.dtype = _resolve_torch_dtype(entry.dtype_str, params)
entry.block_shape = (
Expand Down Expand Up @@ -657,6 +675,8 @@ def _tune_worker(
entry.per_channel_quant,
list(entry.block_shape) if entry.block_shape else None,
entry.num_fused_shared_experts,
activation=entry.activation,
is_gated=entry.is_gated,
num_iters=10,
)
except (triton.runtime.autotuner.OutOfResources, RuntimeError, AssertionError):
Expand Down Expand Up @@ -767,6 +787,7 @@ def run_tuning(entries: List[ModelEntry], batch_sizes: List[int], args: argparse
entry.use_int4,
entry.per_channel_quant,
entry.block_shape,
is_gated=entry.is_gated,
)
sorted_batches = sorted(bs_to_config.keys())
best_configs = {bs: sort_config(bs_to_config[bs]) for bs in sorted_batches}
Expand Down Expand Up @@ -808,7 +829,11 @@ def _benchmark_worker(
)
block_n = entry.block_shape[0] if entry.block_shape else 0
block_k = entry.block_shape[1] if entry.block_shape else 0
N = entry.shard_intermediate_size // 2
N = (
entry.shard_intermediate_size // 2
if entry.is_gated
else entry.shard_intermediate_size
)
if entry.use_int4:
N = N // 2
op_config = get_moe_configs(
Expand Down Expand Up @@ -852,6 +877,8 @@ def _benchmark_worker(
entry.per_channel_quant,
list(entry.block_shape) if entry.block_shape else None,
entry.num_fused_shared_experts,
activation=entry.activation,
is_gated=entry.is_gated,
)
result_queue.put((entry, batch_size, kernel_time, None))
except Exception as e:
Expand Down Expand Up @@ -975,7 +1002,17 @@ def main(args: argparse.Namespace) -> None:
parser.add_argument(
"--dtype",
type=str,
choices=["auto", "fp8_w8a8", "int8_w8a16", "int8_w8a8", "int4_w4a16"],
choices=[
"auto",
"bf16",
"bfloat16",
"fp16",
"float16",
"fp8_w8a8",
"int8_w8a16",
"int8_w8a8",
"int4_w4a16",
],
default="auto",
help="Quantization dtype.",
)
Expand Down
43 changes: 38 additions & 5 deletions src/torchada/triton/autotune/fused_moe/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import os
from typing import Dict, List, TypedDict
from typing import Dict, List, Tuple, TypedDict

import torch
from transformers import AutoConfig
Expand All @@ -24,12 +24,39 @@ class BenchmarkConfig(TypedDict):


def calculate_shard_intermediate_size(
intermediate_size: int, tp_size: int, ep_size: int = 1
intermediate_size: int,
tp_size: int,
ep_size: int = 1,
is_gated: bool = True,
) -> int:
assert tp_size % ep_size == 0
moe_tp_size = tp_size // ep_size
assert intermediate_size % moe_tp_size == 0
return 2 * intermediate_size // moe_tp_size
# Gated projections (for example SwiGLU) store gate and up next to each
# other in w1. Non-gated projections such as Nemotron-H's relu2 use one
# projection, so the w1 output width is the model's intermediate size.
multiplier = 2 if is_gated else 1
return multiplier * intermediate_size // moe_tp_size


def infer_moe_activation(config) -> Tuple[str, bool]:
"""Infer the activation name and projection layout from a HF config.

``NemotronHForCausalLM`` passes ``activation_without_mul`` to its fused
MoE layer. The HF config advertises ``relu2`` while the checkpoint has a
single projection (``relu2_no_mul``). Keep all other architectures on
the historical gated-SiLU default unless their activation already carries
the explicit ``_no_mul`` suffix.
"""
raw = getattr(config, "mlp_hidden_act", None)
if raw is None:
raw = getattr(config, "hidden_act", "silu")
activation = str(raw).replace("torch.", "").lower()
architectures = getattr(config, "architectures", None) or []
architecture = str(architectures[0]) if architectures else type(config).__name__
if architecture == "NemotronHForCausalLM" and not activation.endswith("_no_mul"):
activation = f"{activation}_no_mul"
return activation, not activation.endswith("_no_mul")


def get_num_shared_experts(config, disable_shared_experts_fusion: bool) -> int:
Expand Down Expand Up @@ -146,6 +173,7 @@ def get_model_config(
config = _load_model_config(model_name)

architecture = config.architectures[0]
activation, is_gated = infer_moe_activation(config)
quant_dtype_str = infer_quant_dtype_str(config)
block_shape = None
if hasattr(config, "quantization_config") and "weight_block_size" in config.quantization_config:
Expand Down Expand Up @@ -247,7 +275,9 @@ def get_model_config(
topk = config.num_experts_per_tok
intermediate_size = config.intermediate_size

shard_intermediate_size = calculate_shard_intermediate_size(intermediate_size, tp_size, ep_size)
shard_intermediate_size = calculate_shard_intermediate_size(
intermediate_size, tp_size, ep_size, is_gated=is_gated
)

return {
"num_experts": E,
Expand All @@ -259,6 +289,8 @@ def get_model_config(
"architecture": architecture,
"num_fused_shared_experts": num_fused_shared_experts,
"quant_dtype_str": quant_dtype_str,
"activation": activation,
"is_gated": is_gated,
}


Expand Down Expand Up @@ -333,6 +365,7 @@ def get_config_filename(
use_int4_w4a16: bool,
per_channel_quant: bool,
block_shape: List[int],
is_gated: bool = True,
) -> str:
dtype_str = get_config_dtype_str(
dtype,
Expand All @@ -344,7 +377,7 @@ def get_config_filename(

# NOTE(woosuk): The current naming convention uses w2.shape[2], which
# is the intermediate size after silu_and_mul.
N = shard_intermediate_size // 2
N = shard_intermediate_size // 2 if is_gated else shard_intermediate_size
if use_int4_w4a16:
N = N // 2

Expand Down
4 changes: 4 additions & 0 deletions src/torchada/triton/kernels/moe/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,10 @@ def invoke_fused_moe_kernel(
fuse_add_to_output: bool = False,
add_output_mask: Optional[torch.Tensor] = None,
) -> None:
config = dict(config)
split_k = config.pop("SPLIT_K", 1)
if split_k != 1:
raise ValueError("The torchada fused MoE kernel only supports SPLIT_K=1")
assert topk_weights.stride(1) == 1
assert sorted_token_ids.stride(0) == 1

Expand Down
Loading