# 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