1511 lines
63 KiB
Python
1511 lines
63 KiB
Python
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
MuonShardingOptimizer (Sharding Stage1 V3): Hybrid Tensor-wise + Element-wise Sharding
|
|
==================================================================
|
|
|
|
Designed for Muon optimizer compatibility:
|
|
- 2D (Muon) parameters: assigned as *whole tensors* to ranks (like V1).
|
|
This avoids the expensive sharding gather in Muon's _muon_update.
|
|
- Non-2D (AdamW) parameters: split element-wise via reduce-scatter (like V2).
|
|
This provides memory balancing across ranks.
|
|
|
|
The key insight is that Muon requires the full 2D matrix for Newton-Schulz
|
|
orthogonalisation, so keeping 2D params whole on each rank eliminates the
|
|
need for gather_varlen communication during the optimizer step.
|
|
|
|
Parameters are grouped by their `color` attribute, which specifies the
|
|
communication group to use:
|
|
- color=None or -1: default sharding_group
|
|
- color={'color': <key>, 'group': <group>}: custom group read directly
|
|
from the param, no code changes needed to add new color groups.
|
|
"""
|
|
|
|
import math
|
|
import os
|
|
import warnings
|
|
from collections import defaultdict
|
|
from functools import reduce as functools_reduce
|
|
|
|
import numpy as np
|
|
|
|
import paddle
|
|
from paddle import framework
|
|
from paddle.base.framework import EagerParamBase
|
|
from paddle.distributed import fleet
|
|
from paddle.distributed.communication.batch_isend_irecv import (
|
|
_coalescing_manager,
|
|
)
|
|
from paddle.distributed.communication.reduce import (
|
|
ReduceOp,
|
|
is_avg_reduce_op_supported,
|
|
)
|
|
from paddle.distributed.fleet.utils import timer_helper as timer
|
|
from paddle.distributed.fleet.utils.log_util import logger
|
|
from paddle.distributed.fleet.utils.tensor_fusion_helper import (
|
|
HOOK_ACTION,
|
|
FusedCommBuffer,
|
|
assign_group_by_size,
|
|
)
|
|
|
|
g_shard_bypass_dygraph_optimizer = int(
|
|
os.environ.get("FLAGS_shard_bypass_dygraph_optimizer", 0)
|
|
)
|
|
|
|
|
|
def _is_trainable(param):
|
|
return not param.stop_gradient
|
|
|
|
|
|
def get_same_card_rank(moe_sharding_group_ranks, rank):
|
|
"""
|
|
Get the MoE sharding group rank within the same trainer as the specified rank.
|
|
|
|
Args:
|
|
moe_sharding_group_ranks (set): Set of ranks in the MoE sharding group
|
|
rank (int): Current rank
|
|
|
|
Returns:
|
|
int: The rank within the same trainer that belongs to the MoE sharding group;
|
|
returns -1 if not found
|
|
"""
|
|
trainer = rank // 8
|
|
for i in range(trainer * 8, (trainer + 1) * 8):
|
|
if i in moe_sharding_group_ranks:
|
|
return i
|
|
return -1
|
|
|
|
|
|
def get_trainer_ranks(rank):
|
|
"""
|
|
Get the trainer ID and all ranks belonging to that trainer.
|
|
|
|
In distributed training, every 8 GPUs form a "trainer". This function computes:
|
|
1. The trainer number for the given rank.
|
|
2. The range of all 8 ranks within that same trainer.
|
|
|
|
Args:
|
|
rank (int): The global rank ID of the current process
|
|
|
|
Returns:
|
|
tuple:
|
|
- train_id (int): The trainer index (0, 1, 2...)
|
|
- Ranks iterable: All 8 ranks in this trainer,
|
|
e.g., if rank=10 returns (1, range(8, 16))
|
|
|
|
Example:
|
|
>>> get_trainer_ranks(5)
|
|
(0, range(0, 8))
|
|
>>> get_trainer_ranks(12)
|
|
(1, range(8, 16))
|
|
"""
|
|
trainer = rank // 8
|
|
return trainer, range(trainer * 8, (trainer + 1) * 8)
|
|
|
|
|
|
class MuonShardingOptimizer:
|
|
"""
|
|
Hybrid sharding optimizer for Muon:
|
|
- 2D (Muon) parameters: tensor-wise assignment to ranks (no cross-rank split).
|
|
Gradient communication uses reduce; parameter sync uses broadcast.
|
|
- Non-2D (AdamW) parameters: element-wise split across ranks (like V2).
|
|
Gradient communication uses reduce-scatter; parameter sync uses all-gather.
|
|
|
|
Parameters are grouped by `color` attribute to determine the communication
|
|
group. Each color group has its own 2D parameter partition and communication.
|
|
|
|
This avoids the expensive gather_varlen in Muon's _muon_update while
|
|
maintaining memory balance across ranks.
|
|
"""
|
|
|
|
def __init__(self, optimizer, hcg=None):
|
|
logger.info("init MuonShardingOptimizer")
|
|
if isinstance(optimizer._parameter_list[0], dict):
|
|
raise TypeError(
|
|
"Do not support param_groups now, please set optimizer._parameter_list as a list of Parameter"
|
|
)
|
|
if not hasattr(optimizer, '_apply_optimize') or not callable(
|
|
optimizer._apply_optimize
|
|
):
|
|
raise ValueError(
|
|
"the optimizer object should have _apply_optimize function"
|
|
)
|
|
|
|
self._inner_opt = optimizer
|
|
# Get hcg from fleet if not provided
|
|
if hcg is None:
|
|
hcg = fleet.fleet._hcg
|
|
self._hcg = hcg
|
|
self._sharding_world_size = self._hcg.get_sharding_parallel_world_size()
|
|
self._sharding_rank = self._hcg.get_sharding_parallel_rank()
|
|
self._global_rank = paddle.distributed.get_rank()
|
|
|
|
# Temporarily: TP is not supported in MuonShardingOptimizer
|
|
_tp_world_size = self._hcg.get_model_parallel_world_size()
|
|
assert _tp_world_size == 1, (
|
|
f"MuonShardingOptimizer does not support tensor parallelism yet. "
|
|
f"Got tp_world_size={_tp_world_size}. Please set tensor_parallel_degree=1."
|
|
)
|
|
|
|
strategy = fleet.fleet._user_defined_strategy
|
|
sharding_configs = strategy.hybrid_configs['sharding_configs']
|
|
|
|
self.tensor_fusion = sharding_configs.tensor_fusion
|
|
self.accumulate_steps = sharding_configs.accumulate_steps
|
|
self.comm_overlap = sharding_configs.comm_overlap
|
|
self.comm_buffer_size_MB = sharding_configs.comm_buffer_size_MB
|
|
self.use_reduce_avg = sharding_configs.use_reduce_avg
|
|
self.enable_fuse_optimizer_states = (
|
|
sharding_configs.enable_fuse_optimizer_states
|
|
)
|
|
|
|
self.use_group_call_opt = sharding_configs.comm_group_call_opt
|
|
hcg = fleet.get_hybrid_communicate_group()
|
|
ep_degree = (
|
|
hcg.get_expert_parallel_world_size()
|
|
if hasattr(hcg, "get_expert_parallel_world_size")
|
|
else 1
|
|
)
|
|
moe_sharding_degree = hcg.get_moe_sharding_parallel_world_size()
|
|
if self.use_group_call_opt:
|
|
assert ep_degree == 8 and moe_sharding_degree != 1, (
|
|
"comm_group_call_opt should be enabled when ep_degree is 8 and moe_sharding_degree is not 1"
|
|
)
|
|
|
|
if self.enable_fuse_optimizer_states:
|
|
self._inner_opt.use_fusion_storage()
|
|
|
|
if self.use_reduce_avg and (not is_avg_reduce_op_supported()):
|
|
self.use_reduce_avg = False
|
|
warnings.warn(
|
|
"nccl reduce_avg requires paddle compiled with cuda and nccl>=2.10.0, "
|
|
"please check compilation setups."
|
|
)
|
|
|
|
pp_overlap = strategy.hybrid_configs['pp_configs'].sharding_comm_overlap
|
|
self.pp_overlap = pp_overlap
|
|
assert not self.pp_overlap, (
|
|
"muon_sharding_optimizer do not support PP overlap"
|
|
)
|
|
|
|
self._use_main_grad = hasattr(optimizer._parameter_list[0], "main_grad")
|
|
|
|
# The full original parameter list
|
|
self._parameter_list = list(optimizer._parameter_list)
|
|
self._origin_parameter_list = list(optimizer._parameter_list)
|
|
|
|
# Build color -> group_info mapping dynamically from param.color attributes
|
|
sharding_group = hcg.get_sharding_parallel_group()
|
|
self._color_to_group_info = self._build_color_to_group_info_from_params(
|
|
self._parameter_list, sharding_group
|
|
)
|
|
|
|
# Get muon_param_info_map from the inner Muon optimizer.
|
|
# Each entry has use_muon=True/False, set by the Trainer before construction.
|
|
self._muon_param_info_map = getattr(
|
|
optimizer, '_muon_param_info_map', {}
|
|
)
|
|
|
|
# ---- Step 1: Separate params into 2D (Muon) and 1D (AdamW) by color ----
|
|
# Parameters are grouped by their `color` attribute:
|
|
# - color=None or -1: default sharding_group (key: None)
|
|
# - color={'color': <key>, 'group': <group>}: custom comm group
|
|
#
|
|
# For each color group:
|
|
# - 2D (Muon) params: whole tensor, assigned to ranks via tensor-wise partition
|
|
# - non-2D (AdamW) params: element-wise split via FusedCommBuffer
|
|
self._params_2d_by_color = defaultdict(
|
|
list
|
|
) # color_key -> list of 2D params
|
|
self._params_1d = [] # all non-2D params
|
|
self.clear_color = set()
|
|
self._color_to_comm_buffer_list = {}
|
|
for p in self._parameter_list:
|
|
if not _is_trainable(p):
|
|
continue
|
|
|
|
# Extract color value
|
|
color = getattr(p, 'color', -1)
|
|
if isinstance(color, dict):
|
|
color_val = color.get('color', -1)
|
|
else:
|
|
color_val = color
|
|
|
|
# Normalize color: treat None/-1 as default (None key)
|
|
if color_val == -1 or color_val is None:
|
|
color_key = None
|
|
else:
|
|
color_key = color_val
|
|
|
|
param_info = self._muon_param_info_map.get(p.name)
|
|
assert param_info is not None, (
|
|
f"Parameter {p.name!r} (shape={list(p.shape)}) has no muon_param_info. "
|
|
f"Trainer._build_muon_param_info_map must set muon_param_info on all "
|
|
f"trainable parameters before MuonShardingOptimizer is constructed."
|
|
)
|
|
use_muon = param_info.use_muon
|
|
|
|
if use_muon:
|
|
self._params_2d_by_color[color_key].append(p)
|
|
else:
|
|
# Non-2D params use element-wise split via FusedCommBuffer
|
|
self._params_1d.append(p)
|
|
|
|
# ---- Step 2: Partition 2D params for each color group ----
|
|
# For each color, compute rank-to-params and param-to-rank mappings
|
|
self._rank2params_2d_by_color = {} # color -> {rank -> [params]}
|
|
self._param2rank_2d_by_color = {} # color -> {param_name -> rank}
|
|
|
|
for color_key, params_2d in self._params_2d_by_color.items():
|
|
group_info = self._color_to_group_info.get(color_key, {})
|
|
world_size = group_info.get('world_size', 1)
|
|
|
|
if world_size <= 1:
|
|
# No partition needed, all params stay on rank 0
|
|
self._rank2params_2d_by_color[color_key] = {0: list(params_2d)}
|
|
self._param2rank_2d_by_color[color_key] = {
|
|
p.name: 0 for p in params_2d
|
|
}
|
|
else:
|
|
# Greedy partition across ranks
|
|
label = color_key if color_key else "default"
|
|
self._rank2params_2d_by_color[color_key] = (
|
|
self._partition_2d_parameters(
|
|
list(params_2d), world_size, label=label
|
|
)
|
|
)
|
|
self._param2rank_2d_by_color[color_key] = {}
|
|
for rank, params in self._rank2params_2d_by_color[
|
|
color_key
|
|
].items():
|
|
for p in params:
|
|
self._param2rank_2d_by_color[color_key][p.name] = rank
|
|
|
|
# Sort params within each color by owner rank for deterministic ordering
|
|
for color_key, params_2d in self._params_2d_by_color.items():
|
|
params_2d.sort(
|
|
key=lambda p: self._param2rank_2d_by_color[color_key][p.name]
|
|
)
|
|
|
|
# 2D params owned by this sharding rank
|
|
self._local_2d = []
|
|
for color_key, params_2d in self._params_2d_by_color.items():
|
|
rank2params_2d_by_color = self._rank2params_2d_by_color[color_key]
|
|
|
|
group_info = self._color_to_group_info[color_key]
|
|
sharding_rank = max(group_info['rank'], 0)
|
|
|
|
self._local_2d.extend(rank2params_2d_by_color[sharding_rank])
|
|
|
|
self.sd_release_grads = (
|
|
strategy.hybrid_configs['pp_configs'].release_gradients
|
|
or sharding_configs.release_gradients
|
|
)
|
|
self._use_fuse_gradients = self.comm_buffer_size_MB > 0
|
|
# ---- Build comm buffers for 2D params (V1-style) ----
|
|
if self._use_fuse_gradients:
|
|
self.comm_buffer_2d = self._build_2d_comm_buffers()
|
|
self.comm_buffer_2d.sort(key=lambda x: (x._comm_group._id, x._dst))
|
|
|
|
# ---- Step 3: Build comm buffers for 1D params (V2-style) ----
|
|
self._slice_params = {}
|
|
self._comm_buffer_list = []
|
|
self._local_parameter_list_1d = [
|
|
self._create_slice_param(p) for p in self._params_1d
|
|
]
|
|
|
|
self.param2bucket = {}
|
|
|
|
self._build_1d_comm_buffers()
|
|
|
|
# ---- Step 4: Build the optimizer's parameter list ----
|
|
# The optimizer should see:
|
|
# - All 2D params assigned to this rank (all colors, as whole tensors)
|
|
# - 1D slice_params for all non-2D params (element-wise shards)
|
|
local_opt_params = list(self._local_2d) + list(
|
|
self._local_parameter_list_1d
|
|
)
|
|
|
|
self._set_inner_opt_attr('_parameter_list', local_opt_params)
|
|
self._set_inner_opt_attr('_param_groups', local_opt_params)
|
|
|
|
# For external iteration (clear_grad, etc.), expose all params
|
|
self._local_parameter_list = local_opt_params
|
|
|
|
self._enable_timer = strategy.hybrid_configs.get(
|
|
"enable_optimizer_timer", False
|
|
)
|
|
if self._enable_timer:
|
|
if not timer.is_timer_initialized():
|
|
timer.set_timers()
|
|
self.timers = timer.get_timers()
|
|
|
|
# --- Per-rank parameter size summary (for load balancing diagnostics) ---
|
|
_sg_group = hcg.get_sharding_parallel_group()
|
|
_N = self._sharding_world_size
|
|
|
|
# 2D params owned by this sharding rank
|
|
_local_2d_numel = sum(
|
|
int(functools_reduce(lambda x, y: x * y, p.shape, 1))
|
|
for p in self._local_2d
|
|
)
|
|
# 1D (AdamW) slice: each rank holds ceil(numel / sharding_world_size) elements.
|
|
_local_1d_numel = sum(
|
|
math.ceil(
|
|
int(functools_reduce(lambda x, y: x * y, p.shape, 1)) / _N
|
|
)
|
|
for p in self._params_1d
|
|
)
|
|
|
|
_local_total_numel = _local_2d_numel + _local_1d_numel
|
|
_local_total_MB = (
|
|
_local_total_numel * 2 / (1024 * 1024)
|
|
) # bf16/fp16 = 2 bytes
|
|
|
|
# All-gather total numel from all sharding ranks in this PP stage
|
|
_local_numel_tensor = paddle.to_tensor(
|
|
[_local_total_numel], dtype='int64'
|
|
)
|
|
_all_numel_list = []
|
|
paddle.distributed.all_gather(
|
|
_all_numel_list, _local_numel_tensor, group=_sg_group
|
|
)
|
|
_all_numel = [int(t.item()) for t in _all_numel_list]
|
|
_all_MB = [n * 2 / (1024 * 1024) for n in _all_numel]
|
|
|
|
_max_MB = max(_all_MB)
|
|
_min_MB = min(_all_MB)
|
|
_imbalance = (_max_MB - _min_MB) / _max_MB if _max_MB > 0 else 0.0
|
|
|
|
if self._sharding_rank == 0:
|
|
logger.info(
|
|
f"[MuonSharding global_rank={self._global_rank} sharding_rank={self._sharding_rank}] "
|
|
f"SliceSize sharding_group ranks={_sg_group.ranks} | "
|
|
f"per-rank MB: {[f'{mb:.1f}' for mb in _all_MB]} | "
|
|
f"max memory diff={_imbalance * 100:.2f}%"
|
|
)
|
|
if self.use_group_call_opt:
|
|
self.trainer_comms = {}
|
|
world_size = paddle.distributed.get_world_size()
|
|
num_trainers = world_size // 8
|
|
for i in range(num_trainers):
|
|
ranks = range(i * 8, (i + 1) * 8)
|
|
group = paddle.distributed.new_group(ranks)
|
|
self.trainer_comms[i] = group
|
|
|
|
# ------------------------------------------------------------------
|
|
# 2D partition (V1-style greedy)
|
|
# ------------------------------------------------------------------
|
|
|
|
@staticmethod
|
|
def _build_color_to_group_info_from_params(parameter_list, default_group):
|
|
"""Build color->group_info mapping dynamically from param.color attributes.
|
|
|
|
When param.color is a dict containing 'group', the comm group is read
|
|
directly from the param — no hcg-specific method registration required.
|
|
|
|
Returns:
|
|
dict: {
|
|
None: {'group': default_group, 'world_size': N, 'rank': r},
|
|
'<key>': {'group': group, 'world_size': M, 'rank': s},
|
|
# additional entries auto-populated from param.color dicts
|
|
}
|
|
"""
|
|
color_to_info = {
|
|
None: {
|
|
'group': default_group,
|
|
'world_size': len(default_group.ranks) if default_group else 1,
|
|
'rank': default_group.rank if default_group else 0,
|
|
}
|
|
}
|
|
for p in parameter_list:
|
|
color = getattr(p, 'color', -1)
|
|
if isinstance(color, dict):
|
|
color_key = color.get('color', -1)
|
|
if (
|
|
color_key not in (-1, None)
|
|
and color_key not in color_to_info
|
|
):
|
|
group = color.get('group', default_group)
|
|
color_to_info[color_key] = {
|
|
'group': group,
|
|
'world_size': len(group.ranks) if group else 1,
|
|
'rank': group.rank if group else 0,
|
|
}
|
|
return color_to_info
|
|
|
|
def _partition_2d_parameters(self, params, world_size, label=""):
|
|
"""Partition 2D parameters among ranks using greedy bin-packing.
|
|
|
|
Only assign parameters to the first n ranks such that total size > comm_buffer_size_MB.
|
|
Remaining ranks get no parameters.
|
|
"""
|
|
mapping = {}
|
|
for rank in range(world_size):
|
|
mapping[rank] = []
|
|
|
|
parameters = list(params)
|
|
parameters.sort(
|
|
key=lambda p: functools_reduce(lambda x, y: x * y, p.shape),
|
|
reverse=True,
|
|
)
|
|
|
|
total_numel = sum(
|
|
functools_reduce(lambda x, y: x * y, p.shape, 1) for p in parameters
|
|
)
|
|
total_size_bytes = total_numel * 4
|
|
total_size_mb = total_size_bytes / (1024**2)
|
|
|
|
buffer_size_mb = (
|
|
self.comm_buffer_size_MB if self.comm_buffer_size_MB > 0 else 256
|
|
)
|
|
min_active_ranks = 1
|
|
if total_size_mb > 0:
|
|
min_active_ranks = max(1, int(total_size_mb / buffer_size_mb) + 1)
|
|
|
|
active_ranks = min(min_active_ranks, world_size)
|
|
sizes = [0] * active_ranks
|
|
|
|
for param in parameters:
|
|
rank = sizes.index(min(sizes))
|
|
mapping[rank].append(param)
|
|
numel = functools_reduce(lambda x, y: x * y, param.shape, 1)
|
|
sizes[rank] += numel
|
|
|
|
return mapping
|
|
|
|
def _build_2d_comm_buffers(self):
|
|
"""Build communication buffers for 2D (Tensor-wise) parameters using all-reduce."""
|
|
group_size = (
|
|
self.comm_buffer_size_MB * 1024 * 1024
|
|
if self.comm_buffer_size_MB > 0
|
|
else 256 * 1024 * 1024
|
|
)
|
|
comm_buffers = []
|
|
|
|
for color_key, params_2d in self._params_2d_by_color.items():
|
|
group_info = self._color_to_group_info.get(color_key, {})
|
|
comm_group = group_info.get('group', None)
|
|
|
|
fused_parameter_group = defaultdict(list)
|
|
|
|
for p in params_2d:
|
|
dst_rank = self._param2rank_2d_by_color[color_key][p.name]
|
|
fused_parameter_group[dst_rank].append(p)
|
|
|
|
absolute_dst_ranks = {
|
|
rank: comm_group.ranks[rank] for rank in fused_parameter_group
|
|
}
|
|
|
|
for dst, params in fused_parameter_group.items():
|
|
var_groups = assign_group_by_size(params, group_size)
|
|
abs_dst = absolute_dst_ranks[dst]
|
|
|
|
buffer = [
|
|
FusedCommBuffer(
|
|
group_idx,
|
|
parameters,
|
|
comm_group,
|
|
self.accumulate_steps,
|
|
act=HOOK_ACTION.REDUCE,
|
|
dst=abs_dst,
|
|
release_grads=self.sd_release_grads,
|
|
use_reduce_avg=True,
|
|
)
|
|
for group_idx, parameters in var_groups.items()
|
|
]
|
|
comm_buffers.extend(buffer)
|
|
|
|
return comm_buffers
|
|
|
|
# ------------------------------------------------------------------
|
|
# 1D slice creation (V2-style)
|
|
# ------------------------------------------------------------------
|
|
|
|
def _create_slice_param(self, param):
|
|
"""Create a placeholder slice parameter for 1D (element-wise) sharding."""
|
|
slice_param = EagerParamBase(shape=[1], dtype=param.dtype)
|
|
slice_param.name = param.name
|
|
|
|
def copy_attr(attr_name):
|
|
if hasattr(param, attr_name):
|
|
setattr(slice_param, attr_name, getattr(param, attr_name))
|
|
|
|
copy_attr("is_distributed")
|
|
copy_attr("optimize_attr")
|
|
copy_attr("do_model_average")
|
|
copy_attr("need_clip")
|
|
copy_attr("no_sync")
|
|
copy_attr("is_firstly_shared")
|
|
|
|
self._slice_params[param.name] = slice_param
|
|
return slice_param
|
|
|
|
def _build_1d_comm_buffers(self):
|
|
"""Build communication buffers for 1D (AdamW) parameters using reduce-scatter."""
|
|
if self.pp_overlap:
|
|
return
|
|
|
|
comm_group = self._hcg.get_sharding_parallel_group()
|
|
group_size = (
|
|
self.comm_buffer_size_MB * 1024 * 1024
|
|
if self.comm_buffer_size_MB > 0
|
|
else 256 * 1024 * 1024
|
|
)
|
|
|
|
# Group 1D params by (color, comm_group) so each group uses its own FusedCommBuffer
|
|
color_dict = defaultdict(list)
|
|
for param in self._params_1d:
|
|
color = getattr(param, 'color', -1)
|
|
color_group = comm_group
|
|
if isinstance(color, dict):
|
|
color_color = color.get('color', -1)
|
|
color_group = color.get('group', comm_group)
|
|
else:
|
|
color_color = color
|
|
color_dict[(color_color, color_group)].append(param)
|
|
|
|
if not self.comm_overlap:
|
|
for color, params in color_dict.items():
|
|
params.sort(key=lambda x: str(x.dtype))
|
|
|
|
group_idx = 0
|
|
for color, params in color_dict.items():
|
|
g_color = color[0]
|
|
g_group = color[1]
|
|
var_groups = assign_group_by_size(params, group_size)
|
|
for _, parameters in var_groups.items():
|
|
buffer = FusedCommBuffer(
|
|
group_idx,
|
|
parameters,
|
|
g_group,
|
|
self.accumulate_steps,
|
|
act=HOOK_ACTION.REDUCE_SCATTER,
|
|
release_grads=self.sd_release_grads,
|
|
use_reduce_avg=self.use_reduce_avg,
|
|
free_grads_in_comm=False,
|
|
init_slice_param=False,
|
|
slice_params=self._slice_params,
|
|
)
|
|
group_idx += 1
|
|
self._comm_buffer_list.append(buffer)
|
|
if g_color not in self._color_to_comm_buffer_list.keys():
|
|
self._color_to_comm_buffer_list[g_color] = []
|
|
self._color_to_comm_buffer_list[g_color].append(buffer)
|
|
for p in parameters:
|
|
if p.name in self.param2bucket:
|
|
self.param2bucket[p.name].append(buffer)
|
|
else:
|
|
self.param2bucket[p.name] = [buffer]
|
|
|
|
self._comm_buffer_list.sort(key=lambda x: x._dst)
|
|
|
|
def clear_param_storage(self, color):
|
|
assert self._multi_precision, (
|
|
"Muon Sharding Optimizer only support clear param with multi_precision mode"
|
|
)
|
|
|
|
self.clear_color.add(color)
|
|
# 1D params
|
|
if color in self._color_to_comm_buffer_list.keys():
|
|
for comm_buffer in self._color_to_comm_buffer_list[color]:
|
|
has_clear = False
|
|
for param in comm_buffer.params:
|
|
grad_view = comm_buffer._sharding_param_grad_view[
|
|
param.name
|
|
]
|
|
slice_param = self._slice_params[param.name]
|
|
if (
|
|
not g_shard_bypass_dygraph_optimizer
|
|
and grad_view._param_begin < grad_view._param_end
|
|
):
|
|
grad_view.fill_slice_param(slice_param)
|
|
self._create_master_weight(slice_param)
|
|
if param.name in self._master_weights:
|
|
slice_param._clear_dataptr()
|
|
has_clear = True
|
|
|
|
if has_clear:
|
|
comm_buffer._clear_param_storage()
|
|
# 2D params
|
|
if color in self._params_2d_by_color.keys():
|
|
group_info = self._color_to_group_info[color]
|
|
sharding_rank = max(group_info["rank"], 0)
|
|
rank2params_2d_by_color = self._rank2params_2d_by_color[color]
|
|
local_2d = rank2params_2d_by_color[sharding_rank]
|
|
for param in local_2d:
|
|
if not g_shard_bypass_dygraph_optimizer:
|
|
self._create_master_weight(param)
|
|
|
|
for param in self._params_2d_by_color[color]:
|
|
param._clear_to_zero_allocation()
|
|
|
|
def reset_param_storage(self):
|
|
for color in self.clear_color:
|
|
if color is None:
|
|
continue
|
|
# 1D params
|
|
if color in self._color_to_comm_buffer_list.keys():
|
|
for comm_buffer in self._color_to_comm_buffer_list[color]:
|
|
if not comm_buffer.param_storage._is_initialized():
|
|
comm_buffer._reset_param_storage()
|
|
# 2D params
|
|
if color in self._params_2d_by_color.keys():
|
|
for param in self._params_2d_by_color[color]:
|
|
if not param._is_initialized():
|
|
new_param = paddle.empty_like(param)
|
|
new_param._share_buffer_to(param)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Gradient communication
|
|
# ------------------------------------------------------------------
|
|
|
|
def _get_param_grad(self, param):
|
|
if not param.trainable:
|
|
return None
|
|
if hasattr(param, "main_grad"):
|
|
assert param._grad_ivar() is None, (
|
|
"param.grad should be None when using main_grad"
|
|
)
|
|
return param.main_grad
|
|
return param._grad_ivar()
|
|
|
|
def _reduce_2d_grads(self, params, param2rank, comm_group):
|
|
"""Reduce gradients for 2D params to their owner rank within comm_group."""
|
|
for param in params:
|
|
g_var = self._get_param_grad(param)
|
|
if g_var is None:
|
|
if hasattr(param, "main_grad"):
|
|
g_var = paddle.zeros_like(param, dtype=paddle.float32)
|
|
param.main_grad = g_var
|
|
else:
|
|
g_var = paddle.zeros_like(param, dtype=param.dtype)
|
|
param.grad = g_var
|
|
|
|
reduce_op = ReduceOp.AVG
|
|
if not self.use_reduce_avg:
|
|
nranks = comm_group.nranks
|
|
g_var.scale_(1.0 / nranks)
|
|
reduce_op = ReduceOp.SUM
|
|
|
|
if paddle.distributed.in_auto_parallel_align_mode():
|
|
reduce_op = ReduceOp.SUM
|
|
|
|
param_rank = param2rank[param.name]
|
|
paddle.distributed.reduce(
|
|
g_var,
|
|
dst=comm_group.ranks[param_rank],
|
|
op=reduce_op,
|
|
group=comm_group,
|
|
sync_op=True,
|
|
)
|
|
|
|
def reduce_gradients(self, parameter_list, hcg):
|
|
"""Reduce gradients: reduce for 2D params, reduce-scatter for 1D params."""
|
|
if (
|
|
paddle.is_compiled_with_xpu()
|
|
and os.getenv("XPU_CDNN_CLUSTER_PARALLEL") is not None
|
|
):
|
|
paddle.device.synchronize()
|
|
|
|
with framework.no_grad():
|
|
# --- 2D params: reduce to owner rank via each color's group ---
|
|
if self._use_fuse_gradients:
|
|
for comm_buffer in self.comm_buffer_2d:
|
|
if (
|
|
self.sd_release_grads
|
|
and comm_buffer.grad_storage is None
|
|
):
|
|
if comm_buffer.need_reduce_scale_sync():
|
|
for param in comm_buffer.params:
|
|
comm_buffer._copy_grad_to_buffer(param)
|
|
if not self.use_group_call_opt:
|
|
for comm_buffer in self.comm_buffer_2d:
|
|
comm_buffer._comm_grads()
|
|
else:
|
|
same_card_buffers = []
|
|
all_ring_buffers = []
|
|
hcg = fleet.get_hybrid_communicate_group()
|
|
sharding_group = hcg.get_sharding_parallel_group()
|
|
moe_sharding_group = hcg.get_moe_sharding_parallel_group()
|
|
for comm_buffer in self.comm_buffer_2d:
|
|
if comm_buffer._comm_group == sharding_group:
|
|
all_ring_buffers.append(comm_buffer)
|
|
elif comm_buffer._comm_group == moe_sharding_group:
|
|
same_card_buffers.append(comm_buffer)
|
|
else:
|
|
raise ValueError("Unknown comm group")
|
|
tasks = []
|
|
with _coalescing_manager(moe_sharding_group, tasks):
|
|
for comm_buffer in all_ring_buffers:
|
|
dst_rank = get_same_card_rank(
|
|
list(moe_sharding_group.ranks), comm_buffer._dst
|
|
)
|
|
assert dst_rank != -1, "Please check you dst_rank!"
|
|
assert comm_buffer._use_reduce_avg, (
|
|
"Only support for reduce avg now"
|
|
)
|
|
paddle.distributed.stream.reduce(
|
|
comm_buffer.grad_storage,
|
|
dst=dst_rank,
|
|
op=paddle.distributed.ReduceOp.AVG,
|
|
group=moe_sharding_group,
|
|
sync_op=True,
|
|
use_calc_stream=True,
|
|
)
|
|
|
|
for comm_buffer in same_card_buffers:
|
|
assert (
|
|
comm_buffer._comm_group == moe_sharding_group
|
|
), "Please check comm group"
|
|
assert comm_buffer._use_reduce_avg, (
|
|
"Only support for reduce avg now"
|
|
)
|
|
paddle.distributed.stream.reduce(
|
|
comm_buffer.grad_storage,
|
|
dst=comm_buffer._dst,
|
|
op=paddle.distributed.ReduceOp.AVG,
|
|
group=comm_buffer._comm_group,
|
|
sync_op=True,
|
|
use_calc_stream=True,
|
|
)
|
|
|
|
rank = paddle.distributed.get_rank()
|
|
trainer_idx = rank // 8
|
|
trainer_comm_group = self.trainer_comms[trainer_idx]
|
|
with _coalescing_manager(trainer_comm_group, tasks):
|
|
for comm_buffer in all_ring_buffers:
|
|
trainer, trainer_ranks = get_trainer_ranks(
|
|
comm_buffer._dst
|
|
)
|
|
if rank in trainer_ranks:
|
|
comm_group = self.trainer_comms[trainer]
|
|
assert comm_group == trainer_comm_group, (
|
|
"Please check comm group"
|
|
)
|
|
assert comm_buffer._use_reduce_avg, (
|
|
"Only support for reduce avg now"
|
|
)
|
|
paddle.distributed.stream.reduce(
|
|
comm_buffer.grad_storage,
|
|
dst=comm_buffer._dst,
|
|
op=paddle.distributed.ReduceOp.AVG,
|
|
group=comm_group,
|
|
sync_op=True,
|
|
use_calc_stream=True,
|
|
)
|
|
|
|
else:
|
|
# --- 2D params: reduce to owner rank via each color's group ---
|
|
sharding_group = hcg.get_sharding_parallel_group()
|
|
for color_key, params_2d in self._params_2d_by_color.items():
|
|
if not params_2d:
|
|
continue
|
|
group_info = self._color_to_group_info.get(color_key, {})
|
|
group = group_info.get('group', sharding_group)
|
|
world_size = group_info.get('world_size', 1)
|
|
if world_size > 1:
|
|
param2rank = self._param2rank_2d_by_color.get(
|
|
color_key, {}
|
|
)
|
|
self._reduce_2d_grads(params_2d, param2rank, group)
|
|
|
|
# --- 1D params: reduce-scatter via comm buffers ---
|
|
for comm_buffer in self._comm_buffer_list:
|
|
if self.sd_release_grads and comm_buffer.grad_storage is None:
|
|
if comm_buffer.need_reduce_scale_sync():
|
|
for param in comm_buffer.params:
|
|
comm_buffer._copy_grad_to_buffer(param)
|
|
|
|
if not self.comm_overlap:
|
|
comm_buffer._comm_grads()
|
|
|
|
# wait for all comm_buffer tasks to finish
|
|
if self._use_fuse_gradients:
|
|
if not self.use_group_call_opt:
|
|
for comm_buffer in self.comm_buffer_2d:
|
|
comm_buffer.scale_grads()
|
|
|
|
for comm_buffer in self._comm_buffer_list:
|
|
comm_buffer.scale_grads()
|
|
|
|
def filter_parameters(self, parameter_list, hcg):
|
|
"""Filter parameters: return local 2D params + initialized 1D slices."""
|
|
local_1d = [
|
|
self._slice_params[p.name]
|
|
for p in parameter_list
|
|
if p.name in self._slice_params
|
|
]
|
|
local_1d = [p for p in local_1d if p._is_initialized()]
|
|
return self._local_2d + local_1d
|
|
|
|
# ------------------------------------------------------------------
|
|
# Parameter sync after optimizer step
|
|
# ------------------------------------------------------------------
|
|
def _broadcast_2d_params(self, rank2params, comm_group):
|
|
"""Broadcast 2D params from owner ranks within comm_group."""
|
|
broadcast_tasks = []
|
|
for rank, params in rank2params.items():
|
|
src_rank = comm_group.ranks[rank]
|
|
|
|
if len(params) == 0:
|
|
continue
|
|
|
|
with framework.no_grad():
|
|
# Calculate total size and build param-to-offset mapping
|
|
param_sizes = [np.prod(p.shape) for p in params]
|
|
total_size = sum(param_sizes)
|
|
dtype = params[0].dtype
|
|
|
|
param_buffer = paddle.empty(shape=[total_size], dtype=dtype)
|
|
offset = 0
|
|
for param in params:
|
|
param_shape = param.shape
|
|
stop_gradient = param.stop_gradient
|
|
param.stop_gradient = True
|
|
param.flatten_()
|
|
|
|
paddle.assign(
|
|
param,
|
|
param_buffer._slice(
|
|
offset, offset + np.prod(param_shape)
|
|
),
|
|
)
|
|
|
|
param.get_tensor()._set_dims(param_shape)
|
|
param.stop_gradient = stop_gradient
|
|
param_buffer._slice(
|
|
offset, offset + np.prod(param_shape)
|
|
)._share_buffer_to(param)
|
|
|
|
offset += np.prod(param_shape)
|
|
task = paddle.distributed.broadcast(
|
|
param_buffer,
|
|
src=src_rank,
|
|
group=comm_group,
|
|
sync_op=False,
|
|
)
|
|
broadcast_tasks.append(task)
|
|
return broadcast_tasks
|
|
|
|
def reorder_params(self, comm_list):
|
|
"""Reorder and flatten parameters into contiguous buffers for communication.
|
|
|
|
For each rank in the communication groups, flatten multiple parameters into a
|
|
single contiguous buffer to enable efficient communication operations
|
|
|
|
Args:
|
|
comm_list: List of tuples (rank2params, comm_group), where:
|
|
- rank2params: Dict mapping local rank indices to lists of parameters
|
|
- comm_group: Communication group containing ranks mapping
|
|
|
|
Returns:
|
|
List of dicts with keys:
|
|
- param_buffer: Contiguous buffer containing flattened parameters
|
|
- src_rank: Source rank for this buffer in the comm_group
|
|
"""
|
|
reorder_params_list = []
|
|
for rank2params, group in comm_list:
|
|
for rank, params in rank2params.items():
|
|
src_rank = group.ranks[rank]
|
|
if len(params) == 0:
|
|
continue
|
|
|
|
with framework.no_grad():
|
|
# Calculate total size and build param-to-offset mapping
|
|
param_sizes = [np.prod(p.shape) for p in params]
|
|
total_size = sum(param_sizes)
|
|
dtype = params[0].dtype
|
|
|
|
if len(params) != 1:
|
|
param_buffer = paddle.empty(
|
|
shape=[total_size], dtype=dtype
|
|
)
|
|
offset = 0
|
|
for param in params:
|
|
param_shape = param.shape
|
|
stop_gradient = param.stop_gradient
|
|
param.stop_gradient = True
|
|
param.flatten_()
|
|
|
|
paddle.assign(
|
|
param,
|
|
param_buffer._slice(
|
|
offset, offset + np.prod(param_shape)
|
|
),
|
|
)
|
|
|
|
param.get_tensor()._set_dims(param_shape)
|
|
param.stop_gradient = stop_gradient
|
|
param_buffer._slice(
|
|
offset, offset + np.prod(param_shape)
|
|
)._share_buffer_to(param)
|
|
|
|
offset += np.prod(param_shape)
|
|
else:
|
|
param_buffer = params[0]
|
|
reorder_params_item = {
|
|
"param_buffer": param_buffer,
|
|
"src_rank": src_rank,
|
|
}
|
|
reorder_params_list.append(reorder_params_item)
|
|
return reorder_params_list
|
|
|
|
def _sharding_sync_parameters(self):
|
|
"""Sync parameters: broadcast 2D, all-gather 1D."""
|
|
comm_group = self._hcg.get_sharding_parallel_group()
|
|
|
|
with framework.no_grad():
|
|
all_tasks = []
|
|
|
|
# --- 2D params: broadcast from owner via each color's group ---
|
|
all_ring_list = []
|
|
same_card_list = []
|
|
for color_key, rank2params in self._rank2params_2d_by_color.items():
|
|
group_info = self._color_to_group_info.get(color_key, {})
|
|
group = group_info.get('group', comm_group)
|
|
world_size = group_info.get('world_size', 1)
|
|
if world_size > 1:
|
|
if not self.use_group_call_opt:
|
|
all_tasks.extend(
|
|
self._broadcast_2d_params(rank2params, group)
|
|
)
|
|
else:
|
|
hcg = fleet.get_hybrid_communicate_group()
|
|
sharding_group = hcg.get_sharding_parallel_group()
|
|
moe_sharding_group = (
|
|
hcg.get_moe_sharding_parallel_group()
|
|
)
|
|
if group == sharding_group:
|
|
all_ring_list.append([rank2params, group])
|
|
elif group == moe_sharding_group:
|
|
same_card_list.append([rank2params, group])
|
|
else:
|
|
raise ValueError("Please check your comm group")
|
|
# world_size=1: single rank group, no broadcast needed
|
|
|
|
if self.use_group_call_opt:
|
|
all_ring_reorder_params_list = self.reorder_params(
|
|
all_ring_list
|
|
)
|
|
same_card_reorder_params_list = self.reorder_params(
|
|
same_card_list
|
|
)
|
|
rank = paddle.distributed.get_rank()
|
|
|
|
# trainer inner broadcast
|
|
rank = paddle.distributed.get_rank()
|
|
trainer_idx = rank // 8
|
|
trainer_comm_group = self.trainer_comms[trainer_idx]
|
|
tasks = []
|
|
with _coalescing_manager(trainer_comm_group, tasks):
|
|
for reorder_params_item in all_ring_reorder_params_list:
|
|
param_buffer = reorder_params_item["param_buffer"]
|
|
src_rank = reorder_params_item["src_rank"]
|
|
trainer, trainer_ranks = get_trainer_ranks(src_rank)
|
|
if rank in trainer_ranks:
|
|
comm_group = self.trainer_comms[trainer]
|
|
assert comm_group == trainer_comm_group, (
|
|
"Please check comm group"
|
|
)
|
|
paddle.distributed.stream.broadcast(
|
|
param_buffer,
|
|
src=src_rank,
|
|
group=comm_group,
|
|
sync_op=True,
|
|
use_calc_stream=True,
|
|
)
|
|
|
|
# same card broadcast
|
|
with _coalescing_manager(moe_sharding_group, tasks):
|
|
for reorder_params_item in all_ring_reorder_params_list:
|
|
param_buffer = reorder_params_item["param_buffer"]
|
|
src_rank = reorder_params_item["src_rank"]
|
|
same_card_src_rank = get_same_card_rank(
|
|
list(moe_sharding_group.ranks), src_rank
|
|
)
|
|
assert same_card_src_rank != -1, (
|
|
"Please check your same_card_src_rank in broadcast!"
|
|
)
|
|
paddle.distributed.stream.broadcast(
|
|
param_buffer,
|
|
src=same_card_src_rank,
|
|
group=moe_sharding_group,
|
|
sync_op=True,
|
|
use_calc_stream=True,
|
|
)
|
|
for same_card_item in same_card_reorder_params_list:
|
|
param_buffer = same_card_item["param_buffer"]
|
|
src_rank = same_card_item["src_rank"]
|
|
paddle.distributed.stream.broadcast(
|
|
param_buffer,
|
|
src=src_rank,
|
|
group=moe_sharding_group,
|
|
sync_op=True,
|
|
use_calc_stream=True,
|
|
)
|
|
|
|
if not self.use_group_call_opt:
|
|
for task in all_tasks:
|
|
task.wait()
|
|
|
|
# --- 1D params: all-gather via comm buffers ---
|
|
for comm_buffer in self._comm_buffer_list:
|
|
comm_buffer.sync_params()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Clear gradients
|
|
# ------------------------------------------------------------------
|
|
|
|
def clear_grad(self, set_to_zero=True):
|
|
"""Clear gradients for all parameters."""
|
|
|
|
def clear_grad_func(p):
|
|
if hasattr(p, "main_grad") and p.main_grad is not None:
|
|
assert p._grad_ivar() is None
|
|
if set_to_zero:
|
|
p.main_grad.zero_()
|
|
else:
|
|
p.main_grad._clear()
|
|
p.main_grad = None
|
|
elif not hasattr(p, "main_grad"):
|
|
if self.tensor_fusion:
|
|
if set_to_zero:
|
|
p.grad.zero_()
|
|
else:
|
|
p.grad._clear()
|
|
p.grad = None
|
|
else:
|
|
p.clear_gradient(set_to_zero)
|
|
|
|
for p in self._parameter_list:
|
|
clear_grad_func(p)
|
|
|
|
if self.sd_release_grads and not self.pp_overlap:
|
|
# 1D params are managed by comm buffers
|
|
for comm_buffer in self._comm_buffer_list:
|
|
if comm_buffer.need_reduce_scale_sync():
|
|
comm_buffer._clear_grad_storage()
|
|
# 2D params are managed by comm buffers
|
|
if self._use_fuse_gradients:
|
|
for comm_buffer in self.comm_buffer_2d:
|
|
if comm_buffer.need_reduce_scale_sync():
|
|
comm_buffer._clear_grad_storage()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Optimizer step
|
|
# ------------------------------------------------------------------
|
|
|
|
def _collect_comm_buffers(self):
|
|
"""Collect communication buffers (for PP overlap compatibility)."""
|
|
if self._comm_buffer_list:
|
|
return
|
|
for param in self._params_1d:
|
|
if not hasattr(param, "comm_buffer_ref"):
|
|
continue
|
|
comm_buffer_ref = param.comm_buffer_ref
|
|
del param.comm_buffer_ref
|
|
comm_buffer = comm_buffer_ref()
|
|
self._comm_buffer_list.append(comm_buffer)
|
|
|
|
for bucket in self._comm_buffer_list:
|
|
for p in bucket._params:
|
|
if p.name in self.param2bucket:
|
|
self.param2bucket[p.name].append(bucket)
|
|
else:
|
|
self.param2bucket[p.name] = [bucket]
|
|
|
|
def _assign_slice_grad(self):
|
|
"""Assign gradients from comm buffers to slice params for 1D params."""
|
|
for comm_buffer in self._comm_buffer_list:
|
|
for param in comm_buffer.params:
|
|
if param.name in self._slice_params:
|
|
slice_param = self._slice_params[param.name]
|
|
if self.sd_release_grads and hasattr(
|
|
slice_param, "main_grad"
|
|
):
|
|
if not slice_param.main_grad._is_initialized():
|
|
del slice_param.main_grad
|
|
comm_buffer.assign_slice_grad(param, slice_param)
|
|
|
|
def set_grad_clip_info(self):
|
|
from paddle.distributed.fleet.meta_optimizers.dygraph_optimizer.hybrid_parallel_optimizer import (
|
|
ClipGradByAdaptiveNorm,
|
|
)
|
|
|
|
if isinstance(self._inner_opt._grad_clip, ClipGradByAdaptiveNorm):
|
|
if not hasattr(
|
|
self._inner_opt._grad_clip, "group_index_to_name_to_local_idx"
|
|
):
|
|
# Build parameter groups and their metadata
|
|
group_index_to_name_to_local_idx = {} # group_idx -> {param_name: local_index}
|
|
param_name_to_group_idx = {} # param_name -> group_idx
|
|
group_idx_to_comm_group = {} # group_idx -> comm_group
|
|
group_idx_to_param_num = {} # group_idx -> param count
|
|
|
|
group_idx = 0
|
|
# 1D params
|
|
for _, comm_buffer in enumerate(self._comm_buffer_list):
|
|
name_to_local_idx = {}
|
|
for local_idx, param in enumerate(comm_buffer._params):
|
|
param = self._slice_params[param.name]
|
|
if param._is_initialized():
|
|
name_to_local_idx[param.name] = local_idx
|
|
param_name_to_group_idx[param.name] = group_idx
|
|
group_index_to_name_to_local_idx[group_idx] = (
|
|
name_to_local_idx
|
|
)
|
|
group_idx_to_comm_group[group_idx] = comm_buffer._comm_group
|
|
group_idx_to_param_num[group_idx] = len(comm_buffer._params)
|
|
group_idx += 1
|
|
|
|
# 2D params
|
|
for color_key, params_2d in self._params_2d_by_color.items():
|
|
name_to_local_idx = {}
|
|
local_2d_names = {p.name for p in self._local_2d}
|
|
for local_idx, param in enumerate(params_2d):
|
|
if param.name in local_2d_names:
|
|
name_to_local_idx[param.name] = local_idx
|
|
param_name_to_group_idx[param.name] = group_idx
|
|
group_index_to_name_to_local_idx[group_idx] = (
|
|
name_to_local_idx
|
|
)
|
|
|
|
# Get comm group from color_key
|
|
group_info = self._color_to_group_info[color_key]
|
|
comm_group = group_info['group']
|
|
|
|
group_idx_to_comm_group[group_idx] = comm_group
|
|
group_idx_to_param_num[group_idx] = len(params_2d)
|
|
group_idx += 1
|
|
|
|
self._inner_opt._grad_clip.group_index_to_name_to_local_idx = (
|
|
group_index_to_name_to_local_idx
|
|
)
|
|
self._inner_opt._grad_clip.param_name_to_group_idx = (
|
|
param_name_to_group_idx
|
|
)
|
|
self._inner_opt._grad_clip.group_idx_to_comm_group = (
|
|
group_idx_to_comm_group
|
|
)
|
|
self._inner_opt._grad_clip.group_idx_to_param_num = (
|
|
group_idx_to_param_num
|
|
)
|
|
self._inner_opt._grad_clip.sharding_stage1_v2 = True
|
|
|
|
def step(self):
|
|
"""Optimizer step: update local 2D params and 1D slices, then sync."""
|
|
self.reset_param_storage()
|
|
|
|
self._collect_comm_buffers()
|
|
self._assign_slice_grad()
|
|
|
|
self.set_grad_clip_info()
|
|
|
|
if not isinstance(self._origin_parameter_list[0], dict):
|
|
params_grads = []
|
|
|
|
# --- All 2D params on this rank (all colors): full tensors ---
|
|
# Pass the original param directly to the optimizer.
|
|
# _muon_update handles both shapes:
|
|
# - 2D [H, I]: standard Newton-Schulz
|
|
# - 3D [n_experts, H, I]: per-expert Newton-Schulz loop
|
|
# Keeping the original param name avoids registering _expert_N
|
|
# accumulator keys absent from model_sharded_state_dict, which
|
|
# would break sharded_state_dict (checkpoint save).
|
|
for color_key, rank2params in self._rank2params_2d_by_color.items():
|
|
group_info = self._color_to_group_info.get(color_key, {})
|
|
color_rank = group_info.get('rank', 0)
|
|
world_size = group_info.get('world_size', 1)
|
|
rank_key = color_rank if world_size > 1 else 0
|
|
for param in rank2params.get(rank_key, []):
|
|
if param.stop_gradient:
|
|
continue
|
|
grad_var = param._grad_ivar()
|
|
if (
|
|
hasattr(param, "main_grad")
|
|
and param.main_grad is not None
|
|
):
|
|
grad_var = param.main_grad
|
|
if grad_var is not None:
|
|
params_grads.append((param, grad_var))
|
|
|
|
# --- 1D params: slice params (element-wise shards) ---
|
|
for param in self._params_1d:
|
|
if param.stop_gradient:
|
|
continue
|
|
if param.name not in self._slice_params:
|
|
continue
|
|
slice_p = self._slice_params[param.name]
|
|
if not slice_p._is_initialized():
|
|
continue
|
|
grad_var = slice_p._grad_ivar()
|
|
if (
|
|
hasattr(slice_p, "main_grad")
|
|
and slice_p.main_grad is not None
|
|
):
|
|
grad_var = slice_p.main_grad
|
|
if grad_var is not None:
|
|
params_grads.append((slice_p, grad_var))
|
|
|
|
self._apply_optimize(
|
|
loss=None,
|
|
startup_program=None,
|
|
params_grads=params_grads,
|
|
)
|
|
|
|
# Sync parameters across sharding ranks
|
|
self._sharding_sync_parameters()
|
|
|
|
# ------------------------------------------------------------------
|
|
# State dict (checkpoint save/load)
|
|
# ------------------------------------------------------------------
|
|
|
|
@framework.dygraph_only
|
|
def set_state_dict(self, state_dict):
|
|
inner_state = {}
|
|
# Collect local parameters: 2D whole-tensor params + 1D original params
|
|
parameters = list(self._local_2d) + list(self._params_1d)
|
|
|
|
if "LR_Scheduler" in state_dict:
|
|
inner_state["LR_Scheduler"] = state_dict.pop("LR_Scheduler")
|
|
|
|
if "master_weights" in state_dict:
|
|
master = state_dict.pop("master_weights")
|
|
inner_state["master_weights"] = {}
|
|
for p in parameters:
|
|
for k, v in master.items():
|
|
if p.name == k:
|
|
v.name = self._inner_opt._gen_master_weight_var_name(p)
|
|
inner_state["master_weights"][k] = v
|
|
|
|
for p in parameters:
|
|
for k, v in state_dict.items():
|
|
if p.name in k:
|
|
inner_state[k] = v
|
|
|
|
self._inner_opt.set_state_dict(inner_state)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Utility
|
|
# ------------------------------------------------------------------
|
|
|
|
def _set_inner_opt_attr(self, attr_name, value):
|
|
inner_opt = self._inner_opt
|
|
inner_opt_name = '_inner_opt'
|
|
if not isinstance(attr_name, str):
|
|
raise TypeError(
|
|
f"attr_name should be str type, but is {type(attr_name)}"
|
|
)
|
|
while hasattr(inner_opt, attr_name):
|
|
setattr(inner_opt, attr_name, value)
|
|
inner_opt = getattr(inner_opt, inner_opt_name, None)
|
|
if inner_opt is None:
|
|
break
|
|
|
|
def sharded_state_dict(self, model_sharded_state_dict):
|
|
"""Build a sharded optimizer state dict for flex checkpoint save/load.
|
|
|
|
Overrides the inner Muon optimizer's sharded_state_dict to handle V3's
|
|
hybrid sharding scheme:
|
|
- 2D Muon params: whole tensor, shape matches model's local_shape.
|
|
Handled by delegating to the inner Muon's sharded_state_dict after
|
|
filtering out 1D param states.
|
|
- 1D AdamW params: accumulators are 1D shards (from reduce-scatter);
|
|
wrapped with is_flattened=True + flattened_range, like V2.
|
|
"""
|
|
import paddle as _paddle
|
|
from paddle.distributed.flex_checkpoint.dcp.sharded_weight import (
|
|
ShardedWeight,
|
|
create_sharded_weight_with_new_local,
|
|
)
|
|
|
|
# ---- Step 1: Collect flattened_range for each 1D (AdamW) param ----
|
|
# Identical logic to DygraphShardingOptimizerV2.sharded_state_dict.
|
|
param_slice_info = {} # param_name -> slice(begin, end)
|
|
padded_param = set()
|
|
for buffer in self._comm_buffer_list:
|
|
for (
|
|
param_name,
|
|
grad_view,
|
|
) in buffer._sharding_param_grad_view.items():
|
|
numel = grad_view._param.numel().item()
|
|
param_begin = grad_view._param_begin
|
|
param_end = grad_view._param_end
|
|
index = grad_view._index
|
|
padding_begin = index + numel
|
|
flattened_range = slice(
|
|
param_begin - index,
|
|
max(
|
|
min(padding_begin - index, param_end - index),
|
|
param_begin - index,
|
|
),
|
|
)
|
|
if param_end > padding_begin:
|
|
padded_param.add(param_name)
|
|
param_slice_info[param_name] = flattened_range
|
|
|
|
# ---- Step 2: Build static_name → struct_name mapping ----
|
|
model_sharded_sorted = dict(sorted(model_sharded_state_dict.items()))
|
|
static_to_struct = {}
|
|
for struct_name, sw in model_sharded_sorted.items():
|
|
if sw.local_tensor.name not in static_to_struct:
|
|
static_to_struct[sw.local_tensor.name] = struct_name
|
|
|
|
# ---- Step 3: Process all optimizer states ----
|
|
_FP32_MASTER = "fp32_master_0"
|
|
_optimizer_scalar_names = ["beta1_pow_acc_0", "beta2_pow_acc_0"]
|
|
_optimizer_vector_names = ["moment1_0", "moment2_0"]
|
|
|
|
def _make_2d_entry(uname, t, sp):
|
|
"""Reshape tensor if numel matches but shape differs, then wrap as ShardedWeight."""
|
|
target = sp.local_shape
|
|
if (
|
|
tuple(t.shape) != tuple(target)
|
|
and t.numel() == _paddle.to_tensor(list(target)).prod().item()
|
|
):
|
|
t = t.reshape(target)
|
|
return create_sharded_weight_with_new_local(uname, t, sp)
|
|
|
|
def _split_state_name(vname):
|
|
if _FP32_MASTER in vname:
|
|
return tuple(vname.split("_" + _FP32_MASTER + "_", 1))
|
|
for suffix in _optimizer_scalar_names + _optimizer_vector_names:
|
|
if vname.endswith(suffix):
|
|
return vname[: -(len(suffix) + 1)], suffix
|
|
raise ValueError(
|
|
f"Cannot parse optimizer state variable name: {vname!r}"
|
|
)
|
|
|
|
optimizer_state_dict = self._inner_opt.state_dict()
|
|
master_weights = optimizer_state_dict.pop("master_weights", None)
|
|
optimizer_state_dict.pop("LR_Scheduler", None)
|
|
|
|
sharded_state = {}
|
|
|
|
for key, tensor in optimizer_state_dict.items():
|
|
static_name, state_type = _split_state_name(key)
|
|
if static_name not in static_to_struct:
|
|
continue
|
|
|
|
struct_name = static_to_struct[static_name]
|
|
sharded_param = model_sharded_sorted[struct_name]
|
|
unified_name = f"{struct_name}.{state_type}"
|
|
|
|
is_1d_param = static_name in param_slice_info
|
|
|
|
if state_type in _optimizer_vector_names:
|
|
if is_1d_param:
|
|
# 1D AdamW shard: wrap with is_flattened=True (like V2)
|
|
flattened_range = param_slice_info[static_name]
|
|
if flattened_range.stop - flattened_range.start == 0:
|
|
continue
|
|
is_padded = static_name in padded_param
|
|
if is_padded:
|
|
local_tensor = _paddle.slice(
|
|
tensor,
|
|
axes=[0],
|
|
starts=[0],
|
|
ends=[flattened_range.stop - flattened_range.start],
|
|
)
|
|
else:
|
|
local_tensor = tensor
|
|
sharded_state[unified_name] = ShardedWeight(
|
|
key=unified_name,
|
|
local_tensor=local_tensor,
|
|
local_shape=sharded_param.local_shape,
|
|
global_shape=sharded_param.global_shape,
|
|
global_offset=sharded_param.global_offset,
|
|
is_flattened=True,
|
|
flattened_range=flattened_range,
|
|
)
|
|
elif tensor.is_dist():
|
|
sharded_state[unified_name] = ShardedWeight(
|
|
key=unified_name,
|
|
local_tensor=tensor,
|
|
local_shape=tensor.shape,
|
|
global_shape=tensor.shape,
|
|
global_offset=sharded_param.global_offset,
|
|
)
|
|
else:
|
|
# 2D Muon param (non-MoE or MoE): shape may differ between
|
|
# Python param.shape (3D view) and model storage (2D).
|
|
sharded_state[unified_name] = _make_2d_entry(
|
|
unified_name, tensor, sharded_param
|
|
)
|
|
else:
|
|
# Scalar states (beta_pow): replicated
|
|
sharded_state[unified_name] = ShardedWeight(
|
|
key=unified_name,
|
|
local_tensor=tensor,
|
|
local_shape=(1,),
|
|
global_shape=(1,),
|
|
global_offset=(0,),
|
|
)
|
|
|
|
# FP32 master weights
|
|
if master_weights:
|
|
for weight_key, tensor in master_weights.items():
|
|
if weight_key not in static_to_struct:
|
|
continue
|
|
struct_name = static_to_struct[weight_key]
|
|
sharded_param = model_sharded_sorted[struct_name]
|
|
unified_name = f"{struct_name}.w_0"
|
|
is_1d_param = weight_key in param_slice_info
|
|
|
|
if is_1d_param:
|
|
flattened_range = param_slice_info[weight_key]
|
|
if flattened_range.stop - flattened_range.start == 0:
|
|
continue
|
|
is_padded = weight_key in padded_param
|
|
if is_padded:
|
|
local_tensor = _paddle.slice(
|
|
tensor,
|
|
axes=[0],
|
|
starts=[0],
|
|
ends=[flattened_range.stop - flattened_range.start],
|
|
)
|
|
else:
|
|
local_tensor = tensor
|
|
sharded_state[unified_name] = ShardedWeight(
|
|
key=unified_name,
|
|
local_tensor=local_tensor,
|
|
local_shape=sharded_param.local_shape,
|
|
global_shape=sharded_param.global_shape,
|
|
global_offset=sharded_param.global_offset,
|
|
is_flattened=True,
|
|
flattened_range=flattened_range,
|
|
)
|
|
elif tensor.is_dist():
|
|
sharded_state[unified_name] = ShardedWeight(
|
|
key=unified_name,
|
|
local_tensor=tensor,
|
|
local_shape=tensor.shape,
|
|
global_shape=tensor.shape,
|
|
global_offset=sharded_param.global_offset,
|
|
)
|
|
else:
|
|
# Same reshape logic as for optimizer vector states:
|
|
# FP32 master weight may be 3D (e.g. grouped_gemm_experts
|
|
# [n_experts, H, I]) while model storage is 2D [n_experts*H, I].
|
|
sharded_state[unified_name] = _make_2d_entry(
|
|
unified_name, tensor, sharded_param
|
|
)
|
|
|
|
return sharded_state
|
|
|
|
def __getattr__(self, item):
|
|
return getattr(self._inner_opt, item)
|