Files
paddlepaddle--paddle/python/paddle/optimizer/muon.py
T
2026-07-13 12:40:42 +08:00

838 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (c) 2024 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.
from __future__ import annotations
import logging
import os
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from paddle import Tensor
import paddle
from paddle import _C_ops
from paddle.base import framework
from paddle.distributed.flex_checkpoint.dcp.sharded_weight import (
ShardedStateDict,
ShardedWeight,
create_sharded_weight_with_new_local,
)
from ..nn.clip import GradientClipBase
from .optimizer import Optimizer
# Debug logging for Muon optimizer
_logger = logging.getLogger(__name__)
MUON_DEBUG = os.environ.get("MUON_DEBUG", "0") == "1"
__all__ = []
# ------------------------------------------------------------------
# Parameter metadata dataclasses
# ------------------------------------------------------------------
@dataclass
class MuonParamInfo:
"""Muon update metadata for a single parameter.
This replaces the previous approach of setting dynamic attributes
directly on param objects.
Attributes:
use_muon: If True, use Muon (orthogonal) updates; otherwise AdamW.
split_concat_func: Optional callable that implements the slice strategy.
Signature: split_concat_func(matrix, ortho_fn, **kwargs) -> sliced_matrix
If None, whole-matrix orthogonalisation is used.
"""
use_muon: bool = True
split_concat_func: Callable | None = None
# Type alias for the parameter info mapping
MuonParamInfoMap = dict[str, MuonParamInfo]
# ------------------------------------------------------------------
# Newton-Schulz coefficient sets
# ------------------------------------------------------------------
_NS_COEFFICIENT_SETS = {
# Simple coefficient set (original)
"simple": [
(3.4445, -4.7750, 2.0315),
],
# Quintic iteration with optimized coefficients
# Source: https://leloykun.github.io/ponder/muon-opt-coeffs/
"quintic": [
(4.0848, -6.8946, 2.9270),
(3.9505, -6.3029, 2.6377),
(3.7418, -5.5913, 2.3037),
(2.8769, -3.1427, 1.2046),
(2.8366, -3.0525, 1.2012),
],
# Polar Express iteration from https://arxiv.org/abs/2505.16932
"polar_express": [
(8.2051, -22.9019, 16.4607),
(4.0664, -2.8612, 0.5184),
(3.9096, -2.8234, 0.5250),
(3.2856, -2.4153, 0.4853),
(2.2779, -1.6198, 0.3985),
(1.8726, -1.2307, 0.3585),
(1.8564, -1.2132, 0.3568),
(1.8750, -1.2500, 0.3750),
],
# AOL coefficients from https://github.com/thib-s/flash-newton-schulz
"aol": [
(4.0098, -7.0585, 2.4635),
(3.4585, -5.5479, 2.5959),
(2.7573, -3.2939, 1.4254),
(2.7215, -3.0494, 1.3169),
],
"deepseekv4":
# From DeepSeekV4: https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro/resolve/main/DeepSeek_V4.pdf
[(3.4445, -4.7750, 2.0315)] * 8 + [(2.0, -1.5, 0.5)] * 2,
}
# ------------------------------------------------------------------
# Default parameter classification
# ------------------------------------------------------------------
def _default_should_use_muon(name, shape, exclude_patterns):
"""Default fallback logic for determining if a parameter should use Muon.
This is only used when param.is_muon is not set. The actual exclusion
patterns must be configured via training_args.muon_exclude_patterns in yaml.
Args:
name: Parameter name.
shape: Parameter shape.
exclude_patterns: List of substrings to exclude from Muon updates.
Must be provided (e.g., ['embed', 'bias', 'lm_head', 'mlp.gate']).
Returns:
True if the parameter should use Muon (orthogonal) updates.
Raises:
ValueError: If exclude_patterns is None.
"""
if exclude_patterns is None:
raise ValueError(
"muon_exclude_patterns must be set in yaml config. "
"Example: muon_exclude_patterns: ['embed', 'bias', 'lm_head', 'mlp.gate']"
)
if len(shape) not in (2, 3):
return False
name_lower = name.lower()
for pattern in exclude_patterns:
if pattern.lower() in name_lower:
return False
return True
class Muon(Optimizer):
r"""
Muon optimizer for MuonShardingOptimizer (Sharding Stage1 V3) usage.
For 2-D weight matrices (identified by :func:`_default_should_use_muon`), Muon
applies orthogonal gradient updates via Newton-Schulz iteration. For all
other parameters (embeddings, biases, expert weights, …) it falls back to
a standard AdamW update.
Designed for ``MuonShardingOptimizer`` (Sharding Stage1 V3), where 2D parameters are
assigned as whole tensors to ranks. Currently we do not support TP=1, no sharding gather
or TP communication is needed during the optimizer step.
Args:
learning_rate (float | LRScheduler): Learning rate. Default: ``0.02``.
parameters (list[Tensor]): Flat list of parameters to optimize.
momentum (float): Momentum coefficient for the Muon update. Default: ``0.95``.
adam_beta1 (float): β₁ for the AdamW fallback. Default: ``0.9``.
adam_beta2 (float): β₂ for the AdamW fallback. Default: ``0.95``.
weight_decay (float): Decoupled weight decay. Default: ``0.01``.
ns_steps (int): Newton-Schulz iteration steps. Default: ``5``.
ns_coeff_type (str): Preset name for Newton-Schulz coefficients.
Options: ``"simple"``, ``"quintic"``, ``"polar_express"``,
``"aol"``, ``"deepseekv4"``, ``"custom"``. Default: ``"simple"``.
ns_coeffs (list[tuple[float, float, float]] | None): Custom
Newton-Schulz coefficient set. Each tuple is ``(a, b, c)``
for one iteration step. Default: ``None``.
Only used when ns_coeff_type=``custom``.
nesterov (bool): Use Nesterov momentum in Muon. Default: ``True``.
adam_epsilon (float): ε for numerical stability in AdamW. Default: ``1e-9``.
grad_clip (GradientClipBase | None): Gradient clipping. Default: ``None``.
apply_decay_param_fun (callable | None): Function to select which
parameters receive weight decay. Default: ``None``.
muon_version (int): Scaling-function version (1/2/3). Default: ``1``.
muon_exclude_patterns (list[str] | None): Parameter names containing
any of these substrings will use AdamW instead of Muon.
Example: ``['embed', 'bias', 'lm_head', 'mlp.gate']``.
Default: ``None``.
muon_extra_scale_factor (float): Extra multiplicative scale applied
after the dimension-dependent scaling in ``_scaling_fn``.
Default: ``0.2``.
muon_param_info_map (MuonParamInfoMap | None): Per-parameter metadata
dict mapping param name to :class:`MuonParamInfo` (use_muon,
split_concat_func). Built by Trainer and passed in.
Default: ``None``.
ns_matmul_dtype (paddle.dtype | None): Dtype for Newton-Schulz matmul
iterations. ``None`` = auto-detect: bfloat16 on Ampere+ (capability
>= 8.0), float32 on V100 and older. Pass ``paddle.float32``
explicitly to force float32. Default: ``None``.
multi_precision (bool): Maintain FP32 master weights when training in
BF16/FP16. Default: ``False``.
name (str | None): Optional name for the optimizer instance.
"""
_moment_acc_str = "moment1"
_moment2_acc_str = "moment2"
_beta1_pow_acc_str = "beta1_pow_acc"
_beta2_pow_acc_str = "beta2_pow_acc"
def __init__(
self,
learning_rate=0.02,
parameters=None,
momentum=0.95,
adam_beta1=0.9,
adam_beta2=0.95,
weight_decay=0.01,
ns_steps=5,
ns_coeff_type="simple",
ns_coeffs=None,
nesterov=True,
adam_epsilon=1e-9,
grad_clip=None,
lr_ratio: Callable[[Tensor], float] | None = None,
apply_decay_param_fun: Callable[[str], bool] | None = None,
muon_version=1,
muon_exclude_patterns=None,
muon_extra_scale_factor=0.2,
muon_param_info_map: MuonParamInfoMap | None = None,
ns_matmul_dtype=None,
multi_precision=False,
name=None,
**kwargs,
):
if parameters is None:
raise ValueError(
"parameters argument given to the Optimizer should not be None."
)
if not isinstance(parameters, list):
raise TypeError("parameters must be a list.")
if len(parameters) > 0 and isinstance(parameters[0], dict):
raise TypeError(
"Muon optimizer only supports a flat list of parameters, "
"not a list of parameter groups."
)
if grad_clip is not None and not isinstance(
grad_clip, GradientClipBase
):
raise TypeError(
"'grad_clip' should be an instance of GradientClipBase's derived class"
)
defaults = {
"momentum": momentum,
"adam_beta1": adam_beta1,
"adam_beta2": adam_beta2,
"weight_decay": weight_decay,
"ns_steps": ns_steps,
"nesterov": nesterov,
"epsilon": adam_epsilon,
"muon_version": muon_version,
"ns_coeff_type": ns_coeff_type,
}
super().__init__(
learning_rate=learning_rate,
parameters=parameters,
weight_decay=weight_decay,
grad_clip=grad_clip,
name=name,
)
self._multi_precision = multi_precision
self._master_weights = {}
self._lr_ratio = lr_ratio
self._apply_decay_param_fun = apply_decay_param_fun
self._muon_split_logged = False
self._muon_exclude_patterns = muon_exclude_patterns
self._muon_extra_scale_factor = muon_extra_scale_factor
self._ns_coeff_type = ns_coeff_type
if ns_coeff_type == "custom":
assert ns_coeffs is not None, (
"ns_coeffs must be provided when ns_coeff_type is 'custom'."
)
self._ns_coeffs = ns_coeffs
else:
assert ns_coeff_type in _NS_COEFFICIENT_SETS, (
f"Invalid ns_coeff_type: {ns_coeff_type}"
)
self._ns_coeffs = _NS_COEFFICIENT_SETS[ns_coeff_type]
self._muon_param_info_map = muon_param_info_map or {}
# Dtype for Newton-Schulz matmul.
# None = auto: bfloat16 on Ampere+ (capability >= 8.0), float32 on older.
if ns_matmul_dtype is None:
cap = (
paddle.device.cuda.get_device_capability()
if paddle.is_compiled_with_cuda()
else (0, 0)
)
self._ns_matmul_dtype = (
paddle.bfloat16 if cap[0] >= 8 else paddle.float32
)
else:
self._ns_matmul_dtype = ns_matmul_dtype
self._default_dict.update(defaults)
# ------------------------------------------------------------------
# Accumulator management
# ------------------------------------------------------------------
def _ensure_accumulators(self, param, use_muon, group):
"""Create optimizer accumulators for *param* if they do not exist yet."""
if (
self._moment_acc_str in self._accumulators
and param.name in self._accumulators[self._moment_acc_str]
):
return
# FP32 master weight for mixed-precision training
if self._multi_precision and self._is_dtype_fp16_or_bf16(param.dtype):
if param.name not in self._master_weights:
self._create_master_weight(param)
self._add_accumulator(
self._moment_acc_str,
param,
dtype=paddle.float32,
fill_value=0.0,
shape=param.shape,
type=framework.core.VarDesc.VarType.DENSE_TENSOR,
)
if not use_muon:
# AdamW-specific states
self._add_accumulator(
self._moment2_acc_str,
param,
dtype=paddle.float32,
fill_value=0.0,
shape=param.shape,
type=framework.core.VarDesc.VarType.DENSE_TENSOR,
)
for acc_name, init_val in [
(self._beta1_pow_acc_str, group.get("adam_beta1", 0.9)),
(self._beta2_pow_acc_str, group.get("adam_beta2", 0.95)),
]:
self._add_accumulator(
acc_name,
param,
dtype=paddle.float32,
fill_value=init_val,
shape=[1],
type=framework.core.VarDesc.VarType.DENSE_TENSOR,
)
def _create_accumulators(self, block, parameters):
"""Standard entry-point used by checkpoint-resume infrastructure."""
if isinstance(parameters, dict):
parameters = self._update_param_group(parameters)
for p in parameters:
param_info = self._muon_param_info_map.get(p.name)
if param_info is not None:
use_muon = param_info.use_muon
else:
use_muon = _default_should_use_muon(
p.name,
getattr(p, "original_shape", p.shape),
self._muon_exclude_patterns,
)
self._ensure_accumulators(p, use_muon, self._default_dict)
# ------------------------------------------------------------------
# Newton-Schulz orthogonalisation
# ------------------------------------------------------------------
@staticmethod
def _zeropower_via_newtonschulz5(
X,
steps=5,
eps=1e-9,
ns_coeffs=None,
ns_matmul_dtype=paddle.bfloat16,
):
"""Approximate the matrix sign function via Newton-Schulz iteration.
Args:
X: Input tensor to orthogonalize. Must be 2D (M, N) or
3D (B, M, N) for batched operation.
steps: Number of Newton-Schulz iterations.
eps: Small constant for numerical stability.
ns_coeffs: List of (a, b, c) coefficient tuples for iteration.
If None, uses the "simple" preset.
ns_matmul_dtype: Dtype for matmul iterations. Defaults to
bfloat16. Pass paddle.float32 for V100 compatibility.
"""
if X.ndim < 2 or X.ndim > 3:
raise ValueError(
f"Input tensor X must be 2D or 3D (batched), got {X.ndim}D"
)
coeff_sets = (
ns_coeffs
if ns_coeffs is not None
else _NS_COEFFICIENT_SETS["simple"]
)
if X.shape[-2] > X.shape[-1]:
X = paddle.transpose(
X,
perm=[1, 0] if X.ndim == 2 else [0, 2, 1],
)
transpose = True
else:
transpose = False
orig_shape = X.shape
X_flat = X.flatten(start_axis=-2)
X_flat = paddle.nn.functional.normalize(
X_flat, p=2, axis=-1, epsilon=eps
)
X = X_flat.reshape(orig_shape).astype(ns_matmul_dtype)
if X.ndim == 3:
ns_step_fn = Muon._batched_newton_schulz_step
else:
ns_step_fn = Muon._newton_schulz_step
for i in range(steps):
a, b, c = coeff_sets[i % len(coeff_sets)]
X = ns_step_fn(X, a, b, c)
if transpose:
X = paddle.transpose(X, perm=[1, 0] if X.ndim == 2 else [0, 2, 1])
return X
@staticmethod
def _newton_schulz_step(X, a, b, c):
"""Single Newton-Schulz iteration step for 2D input."""
A = paddle.matmul(X, X, transpose_y=True)
B = paddle.addmm(input=A, x=A, y=A, beta=b, alpha=c)
X = paddle.addmm(input=X, x=B, y=X, beta=a, alpha=1.0)
return X
@staticmethod
def _batched_newton_schulz_step(X, a, b, c):
"""Single Newton-Schulz iteration step for 3D batched input."""
A = paddle.matmul(X, X, transpose_y=True)
B = paddle.baddbmm(A, A, A, beta=b, alpha=c)
X = paddle.baddbmm(X, B, X, beta=a, alpha=1.0)
return X
@staticmethod
def _scaling_fn(orthogonal_update, version, extra_scale_factor=1.0):
"""Apply dimension-dependent scaling to the orthogonal update."""
din, dout = orthogonal_update.shape[-2], orthogonal_update.shape[-1]
if version == 1:
scale = max(1, dout / din) ** 0.5
elif version == 2:
scale = (dout / din) ** 0.5
else: # version == 3 (default)
scale = max(dout, din) ** 0.5
return orthogonal_update * scale * extra_scale_factor
# ------------------------------------------------------------------
# Per-parameter update rules
# ------------------------------------------------------------------
def _adamw_update(
self,
param,
grad,
lr,
moment1,
moment2,
beta1_pow,
beta2_pow,
beta1,
beta2,
epsilon,
weight_decay,
):
"""In-place AdamW update for 1-D sharded parameters."""
lr_ratio = 1.0 if self._lr_ratio is None else self._lr_ratio(param)
with_decay = True
if (
self._apply_decay_param_fun is not None
and not self._apply_decay_param_fun(param.name)
):
with_decay = False
find_master = param.name in self._master_weights
master_weight = (
self._master_weights[param.name] if find_master else None
)
_, _, _, _, _, _, _ = _C_ops.adamw_(
param,
grad,
lr,
moment1,
moment2,
None, # moment2_max
beta1_pow,
beta2_pow,
master_weight,
None, # found_inf
beta1,
beta2,
epsilon,
lr_ratio,
weight_decay,
with_decay,
False, # lazy_mode
1000,
find_master,
False,
False, # amsgrad
)
def _muon_update(
self,
param,
grad,
lr,
momentum_buffer,
momentum_beta,
ns_steps,
nesterov,
epsilon,
weight_decay,
version,
):
"""In-place Muon update for a 2D parameter tensor.
Applies Newton-Schulz orthogonalisation to the 2D weight matrix and
updates the parameter in-place. MuonShardingOptimizer assigns whole
2D tensors to ranks, so no sharding gather or TP communication is needed.
"""
param_shape = getattr(param, "original_shape", param.shape)
param_info = self._muon_param_info_map.get(param.name)
with paddle.no_grad():
grad_f32 = (
grad.astype(momentum_buffer.dtype)
if grad.dtype != momentum_buffer.dtype
else grad
)
# Step 1: Momentum update
new_momentum = paddle.lerp(
momentum_buffer, grad_f32, 1.0 - momentum_beta
)
paddle.assign(new_momentum, momentum_buffer)
update_buffer = (
paddle.lerp(grad_f32, momentum_buffer, momentum_beta)
if nesterov
else momentum_buffer
)
# Step 2: Reshape update buffer to 2D matrix.
# MuonShardingOptimizer assigns whole 2D tensors to ranks, so params
# are already 2D/3D (no sharding gather needed).
matrix_2d_global = update_buffer.reshape(param_shape)
# Shared NS + scaling closure (captures ns_steps, epsilon, version, ns_coeffs)
def ortho_fn(m):
ns_out = Muon._zeropower_via_newtonschulz5(
m,
steps=ns_steps,
eps=epsilon,
ns_coeffs=self._ns_coeffs,
ns_matmul_dtype=self._ns_matmul_dtype,
)
scaled = Muon._scaling_fn(
ns_out, version, self._muon_extra_scale_factor
)
return scaled
# Step 3: Newton-Schulz orthogonalisation
# Use split_concat_func from param_info if provided, otherwise default to whole matrix
if (
param_info is not None
and param_info.split_concat_func is not None
):
# Use slice function defined in model configuration
orthogonal_update = param_info.split_concat_func(
matrix_2d_global, ortho_fn
)
if MUON_DEBUG:
_global_rank = paddle.distributed.get_rank()
if _global_rank == 0:
_sf = param_info.split_concat_func
_logger.info(
f"[Muon] Using split_concat_func: param={param.name}, "
f"split_concat_func={_sf.func.__name__}, "
f"args={_sf.args}, kwargs={_sf.keywords}"
)
else:
# Default: whole matrix orthogonalisation
orthogonal_update = ortho_fn(matrix_2d_global)
find_master = param.name in self._master_weights
master_weight = (
self._master_weights[param.name] if find_master else None
)
with_decay = True
if (
self._apply_decay_param_fun is not None
and not self._apply_decay_param_fun(param.name)
):
with_decay = False
if with_decay and weight_decay > 0:
if find_master:
master_weight.scale_(1.0 - lr * weight_decay)
else:
param.scale_(1.0 - lr * weight_decay)
final_step = orthogonal_update * lr
if find_master:
master_weight.subtract_(final_step)
paddle.assign(master_weight.astype(param.dtype), param)
else:
param.subtract_(final_step.astype(param.dtype))
# ------------------------------------------------------------------
# Core optimization step
# ------------------------------------------------------------------
def _apply_optimize(self, loss, startup_program, params_grads):
if not framework.in_dygraph_mode():
raise NotImplementedError(
"Muon optimizer only supports dygraph mode."
)
if self._grad_clip is not None:
params_grads = self._grad_clip(params_grads)
# apply for zcc
self._maybe_refuse()
group = self._default_dict
lr = self._learning_rate
if isinstance(lr, paddle.optimizer.lr.LRScheduler):
lr = lr()
wd = group.get("weight_decay", 0.0)
muon_params = []
adamw_params = []
for param, grad in params_grads:
if grad is None:
continue
param_info = self._muon_param_info_map.get(param.name)
assert param_info is not None, (
f"muon_param_info_map does not have {param.name}"
)
use_muon = param_info.use_muon
self._ensure_accumulators(param, use_muon, group)
if use_muon:
muon_params.append((param, grad))
else:
adamw_params.append((param, grad))
# --- Pass 1: Muon updates (large temporary tensors) ---
lr_tensor = paddle.to_tensor(lr, dtype=paddle.float32)
lr_tensor_f64 = paddle.to_tensor(lr, dtype=paddle.float64)
for param, grad in muon_params:
self._muon_update(
param,
grad,
lr_tensor,
self._get_accumulator(self._moment_acc_str, param),
group.get("momentum", 0.95),
group.get("ns_steps", 5),
group.get("nesterov", True),
group.get("epsilon", 1e-9),
wd,
version=group.get("muon_version", 3),
)
# --- Pass 2: AdamW updates ---
for param, grad in adamw_params:
self._adamw_update(
param,
grad,
lr_tensor_f64,
self._get_accumulator(self._moment_acc_str, param),
self._get_accumulator(self._moment2_acc_str, param),
self._get_accumulator(self._beta1_pow_acc_str, param),
self._get_accumulator(self._beta2_pow_acc_str, param),
group.get("adam_beta1", 0.9),
group.get("adam_beta2", 0.95),
group.get("epsilon", 1e-9),
wd,
)
@framework.dygraph_only
def step(self) -> None:
params_grads = [
(param, param._grad_ivar())
for param in self._parameter_list
if not param.stop_gradient and param._grad_ivar() is not None
]
self._apply_optimize(
loss=None, startup_program=None, params_grads=params_grads
)
def sharded_state_dict(
self,
model_sharded_state_dict: ShardedStateDict,
) -> ShardedStateDict:
"""Build a sharded optimizer state dict for flex checkpoint save/load.
The layout mirrors :class:`paddle.optimizer.AdamW`'s implementation so
that the same ``dist.save_state_dict`` / ``dist.load_state_dict`` path
works for Muon checkpoints.
Args:
model_sharded_state_dict: Sharded model state dict produced by
``model.sharded_state_dict()``.
Returns:
A dict mapping ``"<struct_name>.<state_type>"`` keys to
:class:`ShardedWeight` objects.
"""
_FP32_MASTER = "fp32_master_0"
_optimizer_scalar_names = [
"beta1_pow_acc_0",
"beta2_pow_acc_0",
]
_optimizer_vector_names = [
"moment1_0",
"moment2_0",
]
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}"
)
model_sharded_state_dict = dict(
sorted(model_sharded_state_dict.items())
)
# Build static-name → struct-name mapping (handles shared weights)
static_to_struct = {}
for struct_name, sw in model_sharded_state_dict.items():
local_name = sw.local_tensor.name
if local_name not in static_to_struct:
static_to_struct[local_name] = struct_name
optimizer_state_dict = self.state_dict()
master_weights = optimizer_state_dict.pop("master_weights", None)
optimizer_state_dict.pop("LR_Scheduler", None)
sharded_state: ShardedStateDict = {}
# Optimizer states (moment1, moment2, beta_pow scalars)
for key, tensor in optimizer_state_dict.items():
static_name, state_type = _split_state_name(key)
struct_name = static_to_struct[static_name]
sharded_param = model_sharded_state_dict[struct_name]
unified_name = f"{struct_name}.{state_type}"
if state_type in _optimizer_vector_names:
# Vector states share the same sharding layout as the parameter
if 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:
# Reshape accumulator if numel matches but shape differs.
# MoE: grouped_gemm_experts param.shape is 3D
# [n_experts, H, I] but model.state_dict() returns actual
# C++ storage shape 2D [n_experts*H, I]. moment1 was
# created with 3D shape, so we need to reshape here.
# V2 is unaffected: its moments are always 1D shards,
# so shape always matches and reshape is never triggered.
target_shape = sharded_param.local_shape
if (
tuple(tensor.shape) != tuple(target_shape)
and tensor.numel()
== paddle.to_tensor(list(target_shape)).prod().item()
):
tensor = tensor.reshape(target_shape)
sharded_state[unified_name] = (
create_sharded_weight_with_new_local(
unified_name, tensor, sharded_param
)
)
else:
# Scalar states (beta_pow) are replicated save as-is
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():
struct_name = static_to_struct[weight_key]
sharded_param = model_sharded_state_dict[struct_name]
unified_name = f"{struct_name}.w_0"
if 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:
sharded_state[unified_name] = (
create_sharded_weight_with_new_local(
unified_name, tensor, sharded_param
)
)
return sharded_state