Skip to content
Merged
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
41 changes: 41 additions & 0 deletions megatron/core/ssm/gated_delta_net.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import torch.nn.functional as F
from torch import Tensor

from megatron.core import tensor_parallel
from megatron.core.dist_checkpointing import ShardedTensor
from megatron.core.dist_checkpointing.mapping import ReplicaId, ShardedTensorFactory
from megatron.core.fp8_utils import get_fp8_align_size
Expand Down Expand Up @@ -260,6 +261,13 @@ def __init__(
name=(name + ".out_proj") if name is not None else None,
)

# Whole-module recompute: when "gdn" is in recompute_modules (selective granularity),
# the entire GatedDeltaNet compute is wrapped in a normal checkpoint and recomputed
# in the backward pass.
self.recompute_gdn = False
if self.config.recompute_granularity == "selective" and self.config.recompute_modules:
self.recompute_gdn = "gdn" in self.config.recompute_modules

self.reset_parameters()

def reset_parameters(self):
Expand Down Expand Up @@ -363,6 +371,39 @@ def forward(
cu_seqlens_q = None
cu_seqlens_kv = None

if self.recompute_gdn and self.training:

def _checkpointed_compute(hidden_states):
return self._forward_compute(
hidden_states,
batch,
seq_len,
cp_size,
cp_group,
cu_seqlens_q,
packed_seq_params,
)

out, out_bias = tensor_parallel.checkpoint(_checkpointed_compute, False, hidden_states)
else:
out, out_bias = self._forward_compute(
hidden_states, batch, seq_len, cp_size, cp_group, cu_seqlens_q, packed_seq_params
)

return out, out_bias

def _forward_compute(
self, hidden_states, batch, seq_len, cp_size, cp_group, cu_seqlens_q, packed_seq_params
):
"""Core GDN computation (in_proj -> conv1d -> gated_delta_rule -> gated norm -> out_proj).

Extracted from ``forward`` so the entire module can be wrapped in a recompute
checkpoint when ``recompute_modules`` contains ``"gdn"`` (selective full-module
recompute, normal checkpointing).

Returns:
Tuple of (output, output_bias).
"""
# Input projection
nvtx_range_push(suffix="in_proj")
qkvzba, _ = self.in_proj(hidden_states)
Expand Down
17 changes: 15 additions & 2 deletions megatron/core/transformer/transformer_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ class TransformerConfig(ModelParallelConfig):
recompute_modules: Optional[List[str]] = None
"""The submodules to recompute.
choices: "core_attn", "moe_act", "layernorm", "mla_up_proj", "mlp", "moe",
"shared_experts", "mhc".
"shared_experts", "mhc", "gdn".
default: ["core_attn"].
"core_attn": recompute the core attention part of the transformer layer.
"moe_act": recompute the MoE MLP activation function.
Expand All @@ -566,8 +566,11 @@ class TransformerConfig(ModelParallelConfig):
"mhc": recompute HyperConnection intermediate activations via
CheckpointWithoutOutput + CheckpointManager. Requires
enable_hyper_connections=True. Cannot be used with "mlp".
"gdn": recompute the entire GatedDeltaNet module (in_proj, conv1d, gated delta rule,
gated norm, CP all-to-all and out_proj). Requires
experimental_attention_variant="gated_delta_net".
"moe_act", "layernorm", "mla_up_proj", and "mhc" use output-discarding checkpointing,
"core_attn", "mlp", "moe", and "shared_experts" use normal checkpointing.
"core_attn", "mlp", "moe", "shared_experts", and "gdn" use normal checkpointing.
"""

####################
Expand Down Expand Up @@ -1865,6 +1868,7 @@ def __post_init__(self):
"moe",
"shared_experts",
"mhc",
"gdn",
}
invalid_modules = set(self.recompute_modules) - allowed_modules
assert not invalid_modules, (
Expand All @@ -1883,6 +1887,15 @@ def __post_init__(self):
"multi_latent_attention."
)

if (
"gdn" in self.recompute_modules
and self.experimental_attention_variant != "gated_delta_net"
):
raise ValueError(
"gdn in recompute_modules is only supported with "
"experimental_attention_variant='gated_delta_net'."
)

if "core_attn" in self.recompute_modules:
warnings.warn(
"If you are using transformer_engine as the transformer implementation, "
Expand Down
52 changes: 52 additions & 0 deletions tests/unit_tests/ssm/test_gated_delta_net.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,58 @@ def test_gpu_forward(self):
output.dtype == hidden_states.dtype
), f"Output dtype {output.dtype=} mismatch with {hidden_states.dtype=}"

def test_selective_recompute_gdn(self):
"""Whole-module 'gdn' recompute must match the non-recompute forward and gradients.

The same module/input is run twice (recompute off, then on); the forward output and
all parameter / input gradients must agree within a tight tolerance (rtol/atol=1e-4).
The recompute path is run-to-run deterministic on these kernels (empirically bitwise),
so a tolerance well below the bf16 floor is expected to hold.
"""
gdn = self.gdn
gdn.train()

micro_batch_size = 2
seq_length = 64
torch.manual_seed(1234)
base_input = torch.randn(
(seq_length // self.sp_size // self.cp_size, micro_batch_size, gdn.config.hidden_size),
device=torch.cuda.current_device(),
dtype=torch.bfloat16,
)

def run(recompute):
gdn.recompute_gdn = recompute
gdn.zero_grad(set_to_none=True)
hidden_states = base_input.clone().detach().requires_grad_(True)
output, _ = gdn(hidden_states, None)
output.float().square().mean().backward()
param_grads = {
name: param.grad.detach().clone()
for name, param in gdn.named_parameters()
if param.grad is not None
}
return output.detach().clone(), hidden_states.grad.detach().clone(), param_grads

try:
out_ref, dinput_ref, pgrad_ref = run(recompute=False)
out_rc, dinput_rc, pgrad_rc = run(recompute=True)
finally:
gdn.recompute_gdn = False

torch.testing.assert_close(out_rc, out_ref, rtol=1e-4, atol=1e-4)
torch.testing.assert_close(dinput_rc, dinput_ref, rtol=1e-4, atol=1e-4)
assert pgrad_ref.keys() == pgrad_rc.keys(), "recompute changed the set of grad params"
assert len(pgrad_ref) > 0, "expected at least one parameter gradient"
for name in pgrad_ref:
torch.testing.assert_close(
pgrad_rc[name],
pgrad_ref[name],
rtol=1e-4,
atol=1e-4,
msg=lambda m, n=name: f"gradient mismatch for parameter '{n}': {m}",
)

def test_jit_compiled_helpers(self):
import torch._dynamo

Expand Down
Loading