Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2e618b3
Support GEMM + Swiglu fused MLP (#3890)
ksivaman Mar 20, 2026
d3fcd28
Update megatron/core/transformer/moe/experts.py
ksivaman Mar 23, 2026
a78c09d
fix lint
ksivaman Mar 24, 2026
24d04f0
small fix
yaox12 Mar 31, 2026
f8515fa
Address PR 3971 rebase follow-ups
Connor-XY May 5, 2026
97a0a53
Fix test_fused_forward assertions
Connor-XY May 5, 2026
c6275d2
Add tests for uncovered PR 3971 paths
Connor-XY May 5, 2026
71b2913
Address strict-review findings on _make_fused_ops
Connor-XY May 5, 2026
2b33d05
Rename single_grouped_parameter to single_grouped_weight for current TE
Connor-XY May 5, 2026
b6a13ab
Test normalize_grouped_parameter_keys hook orchestration
Connor-XY May 5, 2026
6512a1d
Fix lint: project black formatting + pylint line length
Connor-XY May 5, 2026
af6a39e
TE fused grouped mlp with grouped bias and delayed wgrad (#4095)
ksivaman Apr 7, 2026
4cd3e8e
Enabled fused grouped MLP for `quick_gelu` and add config for grouped…
ksivaman Apr 9, 2026
81d4d95
Fix checkpoint loading with `load_main_params_from_ckpt=True` for gro…
ksivaman May 7, 2026
c824c66
test: align grouped-MLP fakes with PR 4095/4219 fields
Connor-XY May 7, 2026
4a38f4f
Use wrapper's combined delay_wgrad mode in fused path; add coverage
Connor-XY May 7, 2026
29c90ad
test: cover _normalize_state_dict_for_grouped_params; fix GeGLU typo
Connor-XY May 7, 2026
f3e955d
Fix fused grouped MLP wgrad hooks for DDP reduce-scatter (#4311)
gdengk Apr 17, 2026
0a9d86c
test: cover wrapper hook trigger in fused backward_dw
Connor-XY May 8, 2026
2a4e41a
[Dev] Skip routed expert padding for graph-safe MoE (#4071)
zhongbozhu Apr 3, 2026
4812c96
test: align fused_forward fake config with PR 4071 padding helper
Connor-XY May 8, 2026
23186ec
test: unwrap MlpBuilder partial via get_submodules in real-TE fused-o…
Connor-XY May 12, 2026
e9704da
Reject HybridEP dispatcher in fused TE op-fuser support check
Connor-XY May 12, 2026
1a99f7e
test: robust fused-path convergence test (Adam + target-driven loss)
Connor-XY May 12, 2026
093caf1
test: add functional MoE recipe exercising the fused TE op-fuser path
Connor-XY May 12, 2026
5d66788
lint: collapse pytest.skip onto a single line
Connor-XY May 12, 2026
20cfe67
Revert HybridEP guard and duplicate CLI flag
Connor-XY May 13, 2026
0909634
test: add golden values for fused TE op-fuser MoE functional test
Connor-XY May 13, 2026
3f4cd68
test: set NVTE_CUTEDSL_FUSED_GROUPED_MLP in functional recipe; clarif…
Connor-XY May 13, 2026
4868335
test: clarify cuDSL also requires SM100 (Blackwell)
Connor-XY May 13, 2026
46be5d1
test: export NVTE_CUTEDSL_FUSED_GROUPED_MLP=1 for MoE unit-test buckets
Connor-XY May 13, 2026
8d3920d
Fail fast: reject moe_single_grouped_weight outside fp8+mxfp8
Connor-XY May 13, 2026
d49ad27
Drop stale grad-mode tracking from #3890's param_and_grad_buffer hunk
Connor-XY May 14, 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
44 changes: 28 additions & 16 deletions megatron/core/distributed/param_and_grad_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ def __init__(
# or bucket.grad_data.
self.cached_param_buffer_shard_list = [None] * len(self.buckets)
self.cached_grad_buffer_shard_list = [None] * len(self.buckets)
# Cached views remember the grad-mode context in which they were created. Rebuild them
# after a mode change so param all-gathers never mutate no_grad-created views while
# autograd is enabled.
self._cached_param_buffer_shards_grad_enabled = None

@zhongbozhu zhongbozhu May 8, 2026 •

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.

this line of code looks already a dangling pointer in dev branch, but somehow it's still referenced in this PR, this might be related with merge conflict resolving, but we need more testing for this part

@Connor-XY Connor-XY May 8, 2026 •

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.

Addressed inline at #4636 (comment) — TL;DR: this PR is not dangling, the read site at L413 still uses this field. It is dangling on dev (consumption side removed by later refactors), but main doesn't have those refactors yet, so removing the protection here would re-introduce the autograd-mid-AG issue PR #3890 was originally guarding against. Awaiting @ksivaman's call on whether to delete or keep.


def reset(self):
"""
Expand Down Expand Up @@ -417,23 +421,31 @@ def start_param_sync(self, force_sync: bool = False):
# Standard distributed optimizer path: use _coalescing_manager.
# all_gather_into_tensor writes directly into a contiguous output buffer and
# does not need a copy-back step, so coalescing works correctly.
with _coalescing_manager(
self.intra_distributed_optimizer_instance_group, async_ops=async_op
) as cm:
for idx, bucket in enumerate(self.buckets):
if self.cached_param_buffer_shard_list[idx] is None:
self.cached_param_buffer_shard_list[idx] = shard_buffer(
bucket.param_data, self.intra_distributed_optimizer_instance_size
current_grad_enabled = torch.is_grad_enabled()
if self._cached_param_buffer_shards_grad_enabled != current_grad_enabled:

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.

this line has been removed in dev branch, @ksivaman can you double check the logic for this part?

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.

@zhongbozhu thanks — I traced this across main / dev / this PR. The state is more nuanced than fully-dangling:

_cached_param_buffer_shards_grad_enabled = None (init) current_grad_enabled check + cache reset (read site, ~L413) with torch.no_grad(): wrap (~L420)
nvidia/main absent absent absent
nvidia/dev present (dangling — never read) removed removed
this PR present present present

So this PR is not dangling — it carries both the init AND the read site (the read at L413 + the no_grad wrap that PR #3890 originally introduced). What was removed on dev is the consumption side, leaving only the orphan init.

Recent dev work (#4560, #4563, #4649, #4609) replaced the grad-mode tracking with explicit post_all_gather_processing hooks for MXFP8 / NVFP4 / layerwise overlap. Those hooks are NOT carried by this PR (different scope), so on main the grad-mode protection here is the only mechanism guarding against the autograd-mid-AG issue PR #3890 was written to fix.

@ksivaman can you confirm whether keeping the L413 read site + no_grad wrap is still required when #3890 lands on main ahead of the dev-side post-processing refactors, or whether the migration plan is to bring those over too? If the protection is no longer needed, happy to delete the dead init + read site here in one commit.

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.

@Connor-XY #4560 and #4649 are PRs to a temporary staging branch, so #4563 is the actual dev2main PR that we should consider if it should be shipped together this PR.

@Connor-XY Connor-XY May 12, 2026 •

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.

Thanks for the pointer to #4563 (and for clarifying that #4560 / #4649 are staging-branch PRs, not dev2main candidates).

On bundling #4563: I'd recommend keeping them separate.

Happy to flip the call if you'd rather bundle to amortize a single review cycle.

self.cached_param_buffer_shard_list = [None] * len(self.buckets)
self._cached_param_buffer_shards_grad_enabled = current_grad_enabled
# Param all-gather writes directly into bucket param buffers. These in-place updates
# are optimizer state movement, not differentiable work, so keep the cached
# destination views and the collective inside no_grad.
with torch.no_grad():
with _coalescing_manager(
self.intra_distributed_optimizer_instance_group, async_ops=async_op
) as cm:
for idx, bucket in enumerate(self.buckets):
if self.cached_param_buffer_shard_list[idx] is None:
self.cached_param_buffer_shard_list[idx] = shard_buffer(
bucket.param_data, self.intra_distributed_optimizer_instance_size
)
local_data_view = self.cached_param_buffer_shard_list[idx][
self.intra_distributed_optimizer_instance_rank
]
dist_all_gather_func(
bucket.param_data,
local_data_view,
group=self.intra_distributed_optimizer_instance_group,
async_op=async_op,
)
local_data_view = self.cached_param_buffer_shard_list[idx][
self.intra_distributed_optimizer_instance_rank
]
dist_all_gather_func(
bucket.param_data,
local_data_view,
group=self.intra_distributed_optimizer_instance_group,
async_op=async_op,
)
if async_op:
self.param_gather_handle = cm
else:
Expand Down
103 changes: 100 additions & 3 deletions megatron/core/extensions/transformer_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1707,11 +1707,11 @@ def __init__(
self.disable_parameter_transpose_cache = self.config.disable_parameter_transpose_cache

extra_kwargs = _get_extra_te_kwargs(config)

self.delay_wgrad_compute = (
self.config.delay_wgrad_compute
or self.config.overlap_dispatch_backward_with_experts_wgrad
)

if self.delay_wgrad_compute:
if is_te_min_version("2.3.0"):
extra_kwargs["delay_wgrad_compute"] = True
Expand Down Expand Up @@ -1755,6 +1755,14 @@ def __init__(
tp_size = 1
tp_group_for_te = None

if is_te_min_version("2.14.0"):
extra_kwargs["single_grouped_weight"] = getattr(
config, "moe_single_grouped_weight", False
)
extra_kwargs["single_grouped_bias"] = getattr(
config, "moe_single_grouped_bias", False
)

super().__init__(
num_gemms=num_gemms,
in_features=input_size,
Expand Down Expand Up @@ -1791,6 +1799,10 @@ def __init__(
setattr(weight, "partition_dim", part_dim)
setattr(weight, "partition_stride", 1)

self._register_load_state_dict_pre_hook(
type(self)._normalize_grouped_parameter_keys, with_module=True
)

def merge_extra_states(
self,
state_dict,
Expand Down Expand Up @@ -1881,6 +1893,76 @@ def merge_extra_states(

self._register_load_state_dict_pre_hook(merge_extra_states, with_module=True)

def _normalize_grouped_parameter_keys(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
"""Make grouped checkpoint keys compatible across parameter layouts.

Registered as a load_state_dict pre-hook to bridge checkpoints saved
in one layout (single grouped tensor vs per-GEMM indexed tensors)
and a model expecting the other.
"""

def maybe_remap_param(param_name: str, single_grouped: bool) -> None:
grouped_key = f"{prefix}{param_name}"
indexed_keys = [
f"{prefix}{param_name}{gemm_idx}" for gemm_idx in range(self.num_gemms)
]
has_grouped_key = grouped_key in state_dict
has_any_indexed_key = any(key in state_dict for key in indexed_keys)
has_all_indexed_keys = all(key in state_dict for key in indexed_keys)

if single_grouped:
if has_grouped_key or not has_all_indexed_keys:
return
state_dict[grouped_key] = torch.stack(
[state_dict.pop(key) for key in indexed_keys], dim=0
)
else:
if has_any_indexed_key or not has_grouped_key:
return
split_tensors = self._split_grouped_checkpoint_tensor(
state_dict.pop(grouped_key), grouped_key
)
for gemm_idx, tensor in enumerate(split_tensors):
state_dict[f"{prefix}{param_name}{gemm_idx}"] = tensor

maybe_remap_param("weight", getattr(self, "single_grouped_weight", False))
if self.use_bias:
maybe_remap_param("bias", getattr(self, "single_grouped_bias", False))

def _split_grouped_checkpoint_tensor(
self, tensor: torch.Tensor, checkpoint_key: str
) -> list[torch.Tensor]:
"""Split grouped checkpoint tensor into one tensor per GEMM."""
if hasattr(tensor, "split_into_quantized_tensors") and callable(
tensor.split_into_quantized_tensors
):
grouped_tensors = getattr(tensor, "quantized_tensors", None)
if grouped_tensors is None:
grouped_tensors = tensor.split_into_quantized_tensors()
if len(grouped_tensors) != self.num_gemms:
raise RuntimeError(
f"Grouped checkpoint tensor {checkpoint_key} has {len(grouped_tensors)} "
f"groups, expected {self.num_gemms}."
)
return list(grouped_tensors)
if tensor.ndim > 0 and tensor.shape[0] == self.num_gemms:
return list(tensor.unbind(dim=0))
if tensor.ndim > 0 and tensor.shape[0] % self.num_gemms == 0:
return list(torch.chunk(tensor, self.num_gemms, dim=0))
raise RuntimeError(
f"Cannot split checkpoint tensor {checkpoint_key} with shape {tuple(tensor.shape)} "
f"into {self.num_gemms} GEMM shards."
)

def finish_init(self, quantization_config: QuantizationConfig):
"""Post-init of quantization override"""
if quantization_config is None:
Expand Down Expand Up @@ -1987,18 +2069,33 @@ def _sharded_state_dict_grouped(
singleton_local_shards = (metadata or {}).get('singleton_local_shards', False)
sharded_state_dict = {}
full_state_dict = self.state_dict(prefix="", keep_vars=True)
grouped_split_cache = {}

def get_gemm_tensor(param_name: str, gemm_idx: int) -> torch.Tensor:
indexed_name = f"{param_name}{gemm_idx}"
if indexed_name in full_state_dict:
return full_state_dict[indexed_name]
if param_name not in full_state_dict:
raise KeyError(indexed_name)
if param_name not in grouped_split_cache:
grouped_split_cache[param_name] = self._split_grouped_checkpoint_tensor(
full_state_dict[param_name], param_name
)
grouped_splits = grouped_split_cache[param_name]
return grouped_splits[gemm_idx]

num_global_experts = get_pg_size(self._pg_collection.ep) * self.num_gemms
local_expert_indices_offset = get_pg_rank(self._pg_collection.ep) * self.num_gemms
ep_axis = len(sharded_offsets)
extra_states = self._split_extra_state(full_state_dict["_extra_state"])
for gemm_idx in range(self.num_gemms):
global_expert_idx = local_expert_indices_offset + gemm_idx
state_dict = {
f"{gemm_idx}.weight": full_state_dict[f"weight{gemm_idx}"],
f"{gemm_idx}.weight": get_gemm_tensor("weight", gemm_idx),
f"{gemm_idx}._extra_state": extra_states[gemm_idx],
}
if self.use_bias:
state_dict[f"{gemm_idx}.bias"] = full_state_dict[f"bias{gemm_idx}"]
state_dict[f"{gemm_idx}.bias"] = get_gemm_tensor("bias", gemm_idx)
if singleton_local_shards:
expert_prefix = f"{global_expert_idx}.{prefix}"
new_sharded_offsets = sharded_offsets
Expand Down
Loading
Loading