chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:42 +08:00
commit e25996e7db
15472 changed files with 3536181 additions and 0 deletions
@@ -0,0 +1,57 @@
# Copyright (c) 2021 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.
# isort: skip_file
from .meta_parallel_base import MetaParallelBase # noqa: F401
from .parallel_layers import ( # noqa: F401
ColumnParallelLinear,
LayerDesc,
LocalSharedLayerDesc,
ParallelCrossEntropy,
PipelineLayer,
RNGStatesTracker,
RowParallelLinear,
SharedLayerDesc,
VocabParallelEmbedding,
get_rng_state_tracker,
model_parallel_random_seed,
LayerSpec,
import_spec_layer,
get_spec_layer,
build_spec_layer,
)
from .pipeline_parallel import ( # noqa: F401
NoPipelineParallel,
PipelineParallel,
PipelineParallelMicroStepLocations,
PipelineParallelWithInterleave,
PipelineParallelWithInterleaveFthenB,
PipelineDatasetPreprocessor,
VPPFhenBInBalancedMemory,
register_global_pipeline_parallel_hook,
)
from .dualpipev import DualPipeVParallel # noqa: F401
from .segment_parallel import SegmentParallel # noqa: F401
from .sharding_parallel import ShardingParallel # noqa: F401
from .tensor_parallel import TensorParallel # noqa: F401
from .pp_utils.forward_backward_overlap_utils import ( # noqa: F401
ScheduleNode,
ScheduleChunk,
)
from .pp_utils.utils import ( # noqa: F401
dict_to_tuple_helper,
)
__all__ = []
@@ -0,0 +1,851 @@
# 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.
# The file has been adapted from DeepSeek DualPipe project
# Copyright (c) 2025 DeepSeek
# Licensed under the MIT License - https://github.com/deepseek-ai/DualPipe/blob/main/LICENSE
from __future__ import annotations
import paddle
from paddle import framework
from paddle.distributed.communication.batch_isend_irecv import (
P2POp,
batch_isend_irecv,
)
try:
from paddle.distributed.communication import deep_ep
except ImportError:
deep_ep = None
from ..utils.log_util import logger
from .pipeline_parallel import (
FakeMicroDataset,
HybridParallelOptimizer,
PipelineDatasetPreprocessor,
PipelineParallel,
)
from .pp_utils.batch_comm_helper import BatchCommHelper
from .pp_utils.forward_backward_overlap_utils import ScheduleChunk
from .zero_bubble_utils import EventStore, WeightGradStore
__all__ = []
def detach_and_requires_grad(x):
o = x.detach()
o.stop_gradient = False
return o
class DualPipeVParallel(PipelineParallel):
"""
An implementation of the DualPipeV, based on
https://github.com/deepseek-ai/DualPipe/blob/main/dualpipe/dualpipe.py.
"""
def __init__(self, layers, hcg, strategy):
super().__init__(layers=layers, hcg=hcg, strategy=strategy)
self.overlapped_forward_backward = hasattr(
type(self._layers), "overlapped_forward_backward"
)
logger.info(
f"Using DualPipeVParallel with overlapping forward backward={self.overlapped_forward_backward}"
)
self.num_ranks = self.num_stages
self.group_rank = self.pp_group.rank
self.prev_rank = self.pp_group.ranks[
(self.group_rank - 1) % self.pp_group.world_size
]
self.next_rank = self.pp_group.ranks[
(self.group_rank + 1) % self.pp_group.world_size
]
# NOTE(zhangyuqin1998): The first rank has to broadcast the meta information
# of the P2P communication after the first forward.
self.need_broadcast_meta = self.is_pipeline_first_stage()
self.need_recv_meta = not self.is_pipeline_first_stage()
self._p2p_helper = BatchCommHelper(self._using_cache)
def is_pipeline_first_stage(self):
return self.group_rank == 0
def is_pipeline_last_stage(self):
return self.group_rank == self.num_ranks - 1
def _reset_states(self):
self.input_tensors = ([], [])
self.output_tensors = ([], [])
self.input_grad_tensors = ([], [])
self.output_grad_tensors = ([], [])
self.loss_tensors: list[paddle.Tensor] = []
self.schedule_chunks = ([], [])
self.loss_fn_chunks = []
# The first value in the list corresponds to phase 0, and the second value corresponds to phase 1.
self.current_f_acc_id = [0, 0]
self.current_b_acc_id = [0, 0]
self.current_send_f_acc_id = [0, 0]
self.current_send_b_acc_id = [0, 0]
self.current_recv_f_acc_id = [0, 0]
self.current_recv_b_acc_id = [0, 0]
self.comm_forward_ops: list[P2POp] = []
self.comm_backward_ops: list[P2POp] = []
self.to_free: list[paddle.Tensor] = []
def _get_forward_inputs(self, micro_datasets, phase, acc_id):
is_first_stage = self.is_pipeline_first_stage() and phase == 0
if is_first_stage:
assert micro_datasets is not None
self.input_tensors[phase].append(next(micro_datasets[phase])[0])
if self.forward_only:
self.input_tensors[phase][acc_id] = None
return self.input_tensors[phase][acc_id]
def _get_forward_labels(self, micro_datasets, phase, acc_id):
is_last_stage = self.is_pipeline_first_stage() and phase == 1
if is_last_stage and self._compute_loss:
assert micro_datasets is not None
labels = next(micro_datasets[phase])[1]
self._check_micro_batch_data_valid(labels)
return labels
else:
return None
def _loss_compute(self, micro_datasets, phase, acc_id, logits):
labels = self._get_forward_labels(micro_datasets, phase, acc_id)
loss_fn_node = None
if not self.overlapped_forward_backward:
loss_tensor = self._layers._loss_fn[0](logits, labels)
else:
loss_fn_node = self._layers._loss_fn[0].build_schedule_node()
loss_fn_node.labels = labels
loss_tensor = loss_fn_node.forward(logits)
self._store_forward_loss(phase, loss_tensor, loss_fn_node)
def _store_forward_tensors(self, phase, outputs, schedule_chunk):
self.schedule_chunks[phase].append(schedule_chunk)
if self.is_pipeline_last_stage() and phase == 0:
self.input_tensors[1].append(
[detach_and_requires_grad(output) for output in outputs]
)
is_last_stage = self.is_pipeline_first_stage() and phase == 1
if not is_last_stage:
self.output_tensors[phase].append(outputs)
def _forward_compute(self, phase: int, micro_datasets=None) -> None:
acc_id = self.current_f_acc_id[phase]
self.current_f_acc_id[phase] += 1
inputs = self._get_forward_inputs(micro_datasets, phase, acc_id)
if self.overlapped_forward_backward:
schedule_chunk = self._layers.get_schedule_chunk(chunk_id=phase)
outputs = schedule_chunk.forward(inputs)
else:
schedule_chunk = None
outputs = self._layers.forward(inputs, chunk_id=phase)
outputs = [outputs] if isinstance(outputs, paddle.Tensor) else outputs
is_last_stage = self.is_pipeline_first_stage() and phase == 1
if is_last_stage and self._compute_loss:
self._loss_compute(micro_datasets, phase, acc_id, outputs)
self._store_forward_tensors(phase, outputs, schedule_chunk)
def _get_backward_inputs(self, phase, acc_id):
outputs = self.output_tensors[phase][acc_id]
self.output_tensors[phase][acc_id] = None
output_grads = self.output_grad_tensors[phase][acc_id]
self.output_grad_tensors[phase][acc_id] = None
non_empty = [
(t, g) for t, g in zip(outputs, output_grads) if g is not None
]
outputs, output_grads = list(zip(*non_empty))
return outputs, output_grads
def _store_backward_tensors(self, phase, acc_id, input_grads=None):
if input_grads is None:
inputs = self.input_tensors[phase][acc_id]
input_grads = [
t.grad
for t in inputs
if (t is not None and not t.stop_gradient)
]
self.input_tensors[phase][acc_id] = None
if isinstance(input_grads, paddle.Tensor):
input_grads = (input_grads,)
if self.is_pipeline_last_stage() and phase == 1:
self.output_grad_tensors[0].append(input_grads)
else:
self.input_grad_tensors[phase].append(input_grads)
def _store_forward_loss(self, phase, loss_tensor, loss_fn_node=None):
is_last_stage = self.is_pipeline_first_stage() and phase == 1
if is_last_stage and self._compute_loss:
if isinstance(loss_tensor, (tuple, list)):
assert len(loss_tensor) == 1
loss_tensor = loss_tensor[0]
assert isinstance(loss_tensor, paddle.Tensor), (
"Currently, loss_fn should obtain Paddle.Tensor dtype"
)
self.loss_tensors.append(loss_tensor)
self.loss_fn_chunks.append(loss_fn_node)
def _backward_compute(self, phase: int, enable_zb: bool = False) -> None:
if self.forward_only:
return
acc_id = self.current_b_acc_id[phase]
self.current_b_acc_id[phase] += 1
is_last_stage = self.is_pipeline_first_stage() and phase == 1
WeightGradStore.enabled = enable_zb
input_grads = None
with paddle.amp.auto_cast(enable=False):
if is_last_stage:
loss = self.loss_tensors[acc_id]
if self.overlapped_forward_backward:
loss_fn_node = self.loss_fn_chunks[acc_id]
backward_chunk = self.schedule_chunks[phase][acc_id]
_, _, input_grads = (
self._layers.overlapped_forward_backward(
ScheduleChunk([]), # forward_chunk
None, # forward_inputs
None, # forward_loss_fn_node
backward_chunk,
loss_fn_node,
None, # input_grads
self.scaler,
combine_bw_event_to_wait=None,
pp_stream=None,
)
)
self.loss_fn_chunks[acc_id] = None
self.schedule_chunks[phase][acc_id] = None
else:
if self.scaler:
paddle.autograd.backward(self.scaler.scale(loss))
else:
paddle.autograd.backward(loss)
else:
outputs, output_grads = self._get_backward_inputs(phase, acc_id)
if self.overlapped_forward_backward:
backward_chunk = self.schedule_chunks[phase][acc_id]
_, _, input_grads = (
self._layers.overlapped_forward_backward(
ScheduleChunk([]), # forward_chunk
None, # forward_inputs
None, # forward_loss_fn_node
backward_chunk,
None, # backward_loss_fn_node
output_grads,
None, # scaler
combine_bw_event_to_wait=None,
pp_stream=None,
)
)
self.schedule_chunks[phase][acc_id] = None
else:
if len(outputs) > 0:
outputs = [t for t in outputs if not t.stop_gradient]
paddle.autograd.backward(
tensors=outputs,
grad_tensors=output_grads,
)
WeightGradStore.enabled = False
if enable_zb:
WeightGradStore.flush()
self._store_backward_tensors(phase, acc_id, input_grads=input_grads)
def _forward_backward_compute(
self,
forward_phase: int,
backward_phase: int,
micro_datasets=None,
combine_backward_event_to_wait=None,
pass_pp_stream=False,
) -> None:
if self.forward_only:
self._forward_compute(forward_phase, micro_datasets)
return
if not self.overlapped_forward_backward:
self._forward_compute(forward_phase, micro_datasets)
self._backward_compute(backward_phase)
return
# pre-forward
forward_acc_id = self.current_f_acc_id[forward_phase]
self.current_f_acc_id[forward_phase] += 1
forward_inputs = self._get_forward_inputs(
micro_datasets, forward_phase, forward_acc_id
)
forward_labels = self._get_forward_labels(
micro_datasets, forward_phase, forward_acc_id
)
if forward_labels is not None:
forward_loss_fn_node = self._layers._loss_fn[
0
].build_schedule_node()
forward_loss_fn_node.labels = forward_labels
else:
forward_loss_fn_node = None
# pre-backward
backward_acc_id = self.current_b_acc_id[backward_phase]
self.current_b_acc_id[backward_phase] += 1
is_last_stage1 = self.is_pipeline_first_stage() and backward_phase == 1
if is_last_stage1:
backward_loss_fn_node = self.loss_fn_chunks[backward_acc_id]
backward_grads = None
else:
backward_loss_fn_node = None
_, backward_grads = self._get_backward_inputs(
backward_phase, backward_acc_id
)
# event_to_wait = deep_ep.get_event_from_custom_stream(paddle.device.current_stream().stream_base)
# forward & backward
forward_chunk = self._layers.get_schedule_chunk(chunk_id=forward_phase)
backward_chunk = self.schedule_chunks[backward_phase][backward_acc_id]
forward_outputs, forward_loss, backward_input_grads = (
self._layers.overlapped_forward_backward(
forward_chunk,
forward_inputs,
forward_loss_fn_node,
backward_chunk,
backward_loss_fn_node,
backward_grads,
self.scaler,
combine_bw_event_to_wait=combine_backward_event_to_wait,
pp_stream=(
self.pp_group.process_group.get_stream(
paddle.framework._current_expected_place_()
)
if pass_pp_stream
else None
),
)
)
self.schedule_chunks[backward_phase][backward_acc_id] = None
# post-forward
self._store_forward_tensors(
forward_phase, forward_outputs, forward_chunk
)
self._store_forward_loss(
forward_phase, forward_loss, forward_loss_fn_node
)
# post-backward
self._store_backward_tensors(
backward_phase, backward_acc_id, input_grads=backward_input_grads
)
def _commit_and_wait_comm(
self, p2p_overlap=False, use_outer_event_wait=False
) -> None:
common_forward_ops_num = (
len(self.comm_forward_ops)
if self.comm_forward_ops is not None
else 0
)
common_backward_ops_num = (
len(self.comm_backward_ops)
if self.comm_backward_ops is not None
else 0
)
if common_forward_ops_num == 0 and common_backward_ops_num == 0:
if EventStore.event is not None:
e_t = EventStore.event
EventStore.event = None
return e_t
return deep_ep.get_event_from_custom_stream(
paddle.device.current_stream().stream_base
)
use_stream_wait_event = (
p2p_overlap and self._overlap_p2p_comm and deep_ep is not None
)
pp_raw_stream = self.pp_group.process_group.get_stream(
paddle.framework._current_expected_place_()
)
if use_outer_event_wait:
self.pp_group.process_group.set_outer_wait(True)
if common_forward_ops_num > 0:
fwd_reqs = batch_isend_irecv(self.comm_forward_ops)
if not use_stream_wait_event:
for req in fwd_reqs:
req.wait()
if use_outer_event_wait:
self.pp_group.process_group.set_outer_wait(False)
if use_stream_wait_event:
forward_event_to_wait = deep_ep.get_event_from_custom_stream(
pp_raw_stream
)
backward_outer_event_wait = False
if EventStore.event is not None:
with paddle.device.stream_guard(
paddle.device.Stream(stream_base=pp_raw_stream)
):
EventStore.event.current_stream_wait()
EventStore.set(None)
self.pp_group.process_group.set_outer_wait(True)
backward_outer_event_wait = True
if common_backward_ops_num > 0:
bwd_reqs = batch_isend_irecv(self.comm_backward_ops)
if not use_stream_wait_event:
for req in bwd_reqs:
req.wait()
if backward_outer_event_wait:
self.pp_group.process_group.set_outer_wait(False)
if use_stream_wait_event:
forward_event_to_wait.current_stream_wait()
combine_bw_event_to_wait = deep_ep.get_event_from_custom_stream(
pp_raw_stream
)
else:
combine_bw_event_to_wait = deep_ep.get_event_from_custom_stream(
paddle.device.current_stream().stream_base
)
self.comm_forward_ops = []
self.comm_backward_ops = []
self._free_tensors()
return combine_bw_event_to_wait
def _weight_pass(self) -> None:
if self.forward_only:
return
self._commit_and_wait_comm()
# Assume FIFO
WeightGradStore.pop()
def _free_tensors(self) -> None:
self._release_output(self.to_free)
self.to_free = []
def _recv_forward(self, phase: int) -> None:
if (self.is_pipeline_first_stage() and phase == 0) or (
self.is_pipeline_last_stage() and phase == 1
):
return
self.current_recv_f_acc_id[phase] += 1
tensors = self._p2p_helper.append_irecv(
self.comm_forward_ops,
self.prev_rank if phase == 0 else self.next_rank,
self.pp_group,
alloc_on_comm_stream=self._overlap_p2p_comm,
)
self.input_tensors[phase].append(tensors)
def _send_forward(self, phase: int) -> None:
if (self.is_pipeline_first_stage() and phase == 1) or (
self.is_pipeline_last_stage() and phase == 0
):
return
acc_id = self.current_send_f_acc_id[phase]
self.current_send_f_acc_id[phase] += 1
tensors = self.output_tensors[phase][acc_id]
self._p2p_helper.append_isend(
self.comm_forward_ops,
tensors,
self.next_rank if phase == 0 else self.prev_rank,
self.pp_group,
self.need_broadcast_meta,
)
self.need_broadcast_meta = False
self.to_free.extend(tensors)
def _recv_backward(self, phase: int) -> None:
if self.forward_only:
return
if (self.is_pipeline_first_stage() and phase == 1) or (
self.is_pipeline_last_stage() and phase == 0
):
return
self.current_recv_b_acc_id[phase] += 1
tensors = self._p2p_helper.append_irecv(
self.comm_backward_ops,
self.next_rank if phase == 0 else self.prev_rank,
self.pp_group,
alloc_on_comm_stream=self._overlap_p2p_comm,
)
self.output_grad_tensors[phase].append(tensors)
def _send_backward(self, phase: int) -> None:
if self.forward_only:
return
if (self.is_pipeline_first_stage() and phase == 0) or (
self.is_pipeline_last_stage() and phase == 1
):
return
acc_id = self.current_send_b_acc_id[phase]
self.current_send_b_acc_id[phase] += 1
tensors = self.input_grad_tensors[phase][acc_id]
self.input_grad_tensors[phase][acc_id] = None
self._p2p_helper.append_isend(
self.comm_backward_ops,
tensors,
self.prev_rank if phase == 0 else self.next_rank,
self.pp_group,
)
def _forward_pass(
self,
phase: int,
micro_datasets=None,
recv: bool = True,
send: bool = True,
) -> None:
if recv:
self._recv_forward(phase)
self._commit_and_wait_comm()
self._forward_compute(phase, micro_datasets)
if send:
self._send_forward(phase)
def _backward_pass(
self,
phase: int,
enable_zb: bool = False,
recv: bool = True,
send: bool = True,
) -> None:
if recv:
self._recv_backward(phase)
self._commit_and_wait_comm()
self._backward_compute(phase, enable_zb)
if send:
self._send_backward(phase)
def _forward_backward_pass(
self,
forward_phase: int,
backward_phase: int,
micro_datasets=None,
recv0: bool = True,
first_chunk=False,
last_chunk=False,
main_stage=False,
last_stage_and_first_chunk=False,
) -> None:
if recv0:
self._recv_forward(forward_phase)
self._recv_backward(backward_phase)
need_send_forward = not (
self.is_pipeline_first_stage() and forward_phase == 1
) or (self.is_pipeline_last_stage() and forward_phase == 0)
need_send_backward = not (
self.is_pipeline_first_stage() and backward_phase == 0
) or (self.is_pipeline_last_stage() and backward_phase == 1)
use_outer_event_wait = (
main_stage
and not first_chunk
and self._overlap_p2p_comm
and deep_ep is not None
and (need_send_forward and need_send_backward)
)
pass_pp_stream = (
main_stage
and not last_chunk
and self._overlap_p2p_comm
and deep_ep is not None
and (need_send_forward and need_send_backward)
and (not last_stage_and_first_chunk)
)
combine_bw_wait_event = self._commit_and_wait_comm(
not last_chunk, use_outer_event_wait
)
self._forward_backward_compute(
forward_phase,
backward_phase,
micro_datasets,
combine_backward_event_to_wait=combine_bw_wait_event,
pass_pp_stream=pass_pp_stream,
)
self._send_forward(forward_phase)
self._send_backward(backward_phase)
def _wrap_data(self, data, phase):
"""
for backward compatibility, wrap data to Fake FakeMicroDataset if it is of type list or tuple
"""
if isinstance(data, PipelineDatasetPreprocessor):
data = data()
if (not isinstance(data, tuple)) and (not isinstance(data, list)):
return data
micro_dataset = FakeMicroDataset(
data,
self.is_pipeline_first_stage() and phase == 0,
self.is_pipeline_first_stage() and phase == 1,
self.accumulate_steps,
self.micro_batch_size,
)
return micro_dataset
def _prepare_training(self, data, optimizer, lr_scheduler):
assert isinstance(optimizer, HybridParallelOptimizer), (
'optimizer should be HybridParallelOptimizer subclass.'
)
assert framework._dygraph_tracer()._has_grad, (
'Please enable the generation of gradients.'
)
if self.is_pipeline_first_stage():
assert data is not None, (
"For the first and the last stage, the data must be set."
)
else:
data = None
self.optimizer = optimizer
self.lr_scheduler = lr_scheduler
self._layers.train()
self.register_sharding_comm_overlap_hook(optimizer)
return data
def _broadcast_final_loss(self):
loss_sum_tensor = paddle.zeros([1], "float32")
if self.is_pipeline_first_stage():
assert len(self.loss_tensors) > 0, (
"train_batch() in last stage should obtain valid loss"
)
for loss in self.loss_tensors:
loss_sum_tensor += loss.detach().astype("float32")
loss_sum_tensor /= self.accumulate_steps
paddle.distributed.all_reduce(
loss_sum_tensor, group=self.pp_group, sync_op=True
)
return loss_sum_tensor
def forward_backward_pipeline(
self,
data,
scaler,
forward_only=False,
compute_loss=True,
):
self.scaler = scaler
rank = self.group_rank
num_ranks = self.num_ranks
assert (
self.accumulate_steps > 0 and self.accumulate_steps >= num_ranks * 2
), f"{self.accumulate_steps=}, {num_ranks=}"
self.forward_only = forward_only
self._reset_states()
# NOTE(zhangyuqin1998): Tensors to be sent or received must have a
# consistent shape and data type throughout the entire pipeline. We
# broadcast the meta info in the first forward of the first rank.
self._p2p_helper.recv_meta_from_head(self.pp_group, self.need_recv_meta)
self.need_recv_meta = False
micro_dataset_phase0 = self._wrap_data(data, 0)
micro_dataset_phase1 = self._wrap_data(data, 1)
micro_datasets = [micro_dataset_phase0, micro_dataset_phase1]
# Step 1: nF0
step_1 = (num_ranks - rank - 1) * 2
for i in range(step_1):
self._forward_pass(0, micro_datasets)
# Step 2: nF0F1
step_2 = rank + 1
self._recv_forward(0)
for i in range(step_2):
self._forward_pass(0, micro_datasets, recv=False, send=False)
self._recv_forward(0)
self._forward_pass(
1,
micro_datasets,
send=(not self.is_pipeline_last_stage()) or (i < step_2 - 1),
)
self._send_forward(0)
# Step 3: nB1W1F1 (Use zero bubble)
step_3 = num_ranks - rank - 1
for i in range(step_3):
self._backward_pass(1, enable_zb=True)
self._recv_forward(1)
self._weight_pass()
self._forward_pass(1, micro_datasets, recv=False)
# Step 4 (Main step): nF0B1F1B0
step_4 = self.accumulate_steps - num_ranks * 2 + rank + 1
have_step5 = num_ranks - rank - 1 > 0
# Update code to support send/recv overlap
# Only support send/recv overlap in MainStep
for i in range(step_4):
is_last_chunk = i + 1 == step_4
if i == 0:
if self.is_pipeline_last_stage():
# NOTE: We don't overlap these two passes to further reduce bubble size.
self._forward_pass(
0, micro_datasets, recv=False, send=False
)
self._send_forward(1)
self._backward_pass(1, send=False)
self._send_forward(0)
self._send_backward(1)
self._forward_backward_pass(
1,
0,
micro_datasets,
first_chunk=True,
last_chunk=is_last_chunk,
main_stage=True,
)
else:
self._forward_backward_pass(
0,
1,
micro_datasets,
recv0=False,
first_chunk=True,
main_stage=True,
)
self._forward_backward_pass(
1,
0,
micro_datasets,
last_chunk=is_last_chunk,
main_stage=True,
)
else:
self._forward_backward_pass(
0,
1,
micro_datasets,
main_stage=True,
last_stage_and_first_chunk=self.is_pipeline_last_stage(),
)
self._forward_backward_pass(
1,
0,
micro_datasets,
last_chunk=is_last_chunk,
main_stage=True,
)
# Step 5: nB1F1B0
step_5 = num_ranks - rank - 1
for i in range(step_5):
self._backward_pass(1)
self._forward_backward_pass(1, 0, micro_datasets)
# Step 6: nB1B0 (The second half of the passes use zero bubble)
step_6 = rank + 1
enable_zb = False
for i in range(step_6):
if i == step_6 // 2 and rank % 2 == 1:
enable_zb = True
self._backward_pass(1, enable_zb=enable_zb)
if i == step_6 // 2 and rank % 2 == 0:
enable_zb = True
self._backward_pass(0, enable_zb=enable_zb)
# Step 7: nWB0 (Use zero bubble)
step_7 = num_ranks - rank - 1
for i in range(step_7):
self._weight_pass()
self._backward_pass(0, enable_zb=True)
# Step 8: nW
step_8 = rank + 1
for i in range(step_8):
self._weight_pass()
assert WeightGradStore.funcs_queue.empty()
self._commit_and_wait_comm()
self._layers.allreduce_shared_weight_gradients()
with paddle.amp.auto_cast(enable=False):
train_loss = self._broadcast_final_loss()
self._reset_states()
return train_loss
def train_batch(
self,
data,
optimizer,
lr_scheduler=None,
scaler=None,
):
data = self._prepare_training(data, optimizer, lr_scheduler)
train_loss = self.forward_backward_pipeline(data, scaler)
# optimizer
with paddle.amp.auto_cast(enable=False):
self._optimizer_step()
return train_loss
@@ -0,0 +1,44 @@
# Copyright (c) 2021 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 paddle import nn
__all__ = []
class MetaParallelBase(nn.Layer):
def __init__(self, layers, hcg, strategy):
super().__init__(layers.full_name() + "_meta_parallel_base")
self._layers = layers
self._hcg = hcg
self._strategy = strategy
self._prepare_for_model()
def _prepare_for_model(self):
pass
def _pre_forward(self, *inputs, **kwargs):
pass
def forward(self, *inputs, **kwargs):
self._pre_forward(*inputs, **kwargs)
output = self._layers(*inputs, **kwargs)
self._post_forward(output)
return output
def _post_forward(self, output):
pass
@@ -0,0 +1,39 @@
# Copyright (c) 2021 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 .mp_layers import ( # noqa: F401
ColumnParallelLinear,
ParallelCrossEntropy,
RowParallelLinear,
VocabParallelEmbedding,
)
from .pp_layers import ( # noqa: F401
LayerDesc,
LocalSharedLayerDesc,
PipelineLayer,
SharedLayerDesc,
)
from .random import ( # noqa: F401
RNGStatesTracker,
get_rng_state_tracker,
model_parallel_random_seed,
)
from .spec_utils import (
LayerSpec as LayerSpec,
build_spec_layer as build_spec_layer,
get_spec_layer as get_spec_layer,
import_spec_layer as import_spec_layer,
)
__all__ = []
@@ -0,0 +1,22 @@
# Copyright (c) 2022 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 ...layers.mpu.mp_layers import ( # noqa: F401
ColumnParallelLinear,
ParallelCrossEntropy,
RowParallelLinear,
VocabParallelEmbedding,
)
__all__ = []
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
# Copyright (c) 2022 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 ...layers.mpu.random import ( # noqa: F401
RNGStatesTracker,
dropout,
get_rng_state_tracker,
model_parallel_random_seed,
)
__all__ = []
@@ -0,0 +1,141 @@
# Copyright (c) 2026 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.
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
from __future__ import annotations
import types
import warnings
from dataclasses import dataclass, field
@dataclass
class LayerSpec:
"""This is a Layer Specification dataclass.
Specification defines the location of the layer (to import dynamically)
or the imported layer itself. It also defines the extra_kwargs that need to be
passed to initialize the layer.
Args:
layer (tuple | type): A tuple describing the location of the
layer class e.g. `(layer.location, LayerClass)` or the imported
layer class itself e.g. `LayerClass` (which is already imported
using `from layer.location import LayerClass`).
extra_kwargs (dict): A dictionary of extra_kwargs that need to be passed while init.
"""
layer: tuple | type
extra_kwargs: dict = field(default_factory=lambda: {})
sublayers_spec: type = None
def __repr__(self):
rst = ""
if isinstance(self.layer, tuple):
for sub_layer in self.layer:
rst = rst + repr(sub_layer) + ","
else:
rst = repr(self.layer) + repr(self.extra_kwargs)
return rst
def import_spec_layer(layer_path: tuple[str]):
"""Import a named object from a layer in the context of this function."""
base_path, name = layer_path
try:
layer = __import__(base_path, globals(), locals(), [name])
except ImportError as e:
print(f"couldn't import layer due to {e}")
return None
return vars(layer)[name]
def get_spec_layer(spec_or_layer: LayerSpec | type, **additional_kwargs):
# If a layer class is already provided return it as is
if isinstance(spec_or_layer, (type, types.FunctionType)):
return spec_or_layer
# If the layer is provided instead of layer path, then return it as is
if isinstance(spec_or_layer.layer, (type, types.FunctionType)):
return spec_or_layer.layer
# Otherwise, return the dynamically imported layer from the layer path
return import_spec_layer(spec_or_layer.layer)
def build_spec_layer(spec_or_layer: LayerSpec | type, *args, **kwargs):
# If the passed `spec_or_layer` is
# a `Function`, then return it as it is
# NOTE: to support an already initialized layer add the following condition
# `or isinstance(spec_or_layer, paddle.nn.Layer)` to the following if check
if isinstance(spec_or_layer, types.FunctionType):
return spec_or_layer
# If the passed `spec_or_layer` is actually a spec (instance of
# `LayerSpec`) and it specifies a `Function` using its `layer`
# field, return the `Function` as it is
if isinstance(spec_or_layer, LayerSpec) and isinstance(
spec_or_layer.layer, types.FunctionType
):
return spec_or_layer.layer
# Check if a layer class is provided as a spec or if the layer path
# itself is a class
if isinstance(spec_or_layer, type):
layer = spec_or_layer
elif hasattr(spec_or_layer, "layer") and isinstance(
spec_or_layer.layer, type
):
layer = spec_or_layer.layer
else:
# Otherwise, dynamically import the layer from the layer path
layer = import_spec_layer(spec_or_layer.layer)
# If the imported layer is actually a `Function` return it as it is
if isinstance(layer, types.FunctionType):
return layer
# Finally return the initialized layer with extra_kwargs from the spec as well
# as those passed as **kwargs from the code
# Add the `sublayers_spec` argument to the layer init call if it exists in the
# spec.
if (
hasattr(spec_or_layer, "sublayers_spec")
and spec_or_layer.sublayers_spec is not None
):
kwargs["sublayers_spec"] = spec_or_layer.sublayers_spec
if hasattr(spec_or_layer, "extra_kwargs"):
for key in spec_or_layer.extra_kwargs.keys():
if key in kwargs:
warnings.warn(
f"Got same key {key} in extra_kwargs and kwargs during init {layer.__name__}. Will keep the value ing extra_kwargs."
)
kwargs.pop(key)
try:
return layer(
*args,
**spec_or_layer.extra_kwargs
if hasattr(spec_or_layer, "extra_kwargs")
else {},
**kwargs,
)
except Exception as e:
# improve the error message since we hide the layer name in the line above
import sys
raise type(e)(
f"{e!s} when instantiating {layer.__name__}"
).with_traceback(sys.exc_info()[2])
@@ -0,0 +1,56 @@
# 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
from collections import defaultdict
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable
class PipelineHook:
def __init__(self):
self.hooks: dict[int, list[Callable]] = defaultdict(list)
self._hooks_capacity = 0
self.reset_current_id()
def reset_current_id(self):
self._current_id = 0
def set_hooks_capacity(self, capacity: int):
self._hooks_capacity = capacity
def register_hook(self, hook_id: int, hook: Callable):
assert hook_id < self._hooks_capacity, (
f"hook_id {hook_id} is out of range, maximum capacity is {self._hooks_capacity}."
)
self.hooks[hook_id].append(hook)
def run_hook(self):
assert self._current_id < self._hooks_capacity, (
f"hook_id {self._current_id} is out of range, maximum capacity is {self._hooks_capacity}."
)
for hook in self.hooks[self._current_id]:
hook(self._current_id)
self._current_id += 1
@property
def current_id(self):
return self._current_id
@property
def hooks_capacity(self):
return self._hooks_capacity
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
# Copyright (c) 2021 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.
__all__ = []
@@ -0,0 +1,133 @@
# 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.
from __future__ import annotations
import paddle
from paddle.distributed.communication.batch_isend_irecv import (
P2POp,
)
from .p2p_communication import SendRecvMeta
from .utils import (
number_2_dtype,
paddle_2_number,
)
class BatchCommHelper:
# NOTE(zhangyuqin1998): Tensors to be sent or received must have a
# consistent shape and data type throughout the entire pipeline.
def __init__(self, use_cache=True):
self._send_recv_meta = SendRecvMeta()
self._use_cache = use_cache
def clear_meta_cache(self):
self._send_recv_meta.init_or_erase_meta()
def _send_meta(self, tensors, group, broadcast=False):
self._send_recv_meta.set_send_message(tensors)
self._send_recv_meta.send_meta(tensors, group, broadcast=broadcast)
self._send_recv_meta.recv_shape_message = (
self._send_recv_meta.send_shape_message
)
self._send_recv_meta.recv_dtype_message = (
self._send_recv_meta.send_dtype_message
)
def _recv_meta(self, group, broadcast=False):
self._send_recv_meta.recv_meta(group, broadcast=broadcast)
def _build_from_meta(self):
shape_message = self._send_recv_meta.recv_shape_message
dtype_message = self._send_recv_meta.recv_dtype_message
stop_gradient = self._send_recv_meta.recv_stop_gradient
assert (shape_message is not None) and (dtype_message is not None), (
"Failed to build from meta."
)
res = []
if isinstance(shape_message, tuple):
for idx, shape in enumerate(shape_message):
tmp = paddle.empty(
shape=shape, dtype=number_2_dtype(dtype_message[idx])
)
tmp.stop_gradient = (
stop_gradient[idx] if stop_gradient is not None else False
)
res.append(tmp)
else:
tmp = paddle.empty(
shape=shape_message, dtype=number_2_dtype(dtype_message)
)
tmp.stop_gradient = stop_gradient
res.append(tmp)
return res
def _check_valid(self, tensors):
shape_message = self._send_recv_meta.recv_shape_message
dtype_message = self._send_recv_meta.recv_dtype_message
assert (shape_message is not None) and (dtype_message is not None), (
"Failed to build from meta."
)
if isinstance(shape_message, tuple):
assert isinstance(tensors, (list, tuple))
assert len(tensors) == len(shape_message)
for idx, (shape, dtype, tensor) in enumerate(
zip(shape_message, dtype_message, tensors)
):
assert tensor.shape == shape, "Invalid shape."
assert number_2_dtype(
paddle_2_number(tensor.dtype)
) == number_2_dtype(dtype), "Invalid dtype."
else:
if isinstance(tensors, (list, tuple)):
assert len(tensors) == 1
tensors = tensors[0]
assert tensors.shape == shape_message, "Invalid shape."
assert number_2_dtype(
paddle_2_number(tensors.dtype)
) == number_2_dtype(dtype_message), "Invalid dtype."
def recv_meta_from_head(self, group, need_recv_meta):
if not need_recv_meta:
return
self._recv_meta(group, broadcast=True)
def append_irecv(self, ops, src, group, alloc_on_comm_stream=False):
if alloc_on_comm_stream:
send_recv_stream = paddle.device.Stream(
stream_base=group.process_group.get_stream(
paddle.framework._current_expected_place_()
)
)
with paddle.device.stream_guard(send_recv_stream):
tensors = self._build_from_meta()
else:
tensors = self._build_from_meta()
for tensor in tensors:
if tensor is not None:
ops.append(P2POp(paddle.distributed.irecv, tensor, src, group))
return tensors
def append_isend(self, ops, tensors, dst, group, need_broadcast_meta=False):
if need_broadcast_meta:
self._send_meta(tensors, group, broadcast=True)
self._check_valid(tensors)
for tensor in tensors:
if tensor is not None:
ops.append(P2POp(paddle.distributed.isend, tensor, dst, group))
@@ -0,0 +1,201 @@
# 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 paddle
from .utils import dict_to_tuple_helper
class ScheduleChunk:
# NOTE(zhangyuqin): ScheduleChunk is the atomic unit of pipeline scheduling.
# A ScheduleChunk can contain several ScheduleNodes
def __init__(self, nodes):
self.nodes = nodes
self._check_nodes_valid()
def forward(self, inputs):
for n in self.nodes:
inputs = n.forward(inputs)
return inputs
def backward(self, output_grad):
for n in reversed(self.nodes):
output_grad = n.backward(output_grad)
return output_grad
def _check_nodes_valid(self):
for n in self.nodes:
assert isinstance(n, (ScheduleNode, ScheduleChunk))
def detach_and_requires_grad(inputs):
if isinstance(inputs, (tuple, list)):
is_tuple = isinstance(inputs, tuple)
ret = []
for input in inputs:
if isinstance(input, (tuple, list)):
ret.append(detach_and_requires_grad(input))
elif isinstance(input, paddle.Tensor):
tmp = input.detach() if input is not None else None
if tmp is not None:
tmp.stop_gradient = input.stop_gradient
ret.append(tmp)
else:
ret.append(input)
if is_tuple:
ret = tuple(ret)
return ret
elif isinstance(inputs, dict):
ret = {}
for key in inputs.keys():
input = inputs[key]
tmp = input.detach() if input is not None else None
if tmp is not None:
tmp.stop_gradient = input.stop_gradient
ret[key] = tmp
return ret
else:
tmp = inputs.detach()
tmp.stop_gradient = inputs.stop_gradient
return tmp
def clone_and_clear_dataptr(outputs, clear_dataptr=False):
if isinstance(outputs, (tuple, list)):
is_tuple = isinstance(outputs, tuple)
ret = [
FakeClone.apply(o)
for o in outputs
if o is not None and isinstance(o, paddle.Tensor)
]
if clear_dataptr:
for o in ret:
o._clear_dataptr()
if is_tuple:
ret = tuple(ret)
return ret
elif isinstance(outputs, dict):
ret = {}
for key in outputs.keys():
o = outputs[key]
if o is not None and isinstance(o, paddle.Tensor):
ret[key] = FakeClone.apply(o)
if clear_dataptr:
for key in ret:
ret[key]._clear_dataptr()
return ret
else:
ret = FakeClone.apply(outputs)
if clear_dataptr:
ret._clear_dataptr()
return ret
class FakeClone(paddle.autograd.PyLayer):
# NOTE(zhangyuqin): Some input tensors may not be used in the forward function, but their gradients
# need to be retained. Therefore, we need a clone here. To avoid the DtoD copy, we need a FakeClone
@staticmethod
def forward(ctx, input):
return paddle.empty_like(input)
@staticmethod
def backward(ctx, grad_output):
return grad_output
class ScheduleNode:
# NOTE(zhangyuqin): ScheduleNode is a subgraph of the pipeline, capable of independently calling
# forward and backward. Users should not use paddle.autograd.backward on the results of ScheduleNode.forward.
# Instead, they should use ScheduleNode.backward. Otherwise, resource leakage may occur.
def __init__(self, fwd_func, name=""):
self.name = name
self.fwd_func = fwd_func
self.inputs = None
self.outputs = None
self.labels = None
self.scale_loss_factor = None
def forward(self, inputs=(), **kwargs):
detached_inputs = detach_and_requires_grad(inputs)
self.inputs = detached_inputs
if self.labels is not None:
outputs = self.fwd_func(self.inputs, self.labels, **kwargs)
else:
outputs = self.fwd_func(self.inputs, **kwargs)
if self.scale_loss_factor is not None:
outputs /= self.scale_loss_factor
# Do not release the loss tensor.
clear_dataptr = self.labels is None
self.outputs = clone_and_clear_dataptr(outputs, clear_dataptr)
return outputs
def backward(self, output_grad=None, scaler=None):
if output_grad is None:
if isinstance(self.outputs, (tuple, list)):
assert len(self.outputs) == 1
outputs = self.outputs[0]
else:
outputs = self.outputs
assert isinstance(outputs, paddle.Tensor)
if scaler is not None:
paddle.autograd.backward(scaler.scale(outputs))
else:
paddle.autograd.backward(outputs)
else:
# Record the original type (tuple or list) to preserve it after filtering
is_output_grad_tuple = isinstance(output_grad, tuple)
if not isinstance(output_grad, (tuple, list)):
is_output_grad_tuple = True # Single value becomes tuple
output_grad = (output_grad,)
outputs = dict_to_tuple_helper(self.outputs)
if not isinstance(outputs, (tuple, list)):
outputs = (outputs,)
outputs = [t for t in outputs if not t.stop_gradient]
# Filter None values from output_grad
output_grad = [grad for grad in output_grad if grad is not None]
# Preserve original type (tuple or list)
output_grad = (
tuple(output_grad)
if is_output_grad_tuple
else list(output_grad)
)
assert len(outputs) == len(output_grad), (
f"{len(outputs)} of {type(outputs[0])} vs {len(output_grad)} of {type(output_grad[0])}"
)
paddle.autograd.backward(outputs, output_grad)
inputs = dict_to_tuple_helper(self.inputs)
if not isinstance(inputs, (tuple, list)):
inputs = (inputs,)
grad = tuple([e.grad if e is not None else None for e in inputs])
# grad = tuple([e.grad if e is not None and not e.stop_gradient else None for e in inputs])
self._reset_states()
# if len(grad) == 1:
# grad = grad[0]
return grad
def _reset_states(self):
self.inputs = None
self.outputs = None
self.labels = None
self.scale_loss_factor = None
@@ -0,0 +1,870 @@
# Copyright (c) 2021 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 os
import numpy as np
import paddle
from paddle import framework
from ...utils import timer_helper as timer
from ...utils.log_util import logger
from .utils import number_2_dtype, paddle_2_number
_hcg = None
_enable_partial_send_recv = True
_timers = None
_xpu_comm_group_started = False
_sync_send = os.environ.get("PADDLE_P2P_SYNC_SEND", "0")
_sync_send = _sync_send.lower() in ['1', 'true']
def _xpu_comm_group_start():
if not paddle.is_compiled_with_xpu():
return
global _xpu_comm_group_started
assert not _xpu_comm_group_started
framework.core.ProcessGroupBKCL.group_start()
_xpu_comm_group_started = True
def _xpu_comm_group_end():
if not paddle.is_compiled_with_xpu():
return
global _xpu_comm_group_started
if _xpu_comm_group_started:
framework.core.ProcessGroupBKCL.group_end()
_xpu_comm_group_started = False
def initialize_p2p_groups(
hcg, enable_partial_send_recv=True, enable_timer=False
):
global _hcg, _enable_partial_send_recv, _timers
_hcg = hcg
_enable_partial_send_recv = enable_partial_send_recv
if enable_timer:
_timers = timer.get_timers()
(
send_next_group,
send_prev_group,
recv_next_group,
recv_prev_group,
) = _hcg.get_p2p_groups()
debug_str = (
f"P2pInfo: send_next_group: {send_next_group!r}, send_prev_group: {send_prev_group!r}, "
f"recv_next_group: {recv_next_group!r}, recv_prev_group: {recv_prev_group!r}"
)
logger.info(debug_str)
class SendRecvMeta:
"""Mainly used to help p2p communication context information"""
def __init__(self):
self.send_shape_message = None
self.send_dtype_message = None
self.recv_shape_message = None
self.recv_dtype_message = None
self.recv_stop_gradient = None
self.has_send_meta = False
self.has_recv_meta = False
def _recv_shape_dtype(self, group):
# recv len(shape)
dims = paddle.to_tensor([0])
src_rank = _hcg._get_p2p_prev_rank()
paddle.distributed.recv(dims, src=src_rank, group=group)
dims = dims.item()
# recv shape
shape = paddle.to_tensor([0] * dims)
paddle.distributed.recv(shape, src=src_rank, group=group)
# recv dtype
dtype = paddle.to_tensor([0])
paddle.distributed.recv(dtype, src=src_rank, group=group)
# recv stop_gradient
stop_grad = paddle.to_tensor([0])
paddle.distributed.recv(stop_grad, src=src_rank, group=group)
return shape.tolist(), dtype.item(), stop_grad.item()
def recv_meta(self, group):
tensor_type = paddle.to_tensor([0])
src_rank = _hcg._get_p2p_prev_rank()
paddle.distributed.recv(tensor_type, src=src_rank, group=group)
tensor_type = tensor_type.item()
if tensor_type == 0:
shape, dtype, stop_grad = self._recv_shape_dtype(group)
self.recv_shape_message = shape
self.recv_dtype_message = dtype
self.recv_stop_gradient = bool(stop_grad)
elif tensor_type == 1:
num = paddle.to_tensor([0])
paddle.distributed.recv(num, src=src_rank, group=group)
num = num.item()
shapes = []
dtypes = []
stop_grads = []
for i in range(num):
shape, dtype, stop_grad = self._recv_shape_dtype(group)
shapes.append(shape)
dtypes.append(dtype)
stop_grads.append(bool(stop_grad))
self.recv_shape_message = tuple(shapes)
self.recv_dtype_message = tuple(dtypes)
self.recv_stop_gradient = tuple(stop_grads)
def _send_dims_shape_dtype(self, tensor, group):
# send len(shape)
dims = paddle.to_tensor([len(tensor.shape)])
dst_rank = _hcg._get_p2p_next_rank()
paddle.distributed.send(dims, dst=dst_rank, group=group)
# send shape
shape = paddle.to_tensor(tensor.shape)
paddle.distributed.send(shape, dst=dst_rank, group=group)
# send dtype
dtype = paddle.to_tensor([paddle_2_number(tensor.dtype)])
paddle.distributed.send(dtype, dst=dst_rank, group=group)
# send trainable
stop_grad = paddle.to_tensor([int(tensor.stop_gradient)])
paddle.distributed.send(stop_grad, dst=dst_rank, group=group)
def send_meta(self, tensor, group):
dst_rank = _hcg._get_p2p_next_rank()
if isinstance(tensor, paddle.Tensor):
tensor_type = paddle.to_tensor([0])
# send tensor type
paddle.distributed.send(tensor_type, dst=dst_rank, group=group)
self._send_dims_shape_dtype(tensor, group)
elif isinstance(tensor, tuple):
tensor_type = paddle.to_tensor([1])
# send tensor type
paddle.distributed.send(tensor_type, dst=dst_rank, group=group)
nums = paddle.to_tensor([len(tensor)])
paddle.distributed.send(nums, dst=dst_rank, group=group)
for d in tensor:
assert isinstance(d, paddle.Tensor)
self._send_dims_shape_dtype(d, group=group)
def set_send_message(self, tensor):
if isinstance(tensor, paddle.Tensor):
self.send_shape_message = tensor.shape
self.send_dtype_message = paddle_2_number(tensor.dtype)
elif isinstance(tensor, tuple):
self.send_shape_message = tuple(
[d.shape for d in tensor if not d.stop_gradient]
)
self.send_dtype_message = tuple(
[
paddle_2_number(d.dtype)
for d in tensor
if not d.stop_gradient
]
)
def _is_valid_send_recv_partial(tensor, mp_degree):
if not _enable_partial_send_recv:
return False
tensor_numel = np.prod(tensor.shape)
assert tensor_numel != 0, "can't send/recv zero element"
return mp_degree > 1 and tensor_numel % mp_degree == 0
def _partial_send_op(
tensor, group, use_calc_stream, ring_id, dst, nranks, rank_id
):
dst_rank_in_group = dst if group is None else group.get_group_rank(dst)
if framework.in_dynamic_mode():
group = (
paddle.distributed.collective._get_default_group()
if group is None
else group
)
comm_op = (
group.process_group.send_partial_on_calc_stream
if use_calc_stream
else group.process_group.send_partial
)
return comm_op(tensor, dst_rank_in_group, nranks, rank_id)
def send_partial(
tensor, dst=0, nranks=1, rank_id=0, group=None, use_calc_stream=True
):
# dst: local rank in group
if group is not None and not group.is_member():
return
ring_id = 0 if group is None else group.id
dst_rank = (
_hcg._get_p2p_next_rank() if dst == 1 else _hcg._get_p2p_prev_rank()
)
if _is_valid_send_recv_partial(tensor, nranks):
return _partial_send_op(
tensor, group, use_calc_stream, ring_id, dst_rank, nranks, rank_id
)
else:
send_op = paddle.distributed.isend
return send_op(tensor.detach(), dst=dst_rank, group=group)
def _partial_recv_op(
tensor, group, use_calc_stream, ring_id, src, nranks, rank_id
):
src_rank_in_group = src if group is None else group.get_group_rank(src)
group = (
paddle.distributed.collective._get_default_group()
if group is None
else group
)
comm_op = (
group.process_group.recv_partial_on_calc_stream
if use_calc_stream
else group.process_group.recv_partial
)
return comm_op(tensor, src_rank_in_group, nranks, rank_id)
def recv_partial(
tensor, src=0, nranks=1, rank_id=0, group=None, use_calc_stream=True
):
# src: local rank in group
if group is not None and not group.is_member():
return
ring_id = 0 if group is None else group.id
src_rank = (
_hcg._get_p2p_prev_rank() if src == 0 else _hcg._get_p2p_next_rank()
)
if _is_valid_send_recv_partial(tensor, nranks):
return _partial_recv_op(
tensor, group, use_calc_stream, ring_id, src_rank, nranks, rank_id
)
else:
if use_calc_stream:
recv_op = paddle.distributed.recv
elif framework.in_dynamic_mode():
recv_op = paddle.distributed.irecv
return recv_op(tensor.detach(), src=src_rank, group=group)
def _partial_allgather_op(
tensor, group, use_calc_stream, ring_id, nranks, rank_id
):
group = (
paddle.distributed.collective._get_default_group()
if group is None
else group
)
comm_op = (
group.process_group.all_gather_partial_on_calc_stream
if use_calc_stream
else group.process_group.all_gather_partial
)
return comm_op(tensor, tensor, nranks, rank_id)
def allgather_partial(
tensor, nranks=1, rank_id=0, group=None, use_calc_stream=True
):
if not _is_valid_send_recv_partial(tensor, nranks):
return tensor
if group is not None and not group.is_member():
return
ring_id = 0 if group is None else group.id
return _partial_allgather_op(
tensor, group, use_calc_stream, ring_id, nranks, rank_id
)
def _p2p_helper(
tensor_send_next,
tensor_send_prev,
recv_prev,
recv_next,
sync_recv=True,
send_recv_meta=None,
):
global _hcg
tensor_recv_prev = None
tensor_recv_next = None
# send / recv message
assert send_recv_meta is not None, "send_recv_meta should not be None"
recv_shape_msg = send_recv_meta.recv_shape_message
recv_dtype_msg = send_recv_meta.recv_dtype_message
recv_stop_gradient = send_recv_meta.recv_stop_gradient
send_shape_msg = send_recv_meta.send_shape_message
send_dtype_msg = send_recv_meta.send_dtype_message
# model parallel message
mp_group = _hcg.get_model_parallel_group()
mp_degree = _hcg.get_model_parallel_world_size()
mp_rank = _hcg.get_model_parallel_rank()
if recv_prev:
if isinstance(recv_shape_msg, tuple):
tensor_recv_prev = []
for idx, shape in enumerate(recv_shape_msg):
tmp = paddle.empty(
shape=shape, dtype=number_2_dtype(recv_dtype_msg[idx])
)
tmp.stop_gradient = recv_stop_gradient[idx]
tensor_recv_prev.append(tmp)
tensor_recv_prev = tuple(tensor_recv_prev)
else:
tensor_recv_prev = paddle.empty(
shape=recv_shape_msg, dtype=number_2_dtype(recv_dtype_msg)
)
tensor_recv_prev.stop_gradient = recv_stop_gradient
if recv_next:
if isinstance(send_shape_msg, tuple):
tensor_recv_next = []
for idx, shape in enumerate(send_shape_msg):
tensor_recv_next.append(
paddle.empty(
shape=shape, dtype=number_2_dtype(send_dtype_msg[idx])
)
)
tensor_recv_next = tuple(tensor_recv_next)
else:
tensor_recv_next = paddle.empty(
shape=send_shape_msg, dtype=number_2_dtype(send_dtype_msg)
)
# TODO(Yuang Liu): use batch_isend_irecv replace all these comm ops
tasks = []
# start to p2p communicate
if _sync_send:
# Some devices(NPU for example) do not support asynchronized send op, So the order is
# recv_prev -> send_next -> recv_next -> send_prev
# When using this order, the environment variable
# 'PADDLE_P2P_SYNC_SEND' should be set True
if tensor_recv_prev is not None:
if isinstance(tensor_recv_prev, tuple):
for d in tensor_recv_prev:
task = recv_partial(
d,
src=0,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.recv_prev_group,
use_calc_stream=sync_recv,
)
if sync_recv:
allgather_partial(
d,
nranks=mp_degree,
rank_id=mp_rank,
group=mp_group,
use_calc_stream=True,
)
else:
tasks.append(task)
else:
task = recv_partial(
tensor_recv_prev,
src=0,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.recv_prev_group,
use_calc_stream=sync_recv,
)
if sync_recv:
allgather_partial(
tensor_recv_prev,
nranks=mp_degree,
rank_id=mp_rank,
group=mp_group,
use_calc_stream=True,
)
else:
tasks.append(task)
if tensor_send_next is not None:
if isinstance(tensor_send_next, tuple):
for d in tensor_send_next:
paddle.distributed.wait(d, use_calc_stream=True)
send_partial(
d,
dst=1,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.send_next_group,
use_calc_stream=False,
)
else:
paddle.distributed.wait(tensor_send_next, use_calc_stream=True)
send_partial(
tensor_send_next,
dst=1,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.send_next_group,
use_calc_stream=False,
)
if tensor_recv_next is not None:
if isinstance(tensor_recv_next, tuple):
for d in tensor_recv_next:
task = recv_partial(
d,
src=1,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.recv_next_group,
use_calc_stream=sync_recv,
)
if sync_recv:
allgather_partial(
d,
nranks=mp_degree,
rank_id=mp_rank,
group=mp_group,
use_calc_stream=True,
)
else:
tasks.append(task)
else:
task = recv_partial(
tensor_recv_next,
src=1,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.recv_next_group,
use_calc_stream=sync_recv,
)
if sync_recv:
allgather_partial(
tensor_recv_next,
nranks=mp_degree,
rank_id=mp_rank,
group=mp_group,
use_calc_stream=True,
)
else:
tasks.append(task)
if tensor_send_prev is not None:
if isinstance(tensor_send_prev, tuple):
for d in tensor_send_prev:
paddle.distributed.wait(d, use_calc_stream=True)
send_partial(
d,
dst=0,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.send_prev_group,
use_calc_stream=False,
)
else:
paddle.distributed.wait(tensor_send_prev, use_calc_stream=True)
send_partial(
tensor_send_prev,
dst=0,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.send_prev_group,
use_calc_stream=False,
)
else:
_xpu_comm_group_start()
if tensor_send_prev is not None:
if isinstance(tensor_send_prev, tuple):
for d in tensor_send_prev:
paddle.distributed.wait(d, use_calc_stream=True)
send_partial(
d,
dst=0,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.send_prev_group,
use_calc_stream=False,
)
else:
paddle.distributed.wait(tensor_send_prev, use_calc_stream=True)
send_partial(
tensor_send_prev,
dst=0,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.send_prev_group,
use_calc_stream=False,
)
if tensor_recv_prev is not None:
if isinstance(tensor_recv_prev, tuple):
for d in tensor_recv_prev:
task = recv_partial(
d,
src=0,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.recv_prev_group,
use_calc_stream=sync_recv,
)
if sync_recv:
_xpu_comm_group_end()
allgather_partial(
d,
nranks=mp_degree,
rank_id=mp_rank,
group=mp_group,
use_calc_stream=True,
)
else:
tasks.append(task)
else:
task = recv_partial(
tensor_recv_prev,
src=0,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.recv_prev_group,
use_calc_stream=sync_recv,
)
if sync_recv:
_xpu_comm_group_end()
allgather_partial(
tensor_recv_prev,
nranks=mp_degree,
rank_id=mp_rank,
group=mp_group,
use_calc_stream=True,
)
else:
tasks.append(task)
if tensor_send_next is not None:
if isinstance(tensor_send_next, tuple):
for d in tensor_send_next:
paddle.distributed.wait(d, use_calc_stream=True)
send_partial(
d,
dst=1,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.send_next_group,
use_calc_stream=False,
)
else:
paddle.distributed.wait(tensor_send_next, use_calc_stream=True)
send_partial(
tensor_send_next,
dst=1,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.send_next_group,
use_calc_stream=False,
)
if tensor_recv_next is not None:
if isinstance(tensor_recv_next, tuple):
for d in tensor_recv_next:
task = recv_partial(
d,
src=1,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.recv_next_group,
use_calc_stream=sync_recv,
)
if sync_recv:
_xpu_comm_group_end()
allgather_partial(
d,
nranks=mp_degree,
rank_id=mp_rank,
group=mp_group,
use_calc_stream=True,
)
else:
tasks.append(task)
else:
task = recv_partial(
tensor_recv_next,
src=1,
nranks=mp_degree,
rank_id=mp_rank,
group=_hcg.recv_next_group,
use_calc_stream=sync_recv,
)
if sync_recv:
_xpu_comm_group_end()
allgather_partial(
tensor_recv_next,
nranks=mp_degree,
rank_id=mp_rank,
group=mp_group,
use_calc_stream=True,
)
else:
tasks.append(task)
_xpu_comm_group_end()
if not sync_recv:
if framework.in_dynamic_mode():
# wait irecv tasks in eager dygraph mode with new comm library
for task in tasks:
assert task is not None
task.wait()
tensors_for_all_gather = []
if tensor_recv_prev is not None:
if isinstance(tensor_recv_prev, tuple):
for d in tensor_recv_prev:
tensors_for_all_gather.append(d)
else:
tensors_for_all_gather.append(tensor_recv_prev)
if tensor_recv_next is not None:
if isinstance(tensor_recv_next, tuple):
for d in tensor_recv_next:
tensors_for_all_gather.append(d)
else:
tensors_for_all_gather.append(tensor_recv_next)
for tensor in tensors_for_all_gather:
allgather_partial(
tensor,
nranks=mp_degree,
rank_id=mp_rank,
group=mp_group,
use_calc_stream=True,
)
return tensor_recv_prev, tensor_recv_next
class P2pHelper:
def __init__(self, use_cache=True):
self._send_recv_meta = SendRecvMeta()
self._use_cache = use_cache
def _send_meta(self, output_tensor, skip_check_meta=False):
if not self._send_recv_meta.has_send_meta:
self._send_recv_meta.set_send_message(output_tensor)
self._send_recv_meta.send_meta(
output_tensor, _hcg.get_pipe_parallel_group()
)
self._send_recv_meta.has_send_meta = self._use_cache
def _recv_meta(self):
if not self._send_recv_meta.has_recv_meta:
self._send_recv_meta.recv_meta(_hcg.get_pipe_parallel_group())
self._send_recv_meta.has_recv_meta = self._use_cache
def recv_forward(self, pp_first_stage, sync_recv=True):
global _timers
if _timers is not None:
_timers("recv_forward").start()
if pp_first_stage:
input_tensor = None
else:
self._recv_meta()
input_tensor, _ = _p2p_helper(
tensor_send_next=None,
tensor_send_prev=None,
recv_prev=True,
recv_next=False,
sync_recv=sync_recv,
send_recv_meta=self._send_recv_meta,
)
if _timers is not None:
_timers("recv_forward").stop()
return input_tensor
def recv_backward(self, pp_last_stage, sync_recv=True):
global _timers
if _timers is not None:
_timers("recv_backward").start()
if pp_last_stage:
output_tensor_grad = None
else:
_, output_tensor_grad = _p2p_helper(
tensor_send_next=None,
tensor_send_prev=None,
recv_prev=False,
recv_next=True,
sync_recv=sync_recv,
send_recv_meta=self._send_recv_meta,
)
if _timers is not None:
_timers("recv_backward").stop()
return output_tensor_grad
def send_forward(self, output_tensor, pp_last_stage, skip_check_meta=False):
global _timers
if _timers is not None:
_timers("send_forward").start()
if not pp_last_stage:
self._send_meta(output_tensor, skip_check_meta=skip_check_meta)
_p2p_helper(
tensor_send_next=output_tensor,
tensor_send_prev=None,
recv_prev=False,
recv_next=False,
send_recv_meta=self._send_recv_meta,
)
if _timers is not None:
_timers("send_forward").stop()
def send_backward(self, input_tensor_grad, pp_first_stage):
global _timers
if _timers is not None:
_timers("send_backward").start()
if not pp_first_stage:
_p2p_helper(
tensor_send_next=None,
tensor_send_prev=input_tensor_grad,
recv_prev=False,
recv_next=False,
send_recv_meta=self._send_recv_meta,
)
if _timers is not None:
_timers("send_backward").stop()
def send_forward_recv_backward(self, output_tensor, pp_last_stage):
global _timers
if _timers is not None:
_timers("send_forward_recv_backward").start()
if pp_last_stage:
output_tensor_grad = None
else:
_, output_tensor_grad = _p2p_helper(
tensor_send_next=output_tensor,
tensor_send_prev=None,
recv_prev=False,
recv_next=True,
send_recv_meta=self._send_recv_meta,
)
if _timers is not None:
_timers("send_forward_recv_backward").stop()
return output_tensor_grad
def send_backward_recv_forward(self, input_tensor_grad, pp_first_stage):
global _timers
if _timers is not None:
_timers("send_backward_recv_forward").start()
if pp_first_stage:
input_tensor = None
else:
input_tensor, _ = _p2p_helper(
tensor_send_next=None,
tensor_send_prev=input_tensor_grad,
recv_prev=True,
recv_next=False,
send_recv_meta=self._send_recv_meta,
)
if _timers is not None:
_timers("send_backward_recv_forward").stop()
return input_tensor
def send_forward_backward_recv_forward_backward(
self, output_tensor, input_tensor_grad, recv_prev, recv_next
):
# always have to send dtype info to downstream
global _timers
if _timers is not None:
_timers("send_forward_backward_recv_forward_backward").start()
self._send_meta(output_tensor)
if recv_prev:
self._recv_meta()
input_tensor, output_tensor_grad = _p2p_helper(
tensor_send_next=output_tensor,
tensor_send_prev=input_tensor_grad,
recv_prev=recv_prev,
recv_next=recv_next,
sync_recv=False,
send_recv_meta=self._send_recv_meta,
)
if _timers is not None:
_timers("send_forward_backward_recv_forward_backward").stop()
return input_tensor, output_tensor_grad
def send_forward_recv_forward(self, output_tensor, recv_prev):
# always have to send dtype info to downstream
global _timers
if _timers is not None:
_timers("send_forward_recv_forward").start()
if output_tensor is not None:
self._send_meta(output_tensor)
if recv_prev:
self._recv_meta()
input_tensor, _ = _p2p_helper(
tensor_send_next=output_tensor,
tensor_send_prev=None,
recv_prev=recv_prev,
recv_next=False,
sync_recv=False,
send_recv_meta=self._send_recv_meta,
)
if _timers is not None:
_timers("send_forward_recv_forward").stop()
return input_tensor
def send_backward_recv_backward(self, input_tensor_grad, recv_next):
global _timers
if _timers is not None:
_timers("send_backward_recv_backward").start()
_, output_tensor_grad = _p2p_helper(
tensor_send_next=None,
tensor_send_prev=input_tensor_grad,
recv_prev=False,
recv_next=recv_next,
sync_recv=False,
send_recv_meta=self._send_recv_meta,
)
if _timers is not None:
_timers("send_backward_recv_backward").stop()
return output_tensor_grad
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
# Copyright (c) 2023 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 os
def main():
all_record = []
all_files = os.listdir('./')
all_files = sorted(
filter(
lambda file: file.startswith("profile_record_tmp_file_for_rank_"),
all_files,
)
)
for files in all_files:
with open(files, 'r') as f:
for line in f:
all_record.append(line.strip())
with open('pipeline_profile.json', 'w') as f:
f.write('[ ')
f.writelines(all_record[i] + ',\n' for i in range(len(all_record) - 1))
f.write(all_record[-1])
f.write(' ]\n')
if __name__ == "__main__":
main()
@@ -0,0 +1,160 @@
# Copyright (c) 2023 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
from paddle import _C_ops
__all__ = []
FLOAT_TYPE_DICT = {
paddle.float16: "float16",
paddle.float32: "float32",
paddle.float64: "float64",
paddle.bfloat16: "bfloat16",
paddle.bool: "bool",
}
PADDLE_TO_NUMBER = {
paddle.float16: 0,
paddle.float32: 1,
paddle.float64: 2,
paddle.int32: 3,
paddle.int64: 4,
paddle.bfloat16: 5,
paddle.bool: 6,
}
NUMBER_TO_DTYPE = {
0: "float16",
1: "float32",
2: "float64",
3: "int32",
4: "int64",
5: "bfloat16",
6: "bool",
}
def is_float_tensor(tensor):
"""Is a float tensor"""
return tensor.dtype in FLOAT_TYPE_DICT.keys()
def get_tensor_dtype(dtype):
assert dtype in FLOAT_TYPE_DICT.keys()
return FLOAT_TYPE_DICT[dtype]
def paddle_2_number(dtype):
assert dtype in PADDLE_TO_NUMBER.keys()
return PADDLE_TO_NUMBER[dtype]
def number_2_dtype(number):
assert number in NUMBER_TO_DTYPE.keys()
return NUMBER_TO_DTYPE[number]
def get_tensor_bytes(tensor):
"""Get the bytes a tensor occupied."""
elem_size = None
if tensor.dtype == paddle.float32:
elem_size = 4
elif tensor.dtype == paddle.float64:
elem_size = 8
elif tensor.dtype == paddle.int64:
elem_size = 8
elif tensor.dtype == paddle.int32:
elem_size = 4
elif tensor.dtype == paddle.float16:
elem_size = 2
elif tensor.dtype == paddle.int8:
elem_size = 1
else:
raise ValueError(f"unknown data type: {tensor.dtype}")
return tensor.numel() * elem_size
def _all_gather(tensor, group=None, use_calc_stream=True):
"""
The main difference with paddle.distributed.all_gather:
no need to pass in tensor_list, the returned tensor is spliced
"""
if group is not None and not group.is_member():
return
ring_id = 0 if group is None else group.id
nranks = (
paddle.distributed.collective._get_global_group().nranks
if group is None
else group.nranks
)
return _C_ops.all_gather(
tensor,
ring_id,
nranks,
)
def tuple_to_dict_helper(input_tensor):
# recv tuple -> fwd input dict
use_dict = False
if isinstance(input_tensor, tuple):
use_dict = hasattr(input_tensor[0], "key")
else: # single tensor
use_dict = hasattr(input_tensor, "key")
if use_dict:
input_tensor = convert_tensor_tuple_to_dict(input_tensor)
return input_tensor, use_dict
def dict_to_tuple_helper(output_tensor):
if isinstance(output_tensor, dict):
output_tensor_tuple = convert_tensor_dict_to_tuple(
output_tensor_dict=output_tensor
)
else: # single tensor or tensor tuple
output_tensor_tuple = output_tensor
return output_tensor_tuple
def convert_tensor_dict_to_tuple(output_tensor_dict):
output_tensor = []
for key, tensor in output_tensor_dict.items():
if isinstance(tensor, (list, tuple)):
for idx, t in enumerate(tensor):
t.key = key + " " + str(idx)
output_tensor.append(t)
else: # single tensor
tensor.key = key
output_tensor.append(tensor)
return tuple(output_tensor)
def convert_tensor_tuple_to_dict(input_tensor_tuple):
input_tensor_dict = {}
for tensor in input_tensor_tuple:
key = tensor.key
if " " in key:
real_key, _ = key.split(" ")
if real_key in input_tensor_dict.keys():
input_tensor_dict[real_key].append(tensor)
else:
input_tensor_dict[real_key] = [tensor]
else:
input_tensor_dict[key] = tensor
delattr(tensor, "key")
return input_tensor_dict
@@ -0,0 +1,40 @@
# Copyright (c) 2023 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 ..utils.hybrid_parallel_util import (
broadcast_dp_parameters,
broadcast_sep_parameters,
broadcast_sharding_parameters,
)
from ..utils.log_util import logger
from .meta_parallel_base import MetaParallelBase
__all__ = []
class SegmentParallel(MetaParallelBase):
def __init__(self, layers, hcg, **kwargs):
super().__init__(layers, hcg, **kwargs)
def _prepare_for_model(self):
logger.info("start broadcast sep parameters")
broadcast_sep_parameters(self._layers, self._hcg)
if self._hcg.get_sharding_parallel_world_size() > 1:
logger.info("start broadcast sharding parameters")
broadcast_sharding_parameters(self._layers, self._hcg)
if self._hcg.get_data_parallel_world_size() > 1:
logger.info("start broadcast dp parameters")
broadcast_dp_parameters(self._layers, self._hcg)
@@ -0,0 +1,13 @@
# Copyright (c) 2021 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.
@@ -0,0 +1,730 @@
# 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 logging
from collections import OrderedDict
from types import MethodType
import numpy as np
import paddle
import paddle.distributed as dist
from paddle import nn
from paddle.distributed import collective, fleet
from paddle.framework import core
from paddle.nn import ClipGradByGlobalNorm
from .group_sharded_stage3 import (
ForwardPostHooks,
ForwardPreHooks,
OrderedSet,
TaskFlow,
_current_layer_params,
_PartitionParam,
_TensorWrapper,
_UnsliceParam,
align,
alignment,
)
from .group_sharded_storage import GradStorage
from .group_sharded_utils import GroupShardedClipGrad, Type, device_guard
def _OptimizerWrapper(optimizer, offload, group, update_params_slice):
if not hasattr(optimizer, "_optim"):
optimizer._optim = optimizer
optimizer.offload = offload
optimizer._group = group
optimizer.update_scaler = None
optimizer.update_slice = update_params_slice
return optimizer
class FullyShardOptimizer:
def __init__(
self,
optimizer,
group=None,
sync_buffers=False,
device="xpu" if core.is_compiled_with_xpu() else "gpu",
segment_size=2**20,
pretrain_sync_models=True,
offload=False,
sync_comm=False,
dp_group=None,
exclude_layer=None,
):
self._default_device = device
self.__sync_buffers = sync_buffers
self._offload = offload
self._sync_comm = sync_comm
# stage3 support some layer set by users to be unslice
# _exclude_layer=[layer_name or id(layer)]
self._exclude_layer = [] if exclude_layer is None else exclude_layer
assert isinstance(self._exclude_layer, (list, tuple)), (
"the exclude_layers must be a list with layers' name or layers' id"
)
# segmentation size
assert segment_size >= 0, "segment_size must be GE than 0."
self._segment_size = segment_size
global param2dtype
param2dtype = {}
hcg = fleet.get_hybrid_communicate_group()
group = hcg.get_sharding_parallel_group()
# Communication group establishment
self._group = (
collective.new_group(collective._get_global_group().ranks)
if group is None
else group
)
self._dp_group = dp_group
self._world_size_scaling = 1.0 / self._group.nranks
assert self._group.nranks > 1, (
"Training must be distributed, ranks must be greater than 1."
)
self._rank = self._group.rank
self._global_root_rank = self._group.ranks[
0
] # picking ranks index 0 as the reference
# Parameter segmentation for global ranks
self._unslice_params = OrderedSet() # param's numel <= segment_size
self._unslice_params2align = {} # {param.name: param's align}
self._grad_storages = {} # {param.dtype: GradStorage}
assert not isinstance(optimizer, list), (
"Multiple optimizers are not supported now."
)
self._optim = _OptimizerWrapper(
optimizer,
self._offload,
self._group,
self._update_params_slice,
)
self._ori_parameter_list = self._optim._parameter_list
self._ori_param_groups = self._optim._param_groups
for p in self._ori_parameter_list:
del p._need_shard
if p._numel() > self._segment_size:
pass
elif p.trainable:
self._unslice_params.add(_UnsliceParam(p))
# check main_grad
self._check_main_grad()
# Replace optimizer's _grad_clip
if isinstance(self._optim._grad_clip, ClipGradByGlobalNorm):
logging.warning(
"While using ClipGradByGlobalNorm in GroupShardedStage3, the grad clip of original optimizer will be changed."
)
if self.use_main_grad:
self._optim._inner_opt._grad_clip = GroupShardedClipGrad(
self._optim._inner_opt._grad_clip,
paddle.get_device(),
self._group,
)
else:
self._optim._grad_clip = GroupShardedClipGrad(
self._optim._grad_clip, paddle.get_device(), self._group
)
if self._optim._parameter_list and isinstance(
self._optim._parameter_list[0], dict
):
for item in self._optim._param_groups:
if "grad_clip" in item.keys():
item["grad_clip"] = self._optim._grad_clip
# Add unslice params to master_weight in fp16
self._setup_master_weights_for_unslice()
# Redefine optimizer step and clear function
self._redefine_opt_step()
self._redefine_opt_clear()
def _check_main_grad(self):
self.use_main_grad = None
for param in self._ori_parameter_list:
if self.use_main_grad is None and hasattr(param, "main_grad"):
self.use_main_grad = True
if self.use_main_grad:
assert hasattr(param, "main_grad"), (
"Params have different main grad attributes."
)
def _setup_master_weights_for_unslice(self):
for param in self._unslice_params:
# Update optimizer master weights
if (
param.dtype == Type.fp16.value or param.dtype == Type.bf16.value
) and not self._offload:
master_tensor = paddle.cast(param, Type.fp32.value)
master_tensor.name = param.name
self._optim._master_weights[param.name] = master_tensor
def _clear_gradients(self):
current_layer_params = self._ori_parameter_list
# 1.Handle param's slice
trainable_params = list(
filter(
lambda p: p.trainable and p not in self._unslice_params,
current_layer_params,
)
)
for param in trainable_params:
if not hasattr(param, "fw_storage"):
continue
assert hasattr(param, "fw_storage"), (
f"Find {param.name} don't have fw_storage attribute."
)
if self.use_main_grad:
param.fw_storage.main_grad._clear()
param.fw_storage.main_grad = None
else:
param.fw_storage.clear_gradient(False)
param.bw_storage._clear()
param.bw_storage = None
# Update param memory slice
def _update_params_slice(self):
update_list = self._update_params()
if not isinstance(self._optim._param_groups[0], dict):
slice_params = [param.fw_storage for param in update_list]
self._optim._parameter_list = slice_params + list(
self._unslice_params
)
self._optim._param_groups = slice_params + list(
self._unslice_params
)
else:
for param_group in self._optim._param_groups:
p_group = []
for p in param_group['params']:
if hasattr(p, "fw_storage"):
p_group.append(p.fw_storage)
else:
p_group.append(p)
param_group['params'] = p_group
def _update_params(self):
"""
Update parameters to optimizer memory slice.
"""
update_list = []
current_layer_params = self._ori_parameter_list
trainable_params = list(
filter(
lambda p: p.trainable and p not in self._unslice_params,
current_layer_params,
)
)
# 1.Handle param's slice
for param in trainable_params:
assert hasattr(param, "fw_storage"), (
f"Find {param.name} don't have fw_storage attribute"
)
param.fw_storage = _TensorWrapper(param)
if self.use_main_grad:
param.fw_storage.main_grad = param.bw_storage
else:
assert param.fw_storage.grad is None
param.fw_storage._copy_gradient_from(param.bw_storage)
update_list.append(param)
return update_list
def _redefine_opt_step(self):
params_slice_func = self._update_params_slice
opt_step = self._optim.step
def _opt_step(self):
if not self.update_scaler:
params_slice_func()
opt_step()
self._optim.step = MethodType(_opt_step, self._optim)
def _redefine_opt_clear(self):
clear_func = self._clear_gradients
def _opt_clear(self):
clear_func()
self._optim.clear_grad = MethodType(_opt_clear, self._optim)
class FullyShard(nn.Layer):
"""
A wrapper for Sharding Stage3 Layer in Dygraph.
.. warning: GroupShardedStage3 encapsulates the layer strategy and integrates it into the nn.Layer.
.. ZeRO: https://arxiv.org/pdf/1910.02054.pdf.
"""
def __init__(
self,
layer,
group=None,
sync_buffers=False,
device="xpu" if core.is_compiled_with_xpu() else "gpu",
segment_size=2**20,
pretrain_sync_models=True,
offload=False,
sync_comm=False,
dp_group=None,
exclude_layer=None,
):
super().__init__()
# Default configs
assert (
core.is_compiled_with_cuda()
or core.is_compiled_with_xpu()
or (device in core.get_all_custom_device_type())
), "Only support CUDA / XPU / CustomDevice."
self._layer = layer
self._default_device = device
self.__sync_buffers = sync_buffers
self._offload = offload
self._sync_comm = sync_comm
# stage3 support some layer set by users to be unslice
self._exclude_layer = [] if exclude_layer is None else exclude_layer
assert isinstance(self._exclude_layer, (list, tuple)), (
"the exclude_layers must be a list with layers' name or layers' id"
)
# segmentation size
assert segment_size >= 0, "segment_size must be GE than 0."
self._segment_size = segment_size
global DEV
DEV = (
"cpu"
if paddle.get_device() == "cpu"
else paddle.get_device().split(":")[0]
)
global DEV_ID
DEV_ID = (
0
if paddle.get_device() == "cpu"
else int(paddle.get_device().split(":")[1])
)
global param2dtype
param2dtype = {}
hcg = fleet.get_hybrid_communicate_group()
group = hcg.get_sharding_parallel_group()
self._group = (
collective.new_group(collective._get_global_group().ranks)
if group is None
else group
)
self._dp_group = dp_group
self._world_size_scaling = 1.0 / self._group.nranks
assert self._group.nranks > 1, (
"Training must be distributed, ranks must be greater than 1."
)
self._rank = self._group.rank
self._global_root_rank = self._group.ranks[
0
] # picking ranks index 0 as the reference
# Parameter segmentation for global ranks
# After flatten -> self._param2buffer_size, self._param2buffer, self._trainable_params
self._param2buffer_size = {} # {param.name: size}
self._param2buffer = {} # {param.name: [(start0, end0),(start1, end1), ...]}
self._trainable_params = {} # {id(layer): [trainable_params]}
self._unslice_params = OrderedSet() # param's numel <= segment_size
self._unslice_params2align = {} # {param.name: param's align}
self._grad_storages = {} # {param.dtype: GradStorage}
self._ori_parameter_list = self._layer.parameters()
for param in self._ori_parameter_list:
param._need_shard = True
# check main_grad
self._check_main_grad()
# Synchronous all ranks models
if pretrain_sync_models:
self._sync_params_and_buffers()
self._segment_rank_params(self._layer)
# In the first step, record the execution order of the layer
self._order_tracer = OrderedDict()
self._order_tracer["order"] = 0
self._order_tracer["layer"] = []
# Add unslice params GradStorage
self._handle_unslice_params()
# Register task flow
self._task_flow = TaskFlow()
# Register forward hooks
self._register_forward_hooks(self._layer)
# Register backward parameter hooks
self._register_backward_hooks()
def _handle_unslice_params(self):
buffer_size = {}
buffer_size[Type.bf16.value] = 0
buffer_size[Type.fp32.value] = 0
buffer_size[Type.fp16.value] = 0
for param in self._unslice_params:
param2dtype[param.name] = param.dtype
p_align = self._param2align(param)
self._unslice_params2align[param.name] = p_align
buffer_size[param.dtype] += param._numel() + p_align
# Create unslice_params'grad
for param in sorted(self._unslice_params, key=lambda p: p.name):
if param.dtype not in self._grad_storages.keys():
self._grad_storages[param.dtype] = GradStorage(
buffer_size[param.dtype],
dtype=(
param.dtype
if not self.use_main_grad
else paddle.float32
),
device=self._default_device,
destination=self._rank,
param2align=self._unslice_params2align,
)
self._grad_storages[param.dtype].add_grad(
param, self._unslice_params2align[param.name]
)
def _check_main_grad(self):
self.use_main_grad = None
for param in self._layer.parameters():
if self.use_main_grad is None and hasattr(param, "main_grad"):
self.use_main_grad = True
if self.use_main_grad:
assert hasattr(param, "main_grad"), (
"Params have different main grad attributes."
)
@paddle.autograd.no_grad()
def _sync_params_and_buffers(self):
"""
Sync all model states for all ranks
"""
for p in self._layer.parameters():
dist.broadcast(
p, src=self._global_root_rank, group=self._group, sync_op=True
)
if self._dp_group is not None and self._dp_group.nranks > 1:
dist.broadcast(
p,
src=self._dp_group.ranks[0],
group=self._dp_group,
sync_op=True,
)
def _sync_grad_storages_hook(self):
for grad_storage in self._grad_storages.values():
grad_storage.buffer.scale_(scale=self._world_size_scaling)
dist.all_reduce(tensor=grad_storage.buffer, group=self._group)
if self._dp_group is not None and self._dp_group.nranks > 1:
grad_storage.buffer.scale_(scale=(1.0 / self._dp_group.nranks))
dist.all_reduce(
tensor=grad_storage.buffer, group=self._dp_group
)
def forward(self, *inputs, **kwargs):
"""
A wrapper for Sharding Stage3 layer.
"""
# add hook to sync grad storage
for grad_storage in self._grad_storages.values():
grad_storage.buffer.zero_()
grad_storage.manual_release()
grad_storage.rebuild()
core.eager._add_backward_final_hook(self._sync_grad_storages_hook)
# 1.Sync layer's buffers state
if self.__sync_buffers:
self._sync_buffers()
# 2.Normal FW on the base model
fw = self._layer(*inputs, **kwargs)
return fw
def _segment_rank_params(self, layer, name="last_layer"):
"""
Flatten parameters according to layer.
"""
current_layer_params = _current_layer_params(layer)
if current_layer_params:
self._flatten_layer_params(layer, current_layer_params)
for name, sub_layer in layer.named_children():
self._segment_rank_params(sub_layer, name)
def _flatten_layer_params(self, layer, current_layer_params):
"""
Parameter segmentation and memory integration.
"""
if id(layer) in self._trainable_params.keys():
return
def _add_manage_info(trainable_param):
return _PartitionParam(trainable_param)
current_params = []
for p in current_layer_params:
if p._numel() > self._segment_size:
current_params.append(_add_manage_info(p))
elif p.trainable:
self._unslice_params.add(_UnsliceParam(p))
self._trainable_params[id(layer)] = current_params
for param in self._trainable_params[id(layer)]:
if param.name in self._param2buffer.keys():
continue
self._param2buffer[param.name] = []
# 1.Params alignment
align_ = self._param2align(param)
offset = align_ + param._numel()
buffer_size = (
offset
if offset % self._group.nranks == 0
else offset + self._group.nranks - (offset % self._group.nranks)
)
self._param2buffer_size[param.name] = buffer_size
# 2.Combination param buffer
assert buffer_size % self._group.nranks == 0
pre_buffer = buffer_size // self._group.nranks
for rank_ in range(self._group.nranks):
self._param2buffer[param.name].append(
(rank_ * pre_buffer, (rank_ + 1) * pre_buffer)
)
# Record param's dtype
param2dtype[param.name] = param.dtype
# 3.Flatten layer params and release other rank buffer
self._param_storage(param, buffer_size)
def _param_storage(self, param, buffer_size):
"""
This is a function to simplify the handling of parameter InternalStorages.
"""
assert isinstance(buffer_size, int)
value = (
np.zeros(buffer_size, dtype=np.float16)
if (
Type.fp16.value == param.dtype or Type.bf16.value == param.dtype
)
else np.zeros(buffer_size, dtype=np.float32)
)
buffer = core.eager.Tensor(value=value, place=core.CPUPlace())
if Type.bf16.value == param.dtype:
buffer = buffer.cast(Type.bf16.value)
param_shape = param.shape
origin_state = param.stop_gradient
param.stop_gradient = True
param.flatten_()
param.stop_gradient = origin_state
start, end = self._param2buffer[param.name][self._rank]
# Copy the current param value
with device_guard():
tmp_var = buffer._slice(0, param._numel())
param_cpu = param.cpu()
tmp_var.get_tensor().set(param_cpu.get_tensor(), core.CPUPlace())
del tmp_var
param.get_tensor()._set_dims(param_shape)
# Current rank param_storage
param.fw_storage = core.eager.Tensor(
value=buffer._slice(start, end), name="slice@" + param.name
)
param.status = "part"
param._clear_data()
def _register_forward_hooks(self, layer):
"""
Register PyLayer to manage memory slices.
There are four stages:
FW
1. Before the forward layers, synchronize the full parameters.
2. After the forward layers, release the full parameter and keep the parameter slice.
BW
3. Before the backward layers, synchronize the full parameters and create param's grad.
4. After the gradient accumulation, release the full parameter and keep the parameter slice.
"""
current_layer_params = _current_layer_params(layer)
if current_layer_params:
# the layer in self._exclude_layer will be added hooks.
if not (
id(layer) in self._exclude_layer
or layer.__class__.__name__ in self._exclude_layer
):
self._register_forward_all_hooks(layer, self._task_flow)
for _, sub_layer in layer.named_children():
self._register_forward_hooks(sub_layer)
def _register_forward_all_hooks(self, sub_layer, task_flow):
def _forward_pre_hook(layer, inputs):
return ForwardPreHooks(
layer,
self._order_tracer,
self._trainable_params,
self._param2buffer_size,
self._group,
self._sync_comm,
self._offload,
task_flow,
)
def _forward_post_hook(layer, inputs, outputs):
if isinstance(outputs, paddle.Tensor):
outputs = (outputs,)
return ForwardPostHooks.apply(
*outputs,
layer=layer,
order_tracer=self._order_tracer,
trainable_params=self._trainable_params,
param2buffer=self._param2buffer,
param2buffer_size=self._param2buffer_size,
rank=self._rank,
group=self._group,
sync_comm=self._sync_comm,
offload=self._offload,
task_flow=task_flow,
)
# register previous forward hooks
sub_layer.register_forward_pre_hook(_forward_pre_hook)
# register post forward hooks
sub_layer.register_forward_post_hook(_forward_post_hook)
@paddle.autograd.no_grad()
def _sync_buffers(self):
"""
Sync all the param buffers from all ranks (exp: batch norm statistics).
"""
for buffer in self._layer.buffers(include_sublayers=True):
dist.broadcast(
buffer, self._global_root_rank, self._group, sync_op=True
)
if self._dp_group is not None and self._dp_group.nranks > 1:
dist.broadcast(
buffer,
self._dp_group.ranks[0],
self._dp_group,
sync_op=True,
)
def __getattr__(self, name):
"""Forward missing attributes to wrapped layer."""
try:
return super().__getattr__(name)
except AttributeError:
return getattr(self._layer, name)
def _register_backward_hooks(self):
current_layer_params = self._layer.parameters(include_sublayers=True)
trainable_params = list(
filter(
lambda p: p.trainable and p not in self._unslice_params,
current_layer_params,
)
)
for param in trainable_params:
allreduce_function = self._get_allreduce_fn(param)
param._register_backward_hook(allreduce_function)
def _get_allreduce_fn(self, param):
@paddle.autograd.no_grad()
def allreduce_(*_):
assert param.trainable, (
"the param must be trainable for grad allreduced"
)
if param.name in self._task_flow.full_grad.keys():
full_grad = self._task_flow.full_grad[param.name]
# Only support sync allreduce current rank's layer now
full_grad.scale_(scale=self._world_size_scaling)
dist.all_reduce(tensor=full_grad, group=self._group)
if self._dp_group is not None and self._dp_group.nranks > 1:
full_grad.scale_(scale=1.0 / self._dp_group.nranks)
dist.all_reduce(tensor=full_grad, group=self._dp_group)
start, end = self._param2buffer[param.name][self._rank]
if param.bw_storage is None:
param.bw_storage = (
full_grad._slice(start, end).detach().clone()
)
else:
param.bw_storage = paddle.add(
param.bw_storage,
full_grad._slice(start, end).detach().clone(),
)
if self.use_main_grad:
param.main_grad = None
else:
param.clear_gradient(False)
del self._task_flow.full_grad[param.name]
if param.name in self._task_flow.full_param.keys():
if param.status == "all":
param.use_count = 0
param._clear_data()
start, end = self._param2buffer[param.name][self._rank]
param.fw_storage = (
self._task_flow.full_param[param.name][0]
._slice(start, end)
.detach()
.clone()
)
param.status = "part"
del self._task_flow.full_param[param.name]
return allreduce_
def _param2align(self, param):
# CUDA alignment 256 bytes
size = param._numel() * align[param.dtype]
device_alignment = alignment[self._default_device]
remaining = size % device_alignment
ali = 0 if remaining == 0 else device_alignment - remaining
align_ = ali // align[param.dtype]
return align_
@@ -0,0 +1,792 @@
# Copyright (c) 2022 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.
# The file has been adapted from fairscale file:
# https://github.com/facebookresearch/fairscale/blob/main/fairscale/optim/oss.py
# Git commit hash: 8acbec718f3c70a6b9785470bb9e05cd84fc3f8e
# We retain the following license from the original files:
# Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
#
# This source code is licensed under the BSD license found in the
# LICENSE file in the root directory of this source tree.
import logging
import warnings
from collections import OrderedDict
import paddle
import paddle.distributed as dist
from paddle.distributed import ParallelMode, fleet
from paddle.distributed.flex_checkpoint.dcp.sharded_weight import (
ShardedStateDict,
ShardedWeight,
create_sharded_weight_with_new_local,
)
from paddle.framework import core
from paddle.nn import ClipGradByGlobalNorm
from paddle.optimizer import Optimizer
HybridParallelClipGrad = fleet.meta_optimizers.dygraph_optimizer.hybrid_parallel_optimizer.HybridParallelClipGrad
from paddle.distributed.collective import _get_global_group, new_group
from .group_sharded_storage import GradStorage, ParamStorage
from .group_sharded_utils import GroupShardedClipGrad, Type, device_guard
# CUDA alignment 256 bytes, cpu alignment 4096 bytes
alignment = {"gpu": 256, "cpu": 4096, "xpu": 256}
align = {
Type.fp16.value: 2,
Type.bf16.value: 2,
Type.fp32.value: 4,
}
class GroupShardedOptimizerStage2(Optimizer):
"""
A wrapper for Sharding Stage2 Optimizer in Dygraph.
.. warning: ShardingOptimizer encapsulates the optimization strategy and integrates it into the optimizer.
.. ZeRO: 1.https://arxiv.org/pdf/1910.02054.pdf 2.https://arxiv.org/pdf/1910.02054.pdf.
"""
# TODO (Baibaifan)
# Feature Notes:
# 1. Unified memory for parameters and parameters.grad to InternalStorage.
# 2. Support the segmentation of optimizer parameters and partial updating of parameters.
# 3. Dynamically adjust training parameters and models.
# 4. Support offload function.
# 5. Support the establishment of independent communication groups.
# 6. Broadcast_fp16 is not supported now.
def __init__(
self,
params,
optim,
group=None,
offload=False,
device="xpu" if core.is_compiled_with_xpu() else "gpu",
pretrain_sync_models=True,
dp_group=None,
**kw,
):
super().__init__(learning_rate=optim._learning_rate, parameters=params)
assert (
core.is_compiled_with_cuda()
or core.is_compiled_with_xpu()
or (device in core.get_all_custom_device_type())
), "Only GPU and XPU and CustomDevice is supported now"
# Segmentation information
self._dtype_rank_params = (
OrderedDict()
) # {dtype:[param1,param2]} device, rank, params
self._param2rank = {}
self.__segment_params = []
self._rank_buffer_size = {} # {dtype: {rank: numel+alignment}}
self._param2align = {} # {param.name: align}
# Default information
self._optim = optim
# sharing stage 2 comm overlap flag
self._reduce_overlap = False
# record the last task used for comm overlap for sharding stage 2
self._comm_task = None
assert hasattr(self._optim, "_master_weights"), (
"Must use optimizer with _master_weights attribute"
)
# Support parameter group and parameter list
self._local_params = []
if isinstance(params[0], dict):
for param_group in params:
self._local_params.extend(list(param_group["params"]))
else:
self._local_params.extend(list(params))
self.use_main_grad = None
for param in self._local_params:
if self.use_main_grad is None and hasattr(param, "main_grad"):
self.use_main_grad = True
if self.use_main_grad:
assert hasattr(param, "main_grad"), (
"Params have different main grad attributes."
)
if self.use_main_grad:
assert not offload, "offload not support main_grad for now"
self._default_device = device
self._pfp16 = (
len(
list(
filter(
lambda x: x.trainable and x.dtype == Type.fp16.value,
self._local_params,
)
)
)
> 0
)
self._pbf16 = (
len(
list(
filter(
lambda x: x.trainable and x.dtype == Type.bf16.value,
self._local_params,
)
)
)
> 0
)
self._broadcast_overlap = False
self._forward_pre_hook_remove_helper = []
try:
# The fp32 params such as layer_norm_0.w_0 will be at the end of param_list.
# Have to sort the params to make sure all params are in the forward using order.
self._broadcast_order_params = sorted(
self.local_params,
key=lambda x: int(x.name.split('.')[0].split('_')[-1]),
)
except ValueError:
self._broadcast_order_params = None
self._group = (
new_group(_get_global_group().ranks) if group is None else group
)
# only support to combine stage2 and dp hybrid parallel now.
self._dp_group = dp_group
self.world_size = self._group.nranks
self._rank = self._group.rank
self._global_root_rank = self._group.ranks[0]
if self._dp_group is not None and self._dp_group.nranks > 1:
assert not offload, (
"Not support! when using offload with sharding stage2, please use pure sharding stage2, exclude data parallel."
)
# Synchronous all ranks models
if pretrain_sync_models:
self._sync_params_and_buffers()
self.param_storages = {} # {dtype: {rank: InternalStorage}}
if isinstance(self._optim._grad_clip, ClipGradByGlobalNorm):
logging.warning(
"While using ClipGradByGlobalNorm in GroupShardedOptimizerStage2, the grad clip of original optimizer will be changed."
)
hcg = fleet.fleet._hcg if hasattr(fleet.fleet, "_hcg") else None
if (
hcg
and hcg.get_parallel_mode() is not ParallelMode.DATA_PARALLEL
and not offload
):
if self.use_main_grad:
self._optim._inner_opt._grad_clip = HybridParallelClipGrad(
self._optim._inner_opt._grad_clip, hcg
)
else:
self._optim._grad_clip = HybridParallelClipGrad(
self._optim._grad_clip, hcg
)
else:
if self.use_main_grad:
self._optim._inner_opt._grad_clip = GroupShardedClipGrad(
self._optim._inner_opt._grad_clip,
paddle.get_device(),
self._group,
)
else:
self._optim._grad_clip = GroupShardedClipGrad(
self._optim._grad_clip, paddle.get_device(), self._group
)
if self._optim._parameter_list and isinstance(
self._optim._parameter_list[0], dict
):
for item in self._optim._param_groups:
if "grad_clip" in item.keys():
item["grad_clip"] = self._optim._grad_clip
if offload:
assert self._pfp16, (
"Only support offload strategy while using 'Adam', 'AdamW' and 'Momentum' optimizer with AMP/Pure FP16"
)
self.offload = offload # Using for offload
self.offload_device = "cpu"
self.offload_buffer_size = 0
self.offload_param2align = {}
self.offload_params = None
self.offload_grads = None
self.dev_id = int(paddle.get_device().split(":")[1])
self._master_params = {}
# Update optimizer parameters and adjust parameter storage and use according to rank.
self._update_opt_status()
def _set_auxiliary_var(self, key, val):
super()._set_auxiliary_var(key, val)
self._optim._set_auxiliary_var(key, val)
@paddle.autograd.no_grad()
def _sync_params_and_buffers(self):
"""
Sync all model states for all ranks
"""
for p in self._local_params:
dist.broadcast(
p, src=self._global_root_rank, group=self._group, sync_op=True
)
if self._dp_group:
dist.broadcast(
p,
src=self._dp_group.ranks[0],
group=self._dp_group,
sync_op=True,
)
def _update_task(self, task):
if self._reduce_overlap:
assert task is not None
# Only track of the last reduce task.
# Since all tasks are on the same stream, only need to wait the last one.
# After waiting for the last reduce task, all reduce tasks before have already finished.
self._comm_task = task
def _set_reduce_overlap(self, reduce_overlap):
# Enable gradients' reduces overlap with backward calculation.
self._reduce_overlap = reduce_overlap
def _set_broadcast_overlap(
self, broadcast_overlap, layers=None, num_groups=None
):
# Enable post optimizer broadcasts overlap with the forward calculation of next batch.
self._broadcast_overlap = broadcast_overlap
if self._broadcast_overlap:
assert layers is not None, (
"To enable broadcast overlap forward, please pass the module to the function."
)
self._layers = layers
warnings.warn(
"Setting overlap broadcast means the `paddle.device.cuda.synchronize()` "
"must be called manually before calling `paddle.save()` and before and inference."
)
if self._broadcast_order_params is None:
# Params' names should be like column_linear_32.w_0 pattern to get the best performance.
warnings.warn(
r"The param name passed to the optimizer doesn't follow .+_[0-9]+\..+ pattern, "
"overlap broadcast may harm the performance."
)
self._broadcast_order_params = self._local_params
if num_groups is None or num_groups > len(self._broadcast_order_params):
warnings.warn(
"The num_groups for broadcast is larger than the number of params to be broadcast. "
"It will set to default value: 1 (use the default sharding group)."
)
num_groups = 1
assert isinstance(num_groups, int) and num_groups > 0, (
"num_groups should be a positive integer"
)
self._number_of_broadcast_groups = num_groups
self._broadcast_groups = [
None for _ in range(self._number_of_broadcast_groups)
]
self._broadcast_groups[0] = self._group
ranks = self._group.ranks
for i in range(1, self._number_of_broadcast_groups):
self._broadcast_groups[i] = new_group(ranks)
def _generate_master_params(self, trainable_params):
if self.offload:
for param in trainable_params:
if param.name not in self._master_params.keys():
self._master_params[param.name] = core.eager.Tensor(
name=param.name,
value=param.cast(dtype=Type.fp32.value).numpy(),
place=core.CPUPlace(),
stop_gradient=param.stop_gradient,
)
else:
for param in trainable_params:
if (
param.dtype == Type.fp16.value
or param.dtype == Type.bf16.value
):
master_tensor = paddle.cast(param, Type.fp32.value)
master_tensor.name = param.name
self._optim._master_weights[param.name] = master_tensor
def _update_opt_status(self):
"""Update optimizer status and parameter storage information, and special functions to be developed."""
# func 1
self._integration_params()
# Segment helpers
def _segment_params(self):
"""
Divide all optimizer parameters equally into rank.
"""
if len(self.__segment_params) == 0:
self.__segment_params, param_lists = (
[[] for _ in range(self.world_size)],
[[] for _ in range(self.world_size)],
)
sizes = [0] * self.world_size
for param in self._local_params:
# Add this param to rank with smallest size.
rank = sizes.index(min(sizes))
param_lists[rank].append(param)
# Statistical real numels
sizes[rank] += param._numel() if param.trainable else 0
for rank, params in enumerate(param_lists):
self.__segment_params[rank].extend(params)
return self.__segment_params
@property
def local_params(self):
return self._local_params
@property
def param2rank(self):
"""Map the params to the rank which owns them"""
if len(self._param2rank) == 0:
for rank, params in enumerate(self._segment_params()):
for param in params:
self._param2rank[param.name] = rank
return self._param2rank
@property
def dtype_rank_params(self):
"""
Divide the parameters into groups according to rank and dtype.
"""
if len(self._dtype_rank_params) == 0:
# Assign the parameters of each rank according to the type
trainable_params = list(
filter(lambda x: x.trainable, self._local_params)
)
for param in trainable_params:
if param.dtype not in self._dtype_rank_params.keys():
self._dtype_rank_params[param.dtype] = [
[] for _ in range(self.world_size)
]
self._dtype_rank_params[param.dtype][
self.param2rank[param.name]
].append(param)
# Sort per rank params by size
for dtype in self._dtype_rank_params.keys():
for rank_params in self._dtype_rank_params[dtype]:
rank_params.sort(key=lambda x: x._numel())
return self._dtype_rank_params
@property
def rank_buffer_size(self):
"""
Count the memory size of the parameters corresponding to rank under the corresponding dtype.
"""
# CUDA alignment 256 bytes
if self._default_device in core.get_all_custom_device_type():
device_alignment = core.libpaddle._get_device_min_chunk_size(
self._default_device
)
else:
device_alignment = alignment[self._default_device]
if len(self._rank_buffer_size) == 0:
for dtype in self.dtype_rank_params.keys():
if dtype not in self._rank_buffer_size.keys():
self._rank_buffer_size[dtype] = {}
for dst_rank, per_rank_params in enumerate(
self.dtype_rank_params[dtype]
):
if dst_rank not in self._rank_buffer_size[dtype].keys():
self._rank_buffer_size[dtype][dst_rank] = 0
for param in per_rank_params:
if not param.trainable:
continue
size = param._numel() * align[dtype]
remaining = size % device_alignment
ali = (
0
if remaining == 0
else device_alignment - remaining
)
align_ = ali // align[dtype]
self._rank_buffer_size[dtype][dst_rank] += (
param._numel() + align_
)
self._param2align[param.name] = align_
return self._rank_buffer_size
def _integration_params(self):
"""
Integrate the parameters into a continuous memory according to rank, and support the update of training parameters.
"""
for dtype, per_rank_params in self.dtype_rank_params.items():
if dtype not in self.param_storages.keys():
self.param_storages[dtype] = {}
for dst_rank, params in enumerate(per_rank_params):
if len(params) > 0:
# Merge all the trainable params in a single InternalStorage
trainable_params = list(
filter(lambda x: x.trainable, params)
)
if (self._pfp16 or self._pbf16) and dst_rank == self._rank:
self._generate_master_params(trainable_params)
if trainable_params:
param_storage = ParamStorage(
size=self.rank_buffer_size[dtype][dst_rank],
dtype=dtype,
device=self._default_device,
)
param_storage.add_rank_params(
trainable_params, self._param2align
)
self.param_storages[dtype][dst_rank] = param_storage
# Clear the InternalStorage keys which are not in use anymore
dtype_in_use = list(self.dtype_rank_params.keys())
dtype_to_pop = list(
filter(lambda x: x not in dtype_in_use, self.param_storages.keys())
)
for d in dtype_to_pop:
self.param_storages.pop(d)
if self.offload:
self._optim._master_weights = self._master_params
cpu_master_params = list(self._master_params.values())
if self._default_device in core.get_all_custom_device_type():
device_alignment = core.libpaddle._get_device_min_chunk_size(
self._default_device
)
else:
device_alignment = alignment[self._default_device]
for param in cpu_master_params:
size = param._numel() * align[Type.fp32.value]
remaining = size % device_alignment
ali = 0 if remaining == 0 else device_alignment - remaining
align_ = ali // align[Type.fp32.value]
self.offload_buffer_size += param._numel() + align_
self.offload_param2align[param.name] = align_
if cpu_master_params:
with device_guard(self._rank, self.offload_device):
self.offload_params = ParamStorage(
size=self.offload_buffer_size,
dtype=Type.fp32.value,
device=self.offload_device,
)
self.offload_params.buffer.name = "offload_buffer"
self.offload_params.add_rank_params(
cpu_master_params, self.offload_param2align, False
)
self.offload_params.buffer.stop_gradient = False
self.offload_grads = GradStorage(
size=self.offload_buffer_size,
dtype=Type.fp32.value,
device=self.offload_device,
destination=self._rank,
param2align=self.offload_param2align,
convert_cpu=True,
)
for p in cpu_master_params:
self.offload_grads.add_grad(
p, self.offload_param2align[p.name]
)
self._optim._master_weights[
self.offload_params.buffer.name
] = self.offload_params.buffer
def _offload_acc_grad(self, param_name, grad_fp32_cpu):
"""accumulate grads with offload strategy"""
with device_guard(self._rank, self.offload_device):
if param_name in self._master_params.keys():
if self._master_params[param_name].grad is None:
self._master_params[param_name]._copy_gradient_from(
grad_fp32_cpu
)
else:
self._master_params[param_name].grad.add_(grad_fp32_cpu)
self.offload_params.buffer._copy_gradient_from(
self.offload_grads.buffer
)
def _offload_scale_grad(self, scale_size):
"""scale grads with offload strategy"""
with device_guard(self._rank, self.offload_device):
self.offload_grads.buffer.scale_(scale=scale_size)
def _offload_clear_grad(self):
"""clear grads with offload strategy"""
with device_guard(self._rank, self.offload_device):
self.offload_grads.buffer.zero_()
def _step(self):
if self._broadcast_overlap:
# Clear the pre forward hook in the optimizer step.
for hook_remove in self._forward_pre_hook_remove_helper:
hook_remove.remove()
self._forward_pre_hook_remove_helper = []
if self.offload:
params_list = [self.offload_params.buffer]
# TODO(Baibaifan): Offload will support param_groups later
if not isinstance(self._optim._param_groups[0], dict):
self._optim._parameter_list = params_list
self._optim._param_groups = params_list
# Run the optimizer of the current rank step
if self.offload:
with device_guard(device=self.offload_device):
self._optim.step()
for param in self._local_params:
if param.name in self._master_params.keys():
if (
self._default_device
in core.get_all_custom_device_type()
):
param.set_value(
self._master_params[param.name]
._copy_to(
paddle.CustomPlace(
self._default_device, self.dev_id
),
True,
)
.cast(dtype=param.dtype)
)
elif self._default_device == "xpu":
param.set_value(
self._master_params[param.name]
.to("xpu:" + str(self.dev_id))
.cast(dtype=param.dtype)
)
else:
param.set_value(
self._master_params[param.name]
.cuda(self.dev_id)
.cast(dtype=param.dtype)
)
else:
self._optim.step()
# Synchronize all the updated shards in between the ranks
self._broadcast_params()
def step(self):
"""
A wrapper for Optimizer's step function to finish the update operation of the optimizer.
"""
# This method won't be called directly by opt.step()!
# The _redefine_opt_step() in class GroupShardedStage2 will wrap this function.
self._step()
def minimize(self):
raise RuntimeError(
"optimizer.minimize() not support now, please use optimizer.step()"
)
def set_state_dict(self, state_dict):
self._optim.set_state_dict(state_dict)
def state_dict(self):
return self._optim.state_dict()
def _clear_cache(self):
self.__segment_params.clear()
self._dtype_rank_params.clear()
self._param2rank.clear()
@paddle.autograd.no_grad()
def _broadcast_params(self):
"""Broadcast the parameters of the current rank to each rank"""
# Exchange all the shards with the other ranks
if self._broadcast_overlap:
self._broadcast_params_overlap_forward()
else:
for dtype_per_rank in self.param_storages.values():
for dst_rank, internal_storage in dtype_per_rank.items():
dist.broadcast(
tensor=internal_storage.buffer,
src=self._group.ranks[dst_rank],
group=self._group,
sync_op=True,
)
def _forward_pre_hook_function(self, tasks):
# Since the layers will call pre hook by `forward_pre_hook(self, inputs)`,
# the helper functions needs the x and y to take those params.
def __impl__(x, y):
for task in tasks:
# Wait for broadcast task before using the result of the broadcast.
task.wait()
return __impl__
def set_lr(self, lr):
super().set_lr(lr)
self._optim.set_lr(lr)
def get_lr(self):
return self._optim.get_lr()
@paddle.autograd.no_grad()
def _broadcast_params_overlap_forward(self):
# Exchange all the shards with the other ranks,
# but overlap the broadcast with next batch's calculation.
group_idx = 0
param2task = {}
for x in self._broadcast_order_params:
if x.trainable:
group = self._broadcast_groups[group_idx]
group_idx = (group_idx + 1) % self._number_of_broadcast_groups
task = dist.broadcast(
tensor=x,
src=group.ranks[self._param2rank[x.name]],
group=group,
sync_op=False,
)
assert x.name not in param2task
param2task[x.name] = task
for layer in self._layers.sublayers():
if len(layer.sublayers()) == 0:
# Register forward pre hood for leaf layers. This will get the best performance.
tasks = []
for param in layer.parameters():
if param.trainable:
if param.name in param2task:
tasks.append(param2task[param.name])
self._forward_pre_hook_remove_helper.append(
layer.register_forward_pre_hook(
self._forward_pre_hook_function(tasks)
)
)
def sharded_state_dict(
self,
model_sharded_state_dict: ShardedStateDict,
) -> ShardedStateDict:
"""
Convert optimizer state dict to a sharded state dict based on model sharding information.
Args:
model_sharded_state_dict (dict): Sharded state dict of the model, containing tensor metadata.
Returns:
dict: A new optimizer state dict where weights are wrapped as ShardedWeight.
"""
_FP32_MASTER = "fp32_master_0"
_MOMENT_NAME = "moment"
_optimizer_scalar_name = [
"beta1_pow_acc_0",
"beta2_pow_acc_0",
]
_optimizer_non_scaler_name = [
"moment1_0",
"moment2_0",
"velocity_0",
]
def _generate_base_static_name(vname):
if _FP32_MASTER in vname:
return tuple(vname.split("_" + _FP32_MASTER + "_", 1))
for name in _optimizer_scalar_name + _optimizer_non_scaler_name:
if vname.endswith(name):
return vname[: -(len(name) + 1)], name
raise ValueError(f"Cannot split variable name: {vname}.")
optimizer_sharded_state_dict = {}
optimizer_state_dict = self.state_dict()
# Build name mapping and remove non-tensor entries from optimizer state
static_to_struct_mapping = {}
model_sharded_state_dict = dict(
sorted(model_sharded_state_dict.items())
)
for k, v in model_sharded_state_dict.items():
# When shared weights exist, the v.local_tensor.name of shared parameters are identical, but only the first parameter has optimizer states. Therefore, only the key-value pairs of the first occurrence in the shared parameter group need to be retained.
if v.local_tensor.name not in static_to_struct_mapping:
static_to_struct_mapping[v.local_tensor.name] = k
master_weights = optimizer_state_dict.pop("master_weights", None)
optimizer_state_dict.pop("LR_Scheduler", None)
# Process main optimizer states
for key, tensor in optimizer_state_dict.items():
static_name, optim_state_type = _generate_base_static_name(key)
struct_name = static_to_struct_mapping[static_name]
sharded_weight = model_sharded_state_dict[struct_name]
unified_name = f"{struct_name}.{optim_state_type}"
# Determine tensor partitioning scheme
if _MOMENT_NAME in optim_state_type:
optimizer_sharded_state_dict[unified_name] = (
create_sharded_weight_with_new_local(
unified_name, tensor, sharded_weight
)
)
else: # Non-momentum parameters
optimizer_sharded_state_dict[unified_name] = ShardedWeight(
key=unified_name,
local_tensor=tensor,
local_shape=(1,),
global_shape=(1,),
global_offset=(0,),
)
# Process master weights if using mixed precision
if master_weights is not None:
for key, tensor in master_weights.items():
struct_name = static_to_struct_mapping[key]
sharded_weight = model_sharded_state_dict[struct_name]
unified_name = f"{struct_name}.w_0"
optimizer_sharded_state_dict[unified_name] = (
create_sharded_weight_with_new_local(
unified_name, tensor, sharded_weight
)
)
return optimizer_sharded_state_dict
@@ -0,0 +1,720 @@
# Copyright (c) 2022 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.
# The file has been adapted from fairscale file:
# https://github.com/facebookresearch/fairscale/blob/main/fairscale/nn/data_parallel/sharded_ddp.py
# Git commit hash: 8acbec718f3c70a6b9785470bb9e05cd84fc3f8e
# We retain the following license from the original files:
# Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
#
# This source code is licensed under the BSD license found in the
# LICENSE file in the root directory of this source tree.
import logging
from functools import reduce
from types import MethodType
import paddle
import paddle.distributed as dist
from paddle import nn
from paddle.distributed import collective
from paddle.distributed.utils.log_utils import get_logger
from paddle.framework import core
from .group_sharded_optimizer_stage2 import GroupShardedOptimizerStage2
from .group_sharded_storage import GradStorage
from .group_sharded_utils import Type, device_guard
logger_ = get_logger(logging.WARNING)
def _trainable(param):
return param.trainable
class GroupShardedStage2(nn.Layer):
"""
A wrapper for Sharding Stage2 Layer in Dygraph.
.. warning: GroupShardedStage2 encapsulates the layer strategy and integrates it into the nn.Layer.
.. ZeRO: https://arxiv.org/pdf/1910.02054.pdf.
"""
# TODO (Baibaifan)
# Feature Notes::
# 1. Unified memory for param and param.grad to InternalStorage.
# 2. Divide param.grad according to rank to centrally apply for and release GPU memory.
# 3. Dynamically adjust training parameters and models.
# 4. Support offload function.
# 5. Support the establishment of independent communication groups.
def __init__(
self,
layer,
sharding_optimizer,
group=None,
sync_buffers=False,
buffer_max_size=2**23, # 8MB
auto_refresh_trainable=True,
device="xpu" if core.is_compiled_with_xpu() else "gpu",
dp_group=None,
):
super().__init__()
# training options
self._layer = layer
self._sharding_optimizers = (
[sharding_optimizer]
if not isinstance(sharding_optimizer, list)
else sharding_optimizer
)
assert all(
isinstance(opt, GroupShardedOptimizerStage2)
for opt in self._sharding_optimizers
), "Please use GroupShardedOptimizerStage2 optimizer"
self._sync_buffers = sync_buffers
self._auto_refresh_trainable = auto_refresh_trainable
# Communication related attributes
self._group = (
collective.new_group(collective._get_global_group().ranks)
if group is None
else group
)
self._world_size_scaling = 1.0 / self._group.nranks
assert self._group.nranks > 1, (
"Training must be distributed, ranks must be greater than 1"
)
self._rank = self._group.rank
self._global_root_rank = self._group.ranks[
0
] # picking ranks index 0 as the reference
self._default_device = device
self._dp_group = dp_group
# Global statistical parameters
self._all_params = []
for optim in self._sharding_optimizers:
self._all_params.extend(list(optim.local_params))
self.use_main_grad = None
for param in self._all_params:
if self.use_main_grad is None and hasattr(param, "main_grad"):
self.use_main_grad = True
if self.use_main_grad:
assert hasattr(param, "main_grad"), (
"Params have different main grad attributes."
)
# sharing stage 2 comm overlap flag
self._reduce_overlap = False
self._grad_reduced = []
self._trainable_param2rank = {}
self._trainable_param2align = {}
self._trainable_params = list(
filter(lambda x: x.trainable, self._all_params)
)
self._trainable_mask = list(map(_trainable, self._trainable_params))
self._param_grads = []
# Set grad storage size & Display param sizes and model sizes
model_size = sum([p._numel() for p in self._layer.parameters()])
assert buffer_max_size >= 0, "buffer_max_size must be GE than 0."
self._buffer_max_size = self._rank_buffer_size(
buffer_max_size, model_size
)
self._use_grad_storage = buffer_max_size > 0
self._grad_storages = {} # {dtype: {rank: GradStorage}}
self._has_grad_storage = []
self._grad_storage_list = []
# Offload
# TODO(haohongxiang): Now it's not be supported for multi-optimizers using Offload strategy
self._offload_optims = list(
filter(lambda optim: optim.offload, self._sharding_optimizers)
)
if len(self._offload_optims) > 0:
assert len(self._sharding_optimizers) == 1, (
"Only support offload strategy for single optimizer"
)
self._offload = len(self._offload_optims) > 0
self._offload_device = "cpu"
# Set backward pass hooks
self._bw_hooks = []
self.scale_in_opt = False
# TODO (Baibaifan) Set tasks flow support asynchronous communicate
# self._tasks_flow = deque()
# Define optimizer step and clear_grad
self._redefine_opt_step()
self._redefine_opt_clear()
def forward(self, *inputs, **kwargs):
"""
A wrapper for Sharding Stage2 layer.
- Fresh trainable params or rebuild grad storage
- Sync layer's buffer params
- Clear all flags states
- Forward for origin layers
"""
# Whether to need to reset trainable parameters
needs_fresh = len(self._bw_hooks) == 0 and self.training
if self._auto_refresh_trainable:
needs_fresh |= self._detect_train_change()
# Front hook
self._init_internal_storage(needs_fresh)
# Sync layer's buffers state
if self._sync_buffers:
self.__sync_buffers()
# Normal FW on the base model
fw = self._layer(*inputs, **kwargs)
return fw
def set_state_dict(self, state_dict, use_structured_name=True):
self._layer.set_state_dict(
state_dict, use_structured_name=use_structured_name
)
def state_dict(
self,
destination=None,
include_sublayers=True,
structured_name_prefix="",
):
return self._layer.state_dict(
destination=destination,
include_sublayers=include_sublayers,
structured_name_prefix=structured_name_prefix,
)
def _clear_gradients(self):
"""
Set zero to the gradient of the optimizer's current rank trainable parameters.
"""
# Release grad storages
for dtype in self._grad_storages.keys():
if (
not self._offload
and self._rank in self._grad_storages[dtype].keys()
):
self._grad_storages[dtype][self._rank].buffer.zero_()
# Release grads of params
for param in self._trainable_params:
if param.name in self._param_grads:
if self.use_main_grad and param.main_grad is not None:
param.main_grad.zero_()
elif param.grad is not None:
param._zero_grads()
# Release grads of master params with offload strategy
if self._offload:
self._sharding_optimizers[0]._offload_clear_grad()
def _grad_scale(self):
"""
this function will do 2 things:
1. Before the optimization, scale main_grad to support gradient merge if param has main_grad, or to support fused_linear_param_grad_add gradient merge.
2. Before the optimization, scale the gradients before allreduce of dp_group.
"""
need_dp_scale = self._dp_group is not None and self._dp_group.nranks > 1
if self.scale_in_opt:
scale_factor = self._world_size_scaling
else:
scale_factor = 1.0
if need_dp_scale:
dp_scale_factor = 1.0 / (self._dp_group.nranks)
scale_factor = scale_factor * dp_scale_factor
# Scale grad storages
for dtype in self._grad_storages.keys():
if (
not self._offload
and self._rank in self._grad_storages[dtype].keys()
):
self._grad_storages[dtype][self._rank].buffer.scale_(
scale=scale_factor
)
# Scale grads of params
with paddle.no_grad():
for param in self._trainable_params:
if param.name in self._param_grads:
if self.use_main_grad and param.main_grad is not None:
param.main_grad.scale_(scale=scale_factor)
elif param.grad is not None:
param.grad.scale_(scale=scale_factor)
# Scale grads of master params with offload strategy
if self._offload:
if need_dp_scale is False:
return
self._sharding_optimizers[0]._offload_scale_grad(
scale=1.0 / (self._dp_group.nranks)
)
def _init_internal_storage(self, needs_fresh):
"""
Judge Fresh trainable params or rebuild grad storage.
"""
if needs_fresh:
self._fresh_trainable()
else:
self._build_grad_storages()
# Clear all flags state
self._clear_counters()
def to(self, device=None, dtype=None, blocking=True):
"""
Synchronously or asynchronously convert the data type of the layer, the device is not supported now.
"""
assert isinstance(device, str), "Device must be type str"
assert device == self._default_device, (
"New devices are not supported, because of the optimizer state is not sync"
)
self._layer.to(device=device, dtype=dtype, blocking=blocking)
# Re-build the buckets, hooks, etc..
self._fresh_trainable()
def _fresh_trainable(self):
"""Whether to update training parameters."""
# Make sure that this is not done while gradients are waiting to be reduced (if no_sync context for instance)
if reduce(lambda x, y: x or y, self._grad_reduced, False):
logging.warning("Grads waiting to be reduced.")
self._trainable_params = list(
filter(lambda x: x.trainable, self._all_params)
)
self._trainable_params.sort(key=lambda x: x._numel())
self._trainable_param2rank = {}
for optim in self._sharding_optimizers:
# Need to be wrapped for Sharding Stage2 Optimizer
if len(optim.param_storages.keys()) == 0:
optim._update_opt_status()
# Get the parameters split by the optimizer according to rank
for per_rank_params in (
optim.dtype_rank_params.values()
): # all the params from all ranks
for params in per_rank_params:
for param in filter(lambda x: x.trainable, params):
self._trainable_param2rank[param.name] = (
optim.param2rank[param.name]
)
self._trainable_param2align[param.name] = (
optim._param2align[param.name]
)
# Create grad_storage
self._setup_use_grad_storage()
# setup backward hooks
self._setup_backward_hooks()
@paddle.autograd.no_grad()
def __sync_buffers(self):
"""
Sync all the param buffers from all ranks (exp: batch norm statistics).
"""
for buffer in self._layer.buffers(include_sublayers=True):
dist.broadcast(
buffer, self._global_root_rank, self._group, sync_op=True
)
if self._dp_group and self._dp_group.nranks > 1:
dist.broadcast(
buffer,
self._dp_group.ranks[0],
self._dp_group,
sync_op=True,
)
def __getattr__(self, name):
"""Forward missing attributes to wrapped layer."""
try:
return super().__getattr__(name)
except AttributeError:
return getattr(self._layer, name)
@paddle.autograd.no_grad()
def _clear_counters(self):
"""Reset all the grad reduce and call counters."""
if self.training:
self._grad_reduced = [True for _ in self._trainable_params]
if self._use_grad_storage:
for grad_storage in self._grad_storage_list:
grad_storage.reset_checked_in()
def _set_reduce_overlap(self, reduce_overlap):
# Hacky way to not add an extra parameter to the `group_sharded_parallel` funct.
# User should use this like:
# model, optimizer, scaler = group_sharded_parallel(...)
# model._set_reduce_overlap(True)
self._reduce_overlap = reduce_overlap
if self._reduce_overlap:
assert len(self._sharding_optimizers) == 1, (
"Only support comm overlap strategy for single optimizer"
)
self._sharding_optimizers[0]._set_reduce_overlap(reduce_overlap)
def _get_scaled_grad_fn(self, param):
@paddle.autograd.no_grad()
def scale(grad):
# do gradient scale separately
# For grad scale, we need to do it in the backward hook due to fp16 may overflow if we first add grad and then scale
# For main_grad scale and fused_linear_param_grad_add, we do scale in the optimizer.
if not self.scale_in_opt:
if (
not hasattr(param, "main_grad")
and grad is not None
and grad.dtype == Type.fp16.value
):
assert grad._is_initialized(), (
"grad should be initialized in stage2"
)
grad.scale_(self._world_size_scaling)
else:
self.scale_in_opt = True
return scale
def _get_reduce_fn(self, index, param, dst_rank):
"""
There are two ways to reduce gradient.
- 1. Do not use self._use_grad_storage or exceeded buffer_max_size will be reduced separately.
- 2. Use grad_storage Reduce the storage to get the full gradient from different ranks.
"""
if not self._use_grad_storage or not self._has_grad_storage[index]:
# Direct reduction
@paddle.autograd.no_grad()
def reduce(*_):
# Skip gradient reduction, do not change status information
if self._grad_reduced[index]:
assert (
param.grad is not None or param.main_grad is not None
), "Parameter should have grad or main grad"
# Change reduce information
self._grad_reduced[index] = False
# Clear the gradient that does not belong to the current rank through the callback function
def cleanup():
if dst_rank != self._rank:
if self.use_main_grad:
param.main_grad._clear_data()
param.main_grad = None
else:
param.clear_gradient(False)
elif self._offload:
tmp_grad = param.grad.cast(
dtype=Type.fp32.value
).cpu()
self._sharding_optimizers[0]._offload_acc_grad(
param.name, tmp_grad
)
del tmp_grad
param.clear_gradient(False)
# Synchronize the reduce parameter gradient asynchronize
self._sharding_optimizers[0]._update_task(
dist.reduce(
tensor=(
param.grad
if not self.use_main_grad
else param.main_grad
),
dst=self._group.ranks[dst_rank],
group=self._group,
sync_op=not self._reduce_overlap,
)
)
# Clear the task flow and trigger callback to clear the redundant gradient
# self._clear_task_flow()
cleanup()
else:
# Buffer reduction
@paddle.autograd.no_grad()
def reduce(*_):
# Skip gradient reduction, do not change status information
if self._grad_reduced[index]:
assert (
param.grad is not None or param.main_grad is not None
), "Parameter should have grad or main grad"
# Change reduce information
self._grad_reduced[index] = False
grad_storage = self._grad_storages[param.dtype][dst_rank]
grad_storage.params_checked_in += 1
if grad_storage.all_checked_in:
assert grad_storage.buffer is not None
# Clearing up the grad_storage buffer
def cleanup():
if dst_rank != self._rank:
for p in grad_storage._params:
if self.use_main_grad:
p.main_grad._clear_data()
p.main_grad = None
else:
p.clear_gradient(False)
grad_storage.buffer._clear_data()
elif self._offload:
grad_storage.to(device=self._offload_device)
for p in grad_storage._params:
with device_guard():
tmp_grad = p.grad.cast(
dtype=Type.fp32.value
)
self._sharding_optimizers[
0
]._offload_acc_grad(p.name, tmp_grad)
p.clear_gradient(False)
grad_storage._device = self._default_device
grad_storage.buffer._clear_data()
# Reduce the bucket
grad_storage.sent = True
# Synchronize the reduce parameter gradient asynchronize
self._sharding_optimizers[0]._update_task(
dist.reduce(
tensor=grad_storage.buffer,
dst=self._group.ranks[grad_storage.destination],
group=self._group,
sync_op=not self._reduce_overlap,
)
)
cleanup()
# Clear the task flow and trigger callback to clear the redundant gradient
# self._clear_task_flow()
return reduce
def _setup_backward_hooks(self):
"""
Set the backward hook to synchronize the gradients of all rank by reduce group ranks.
"""
# Remove previous backward hooks
while len(self._bw_hooks) > 0:
self._bw_hooks.pop().remove()
# Go through the parameters, attach the hook
if not self.training:
return
for index, param in enumerate(self._trainable_params):
param._register_grad_hook(self._get_scaled_grad_fn(param))
dst_rank = self._trainable_param2rank[param.name]
reduce_function = self._get_reduce_fn(index, param, dst_rank)
self._bw_hooks.append(
param._register_backward_hook(reduce_function)
)
def _setup_use_grad_storage(self):
"""
Integrate the parameters gradient into a continuous memory according to rank, and support the update of training parameters.
"""
# According to parameters's numel sort, allocate memory of parameter gradient to continuous memory according to rank
self._grad_storages = {}
self._has_grad_storage = [False for _ in self._trainable_params]
for index, param in enumerate(self._trainable_params):
dst_rank = self._trainable_param2rank[param.name]
if param.dtype not in self._grad_storages.keys():
self._grad_storages[param.dtype] = {}
if dst_rank not in self._grad_storages[param.dtype].keys():
self._grad_storages[param.dtype][dst_rank] = GradStorage(
self._buffer_max_size[param.dtype],
dtype=(
param.dtype
if not self.use_main_grad
else paddle.float32
),
device=self._default_device,
destination=dst_rank,
param2align=self._trainable_param2align,
)
# Criteria to decide whether this parameter is to be put in GradStorage
if self._grad_storages[param.dtype][dst_rank].can_add_grad_view(
param, self._trainable_param2align[param.name]
):
self._grad_storages[param.dtype][dst_rank].add_grad(
param, self._trainable_param2align[param.name]
)
self._has_grad_storage[index] = True
else:
self._param_grads.append(param.name)
for dtype in self._grad_storages.keys():
self._grad_storage_list.extend(
list(self._grad_storages[dtype].values())
)
# def _clear_task_flow(self):
# """Try to consume the previous tasks."""
# while len(self._tasks_flow) > 0:
# task = self._tasks_flow.popleft()
# task.wait()
# if task.callback is not None:
# task.callback()
def _detect_train_change(self):
# Current trainable parameters
trainable_mask = list(map(_trainable, self._trainable_params))
# Whether parameters trainability changed
trainability_changed = trainable_mask != self._trainable_mask
if trainability_changed:
logging.warning(
"Trainable params changed, because of eval/train mode or parameter freezing/unfreeze."
)
self._trainable_mask = trainable_mask
return trainability_changed
def _build_grad_storages(self):
"""
Rebuild grad storages.
"""
# Rebuild fp16/fp32 grad storages
for dtype in self._grad_storages.keys():
for dst_rank, grad_storage in self._grad_storages[dtype].items():
if self._offload or dst_rank != self._rank:
grad_storage.manual_release()
grad_storage.rebuild()
def _rank_buffer_size(self, buffer_max_size, model_size):
"""
Generate the minimum buffer size for each rank & Display param sizes and model sizes.
"""
# Initialize buffer size
rank_buffer_size = {}
for shard_opt in self._sharding_optimizers:
if shard_opt.rank_buffer_size:
for dtype in shard_opt.rank_buffer_size.keys():
sizes = max(shard_opt.rank_buffer_size[dtype].values())
rank_buffer_size[dtype] = min(sizes, buffer_max_size)
if Type.fp16.value in rank_buffer_size.keys():
# FP16 GradStorage and model size
logger_.info(
f"====== FP16 GradStorage size: {rank_buffer_size[Type.fp16.value] / 2**19:.2f}M parameters, Model size {model_size / 2**19:.2f}M parameters ======"
)
if Type.bf16.value in rank_buffer_size.keys():
# FP16 GradStorage and model size
logger_.info(
f"====== BF16 GradStorage size: {rank_buffer_size[Type.bf16.value] / 2**19:.2f}M parameters, Model size {model_size / 2**19:.2f}M parameters ======"
)
if Type.fp32.value in rank_buffer_size.keys():
# FP32 GradStorage and model size
logger_.info(
f"====== FP32 GradStorage size: {rank_buffer_size[Type.fp32.value] / 2**18:.2f}M parameters, Model size {model_size / 2**18:.2f}M parameters ======"
)
return rank_buffer_size
def _dp_allreduce(self):
# do dp allreduce here for gradient merge.
if self._dp_group and self._dp_group.nranks > 1:
for dtype in self._grad_storages.keys():
for rank, g in sorted(
self._grad_storages[dtype].items(), key=lambda x: x[0]
):
if g.destination == self._rank:
assert g.buffer._is_initialized()
dist.all_reduce(
tensor=g.buffer,
group=self._dp_group,
sync_op=True,
)
for param in self._trainable_params:
if param.name in self._param_grads:
if self.use_main_grad and param.main_grad is None:
continue
elif param.grad is None:
continue
dst_rank = self._trainable_param2rank[param.name]
if dst_rank == self._rank:
dist.all_reduce(
tensor=(
param.grad
if not self.use_main_grad
else param.main_grad
),
group=self._dp_group,
sync_op=True,
)
def _redefine_opt_step(self):
grad_func = self._grad_scale
dp_allreduce_func = self._dp_allreduce
for opt in self._sharding_optimizers:
opt_step = opt.step
def _opt_step(self):
if self._reduce_overlap:
# Wait for the last reduce task. This wait must before grad scale function.
assert self._comm_task is not None
self._comm_task.wait()
grad_func()
dp_allreduce_func()
opt_step()
opt.step = MethodType(_opt_step, opt)
def _redefine_opt_clear(self):
clear_func = self._clear_gradients
def _opt_clear(self):
clear_func()
for opt in self._sharding_optimizers:
opt.clear_grad = MethodType(_opt_clear, opt)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,363 @@
# Copyright (c) 2022 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.
# The file has been adapted from fairscale file:
# https://github.com/facebookresearch/fairscale/blob/main/fairscale/nn/misc/param_bucket.py
# Git commit hash: 8acbec718f3c70a6b9785470bb9e05cd84fc3f8e
# We retain the following license from the original files:
# Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
#
# This source code is licensed under the BSD license found in the
# LICENSE file in the root directory of this source tree.
import numpy as np
import paddle
from paddle.framework import core
from .group_sharded_utils import Type, cvt_to_device, device_guard
class BufferWarper(core.eager.Tensor):
def __init__(self):
super().__init__()
self.need_clip = True
self.is_distributed = False
self.trainable = True
class InternalStorage:
"""
This is a basic class, which is responsible for consolidating the basic storage tensor.
"""
# Support integration parameter tensor
def __init__(self, size, dtype, device, convert_cpu=False):
self._params = []
self._param_ids = []
self._fill = 0
self._device = device
self._dtype = dtype
# The flatten tensor
size = [size] if isinstance(size, int) else size
if convert_cpu:
value = (
np.zeros(size, dtype=np.float16)
if Type.fp16.value == dtype
else np.zeros(size, dtype=np.float32)
)
self.buffer = core.eager.Tensor(value=value, place=core.CPUPlace())
if dtype == Type.bf16.value:
self.buffer = paddle.cast(self.buffer, dtype=paddle.bfloat16)
else:
self.buffer = paddle.zeros(size, dtype=dtype)
self.dev_id = (
0
if paddle.get_device() == "cpu"
else int(paddle.get_device().split(":")[1])
)
def to(self, device, dtype=None, keep_alignment=True):
"""
Move the underlying buffer
"""
assert self.buffer is not None, (
"Cannot move a collapsed bucket, please rebuild it"
)
assert dtype == Type.fp32.value or Type.fp16.value, (
"Conversion type is not supported now"
)
if self._device != device:
if device in paddle.device.get_all_custom_device_type():
tmp_buffer = self.buffer._copy_to(
paddle.CustomPlace(device, self.dev_id), True
)
else:
tmp_buffer = (
cvt_to_device(self.buffer, self.dev_id)
if device in ["gpu", "xpu"]
else self.buffer.cpu()
)
for param in self._params:
param.clear_gradient(False)
del self.buffer
self.buffer = tmp_buffer
self._device = device
if dtype is not None:
self.buffer = self.buffer.cast(dtype=dtype)
self._dtype = dtype
def warp_buffer(self):
tmp_buffer = BufferWarper()
self._buffer = self.buffer
tmp_buffer.get_tensor()._share_data_with(self.buffer.get_tensor())
self.buffer = tmp_buffer
class ParamStorage(InternalStorage):
"""
This is a basic class to simplify the handling of parameter InternalStorages.
"""
def __init__(self, size, dtype, device):
super().__init__(size, dtype, device, convert_cpu=True)
self.param2align = None
def to(self, device, dtype=None, keep_alignment=True):
"""
Move the underlying buffer
"""
super().to(device, dtype)
if keep_alignment:
self._array_params()
@paddle.autograd.no_grad()
def add_rank_params(self, trainable_params, param2align, convert_gpu=True):
"""
Add new parameters to the InternalStorage. Params becomes a view of this InternalStorage buffer.
"""
assert all(
id(param) not in self._param_ids for param in trainable_params
), "The same param cannot be checked in twice"
assert self.buffer is not None
self.param2align = param2align
cpu_param_shape = []
for param in trainable_params:
p_shape = self._add_param_as_view(
param, param2align[param.name], convert_gpu
)
cpu_param_shape.append(p_shape)
if convert_gpu:
if self._device in paddle.device.get_all_custom_device_type():
self.buffer = self.buffer._copy_to(
paddle.CustomPlace(self._device, self.dev_id), True
)
else:
# buffer convert from cpu to cuda
self.buffer = cvt_to_device(self.buffer, self.dev_id)
self._fill = 0
for idx, param in enumerate(trainable_params):
self._convert_buffer(
param, cpu_param_shape[idx], param2align[param.name]
)
self._params.append(param)
self._param_ids.append(id(param))
@paddle.autograd.no_grad()
def _add_param_as_view(self, param, align, convert_gpu=True):
assert param.dtype == self.buffer.dtype, (
f"Different types for the InternalStorage and the param, cannot proceed: {param.dtype} - {self.buffer.dtype}"
)
var_end = self._fill + param._numel()
offset = var_end + align
assert offset <= self.buffer._numel()
p_shape = param.shape
origin_state = param.stop_gradient
param.stop_gradient = True
param.flatten_()
param.stop_gradient = origin_state
# Copy the current param value
with device_guard(self.dev_id, "cpu"):
tmp_var = self.buffer._slice(self._fill, var_end)
if convert_gpu:
param_cpu = param.cpu()
param._clear_data()
tmp_var.set_value(param_cpu)
else:
tmp_var.set_value(param)
del tmp_var
self._fill = offset
return p_shape
@paddle.autograd.no_grad()
def _convert_buffer(self, param, p_shape, align):
var_end = self._fill + np.prod(p_shape).tolist()
offset = var_end + align
assert offset <= self.buffer._numel()
# Convert the param value
with device_guard(self.dev_id, self._device):
tmp_tensor = self.buffer._slice(self._fill, var_end)
tmp_tensor._share_buffer_to(param)
param.get_tensor()._set_dims(p_shape)
self._fill = offset
@paddle.autograd.no_grad()
def _array_params(self):
"""
Given the parameters which have been registered previously, rebuild the whole InternalStorage.
"""
assert len(self._params) > 0
assert self.param2align is not None
self._fill = 0
for p in self._params:
self._convert_buffer(p, p.shape, self.param2align[p.name]) # modify
class GradStorage(InternalStorage):
"""
This is a basic class to simplify the handling of gradient InternalStorages
"""
def __init__(
self, size, dtype, device, destination, param2align, convert_cpu=False
):
if isinstance(size, np.int64):
size = size.tolist()
super().__init__(size, dtype, device, convert_cpu)
self._max_size = size
self._release = False
self.params_checked_in = 0
self.destination = destination
self._param2align = param2align
self.sent = False
def reset_checked_in(self):
"""Reset the counter of the parameter grads which have been checked in"""
self.params_checked_in = 0
self.sent = False
@property
def all_checked_in(self):
"""Judge all the expected gradient check-in happened"""
return len(self._params) == self.params_checked_in
def can_add_grad_view(self, param, align):
"""Is there enough InternalStorage to add this parameter gradient, and whether this param have already checked in."""
return (
self._fill + param._numel() + align <= self._max_size
and id(param) not in self._param_ids
)
def to(self, device, dtype=None, keep_alignment=True):
"""
Move the underlying buffer
"""
if self._release:
self.rebuild()
super().to(device, dtype)
if keep_alignment:
self._array_grads()
@paddle.autograd.no_grad()
def add_grad(self, param, align):
"""
Add a new parameter gradient to the InternalStorage. Param.grad becomes a view of this InternalStorage buffer.
"""
assert id(param) not in self._param_ids, (
"The same gradients cannot be checked in twice"
)
self._add_grad_as_view(param, align)
self._params.append(param)
self._param_ids.append(id(param))
@paddle.autograd.no_grad()
def manual_release(self):
"""
Release the buffer from InternalStorage. The InternalStorage will need to be rebuilt before use.
"""
if not self._release:
for p in self._params:
use_main_grad = hasattr(p, "main_grad")
if use_main_grad and p.main_grad is not None:
p.main_grad._clear_data()
p.main_grad = None
elif p.grad is not None:
p.clear_gradient(False)
self.buffer = None
self._fill = 0
self.params_checked_in = 0
self._release = True
@paddle.autograd.no_grad()
def rebuild(self):
"""
Given the parameter gradients which have been registered previously, rebuild the whole InternalStorage.
"""
if self._release:
self.buffer = paddle.zeros([self._max_size], dtype=self._dtype)
for p in self._params:
self._add_grad_as_view(p, self._param2align[p.name])
self._release = False
@paddle.autograd.no_grad()
def _array_grads(self):
"""
Given the parameters gradients which have been registered previously, rebuild the whole InternalStorage.
"""
if len(self._params) > 0:
self._fill = 0
for p in self._params:
self._add_grad_as_view(p, self._param2align[p.name])
@paddle.autograd.no_grad()
def _add_grad_as_view(self, param, align):
assert param._numel() > 0, (
"Cannot add a gradient to a released InternalStorage, please rebuild"
)
use_main_grad = hasattr(param, "main_grad")
if use_main_grad:
assert self.buffer.dtype == paddle.float32
else:
assert param.dtype == self.buffer.dtype
grad_end = self._fill + param._numel()
offset = grad_end + align
assert offset <= self.buffer._numel()
# Copy the current grad value to InternalStorage
with device_guard(self.dev_id, self._device):
tmp_var = self.buffer._slice(self._fill, grad_end)
tmp_var.get_tensor()._set_dims(param.shape)
if not use_main_grad:
param._copy_gradient_from(tmp_var)
else:
param.main_grad = tmp_var
del tmp_var
self._fill = offset
@@ -0,0 +1,352 @@
# Copyright (c) 2022 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 contextlib
from enum import Enum
from types import MethodType
import numpy as np
import paddle
from paddle import _C_ops, _legacy_C_ops
from paddle.base import core
from paddle.common_ops_import import dygraph_only
from paddle.nn import clip
class Taskflow:
"""
Task flows, one way linked list for task acquisition.
"""
def __init__(self, task, callback):
self.task = task
self.callback = callback
class Type(Enum):
"""
Type of trainable parameters
"""
fp16 = paddle.float16
bf16 = paddle.bfloat16
fp32 = paddle.float32
class GroupShardedClipGrad:
def __init__(self, clip, device, group):
self._clip = clip
self._device = device
self._group = group
@paddle.autograd.no_grad()
def _dygraph_clip(self, params_grads):
sum_square_fp32, sum_square_fp16, sum_square_bfp16 = [], [], []
unslice_params_fp32, unslice_params_fp16, unslice_params_bfp16 = (
[],
[],
[],
)
for p, g in params_grads:
p_slice = True # using for slice parameter in sharding stage3
if g is None or getattr(p, 'need_clip', True) is False:
continue
if hasattr(p, "unslice"):
p_slice = False
merge_grad = g
if g.type == core.VarDesc.VarType.SELECTED_ROWS:
merge_grad = clip.get_tensor_from_selected_rows(
clip.merge_selected_rows(g)
)
square = paddle.square(merge_grad)
sum_square = paddle.sum(square)
if p.dtype == paddle.float16:
if p_slice:
sum_square_fp16.append(sum_square)
else:
unslice_params_fp16.append(sum_square)
elif p.dtype == paddle.float32:
if p_slice:
sum_square_fp32.append(sum_square)
else:
unslice_params_fp32.append(sum_square)
elif p.dtype == paddle.bfloat16:
if p_slice:
sum_square_bfp16.append(sum_square)
else:
unslice_params_bfp16.append(sum_square)
# global norm of non-distributed FP16 params_and_grads
if len(sum_square_fp16) == 0:
global_norm_fp16 = paddle.to_tensor(
np.array(0.0), dtype=paddle.float32
)
else:
global_norm_fp16 = paddle.add_n(sum_square_fp16)
global_norm_fp16 = paddle.cast(
global_norm_fp16, dtype=paddle.float32
)
# global norm of non-distributed BFP16 params_and_grads
if len(sum_square_bfp16) == 0:
global_norm_bfp16 = paddle.to_tensor(
np.array(0.0), dtype=paddle.float32
)
else:
global_norm_bfp16 = paddle.add_n(sum_square_bfp16)
global_norm_bfp16 = paddle.cast(
global_norm_bfp16, dtype=paddle.float32
)
# global norm of non-distributed FP16 params_and_grads for unslice parameters
if len(unslice_params_fp16) == 0:
global_unslice_fp16 = paddle.to_tensor(
np.array(0.0), dtype=paddle.float32
)
else:
global_unslice_fp16 = paddle.add_n(unslice_params_fp16)
global_unslice_fp16 = paddle.cast(
global_unslice_fp16, dtype=paddle.float32
)
# global norm of non-distributed BFP16 params_and_grads for unslice parameters
if len(unslice_params_bfp16) == 0:
global_unslice_bfp16 = paddle.to_tensor(
np.array(0.0), dtype=paddle.float32
)
else:
global_unslice_bfp16 = paddle.add_n(unslice_params_bfp16)
global_unslice_bfp16 = paddle.cast(
global_unslice_bfp16, dtype=paddle.float32
)
# global norm of non-distributed FP32 params_and_grads
if len(sum_square_fp32) == 0:
global_norm_fp32 = paddle.to_tensor(
np.array(0.0), dtype=paddle.float32
)
else:
global_norm_fp32 = paddle.add_n(sum_square_fp32)
# global norm of non-distributed FP32 params_and_grads for unslice parameters
if len(unslice_params_fp32) == 0:
global_unslice_fp32 = paddle.to_tensor(
np.array(0.0), dtype=paddle.float32
)
else:
global_unslice_fp32 = paddle.add_n(unslice_params_fp32)
global_unslice_var = (
global_unslice_fp16 + global_unslice_fp32 + global_unslice_bfp16
)
global_norm_var = (
global_norm_fp16 + global_norm_fp32 + global_norm_bfp16
)
# add all reduce to get global norm of distributed params_and_grads
dev_id = int(self._device.split(":")[1])
dev_type = self._device.split(':')[0]
if paddle.device.get_device() == "cpu":
if dev_type in paddle.device.get_all_custom_device_type():
global_norm_var = global_norm_var._copy_to(
paddle.CustomPlace(dev_type, dev_id), True
)
elif dev_type == "xpu":
global_norm_var = global_norm_var.to(self._device)
else:
global_norm_var = global_norm_var.cuda(dev_id)
with device_guard(dev_id, self._device.split(":")[0]):
paddle.distributed.all_reduce(global_norm_var, group=self._group)
global_norm_var = paddle.sqrt(global_norm_var + global_unslice_var)
max_global_norm = paddle.full(
shape=[], 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),
)
clip_var_fp16 = paddle.cast(clip_var, paddle.float16)
for p, g in params_grads:
if getattr(p, 'need_clip', True) is False or g is None:
continue
origin_state = g.stop_gradient
g.stop_gradient = True
if p.dtype == paddle.float16:
g.scale_(clip_var_fp16)
else:
g.scale_(clip_var)
g.stop_gradient = origin_state
# p._reset_grad_inplace_version(True)
return params_grads
def __getattr__(self, item):
return getattr(self._clip, item)
def __call__(self, params_grads):
return self._dygraph_clip(params_grads)
@contextlib.contextmanager
def device_guard(dev_id=0, device="cpu"):
origin_device = paddle.device.get_device()
if device == "cpu":
paddle.set_device(device)
elif device in ["gpu", "xpu"]:
paddle.set_device(f"{device}:{dev_id}")
elif device in paddle.device.get_all_custom_device_type():
paddle.set_device(f"{device}:{dev_id}")
try:
yield
finally:
paddle.set_device(origin_device)
@dygraph_only
def GroupShardedScaler(scaler):
def unscale_method(self, optimizer):
if not self._enable:
return
param_grads = []
param_grads_bfp16 = []
param_grads_fp16 = []
param_grads_fp32 = []
if hasattr(optimizer, "update_slice"):
optimizer.update_slice()
optimizer.update_scaler = True
if getattr(optimizer._optim, '_param_groups', None) and isinstance(
optimizer._optim._param_groups[0], dict
):
for group in optimizer._optim._param_groups:
for param in group['params']:
tgt_grad = None
if (
hasattr(param, "main_grad")
and param.main_grad is not None
):
tgt_grad = param.main_grad
elif param.grad is not None:
tgt_grad = param.grad
if tgt_grad is not None:
param_grads.append(tgt_grad)
if tgt_grad.dtype in [
core.VarDesc.VarType.FP16,
paddle.float16,
]:
param_grads_fp16.append(tgt_grad)
elif tgt_grad.dtype in [paddle.bfloat16]:
param_grads_bfp16.append(tgt_grad)
else:
param_grads_fp32.append(tgt_grad)
else:
for param in optimizer._optim._parameter_list:
tgt_grad = None
if hasattr(param, "main_grad") and param.main_grad is not None:
tgt_grad = param.main_grad
elif param.grad is not None:
tgt_grad = param.grad
if tgt_grad is not None:
param_grads.append(tgt_grad)
if tgt_grad.dtype in [
core.VarDesc.VarType.FP16,
paddle.float16,
]:
param_grads_fp16.append(tgt_grad)
elif tgt_grad.dtype in [paddle.bfloat16]:
param_grads_bfp16.append(tgt_grad)
else:
param_grads_fp32.append(tgt_grad)
temp_found_inf_fp16 = paddle.to_tensor(np.array([0]).astype(np.bool_))
temp_found_inf_bfp16 = paddle.to_tensor(np.array([0]).astype(np.bool_))
temp_found_inf_fp32 = paddle.to_tensor(np.array([0]).astype(np.bool_))
device = paddle.get_device().split(":")[0]
device = "cpu" if optimizer.offload else device
dev_id = (
0 if device == "cpu" else int(paddle.get_device().split(":")[1])
)
self._found_inf = self._temp_found_inf_value_false
with device_guard(dev_id, device):
if len(param_grads_bfp16):
_legacy_C_ops.check_finite_and_unscale(
param_grads_bfp16,
self._scale,
param_grads_bfp16,
temp_found_inf_bfp16,
)
self._found_inf = _C_ops.bitwise_or(
self._found_inf, temp_found_inf_bfp16
)
if len(param_grads_fp16):
_legacy_C_ops.check_finite_and_unscale(
param_grads_fp16,
self._scale,
param_grads_fp16,
temp_found_inf_fp16,
)
self._found_inf = _C_ops.bitwise_or(
self._found_inf, temp_found_inf_fp16
)
if len(param_grads_fp32):
_legacy_C_ops.check_finite_and_unscale(
param_grads_fp32,
self._scale,
param_grads_fp32,
temp_found_inf_fp32,
)
self._found_inf = _C_ops.bitwise_or(
self._found_inf, temp_found_inf_fp32
)
self._found_inf = self._found_inf.cast("int32")
paddle.distributed.all_reduce(
self._found_inf, op=paddle.distributed.ReduceOp.MAX, group=None
)
self._found_inf = self._found_inf.cast("bool")
scaler._unscale = MethodType(unscale_method, scaler)
return scaler
def cvt_to_device(x, dev_id, blocking=True):
"""
Copy data in x from cpu memory to supported device
"""
if paddle.is_compiled_with_cuda():
place = paddle.CUDAPlace(dev_id)
elif paddle.is_compiled_with_xpu():
place = paddle.XPUPlace(dev_id)
else:
supported_custom_devices = ["npu"]
place = paddle.framework._current_expected_place()
if place.get_device_type() not in supported_custom_devices:
raise OSError(
"Only supported compiled paddle with gpu/rocm and xpu, but current version is compiled with cpu."
)
return x._copy_to(place, blocking)
@@ -0,0 +1,37 @@
# Copyright (c) 2021 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 ..utils.hybrid_parallel_util import (
broadcast_dp_parameters,
broadcast_sharding_parameters,
)
from ..utils.log_util import logger
from .meta_parallel_base import MetaParallelBase
__all__ = []
class ShardingParallel(MetaParallelBase):
def __init__(self, layers, hcg, **kwargs):
super().__init__(layers, hcg, **kwargs)
def _prepare_for_model(self):
logger.info("start broadcast sharding parameters")
broadcast_sharding_parameters(self._layers, self._hcg)
if self._hcg.get_data_parallel_world_size() > 1:
logger.info("start broadcast dp parameters")
broadcast_dp_parameters(self._layers, self._hcg)
logger.info("sharding's parameters is ready")
@@ -0,0 +1,66 @@
# Copyright (c) 2021 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 ..utils.hybrid_parallel_util import (
broadcast_dp_parameters,
broadcast_input_data,
broadcast_moe_sharding_parameters,
broadcast_mp_parameters,
broadcast_sep_parameters,
broadcast_sharding_parameters,
)
from ..utils.log_util import logger
from .meta_parallel_base import MetaParallelBase
__all__ = []
class TensorParallel(MetaParallelBase):
def __init__(self, layers, hcg, **kwargs):
super().__init__(layers, hcg, **kwargs)
def _prepare_for_model(self):
logger.info("start broadcast mp parameters")
broadcast_mp_parameters(self._layers, self._hcg)
if self._hcg.get_sep_parallel_world_size() > 1:
logger.info("start broadcast sep parameters")
broadcast_sep_parameters(self._layers, self._hcg, fuse_params=False)
if self._hcg.get_sharding_parallel_world_size() > 1:
logger.info("start broadcast sharding parameters")
broadcast_sharding_parameters(
self._layers, self._hcg, fuse_params=False
)
if self._hcg.get_data_parallel_world_size() > 1:
logger.info("start broadcast dp parameters")
broadcast_dp_parameters(self._layers, self._hcg, fuse_params=False)
if self._hcg.get_moe_sharding_parallel_world_size() > 1:
logger.info("start broadcast moe sharding parameters")
broadcast_moe_sharding_parameters(
self._layers, self._hcg, fuse_params=False
)
logger.info("mp's parameters is ready")
def _pre_forward(self, *inputs, **kwargs):
need_broadcast_data = True
if self._strategy is not None:
mp_configs = self._strategy.hybrid_configs["mp_configs"]
need_broadcast_data = mp_configs.need_broadcast_data
if need_broadcast_data:
logger.debug("mp start broadcast input data")
return broadcast_input_data(self._hcg, *inputs, **kwargs)
@@ -0,0 +1,133 @@
# 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.
# The file has been adapted from DeepSeek DualPipe project
# Copyright (c) 2025 DeepSeek
# Licensed under the MIT License - https://github.com/deepseek-ai/DualPipe/blob/main/LICENSE
import queue
from functools import partial
import paddle
import paddle.nn.functional as F
from paddle import nn
from paddle.autograd import PyLayer
class WeightGradStore:
enabled = False
cache = []
funcs_queue = queue.Queue()
@classmethod
def put(cls, func) -> None:
cls.cache.append(func)
@classmethod
def flush(cls) -> None:
cls.funcs_queue.put(cls.cache)
cls.cache = []
@classmethod
def pop(cls) -> None:
assert not cls.funcs_queue.empty(), "Pop empty queue."
funcs = cls.funcs_queue.get()
for func in funcs:
func()
@classmethod
def clear(cls) -> None:
cls.cache = []
cls.funcs_queue = queue.Queue()
class EventStore:
event = None
@classmethod
def set(cls, event) -> None:
cls.event = event
def fold_init_dims(tensor):
# NOTE(zhangyuqin1998): Reshape a rank-3 tensor from P x M x N to (P * M) x N,
# to keep weight_grad in a correct rank. See phi::FoldInitDims.
if tensor.ndim == 3:
tensor = paddle.reshape(tensor, [-1, tensor.shape[-1]])
return tensor
def grad_weight_fn(input, weight, out_grad, inplace_update_grad=True):
if weight.stop_gradient:
return
with paddle.no_grad():
weight_grad = paddle.matmul(
x=fold_init_dims(input),
y=fold_init_dims(out_grad),
transpose_x=True,
transpose_y=False,
)
if hasattr(weight, "main_grad"):
if weight.main_grad is None:
weight.main_grad = paddle.base.framework.core.eager.Tensor(
value=weight_grad.cast(paddle.float32).value(),
place=weight_grad.place,
name="main_grad@" + weight.name,
)
else:
weight.main_grad.add_(weight_grad)
weight_grad._clear_data()
else:
if weight.grad is None:
weight.grad = paddle.zeros_like(weight, dtype=weight.dtype)
weight.grad = paddle.add(weight.grad, weight_grad)
class SplitBWMatmul(PyLayer):
@staticmethod
def forward(ctx, input, weight, bias):
ctx.save_for_backward(input, weight, bias)
out = F.linear(x=input, weight=weight, bias=bias)
return out
@staticmethod
def backward(ctx, out_grad):
input, weight, bias = ctx.saved_tensor()
if WeightGradStore.enabled:
WeightGradStore.put(
partial(grad_weight_fn, input, weight, out_grad)
)
else:
grad_weight_fn(input, weight, out_grad)
input_grad = None
if not input.stop_gradient:
input_grad = paddle.matmul(
x=out_grad, y=weight, transpose_x=False, transpose_y=True
)
if bias is not None:
bias_grad = None
if not bias.stop_gradient:
bias_grad = paddle.sum(fold_init_dims(out_grad), axis=0)
return input_grad, None, bias_grad
else:
return input_grad, None
class SplitBWLinear(nn.Linear):
def forward(self, input):
return SplitBWMatmul.apply(input, self.weight, bias=self.bias)