Skip to content
Open
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
13 changes: 13 additions & 0 deletions megatron/core/extensions/transformer_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -2103,6 +2103,10 @@ def sharded_state_dict(

if HAVE_TE and is_te_min_version("1.9.0.dev0"):

_TE_GROUPED_LINEAR_SUPPORTS_GROUPED_TENSOR = (
"use_grouped_tensor" in inspect.signature(te.pytorch.GroupedLinear.__init__).parameters
)

class TEGroupedLinear(te.pytorch.GroupedLinear):
"""
Wrapper for the Transformer-Engine's `GroupedLinear` layer.
Expand Down Expand Up @@ -2205,6 +2209,14 @@ def __init__(
config, "moe_single_grouped_bias", False
)

if _TE_GROUPED_LINEAR_SUPPORTS_GROUPED_TENSOR:
extra_kwargs["use_grouped_tensor"] = config.moe_use_grouped_tensor
elif config.moe_use_grouped_tensor:
raise RuntimeError(
"moe_use_grouped_tensor=True requires a Transformer Engine GroupedLinear "
"that exposes the use_grouped_tensor argument."
)

self.te_quant_params: Optional[TEQuantizationParams] = None
quant_config = get_quant_config_or_none(name, config.quant_recipe)
self.finish_init(quant_config)
Expand Down Expand Up @@ -2712,6 +2724,7 @@ def sharded_state_dict(self, prefix="", sharded_offsets=(), metadata=None):
)

else:
_TE_GROUPED_LINEAR_SUPPORTS_GROUPED_TENSOR = False
TEGroupedLinear = None # type: ignore[assignment, misc]
TEColumnParallelGroupedLinear = None # type: ignore[assignment, misc]
TERowParallelGroupedLinear = None # type: ignore[assignment, misc]
Expand Down
116 changes: 108 additions & 8 deletions megatron/core/transformer/moe/experts.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,25 +301,74 @@ def __init__(
self.config.moe_mlp_glu_interleave_size,
)

if self.config.fp8 or self.config.fp4:
assert HAVE_TE, "FP8 and FP4 requires TE."
align_size = 256 if self._with_fused_impl else None
self._use_grouped_tensor = self.config.moe_use_grouped_tensor
if self.config.fp8 or self.config.fp4 or self._use_grouped_tensor:
assert HAVE_TE, "Quantized or TE grouped-tensor GroupedMLP execution requires TE."
align_size = (
get_align_size_for_quantization(self.config) if self._use_grouped_tensor else None
)
self.quantization_padding = Fp8Padding(self.num_local_experts, align_size=align_size)
self.quantization_unpadding = Fp8Unpadding(
self.num_local_experts, align_size=align_size
)

@staticmethod
def _apply_packed_bias(intermediate_parallel, packed_bias, tokens_per_expert, permuted_probs):

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.

Note: only models with routed expert bias will use this

"""Apply a packed expert bias without reading token counts on the host."""
# TODO: get rid of the .float() by having fused kernel compute in FP32
shape = intermediate_parallel.shape
hidden_size = shape[-1]
output_dtype = intermediate_parallel.dtype
flat_output = intermediate_parallel.view(-1, hidden_size).float()
flat_probs = permuted_probs.reshape(-1, 1).float()

if tokens_per_expert.device != packed_bias.device:
raise ValueError("Packed MoE bias and tokens_per_expert must be on the same device.")

# Permutation stores tokens contiguously by expert. Repeat bias row e by that expert's
# token count to create one bias row per permuted token:
#
# packed_bias = [bias_e0, bias_e1]
# tokens_per_expert = [ 2, 1]
# bias_per_token = [bias_e0, bias_e0, bias_e1]
#
# output_size avoids a stream synchronization to compute sum(tokens_per_expert).
# Cast before repeating so both forward arithmetic and repeat_interleave's backward
# reduction are computed in FP32. Autograd casts the final parameter gradient once.
bias_per_token = torch.repeat_interleave(
packed_bias.float(), tokens_per_expert, dim=0, output_size=flat_output.size(0)
)
return (flat_output + bias_per_token * flat_probs).view(shape).to(output_dtype)

@staticmethod
def _apply_bias(intermediate_parallel, bias_parallel, tokens_per_expert, permuted_probs):
if bias_parallel is None:
return intermediate_parallel

# CUDA-graph-safe packed path. With single_grouped_bias=True, TE returns one packed
# GroupedTensor [num_experts, hidden_size]. The grouped-tensor backend also provides
# tokens_per_expert as a tensor on the same device.
if isinstance(bias_parallel, torch.Tensor) and isinstance(tokens_per_expert, torch.Tensor):
return TEGroupedMLP._apply_packed_bias(
intermediate_parallel, bias_parallel, tokens_per_expert, permuted_probs
)

# Eager-only CPU-metadata path. The legacy contract returns List[Tensor[hidden_size]],
# and torch.split plus the Python zip below require concrete host token counts. A packed
# bias paired with Python counts also uses this compatibility path. Converting a tensor
# with .tolist() synchronizes and copies device data to the host, so this path must never
# be included in a CUDA graph.
if isinstance(tokens_per_expert, torch.Tensor):
tokens_per_expert = tokens_per_expert.tolist()

shape = intermediate_parallel.shape
flat_output = intermediate_parallel.view(-1, shape[-1])
return (
torch.cat(
[
t + b * p
for t, b, p in zip(
torch.split(intermediate_parallel.view(-1, shape[-1]), tokens_per_expert),
torch.split(flat_output, tokens_per_expert),
bias_parallel,
torch.split(permuted_probs, tokens_per_expert),
)
Expand Down Expand Up @@ -634,9 +683,21 @@ def _fused_forward(

# Apply padding if needed
unpadded_tokens_per_expert = None
# Some dispatchers have already padded each expert's token segment before the tokens
# reach this module:
# * router padding changes the routing map before dispatch;
# * HybridEP pads as part of its fused dispatch/permute operation;
# * DeepEP can pad in the fused local permutation after communication.
# Padding those tensors again would insert a second set of dummy tokens and make
# tokens_per_expert disagree with the already-permuted token buffer, so skip the local
# Fp8Padding fallback in those cases.
if skip_routed_expert_padding(self.config):
pass
elif self.config.fp8 or self.config.fp4:
# Regular AllToAll normally reaches this branch because its permutation does not insert
# the padding needed by the fused grouped-MLP contract. FP8/FP4 require recipe-specific
# alignment, while the TE operation-fuser grouped-tensor path currently uses 256-token
# expert segments.
elif self.config.fp8 or self.config.fp4 or self._use_grouped_tensor:
tokens_per_expert = tokens_per_expert.tolist()
unpadded_tokens_per_expert = tokens_per_expert
permuted_local_hidden_states, tokens_per_expert = self.quantization_padding(
Expand All @@ -647,7 +708,22 @@ def _fused_forward(
)
permuted_probs = permuted_probs.squeeze(-1)
tokens_per_expert = torch.tensor(
tokens_per_expert, dtype=torch.int, device=permuted_probs.device
tokens_per_expert, dtype=torch.int64, device=permuted_probs.device
)

if self._use_grouped_tensor:
if not isinstance(tokens_per_expert, torch.Tensor):
tokens_per_expert = torch.tensor(
tokens_per_expert, dtype=torch.int64, device=permuted_local_hidden_states.device
)
else:
tokens_per_expert = tokens_per_expert.to(
device=permuted_local_hidden_states.device, dtype=torch.int64, non_blocking=True
)
else:
raise RuntimeError(
"The Transformer Engine operation-fuser MoE path requires "
"moe_use_grouped_tensor=True."
)
# if the number of tokens is 0, pad the hidden states to 256

Expand Down Expand Up @@ -766,11 +842,23 @@ def forward(

# Apply padding if needed
unpadded_tokens_per_expert = None
tokens_per_expert: list[int] = tokens_per_expert.tolist()
permuted_probs = permuted_probs.unsqueeze(-1)
# The token buffer may already contain per-expert padding when padding was performed
# before expert compute:
# * router padding modified the routing map before dispatch;
# * HybridEP fused padding into dispatch/permute;
# * DeepEP fused padding into its post-communication local permutation.
# In those cases tokens_per_expert already describes the padded expert segments. Running
# Fp8Padding again would change the segment lengths without matching the existing token
# layout, so this module must leave both tensors unchanged.
if skip_routed_expert_padding(self.config):
pass
elif self.config.fp8 or self.config.fp4:
# Regular AllToAll normally supplies unpadded expert segments and therefore uses this
# explicit fallback. FP8/FP4 need their recipe-specific alignment. MCore currently also
# applies its common aligned-segment contract to the GroupedTensor backend so quantized
# grouped execution receives supported shapes
elif self.config.fp8 or self.config.fp4 or self._use_grouped_tensor:
tokens_per_expert = tokens_per_expert.tolist()
unpadded_tokens_per_expert = tokens_per_expert
permuted_local_hidden_states, tokens_per_expert = self.quantization_padding(
permuted_local_hidden_states, tokens_per_expert
Expand All @@ -779,6 +867,18 @@ def forward(
permuted_probs, unpadded_tokens_per_expert
)

if self._use_grouped_tensor:
if not isinstance(tokens_per_expert, torch.Tensor):
tokens_per_expert = torch.tensor(
tokens_per_expert, dtype=torch.int64, device=permuted_local_hidden_states.device
)
else:
tokens_per_expert = tokens_per_expert.to(
device=permuted_local_hidden_states.device, dtype=torch.int64, non_blocking=True
)
elif isinstance(tokens_per_expert, torch.Tensor):
tokens_per_expert = tokens_per_expert.tolist()

if self.config.moe_apply_probs_on_input:
assert (
self.config.moe_router_topk == 1
Expand Down
30 changes: 23 additions & 7 deletions megatron/core/transformer/moe/moe_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1378,8 +1378,11 @@ def forward(
inp = inp.view(-1, inp_shape[-1])

if te_general_gemm is not None and router_dtype != torch.float64:
output = te_general_gemm(weight, inp, router_dtype, layout="TN", bias=bias)
output = output[0]
# cuBLASLt's non-FP8 bias epilogue expects bias and output to have the same
# dtype. Router parameters may be BF16 while router logits are FP32, so cast the
# small bias vector before passing it to TE.
gemm_bias = bias.to(router_dtype) if bias is not None else None
output = te_general_gemm(weight, inp, router_dtype, layout="TN", bias=gemm_bias)[0]
elif bias is None:
output = torch.mm(inp.to(router_dtype), weight.to(router_dtype).t())
else:
Expand Down Expand Up @@ -1457,22 +1460,33 @@ def get_align_size_for_quantization(config: TransformerConfig) -> int:
Returns:
int: The alignment size for quantization.
"""
# CUTLASS kernel for grouped GEMM assumes 256 alignment.
if config.use_transformer_engine_op_fuser:
# TE's grouped-tensor and fused grouped-MLP kernels require 256-token alignment.
if config.use_transformer_engine_op_fuser or config.moe_use_grouped_tensor:
return 256
if config.fp8:
return get_fp8_align_size(config.fp8_recipe)
if config.fp4:
return get_fp4_align_size(config.fp4_recipe)
# Only FP8 or FP4 requires padding. Defaults to 0.
# Legacy high-precision grouped GEMM does not require padding. Defaults to 0.
return 0


def _deepep_permute_pads_grouped_tensor_input(config: TransformerConfig) -> bool:
"""Whether DeepEP fused permutation pads input for TE grouped-tensor GEMM."""
return (
config.moe_use_grouped_tensor
and config.moe_token_dispatcher_type == "flex"
and config.moe_flex_dispatcher_backend == "deepep"
and config.moe_permute_fusion
and fused_permute_and_pad_with_probs is not None
)


def skip_routed_expert_padding(config: TransformerConfig) -> bool:
"""Whether the expert module should skip quantization padding.

Returns True when padding is already applied by the router or the
HybridEP / NCCL-EP dispatcher.
Returns True when padding is already applied by the router, the HybridEP / NCCL-EP
dispatcher, or DeepEP's fused permutation kernel.
"""
if config.moe_router_padding_for_quantization:
return True
Expand All @@ -1481,6 +1495,8 @@ def skip_routed_expert_padding(config: TransformerConfig) -> bool:
"ncclep",
):
return True
if _deepep_permute_pads_grouped_tensor_input(config):
return True
return False


Expand Down
5 changes: 3 additions & 2 deletions megatron/core/transformer/moe/token_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -1136,8 +1136,9 @@ def dispatch(
"HybridEP only supports float32 probs, please set --moe-router-dtype=fp32"
)
self.token_probs = self.token_probs.float() # downcast or upcast
if self.config.fp8 or self.config.fp4:
self.pad_multiple = get_align_size_for_quantization(self.config)
align_size = get_align_size_for_quantization(self.config)
if align_size > 0:
self.pad_multiple = align_size
if self._padded_num_tokens is not None and hidden_states.shape[0] < self._padded_num_tokens:
pad_rows = self._padded_num_tokens - hidden_states.shape[0]
hidden_states = torch.cat(
Expand Down
37 changes: 24 additions & 13 deletions megatron/core/transformer/transformer_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,21 +827,29 @@ class TransformerConfig(ModelParallelConfig):
"""

moe_grouped_gemm: bool = False
"""When there are multiple experts per rank, compress multiple local (potentially small) gemms
in a single kernel launch to improve the utilization and performance by leveraging the Grouped
GEMM feature introduced since CUTLASS 2.8 (https://github.com/fanshiqing/grouped_gemm).
"""Use grouped GEMM to execute multiple local MoE experts together.

The concrete implementation is selected by Transformer Engine. Set
``moe_use_grouped_tensor=True`` to use its CUDA-graph-safe GroupedTensor path.
"""

moe_use_grouped_tensor: bool = False
"""Use Transformer Engine's native GroupedTensor path for grouped MoE GEMMs.

This path uses padded expert segments and CUDA split metadata so it can be captured in CUDA
graphs. Enabling the Transformer Engine operation fuser also enables this option.
"""

moe_single_grouped_weight: bool = False
"""When using TE GroupedLinear for MoE experts, store expert weights as a single grouped
parameter via Transformer Engine's `GroupedTensor`. Requires ``moe_grouped_gemm=True`` and
``use_transformer_engine_op_fuser=True``.
``moe_use_grouped_tensor=True``.
"""

moe_single_grouped_bias: bool = False
"""When using TE GroupedLinear for MoE experts, store expert biases as a single grouped
parameter via Transformer Engine's `GroupedTensor`. Requires ``moe_grouped_gemm=True``
and ``add_bias_linear=True``."""
parameter via Transformer Engine's `GroupedTensor`. Requires ``moe_grouped_gemm=True``,
``moe_use_grouped_tensor=True``, and ``add_bias_linear=True``."""

moe_aux_loss_coeff: Union[float, List[float]] = 0.0
"""Scaling coefficient for the aux loss. A starting value of 1e-2 is recommended.
Expand Down Expand Up @@ -1298,6 +1306,12 @@ def __post_init__(self):
"""
super().__post_init__()

if self.use_transformer_engine_op_fuser and self.moe_grouped_gemm:
self.moe_use_grouped_tensor = True

if self.moe_use_grouped_tensor and not self.moe_grouped_gemm:
raise ValueError("moe_use_grouped_tensor=True requires moe_grouped_gemm=True.")

# When fp32 residual connections are enabled, pipeline parallel communication must
# use fp32 to match the dtype of the residual stream between pipeline stages.
if self.fp32_residual_connection and self.pipeline_dtype is not None:
Expand Down Expand Up @@ -1602,15 +1616,12 @@ def __post_init__(self):
"(--fp4-param-gather). Without FP4 parameter gather, Transformer Engine "
"uses a split-quantize fallback that is being deprecated."
)
if not self.use_transformer_engine_op_fuser:
raise ValueError(
"moe_single_grouped_weight requires "
"use_transformer_engine_op_fuser=True. The non-op-fuser TE GroupedLinear "
"path splits the grouped parameter into per-expert tensors and does not "
"support single-grouped-weight training."
)
if not self.moe_use_grouped_tensor:
raise ValueError("moe_single_grouped_weight requires moe_use_grouped_tensor=True.")
if self.moe_single_grouped_bias and not self.add_bias_linear:
raise ValueError("moe_single_grouped_bias requires add_bias_linear=True.")
if self.moe_single_grouped_bias and not self.moe_use_grouped_tensor:
raise ValueError("moe_single_grouped_bias requires moe_use_grouped_tensor=True.")

if self.moe_enable_deepep:
if self.moe_token_dispatcher_type != "flex":
Expand Down
Loading