Files
2026-07-13 12:40:42 +08:00

1133 lines
37 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.
import enum
import itertools
import os
import weakref
from collections import OrderedDict
import numpy as np
import paddle
from paddle.framework import (
_current_expected_place_,
base as imperative_base,
core,
)
from paddle.framework.recall_error import check_naninf
from paddle.utils import strtobool
from .log_util import logger
def _share_tensor_ipc_meta(tensor):
if tensor is None:
return None
if paddle.is_compiled_with_xpu():
return tensor.value().get_tensor()._share_xpu()
if core.is_compiled_with_cuda() and not core.is_compiled_with_rocm():
return tensor.value().get_tensor()._share_cuda()
return None
class HOOK_ACTION:
ALL_REDUCE = 0
REDUCE = 1
REDUCE_SCATTER = 2
alignment = {
"gpu": 256,
"npu": 256,
"xpu": 256,
}
align = {
paddle.float16: 2,
paddle.bfloat16: 2,
paddle.float32: 4,
}
__current_device_type__ = None
def get_current_device_type():
global __current_device_type__
if __current_device_type__ is None:
if paddle.is_compiled_with_cuda():
device_type = "gpu"
elif paddle.is_compiled_with_xpu():
device_type = "xpu"
else:
current_device = _current_expected_place_()
try:
device_type = current_device.get_device_type()
except:
device_type = "unknown"
assert device_type in alignment.keys(), (
f"tensor fusion helper now only support {alignment.keys()}, but got device {device_type} instead."
)
__current_device_type__ = device_type
return __current_device_type__
def assign_group_by_size(parameters, group_size=128 * 1024 * 1024):
is_sparse_gradient = [False] * len(parameters)
group_indices = core.eager_assign_group_by_size(
parameters, is_sparse_gradient, [group_size, group_size]
)
var_groups = OrderedDict()
group_msg = []
for group_idx, indices in enumerate(group_indices):
group_size = 0
for index in indices:
var_groups.setdefault(group_idx, []).append(parameters[index])
group_size += np.prod(parameters[index].shape)
dtype = parameters[indices[0]].dtype
bytes = group_size * core.size_of_dtype(dtype)
msg = f"group_{group_idx}: {bytes / 1024**2:.4f} MB, dtype: {dtype!s}"
group_msg.append(msg)
logger.info(f"Tensor Fusion Group Info:\n{group_msg}\n")
return var_groups
def get_group_size(parameters, group_size=128 * 1024 * 1024):
is_sparse_gradient = [False] * len(parameters)
group_indices = core.eager_assign_group_by_size(
parameters, is_sparse_gradient, [group_size, group_size]
)
opt_states_sizes = []
for group_idx, indices in enumerate(group_indices):
group_size = 0
for index in indices:
group_size += np.prod(parameters[index].shape)
dtype = parameters[indices[0]].dtype
bytes = group_size * core.size_of_dtype(dtype)
param_size_G = bytes / 1024**3
opt_states_size_G = param_size_G * 12 / core.size_of_dtype(dtype)
opt_states_sizes.append(opt_states_size_G)
return opt_states_sizes
def flatten_dense_tensors(
parameters,
use_main_grad=False,
fuse_param=True,
warp_buffer=False,
release_grad=False,
):
from paddle.distributed.fleet.meta_parallel.sharding.group_sharded_storage import (
GradStorage,
ParamStorage,
)
_buffer_size = 0
_param2align = {}
_param2offset = {}
dtype = parameters[0].dtype
for param in parameters:
assert param.trainable, "param must be trainable..."
size = np.prod(param.shape) * align[dtype]
remaining = size % alignment[get_current_device_type()]
ali = (
0
if remaining == 0
else alignment[get_current_device_type()] - remaining
)
align_ = ali // align[dtype]
_param2offset[param.name] = _buffer_size
_buffer_size += np.prod(param.shape) + align_
_param2align[param.name] = align_
if release_grad:
return None, _buffer_size, _param2offset
if fuse_param:
param_storage = ParamStorage(
size=_buffer_size, dtype=dtype, device=get_current_device_type()
)
param_storage.add_rank_params(parameters, _param2align)
# process gradient
grad_dtype = paddle.float32 if use_main_grad else dtype
grad_storage = GradStorage(
size=_buffer_size,
dtype=grad_dtype,
device=get_current_device_type(),
destination="0",
param2align=_param2align,
)
for param in parameters:
grad_storage.add_grad(param, _param2align[param.name])
if warp_buffer:
if fuse_param:
param_storage.warp_buffer()
grad_storage.warp_buffer()
outputs = (grad_storage,)
if fuse_param:
if not use_main_grad:
# param_storage --> grad_storage
param_storage.buffer._copy_gradient_from(grad_storage.buffer)
else:
param_storage.buffer.main_grad = grad_storage.buffer
param_storage.buffer.stop_gradient = False
outputs = (param_storage, *outputs)
if release_grad:
outputs = (*outputs, _buffer_size, _param2offset)
return outputs
def bw_hook_func(buffer, param):
@paddle.autograd.no_grad()
def fused_comm(*_):
buffer.add_grad(param)
return fused_comm
class ShardingGradView:
def __init__(
self,
param,
param_buffer,
grad_buffer,
index,
padded_size,
sharding_degree,
rank,
use_main_grad=False,
release_grad=False,
):
self._param = param
self._param_buffer = param_buffer
self._grad_buffer = grad_buffer
self._index = index
self._padded_size = padded_size
self._sharding_degree = sharding_degree
self._rank = rank
self._use_main_grad = use_main_grad
self._release_grad = release_grad
shard_size = param_buffer._numel() // sharding_degree
rank_begin = max(rank, 0) * shard_size
rank_end = rank_begin + shard_size
param_begin = max(self._index, rank_begin)
param_end = min(self._index + self._padded_size, rank_end)
self._param_begin = param_begin
self._param_end = param_end
self._rank_begin = rank_begin
self._slice_grad = None
if not self._release_grad:
self._link_grad_to_buffer()
# share param buffer
self._share_param_buffer()
def _get_padding(self):
if self._param_begin < self._param_end and self._slice_grad is not None:
padding_start = self._index + self._param._numel()
padding_end = self._index + self._padded_size
padding_start = max(self._param_begin, padding_start)
padding_end = min(self._param_end, padding_end)
if padding_start >= padding_end:
return None
padding = padding_end - padding_start
grad_numel = self._slice_grad._numel()
assert grad_numel >= padding, f"{grad_numel} vs {padding}"
padding_grad = self._slice_grad._slice(
grad_numel - padding, grad_numel
)
return padding_grad
else:
return None
def _slice_grad_from_buffer(self):
assert self._grad_buffer is not None
if self._param_begin < self._param_end:
self._slice_grad = self._grad_buffer._slice(
self._param_begin, self._param_end
)
tmp_grad = self._grad_buffer._slice(
self._index, self._index + self._param._numel()
)
return tmp_grad
def _link_grad_to_buffer(self):
tmp_grad = self._slice_grad_from_buffer()
tmp_grad.get_tensor()._set_dims(self._param.shape)
if not self._use_main_grad:
self._param._copy_gradient_from(tmp_grad)
else:
self._param.main_grad = tmp_grad
def _share_param_buffer(self):
param_shape = self._param.shape
stop_gradient = self._param.stop_gradient
self._param.stop_gradient = True
self._param.flatten_()
paddle.assign(
self._param,
self._param_buffer._slice(
self._index, self._index + self._param._numel()
),
)
self._param.get_tensor()._set_dims(param_shape)
self._param.stop_gradient = stop_gradient
self._param_buffer._slice(
self._index, self._index + self._param._numel()
)._share_buffer_to(self._param)
def fill_slice_param(self, slice_param):
slice_begin = self._param_begin
slice_end = self._param_end
if slice_param._is_initialized():
assert self._param_buffer._is_shared_buffer_with(slice_param)
assert len(slice_param.shape) == 1
assert slice_param.shape[0] == (slice_end - slice_begin)
slice_begin = self._param_begin
slice_end = self._param_end
slice_buffer = self._param_buffer._slice(slice_begin, slice_end)
slice_buffer._share_buffer_to(slice_param)
slice_param.get_tensor()._set_dims([slice_end - slice_begin])
def assign_slice_grad(self, slice_param):
assert self._param_buffer._is_shared_buffer_with(self._param)
slice_grad = self._slice_grad
if slice_grad is None:
return
self.fill_slice_param(slice_param)
if hasattr(self._param, "main_grad"):
if not hasattr(slice_param, "main_grad"):
slice_param.main_grad = slice_grad
else:
assert slice_param.main_grad is slice_grad
elif slice_grad is not None:
if slice_param.grad is None:
slice_param._copy_gradient_from(slice_grad)
else:
assert slice_param.grad._is_shared_buffer_with(slice_grad)
def _clear_param_buffer(self):
self._param._clear_to_zero_allocation()
self._param_buffer._clear_to_zero_allocation()
def _reset_param_buffer(self, new_param_storage):
new_param = paddle.empty_like(self._param)
new_param._share_buffer_to(self._param)
new_param_storage._share_buffer_to(self._param_buffer)
self._share_param_buffer()
def _clear_grad_buffer(self):
if self._slice_grad is not None:
self._slice_grad._clear_dataptr()
self._slice_grad = None
if self._grad_buffer is not None:
self._grad_buffer._clear_dataptr()
self._grad_buffer = None
def _reset_grad_buffer(self, slice_grad_buffer):
self._clear_grad_buffer()
self._grad_buffer = slice_grad_buffer
if self._param_begin < self._param_end:
self._slice_grad = self._grad_buffer._slice(
self._param_begin - self._rank_begin,
self._param_end - self._rank_begin,
)
@property
def has_effective_slice_param(self):
return self._param_begin < self._param_end
def build_reduce_scatter_buffer(
parameters,
sharding_degree,
rank,
use_main_grad=False,
release_grad=False,
init_slice_param=False,
slice_params={},
):
total_buffer_size = 0
param2index = {}
dtype = parameters[0].dtype
def get_padded_size(param):
size = np.prod(param.shape)
align_size = alignment[get_current_device_type()] // align[dtype]
align_size = align_size * sharding_degree
padded_size = ((size + align_size - 1) // align_size) * align_size
return padded_size
for param in parameters:
assert param.trainable, "param must be trainable..."
param2index[param.name] = total_buffer_size
total_buffer_size += get_padded_size(param)
grad_dtype = paddle.float32 if use_main_grad else dtype
param_buffer = paddle.zeros(shape=[total_buffer_size], dtype=dtype)
param_buffer_ipc_meta = _share_tensor_ipc_meta(param_buffer)
grad_buffer = (
paddle.zeros(shape=[total_buffer_size], dtype=grad_dtype)
if not release_grad
else None
)
sharding_grad_view = {}
for param in parameters:
padded_size = get_padded_size(param)
grad_view = ShardingGradView(
param,
param_buffer,
grad_buffer,
param2index[param.name],
padded_size,
sharding_degree,
rank,
use_main_grad,
release_grad,
)
if init_slice_param and grad_view.has_effective_slice_param:
assert param.name in slice_params
grad_view.fill_slice_param(slice_params[param.name])
# hack main_grad
sharding_grad_view[param.name] = grad_view
return (
sharding_grad_view,
total_buffer_size,
param_buffer,
grad_buffer,
param_buffer_ipc_meta,
)
def get_grad_address(param, use_main_grad):
addr = None
if use_main_grad:
if param.main_grad is not None:
addr = param.main_grad.data_ptr()
else:
if (param.grad is not None) and param.grad._is_initialized():
addr = param.grad.data_ptr()
return addr
class FusedCommBuffer:
class Status(enum.Enum):
"""Status of this bucket, Only useful when param allgather overlap is enabled"""
# Parameters are sharded between processes
SHARDED = enum.auto()
# Asynchronous communication is in progress
SYNCING = enum.auto()
# Parameters are ready to use
READY = enum.auto()
def __init__(
self,
id,
params,
comm_group,
acc_steps=1,
act=None,
dst=-1,
use_main_grad=None,
fuse_param=False,
scale_after_comm=True,
release_grads=False,
use_reduce_avg=False,
free_grads_in_comm=False,
init_slice_param=False,
slice_params={},
):
self._id = id
self._params = params
self._acc_steps = acc_steps
self._comm_group = comm_group
self._scale_after_comm = scale_after_comm
self._fuse_param = fuse_param
self._release_grads = release_grads
self._use_reduce_avg = use_reduce_avg
self._free_grads_in_comm = free_grads_in_comm
self._log_message_printed = False
self.status = FusedCommBuffer.Status.READY
self.sync_param_task = None
if self._free_grads_in_comm:
assert acc_steps == 1, (
f"No need to use free_grads_in_comm when acc_steps `{acc_steps}` != 1"
)
assert act == HOOK_ACTION.REDUCE_SCATTER, (
"Currently, only support reduce_scatter"
)
assert release_grads, "Currently, only support release_grads"
assert not (self._fuse_param and self._release_grads), (
"It's not supported when using fuse_param and release_grad at the same time."
)
self.use_main_grad = (
use_main_grad
if use_main_grad is not None
else hasattr(self._params[0], "main_grad")
)
self._task = None
self._dtype = (
paddle.float32 if self.use_main_grad else self._params[0].dtype
)
self._params_step_dict = {}
self._params_checked_in = 0
self._grads_to_addr = {}
self._param_buffer_meta_tensor = None
self._act = act
if self._act == HOOK_ACTION.ALL_REDUCE:
assert dst == -1
elif self._act == HOOK_ACTION.REDUCE_SCATTER:
assert dst == -1
elif self._act == HOOK_ACTION.REDUCE:
assert dst != -1
else:
raise ValueError(
"The act should be allreduce for dp or reduce for sharding."
)
self._dst = dst
self._init_step_dict()
if self._act != HOOK_ACTION.REDUCE_SCATTER:
if self._fuse_param:
self.param_storage, self.grad_storage = flatten_dense_tensors(
self._params,
use_main_grad=use_main_grad,
fuse_param=True,
warp_buffer=True,
)
self.param_storage = self.param_storage.buffer
self.grad_storage = self.grad_storage.buffer
elif self._release_grads:
self.param_storage = None
(
grad_storage,
self.buffer_size,
self.param2offset,
) = flatten_dense_tensors(
self._params,
use_main_grad=self.use_main_grad,
fuse_param=False,
warp_buffer=False,
release_grad=True,
)
self.grad_storage = (
None if grad_storage is None else grad_storage.buffer
)
else:
self.param_storage = None
self.grad_storage = flatten_dense_tensors(
self._params,
use_main_grad=self.use_main_grad,
fuse_param=False,
warp_buffer=False,
)[0].buffer
else:
assert not self._fuse_param, "not supported"
(
self._sharding_param_grad_view,
self.buffer_size,
self.param_storage,
self.grad_storage,
_,
) = build_reduce_scatter_buffer(
self._params,
self._comm_group.nranks,
self._comm_group.rank,
use_main_grad=self.use_main_grad,
release_grad=self._release_grads,
init_slice_param=init_slice_param,
slice_params=slice_params,
)
# hack, for parameter sync in dygraph sharding optimizer after step
self._params[0].comm_buffer_ref = weakref.ref(self)
self._param_buffer_meta_tensor = self.param_storage
if not self._release_grads:
self._record_addr()
def _refresh_param_buffer_ipc_meta(self):
if self._param_buffer_meta_tensor is None:
return None
return _share_tensor_ipc_meta(self._param_buffer_meta_tensor)
@property
def param_buffer_ipc_meta(self):
return self._refresh_param_buffer_ipc_meta()
def _record_addr(self):
for param in self._params:
self._grads_to_addr[param.name] = get_grad_address(
param, self.use_main_grad
)
def _clear_param_storage(self):
self.param_storage._clear_to_zero_allocation()
for param in self._params:
self._sharding_param_grad_view[param.name]._clear_param_buffer()
def _reset_param_storage(self):
new_param_storage = paddle.empty_like(self.param_storage)
new_param_storage._share_buffer_to(self.param_storage)
for param in self._params:
grad_view = self._sharding_param_grad_view[param.name]
grad_view._reset_param_buffer(new_param_storage)
def _clear_grad_storage(self):
self.grad_storage._clear_dataptr()
self.grad_storage = None
if self._act == HOOK_ACTION.REDUCE_SCATTER:
for param in self._params:
self._sharding_param_grad_view[param.name]._clear_grad_buffer()
def _reset_grad_storage(self, slice_grad_buffer):
self._clear_grad_storage()
for param in self._params:
self._sharding_param_grad_view[param.name]._reset_grad_buffer(
slice_grad_buffer
)
self.grad_storage = slice_grad_buffer
def _init_step_dict(self):
for p in self._params:
self._params_step_dict[p.name] = 0
def _copy_grad_to_buffer(self, param):
if self._params_step_dict[param.name] > 0:
return
if self.grad_storage is None:
assert self._params_step_dict[param.name] == 0
self.grad_storage = paddle.zeros(
[self.buffer_size], dtype=self._dtype
)
if self._act == HOOK_ACTION.REDUCE_SCATTER:
self._sharding_param_grad_view[
param.name
]._grad_buffer = self.grad_storage
tmp_var = self._sharding_param_grad_view[
param.name
]._slice_grad_from_buffer()
else:
grad_end = self.param2offset[param.name] + np.prod(param.shape)
assert grad_end <= self.buffer_size
tmp_var = self.grad_storage._slice(
self.param2offset[param.name], grad_end
)
grad_var = param.main_grad if self.use_main_grad else param.grad
if grad_var is not None:
grad_var.stop_gradient = True
grad_var.flatten_()
tmp_var.add_(grad_var)
grad_var._clear()
tmp_var.get_tensor()._set_dims(param.shape)
if self.use_main_grad:
if not self._free_grads_in_comm:
param.main_grad = tmp_var
param.main_grad.name = "main_grad@" + param.name
else:
if not self._free_grads_in_comm:
param._copy_gradient_from(tmp_var)
# record address for the following `acc_steps - 1` steps.
self._grads_to_addr[param.name] = get_grad_address(
param, self.use_main_grad
)
def _reset_params_checked_in(self):
self._task = None
self._init_step_dict()
self._params_checked_in = 0
@property
def _all_params_checked_in(self):
return (
len(self._params) == self._params_checked_in
and len(self._params_step_dict) == 0
)
def add_grad(self, param, use_comm=True):
assert param.name in self._params_step_dict
if not self._release_grads or self._params_step_dict[param.name] > 0:
current_ptr = get_grad_address(param, self.use_main_grad)
if self._grads_to_addr[param.name] != current_ptr:
error_message = f"The address of the grad/main_grad of param {param.name} has been changed during training, which is not allowed for dp/sharding overlap with pp. This may be caused by some non-inplace operations on the grad/main_grad. Here are some examples: 1. The grad/main_grad of the param is changed by other operations, such as: clear_grad; 2. Using non-inplace operations on the grad/main_grad, such as: add, sub, mul, div, etc."
logger.error(error_message)
raise ValueError(error_message)
else:
# When release_grads is enabled, fusing of gradients only happen
# in the 0-th gradient accumulation step, and remain unchanged for
# the following `acc_steps - 1` steps.
self._copy_grad_to_buffer(param)
self._params_step_dict[param.name] += 1
if self._params_step_dict[param.name] == self._acc_steps:
self._params_checked_in += 1
self._params_step_dict.pop(param.name)
if self._all_params_checked_in and use_comm:
self.comm_grads()
@imperative_base.no_grad
def assign_slice_grad(self, param, slice_param):
assert self._act == HOOK_ACTION.REDUCE_SCATTER
assert param.name in self._sharding_param_grad_view
grad_view = self._sharding_param_grad_view[param.name]
grad_view.assign_slice_grad(slice_param)
@imperative_base.no_grad
def sync_params(self, sync=True, param2task={}):
if not self.need_reduce_scale_sync():
return
assert self._act == HOOK_ACTION.REDUCE_SCATTER
full_buffer = self.param_storage
group = self._comm_group
shard_size = full_buffer._numel() // group.nranks
begin = shard_size * max(group.rank, 0)
end = begin + shard_size
slice_buffer = full_buffer._slice(begin, end)
if group.nranks == 1:
return
if sync:
# default sync_op is False, so we need to wait here.
# this will call distributed_py.cc in paddle. In distributed_py.cc, there defines two all gather function, their parameters are different.
group.process_group.all_gather(slice_buffer, full_buffer).wait()
else:
# default sync_op is False, so we don't need to to set sync_op = false here.
task = group.process_group.all_gather(slice_buffer, full_buffer)
self.sync_param_task = task
for param in self.params:
assert param.name not in param2task
param2task[param.name] = task
@property
def params(self):
return self._params
@imperative_base.no_grad
def comm_grads(self):
assert self._all_params_checked_in, (
"Not all params checked in."
f"Parameter number: {len(self._params)}, Check-in number: {self._params_checked_in}"
)
self._comm_grads()
def need_reduce_scale_sync(self):
stop_gradient_values = [param.stop_gradient for param in self.params]
if all(stop_gradient_values):
return False
else:
if any(stop_gradient_values) and not self._log_message_printed:
logger.info(
"There is at least one parameter whose stop_gradient attribute is True"
)
self._log_message_printed = True
return True
@imperative_base.no_grad
def _comm_grads(self):
if not self.need_reduce_scale_sync():
return
reduce_op = (
paddle.distributed.ReduceOp.AVG
if self._use_reduce_avg
else paddle.distributed.ReduceOp.SUM
)
# scale will be skipped when reduce_avg comm operation is enabled.
if not self._scale_after_comm and not self._use_reduce_avg:
scale_factor = 1.0 / self._comm_group.nranks
self.grad_storage.scale_(scale_factor)
need_check = strtobool(os.getenv('FLAGS_pp_check_naninf', '0'))
if need_check:
err_msg = check_naninf(self.grad_storage)
if err_msg is not None:
raise ValueError(
f"{err_msg}. Tensor contains inf or nan values at rank {paddle.distributed.get_rank()} before gradient communication"
)
if self._act == HOOK_ACTION.ALL_REDUCE:
task = paddle.distributed.all_reduce(
self.grad_storage,
op=reduce_op,
group=self._comm_group,
sync_op=False,
)
elif self._act == HOOK_ACTION.REDUCE:
task = paddle.distributed.reduce(
self.grad_storage,
dst=self._dst,
op=reduce_op,
group=self._comm_group,
sync_op=False,
)
elif self._act == HOOK_ACTION.REDUCE_SCATTER:
# In align mode, we scale the grad in advance, so we need a SUM head
if paddle.distributed.in_auto_parallel_align_mode():
reduce_op = paddle.distributed.ReduceOp.SUM
shard_size = self.grad_storage._numel() // self._comm_group.nranks
begin = shard_size * max(self._comm_group.rank, 0)
end = begin + shard_size
reduce_scattered = (
paddle.empty_like(self.grad_storage._slice(begin, end))
if self._free_grads_in_comm
else self.grad_storage._slice(begin, end)
)
task = paddle.distributed.reduce_scatter(
reduce_scattered,
self.grad_storage,
op=reduce_op,
group=self._comm_group,
sync_op=False,
)
if self._free_grads_in_comm:
self._reset_grad_storage(reduce_scattered)
self._task = task
@imperative_base.no_grad
def scale_grads(self):
if self.need_reduce_scale_sync():
if self._comm_group.nranks == 1 and self._task is None:
self._reset_params_checked_in()
return
assert self._task is not None, "Task is not initialized."
self._task.wait()
# scale will be skipped when use reduce_avg comm operation
if self._scale_after_comm and not self._use_reduce_avg:
scale_factor = 1.0 / self._comm_group.nranks
self.grad_storage.scale_(scale_factor)
self._reset_params_checked_in()
def obtain_storage(
parameters,
use_main_grad=False,
clip=True,
dist=False,
fuse_param=True,
comm_overlap=False,
act=None,
comm_group=None,
dst=-1,
acc_steps=1,
scale_after_comm=False,
use_reduce_avg=False,
group_size=256 * 1024 * 1024,
):
if len(parameters) < 1:
return [], []
var_groups = assign_group_by_size(parameters, group_size=group_size)
storage = []
buffers = []
for group_idx, parameters in var_groups.items():
comm_buffer = FusedCommBuffer(
group_idx,
parameters,
comm_group=comm_group,
acc_steps=acc_steps,
act=act,
dst=dst,
use_main_grad=use_main_grad,
fuse_param=fuse_param,
scale_after_comm=scale_after_comm,
use_reduce_avg=use_reduce_avg,
)
if fuse_param:
param_buffer = comm_buffer.param_storage
param_buffer.need_clip = clip
param_buffer.is_distributed = dist
storage.append(param_buffer)
if comm_overlap:
for param in parameters:
param._register_backward_hook(bw_hook_func(comm_buffer, param))
buffers.append(comm_buffer)
return storage, buffers
def filter_params(params, is_fp32, is_distributed, need_clip):
params = list(
filter(
lambda x: (
x.is_distributed if is_distributed else (not x.is_distributed)
),
params,
)
)
params = list(
filter(
lambda x: (
getattr(x, 'need_clip', True)
if need_clip
else (not getattr(x, 'need_clip', True))
),
params,
)
)
params = list(
filter(
lambda x: (
x.dtype == paddle.float32
if is_fp32
else x.dtype != paddle.float32
),
params,
)
)
dtype = None
for p in params:
if dtype is None:
dtype = p.dtype
else:
assert dtype == p.dtype
return params, dtype
def _fused_parameters_impl(
parameters,
use_main_grad=False,
fuse_param=True,
comm_overlap=False,
comm_group=None,
act=None,
dst=-1,
acc_step=1,
scale_after_comm=False,
apply_decay_param_fun=None,
use_reduce_avg=False,
group_size=256 * 1024 * 1024,
):
param_groups = []
attrs = []
is_fp32 = [True, False]
is_distributed = [True, False]
need_clip = [True, False]
no_fp32_dtype = None
for fp32, dist, clip in itertools.product(
is_fp32, is_distributed, need_clip
):
params, dtype = filter_params(parameters, fp32, dist, clip)
if not fp32:
if no_fp32_dtype is None:
no_fp32_dtype = dtype
elif dtype is not None:
assert no_fp32_dtype == dtype
attrs.append([dtype, dist, clip])
param_groups.append(params)
decay_fused = []
all_fused = []
all_buffers = []
for params, attr in zip(param_groups, attrs):
decay_params = []
other_params = []
for param in params:
if apply_decay_param_fun is not None and apply_decay_param_fun(
param.name
):
decay_params.append(param)
else:
other_params.append(param)
is_distributed = attr[1]
need_clip = attr[2]
decay, decay_buffers = obtain_storage(
decay_params,
use_main_grad=use_main_grad,
clip=need_clip,
dist=is_distributed,
fuse_param=fuse_param,
comm_overlap=comm_overlap,
act=act,
comm_group=comm_group,
dst=dst,
acc_steps=acc_step,
scale_after_comm=scale_after_comm,
use_reduce_avg=use_reduce_avg,
group_size=group_size,
)
other, other_buffers = obtain_storage(
other_params,
fuse_param=fuse_param,
comm_overlap=comm_overlap,
use_main_grad=use_main_grad,
clip=need_clip,
dist=is_distributed,
act=act,
comm_group=comm_group,
dst=dst,
acc_steps=acc_step,
scale_after_comm=scale_after_comm,
use_reduce_avg=use_reduce_avg,
group_size=group_size,
)
decay_fused += decay
all_fused += decay
all_fused += other
all_buffers += decay_buffers
all_buffers += other_buffers
return decay_fused, all_fused, all_buffers
def fused_parameters(
parameters,
use_main_grad=False,
fuse_param=True,
comm_overlap=False,
comm_group=None,
act=None,
dst=-1,
acc_step=1,
scale_after_comm=False,
group_params=False,
apply_decay_param_fun=None,
use_reduce_avg=False,
group_size=256 * 1024 * 1024,
):
"""
Fuse gradients. Fuse parameters if be enabled. Prepare for comm overlap if be enabled.
:param parameters: all parameters to be fused.
:param use_main_grad: does the gradient use main grad or not
:param comm_overlap: enable comm overlap or not
:param comm_group: the comm group for comm overlap
:param act: the comm operation, could be chosen from reduce and allreduce
:param dst: the dst for comm overlap
:param acc_step: acc steps, using for comm overlap
:param fuse_param: fuse param or not
:param scale_after_comm: if enable comm overlap, specify the location of grad scale
:param group_params: the format of the input parameters is param group
:param apply_decay_param_fun: the function to filter decay param
:param use_reduce_avg: use reduce_avg comm operation instead of scale and reduce_sum
:param group_size: the size of each group, default is 256MB
:return: param storage if fused, comm buffers if comm overlap, param groups if use group params
"""
if act is None:
act = HOOK_ACTION.REDUCE
if comm_overlap:
if comm_group is None:
assert act == HOOK_ACTION.ALL_REDUCE, (
"Only allreduce action can use default comm group"
)
comm_group = paddle.distributed.collective._get_default_group()
if act == HOOK_ACTION.REDUCE:
assert dst != -1
elif act == HOOK_ACTION.ALL_REDUCE:
dst = -1
if group_params:
updated_parameters = []
comm_buffers = []
for idx, group_param in enumerate(parameters):
assert isinstance(group_param, dict), (
"For group params, each group should be a dictionary."
)
assert 'params' in group_param.keys(), (
"For group params, each group should have parameters."
)
real_param = group_param['params']
(
group_decay_fused,
group_all_fused,
group_all_buffers,
) = _fused_parameters_impl(
real_param,
use_main_grad=use_main_grad,
fuse_param=fuse_param,
comm_overlap=comm_overlap,
comm_group=comm_group,
act=act,
dst=dst,
acc_step=acc_step,
scale_after_comm=scale_after_comm,
apply_decay_param_fun=apply_decay_param_fun,
use_reduce_avg=use_reduce_avg,
group_size=group_size,
)
if comm_overlap:
comm_buffers.extend(group_all_buffers)
for fused_tensor in group_all_fused:
fused_tensor.optimize_attr = real_param[0].optimize_attr
group_param['params'] = group_all_fused
updated_parameters.append(group_param)
return updated_parameters, comm_buffers
else:
decay_fused, all_fused, all_buffers = _fused_parameters_impl(
parameters,
use_main_grad=use_main_grad,
fuse_param=fuse_param,
comm_overlap=comm_overlap,
comm_group=comm_group,
act=act,
dst=dst,
acc_step=acc_step,
scale_after_comm=scale_after_comm,
apply_decay_param_fun=apply_decay_param_fun,
use_reduce_avg=use_reduce_avg,
group_size=group_size,
)
return decay_fused, all_fused, all_buffers