# 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 random import types import numpy as np import paddle import paddle.distributed as dist from paddle import nn from paddle.distributed import fleet from paddle.distributed.auto_parallel._utils import ( _patch_grads_for_step, ) from paddle.distributed.auto_parallel.pipelining.schedules import ( Schedule1F1B, ScheduleFThenB, ScheduleVPP, ) from paddle.distributed.auto_parallel.pipelining.stage import ( PipelineStage, ) from paddle.io import DataLoader, Dataset def fix_seeds(seed=2025): """Fix random seeds to ensure reproducibility""" paddle.seed(seed) random.seed(seed) np.random.seed(seed) class PPMyModel(nn.Layer): def __init__(self): super().__init__() self.mesh = paddle.distributed.ProcessMesh( [0, 1, 2, 3], dim_names=["pp"] ) self.num_layers = 8 self.num_layers_per_card = self.num_layers // 4 self.linears = nn.LayerList() for i in range(self.num_layers): linear = nn.Linear(8, 8, bias_attr=False) # Mark network parameters linear.weight = dist.shard_tensor( linear.weight, self.get_pp_mesh(i), [dist.Replicate()], ) self.linears.append(linear) def get_pp_mesh(self, layer_index): # layer_index=0-3 corresponds to mesh_idx 0,0,1,1,2,2,3,3 mesh_idx = int(layer_index / (self.num_layers / 4)) return self.mesh[mesh_idx] def forward(self, x): x.stop_gradient = False out = x for i in range(self.num_layers): # Mark intermediate variables, reshard when switching devices if i % self.num_layers_per_card == 0 and i > 0: out = dist.reshard(out, self.get_pp_mesh(i), [dist.Replicate()]) out = self.linears[i](out) return paddle.cast(out, 'float32') class PPMyModel_SingleStage(nn.Layer): def __init__(self): super().__init__() self.mesh = paddle.distributed.ProcessMesh( [0, 1, 2, 3], dim_names=["pp"] ) self.num_layers = 8 self.num_layers_per_card = 2 self.linears = nn.LayerList() for i in range(self.num_layers): linear = nn.Linear(8, 8, bias_attr=False) linear.weight = dist.shard_tensor( linear.weight, self.get_pp_mesh(i), [dist.Replicate()], ) self.linears.append(linear) def get_pp_mesh(self, layer_index): # layer_index=0-7 maps to mesh_idx as 0,0,1,1,2,2,3,3 mesh_idx = int(layer_index // self.num_layers_per_card) return self.mesh[mesh_idx] def forward(self, x): x.stop_gradient = False out = x device_id = dist.get_rank() for i in range(self.num_layers): if int(i // self.num_layers_per_card) == device_id: out = self.linears[i](out) return paddle.cast(out, 'float32') class PPMyModel_MultiStage(nn.Layer): def __init__(self): super().__init__() self.mesh = paddle.distributed.ProcessMesh( [0, 1, 2, 3], dim_names=["pp"] ) self.num_layers = 8 self.linears = nn.LayerList() for i in range(self.num_layers): linear = nn.Linear(8, 8, bias_attr=False) linear.weight = dist.shard_tensor( linear.weight, self.get_pp_mesh(i), [dist.Replicate()], ) self.linears.append(linear) def get_pp_mesh(self, layer_index): mesh_idx = int(layer_index % 4) return self.mesh[mesh_idx] def forward(self, x): # For MultiStage, we shard model layers, so forward calls _Pipeline_model_chunk's forward pass class _Pipeline_model_chunk(nn.Layer): def __init__(self, layers): super().__init__() self.layers = layers def forward(self, x): out = x for layer in self.layers: out = layer(out) return out class PP_DP_MyModel(nn.Layer): def __init__(self): super().__init__() pp_mesh0 = paddle.distributed.ProcessMesh([0, 2], dim_names=["dp"]) pp_mesh1 = paddle.distributed.ProcessMesh([1, 3], dim_names=["dp"]) self.num_layers = 8 self.linears = nn.LayerList() for i in range(self.num_layers): linear = nn.Linear(8, 8, bias_attr=False) if i < 4: linear.weight = dist.shard_tensor( linear.weight, pp_mesh0, [dist.Replicate()] ) else: linear.weight = dist.shard_tensor( linear.weight, pp_mesh1, [dist.Replicate()] ) self.linears.append(linear) def forward(self, x): x.stop_gradient = False out = x # Get current rank's position in pp group (0 or 1) pp_rank = dist.get_rank() % 2 # Only process layers belonging to current rank start_layer = ( 4 * pp_rank ) # rank 0/2 processes layers 0-3, rank 1/3 processes layers 4-7 end_layer = start_layer + 4 for i in range(start_layer, end_layer): out = self.linears[i](out) return paddle.cast(out, 'float32') class RandomDataset(Dataset): def __init__(self, image_size, output_size, num_samples=1): super().__init__() self.image_size = image_size self.num_samples = num_samples self.output_size = output_size def __getitem__(self, index): input = paddle.rand([self.image_size], dtype='float32') label = paddle.rand([self.output_size], dtype='float32') return input, label def __len__(self): return self.num_samples class Test_Schedules: @classmethod def setUpClass(cls): """Initialize test class setup""" paddle.distributed.init_parallel_env() cls.group = paddle.distributed.new_group([0, 1, 2, 3]) cls.rank = dist.get_rank() cls.mesh = paddle.distributed.ProcessMesh( [0, 1, 2, 3], dim_names=["pp"] ) fleet.auto.set_mesh(cls.mesh) def test_ScheduleFThenB(self): fix_seeds() self.model = PPMyModel_SingleStage() self.micro_batches = 8 self.stage = PipelineStage(self.model, self.rank, 4, group=self.group) self.stage.has_backward = True loss_fn_ = nn.MSELoss() schedule = ScheduleFThenB( self.stage, self.micro_batches, loss_fn=loss_fn_ ) opt = paddle.optimizer.AdamW( learning_rate=0.001, parameters=self.model.parameters() ) dataset = RandomDataset(image_size=8, output_size=8, num_samples=8) loader = DataLoader(dataset, batch_size=8) losses_by_step = [] num_iterations = 20 for iter_idx in range(num_iterations): losses_by_micro_batch = [] for i, (data, label) in enumerate(loader): schedule.step(data, target=label, losses=losses_by_micro_batch) if self.rank == 3: losses_by_step.append( np.array(losses_by_micro_batch, dtype=np.float32).mean() ) opt.step() opt.clear_grad() return losses_by_step def test_Schedule1F1B(self): fix_seeds() self.model = PPMyModel_SingleStage() self.micro_batches = 8 self.stage = PipelineStage(self.model, self.rank, 4, group=self.group) self.stage.has_backward = True loss_fn_ = nn.MSELoss() schedule = Schedule1F1B( self.stage, self.micro_batches, loss_fn=loss_fn_ ) opt = paddle.optimizer.AdamW( learning_rate=0.001, parameters=self.model.parameters() ) dataset = RandomDataset(image_size=8, output_size=8, num_samples=8) loader = DataLoader(dataset, batch_size=8) losses_by_step = [] num_iterations = 20 for iter_idx in range(num_iterations): losses_by_micro_batch = [] for i, (data, label) in enumerate(loader): schedule.step(data, target=label, losses=losses_by_micro_batch) if self.rank == 3: losses_by_step.append( np.array(losses_by_micro_batch, dtype=np.float32).mean() ) opt.step() opt.clear_grad() return losses_by_step def test_ScheduleVPP(self): fix_seeds() self.model = PPMyModel_MultiStage() self.local_stages = 2 self.micro_batches = 8 self.stage_list = [] for i in range(self.local_stages): stage_model = _Pipeline_model_chunk( self.model.linears[self.rank + i * 4 : self.rank + i * 4 + 1] ) self.stage_list.append( PipelineStage( stage_model, self.rank + i * 4, 8, group=self.group ) ) self.stage_list[i].has_backward = True loss_fn_ = nn.MSELoss() schedule = ScheduleVPP( self.stage_list, self.micro_batches, loss_fn=loss_fn_ ) opt = paddle.optimizer.AdamW( learning_rate=0.001, parameters=self.model.parameters() ) dataset = RandomDataset(image_size=8, output_size=8, num_samples=8) loader = DataLoader(dataset, batch_size=8) losses_by_micro_batch = [] losses_by_step = [] num_iterations = 20 for iter_idx in range(num_iterations): for i, (data, label) in enumerate(loader): schedule.step(data, target=label, losses=losses_by_micro_batch) if self.rank == 3: losses_by_step.append( np.array(losses_by_micro_batch, dtype=np.float32).mean() ) opt.step() opt.clear_grad() return losses_by_step def test_pp_model(self): """Test pipeline parallel model using PPMyModel as the baseline""" fix_seeds() pp_model = PPMyModel() opt = paddle.optimizer.AdamW( learning_rate=0.001, parameters=pp_model.parameters() ) loss_fn = nn.MSELoss() dataset = RandomDataset(image_size=8, output_size=8, num_samples=8) loader = DataLoader(dataset, batch_size=1) pp_losses_step = [] num_iterations = 20 for iter_idx in range(num_iterations): pp_losses_micro_batch = [] for i, (data, label) in enumerate(loader): output = pp_model(data) loss = loss_fn(output, label) pp_losses_micro_batch.append(loss.item()) loss.backward() pp_losses_step.append( np.array(pp_losses_micro_batch, dtype=np.float32).mean() ) opt.step() opt.clear_grad() return pp_losses_step def test_dp_pp(self): fix_seeds() global_mesh = paddle.distributed.ProcessMesh( [[0, 2], [1, 3]], dim_names=["pp", "dp"] ) fleet.auto.set_mesh(global_mesh) self.model = PP_DP_MyModel() pp_mesh0 = paddle.distributed.ProcessMesh([0, 2], dim_names=["dp"]) pp_mesh1 = paddle.distributed.ProcessMesh([1, 3], dim_names=["dp"]) dp_pp_pleacement = [dist.Shard(0)] pp_group_1 = paddle.distributed.new_group([0, 1]) pp_group_2 = paddle.distributed.new_group([2, 3]) dp_group = paddle.distributed.new_group([1, 3]) self.micro_batches = 4 if self.rank < 2: self.stage = PipelineStage( self.model, self.rank % 2, 2, group=pp_group_1 ) else: self.stage = PipelineStage( self.model, self.rank % 2, 2, group=pp_group_2 ) self.stage.has_backward = True loss_fn_ = nn.MSELoss() schedule = ScheduleFThenB( self.stage, self.micro_batches, loss_fn=loss_fn_ ) opt = paddle.optimizer.AdamW( learning_rate=0.001, parameters=self.model.parameters() ) dataset = RandomDataset(image_size=8, output_size=8, num_samples=8) loader = DataLoader(dataset, batch_size=8) losses_by_step = [] num_iterations = 20 all_losses_in_one_step_md5sum = [] for iter_idx in range(num_iterations): losses_by_micro_batch = [] for i, (data, label) in enumerate(loader): # reorder data and label batch_size = data.shape[0] even_indices = list(range(0, batch_size, 2)) odd_indices = list(range(1, batch_size, 2)) reordered_indices = even_indices + odd_indices reordered_data = data[reordered_indices] reordered_label = label[reordered_indices] dist_data = dist.shard_tensor( reordered_data, pp_mesh0, dp_pp_pleacement ) dist_label = dist.shard_tensor( reordered_label, pp_mesh1, dp_pp_pleacement ) schedule.step( dist_data, target=dist_label, losses=losses_by_micro_batch ) # Losses from two dp paths are in Partial(AVG) state, need to do all_reduce if self.rank == 1 or self.rank == 3: reduced_losses = [] for item in losses_by_micro_batch: local_loss = item._local_value() dist.all_reduce( local_loss, op=dist.ReduceOp.AVG, group=dp_group ) reduced_losses.append(local_loss) if iter_idx == 0: all_losses_in_one_step_md5sum.append( local_loss._md5sum() ) if self.rank == 3: # Calculate mean using reduced losses losses_by_step.append( np.array(reduced_losses, dtype=np.float32).mean() ) opt.step() opt.clear_grad() return losses_by_step, all_losses_in_one_step_md5sum def test_pp_model_with_ClipGradByGlobalNorm(self): """Test pipeline parallel model with ClipGradByGlobalNorm using PPMyModel as the baseline""" fix_seeds() pp_model = PPMyModel() opt = paddle.optimizer.AdamW( learning_rate=0.001, parameters=pp_model.parameters(), grad_clip=paddle.nn.ClipGradByGlobalNorm(1.0), ) loss_fn = nn.MSELoss() dataset = RandomDataset(image_size=8, output_size=8, num_samples=8) loader = DataLoader(dataset, batch_size=1) pp_losses_step = [] num_iterations = 20 for iter_idx in range(num_iterations): pp_losses_micro_batch = [] for i, (data, label) in enumerate(loader): output = pp_model(data) loss = loss_fn(output, label) pp_losses_micro_batch.append(loss.item()) loss.backward() pp_losses_step.append( np.array(pp_losses_micro_batch, dtype=np.float32).mean() ) opt.step() opt.clear_grad() return pp_losses_step def test_ScheduleFThenB_with_ClipGradByGlobalNorm(self): fix_seeds() self.model = PPMyModel_SingleStage() self.micro_batches = 8 self.stage = PipelineStage(self.model, self.rank, 4, group=self.group) self.stage.has_backward = True loss_fn_ = nn.MSELoss() schedule = ScheduleFThenB( self.stage, self.micro_batches, loss_fn=loss_fn_ ) opt = paddle.optimizer.AdamW( learning_rate=0.001, parameters=self.model.parameters(), grad_clip=paddle.nn.ClipGradByGlobalNorm(1.0), ) dataset = RandomDataset(image_size=8, output_size=8, num_samples=8) loader = DataLoader(dataset, batch_size=8) losses_by_step = [] num_iterations = 20 for iter_idx in range(num_iterations): losses_by_micro_batch = [] for i, (data, label) in enumerate(loader): schedule.step(data, target=label, losses=losses_by_micro_batch) if self.rank == 3: losses_by_step.append( np.array(losses_by_micro_batch, dtype=np.float32).mean() ) opt.step() opt.clear_grad() return losses_by_step def test_FthenB_align_mode_of_GradientClipByGlobalNorm(self): fix_seeds() paddle.set_flags( {'FLAGS_enable_auto_parallel_align_mode': True} ) # Represents logical alignment with GradientClipByGlobalNorm that is semi-automatically parallel to the original dynamic graph, because the processing logic here is not aligned with the dynamic graph manually parallel self.model = PPMyModel_SingleStage() self.micro_batches = 8 self.stage = PipelineStage(self.model, self.rank, 4, group=self.group) self.stage.has_backward = True loss_fn_ = nn.MSELoss() schedule = ScheduleFThenB( self.stage, self.micro_batches, loss_fn=loss_fn_ ) opt = paddle.optimizer.AdamW( learning_rate=0.001, parameters=self.model.parameters(), grad_clip=paddle.nn.ClipGradByGlobalNorm(1.0), ) if dist.in_auto_parallel_align_mode(): # When in auto parallel align mode, patching the optimizer step function orig_step = ( opt.step.__func__ if hasattr(opt.step, "__func__") else opt.step ) decorator = _patch_grads_for_step(amp_master_grad=True) new_step = decorator( orig_step ) # When the step function is wrapped by the decorator, it initializes gradients for parameters belonging to other ranks prior to step method execution, ensuring their metadata is preserved. opt.step = types.MethodType(new_step, opt) dataset = RandomDataset(image_size=8, output_size=8, num_samples=8) loader = DataLoader(dataset, batch_size=8) losses_by_step = [] num_iterations = 20 for iter_idx in range(num_iterations): losses_by_micro_batch = [] for i, (data, label) in enumerate(loader): schedule.step(data, target=label, losses=losses_by_micro_batch) if self.rank == 3: losses_by_step.append( np.array(losses_by_micro_batch, dtype=np.float32).mean() ) opt.step() opt.clear_grad() paddle.set_flags({'FLAGS_enable_auto_parallel_align_mode': False}) return losses_by_step def test_dp_pp_align_mode(self): fix_seeds() paddle.set_flags( {'FLAGS_enable_auto_parallel_align_mode': True} ) # Represents manual parallel alignment with dynamic graphs, mainly segmenting microbatches when aligning DP and PP mixing global_mesh = paddle.distributed.ProcessMesh( [[0, 2], [1, 3]], dim_names=["pp", "dp"] ) fleet.auto.set_mesh(global_mesh) self.model = PP_DP_MyModel() pp_mesh0 = paddle.distributed.ProcessMesh([0, 2], dim_names=["dp"]) pp_mesh1 = paddle.distributed.ProcessMesh([1, 3], dim_names=["dp"]) dp_pp_pleacement = [dist.Shard(0)] pp_group_1 = paddle.distributed.new_group([0, 1]) pp_group_2 = paddle.distributed.new_group([2, 3]) dp_group = paddle.distributed.new_group([1, 3]) self.micro_batches = 4 if self.rank < 2: self.stage = PipelineStage( self.model, self.rank % 2, 2, group=pp_group_1 ) else: self.stage = PipelineStage( self.model, self.rank % 2, 2, group=pp_group_2 ) self.stage.has_backward = True loss_fn_ = nn.MSELoss() schedule = ScheduleFThenB( self.stage, self.micro_batches, loss_fn=loss_fn_ ) opt = paddle.optimizer.AdamW( learning_rate=0.001, parameters=self.model.parameters() ) dataset = RandomDataset(image_size=8, output_size=8, num_samples=8) loader = DataLoader(dataset, batch_size=8) losses_by_step = [] all_losses_in_one_step_md5sum = [] num_iterations = 20 for iter_idx in range(num_iterations): losses_by_micro_batch = [] for i, (data, label) in enumerate(loader): dist_data = dist.shard_tensor(data, pp_mesh0, dp_pp_pleacement) dist_label = dist.shard_tensor( label, pp_mesh1, dp_pp_pleacement ) schedule.step( dist_data, target=dist_label, losses=losses_by_micro_batch ) # Losses from two dp paths are in Partial(AVG) state, need to do all_reduce if self.rank == 1 or self.rank == 3: reduced_losses = [] for item in losses_by_micro_batch: local_loss = item._local_value() dist.all_reduce( local_loss, op=dist.ReduceOp.AVG, group=dp_group ) reduced_losses.append(local_loss) if iter_idx == 0: all_losses_in_one_step_md5sum.append( local_loss._md5sum() ) if self.rank == 3: # Calculate mean using reduced losses losses_by_step.append( np.array(reduced_losses, dtype=np.float32).mean() ) opt.step() opt.clear_grad() paddle.set_flags({'FLAGS_enable_auto_parallel_align_mode': False}) return losses_by_step, all_losses_in_one_step_md5sum def run_test(self): """Compare losses between three training methods""" self.setUpClass() pp_losses = self.test_pp_model() scheduleFThenB_losses = self.test_ScheduleFThenB() schedule1f1b_losses = self.test_Schedule1F1B() schedulevpp_losses = self.test_ScheduleVPP() pp_model_with_ClipGradByGlobalNorm_losses = ( self.test_pp_model_with_ClipGradByGlobalNorm() ) scheduleFThenB_with_ClipGradByGlobalNorm_losses = ( self.test_ScheduleFThenB_with_ClipGradByGlobalNorm() ) scheduleFthenB_align_mode_losses_of_GradientClipByGlobalNorm = ( self.test_FthenB_align_mode_of_GradientClipByGlobalNorm() ) dp_pp_losses, dp_pp_losses_md5sum = self.test_dp_pp() dp_pp_align_mode_losses, dp_pp_align_mode_losses_md5sum = ( self.test_dp_pp_align_mode() ) if self.rank == 3: np.testing.assert_allclose( pp_losses, scheduleFThenB_losses, rtol=1e-5, ) np.testing.assert_allclose( schedule1f1b_losses, scheduleFThenB_losses, rtol=1e-5, ) np.testing.assert_allclose( schedulevpp_losses, scheduleFThenB_losses, rtol=1e-5, ) np.testing.assert_allclose( dp_pp_losses, scheduleFThenB_losses, rtol=1e-5, ) np.testing.assert_allclose( pp_model_with_ClipGradByGlobalNorm_losses, scheduleFThenB_with_ClipGradByGlobalNorm_losses, rtol=1e-5, ) np.testing.assert_allclose( dp_pp_align_mode_losses, dp_pp_losses, rtol=1e-5, ) np.testing.assert_allclose( scheduleFthenB_align_mode_losses_of_GradientClipByGlobalNorm, pp_model_with_ClipGradByGlobalNorm_losses, rtol=1e-5, ) assert dp_pp_losses_md5sum == dp_pp_align_mode_losses_md5sum if __name__ == '__main__': Test_Schedules().run_test()