Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a892881
feat: unify mHC fused kernels
jingqiny-99 Apr 30, 2026
a93892a
feat: align mHC fused kernel backends
jingqiny-99 Apr 30, 2026
e6baf14
fix: make Triton sinkhorn constant compile-safe
jingqiny-99 Apr 30, 2026
2f9d80f
perf: remove torch zero fills from mHC backward
jingqiny-99 Apr 30, 2026
8b417f6
fix: avoid unmasked cuTile proj_rms stores
jingqiny-99 Apr 30, 2026
e712bac
perf: split mHC compute_h outputs
jingqiny-99 May 5, 2026
3765234
chore: clean up mHC fused kernel files
jingqiny-99 May 5, 2026
1a1c71c
Improve cuTile mHC reduce compute_h tiling
jingqiny-99 May 12, 2026
f33d1c4
Fix: autoformat
jingqiny-99 May 13, 2026
f31deae
upd: fix lint
jingqiny-99 May 13, 2026
08f4183
Constrain mHC reduce compute_h tile tuning
jingqiny-99 May 14, 2026
d27a4ba
Export fused_add_3 public wrapper
jingqiny-99 May 14, 2026
dce7d76
Match proj rms compute_h torch fallback reference
jingqiny-99 May 14, 2026
e09d1ef
Add mHC backend test overrides
jingqiny-99 May 14, 2026
732d614
Address mHC fused review cleanup
jingqiny-99 May 19, 2026
c9184f5
Fix mHC h_res transpose application
jingqiny-99 May 19, 2026
394c173
Fix Triton mHC LOG2E constexpr
jingqiny-99 May 19, 2026
5b43329
Fix: lint
jingqiny-99 May 19, 2026
c4259b9
upd: fix
jingqiny-99 Jun 1, 2026
c005d27
upd: fix lint
jingqiny-99 Jun 1, 2026
e9041a0
fix: handle hybrid mhc tuple and cutile availability
jingqiny-99 Jun 1, 2026
c72b3d8
upd: fix : lint
jingqiny-99 Jun 1, 2026
9881839
test: update hybrid mhc mock tuple
jingqiny-99 Jun 1, 2026
8cffcbd
Update megatron/core/fusions/fused_mhc_kernels.py
jingqiny-99 Jun 2, 2026
833598c
Fix native mHC fallback visibility
jingqiny-99 Jun 2, 2026
03cfe02
fix: golden value for mhc
jingqiny-99 Jun 2, 2026
53e2d36
Align mHC compute_h eps and Sinkhorn semantics
jingqiny-99 Jun 3, 2026
dafa509
Fix mHC fused backend availability and precision
jingqiny-99 Jun 3, 2026
395d7a2
Remove no-CUDA mHC test marker
jingqiny-99 Jun 3, 2026
512e56e
Align cuTile proj RMS eps defaults
jingqiny-99 Jun 3, 2026
7a5ae54
fix: lint and golden values
jingqiny-99 Jun 3, 2026
86e1821
Refine mHC Sinkhorn and h_post eps semantics
jingqiny-99 Jun 4, 2026
961bbca
Use softmax for native Sinkhorn first row step
jingqiny-99 Jun 4, 2026
153c2c7
Merge branch 'dev' into jingqiny/feature-mHC-TE
jingqiny-99 Jun 10, 2026
8b93635
Merge branch 'dev' into jingqiny/feature-mHC-TE
jingqiny-99 Jun 11, 2026
e9687d7
fix: golden values again
jingqiny-99 Jun 11, 2026
d71fffb
Merge branch 'dev' into jingqiny/feature-mHC-TE
jingqiny-99 Jun 12, 2026
3b87348
Merge branch 'dev' into jingqiny/feature-mHC-TE
jingqiny-99 Jun 12, 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
3,277 changes: 2,855 additions & 422 deletions megatron/core/fusions/fused_mhc_kernels.py

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions megatron/core/models/hybrid/hybrid_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,7 @@ def forward(
mhc_recompute_manager=None,
) -> Tuple[Tensor, Optional[Tensor]]:
"""Run the wrapped hybrid layer through one layer-boundary mHC update."""
residual = hidden_states
aggregated, h_res, h_post = self.hyper_connection(
aggregated, h_res, h_post, residual = self.hyper_connection(
hidden_states, mhc_recompute_manager=mhc_recompute_manager
)
layer_output, context = self._call_inner_layer(
Expand Down
157 changes: 124 additions & 33 deletions megatron/core/transformer/hyper_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
if TYPE_CHECKING:
from megatron.core.tensor_parallel.random import CheckpointManager

_MHC_SINKHORN_EPS = 1e-6
_MHC_COMPUTE_H_EPS = 1e-6


@torch.compile
def _sinkhorn_iterations(input_logits: Tensor, num_iterations: int, eps: float) -> Tensor:
row_max = input_logits.max(dim=-1, keepdim=True).values
M = torch.exp(input_logits - row_max)
for _ in range(num_iterations):
M = M / M.sum(dim=-1, keepdim=True).clamp(min=eps)
M = M / M.sum(dim=-2, keepdim=True).clamp(min=eps)
M = input_logits.softmax(dim=-1) + eps
M = M / (M.sum(dim=-2, keepdim=True) + eps)
for _ in range(num_iterations - 1):
M = M / (M.sum(dim=-1, keepdim=True) + eps)
M = M / (M.sum(dim=-2, keepdim=True) + eps)
return M


Expand Down Expand Up @@ -69,11 +72,11 @@ def native_h_aggregate(x: Tensor, h_pre: Tensor) -> Tensor:
def native_h_post_bda(
h_res: Tensor, original_residual: Tensor, h_post: Tensor, x: Tensor, bias: Optional[Tensor]
) -> Tensor:
"""Native H_res @ residual + H_post * (x [+ bias])."""
"""Native H_res.T @ residual + H_post * (x [+ bias])."""
s, b, n, C = original_residual.shape
h_res_batched = h_res.view(s * b, n, n)
residual_batched = original_residual.view(s * b, n, C)
mixed = torch.bmm(h_res_batched, residual_batched).view(s, b, n, C)
mixed = torch.bmm(h_res_batched.transpose(1, 2), residual_batched).view(s, b, n, C)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[IMPORTANT Correctness] This is a significant semantic change — the residual mixing operation changed from H_res @ residual to H_res.T @ residual. Since H_res is a doubly stochastic matrix (not generally symmetric), this changes numerical results for any existing pretrained model using mHC.

Looking at the class docstring (now x_{l+1} = H_res^T @ x_l + ...) and the paper formulation, this appears to be a bug fix — the old code was applying H_res without the transpose, which didn't match the intended algorithm. The new tests with explicit expected values (_h_post_bda_transpose_case) confirm the transpose semantics are correct.

However, this means:

  1. Existing mHC checkpoints trained with the old code will produce different results when loaded with the new code (silent behavioral change on resume).
  2. There is no migration/compatibility shim or warning for this case.

Is there a linked issue or paper reference that confirms H_res.T is the intended operation? And should there be a note in a CHANGELOG or migration guide about this behavioral break for anyone who has trained with the old H_res @ semantics?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[IMPORTANT Compatibility] This changes the mathematical operation from H_res @ residual to H_res.T @ residual. The class docstring (x_{l+1} = H_res^T @ x_l + ...) and the mHC paper both specify the transpose, so this appears to be a correctness fix aligning code with the intended algorithm.

However, this is a checkpoint-breaking change: any model checkpoint trained with the old code (which computed H_res @ residual without the transpose) will produce silently different outputs when loaded with the new code. Since H_res is doubly stochastic but not symmetric in general, H_res != H_res.T.

The same change is applied consistently in apply_h_res (line 452), all three Triton/cuTile/torch backward implementations, and the _torch_h_post_bda_bwd fallback. The change is internally consistent across all backends.

Could you confirm this is an intentional bugfix from the paper? If so, it would be helpful to note the checkpoint incompatibility in the PR description (or provide a migration note) so downstream users are aware that old mHC checkpoints cannot be resumed with the new code without retraining.

x_expanded = h_post.unsqueeze(-1) * x.unsqueeze(2)
if bias is not None:
bias_expanded = h_post.unsqueeze(-1) * bias.view(1, 1, 1, C)
Expand All @@ -92,6 +95,39 @@ def native_proj_rms(x: Tensor, weight: Tensor, eps: float = 1e-6) -> Tuple[Tenso
return proj, r


@torch.compile
def native_fused_add_3(a: Tensor, b: Tensor, c: Tensor) -> Tensor:
"""Native 3-way elementwise add (torch.compile fuses into single kernel)."""
return a + b + c


class BroadcastTensorFused(torch.autograd.Function):
"""Split one tensor into 3 autograd-graph children sharing the same storage.

During backward the three incoming gradients are summed with a caller-
supplied fused-add function (cuTile or torch.compile fallback) instead of
PyTorch's default sequential accumulation.
"""

@staticmethod
def forward(ctx, x, fused_add_3_fn):
"""Return three view aliases and save the fused gradient combiner."""
ctx.fused_add_3_fn = fused_add_3_fn
return x.view_as(x), x.view_as(x), x.view_as(x)

@staticmethod
def backward(ctx, grad1, grad2, grad3):
"""Combine gradients from the three broadcast aliases."""
grads = [g for g in (grad1, grad2, grad3) if g is not None]
if len(grads) == 0:
return None, None
if len(grads) == 1:
return grads[0], None
if len(grads) == 2:
return grads[0] + grads[1], None
return ctx.fused_add_3_fn(grad1, grad2, grad3), None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P3] BroadcastTensorFused takes fused_add_3_fn through apply() and stores it on ctx, but every caller passes self._fused_add_3_op, which is always native_fused_add_3. The nearby comment also says this add intentionally always uses the torch.compile native path regardless of selected backend. Consider hardcoding native_fused_add_3 in this backward and removing _fused_add_3_op plus the extra apply() argument; if a real backend-specific add is needed later, the indirection can be reintroduced then.



@torch.compile
def learned_output_contract(
Comment on lines +103 to 132

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[IMPORTANT Correctness] BroadcastTensorFused.forward stores a callable (fused_add_3_fn) on ctx via ctx.fused_add_3_fn = fused_add_3_fn. This is a non-tensor attribute on the autograd context.

Two concerns:

  1. CUDA graph capture: If this module is used inside a CUDA graph capture region, the callable reference in ctx could create issues if the callable itself holds any state that changes between captures.
  2. torch.compile / torch.export: Storing callables on ctx in custom autograd functions is not always compatible with torch.compile's tracing (Dynamo). If HyperConnectionModule is ever wrapped in torch.compile at a higher level, this could surface as a graph break.

These are not blocking since the current usage pattern (module-level function reference) should be stable, but worth noting for future-proofing.

hidden_states: Tensor, head_fn: Tensor, base: Tensor, scale: Tensor, n: int, eps: float
Expand Down Expand Up @@ -120,13 +156,13 @@ class HyperConnectionModule(MegatronModule):
Unified mHC (Manifold-Constrained Hyper-Connections) module.

Implements the complete mHC propagation:
x_{l+1} = H_res @ x_l + H_post^T @ F(H_pre @ x_l)
x_{l+1} = H_res^T @ x_l + H_post^T @ F(H_pre @ x_l)

This module handles:
1. Computing learnable mappings: H_pre, H_post, H_res (with Sinkhorn-Knopp projection)
2. Aggregation: n-stream → 1-stream (H_pre @ x)
3. Expansion: 1-stream → n-stream (H_post^T @ output)
4. Residual merge: H_res @ x + expanded_output
4. Residual merge: H_res^T @ x + expanded_output
5. Block-level expand/contract for TransformerBlock boundaries

Args:
Expand All @@ -141,6 +177,8 @@ def __init__(self, config: TransformerConfig, layer_number: int):
self.n = config.num_residual_streams
self.hidden_size = config.hidden_size
self.sinkhorn_iterations = config.mhc_sinkhorn_iterations
self.sinkhorn_eps = _MHC_SINKHORN_EPS
self.compute_h_eps = _MHC_COMPUTE_H_EPS

# Projection weights for dynamic mappings
# Input: [s, b, n*C] -> Output: n^2 + 2n values per token
Expand All @@ -161,26 +199,35 @@ def __init__(self, config: TransformerConfig, layer_number: int):
self.bias = nn.Parameter(torch.zeros(self.n * self.n + 2 * self.n))
self.norm_eps = 1e-6

# Choose implementation: fused cuTile kernels vs reference modules.
# Both paths expose the same call signatures so the rest of the code
# is implementation-agnostic.
# Choose implementation: unified fused kernels vs reference modules.
# The fused public API selects the backend per operation internally.
# fused_add_3 always uses torch.compile (native_fused_add_3) regardless
# of the kernel backend. cuTile's register overhead (56 regs/thread for
# a trivial a+b+c) is not worth it for a pure memory-bound elementwise op.
self._fused_add_3_op = native_fused_add_3

if config.use_fused_mhc:
from megatron.core.fusions.fused_mhc_kernels import (
fused_h_aggregate,
fused_h_post_bda,
fused_proj_rms,
fused_proj_rms_compute_h,
fused_sinkhorn,
log_fused_mhc_backend_once,
)

log_fused_mhc_backend_once()
self._sinkhorn_op = fused_sinkhorn
self._h_aggregate_op = fused_h_aggregate
self._h_post_bda_op = fused_h_post_bda
self._proj_rms_op = fused_proj_rms
self._proj_rms_compute_h_op = fused_proj_rms_compute_h
else:
self._sinkhorn_op = native_sinkhorn
self._h_aggregate_op = native_h_aggregate
self._h_post_bda_op = native_h_post_bda
self._proj_rms_op = native_proj_rms
self._proj_rms_compute_h_op = None

self._init_weights()

Expand Down Expand Up @@ -235,10 +282,10 @@ def _compute_h(self, proj: Tensor, r: Tensor) -> Tuple[Tensor, Tensor, Tensor]:
)
h = r * proj * alpha_ + self.bias
# H_pre = σ(α_pre * (θ_pre @ x̃) + b_pre)
h_pre = h[..., : self.n].sigmoid() # [s, b, n]
h_pre = h[..., : self.n].sigmoid() + self.compute_h_eps # [s, b, n]

# H_post = 2σ(α_post * (θ_post @ x̃) + b_post)
h_post = h[..., self.n : 2 * self.n].sigmoid() * 2 # [s, b, n]
h_post = h[..., self.n : 2 * self.n].sigmoid() * 2
h_res = h[..., 2 * self.n :]
return h_pre, h_post, h_res

Expand All @@ -258,12 +305,35 @@ def compute_mappings(self, x: Tensor) -> Tuple[Tensor, Tensor, Tensor]:
h_res: [s, b, n, n] - residual mixing matrix (doubly stochastic)
"""
s, b, _ = x.shape
with torch.cuda.nvtx.range("HyperConnection::projection_and_get_norm"):
proj, r = self._projection_and_get_norm(x)
with torch.cuda.nvtx.range("HyperConnection::compute_h"):
h_pre, h_post, h_res = self._compute_h(proj, r)

if self._proj_rms_compute_h_op is not None:
# Fused path: proj_rms + compute_h in one kernel launch sequence
x_2d = x.reshape(s * b, self.n * self.hidden_size)
with torch.cuda.nvtx.range("HyperConnection::fused_proj_rms_compute_h"):
h_pre, h_post, h_res, _ = self._proj_rms_compute_h_op(
x_2d,
self.mapping_proj.weight,
self.alpha_pre,
self.alpha_post,
self.alpha_res,
self.bias,
self.n,
self.norm_eps,
self.compute_h_eps,
)
h_pre = h_pre.view(s, b, self.n)
h_post = h_post.view(s, b, self.n)
h_res = h_res.view(s, b, self.n, self.n)
else:
# Native path: separate proj_rms + _compute_h
with torch.cuda.nvtx.range("HyperConnection::projection_and_get_norm"):
proj, r = self._projection_and_get_norm(x)
with torch.cuda.nvtx.range("HyperConnection::compute_h"):
h_pre, h_post, h_res = self._compute_h(proj, r)
h_res = h_res.view(s, b, self.n, self.n)

h_res = self._sinkhorn_op(
h_res.view(s, b, self.n, self.n), self.sinkhorn_iterations, self.norm_eps
h_res, self.sinkhorn_iterations, self.sinkhorn_eps
) # [s, b, n, n]

return h_pre, h_post, h_res
Expand Down Expand Up @@ -372,7 +442,7 @@ def apply_h_res(self, h_res: Tensor, residual: Tensor) -> Tensor:
"""
Apply H_res to residual using H_res weights.

Computes: H_res @ residual
Computes: H_res.T @ residual

Args:
h_res: [s, b, n, n] - residual mixing matrix
Expand All @@ -387,33 +457,42 @@ def apply_h_res(self, h_res: Tensor, residual: Tensor) -> Tensor:
# [s, b, n*C] -> [s, b, n, C] -> [s*b, n, C]
residual_batched = residual.view(s, b, n, C).view(s * b, n, C)

# Batch matrix multiply: [s*b, n, n] @ [s*b, n, C] -> [s*b, n, C]
mixed = torch.bmm(h_res_batched, residual_batched)
# Batch matrix multiply: [s*b, n, n].T @ [s*b, n, C] -> [s*b, n, C]
mixed = torch.bmm(h_res_batched.transpose(1, 2), residual_batched)

return mixed.view(s, b, n * C)

def forward(
self, hidden_states: Tensor, mhc_recompute_manager: Optional['CheckpointManager'] = None
) -> Tuple[Tensor, Tensor, Tensor]:
) -> Tuple[Tensor, Tensor, Tensor, Tensor]:
Comment on lines 465 to +467

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[IMPORTANT Compatibility] forward() return type changed from Tuple[Tensor, Tensor, Tensor] to Tuple[Tensor, Tensor, Tensor, Tensor].

This is a public API in megatron/core/ — any downstream code that unpacks the 3-tuple (e.g. aggregated, h_res, h_post = module(hidden_states)) will break with a ValueError: too many values to unpack.

The internal call sites in transformer_layer.py and test_hyper_connection_recompute.py are updated, but external consumers won't be. Consider adding a deprecation shim that detects 3-value unpacking or at minimum documenting this breaking change in the PR description.

"""
Full mHC forward pass.

Uses BroadcastTensorFused to split hidden_states into 3 autograd-graph
children so that gradient accumulation from the 3 consumers
(compute_mappings, aggregate, fused_h_res_h_post_bda) is handled by a
single fused add instead of PyTorch's default sequential accumulation.

Args:
hidden_states: [s, b, n*C] - n-stream hidden states
mhc_recompute_manager: Optional CheckpointManager for checkpoint management.
When provided, uses _forward_with_checkpoint for memory-efficient execution.

Returns:
A 4-tuple. This is an intentional breaking change from the older
3-tuple API because fused_h_res_h_post_bda consumes the residual
branch created by BroadcastTensorFused.
aggregated: [s, b, C] - aggregated input for layer computation
Comment on lines 480 to 485

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[IMPORTANT Compatibility] forward() now returns a 4-tuple instead of a 3-tuple. The docstring explicitly calls this out as "an intentional breaking change," and the internal call sites in transformer_layer.py are updated. However, any external code that unpacks the result as a 3-tuple (e.g. aggregated, h_res, h_post = module(hidden_states)) will break with a ValueError: too many values to unpack.

Since this is on the dev branch, the blast radius may be limited, but consider whether a deprecation shim (returning a named tuple or adding a version check) would be appropriate for the next merge to main.

h_res: [s, b, n, n] - residual mixing matrix (for fused kernel)
h_post: [s, b, n] - expansion weights
residual: [s, b, n*C] - residual view for fused_h_res_h_post_bda
"""
if mhc_recompute_manager is not None:
return self._forward_with_checkpoint(hidden_states, mhc_recompute_manager)
else:
return self._forward_normal(hidden_states)

def _forward_normal(self, hidden_states: Tensor) -> Tuple[Tensor, Tensor, Tensor]:
def _forward_normal(self, hidden_states: Tensor) -> Tuple[Tensor, Tensor, Tensor, Tensor]:
"""
Normal forward pass without checkpointing.

Expand All @@ -424,19 +503,25 @@ def _forward_normal(self, hidden_states: Tensor) -> Tuple[Tensor, Tensor, Tensor
aggregated: [s, b, C] - aggregated input for layer computation
h_res: [s, b, n, n] - residual mixing matrix (for fused kernel)
h_post: [s, b, n] - expansion weights
residual: [s, b, n*C] - residual view for fused_h_res_h_post_bda
"""
# Split into 3 views to avoid extra grad accumulations in backward
hs_for_mappings, hs_for_aggregate, hs_for_residual = BroadcastTensorFused.apply(
hidden_states, self._fused_add_3_op
)

# Compute mappings
h_pre, h_post, h_res = self.compute_mappings(hidden_states)
h_pre, h_post, h_res = self.compute_mappings(hs_for_mappings)

# Aggregate for layer input
with torch.cuda.nvtx.range("HyperConnection::aggregate"):
aggregated = self.aggregate(hidden_states, h_pre)
aggregated = self.aggregate(hs_for_aggregate, h_pre)

return aggregated, h_res, h_post
return aggregated, h_res, h_post, hs_for_residual

def _forward_with_checkpoint(
self, hidden_states: Tensor, manager: 'CheckpointManager'
) -> Tuple[Tensor, Tensor, Tensor]:
) -> Tuple[Tensor, Tensor, Tensor, Tensor]:
"""
Forward pass with checkpointing for memory efficiency.

Expand All @@ -453,17 +538,23 @@ def _forward_with_checkpoint(
aggregated: [s, b, C] - aggregated input for layer computation
h_res: [s, b, n, n] - residual mixing matrix (for fused kernel)
h_post: [s, b, n] - expansion weights
residual: [s, b, n*C] - residual view for fused_h_res_h_post_bda
"""
from megatron.core.tensor_parallel.random import CheckpointWithoutOutput

h_pre, h_post, h_res = self.compute_mappings(hidden_states)
# Split into 3 views to avoid extra grad accumulations in backward
hs_for_mappings, hs_for_aggregate, hs_for_residual = BroadcastTensorFused.apply(
hidden_states, self._fused_add_3_op
)

h_pre, h_post, h_res = self.compute_mappings(hs_for_mappings)

# Checkpoint aggregate - auto-registers to manager
aggregated = CheckpointWithoutOutput(ckpt_manager=manager).checkpoint(
self.aggregate, hidden_states, h_pre
self.aggregate, hs_for_aggregate, h_pre
)

return aggregated, h_res, h_post
return aggregated, h_res, h_post, hs_for_residual

# ==================== Block-level utilities ====================

Expand Down Expand Up @@ -528,7 +619,7 @@ def fused_h_res_h_post_bda(
Currently implements the operations sequentially using native PyTorch.

The computation flow is:
1. mixed = H_res @ original_residual (apply_h_res)
1. mixed = H_res.T @ original_residual (apply_h_res)
2. expanded = H_post^T @ layer_output (apply_h_post)
3. output = dropout(expanded + bias) + mixed (bias-dropout-add)

Expand Down Expand Up @@ -584,7 +675,7 @@ def _fused_h_res_h_post_bda_native(
h_res, h_post and bda.

When dropout is zero (or inference), uses a single fused/reference kernel
for H_res @ residual + H_post * (x + bias). Falls back to unfused
for H_res.T @ residual + H_post * (x + bias). Falls back to unfused
implementation when dropout is needed.

Args:
Expand Down
31 changes: 9 additions & 22 deletions megatron/core/transformer/transformer_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1097,13 +1097,17 @@ class TransformerConfig(ModelParallelConfig):
"""Initial value of Gating Factor (alpha in paper)."""

use_fused_mhc: bool = False
"""Use cuTile fused kernels for mHC operations.
"""Use unified fused kernels for mHC operations.

When True, attempts to replace the reference mHC modules (SinkhornKnopp,
H_aggregate, H_post_bda, ProjRms) with fused cuda.tile (cuTile) autograd
functions for better performance on supported GPUs. Requires cuTile to be
installed; if cuTile is unavailable the flag is silently reset to False and
a warning is emitted.
H_aggregate, H_post_bda, ProjRms) with fused/autograd implementations for
Comment thread
jingqiny-99 marked this conversation as resolved.
better performance on supported GPUs. Backend selection is internal and
op-specific: Triton for Sinkhorn and H_post_bda backward when available,
cuTile for the remaining fused kernels when available, then native torch
fallback. If every mHC operation uses the native torch fallback,
use_fused_mhc remains enabled and a rank-0 warning is emitted. The all-native
fallback is functionally equivalent, but may not provide fused backend
performance benefits.
"""

mhc_recompute_layer_num: Optional[int] = None
Expand Down Expand Up @@ -1970,23 +1974,6 @@ def __post_init__(self):
if self.use_fused_mhc:
if not self.enable_hyper_connections:
raise ValueError("use_fused_mhc requires enable_hyper_connections=True.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[IMPORTANT Compatibility] The __post_init__ guard that silently reset use_fused_mhc=False when cuTile was unavailable has been removed. Previously, users could safely set use_fused_mhc=True in their config and it would degrade gracefully with a warning. Now use_fused_mhc=True always stays True.

This is semantically safe because the fused dispatch functions now fall back to native PyTorch implementations when neither Triton nor cuTile is available. However, the behavior change is silent — existing users who relied on the warning message to know they were on the fallback path will no longer see it. Consider adding a one-time info-level log in the fused dispatch layer indicating which backend was selected per operation.

try:
from megatron.core.fusions.fused_mhc_kernels import is_cutile_available

if not is_cutile_available():
warnings.warn(
"use_fused_mhc is enabled but cuda.tile (cuTile) is not installed. "
"Falling back to reference mHC implementations.",
UserWarning,
)
self.use_fused_mhc = False
except ImportError:
warnings.warn(
"use_fused_mhc is enabled but fused_mhc_kernels module could not be "
"imported. Falling back to reference mHC implementations.",
UserWarning,
)
self.use_fused_mhc = False

if self.fine_grained_activation_offloading:
Comment on lines 1977 to 1978

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[IMPORTANT Compatibility] The removed block previously warned users and auto-reset use_fused_mhc = False when cuTile was unavailable. With the new multi-backend dispatch, this is functionally correct (the fused API transparently falls back to native), but the user now gets no feedback that use_fused_mhc=True is effectively a no-op performance-wise when neither Triton nor cuTile is installed.

Consider adding a log-level warning here (or in log_fused_mhc_backend_once) when all operations resolve to the "native" backend, so users don't unknowingly run without any acceleration:

if self.use_fused_mhc:
    if not self.enable_hyper_connections:
        raise ValueError("use_fused_mhc requires enable_hyper_connections=True.")
    # Optional: warn if no accelerated backend is available

assert (
Expand Down
Loading
Loading