Files
paddlepaddle--paddle/python/paddle/incubate/distributed/models/moe/grad_clip.py
T
2026-07-13 12:40:42 +08:00

239 lines
9.1 KiB
Python

# Copyright (c) 2018 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.
import paddle
import paddle.distributed as dist
from paddle.autograd import no_grad
from paddle.framework import core
from paddle.nn import clip
from paddle.nn.clip import ClipGradBase, _squared_l2_norm
class ClipGradForMOEByGlobalNorm(ClipGradBase):
r"""
The Algorithm is the same as paddle.nn.ClipGradByGlobalNorm
Given a list of Tensor :math:`t\_list` , calculate the global norm for the elements of all tensors in
:math:`t\_list` , and limit it to ``clip_norm`` .
- If the global norm is greater than ``clip_norm`` , all elements of :math:`t\_list` will be compressed by a ratio.
- If the global norm is less than or equal to ``clip_norm`` , nothing will be done.
The list of Tensor :math:`t\_list` is not passed from this class, but the gradients of all parameters set in ``optimizer``.
If ``need_clip`` of specific param is ``False`` in its ``ParamAttr``, then the gradients of this param will not be clipped.
Gradient clip will takes effect after being set in ``optimizer`` , see the document ``optimizer``
(for example: :ref:`api_paddle_optimizer_SGD`).
The clipping formula is:
.. math::
t\_list[i] = t\_list[i] * \frac{clip\_norm}{\max(global\_norm, clip\_norm)}
where:
.. math::
global\_norm = \sqrt{\sum_{i=0}^{N-1}(l2norm(t\_list[i]))^2}
Note:
``need_clip`` of ``ClipGradyGlobalNorm`` HAS BEEN DEPRECATED since 2.0.
Please use ``need_clip`` in ``ParamAttr`` to specify the clip scope.
Reference:
https://github.com/laekov/fastmoe/blob/master/examples/megatron/clip-grad-v2.2.patch
Git commit hash: 295a615aacce7e54a37e7935274ba15e901c78e4
Args:
clip_norm (float): The maximum norm value.
is_expert_param_func (function): a function to decide whether a param should be put into moe_params_grads
moe_group (Group): group for moe experts communication.
group_name (str, optional): The group name for this clip. Default value is ``default_moe_group``.
Examples:
.. code-block:: pycon
>>> import paddle
>>> x = paddle.uniform([10, 10], min=-1.0, max=1.0, dtype='float32')
>>> linear = paddle.nn.Linear(
... in_features=10,
... out_features=10,
... weight_attr=paddle.ParamAttr(need_clip=True),
... bias_attr=paddle.ParamAttr(need_clip=False),
... )
>>> out = linear(x)
>>> loss = paddle.mean(out)
>>> loss.backward()
>>> clip = paddle.nn.ClipGradByGlobalNorm(
... clip_norm=1.0
... ) # Cause paddle.nn hasn't this interface, so we use ClipGradByGlobalNorm here.
>>> sdg = paddle.optimizer.SGD(learning_rate=0.1, parameters=linear.parameters(), grad_clip=clip)
>>> sdg.step()
"""
def __init__(
self,
clip_norm,
is_expert_param_func=None,
moe_group=None,
group_name="default_moe_group",
):
super().__init__()
self.clip_norm = float(clip_norm)
self.group_name = group_name
self.moe_group = moe_group
if moe_group is not None and moe_group.nranks > 1:
assert is_expert_param_func is not None, (
"When moe group size > 1, a function for selecting expert params must be specified."
)
self.is_expert_param_func = is_expert_param_func
def __str__(self):
return f"Gradient Clip By GlobalNorm, global_norm={self.clip_norm:f}"
@staticmethod
def get_l2_norm_pow(params_grads, sum_dtype=None):
sum_square_list = []
sum_square_list_fp16 = []
sum_square_list_fp32 = []
for p, g in params_grads:
if g is None:
continue
if getattr(p, 'need_clip', True) is False:
continue
merge_grad = g
if g.type == core.VarDesc.VarType.SELECTED_ROWS:
merge_grad = clip.merge_selected_rows(g)
merge_grad = clip.get_tensor_from_selected_rows(merge_grad)
sum_square = _squared_l2_norm(merge_grad)
if sum_square.dtype == paddle.float16:
sum_square_list_fp16.append(sum_square)
elif sum_square.dtype == paddle.float32:
sum_square_list_fp32.append(sum_square)
else:
sum_square_list.append(sum_square)
# all parameters have been filtered out
if (
len(sum_square_list)
+ len(sum_square_list_fp16)
+ len(sum_square_list_fp32)
== 0
):
return None, None
assert sum_dtype in [
"float64",
"float32",
None,
], "sum's type must be float64/ float32 / None"
if sum_dtype != "float64":
sum_dtype = 'float64' if len(sum_square_list) > 0 else "float32"
global_norm_var = []
if len(sum_square_list_fp16) > 0:
global_norm_var_fp16 = paddle.add_n(sum_square_list_fp16)
global_norm_var.append(global_norm_var_fp16.astype(sum_dtype))
if len(sum_square_list_fp32) > 0:
global_norm_var_fp32 = paddle.add_n(sum_square_list_fp32)
if sum_dtype == 'float32':
global_norm_var.append(global_norm_var_fp32)
else:
global_norm_var.append(global_norm_var_fp32.astype(sum_dtype))
if len(sum_square_list) > 0:
global_norm_var_fp64 = paddle.add_n(sum_square_list)
global_norm_var.append(global_norm_var_fp64)
global_norm_var = paddle.add_n(global_norm_var)
return global_norm_var, sum_dtype
@no_grad()
def _dygraph_clip(self, params_grads):
normal_params_grads = []
moe_params_grads = []
# separate moe params from normal params
if self.moe_group is not None and self.moe_group.nranks > 1:
for p, g in params_grads:
if self.is_expert_param_func(p):
moe_params_grads.append((p, g))
else:
normal_params_grads.append((p, g))
else:
normal_params_grads = params_grads
# why to return sum_dtype?
# we will call `get_l2_norm_pow` twice and the precisions may be different.
# For convenience and simplification, we use sum_dtype directly instead of global_norm_var_normal.dtype
global_norm_var_normal, sum_dtype = self.get_l2_norm_pow(
normal_params_grads
)
global_norm_var_moe = None
if len(moe_params_grads) > 0:
global_norm_var_moe, _ = self.get_l2_norm_pow(
moe_params_grads, sum_dtype
)
if global_norm_var_moe is not None:
dist.all_reduce(
global_norm_var_moe,
op=dist.ReduceOp.SUM,
group=self.moe_group,
)
if global_norm_var_normal is None and global_norm_var_moe is None:
return params_grads
elif global_norm_var_normal is None:
global_norm_var = global_norm_var_moe
elif global_norm_var_moe is None:
global_norm_var = global_norm_var_normal
else:
if global_norm_var_normal.dtype != global_norm_var_moe.dtype:
# compared with normal norm, moe norm is the later one,
# so its precision is no lower than normal norm
global_norm_var_normal = global_norm_var_normal.astype(
global_norm_var_moe.dtype
)
global_norm_var = global_norm_var_normal + global_norm_var_moe
params_and_grads = []
global_norm_var = paddle.sqrt(global_norm_var)
max_global_norm = paddle.full(
shape=[1], dtype=global_norm_var.dtype, fill_value=self.clip_norm
)
clip_var = paddle.divide(
x=max_global_norm,
y=paddle.maximum(x=global_norm_var, y=max_global_norm),
)
for p, g in params_grads:
if g is None:
continue
if getattr(p, 'need_clip', True) is False:
params_and_grads.append((p, g))
continue
# TODO(wangxi): use inplace elementwise_mul
clip_input = (
clip_var.astype('float16')
if g.dtype == paddle.float16
else clip_var
)
new_grad = paddle.multiply(x=g, y=clip_input)
params_and_grads.append((p, new_grad))
return params_and_grads
ClipGradByGlobalNorm = ClipGradForMOEByGlobalNorm