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
9 changes: 6 additions & 3 deletions megatron/core/distributed/distributed_data_parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def __init__(
# disable_bucketing is True (e.g., we might not want to break up model parameters
# into buckets for model chunks after the first in the interleaved schedule).
self.bucket_size = self.ddp_config.bucket_size
self.force_all_reduce = False
if isinstance(self.pp_group, list):
pp_rank = self.pp_group[0].rank()
else:
Expand Down Expand Up @@ -440,7 +441,9 @@ def hook(*unused):
param.grad = None

if self.ddp_config.overlap_grad_reduce:
self.param_to_bucket_group[param].register_grad_ready(param)
self.param_to_bucket_group[param].register_grad_ready(
param, self.force_all_reduce
)

return hook

Expand Down Expand Up @@ -519,7 +522,7 @@ def start_grad_sync(self, *unused):
for bucket_group in self.bucket_groups + self.expert_parallel_bucket_groups:
bucket_group.start_grad_sync()

def finish_grad_sync(self):
def finish_grad_sync(self, force_all_reduce: Optional[bool] = False):
"""
Finishes grad sync (all-reduce or reduce-scatter) communication operations
for all model gradients.
Expand All @@ -529,7 +532,7 @@ def finish_grad_sync(self):
communication ops.
"""
for bucket_group in self.bucket_groups + self.expert_parallel_bucket_groups:
bucket_group.finish_grad_sync()
bucket_group.finish_grad_sync(force_all_reduce=force_all_reduce)

def scale_gradients(self, scaling_factor: float):
"""Scale all gradients inside the buffers by `scaling_factor`."""
Expand Down
3 changes: 2 additions & 1 deletion megatron/core/distributed/finalize_model_grads.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ def finalize_model_grads(
model: List[torch.nn.Module],
num_tokens: Optional[torch.Tensor] = None,
pg_collection: Optional[ProcessGroupCollection] = None,
force_all_reduce: Optional[bool] = False,
):
"""
All-reduce all model grads across DP replicas, layernorm grads for sequence parallelism,
Expand Down Expand Up @@ -439,7 +440,7 @@ def finalize_model_grads(
if config.timers is not None:
config.timers('all-grads-sync', log_level=1).start(barrier=config.barrier_with_L1_time)
for model_chunk in model:
model_chunk.finish_grad_sync()
model_chunk.finish_grad_sync(force_all_reduce=force_all_reduce)
if config.timers is not None:
config.timers('all-grads-sync').stop()

Expand Down
22 changes: 14 additions & 8 deletions megatron/core/distributed/param_and_grad_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ def finish_param_sync(self, skip_next_bucket_dispatch: bool = False):
if len(fp8_params) > 0:
post_all_gather_processing(fp8_params)

def start_grad_sync(self):
def start_grad_sync(self, force_all_reduce: Optional[bool] = False):
"""
Initiates grad sync (all-reduce or reduce-scatter) communication operations
for all buckets in the bucket group.
Expand Down Expand Up @@ -426,7 +426,7 @@ def start_grad_sync(self):
grad_reduce_handle = None
with stream_context, _coalescing_manager(communication_group, async_ops=async_op) as cm:
for idx, bucket in enumerate(self.buckets):
if self.ddp_config.use_distributed_optimizer:
if self.ddp_config.use_distributed_optimizer and not force_all_reduce:
if self.cached_grad_buffer_shard_list[idx] is None:
self.cached_grad_buffer_shard_list[idx] = shard_buffer(
bucket.grad_data, self.intra_distributed_optimizer_instance_size
Expand All @@ -442,6 +442,10 @@ def start_grad_sync(self):
async_op=async_op,
)
else:
if torch.distributed.get_rank() == 0 and force_all_reduce:
logger.info(
f"Performing reduction using all_reduce because {force_all_reduce=}"
)
torch.distributed.all_reduce(
bucket.grad_data, op=reduce_op, group=communication_group, async_op=async_op
)
Expand Down Expand Up @@ -476,7 +480,7 @@ def start_grad_sync(self):
)

if async_op:
if self.ddp_config.reduce_scatter_with_fp32_accumulation:
if self.ddp_config.reduce_scatter_with_fp32_accumulation and not force_all_reduce:
assert (
len(self.buckets) == 1
), "Only 1 bucket supported with reduce_scatter_with_fp32_accumulation=True"
Expand All @@ -494,7 +498,7 @@ def start_grad_sync(self):
# None.
self.grad_reduce_handle = None

def finish_grad_sync(self):
def finish_grad_sync(self, force_all_reduce: Optional[bool] = False):
"""
Finishes grad sync (all-reduce or reduce-scatter) communication operations
for all buckets in the bucket group.
Expand All @@ -506,13 +510,13 @@ def finish_grad_sync(self):
self.param_gather_dispatched = False
# If overlap_grad_reduce is False, start (and finish) synchronous communication call here.
if not self.ddp_config.overlap_grad_reduce:
self.start_grad_sync()
self.start_grad_sync(force_all_reduce=force_all_reduce)
return
# If first batch, start asynchronous communication here. register_grad_ready() launches
# asynchronous communication only once self.golden_per_param_grad_ready_counts is
# populated at the end of this first batch.
if self.is_first_batch:
self.start_grad_sync()
self.start_grad_sync(force_all_reduce=force_all_reduce)
# When using multiple DistOpt instances, we don't need to sync here as we launch
# communications on a separate communication stream.
if self.ddp_config.num_distributed_optimizer_instances > 1:
Expand All @@ -526,7 +530,9 @@ def finish_grad_sync(self):
self.grad_reduce_handle.wait()
self.grad_reduce_handle = None

def register_grad_ready(self, param: torch.nn.Parameter):
def register_grad_ready(
self, param: torch.nn.Parameter, force_all_reduce: Optional[bool] = False
):
"""
Registers grads for the passed-in param to be "ready" for grad sync.

Expand All @@ -546,7 +552,7 @@ def register_grad_ready(self, param: torch.nn.Parameter):
if not self.is_first_batch:
if self.per_param_grad_ready_counts == self.golden_per_param_grad_ready_counts:
assert len(self.per_param_grad_ready_counts) == len(self.params)
self.start_grad_sync()
self.start_grad_sync(force_all_reduce=force_all_reduce)


class _ParamAndGradBuffer:
Expand Down
16 changes: 14 additions & 2 deletions megatron/core/pipeline_parallel/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ def forward_step(data_iterator, model):
decoder_seq_length (int, optional): The sequence length for the decoder in a dual-stack
transformer. This is ignored for a single-stack transformer.

forward_only (optional, default = False): Perform only the forward step
forward_only (optional, default = False): Perform only the forward step.

collect_non_loss_data (optional, bool, default=False): TODO
collect_non_loss_data (optional, bool, default=False): TODO.

first_val_step (bool, optional): Is the first step of the validation phase. Used by
Transformer Engine modules to only update their fp8 weights only on the first validation
Expand All @@ -125,11 +125,17 @@ def forward_step(data_iterator, model):
respective list of shapes. Thus it is not used in the other forward-backward functions
which have different shape handling.

force_all_reduce (bool, optional): If true, force use of all-reduce for gradient reduction
instead of reduce-scatter (if using distributed optimizer) in this iteration to ensure all
data-parallel ranks have fully reduced gradients. This is useful for easier wgrad saving
(can just inspect DP replica 0 to get full set of wgrads for entire model).

Args:
pp_size (Optional[int]): Pipeline model parallel size to use.
vp_size (Optional[int]): Virtual pipeline model parallel size to use.
If both pp_size and vp_size are None, both values fall back to parallel_state.
Otherwise, provided values are used as-is and None is treated as an explicit input.

"""
if pp_size is None and vp_size is None:
pp_size = parallel_state.get_pipeline_model_parallel_world_size()
Expand Down Expand Up @@ -524,6 +530,7 @@ def forward_backward_no_pipelining(
adjust_tensor_shapes_fn: Optional[Callable] = None, # unused
p2p_communicator: Optional[P2PCommunicator] = None, # unused
pg_collection: Optional[ProcessGroupCollection] = None,
force_all_reduce: Optional[bool] = False,
):
"""Run forward and backward passes with no pipeline parallelism"""

Expand Down Expand Up @@ -674,6 +681,7 @@ def forward_backward_no_pipelining(
[model],
total_num_tokens if config.calculate_per_token_loss else None,
pg_collection=pg_collection,
force_all_reduce=force_all_reduce,
)

if not forward_only and config.fine_grained_activation_offloading:
Expand Down Expand Up @@ -832,6 +840,7 @@ def forward_backward_pipelining_with_interleaving(
adjust_tensor_shapes_fn: Optional[Callable] = None, # unused
p2p_communicator: Optional[P2PCommunicator] = None,
pg_collection: Optional[ProcessGroupCollection] = None,
force_all_reduce: Optional[bool] = False,
):
"""Run interleaved 1F1B schedule (model split into model chunks), with
communication between pipeline stages as needed.
Expand Down Expand Up @@ -1911,6 +1920,7 @@ def pp_post_backward(input_tensor_grad, vp_stage=None):
model,
total_num_tokens if config.calculate_per_token_loss else None,
pg_collection=pg_collection,
force_all_reduce=force_all_reduce,
)

if not forward_only and config.fine_grained_activation_offloading:
Expand Down Expand Up @@ -1974,6 +1984,7 @@ def forward_backward_pipelining_without_interleaving(
adjust_tensor_shapes_fn: Optional[Callable] = None,
p2p_communicator: Optional[P2PCommunicator] = None,
pg_collection: Optional[ProcessGroupCollection] = None,
force_all_reduce: Optional[bool] = False,
):
"""Run non-interleaved 1F1B schedule, with communication between pipeline
stages. Returns dictionary with losses if the last stage, empty dict otherwise."""
Expand Down Expand Up @@ -2301,6 +2312,7 @@ def enable_grad_sync():
[model],
total_num_tokens if config.calculate_per_token_loss else None,
pg_collection=pg_collection,
force_all_reduce=force_all_reduce,
)

if not forward_only and config.fine_grained_activation_offloading:
Expand Down
4 changes: 4 additions & 0 deletions megatron/training/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -2597,6 +2597,10 @@ def _add_checkpointing_args(parser):
help='Output directory to save checkpoints to.')
group.add_argument('--save-interval', '--persistent-save-interval', type=int, default=None,
help='Number of iterations between persistent checkpoint saves.')
group.add_argument('--save-wgrads-interval', type=int, default=None,
help='Number of iterations between wgrad (main_grad) saves.')
group.add_argument('--save-dgrads-interval', type=int, default=None,
help='Number of iterations between dgrad saves.')
group.add_argument('--save-retain-interval', type=int, default=None,
help='Number of iterations between retained checkpoints (other'
'checkpoints _except the last checkpoint_ are automatically deleted).')
Expand Down
36 changes: 36 additions & 0 deletions megatron/training/checkpointing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import threading
import types
from argparse import Namespace
from datetime import datetime
from enum import Enum, auto
from logging import getLogger
from pathlib import Path
Expand Down Expand Up @@ -432,6 +433,41 @@ def _build_sharded_state_dict_metadata(args: Namespace, dp_cp_group: Optional[to
metadata['dp_cp_group'] = dp_cp_group
return metadata


def save_grads(save_dir, state_dict, iteration, grad_label):
"""Persist state_dict of grads onto disk. In case of wgrads, this collection should
be performed before the grads are cleared but after they are reduced.

NOTE: wgrads for non-expert layers will be duplicated if using expert parallelism, but
this can be handled in postprocessing."""

print_rank_0(f" [{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}] saving {grad_label} "
f"from iteration {iteration:7d}")

if mpu.get_expert_data_parallel_rank() == 0:
# Create saving directory.
ep_rank = mpu.get_expert_model_parallel_rank()
pp_rank = mpu.get_pipeline_model_parallel_rank()
tp_rank = mpu.get_tensor_model_parallel_rank()
assert save_dir is not None
assert iteration is not None
save_dir = os.path.join(save_dir, grad_label, f"iter_{iteration:07d}")
os.makedirs(save_dir, exist_ok=True)

# Save state_dict.
checkpoint_name = f"mp_rank_{tp_rank:02d}"
if mpu.get_pipeline_model_parallel_world_size() > 1:
checkpoint_name += f"_{pp_rank:03d}"
if mpu.get_expert_model_parallel_world_size() > 1:
checkpoint_name += f"_{ep_rank:03d}"
full_save_path = os.path.join(save_dir, f"{checkpoint_name}.pth")
# Convert back to dict (e.g., from collections.defaultdict) for easy loading later.
torch.save(dict(state_dict), full_save_path)

print_rank_0(f" [{datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')}] saved {grad_label} "
f"from iteration {iteration:7d}")


def save_checkpoint(iteration, model, optimizer, opt_param_scheduler, num_floating_point_operations_so_far,
checkpointing_context=None, pipeline_rank=None, expert_rank=None, tensor_rank=None, pipeline_parallel=None, expert_parallel=None, non_persistent_ckpt=False,
train_data_iterator=None, preprocess_common_state_dict_fn = None, release=False, tp_group: Optional[torch.distributed.ProcessGroup] = None, pp_group: Optional[torch.distributed.ProcessGroup] = None, dp_cp_group: Optional[torch.distributed.ProcessGroup] = None):
Expand Down
123 changes: 123 additions & 0 deletions megatron/training/dgrad_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.

"""dgrad logging using backward hooks."""

from collections import defaultdict
import torch
import torch.nn as nn

from megatron.core.tensor_parallel.layers import ColumnParallelLinear, RowParallelLinear

from .checkpointing import save_grads
from .utils import unwrap_model


def _get_linear_types():
"""Build tuple of linear layer types to capture gradients from."""
types = [nn.Linear, nn.Embedding, ColumnParallelLinear, RowParallelLinear]

# Add Transformer Engine layers if available.
try:
from megatron.core.extensions.transformer_engine import (
TELinear,
TEColumnParallelLinear,
TERowParallelLinear,
TELayerNormColumnParallelLinear,
)
types.extend([TELinear, TEColumnParallelLinear, TERowParallelLinear,
TELayerNormColumnParallelLinear])
except ImportError:
pass

try:
from megatron.core.extensions.transformer_engine import (
TEGroupedLinear,
TEColumnParallelGroupedLinear,
TERowParallelGroupedLinear,
)
if TEGroupedLinear is not None:
types.extend([TEGroupedLinear, TEColumnParallelGroupedLinear,
TERowParallelGroupedLinear])
except ImportError:
pass

return tuple(types)


LINEAR_TYPES = _get_linear_types()


class DataGradLogger:
"""Captures and saves gradients from all linear layers using backward hooks.

NOTE: Right now, we only save the dgrads for the last microbatch in a batch on DP replica 0.
The code below would need to be extended to save dgrads for all microbatches in a batch."""

def __init__(self, save_dir: str):
self._save_dir = save_dir
self._dgrads_state_dict = defaultdict(dict)
self._hooks = []

def _make_hook(self, model_chunk_name: str, module_name: str):
"""Create a backward hook for a named module."""
def hook(_, grad_input, grad_output):
for idx, grad in enumerate(grad_output):
if grad is not None:
grad_name = f"{module_name}/output{idx}"
self._dgrads_state_dict[model_chunk_name][grad_name] = grad.detach().cpu()
for idx, grad in enumerate(grad_input):
if grad is not None:
grad_name = f"{module_name}/input{idx}"
self._dgrads_state_dict[model_chunk_name][grad_name] = grad.detach().cpu()
return hook

def save(self, iteration: int):
"""Save captured gradients to disk and clear the buffer."""
if not self._dgrads_state_dict:
return
save_grads(self._save_dir, self._dgrads_state_dict, iteration, "dgrads")
self._dgrads_state_dict.clear()

def register_hooks(self, model: torch.nn.Module):
"""Find and register hooks on all linear layers."""
assert len(self._hooks) == 0
for model_chunk_id, model_chunk in enumerate(model):
unwrapped_model_chunk = unwrap_model(model_chunk)
for module_name, module in unwrapped_model_chunk.named_modules():
if isinstance(module, LINEAR_TYPES):
model_chunk_name = f"model_chunk{model_chunk_id}"
handle = module.register_full_backward_hook(
self._make_hook(model_chunk_name, module_name)
)
self._hooks.append(handle)

def remove_hooks(self):
"""Remove all registered hooks."""
for handle in self._hooks:
handle.remove()
self._hooks.clear()


_LOGGER = None


def enable_dgrad_logging(model: torch.nn.Module, save_dir: str):
"""Enable dgrad logging on a model."""
global _LOGGER
if _LOGGER is None:
_LOGGER = DataGradLogger(save_dir)
_LOGGER.register_hooks(model)


def disable_dgrad_logging():
"""Disable dgrad logging on a model."""
global _LOGGER
assert _LOGGER is not None
_LOGGER.remove_hooks()


def save_dgrads(iteration: int):
"""Save dgrads to disk."""
global _LOGGER
assert _LOGGER is not None
_LOGGER.save(iteration)
Loading
Loading