Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
feba22c
[Split 1/N of #3430] feat(mHC): basic pytorch implementation of manif…
Connor-XY Apr 29, 2026
2dd457d
test(mHC): make test_transformer_layer self-contained for split #1
Connor-XY Apr 29, 2026
1a31f14
test(mHC): update test_hybrid_moe_model GOLDEN_CONFIG for new mHC fields
Connor-XY Apr 29, 2026
f1d51a5
test(mHC): add direct coverage for BDA recompute path and block-level…
Connor-XY Apr 30, 2026
3acfdb0
style: collapse short attention_mask to one line for black
Connor-XY Apr 30, 2026
c453dc7
refactor(mHC): address mathemakitten review on PR #4531
Connor-XY May 4, 2026
210f189
refactor(mHC): drop dead is_last_layer_in_recompute_block kwarg pop
Connor-XY May 4, 2026
aa12d2d
refactor(mHC): extract attention hooks to dedup _forward_attention
Connor-XY May 4, 2026
cdb576c
refactor(mHC): thread h_res/h_post via attn_state slot, not instance …
Connor-XY May 4, 2026
07365cb
style(mHC): restore _set_proj_residual context comment
Connor-XY May 4, 2026
41e0e17
refactor(mHC): extract MLP hooks to dedup _forward_mlp
Connor-XY May 6, 2026
143b9cf
style(mHC): shorten docstring line to fit 100-char pylint limit
Connor-XY May 6, 2026
481138b
fix(mHC): only thread mhc_recompute_manager when manager exists
Connor-XY May 6, 2026
009de20
refactor(mHC): add back-compat shim for renamed _forward_post_mlp hook
Connor-XY May 7, 2026
096064e
test(mHC): pass pipeline rank to offload manager
Connor-XY Jul 15, 2026
f51948a
fix(mhc): update CUDA graph module API
FDecaYed Jul 23, 2026
c8cda51
refactor(mHC): address review feedback
Connor-XY Aug 13, 2026
dd54c69
fix(mHC): drop a bad rebase hunk and guard unsupported mHC configs
Connor-XY Aug 13, 2026
2e5bf78
refactor(mHC): drop dead code and move remaining guards to config/init
Connor-XY Aug 13, 2026
b57eba5
test(mHC): make the full-recompute guard case reach the mHC check
Connor-XY Aug 13, 2026
700a9ef
fix(mHC): do not reject MoE at config level
Connor-XY Aug 13, 2026
cc80c69
test(mHC): give the PP-guard case a pipeline_dtype
Connor-XY Aug 13, 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
169 changes: 149 additions & 20 deletions megatron/core/tensor_parallel/random.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.

# Parts of the code here are adapted from PyTorch
# repo: https://github.com/pytorch/pytorch
Expand Down Expand Up @@ -634,7 +634,9 @@ def forward(
@staticmethod
def backward(ctx, *args):
"""Backward pass."""
if not torch.autograd._is_checkpoint_valid():
from megatron.core.transformer.cuda_graphs import is_graph_capturing

if not torch.autograd._is_checkpoint_valid() and not is_graph_capturing():
raise RuntimeError(
"Checkpointing is not compatible with .grad(), "
"please use .backward() if possible"
Expand Down Expand Up @@ -685,10 +687,67 @@ def checkpoint(
return CheckpointFunction.apply(function, distribute_saved_activations, *args)


def _save_args_to_ctx(ctx, args):
"""Save mixed tensor/non-tensor arguments into autograd ctx.

Since save_for_backward only supports tensors, this function separates
tensor and non-tensor arguments, saving tensors via save_for_backward
and storing non-tensor metadata (indices and values) as ctx attributes.

Use _load_args_from_ctx to reconstruct the original args.
"""
tensor_args = []
non_tensor_entries = []

for index, arg in enumerate(args):
if isinstance(arg, torch.Tensor):
tensor_args.append(arg)
continue
non_tensor_entries.append((index, arg))

ctx.save_for_backward(*detach_variable(tuple(tensor_args)))
ctx._non_tensor_entries = tuple(non_tensor_entries)
ctx._total_args_count = len(args)


def _load_args_from_ctx(ctx):
"""Load and reconstruct mixed tensor/non-tensor arguments from autograd ctx.

This is the inverse of _save_args_to_ctx. It retrieves tensors from
ctx.saved_tensors and merges them with stored non-tensor arguments
to reconstruct the original args in their original order.

Returns:
tuple of reconstructed arguments in their original order.
"""

def _detach_with_grad(tensor):
detached = tensor.detach()
detached.requires_grad_(tensor.requires_grad)
return detached

tensor_iter = iter(_detach_with_grad(t) for t in ctx.saved_tensors)
total_args_count = ctx._total_args_count
non_tensor_map = dict(ctx._non_tensor_entries)

reconstructed_args = []
for index in range(total_args_count):
if index in non_tensor_map:
reconstructed_args.append(non_tensor_map[index])
else:
reconstructed_args.append(next(tensor_iter))
return tuple(reconstructed_args)


class CheckpointWithoutOutputFunction(torch.autograd.Function):
"""
Checkpoint Function Helper for CheckpointWithoutOutput.
Save context for recompute.

Handles both tensor and non-tensor arguments:
- Tensor arguments are saved via save_for_backward
- Non-tensor arguments (int, float, bool, None, etc.) are stored separately
in ctx attributes and reconstructed during recomputation
"""

@staticmethod
Expand All @@ -711,7 +770,10 @@ def forward(

with torch.no_grad(), fwd_ctx:
outputs = run_function(*args)
ctx.save_for_backward(*detach_variable(args))

# Save tensor and non-tensor arguments into ctx for recomputation
_save_args_to_ctx(ctx, args)

# the CheckpointWithoutOutput object is passed in, then it can access the saved input
# tensors later for recomputation
checkpoint_without_output_obj.ctx = ctx
Expand All @@ -728,10 +790,56 @@ def backward(ctx, *args):
torch.autograd.backward(outputs, args)
ctx.outputs = None
ctx.inputs = None
grads = tuple(inp.grad if isinstance(inp, torch.Tensor) else inp for inp in inputs)
grads = tuple(inp.grad if isinstance(inp, torch.Tensor) else None for inp in inputs)
return (None, None) + grads


class CheckpointWithoutOutputManager:
"""
Coordinates activation recomputation across multiple CheckpointWithoutOutput instances
within a TransformerBlock, enabling unified recomputation during backward pass.
This is particularly useful for scenarios where multiple checkpoint operations have
sequential dependencies (i.e., the output of one checkpoint is the input of the next).
Comment thread
mathemakitten marked this conversation as resolved.

Usage:
manager = CheckpointWithoutOutputManager()
ckpt_function = CheckpointWithoutOutput(ckpt_manager=manager)
ckpt_function.checkpoint(run_function, *args)
# other checkpointed operations
manager.discard_all_outputs_and_register_unified_recompute(final_output)
"""

def __init__(self):
self.checkpoints = []
# Set by TransformerBlock before each layer forward.
# When True, the layer should keep block-boundary output uncheckpointed.
self.is_last_layer_in_recompute_block = False

def add_checkpoint(self, ckpt):
"""Add a checkpoint to the manager."""
if not isinstance(ckpt, CheckpointWithoutOutput):
raise TypeError("Expected CheckpointWithoutOutput object")
if ckpt.outputs is None:
raise ValueError("CheckpointWithoutOutput must call checkpoint() before adding")
self.checkpoints.append(ckpt)

def discard_all_outputs_and_register_unified_recompute(self, hook_tensor):
"""Discard all checkpoint outputs to save memory and register unified recompute hook."""
for ckpt in self.checkpoints:
for output in ckpt.outputs:
output.untyped_storage().resize_(0)

# Register unified recompute hook
if hook_tensor.requires_grad:
hook_tensor.register_hook(self._unified_recompute_hook)

def _unified_recompute_hook(self, grad_output):
for ckpt in self.checkpoints:
# Call _recompute for each checkpoint in forward order
# The _recompute method will restore the output tensor storage
ckpt._recompute(None)


class CheckpointWithoutOutput(object):
"""
Checkpoint a model or part of the model and release the output.
Expand All @@ -746,8 +854,27 @@ class CheckpointWithoutOutput(object):
discarded output tensors are directly saved in the following modules for backward computation.
"""

def __init__(self, fp8=False):
def __init__(self, fp8=False, ckpt_manager=None):
"""
Initialize CheckpointWithoutOutput.

Args:
fp8: Quantization recipe, or a bool. Note that the default `fp8=False`
still evaluates to `self.fp8 = True`; every caller that constructs
`CheckpointWithoutOutput()` with no arguments therefore takes the
TE `activation_recompute_forward` path. That is long-standing
behavior which several selective-recompute modules ("layernorm",
"moe_act", "gdn_norm_out") depend on for correct FP8 amax
bookkeeping, so do NOT "fix" this to `bool(fp8)` here — tightening
it changes FP8 numerics and needs its own PR with FP8
functional-test evidence.
ckpt_manager: Optional CheckpointWithoutOutputManager instance. When provided,
checkpoint() will auto-register to the manager, and
discard_output_and_register_recompute() will only discard
output without registering individual hooks.
"""
self.fp8 = fp8 is not None
self.ckpt_manager = ckpt_manager
self.run_function = None
self.fwd_cpu_rng_state = None
self.fwd_cuda_rng_state = None
Expand All @@ -756,7 +883,12 @@ def __init__(self, fp8=False):
self.outputs = None

def checkpoint(self, run_function: Callable[[Unpack[_Ts]], _R], *args: Unpack[_Ts]) -> _R:
"""Checkpoint function."""
"""
Checkpoint function.

If ckpt_manager was provided during initialization, this checkpoint
will be automatically registered to the manager after execution.
"""

# If in cuda graph warmup, disable checkpointing, as 'discard_output_and_register_recompute'
# may be called in a separate graph warmup.
Expand All @@ -773,6 +905,11 @@ def checkpoint(self, run_function: Callable[[Unpack[_Ts]], _R], *args: Unpack[_T
self.outputs = outputs
if isinstance(self.outputs, torch.Tensor):
self.outputs = (self.outputs,)

# Auto-register to manager if provided
if self.ckpt_manager is not None:
self.ckpt_manager.add_checkpoint(self)

return outputs

def _recompute(self, _):
Expand All @@ -781,7 +918,7 @@ def _recompute(self, _):
from megatron.core.transformer.cuda_graphs import is_graph_capturing, is_graph_warmup

# The recomputation has been triggered already. Just return.
# Handle cudagraphs, do nothing if currently in graph warmup
# Handle cudagraphs: do nothing if currently in graph warmup
if self.ctx is None or is_graph_warmup():
return

Expand All @@ -803,17 +940,8 @@ def _recompute(self, _):
recompute_ctx = contextlib.nullcontext()
fp8_ctx = contextlib.nullcontext()

# Store the inputs for backward pass
inputs = self.ctx.saved_tensors

def detach(t):
if isinstance(t, torch.Tensor):
requires_grad = t.requires_grad
t = t.detach()
t.requires_grad_(requires_grad)
return t

inputs = tuple(detach(t) for t in inputs)
# Reconstruct full args list from saved ctx
inputs = _load_args_from_ctx(self.ctx)
with torch.enable_grad(), fp8_ctx, recompute_ctx:
outputs = self.run_function(*inputs)

Expand Down Expand Up @@ -846,10 +974,11 @@ def discard_output_and_register_recompute(self, hook_tensor):
in the forward pass and the gradient of the hook_tensor is computed before the recomputed
tensors are used.
"""

# When ckpt_manager is set, this is a no-op.
# Manager handles all discarding and hook registration uniformly.
from megatron.core.transformer.cuda_graphs import is_graph_warmup

if is_graph_warmup():
if self.ckpt_manager is not None or is_graph_warmup():
return

# use resize to release the output tensor memory and still keep the metadata in the tensors.
Expand Down
8 changes: 6 additions & 2 deletions megatron/core/transformer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved.
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.

from .module import MegatronModule
from .spec_utils import ModuleSpec, build_module
from .transformer_config import MLATransformerConfig, TransformerConfig
from .transformer_layer import TransformerLayer, TransformerLayerSubmodules
from .transformer_layer import (
HyperConnectionTransformerLayer,
TransformerLayer,
TransformerLayerSubmodules,
)
Loading
Loading