1133 lines
37 KiB
Python
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
|