Skip to content
7 changes: 7 additions & 0 deletions .fork-base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"schema": 1,
"fork_repo": "https://github.com/AlignmentResearch/Megatron-LM",
"upstream_repo": "https://github.com/NVIDIA/Megatron-LM",
"upstream_branch": "main",
"upstream_base": "d12f6c8c9aff51e166d872fd70151687a8e3f375"
}
109 changes: 109 additions & 0 deletions megatron/core/extensions/transformer_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import re
import warnings
from contextlib import contextmanager, nullcontext
from itertools import accumulate
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, cast

import torch
Expand Down Expand Up @@ -2358,6 +2359,7 @@ def __init__(
name (str | None): module instance name passed top-down from its paranet module
"""
self.config = config
self.expert_gemm_backend = config.moe_expert_gemm_backend

# TE returns a zero length Tensor when bias=False and
# return_bias=True, but we prefer None. So in that case we
Expand Down Expand Up @@ -2632,6 +2634,110 @@ def _split_grouped_checkpoint_tensor(
f"into {self.num_gemms} GEMM shards."
)

def _torch_grouped_weight_layout_is_current(self) -> bool:
"""Return whether each expert parameter still views the contiguous buffer."""
grouped_weight = self._buffers.get('_torch_grouped_weight')
if grouped_weight is None or grouped_weight.shape[0] != self.num_gemms:
return False

first_weight = self.weight0
if (
grouped_weight.device != first_weight.device
or grouped_weight.dtype != first_weight.dtype
or grouped_weight.shape[1:] != first_weight.shape
):
return False

storage_pointer = grouped_weight.untyped_storage().data_ptr()
expert_elements = first_weight.numel()
for index in range(self.num_gemms):
weight = getattr(self, f'weight{index}')
grouped_weight_view = grouped_weight[index]
if (
weight.device != grouped_weight.device
or weight.dtype != grouped_weight.dtype
or weight.shape != grouped_weight_view.shape
or weight.untyped_storage().data_ptr() != storage_pointer
or weight.storage_offset() != index * expert_elements
or weight.stride() != grouped_weight_view.stride()
):
return False
return True

@torch.no_grad()
def prepare_torch_grouped_mm(self) -> int:
"""Relocate frozen BF16 expert weights into one contiguous allocation.

The existing per-expert ``Parameter`` objects become views of the new allocation, so
parameter names and checkpoint structure remain unchanged. The backing allocation is a
non-persistent buffer and therefore does not add a checkpoint entry.

Returns:
Number of bytes in the contiguous backing allocation.
"""
if self.expert_gemm_backend != 'torch':
raise RuntimeError(
"prepare_torch_grouped_mm requires moe_expert_gemm_backend='torch'"
)
if not hasattr(torch, '_grouped_mm'):
raise RuntimeError("this PyTorch build does not provide torch._grouped_mm")
if self.use_bias:
raise RuntimeError("torch grouped expert GEMM does not support bias")
if getattr(self, 'single_grouped_weight', False):
raise RuntimeError(
"torch grouped expert GEMM expects per-GEMM weight parameters, but "
"moe_single_grouped_weight=True makes TE hold one grouped weight tensor"
)

weights = [getattr(self, f'weight{index}') for index in range(self.num_gemms)]
if any(weight.requires_grad for weight in weights):
raise RuntimeError("torch grouped expert GEMM requires frozen base weights")
if any(weight.device.type != 'cuda' for weight in weights):
raise RuntimeError("torch grouped expert GEMM requires CUDA weights")
if any(weight.dtype != torch.bfloat16 for weight in weights):
raise RuntimeError("torch grouped expert GEMM requires BF16 weights")
if any(weight.shape != weights[0].shape for weight in weights[1:]):
raise RuntimeError("torch grouped expert GEMM requires uniform expert shapes")

if not self._torch_grouped_weight_layout_is_current():
grouped_weight = torch.stack([weight.detach() for weight in weights]).contiguous()
for index, weight in enumerate(weights):
weight.data = grouped_weight[index]
if '_torch_grouped_weight' in self._buffers:
self._buffers['_torch_grouped_weight'] = grouped_weight
else:
self.register_buffer('_torch_grouped_weight', grouped_weight, persistent=False)

grouped_weight = self._buffers['_torch_grouped_weight']
return grouped_weight.numel() * grouped_weight.element_size()

@property
def torch_grouped_mm_prepared(self) -> bool:
"""Return whether the torch grouped GEMM backing allocation is ready."""
return self._torch_grouped_weight_layout_is_current()

def _torch_grouped_mm_forward(self, x, m_splits):
"""Run the frozen expert base branch with ``torch._grouped_mm``."""
if not self._torch_grouped_weight_layout_is_current():
self.prepare_torch_grouped_mm()

x_2d = x.reshape(-1, x.shape[-1])
input_rows = sum(m_splits)
if input_rows != x_2d.shape[0]:
raise RuntimeError(
f"expert splits sum to {input_rows}, but input has {x_2d.shape[0]} rows"
)

grouped_weight = self._buffers['_torch_grouped_weight']
if input_rows == 0:
output = x_2d.matmul(grouped_weight[0].transpose(0, 1))
else:
offsets = torch.tensor(
list(accumulate(m_splits)), device=x.device, dtype=torch.int32
)
output = torch._grouped_mm(x_2d, grouped_weight.transpose(1, 2), offs=offsets)
return output.reshape(*x.shape[:-1], output.shape[-1]), None

def finish_init(self, quantization_config: QuantizationConfig):
"""Post-init of quantization override"""
if quantization_config is None:
Expand All @@ -2647,6 +2753,9 @@ def will_execute_quantized(self, is_context_quantized: bool) -> bool:

def forward(self, x, m_splits):
"""Forward."""
if self.expert_gemm_backend == 'torch':
return self._torch_grouped_mm_forward(x, m_splits)

_is_first_microbatch = (
None if self.disable_parameter_transpose_cache else self.is_first_microbatch
)
Expand Down
7 changes: 7 additions & 0 deletions megatron/core/recompute.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ def chunk_runner(start: int, end: int, use_checkpoint: bool):
if (start + layer_offset) in extract_layer_indices:
intermediate_hidden_states.append(hidden_states)

if not hidden_states.requires_grad:
# Re-entrant checkpointing only attaches a grad_fn when some tensor input
# requires grad. With a frozen embedding (adapter-only training) every chunk
# output would otherwise carry no grad_fn and the adapters inside the chunks
# would receive no gradient.
hidden_states = hidden_states.detach().requires_grad_(True)

if self.config.recompute_method == 'uniform':
# Uniformly divide the total number of layers and checkpoint
# the input activation of each divided chunk.
Expand Down
37 changes: 37 additions & 0 deletions megatron/core/transformer/transformer_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,14 @@ class TransformerConfig(ModelParallelConfig):
parameter via Transformer Engine's `GroupedTensor`. Requires ``moe_grouped_gemm=True``
and ``add_bias_linear=True``."""

moe_expert_gemm_backend: Literal['transformer_engine', 'torch'] = 'transformer_engine'
"""Backend for grouped expert linear layers during training.

``torch`` uses ``torch._grouped_mm`` for frozen BF16, bias-free expert weights. It is intended
for parameter-efficient fine-tuning where only the adapter branch is trainable. The default
``transformer_engine`` backend supports trainable expert weights and other precisions.
"""

moe_aux_loss_coeff: Union[float, List[float]] = 0.0
"""Scaling coefficient for the aux loss. A starting value of 1e-2 is recommended.
If a list of load balancing types is provided for `moe_router_load_balancing_type`,
Expand Down Expand Up @@ -1551,6 +1559,35 @@ def __post_init__(self):
if self.num_moe_experts is not None and self.num_moe_experts <= 0:
raise ValueError("num_moe_experts must be non-negative.")

if self.moe_expert_gemm_backend not in ('transformer_engine', 'torch'):
raise ValueError(
"moe_expert_gemm_backend must be 'transformer_engine' or 'torch', "
f"got {self.moe_expert_gemm_backend!r}"
)
if self.moe_expert_gemm_backend == 'torch':
if self.num_moe_experts is None:
raise ValueError("moe_expert_gemm_backend='torch' requires num_moe_experts")
if not self.moe_grouped_gemm:
raise ValueError("moe_expert_gemm_backend='torch' requires moe_grouped_gemm=True")
if not self.bf16 or self.params_dtype != torch.bfloat16:
raise ValueError("moe_expert_gemm_backend='torch' requires BF16 parameters")
if self.add_bias_linear:
raise ValueError("moe_expert_gemm_backend='torch' does not support expert bias")
if self.fp8 or self.fp4:
raise ValueError(
"moe_expert_gemm_backend='torch' does not support FP8 or FP4 experts"
)
if self.moe_single_grouped_weight:
raise ValueError(
"moe_expert_gemm_backend='torch' requires per-GEMM expert weight parameters "
"and is incompatible with moe_single_grouped_weight=True"
)
if self.use_transformer_engine_op_fuser:
raise ValueError(
"moe_expert_gemm_backend='torch' is incompatible with "
"use_transformer_engine_op_fuser=True"
)

if self.num_moe_experts is not None and self.moe_ffn_hidden_size is None:
self.moe_ffn_hidden_size = self.ffn_hidden_size
warnings.warn("moe_ffn_hidden_size is not set, using ffn_hidden_size instead.")
Expand Down
40 changes: 40 additions & 0 deletions tests/unit_tests/ssm/test_hybrid_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,46 @@ def run(block, hs, am):
gb, gr = base_grads[name], rec_grads[name]
assert torch.equal(gr, gb), f"Grad should be bitwise matched for {name}"

def test_full_recompute_with_frozen_input_trains_adapter(self):
"""Full re-entrant recompute preserves gradients for adapter-only training."""
block = self.get_hybrid_block(
Symbols.MLP,
add_bias_linear=False,
recompute_granularity="full",
recompute_method="uniform",
recompute_num_layers=1,
).cuda()
block.requires_grad_(False)
block.train()

adapter = torch.nn.Sequential(
torch.nn.Linear(block.config.hidden_size, 4, bias=False),
torch.nn.Linear(4, block.config.hidden_size, bias=False),
).cuda()
block.layers[0].add_module("adapter", adapter)

def add_adapter(_module, _args, kwargs, output):
hidden_states, context = output
return hidden_states + adapter(kwargs["hidden_states"]), context

block.layers[0].register_forward_hook(add_adapter, with_kwargs=True)

sequence_length, micro_batch_size = 4, 1
hidden_states = torch.randn(
sequence_length, micro_batch_size, block.config.hidden_size, device="cuda"
)
attention_mask = torch.ones(
(micro_batch_size, 1, sequence_length, sequence_length), dtype=bool, device="cuda"
)

assert not hidden_states.requires_grad
output = block(hidden_states, attention_mask=attention_mask)
output.float().square().mean().backward()

for parameter in adapter.parameters():
assert parameter.grad is not None
assert torch.count_nonzero(parameter.grad) > 0

def test_layer_types(self):
"""
Make sure that the layer types specified with layer_pattern
Expand Down
Loading
Loading