Skip to content
40 changes: 24 additions & 16 deletions megatron/core/distributed/param_and_grad_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ 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)
# Track grad mode used to create cached param views. Rebuild if mode changes to avoid
# mixing no_grad-created views with in-place updates in grad-enabled mode.
self._cached_param_buffer_shards_grad_enabled = None

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.

What is the purpose of this?


def reset(self):
"""
Expand Down Expand Up @@ -403,23 +406,28 @@ 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:
self.cached_param_buffer_shard_list = [None] * len(self.buckets)
self._cached_param_buffer_shards_grad_enabled = current_grad_enabled
with torch.no_grad():

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.

Comment on what this is trying to do?

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
88 changes: 86 additions & 2 deletions megatron/core/extensions/transformer_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1787,6 +1787,50 @@ def __init__(
setattr(weight, "partition_dim", part_dim)
setattr(weight, "partition_stride", 1)

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."""

def maybe_remap_param(param_name: str) -> 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 getattr(self, "single_grouped_parameter", False):
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")
if self.use_bias:
maybe_remap_param("bias")

self._register_load_state_dict_pre_hook(
normalize_grouped_parameter_keys, with_module=True
)

def merge_extra_states(
self,
state_dict,
Expand Down Expand Up @@ -1877,6 +1921,31 @@ def merge_extra_states(

self._register_load_state_dict_pre_hook(merge_extra_states, with_module=True)

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 @@ -1981,18 +2050,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
112 changes: 102 additions & 10 deletions megatron/core/optimizer/distrib_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,10 @@ def _build_model_and_main_param_groups(
if model_param.type() in ['torch.cuda.HalfTensor', 'torch.cuda.BFloat16Tensor']:

# Generate sharded model param.
if is_float8tensor(model_param) and config.fp8_recipe != "delayed":
if (
cls._is_distopt_quantized_param(model_param)
and config.fp8_recipe != "delayed"
):
# MXFP8Tensor and BlockwiseQTensor don't support view(-1)
shard_model_param = None
else:
Expand All @@ -381,7 +384,7 @@ def _build_model_and_main_param_groups(
# precision at the beginning of training (this problem will not occur if the
# training is long enough or if the main params are loaded from a
# checkpoint).
if is_float8tensor(model_param):
if cls._is_distopt_quantized_param(model_param):
if hasattr(model_param, 'get_high_precision_init_val'):
shard_main_param = (
model_param.get_high_precision_init_val()
Expand Down Expand Up @@ -913,6 +916,70 @@ def _get_main_param_and_optimizer_states(self, model_param):
tensors[k] = v
return tensors

@staticmethod
def _is_grouped_quantized_tensor(tensor: torch.Tensor) -> bool:
"""Check if tensor is a TE GroupedTensor using quantized storage."""
return (
hasattr(tensor, "split_into_quantized_tensors")
and callable(tensor.split_into_quantized_tensors)
and getattr(tensor, "quantizer", None) is not None
)

@classmethod
def _is_distopt_quantized_param(cls, tensor: torch.Tensor) -> bool:
"""Check if tensor should follow quantized parameter path in dist optimizer."""
return is_float8tensor(tensor) or cls._is_grouped_quantized_tensor(tensor)

def _expand_quantized_param_shard_for_cast(
self,
model_param: torch.Tensor,
shard_main_param: Optional[torch.Tensor],
start_offset: Optional[int],
):
"""Expand one quantized model param to cast-ready entries.

For grouped quantized tensors, split into member quantized tensors and map the sharded
master slice to per-member offset ranges, while preserving deterministic ordering across
DP ranks.
"""
if not self._is_grouped_quantized_tensor(model_param):
return [model_param], [shard_main_param], [start_offset]

quantized_members = model_param.quantized_tensors
if quantized_members is None:
quantized_members = model_param.split_into_quantized_tensors()

shard_start = 0 if start_offset is None else start_offset
shard_size = 0 if shard_main_param is None else shard_main_param.numel()
shard_end = shard_start + shard_size
shard_flat = None if shard_main_param is None else shard_main_param.view(-1)

expanded_model_params = []
expanded_shard_main_params = []
expanded_start_offsets = []
member_offset = 0
for member in quantized_members:
member_numel = member.numel()
member_start = member_offset
member_end = member_start + member_numel
overlap_start = max(member_start, shard_start)
overlap_end = min(member_end, shard_end)

member_master = None
member_start_offset = None
if overlap_start < overlap_end:
local_start = overlap_start - shard_start
local_end = overlap_end - shard_start
member_master = shard_flat[local_start:local_end]
member_start_offset = overlap_start - member_start

expanded_model_params.append(member)
expanded_shard_main_params.append(member_master)
expanded_start_offsets.append(member_start_offset)
member_offset = member_end

return expanded_model_params, expanded_shard_main_params, expanded_start_offsets

def _set_main_param_and_optimizer_states(self, model_param, tensors):
"""Set the main param and optimizer states corresponding to the input model_param.

Expand Down Expand Up @@ -2145,7 +2212,7 @@ def split_state_dict_if_needed(self, state_dict):
fp8_gbuf_indices = []
for gbuf_idx, gbuf_range_maps in enumerate(self.gbuf_ranges):
for dtype, _ in gbuf_range_maps.items():
if is_float8tensor(self.buffers[gbuf_idx].params[0]):
if self._is_distopt_quantized_param(self.buffers[gbuf_idx].params[0]):
fp8_gbuf_indices.append(gbuf_idx)
if len(fp8_gbuf_indices) == 0:
return
Expand All @@ -2167,7 +2234,7 @@ def split_state_dict_if_needed(self, state_dict):
new_state_dict = {'buckets_coalesced': state_dict['buckets_coalesced']}
for gbuf_idx, gbuf_range_maps in enumerate(self.gbuf_ranges):
for dtype, _ in gbuf_range_maps.items():
if not is_float8tensor(self.buffers[gbuf_idx].params[0]):
if not self._is_distopt_quantized_param(self.buffers[gbuf_idx].params[0]):
new_state_dict[gbuf_idx] = state_dict[dtype_to_gbuf_idx[dtype]]

for fp8_gbuf_idx in fp8_gbuf_indices:
Expand Down Expand Up @@ -2367,7 +2434,7 @@ def _get_fp8_params_and_shard_fp32_from_fp8(self):
idx = 0
for buffer in buffers:
for param in buffer.params:
if is_float8tensor(param):
if self._is_distopt_quantized_param(param):
fp8_params.append(param)
shard_fp32_from_fp8.append(None)
shard_offsets_in_fp8.append(None)
Expand All @@ -2382,7 +2449,7 @@ def get_shard_fp32_from_fp8(shard_main_groups, model_groups):
"""
for shard_main_group, model_group in zip(shard_main_groups, model_groups):
for shard_main_param, model_param in zip(shard_main_group, model_group):
if is_float8tensor(model_param):
if self._is_distopt_quantized_param(model_param):
param_range_map = self._get_model_param_range_map(model_param)
param_range = param_range_map["param"]
assert param_range.size == shard_main_param.nelement()
Expand Down Expand Up @@ -2459,8 +2526,29 @@ def _copy_main_params_to_model_params(self):
if self.config.use_precision_aware_optimizer_no_fp8_or_ds_fp8:
return

fp8_params, shard_fp32_from_fp8, shard_offsets_in_fp8 = (

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.

Comment on what this is trying to do?

self._get_fp8_params_and_shard_fp32_from_fp8()
)
expanded_fp8_params = []
expanded_shard_fp32_from_fp8 = []
expanded_shard_offsets_in_fp8 = []
for model_param, shard_main_param, start_offset in zip(
fp8_params, shard_fp32_from_fp8, shard_offsets_in_fp8
):
sub_model_params, sub_shard_main_params, sub_start_offsets = (
self._expand_quantized_param_shard_for_cast(
model_param, shard_main_param, start_offset
)
)
expanded_fp8_params.extend(sub_model_params)
expanded_shard_fp32_from_fp8.extend(sub_shard_main_params)
expanded_shard_offsets_in_fp8.extend(sub_start_offsets)

quantize_param_shard(
*self._get_fp8_params_and_shard_fp32_from_fp8(), self.data_parallel_group
expanded_fp8_params,
expanded_shard_fp32_from_fp8,
expanded_shard_offsets_in_fp8,
self.data_parallel_group,
)

# Utility method for copying group params.
Expand All @@ -2480,7 +2568,7 @@ def copy_group_params(shard_main_groups, model_groups):
world_range.start : world_range.end
]

if is_float8tensor(model_param):
if self._is_distopt_quantized_param(model_param):
# FP8 params are quantized in the above "quantize_param_shard" function.
continue
else:
Expand Down Expand Up @@ -2592,8 +2680,12 @@ def copy_group_params(model_groups, shard_main_groups):
# Use param from state_dict to initialize main_param
model_param = model_param_to_state_dict_param_map[model_param]

if is_float8tensor(model_param):
shard_model_param = dequantize_fp8_tensor(model_param).view(-1)[
if self._is_distopt_quantized_param(model_param):
if self._is_grouped_quantized_tensor(model_param):
dequantized_model_param = model_param.float()
else:
dequantized_model_param = dequantize_fp8_tensor(model_param)
shard_model_param = dequantized_model_param.view(-1)[
param_range.start : param_range.end
]
else:
Expand Down
Loading
Loading