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

4193 lines
161 KiB
Python

# 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
from __future__ import annotations
import os
import queue
import sys
import time
import warnings
from collections import defaultdict, deque
from dataclasses import dataclass
from enum import Enum
from functools import partial
import paddle
from paddle import framework
from ..meta_optimizers.dygraph_optimizer import HybridParallelOptimizer
from ..utils import timer_helper as timer
from ..utils.hybrid_parallel_util import (
broadcast_dp_parameters,
broadcast_moe_sharding_parameters,
broadcast_mp_parameters,
broadcast_sep_parameters,
broadcast_sharding_parameters,
)
from ..utils.log_util import get_sync_logger, logger
from .meta_parallel_base import MetaParallelBase
from .parallel_layers.pp_layers import PipelineLayer
_use_four_directions = os.environ.get(
'PADDLE_USE_FOUR_DIRECTIONS_P2P', paddle.base.core.is_compiled_with_xpu()
)
_use_four_directions = False # xpu use the same p2p method as gpu
if _use_four_directions:
from .pp_utils import four_directions_p2p_communication as p2p
else:
from .pp_utils import p2p_communication as p2p
from typing import TYPE_CHECKING
from paddle.distributed import fleet
from paddle.distributed.fleet.utils.tensor_fusion_helper import (
HOOK_ACTION,
FusedCommBuffer,
assign_group_by_size,
)
from .pipeline_hooks import (
PipelineHook,
)
from .pp_utils.utils import dict_to_tuple_helper, tuple_to_dict_helper
from .zero_bubble_utils import WeightGradStore
if TYPE_CHECKING:
from collections.abc import Callable
g_profile_pipeline_details_steps = int(
os.getenv("FLAGS_profile_pipeline_details_steps", "0")
)
__all__ = []
def profile_pipeline_details(msg):
GB = 1024.0 * 1024.0 * 1024.0
if paddle.base.core.is_compiled_with_cuda():
memory_allocated_size = paddle.device.cuda.memory_allocated() / GB
memory_reserved_size = paddle.device.cuda.memory_reserved() / GB
else:
memory_allocated_size, memory_reserved_size = 0, 0
get_sync_logger().info(
f"{msg}: memory_allocated_size={memory_allocated_size:.2f}, memory_reserved_size={memory_reserved_size:.2f}"
)
def get_action(is_dp, shard_split_param=False):
if is_dp:
return HOOK_ACTION.ALL_REDUCE
if shard_split_param:
return HOOK_ACTION.REDUCE_SCATTER
return HOOK_ACTION.REDUCE
def _get_align_mode_scale():
hcg = fleet.get_hybrid_communicate_group()
data_parallel_world_size = hcg.get_data_parallel_world_size()
sharding_parallel_world_size = hcg.get_sharding_parallel_world_size()
return max(data_parallel_world_size, 1) * max(
sharding_parallel_world_size, 1
)
def _can_free(t):
"""
Check if a tensor can be freed.
A tensor can be freed only if all of the following conditions are met:
1. Tensor is not None
2. Is a paddle.Tensor type
3. Has been initialized
4. inplace_version is 0 (not using in-place ops) or explicitly marked as freeable
Args:
t: The tensor to check
Returns:
bool: True if the tensor can be freed, False otherwise
"""
return (
t is not None
and isinstance(t, paddle.Tensor)
and t._is_initialized()
and (t.inplace_version == 0 or getattr(t, "pp_can_free", False))
)
def _collect_all_tensors(obj, tensor_set):
"""
Recursively collect all tensors from a complex object.
This function traverses nested data structures (tuple, list, dict) and finds
all paddle.Tensor instances, adding them to the tensor_set. Used in Pipeline
Parallel to identify all tensors that need to be managed.
Args:
obj: Any complex object that may contain nested tuple, list, dict and paddle.Tensor
tensor_set: A set to store the collected tensors
"""
visited = set()
stack = [obj]
while stack:
current = stack.pop()
obj_id = id(current)
if obj_id in visited:
continue
visited.add(obj_id)
if isinstance(current, (tuple, list)):
stack.extend(current)
elif isinstance(current, dict):
stack.extend(current.values())
elif isinstance(current, paddle.Tensor):
# Check for duplicate addition
if current in tensor_set:
logger.debug(f"Duplicate tensor detected: {current}")
tensor_set.add(current)
def _release_output(output):
"""
Release the data pointer of output tensors.
Collects all tensors from output and frees the data pointer of those that
meet the release criteria. Used in Pipeline Parallel to release output
tensor memory after forward propagation to avoid unnecessary memory usage.
Args:
output: The output object, which can be a tensor, tuple, list, or dict
"""
all_tensors = set()
_collect_all_tensors(output, all_tensors)
for t in all_tensors:
if _can_free(t):
t._clear_dataptr()
def _release_input(input, output):
"""
Release the data pointer of input tensors.
Only releases input tensors that do not appear in the output. This is because
in Pipeline Parallel, if an input tensor is used in the output (e.g., residual
connection), it cannot be freed early. This function ensures that input memory
is released without affecting tensors needed for subsequent computation.
Args:
input: The input object, which can be a tensor, tuple, list, or dict
output: The output object, used to determine which input tensors should not be freed
"""
output_tensors = set()
_collect_all_tensors(output, output_tensors)
def can_release(t):
if not _can_free(t):
return False
return t not in output_tensors
input_tensors = set()
_collect_all_tensors(input, input_tensors)
for t in input_tensors:
if can_release(t):
t._clear_dataptr()
# assume only the first stage and last stage need data, and data consumption is ordered
# to be replaced by real micro dataset from reader
class FakeMicroDataset:
def __init__(
self,
data,
is_first_stage,
is_last_stage,
acc_steps,
micro_batch_size,
):
self._data = data
self._index = 0
self._acc_steps = acc_steps
self._is_first_stage = is_first_stage
self._is_last_stage = is_last_stage
self._micro_batch_size = micro_batch_size
def __iter__(self):
return self
def __next__(self):
if self._index >= self._acc_steps:
raise StopIteration
assert self._is_first_stage or self._is_last_stage
micro_batch_data = self._load_micro_batch(self._index)
self._index += 1
if self._index >= self._acc_steps:
self._data = None # clearup
return micro_batch_data
def _load_micro_batch(self, micro_step):
inputs = self._data
data = None
label = None
if self._is_first_stage:
assert len(inputs) == 2, "length of input should be 2"
data = self._load_micro_batch_impl(inputs[0], micro_step)
if self._is_last_stage:
assert len(inputs) == 2, "length of input should be 2"
label = self._load_micro_batch_impl(inputs[1], micro_step)
return (data, label)
def _load_micro_batch_impl(self, inputs, micro_step):
begin = micro_step * self._micro_batch_size
end = begin + self._micro_batch_size
if isinstance(inputs, tuple):
output = []
for data in inputs:
if isinstance(data, list):
assert len(data) == self._acc_steps, (
f"length of data should be {self._acc_steps}, but it is {len(data)}"
)
output.append(
data[micro_step].detach()
if data[micro_step] is not None
else None
)
elif data is not None:
self._check_data_valid(data)
output.append(data[begin:end, :].detach())
else:
output.append(None)
return tuple(output)
elif isinstance(inputs, dict):
output_dict = {}
for key, data in inputs.items():
if isinstance(data, list):
assert len(data) == self._acc_steps, (
f"length of data should be {self._acc_steps}, but it is {len(data)}"
)
output_dict[key] = (
data[micro_step].detach()
if data[micro_step] is not None
else None
)
elif data is not None:
self._check_data_valid(data)
output_dict[key] = data[begin:end, :].detach()
else:
output_dict[key] = None
return output_dict
elif isinstance(inputs, list):
assert len(inputs) == self._acc_steps, (
f"length of data should be {self._acc_steps}, but it is {len(inputs)}"
)
if isinstance(inputs[micro_step], list):
return [
tensor.detach() if tensor is not None else None
for tensor in inputs[micro_step]
]
return inputs[micro_step].detach()
elif inputs is not None:
self._check_data_valid(inputs)
return inputs[begin:end, :].detach()
else:
return None
def _check_data_valid(self, data):
batch_size = data.shape[0]
assert self._micro_batch_size * self._acc_steps == batch_size, (
"batch_size needs to be divisible by micro_batch_size. Currently, "
f"batch_size = {batch_size}, micro_batch_size = {self._micro_batch_size}, accumulate_steps = {self._acc_steps}."
)
# A wrapper for pipeline dataser, to avoid GPU memory leaks.
class PipelineDatasetPreprocessor:
def __init__(self, function):
self.function = function
def __call__(self):
return self.function()
# Enum for specifying the pipeline parallel micro-step locations.
class PipelineParallelMicroStepLocations(Enum):
FORWARD_BEGIN = 'forward_begin'
FORWARD_END = 'forward_end'
BACKWARD_BEGIN = 'backward_begin'
BACKWARD_END = 'backward_end'
# A callback class for managing hooks at different stages of a pipeline parallel process.
class PipelineParallelMicroStepCallback:
def __init__(self):
# Initializes a dictionary to store hooks for each micro-step location in the pipeline.
self.hooks: dict[PipelineParallelMicroStepLocations, list[Callable]] = {
PipelineParallelMicroStepLocations.FORWARD_BEGIN: [],
PipelineParallelMicroStepLocations.FORWARD_END: [],
PipelineParallelMicroStepLocations.BACKWARD_BEGIN: [],
PipelineParallelMicroStepLocations.BACKWARD_END: [],
}
def register_hook(
self, location: PipelineParallelMicroStepLocations, hook: Callable
):
"""
Registers a hook function to be called at a specified pipeline parallel micro-step location.
Args:
location (PipelineParallelMicroStepLocations): The micro-step location where the hook should be registered.
hook (Callable): The hook function to be registered. The function should accept the following optional keyword arguments:
- input_tensor (paddle.Tensor): The input tensor to the current micro-step.
- output_tensor (paddle.Tensor): The output tensor from the current micro-step.
- input_tensor_grad (paddle.Tensor): The gradient of the input tensor.
- output_tensor_grad (paddle.Tensor): The gradient of the output tensor.
- step_id (paddle.Tensor): An identifier for the current step in the pipeline.
Raises:
AssertionError: If the specified location is not a valid micro-step location.
"""
assert location in self.hooks, (
f"Invalid location '{location}'. Valid locations are 'forward_begin', 'forward_end', 'backward_begin', or 'backward_end'."
)
self.hooks[location].append(hook)
def on_location(
self, location: PipelineParallelMicroStepLocations, **kwargs
):
"""
Triggers all registered hooks at a specified pipeline parallel micro-step location.
Args:
location (PipelineParallelMicroStepLocations): The micro-step location where the hooks should be triggered.
kwargs: Additional keyword arguments to be passed to the hook functions.
Raises:
AssertionError: If the specified location is not a valid micro-step location.
"""
assert location in self.hooks, (
f"Invalid location '{location}'. Valid locations are 'forward_begin', 'forward_end', 'backward_begin', or 'backward_end'."
)
for hook in self.hooks[location]:
hook(**kwargs)
pipeline_parallel_callbacks_ = PipelineParallelMicroStepCallback()
# It is typically very difficult for us to directly access the PipelineParallel object.
# Users may use fleet.distributed_model to wrap a model into a pipeline parallel model (PP model).
# We may not have access to the wrapped model when we want to register hooks, for example, when using PaddleNLP trainer to wrap around the PP model.
# Additionally, we usually have only one `PipelineParallel` model, so the callbacks are registered globally.
def register_global_pipeline_parallel_hook(
location: PipelineParallelMicroStepLocations, hook: Callable
):
"""
Registering global hooks for pipeline parallelism.
"""
pipeline_parallel_callbacks_.register_hook(location, hook)
class NoPipelineParallel(MetaParallelBase):
def __init__(self, layers, strategy, hcg=None):
assert isinstance(layers, PipelineLayer)
super().__init__(layers, hcg, strategy)
self._layers = layers
self._strategy = strategy
self._hcg = hcg
self.micro_batch_size = self._strategy.pipeline_configs[
"micro_batch_size"
]
self.accumulate_steps = self._strategy.pipeline_configs[
"accumulate_steps"
]
self._dp_comm_overlap = False
self._sharding_comm_overlap = False
# store total loss of entire batch. It contains the loss of each micro batch in a list, then contains many loss_fn's list in total_loss.
self.total_loss = None
# default loss function index
self.loss_fn_idx = 0
if self._hcg is not None:
self.use_data_parallel = (
self._hcg.get_data_parallel_world_size() > 1
)
self.use_model_parallel = (
self._hcg.get_model_parallel_world_size() > 1
)
self.use_sep_parallel = self._hcg.get_sep_parallel_world_size() > 1
self.use_sharding_parallel = (
self._hcg.get_sharding_parallel_world_size() > 1
)
self.use_moe_sharding_parallel = (
self._hcg.get_moe_sharding_parallel_world_size() > 1
)
self.dp_group = self._hcg.get_data_parallel_group()
# fused sep and dp
if self.use_sep_parallel:
self.dp_group = self._hcg.get_dp_sep_parallel_group()
if self.use_model_parallel:
logger.info("start broadcast mp parameters")
broadcast_mp_parameters(self._layers, self._hcg)
if self.use_sep_parallel:
logger.info("start broadcast sep parameters")
broadcast_sep_parameters(self._layers, self._hcg)
if self.use_sharding_parallel:
logger.info("start broadcast sharding parameters")
broadcast_sharding_parameters(self._layers, self._hcg)
if self.use_data_parallel:
logger.info("start broadcast dp parameters")
broadcast_dp_parameters(self._layers, self._hcg)
if self.use_moe_sharding_parallel:
logger.info("start broadcast moe_sharding parameters")
broadcast_moe_sharding_parameters(self._layers, self._hcg)
def is_pipeline_last_stage(self, ignore_virtual=False):
return True
def _check_micro_batch_data_valid(self, micro_batch_data):
if isinstance(micro_batch_data, (tuple, list)):
for data in micro_batch_data:
self._check_micro_batch_data_valid(data)
elif isinstance(micro_batch_data, dict):
for value in micro_batch_data.values():
self._check_micro_batch_data_valid(value)
elif micro_batch_data is not None:
assert isinstance(micro_batch_data, paddle.Tensor)
def _prepare_training(self, data, optimizer, lr_scheduler):
assert framework._dygraph_tracer()._has_grad, (
"Please enable the generation of gradients."
)
self.optimizer = optimizer
self.lr_scheduler = lr_scheduler
self._layers.train()
return data
def _optimizer_step(self):
for p in self._layers.parameters():
if hasattr(p, "main_grad") and p.main_grad is not None:
assert p.grad is None
p.main_grad = p.main_grad.scale(1.0 / self.accumulate_steps)
elif p.grad is not None:
p.grad = p.grad.scale(1.0 / self.accumulate_steps)
if self.scaler:
self.scaler.step(self.optimizer)
self.scaler.update()
else:
self.optimizer.step()
self.optimizer.clear_grad()
if self.lr_scheduler:
self.lr_scheduler.step()
def forward_backward_pipeline(
self,
data,
scaler=None,
return_micro_batch_loss=False,
):
self.scaler = scaler
self.total_loss = None
if isinstance(data, PipelineDatasetPreprocessor):
data = data()
if (not isinstance(data, tuple)) and (not isinstance(data, list)):
micro_dataset = data
else:
micro_dataset = FakeMicroDataset(
data,
True,
True,
self.accumulate_steps,
self.micro_batch_size,
)
loss_list = []
for _ in range(self.accumulate_steps):
# data prepare
data_iter = next(micro_dataset)
input_tensor = data_iter[0]
label = data_iter[1]
self._check_micro_batch_data_valid(input_tensor)
self._check_micro_batch_data_valid(label)
# forward
output_tensor = self._layers.forward(input_tensor)
# loss is loss_fn[loss_fn_idx]'s result
loss = None
# cal loss
for idx, loss_fn in enumerate(self._layers._loss_fn):
loss_tensor = loss_fn(output_tensor, label)
assert isinstance(loss_tensor, paddle.Tensor), (
"Currently, loss_fn should obtain Paddle.Tensor dtype"
)
if self.total_loss is None:
self.total_loss = []
# when self.total_loss length is less than idx, append a new tensor
if len(self.total_loss) <= idx:
self.total_loss.append([])
self.total_loss[idx].append(loss_tensor.detach())
if idx == self.loss_fn_idx:
loss = loss_tensor
# backward
with paddle.amp.auto_cast(enable=False):
if self.scaler:
paddle.autograd.backward(self.scaler.scale(loss))
else:
paddle.autograd.backward(loss)
assert self.total_loss is not None, (
"train_batch() in last stage should obtain valid loss"
)
losses = []
with paddle.amp.auto_cast(enable=False):
for idx in range(len(self._layers._loss_fn)):
self.total_loss[idx] = paddle.to_tensor(self.total_loss[idx])
if not return_micro_batch_loss:
# TODO(shenliang03): it will use mean/sum to calculate loss
tmp = paddle.zeros_like(self.total_loss[idx][0])
for loss in self.total_loss[idx]:
tmp += loss.detach()
losses.append(tmp / self.accumulate_steps)
else:
losses.append(self.total_loss[idx].detach())
return losses[0] if len(losses) == 1 else losses
def train_batch(
self,
data,
optimizer,
lr_scheduler=None,
scaler=None,
loss_fn_idx=0,
return_micro_batch_loss=False,
):
data = self._prepare_training(data, optimizer, lr_scheduler)
# check loss_fn_idx is valid and loss_fn exists
assert (
loss_fn_idx in range(len(self._layers._loss_fn))
and self._layers._loss_fn[loss_fn_idx] is not None
), f"loss function {loss_fn_idx} should exist to compute loss"
self.loss_fn_idx = loss_fn_idx
# no pipeline parallel
train_loss = self.forward_backward_pipeline(
data, scaler, return_micro_batch_loss=return_micro_batch_loss
)
# optimizer
with paddle.amp.auto_cast(enable=False):
self._optimizer_step()
return train_loss
def eval_batch(
self, data, compute_loss=False, loss_fn_idx=0, return_host_tensor=False
):
# check loss_fn_idx is valid and loss_fn exists
assert (
loss_fn_idx in range(len(self._layers._loss_fn))
and self._layers._loss_fn[loss_fn_idx] is not None
), f"loss function {loss_fn_idx} should exist to compute loss"
self.loss_fn_idx = loss_fn_idx
self.total_loss = None
if isinstance(data, PipelineDatasetPreprocessor):
data = data()
if (not isinstance(data, tuple)) and (not isinstance(data, list)):
micro_dataset = data
else:
micro_dataset = FakeMicroDataset(
data,
True,
True,
self.accumulate_steps,
self.micro_batch_size,
)
loss_list = []
output_list = []
for _ in range(self.accumulate_steps):
# data prepare
data_iter = next(micro_dataset)
input_tensor = data_iter[0]
label = data_iter[1]
self._check_micro_batch_data_valid(input_tensor)
self._check_micro_batch_data_valid(label)
# forward
output_tensor = self._layers.forward(input_tensor)
if compute_loss:
# loss is loss_fn[loss_fn_idx]'s result
loss = None
# cal loss
for idx, loss_fn in enumerate(self._layers._loss_fn):
loss_tensor = loss_fn(output_tensor, label)
assert isinstance(loss_tensor, paddle.Tensor), (
"Currently, loss_fn should obtain Paddle.Tensor dtype"
)
if self.total_loss is None:
self.total_loss = []
# when self.total_loss length is less than idx, append a new tensor
if len(self.total_loss) <= idx:
self.total_loss.append([])
self.total_loss[idx].append(loss_tensor.detach())
if idx == self.loss_fn_idx:
loss = loss_tensor
assert self.total_loss is not None, (
"train_batch() in last stage should obtain valid loss"
)
else:
if return_host_tensor:
self._offload_tensors(output_tensor)
output_list.append(output_tensor)
if compute_loss:
losses = []
return_micro_batch_loss = False
for idx in range(len(self._layers._loss_fn)):
self.total_loss[idx] = paddle.to_tensor(self.total_loss[idx])
# if not return_micro_batch_loss:
# TODO(shenliang03): it will use mean/sum to calculate loss
tmp = paddle.zeros_like(self.total_loss[idx][0])
for loss in self.total_loss[idx]:
tmp += loss.detach()
losses.append(tmp / self.accumulate_steps)
# else:
# losses.append(self.total_loss[idx].detach())
res = losses[0] if len(losses) == 1 else losses
else:
res = output_list
return res
def _offload_tensors(self, output_tensor):
if isinstance(output_tensor, (tuple, list)):
for t in output_tensor:
if not isinstance(t, paddle.Tensor):
continue
host_tensor = (
t.pin_memory() if hasattr(t, "pin_memory") else t.cpu()
)
host_tensor._share_buffer_to(t)
else:
if not isinstance(output_tensor, paddle.Tensor):
return
host_tensor = (
output_tensor.pin_memory()
if hasattr(output_tensor, "pin_memory")
else output_tensor.cpu()
)
host_tensor._share_buffer_to(output_tensor)
class PipelineParallel(MetaParallelBase):
def __init__(self, layers, hcg, strategy):
if not isinstance(layers, PipelineLayer):
raise TypeError(
"The Layer should be a derived class of PipelineLayer."
)
super().__init__(layers, hcg, strategy)
self.use_data_parallel = self._hcg.get_data_parallel_world_size() > 1
self.use_model_parallel = self._hcg.get_model_parallel_world_size() > 1
self.use_sep_parallel = self._hcg.get_sep_parallel_world_size() > 1
self.use_sharding_parallel = (
self._hcg.get_sharding_parallel_world_size() > 1
)
self.use_moe_sharding_parallel = (
self._hcg.get_moe_sharding_parallel_world_size() > 1
)
self.use_dict_in_pp = True
self.total_loss = None
self.micro_batch_size = self._strategy.pipeline_configs[
'micro_batch_size'
]
self.accumulate_steps = self._strategy.pipeline_configs[
'accumulate_steps'
]
# If sent tensor are not the same from different hosts,
# they shouldn't been sent partially and then concatenated as a whole tensor.
self._enable_partial_send_recv = self._strategy.pipeline_configs[
'enable_partial_send_recv'
]
self._using_cache = self._strategy.pipeline_configs['p2p_cache_shape']
self.num_stages = self._hcg.get_pipe_parallel_world_size()
self.stage_id = self._hcg.get_stage_id()
self.global_rank = self._hcg.get_global_rank()
self.pp_group = self._hcg.get_pipe_parallel_group()
self.dp_group = self._hcg.get_data_parallel_group()
# fused sep and dp
if self.use_sep_parallel:
self.dp_group = self._hcg.get_dp_sep_parallel_group()
self.sharding_group = self._hcg.get_sharding_parallel_group()
self._virtual_pp_world_size = None
self._virtual_pp_rank = None
self._real_pp_world_size = self.num_stages
self._real_pp_rank = self.stage_id
# TODO(PP Dev): support dp_comm_overlap without use_main_grad training.
# This combination will trigger inplace check error during `reshape_` in function `_split_tensors`.
self._dp_comm_overlap = self._strategy.hybrid_configs[
"pp_configs"
].dp_comm_overlap
self._sharding_comm_overlap = self._strategy.hybrid_configs[
"pp_configs"
].sharding_comm_overlap
self._enable_timer = self._strategy.hybrid_configs[
"pp_configs"
].enable_timer
self._release_gradients = self._strategy.hybrid_configs[
"pp_configs"
].release_gradients
self._sharding_split_param = self._strategy.hybrid_configs[
"sharding_configs"
].split_param
self._overlap_p2p_comm = self._strategy.hybrid_configs[
"pp_configs"
].overlap_p2p_comm
self._clear_every_step_cache = self._strategy.hybrid_configs[
"pp_configs"
].clear_every_step_cache
self._use_batch_p2p_comm = self._strategy.hybrid_configs[
"pp_configs"
].use_batch_p2p_comm
self._dynamic_shape = self._strategy.hybrid_configs[
'pp_configs'
].enable_dynamic_shape
logger.info(
f"Pipeline scheduler is in dynamic_shape mode={self._dynamic_shape}"
)
if self._use_batch_p2p_comm and self._overlap_p2p_comm:
warnings.warn(
"non_batch_p2p_comm should be enabled when overlap_p2p_comm is activated, setting non_batch_p2p_comm=True."
)
self._use_batch_p2p_comm = False
logger.info(
f"dp_comm_overlap {self._dp_comm_overlap}; \
sharding_comm_overlap {self._sharding_comm_overlap}; \
sharding_split_param {self._sharding_split_param};"
)
self._profiling = self._strategy.hybrid_configs["pp_configs"].profiling
self._records = []
self._record_format = (
'"name": "{}{}", "cat": "pipeline timeline", "ph": {}, "pid": 0, "tid": '
+ str(self.stage_id + 1)
+ ', "ts": {}, "cname": "{}"'
)
self._forward_color = "thread_state_running" # RGB: 126, 200, 148
self._backward_color = "rail_idle" # RGB: 238, 142, 0
if self._profiling:
logger.info(
"If enable pp profiling, the max training steps should be restricted "
"to a reasonable value (such as 5) to avoid generating large profile files. "
"The profiler will generate a profile file 'profile_record_tmp_file_for_rank_*' "
"for each rank. Users should gather all profile files for one entire pipeline "
"to one node (rank 0 is recommended) to get the full view of the pipeline profile. "
"[DONT CHANGE THE NAME OF THE PROFILE FILES!]. "
"Then get the profile parser from this url: "
"https://github.com/PaddlePaddle/Paddle/blob/develop/python/paddle/distributed/fleet/meta_parallel/pp_utils/profiler_helper.py "
"and save the script to the same directory of all profile files."
"Parse those files by this command: `python profiler_helper.py`. "
"After parsing, a new file 'pipeline_profile.json' will be generated. "
"Users can inspect this file by chrome://tracing website."
)
if self._dp_comm_overlap:
assert self.use_data_parallel and self.num_stages > 1
if self._sharding_comm_overlap:
assert self.use_sharding_parallel and self.num_stages > 1
assert not (self._dp_comm_overlap and self._sharding_comm_overlap), (
"Cannot use dp pp overlap and sharding pp overlap at the same time."
)
self._chunk_2_comm_buffers = defaultdict(list)
self._comm_overlap = (
self._dp_comm_overlap or self._sharding_comm_overlap
)
if self._enable_timer:
if not timer.is_timer_initialized():
timer.set_timers()
self.timers = timer.get_timers()
p2p.initialize_p2p_groups(
hcg,
self._enable_partial_send_recv,
self._enable_timer,
)
# construct pipeline meta info
self._p2p_helper = p2p.P2pHelper(
self._using_cache, dynamic_shape=self._dynamic_shape
)
self.global_rank = self._hcg.get_global_rank()
self.micro_batch_id = 0
# default loss function index
self.loss_fn_idx = 0
self._compute_loss = True
self._return_host_tensor = False
self.callbacks = pipeline_parallel_callbacks_
logger.info(
f"Pipeline Info -- num_stages: {self.num_stages}, stage_id: {self.stage_id}"
)
if self.use_model_parallel:
logger.info("start broadcast mp parameters")
broadcast_mp_parameters(self._layers, self._hcg)
if self.use_sep_parallel:
logger.info("start broadcast sep parameters")
broadcast_sep_parameters(self._layers, self._hcg)
if self.use_sharding_parallel:
logger.info("start broadcast sharding parameters")
broadcast_sharding_parameters(self._layers, self._hcg)
if self.use_data_parallel:
logger.info("start broadcast dp parameters")
broadcast_dp_parameters(self._layers, self._hcg)
if self.use_moe_sharding_parallel:
logger.info("start broadcast moe_sharding parameters")
broadcast_moe_sharding_parameters(self._layers, self._hcg)
if self._dp_comm_overlap:
self.register_allreduce_overlap_hook(
self._layers, self.dp_group, self.accumulate_steps, True
)
self.processed_steps = 0
self._init_user_hooks()
# only support user hooks during training
self.user_hooks_enabled = True
def register_hook(
self, location: PipelineParallelMicroStepLocations, hook: Callable
):
self.callbacks.register_hook(location, hook)
def _init_user_hooks(self):
self._init_user_forward_backward_hooks()
self._init_user_bubble_hooks()
def _init_user_forward_backward_hooks(self):
# initialize forward hooks
self.forward_hooks = PipelineHook()
self.forward_hooks.set_hooks_capacity(
(
self._virtual_pp_world_size
if self._virtual_pp_world_size is not None
else 1
)
* self.accumulate_steps
)
# initialize backward hooks
self.backward_hooks = PipelineHook()
self.backward_hooks.set_hooks_capacity(
(
self._virtual_pp_world_size
if self._virtual_pp_world_size is not None
else 1
)
* self.accumulate_steps
)
def _init_user_bubble_hooks(self):
# (TODO:gexiao) support bubble hooks if needed
self.bubble_hooks = None
# self.bubble_hooks = PipelineHook()
# self.bubble_hooks.set_hooks_capacity(2 * self.num_stages - 2)
def _reset_user_hooks_status(self):
if self.bubble_hooks:
self.bubble_hooks.reset_current_id()
if self.forward_hooks:
self.forward_hooks.reset_current_id()
if self.backward_hooks:
self.backward_hooks.reset_current_id()
def _check_user_hooks_status_at_step_end(self):
if not self.user_hooks_enabled:
return
expected_bubble_step = 2 * self.num_stages - 2
expected_forward_step = (
self._virtual_pp_world_size
if self._virtual_pp_world_size is not None
else 1
) * self.accumulate_steps
expected_backward_step = (
self._virtual_pp_world_size
if self._virtual_pp_world_size is not None
else 1
) * self.accumulate_steps
if self.bubble_hooks:
assert (self.bubble_hooks.current_id) == expected_bubble_step, (
f"bubble hooks status is not correct, current id is {self.bubble_hooks.current_id}, expected id is {expected_bubble_step}"
)
if self.forward_hooks:
assert (self.forward_hooks.current_id) == expected_forward_step, (
f"forward hooks status is not correct, current id is {self.forward_hooks.current_id}, expected id is {expected_forward_step}"
)
if self.backward_hooks:
assert (self.backward_hooks.current_id) == expected_backward_step, (
f"backward hooks status is not correct, current id is {self.backward_hooks.current_id}, expected id is {expected_backward_step}"
)
def register_bubble_pipeline_parallel_hook(
self, location: int, hook: Callable
):
"""
Registering bubble hooks for pipeline parallelism.
"""
if not self.bubble_hooks:
raise ValueError("Bubble hooks are not supported yet.")
self.bubble_hooks.register_hook(location, hook)
def register_forward_pipeline_parallel_hook(
self, location: int, hook: Callable
):
"""
Registering forward hooks for pipeline parallelism.
"""
if not self.forward_hooks:
raise ValueError("Forward hooks are not supported yet.")
self.forward_hooks.register_hook(location, hook)
def register_backward_pipeline_parallel_hook(
self, location: int, hook: Callable
):
"""
Registering backward hooks for pipeline parallelism.
"""
if not self.backward_hooks:
raise ValueError("Backward hooks are not supported yet.")
self.backward_hooks.register_hook(location, hook)
@property
def bubble_pipeline_parallel_hook_capacity(self):
capacity = 0
if self.bubble_hooks:
capacity = self.bubble_hooks.hooks_capacity
return capacity
@property
def forward_pipeline_parallel_hook_capacity(self):
capacity = 0
if self.forward_hooks:
capacity = self.forward_hooks.hooks_capacity
return capacity
@property
def backward_pipeline_parallel_hook_capacity(self):
capacity = 0
if self.backward_hooks:
capacity = self.backward_hooks.hooks_capacity
return capacity
def is_pipeline_first_stage(self, ignore_virtual=False):
if not ignore_virtual:
if self._virtual_pp_world_size is not None:
assert self._virtual_pp_rank is not None
if self._virtual_pp_rank != 0:
return False
assert self._real_pp_rank is not None
return self._real_pp_rank == 0
def is_pipeline_last_stage(self, ignore_virtual=False):
if not ignore_virtual:
if self._virtual_pp_world_size is not None:
assert self._virtual_pp_rank is not None
if self._virtual_pp_rank != (self._virtual_pp_world_size - 1):
return False
assert self._real_pp_rank is not None
assert self._real_pp_world_size is not None
return self._real_pp_rank == (self._real_pp_world_size - 1)
def set_virtual_pipeline_rank(self, rank):
self._virtual_pp_rank = rank
def fused_gradient(
self, model, comm_group, acc_steps, dp, group_size=128 * 1024 * 1024
):
if model.get_num_virtual_stages() > 1:
models = model.get_model_chunks()
else:
models = [model]
act = get_action(dp, self._sharding_split_param)
if act == HOOK_ACTION.REDUCE:
assert hasattr(self, "optimizer")
assert hasattr(self.optimizer, "_param2rank")
_param2rank = self.optimizer._param2rank
for chunk_idx, model in enumerate(models):
# For virtual pipeline. Will separate parameters in different chunk into
# different groups to get the best performance.
fused_parameter_group = {}
parameter_list = [
p for p in model.parameters() if not p.stop_gradient
]
if len(parameter_list) < 1:
return
if act == HOOK_ACTION.REDUCE:
# Sort parameters for sharding, since they have different dst rank
for p in parameter_list:
assert p.name in _param2rank
dst_rank = _param2rank[p.name]
if dst_rank in fused_parameter_group:
fused_parameter_group[dst_rank].append(p)
else:
fused_parameter_group[dst_rank] = [p]
else:
fused_parameter_group[-1] = parameter_list
for dst in fused_parameter_group:
parameter_list = fused_parameter_group[dst]
if act == HOOK_ACTION.REDUCE:
# parse the relative dst rank to absolute dst rank for sharding
dst = comm_group.ranks[dst]
var_groups = assign_group_by_size(parameter_list, group_size)
for group_idx, parameters in var_groups.items():
buffer = FusedCommBuffer(
group_idx,
parameters,
comm_group,
acc_steps,
act,
dst,
release_grads=self._release_gradients,
)
self._chunk_2_comm_buffers[chunk_idx].append(buffer)
return self._chunk_2_comm_buffers
def bw_hook_func(self, buffer, param):
@paddle.autograd.no_grad()
def fused_allreduce(*_):
buffer.add_grad(param)
return fused_allreduce
def register_allreduce_overlap_hook(
self, model, comm_group, acc_steps, dp, group_size=128 * 1024 * 1024
):
# register hook
self.fused_gradient(model, comm_group, acc_steps, dp, group_size)
for _, buffers in self._chunk_2_comm_buffers.items():
for buffer in buffers:
for param in buffer._params:
param._register_backward_hook(
self.bw_hook_func(buffer, param)
)
def timer_printer(self):
if not self._enable_timer:
return
all_flag_names = self.timers.timers.keys()
self.timers.log(all_flag_names)
def _record_stamp(self, name, step, phase, color):
if self._profiling:
paddle.device.synchronize()
self._records.append(
'{'
+ self._record_format.format(
name,
step,
phase,
int(time.time() * 1000),
color,
)
+ '}'
)
def _flush_records(self):
if self._profiling:
with open(
f'./profile_record_tmp_file_for_rank_{self.global_rank}',
'a+',
) as f:
f.writelines(record + '\n' for record in self._records)
self._records = []
def forward_backward_pipeline(
self,
data,
scaler=None,
static_scheduler=False,
return_micro_batch_loss=False,
):
# use the 1f1b scheduling strategy.
# this strategy is inspired by:
# https://github.com/NVIDIA/Megatron-LM/blob/main/megatron/schedules.py
self._reset_user_hooks_status()
# no _forward_only mode
self.user_hooks_enabled = True
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
"[Pipeline details] Start_forward_backward_pipeline"
)
if static_scheduler:
assert not self._profiling, (
"While _profiling, static scheduler is not available"
)
if data is not None:
warnings.warn(
"Static scheduler run won't real run the model, but data has been provided"
)
logger.info(
"enable static_scheduler will return the pp schedule instead of the loss"
)
schedule = ""
self.scaler = scaler
# store total loss of entire batch
self.total_loss = None
# store data id for micro_batch
self.micro_batch_id = 0
startup_steps = self.num_stages - self.stage_id - 1
startup_steps = min(startup_steps, self.accumulate_steps)
steady_steps = self.accumulate_steps - startup_steps
input_buffers = []
output_buffers = []
micro_dataset = self._wrap_data(data)
for step_id in range(startup_steps):
if static_scheduler:
schedule += f"f{step_id};"
logger.info(f"forward step for micro step {step_id}")
continue
input_tensor = self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(),
batch_p2p_comm=self._use_batch_p2p_comm,
)
input_tensor_dict, use_dict = tuple_to_dict_helper(input_tensor)
self._record_stamp("F", step_id, '"B"', self._forward_color)
output_tensor, _, _ = self._forward_step(
input_tensor=input_tensor_dict if use_dict else input_tensor,
micro_dataset=micro_dataset,
step_id=step_id,
)
# convert dict to tuple whose tensor element has a key attribution
output_tensor_tuple = dict_to_tuple_helper(output_tensor)
self._record_stamp("F", step_id, '"E"', self._forward_color)
# fwd output dict -> send tuple
self._p2p_helper.send_forward(
output_tensor=output_tensor_tuple,
pp_last_stage=self.is_pipeline_last_stage(),
batch_p2p_comm=self._use_batch_p2p_comm,
)
input_buffers.append(input_tensor)
output_buffers.append(output_tensor_tuple)
if not self.is_pipeline_last_stage():
_release_output(output_tensor_tuple)
if steady_steps > 0 and not static_scheduler:
input_tensor = self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(),
batch_p2p_comm=self._use_batch_p2p_comm,
)
for i in range(steady_steps):
if static_scheduler:
schedule += f"f{startup_steps + i};"
schedule += f"b{i};"
logger.info(f"forward step for micro step {startup_steps + i}")
logger.info(f"backward step for micro step {i}")
continue
last_iter = i == (steady_steps - 1)
input_tensor_dict, use_dict = tuple_to_dict_helper(input_tensor)
self._record_stamp(
"F", startup_steps + i, '"B"', self._forward_color
)
output_tensor, _, _ = self._forward_step(
input_tensor=input_tensor_dict if use_dict else input_tensor,
micro_dataset=micro_dataset,
step_id=startup_steps + i,
)
self._record_stamp(
"F", startup_steps + i, '"E"', self._forward_color
)
output_tensor_tuple = dict_to_tuple_helper(output_tensor)
# NOTE: `send_forward_recv_backward` is intentionally unused to
# prevent hanging bugs in dynamic shape mode.
self._p2p_helper.send_forward(
output_tensor_tuple,
self.is_pipeline_last_stage(ignore_virtual=True),
batch_p2p_comm=self._use_batch_p2p_comm,
)
output_tensor_grad = self._p2p_helper.recv_backward(
self.is_pipeline_last_stage(ignore_virtual=True),
batch_p2p_comm=self._use_batch_p2p_comm,
)
input_buffers.append(input_tensor)
output_buffers.append(output_tensor_tuple)
if not self.is_pipeline_last_stage():
_release_output(output_tensor_tuple)
input_tensor, output_tensor = (
input_buffers.pop(0),
output_buffers.pop(0),
)
self._record_stamp("B", i, '"B"', self._backward_color)
input_tensor_grad = self._backward_step(
input_tensor, output_tensor, output_tensor_grad, step_id=i
)
self._record_stamp("B", i, '"E"', self._backward_color)
if last_iter:
input_tensor = None
self._p2p_helper.send_backward(
input_tensor_grad,
self.is_pipeline_first_stage(),
batch_p2p_comm=self._use_batch_p2p_comm,
)
else:
# NOTE: `send_backward_recv_forward` is intentionally unused to
# prevent hanging bugs in dynamic shape mode.
input_tensor = self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(ignore_virtual=True),
batch_p2p_comm=self._use_batch_p2p_comm,
)
self._p2p_helper.send_backward(
input_tensor_grad,
self.is_pipeline_first_stage(ignore_virtual=True),
batch_p2p_comm=self._use_batch_p2p_comm,
)
for i in range(startup_steps):
if static_scheduler:
schedule += f"b{steady_steps + i};"
logger.info(f"backward step for micro step {steady_steps + i}")
continue
input_tensor = input_buffers.pop(0)
output_tensor = output_buffers.pop(0)
output_tensor_grad = self._p2p_helper.recv_backward(
self.is_pipeline_last_stage(),
batch_p2p_comm=self._use_batch_p2p_comm,
)
self._record_stamp(
"B", steady_steps + i, '"B"', self._backward_color
)
input_tensor_grad = self._backward_step(
input_tensor,
output_tensor,
output_tensor_grad,
step_id=steady_steps + i,
)
self._record_stamp(
"B", steady_steps + i, '"E"', self._backward_color
)
self._p2p_helper.send_backward(
input_tensor_grad,
self.is_pipeline_first_stage(),
batch_p2p_comm=self._use_batch_p2p_comm,
)
if static_scheduler:
return schedule
self._flush_records()
if self._comm_overlap:
assert len(self._chunk_2_comm_buffers) > 0, (
"comm buffers should be created"
)
for _, buffers in self._chunk_2_comm_buffers.items():
for buffer in buffers:
buffer.scale_grads()
if self._enable_timer:
self.timers("allreduce_shared_weight_gradients").start()
self._layers.allreduce_shared_weight_gradients()
if self._enable_timer:
self.timers("allreduce_shared_weight_gradients").stop()
self.timers("broadcast_final_loss").start()
with paddle.amp.auto_cast(enable=False):
train_loss = self._broadcast_final_loss(return_micro_batch_loss)
if self._enable_timer:
self.timers("broadcast_final_loss").stop()
if self._clear_every_step_cache:
self._p2p_helper.clear_meta_cache()
self.timer_printer()
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
"[Pipeline details] End_forward_backward_pipeline"
)
self.processed_steps += 1
self._check_user_hooks_status_at_step_end()
return train_loss
def register_sharding_comm_overlap_hook(self, optimizer):
"""for delayed hook register until we get optimizer"""
assert isinstance(optimizer, HybridParallelOptimizer), (
'optimizer should be HybridParallelOptimizer subclass.'
)
self.optimizer = optimizer
if self._sharding_comm_overlap and len(self._chunk_2_comm_buffers) == 0:
self.register_allreduce_overlap_hook(
self._layers, self.sharding_group, self.accumulate_steps, False
)
def _prepare_training(self, data, optimizer, lr_scheduler):
# reset the virtual pp rank for each run
self.set_virtual_pipeline_rank(0)
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(
ignore_virtual=True
) or self.is_pipeline_last_stage(ignore_virtual=True):
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 _wrap_data(self, data):
"""
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(ignore_virtual=True),
self.is_pipeline_last_stage(ignore_virtual=True),
self.accumulate_steps,
self.micro_batch_size,
)
return micro_dataset
def train_batch(
self,
data,
optimizer,
lr_scheduler=None,
scaler=None,
loss_fn_idx=0,
return_micro_batch_loss=False,
):
data = self._prepare_training(data, optimizer, lr_scheduler)
# check loss_fn_idx is valid and loss_fn exists
assert (
loss_fn_idx in range(len(self._layers._loss_fn))
and self._layers._loss_fn[loss_fn_idx] is not None
), f"loss function {loss_fn_idx} should exist to compute loss"
self.loss_fn_idx = loss_fn_idx
# 1f1b scheduler for pipeline parallel
train_loss = self.forward_backward_pipeline(
data, scaler, return_micro_batch_loss=return_micro_batch_loss
)
# optimizer
with paddle.amp.auto_cast(enable=False):
self._optimizer_step()
return train_loss
def eval_batch(
self, data, compute_loss=False, loss_fn_idx=0, return_host_tensor=False
):
self.user_hooks_enabled = False
# reset the virtual pp rank for each run
self.set_virtual_pipeline_rank(0)
self._layers.eval()
origin_compute_loss = self._compute_loss
self._compute_loss = compute_loss
origin_return_host_tensor = self._return_host_tensor
self._return_host_tensor = return_host_tensor
# store data id for micro_batch
self.micro_batch_id = 0
# store total loss of entire batch
self.total_loss = None
# check loss_fn_idx is valid and loss_fn exists
assert (
loss_fn_idx in range(len(self._layers._loss_fn))
and self._layers._loss_fn[loss_fn_idx] is not None
), f"loss function {loss_fn_idx} should exist to compute loss"
self.loss_fn_idx = loss_fn_idx
startup_steps = self.num_stages - self.stage_id - 1
startup_steps = min(startup_steps, self.accumulate_steps)
steady_steps = self.accumulate_steps - startup_steps
output_buffers = []
# convert to micro dataset
micro_dataset = self._wrap_data(data)
for step_id in range(startup_steps):
input_tensor = self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(),
batch_p2p_comm=self._use_batch_p2p_comm,
)
# p2p data type: tuple
# model input/return type: dict
# here, convert p2p tuple -> dict input
input_tensor_dict, use_dict = tuple_to_dict_helper(input_tensor)
output_tensor, _, _ = self._forward_step(
input_tensor_dict if use_dict else input_tensor,
micro_dataset,
step_id=None,
)
# convert dict to tuple whose tensor element has a key attribution
output_tensor_tuple = dict_to_tuple_helper(output_tensor)
self._p2p_helper.send_forward(
output_tensor_tuple,
self.is_pipeline_last_stage(),
skip_check_meta=True,
batch_p2p_comm=self._use_batch_p2p_comm,
)
if not self.is_pipeline_last_stage():
_release_output(output_tensor_tuple)
else:
self._offload_tensors(output_tensor_tuple)
output_buffers.append(output_tensor_tuple)
if steady_steps > 0:
input_tensor = self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(),
batch_p2p_comm=self._use_batch_p2p_comm,
)
for i in range(steady_steps):
last_iter = i == (steady_steps - 1)
input_tensor_dict, use_dict = tuple_to_dict_helper(input_tensor)
output_tensor, _, _ = self._forward_step(
input_tensor_dict if use_dict else input_tensor,
micro_dataset,
step_id=None,
)
output_tensor_tuple = dict_to_tuple_helper(output_tensor)
self._p2p_helper.send_forward(
output_tensor_tuple,
self.is_pipeline_last_stage(),
skip_check_meta=True,
batch_p2p_comm=self._use_batch_p2p_comm,
)
if not self.is_pipeline_last_stage():
_release_output(output_tensor_tuple)
else:
self._offload_tensors(output_tensor_tuple)
output_buffers.append(output_tensor_tuple)
if not last_iter:
input_tensor = self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(),
batch_p2p_comm=self._use_batch_p2p_comm,
)
if self._compute_loss:
train_loss = self._broadcast_final_loss()
else:
train_loss = output_buffers
self._compute_loss = origin_compute_loss
self._return_host_tensor = origin_return_host_tensor
return train_loss
def _maybe_loss_compute(
self, output_tensor, micro_dataset, overlap_schedule_mode=False
):
backward_loss_tensor = None
backward_loss_fn_node = None
loss_fn_node = None
if self.is_pipeline_last_stage():
# train calculate loss for train
if self._compute_loss:
assert self._layers._loss_fn[self.loss_fn_idx] is not None, (
"loss function should exist to compute loss"
)
labels = next(micro_dataset)[1]
self._check_micro_batch_data_valid(labels)
for idx, loss_fn in enumerate(self._layers._loss_fn):
if overlap_schedule_mode:
loss_fn_node = loss_fn.build_schedule_node()
loss_fn_node.labels = labels
loss_tensor = loss_fn_node.forward(output_tensor)
else:
loss_tensor = loss_fn(output_tensor, labels)
assert isinstance(loss_tensor, paddle.Tensor), (
"Currently, loss_fn should obtain Paddle.Tensor dtype"
)
if self.total_loss is None:
self.total_loss = []
# when self.total_loss length is less than idx, append a new tensor
if len(self.total_loss) <= idx:
self.total_loss.append([])
self.total_loss[idx].append(loss_tensor.detach())
if idx == self.loss_fn_idx:
backward_loss_tensor = loss_tensor
backward_loss_fn_node = loss_fn_node
return backward_loss_tensor, backward_loss_fn_node
def _forward_step(
self,
input_tensor,
micro_dataset,
chunk_id=None,
step_id=None,
overlap_schedule_mode=False,
):
if self.user_hooks_enabled:
self.forward_hooks.run_hook()
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
f"[Pipeline details] Before_forward_step_chunk_{chunk_id}_step_{step_id}"
)
if self._enable_timer:
self.timers("forward_step").start()
if self.is_pipeline_first_stage():
input_tensor = next(micro_dataset)[0]
self._check_micro_batch_data_valid(input_tensor)
assert chunk_id is None or isinstance(chunk_id, int)
self.callbacks.on_location(
PipelineParallelMicroStepLocations.FORWARD_BEGIN,
input_tensor=input_tensor,
step_id=step_id,
)
schedule_chunk = None
if overlap_schedule_mode:
schedule_chunk = self._layers.get_schedule_chunk(chunk_id=chunk_id)
output_tensor = schedule_chunk.forward(input_tensor)
else:
output_tensor = self._layers.forward(
input_tensor, chunk_id=chunk_id
)
self.callbacks.on_location(
PipelineParallelMicroStepLocations.FORWARD_END,
input_tensor=input_tensor,
output_tensor=output_tensor,
step_id=step_id,
)
backward_loss_tensor, backward_loss_fn_node = self._maybe_loss_compute(
output_tensor, micro_dataset, overlap_schedule_mode
)
if self.is_pipeline_first_stage() or self.is_pipeline_last_stage():
# Only increase micro batch id at virtual first/last pp stage.
# The micro batch id is used to load data, therefore, only increase it when load data.
self.micro_batch_id += 1
_release_input(input_tensor, output_tensor)
if self._enable_timer:
self.timers("forward_step").stop()
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
f"[Pipeline details] After_forward_step_chunk_{chunk_id}_step_{step_id}"
)
if self.is_pipeline_last_stage() and self._compute_loss:
return backward_loss_tensor, schedule_chunk, backward_loss_fn_node
return output_tensor, schedule_chunk, backward_loss_fn_node
def _backward_step(
self,
input_tensor,
output_tensor,
output_tensor_grad,
chunk_id=None,
step_id=None,
overlap_schedule_mode=False,
schedule_chunk=None,
loss_fn_node=None,
):
if self.user_hooks_enabled:
self.backward_hooks.run_hook()
if self._enable_timer:
self.timers("backward_step").start()
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
f"[Pipeline details] Before_backward_step_chunk_{chunk_id}_step_{step_id}"
)
with paddle.amp.auto_cast(enable=False):
self.callbacks.on_location(
PipelineParallelMicroStepLocations.BACKWARD_BEGIN,
input_tensor=input_tensor,
output_tensor=output_tensor,
output_tensor_grad=output_tensor_grad,
step_id=step_id,
)
if self.is_pipeline_last_stage():
assert output_tensor_grad is None
if overlap_schedule_mode:
assert (
loss_fn_node is not None and schedule_chunk is not None
), (
"loss_fn_node and schedule_chunk should not be None in overlap_schedule_mode"
)
input_tensor_grad = loss_fn_node.backward(
scaler=self.scaler
)
input_tensor_grad = schedule_chunk.backward(
input_tensor_grad
)
else:
# In align mode, we scale the grad directly after forward
if paddle.distributed.in_auto_parallel_align_mode():
output_tensor = output_tensor / _get_align_mode_scale()
if self.scaler:
paddle.autograd.backward(
self.scaler.scale(output_tensor)
)
else:
paddle.autograd.backward(output_tensor)
else:
if isinstance(output_tensor, tuple):
outputs = [t for t in output_tensor if not t.stop_gradient]
assert len(outputs) == len(output_tensor_grad)
grad_tensors = list(output_tensor_grad)
else:
outputs = [output_tensor]
grad_tensors = [output_tensor_grad]
if overlap_schedule_mode:
assert schedule_chunk is not None, (
"schedule_chunk should not be None in overlap_schedule_mode"
)
input_tensor_grad = schedule_chunk.backward(grad_tensors)
else:
paddle.autograd.backward(
tensors=outputs,
grad_tensors=grad_tensors,
)
if not overlap_schedule_mode:
# Extract input_tensor_grad from the input tensor. In overlap_schedule_mode,
# the input_tensor_grad is extracted inside the schedule_chunk.
input_tensor_grad = None
if input_tensor is not None:
if isinstance(input_tensor, tuple):
input_tensor_grad = tuple(
[
t.grad
for t in input_tensor
if not t.stop_gradient
]
)
else:
input_tensor_grad = input_tensor.grad
if self._enable_timer:
self.timers("backward_step").stop()
self.callbacks.on_location(
PipelineParallelMicroStepLocations.BACKWARD_END,
input_tensor=input_tensor,
output_tensor=output_tensor,
input_tensor_grad=input_tensor_grad,
output_tensor_grad=output_tensor_grad,
step_id=step_id,
)
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
f"[Pipeline details] After_backward_step_chunk_{chunk_id}_step_{step_id}"
)
return input_tensor_grad
def _check_micro_batch_data_valid(self, micro_batch_data):
if isinstance(micro_batch_data, (tuple, list)):
for data in micro_batch_data:
self._check_micro_batch_data_valid(data)
elif isinstance(micro_batch_data, dict):
for value in micro_batch_data.values():
self._check_micro_batch_data_valid(value)
elif micro_batch_data is not None:
assert isinstance(micro_batch_data, paddle.Tensor)
def _broadcast_final_loss(self, return_micro_batch_loss=False):
# Since the last backward run in interleave will set the virtual rank to 0,
# here we need to check last stage ignoring virtual stage.
if self.is_pipeline_last_stage(ignore_virtual=True):
assert self.total_loss is not None, (
"train_batch() in last stage should obtain valid loss"
)
losses = []
for idx in range(len(self._layers._loss_fn)):
self.total_loss[idx] = paddle.to_tensor(self.total_loss[idx])
if not return_micro_batch_loss:
# TODO(shenliang03): it will use mean/sum to calculate loss
tmp = paddle.zeros_like(self.total_loss[idx][0])
for loss in self.total_loss[idx]:
tmp += loss.detach()
losses.append(tmp / self.accumulate_steps)
else:
losses.append(self.total_loss[idx].detach())
for idx in range(len(self._layers._loss_fn)):
is_fp32 = (
paddle.full([], 1, 'int64')
if losses[idx].dtype == paddle.float32
else paddle.full([], 0, 'int64')
)
paddle.distributed.broadcast(
is_fp32,
src=self.global_rank,
sync_op=True,
group=self.pp_group,
)
paddle.distributed.broadcast(
losses[idx],
src=self.global_rank,
sync_op=True,
group=self.pp_group,
)
else:
losses = []
for idx in range(len(self._layers._loss_fn)):
is_fp32 = paddle.full([], 1, 'int64')
paddle.distributed.broadcast(
is_fp32,
src=self._hcg.get_rank_from_stage(self.num_stages - 1),
sync_op=True,
group=self.pp_group,
)
if return_micro_batch_loss:
loss_shape = [self.accumulate_steps]
else:
loss_shape = [1]
losses.append(
paddle.zeros(shape=loss_shape, dtype="float32")
if is_fp32.item()
else paddle.zeros(shape=loss_shape, dtype="float16")
)
paddle.distributed.broadcast(
losses[idx],
src=self._hcg.get_rank_from_stage(self.num_stages - 1),
sync_op=True,
group=self.pp_group,
)
return losses[0] if len(losses) == 1 else losses
def _optimizer_step(self):
for p in self._layers.parameters():
if hasattr(p, "main_grad") and p.main_grad is not None:
assert p.grad is None
p.main_grad = p.main_grad.scale(1.0 / self.accumulate_steps)
elif p.grad is not None:
p.grad = p.grad.scale(1.0 / self.accumulate_steps)
if self.scaler:
self.scaler.step(self.optimizer)
self.scaler.update()
else:
self.optimizer.step()
if self._release_gradients:
self.optimizer.clear_grad(set_to_zero=False)
for _, buffers in self._chunk_2_comm_buffers.items():
for buffer in buffers:
buffer._clear_grad_storage()
else:
self.optimizer.clear_grad()
if self.lr_scheduler:
self.lr_scheduler.step()
def _offload_tensors(self, output_tensor):
if not self._return_host_tensor:
return
if isinstance(output_tensor, (tuple, list)):
for t in output_tensor:
if not isinstance(t, paddle.Tensor) or isinstance(
t, paddle.base.framework.EagerParamBase
):
continue
host_tensor = (
t.pin_memory() if hasattr(t, "pin_memory") else t.cpu()
)
host_tensor._share_buffer_to(t)
else:
if not isinstance(output_tensor, paddle.Tensor):
return
host_tensor = (
output_tensor.pin_memory()
if hasattr(output_tensor, "pin_memory")
else output_tensor.cpu()
)
host_tensor._share_buffer_to(output_tensor)
def _release_output(self, output):
def can_free(t):
return (
t is not None
and isinstance(t, paddle.Tensor)
and t._is_initialized()
and (t.inplace_version == 0 or getattr(t, "pp_can_free", False))
)
if isinstance(output, (tuple, list)):
for t in output:
if can_free(t):
t._clear_dataptr()
elif can_free(output):
output._clear_dataptr()
def get_static_scheduler(self):
return self.forward_backward_pipeline(data=None, static_scheduler=True)
@dataclass
class P2PAsyncHandle:
# funcs
forward_handle_wait_fn: Callable
forward_async_comm_fn: Callable
backward_handle_wait_fn: Callable
backward_async_comm_fn: Callable
# outputs
next_forward_virtual_pp_rank = None
input_tensor = None
out_fwd_wait_handles = None
next_backward_virtual_pp_rank = None
output_tensor_grad = None
recv_next = None
out_bwd_wait_handles = None
def forward_handle_wait(self):
self.forward_handle_wait_fn()
def forward_async_comm(self, output_tensor):
(
self.next_forward_virtual_pp_rank,
self.input_tensor,
self.out_fwd_wait_handles,
) = self.forward_async_comm_fn(output_tensor=output_tensor)
def backward_handle_wait(self):
self.backward_handle_wait_fn()
def backward_async_comm(self, input_tensor_grad):
(
self.next_backward_virtual_pp_rank,
self.output_tensor_grad,
self.recv_next,
self.out_bwd_wait_handles,
) = self.backward_async_comm_fn(input_tensor_grad=input_tensor_grad)
class PipelineParallelWithInterleave(PipelineParallel):
# pipeline parallel with interleave scheduler
def __init__(self, layers, hcg, strategy):
super().__init__(layers=layers, hcg=hcg, strategy=strategy)
self.overlap_schedule_mode = (
hasattr(type(self._layers), "overlapped_forward_backward")
and self._strategy.hybrid_configs[
"pp_configs"
].forward_backward_overlap_scheduler
)
if self.overlap_schedule_mode:
assert not self._profiling, (
"Profiling is not compatible with overlap_schedule_mode."
)
logger.info(f"Using {self._get_scheduler_name()}")
self._record_format = (
'"name": "{}{}_VP{}", "cat": "virtual pipeline timeline", "ph": {}, "pid": 0, "tid": '
+ str(self.stage_id + 1)
+ ', "ts": {}, "cname": "{}"'
)
self._forward_colors = [
"thread_state_running", # RGB: 126, 200, 148
"thread_state_unknown", # RGB: 199, 155, 125
]
self._backward_colors = [
"rail_load", # RGB: 13, 168, 97
"rail_idle", # RGB: 238, 142, 0
]
# Structures to record the micro step for each layer chunk
self._forward_micro_step_counter = {}
self._backward_micro_step_counter = {}
assert layers.get_num_virtual_stages() > 1
# setup for interleave scheduler
self._check_sanity()
self.num_model_chunks = layers.get_num_virtual_stages()
self.model_chunks = layers.get_model_chunks()
assert self.model_chunks is not None
assert len(self.model_chunks) == self.num_model_chunks
self._virtual_pp_world_size = self.num_model_chunks
self._virtual_pp_rank = 0
self._reset_counter()
self._best_unbalanced_scheduler = self._strategy.hybrid_configs[
"pp_configs"
].best_unbalanced_scheduler
if self._best_unbalanced_scheduler:
assert not self._comm_overlap, (
"pp best unbalaced scheduler can not run together with dp/sharding overlap"
)
self._enable_offload_queue = self._strategy.hybrid_configs[
"pp_configs"
].enable_offload_queue
# reinit user hook since now we have virtual stages
self._init_user_hooks()
def _get_scheduler_name(self):
return f"PipelineParallelWithInterleave with overlapping forward backward={self.overlap_schedule_mode}, overlap p2p comm={self._overlap_p2p_comm}"
def _init_user_bubble_hooks(self):
# initialize bubble hooks
self.bubble_hooks = PipelineHook()
self.bubble_hooks.set_hooks_capacity(2 * self.num_stages - 2)
def _check_sanity(self):
assert framework.in_dynamic_mode(), (
"virtual pipeline stage with interleave only support eager dygraph mode"
)
assert self.num_stages > 2, (
"virtual pipeline must run under pp degree > 2"
)
assert self.accumulate_steps >= 2 * self.num_stages, (
f"accumulate_steps({self.accumulate_steps}) should be greater than or equal to 2 * num_stages({self.num_stages}) for pipeline with interleave"
)
def _reset_counter(self):
for i in range(self.num_model_chunks):
self._forward_micro_step_counter[i] = 0
self._backward_micro_step_counter[i] = 0
def _record_stamp(self, name, step, phase, forward=True):
if self._profiling:
paddle.device.synchronize()
virtual_pp_rank = self._get_virtual_pp_rank(step, forward=forward)
color_idx = virtual_pp_rank % 2
# Get the profile color and micro step for current layer chunk
if forward:
color = self._forward_colors[color_idx]
micro_step = self._forward_micro_step_counter[virtual_pp_rank]
if phase == '"E"':
self._forward_micro_step_counter[virtual_pp_rank] += 1
else:
color = self._backward_colors[color_idx]
micro_step = self._backward_micro_step_counter[virtual_pp_rank]
if phase == '"E"':
self._backward_micro_step_counter[virtual_pp_rank] += 1
self._records.append(
'{'
+ self._record_format.format(
name,
micro_step,
virtual_pp_rank,
phase,
int(time.time() * 1000),
color,
)
+ '}'
)
def _flush_records(self):
if self._profiling:
with open(
f'./profile_record_tmp_file_for_rank_{self.global_rank}',
'a+',
) as f:
f.writelines(record + '\n' for record in self._records)
self._records = []
self._reset_counter()
def _get_virtual_pp_rank(self, micro_step, forward):
first_chunk_acc = (
self.accumulate_steps % self.num_stages + self.num_stages
)
first_chunk_steps = first_chunk_acc * self.num_model_chunks
if self._best_unbalanced_scheduler:
num_group_last_chunk_forward = (
(micro_step - first_chunk_acc) // self.num_stages
) // self.num_model_chunks
misplace_start = (
first_chunk_acc
+ self.num_model_chunks
* self.num_stages
* num_group_last_chunk_forward
)
misplace_end = (
self.accumulate_steps % self.num_stages
+ num_group_last_chunk_forward * self.num_stages
) * self.num_model_chunks + self.num_stages
forward_virtual_pp_stage = (
(micro_step - first_chunk_acc) // self.num_stages
) % self.num_model_chunks
if micro_step < first_chunk_steps:
virtual_pp_stage = micro_step // first_chunk_acc
if not forward and self._best_unbalanced_scheduler:
if (
micro_step
>= first_chunk_acc
+ (self.num_model_chunks - 1) * self.num_stages
):
if forward_virtual_pp_stage == self.num_model_chunks - 1:
virtual_pp_stage = 0
elif (
micro_step >= misplace_start
and micro_step < misplace_end
):
virtual_pp_stage = (
micro_step - self.num_stages
) // first_chunk_acc
else:
origin_micro_step = micro_step
micro_step -= first_chunk_steps
virtual_pp_stage = micro_step % (
self.num_stages * self.num_model_chunks
)
virtual_pp_stage = virtual_pp_stage // self.num_stages
if not forward and self._best_unbalanced_scheduler:
total_num_forward_step_from_steady = (
first_chunk_acc
+ (self.accumulate_steps - first_chunk_acc)
* self.num_model_chunks
)
if (
origin_micro_step <= total_num_forward_step_from_steady
and forward_virtual_pp_stage == self.num_model_chunks - 1
):
virtual_pp_stage = 0
elif (
misplace_start <= total_num_forward_step_from_steady
and origin_micro_step >= misplace_start
and origin_micro_step < misplace_end
):
if origin_micro_step < first_chunk_steps + self.num_stages:
virtual_pp_stage = (
origin_micro_step - self.num_stages
) // first_chunk_acc
else:
virtual_pp_stage = (micro_step - self.num_stages) % (
self.num_stages * self.num_model_chunks
)
virtual_pp_stage = virtual_pp_stage // self.num_stages
if not forward:
virtual_pp_stage = self.num_model_chunks - virtual_pp_stage - 1
return virtual_pp_stage
def _get_forward_input(self, virtual_pp_rank):
# some checkers
assert hasattr(self, 'input_tensors')
assert hasattr(self, 'output_tensors')
if not self._forward_only:
assert hasattr(self, 'output_tensor_grads')
assert len(self.input_tensors[virtual_pp_rank]) == (
len(self.output_tensors[virtual_pp_rank]) + 1
)
input_tensor = self.input_tensors[virtual_pp_rank][-1]
else:
input_tensor = self.input_tensors[virtual_pp_rank].pop()
return input_tensor
def _store_forward_outputs(
self,
virtual_pp_rank,
output_tensor,
schedule_chunk=None,
loss_fn_node=None,
):
self.output_tensors[virtual_pp_rank].append(output_tensor)
# If overlap_schedule_mode eq False, the schedule chunk is a None
self.schedule_chunks[virtual_pp_rank].append(schedule_chunk)
if self.is_pipeline_last_stage():
self.loss_fn_chunks.append(loss_fn_node)
if self._forward_only:
# no need to store tensor for backward
if self._compute_loss:
self.output_tensors[virtual_pp_rank].pop()
# save output_tensors for return value of eval batch
else:
self._offload_tensors(output_tensor)
else:
# no need to store tensor for backward
if self._forward_only:
self.output_tensors[virtual_pp_rank].pop()
def _forward_step_helper(
self,
micro_dataset,
micro_step,
overlap_schedule_mode=False,
check_is_last_chunk=False,
):
virtual_pp_rank = self._get_virtual_pp_rank(micro_step, forward=True)
if check_is_last_chunk and virtual_pp_rank == self.num_model_chunks - 1:
os.environ["FLAGS_last_vpp_chunk_forward"] = "1"
self.set_virtual_pipeline_rank(virtual_pp_rank)
input_tensor = self._get_forward_input(virtual_pp_rank)
input_tensor_dict, use_dict = tuple_to_dict_helper(input_tensor)
output_tensor, schedule_chunk, loss_fn_node = self._forward_step(
input_tensor_dict if use_dict else input_tensor,
micro_dataset,
virtual_pp_rank, # chunk_id
step_id=micro_step,
overlap_schedule_mode=overlap_schedule_mode,
)
output_tensor_tuple = dict_to_tuple_helper(output_tensor)
self._store_forward_outputs(
virtual_pp_rank, output_tensor_tuple, schedule_chunk, loss_fn_node
)
return output_tensor_tuple
def _overlap_comm_grads(self):
if self._comm_overlap:
self._backward_step_count += 1
sync_step = self._backward_step_count - self.stage_id
if sync_step > 0 and sync_step % self.num_stages == 0:
chunk_idx = self._virtual_pp_world_size - (
sync_step // self.num_stages
)
for buffer in self._chunk_2_comm_buffers[chunk_idx]:
buffer.comm_grads()
if self.stage_id != 0:
if (
self._backward_step_count
== self.num_stages * self.num_model_chunks
):
for buffer in self._chunk_2_comm_buffers[0]:
buffer.comm_grads()
def _sync_overlap_grads(self):
if self._comm_overlap:
assert (
self._backward_step_count
== self.num_stages * self.num_model_chunks
), (
"backward step count should be equal to accumulate steps * virtual pp world size,"
f" but get {self._backward_step_count}, excepted result is {self.num_stages * self.num_model_chunks}"
)
for _, buffers in self._chunk_2_comm_buffers.items():
for buffer in buffers:
buffer.scale_grads()
def _get_backward_input(self, virtual_pp_rank):
# some checkers
assert hasattr(self, 'input_tensors')
assert hasattr(self, 'output_tensors')
assert hasattr(self, 'output_tensor_grads')
assert len(self.output_tensor_grads[virtual_pp_rank]) > 0, (
f"output_tensor_grads is empty for virtual_pp_rank {virtual_pp_rank}"
)
assert len(self.input_tensors[virtual_pp_rank]) > 0
assert len(self.output_tensors[virtual_pp_rank]) > 0
input_tensor = self.input_tensors[virtual_pp_rank].pop(0)
output_tensor = self.output_tensors[virtual_pp_rank].pop(0)
output_tensor_grad = self.output_tensor_grads[virtual_pp_rank].pop(0)
schedule_chunk = self.schedule_chunks[virtual_pp_rank].pop(0)
if self.is_pipeline_last_stage():
loss_fn_node = self.loss_fn_chunks.pop(0)
else:
loss_fn_node = None
return (
input_tensor,
output_tensor,
output_tensor_grad,
schedule_chunk,
loss_fn_node,
)
def _backward_step_helper(self, micro_step, overlap_schedule_mode=False):
virtual_pp_rank = self._get_virtual_pp_rank(micro_step, forward=False)
self.set_virtual_pipeline_rank(virtual_pp_rank)
(
input_tensor,
output_tensor,
output_tensor_grad,
schedule_chunk,
loss_fn_node,
) = self._get_backward_input(virtual_pp_rank)
input_tensor_grad = self._backward_step(
input_tensor,
output_tensor,
output_tensor_grad,
chunk_id=virtual_pp_rank,
step_id=micro_step,
overlap_schedule_mode=overlap_schedule_mode,
schedule_chunk=schedule_chunk,
loss_fn_node=loss_fn_node,
)
self._overlap_comm_grads()
return input_tensor_grad
def _forward_backward_helper(
self,
micro_dataset,
forward_micro_step_id,
backward_micro_step_id,
p2p_async_handle=None,
):
if not self.overlap_schedule_mode:
if p2p_async_handle is not None:
p2p_async_handle.forward_handle_wait()
self._record_stamp("F", forward_micro_step_id, '"B"', forward=True)
output_tensor = self._forward_step_helper(
micro_dataset,
forward_micro_step_id,
)
self._record_stamp("F", forward_micro_step_id, '"E"', forward=True)
if p2p_async_handle is not None:
p2p_async_handle.forward_async_comm(output_tensor)
p2p_async_handle.backward_handle_wait()
# backward
self._record_stamp(
"B", backward_micro_step_id, '"B"', forward=False
)
input_tensor_grad = self._backward_step_helper(
backward_micro_step_id,
)
self._record_stamp(
"B", backward_micro_step_id, '"E"', forward=False
)
if p2p_async_handle is not None:
p2p_async_handle.backward_async_comm(input_tensor_grad)
return
else:
return output_tensor, input_tensor_grad
else:
# 1. prepare forward inputs
forward_virtual_pp_rank = self._get_virtual_pp_rank(
forward_micro_step_id, forward=True
)
self.set_virtual_pipeline_rank(forward_virtual_pp_rank)
if self.user_hooks_enabled:
self.forward_hooks.run_hook()
forward_inputs = self._get_forward_input(forward_virtual_pp_rank)
input_tensor_dict, use_dict = tuple_to_dict_helper(forward_inputs)
if self.is_pipeline_first_stage():
forward_inputs = next(micro_dataset)[0]
self._check_micro_batch_data_valid(forward_inputs)
if self.is_pipeline_last_stage():
labels = next(micro_dataset)[1]
# 2. get forward chunks
forward_chunk = self._layers.get_schedule_chunk(
chunk_id=forward_virtual_pp_rank
)
if self.is_pipeline_last_stage():
assert len(self._layers._loss_fn) == 1
forward_loss_fn_node = self._layers._loss_fn[
0
].build_schedule_node()
forward_loss_fn_node.labels = labels
else:
forward_loss_fn_node = None
# 3. prepare backward inputs & get backward chunks
backward_virtual_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id, forward=False
)
self.set_virtual_pipeline_rank(backward_virtual_pp_rank)
if self.user_hooks_enabled:
self.backward_hooks.run_hook()
(
_,
_,
backward_grads,
backward_chunk,
backward_loss_fn_node,
) = self._get_backward_input(backward_virtual_pp_rank)
# 4. forward & backward
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
"[Pipeline details] Start_forward_backward_step"
)
if self._enable_timer:
self.timers("forward_backward_step").start()
output_tensor, forward_loss, input_tensor_grad = (
self._layers.overlapped_forward_backward(
forward_chunk,
input_tensor_dict if use_dict else forward_inputs,
forward_loss_fn_node,
backward_chunk,
backward_loss_fn_node,
backward_grads,
self.scaler,
p2p_async_handle=p2p_async_handle,
)
)
output_tensor_tuple = dict_to_tuple_helper(output_tensor)
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
"[Pipeline details] After_forward_backward_step"
)
if self._enable_timer:
self.timers("forward_backward_step").stop()
# 5. process forward outputs
forward_virtual_pp_rank = self._get_virtual_pp_rank(
forward_micro_step_id, forward=True
)
self.set_virtual_pipeline_rank(forward_virtual_pp_rank)
self._store_forward_outputs(
forward_virtual_pp_rank,
output_tensor_tuple,
forward_chunk,
forward_loss_fn_node,
)
if self.is_pipeline_first_stage() or self.is_pipeline_last_stage():
# Only increase micro batch id at virtual first/last pp stage.
# The micro batch id is used to load data, therefore, only increase it when load data.
self.micro_batch_id += 1
if self.is_pipeline_last_stage():
# In overlap mode, only one loss_fn is supported.
if self.total_loss is None:
self.total_loss = [[]]
self.total_loss[0].append(forward_loss.detach())
# 6. process backward outputs
backward_virtual_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id, forward=False
)
self.set_virtual_pipeline_rank(backward_virtual_pp_rank)
self._overlap_comm_grads()
return output_tensor_tuple, input_tensor_grad
def bw_hook_func(self, buffer, param):
# For pipeline with interleave, we need to add grad to buffer without communication.
# Use communication where appropriate to avoid dp communication and pp scheduling conflicts.
# all reduce hook
@paddle.autograd.no_grad()
def fused_allreduce(*_):
buffer.add_grad(param, use_comm=False)
return fused_allreduce
def register_allreduce_overlap_hook(self, model, comm_group, acc_steps, dp):
super().register_allreduce_overlap_hook(
model, comm_group, acc_steps, dp, group_size=sys.maxsize
)
def _init_buffers(self):
# init some data buffers for interleave scheduler
self.input_tensors = [[] for _ in range(self.num_model_chunks)]
self.output_tensors = [[] for _ in range(self.num_model_chunks)]
self.output_tensor_grads = [[] for _ in range(self.num_model_chunks)]
self.schedule_chunks = [[] for _ in range(self.num_model_chunks)]
self.loss_fn_chunks = []
def forward_backward_pipeline(
self,
data,
scaler,
forward_only=False,
compute_loss=True,
static_scheduler=False,
return_micro_batch_loss=False,
):
"""
Executes forward and backward passes for pipeline parallel training with interleaved scheduling.
This method implements pipeline parallel training using interleaved scheduling strategy,
inspired by Megatron-LM's implementation. It handles forward pass, backward pass, and
gradient computation while managing communication and synchronization between stages.
Args:
data: Input data that will be wrapped into micro-batches
scaler: Gradient scaler for mixed precision training
forward_only: Whether to only perform forward pass (default: False)
compute_loss: Whether to compute loss (default: True)
return_micro_batch_loss: Whether to return micro-batch level loss (default: False)
Returns:
Training loss or logits if compute_loss is True;
Otherwise returns output logits from the last stage
Raises:
AssertionError:
- When compute_loss=False but forward_only=False
- When cache is disabled but using interleaved pipeline
- When buffers are not empty after execution
Note:
- Uses interleaved scheduling strategy (requires cache to be enabled)
- Supports overlapping communication and computation for optimization
- Handles startup phase, steady phase, and cooldown phase
- Supports best unbalanced scheduler (_best_unbalanced_scheduler)
"""
self._reset_user_hooks_status()
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
"[Pipeline details] Start_forward_backward_step"
)
# use interleave scheduling strategy.
# this strategy is inspired by:
# https://github.com/NVIDIA/Megatron-LM/blob/main/megatron/schedules.py
if not compute_loss:
assert forward_only, (
"compute_loss can only be set to False when forward_only is set to True"
)
if static_scheduler:
assert not forward_only, (
"static_scheduler only for training not for eval"
)
assert not self._profiling, (
"While _profiling, static scheduler is not available"
)
if data is not None:
warnings.warn(
"Static scheduler run won't real run the model, but data has been provided"
)
logger.info(
"enable static_scheduler will return the pp schedule instead of the loss"
)
schedule = ""
# NOTE(shenliang03): Due to ring_exchange for pipeline with interleave, cache should be enabled
assert self._using_cache, (
"cache should be enabled for pipeline with interleave"
)
self.overlap_schedule_mode = (
hasattr(type(self._layers), "overlapped_forward_backward")
and self._strategy.hybrid_configs[
"pp_configs"
].forward_backward_overlap_scheduler
)
if forward_only:
self.overlap_schedule_mode = False
# init some attributes for this batch run
self.scaler = scaler
self.total_loss = None
self.micro_batch_id = 0
self._forward_only = forward_only
self.user_hooks_enabled = not self._forward_only
first_chunk_acc = (
self.accumulate_steps % self.num_stages + self.num_stages
)
first_chunk_steps = first_chunk_acc * self.num_model_chunks
fwd_buffer_queue = queue.Queue()
bwd_buffer_queue = queue.Queue()
skip_steps = self.accumulate_steps % self.num_stages
last_stage_recv_queue = deque()
left_id = skip_steps
right_id = left_id + first_chunk_acc * (self.num_model_chunks - 1)
def _process_fwd_buffer(step_id, tensor):
if step_id < first_chunk_steps:
if not self.is_pipeline_last_stage():
fwd_buffer_queue.put(tensor)
if left_id <= step_id < right_id:
tensor = fwd_buffer_queue.get()
else:
tensor = None
else:
if self.is_pipeline_last_stage():
tensor = None
return tensor
def _last_stage_need_recv_next(micro_step):
if micro_step >= first_chunk_acc:
if len(last_stage_recv_queue) == 0:
return False
else:
res = last_stage_recv_queue[0]
if micro_step - res[0] < self.num_stages:
return False
else:
return True
else:
return False
def _last_stage_recv_pp_rank(micro_step):
if micro_step >= first_chunk_acc:
assert len(last_stage_recv_queue) != 0, (
"last_stage_recv_queue can't be empty"
)
virtual_pp_stage = (last_stage_recv_queue.popleft())[1]
return virtual_pp_stage - 1
else:
return self.num_model_chunks - 1
def _process_bwd_buffer(step_id, tensor):
if self._best_unbalanced_scheduler:
if not self.is_pipeline_first_stage():
bwd_buffer_queue.put(tensor)
if step_id >= left_id and not bwd_buffer_queue.empty():
tensor = bwd_buffer_queue.get()
else:
tensor = None
else:
if step_id < first_chunk_steps:
if not self.is_pipeline_first_stage():
bwd_buffer_queue.put(tensor)
if left_id <= step_id < right_id:
tensor = bwd_buffer_queue.get()
else:
tensor = None
else:
if self.is_pipeline_first_stage():
tensor = None
return tensor
per_stage_accumulate_steps = self.accumulate_steps // self.num_stages
self._backward_step_count = -(
first_chunk_steps
+ (per_stage_accumulate_steps - 2)
* self.num_stages
* self.num_model_chunks
)
self._init_buffers()
micro_dataset = self._wrap_data(data)
num_steps = self.accumulate_steps * self.num_model_chunks
if forward_only:
# If only forward, since there is no backward during running, all steps are startup steps
startup_steps = num_steps
else:
# actually startup_steps is calculated from two number:
# first_forward_cross_to_end = (self.num_stages - self.stage_id - 1) + (self.num_model_chunks - 1) * self.num_stages
# end_to_first_backward_cross = (self.num_stages - self.stage_id - 1)
# startup_steps = first_forward_cross_to_end + end_to_first_backward_cross
startup_steps = (self.num_stages - self.stage_id - 1) * 2
startup_steps += (self.num_model_chunks - 1) * first_chunk_acc
startup_steps = min(startup_steps, num_steps)
# An additional micro step is needed for overplapping schedule
if self.overlap_schedule_mode:
startup_steps += 1
steady_steps = num_steps - startup_steps
for location in range(self.stage_id):
if self.user_hooks_enabled:
self.bubble_hooks.run_hook()
rest_bubble_times = self.num_stages - 1 - self.stage_id
self.set_virtual_pipeline_rank(0)
if not static_scheduler:
self.input_tensors[0].append(
self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(),
sync_recv=False,
batch_p2p_comm=self._use_batch_p2p_comm,
)
)
fwd_wait_handles = None
bwd_wait_handles = None
# run startup steps
for micro_step in range(startup_steps):
if fwd_wait_handles is not None:
for req in fwd_wait_handles:
req.wait()
if static_scheduler:
virtual_pp_rank = self._get_virtual_pp_rank(
micro_step, forward=True
)
real_micro_step = self._forward_micro_step_counter[
virtual_pp_rank
]
self._forward_micro_step_counter[virtual_pp_rank] += 1
schedule += f"f{real_micro_step}_vp{virtual_pp_rank};"
logger.info(
f"forward step for {real_micro_step} with virtual pp rank {virtual_pp_rank}"
)
continue
self._record_stamp("F", micro_step, '"B"', forward=True)
output_tensor = self._forward_step_helper(
micro_dataset,
micro_step,
overlap_schedule_mode=self.overlap_schedule_mode,
)
self._record_stamp("F", micro_step, '"E"', forward=True)
if micro_step >= startup_steps - rest_bubble_times:
if self.user_hooks_enabled:
self.bubble_hooks.run_hook()
# determine whether recv forward tensor or not
next_virtual_pp_rank = self._get_virtual_pp_rank(
micro_step + 1, forward=True
)
recv_prev = True
if self.is_pipeline_first_stage(ignore_virtual=True):
if next_virtual_pp_rank == 0:
# next chunk is the first chunk, not need to pre recv an input tensor
recv_prev = False
# last micro step, no next run
if micro_step == (num_steps - 1):
recv_prev = False
# last stage shouldn't send tensor to downstream
if self.is_pipeline_last_stage(ignore_virtual=True):
output_tensor = _process_fwd_buffer(micro_step, output_tensor)
if not self._overlap_p2p_comm:
# prepare for the first steady step
if (
micro_step == (startup_steps - 1)
and (not forward_only)
and steady_steps
):
input_tensor_grad = None
recv_next = True
if self.is_pipeline_last_stage(ignore_virtual=True):
recv_next = False
# the last startup step needs on four direction comm to set up for steady 1f1b
(
input_tensor,
output_tensor_grad,
) = self._p2p_helper.send_forward_backward_recv_forward_backward(
output_tensor,
input_tensor_grad,
recv_prev=recv_prev,
recv_next=recv_next,
batch_p2p_comm=self._use_batch_p2p_comm,
skip_check_meta=not self.training,
)
# output_tensor_grad is not none if recv_next
# append output_tensor_grad no matter none or not
self.output_tensor_grads[self.num_model_chunks - 1].append(
output_tensor_grad
)
else:
input_tensor = self._p2p_helper.send_forward_recv_forward(
output_tensor,
recv_prev=recv_prev,
batch_p2p_comm=self._use_batch_p2p_comm,
skip_check_meta=not self.training,
)
# append input_tensor no matter none or not
self.input_tensors[next_virtual_pp_rank].append(input_tensor)
else:
(
input_tensor,
fwd_wait_handles,
) = self._p2p_helper.send_forward_recv_forward(
output_tensor,
recv_prev=recv_prev,
batch_p2p_comm=self._use_batch_p2p_comm,
overlap_p2p_comm=True,
skip_check_meta=not self.training,
)
if (
micro_step == (startup_steps - 1)
and (not forward_only)
and steady_steps
):
input_tensor_grad = None
recv_next = True
if self.is_pipeline_last_stage(ignore_virtual=True):
recv_next = False
(
output_tensor_grad,
bwd_wait_handles,
) = self._p2p_helper.send_backward_recv_backward(
input_tensor_grad,
recv_next=recv_next,
batch_p2p_comm=self._use_batch_p2p_comm,
overlap_p2p_comm=True,
)
self.output_tensor_grads[self.num_model_chunks - 1].append(
output_tensor_grad
)
# append input_tensor no matter none or not
self.input_tensors[next_virtual_pp_rank].append(input_tensor)
_release_output(output_tensor)
# run 1f1b steady steps
for micro_step in range(steady_steps):
if static_scheduler:
forward_micro_step_id = micro_step + startup_steps
forward_virtual_pp_rank = self._get_virtual_pp_rank(
forward_micro_step_id, forward=True
)
backward_micro_step_id = micro_step
backward_virtual_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id, forward=False
)
real_forward_micro_step = self._forward_micro_step_counter[
forward_virtual_pp_rank
]
self._forward_micro_step_counter[forward_virtual_pp_rank] += 1
real_backward_micro_step = self._backward_micro_step_counter[
backward_virtual_pp_rank
]
self._backward_micro_step_counter[backward_virtual_pp_rank] += 1
schedule += (
f"f{real_forward_micro_step}_vp{forward_virtual_pp_rank};"
)
schedule += (
f"b{real_backward_micro_step}_vp{backward_virtual_pp_rank};"
)
logger.info(
f"forward step for {real_forward_micro_step} with virtual pp rank {forward_virtual_pp_rank}"
)
logger.info(
f"backward step for {real_backward_micro_step} with virtual pp rank {backward_virtual_pp_rank}"
)
continue
# forward
forward_micro_step_id = micro_step + startup_steps
if self._overlap_p2p_comm:
backward_micro_step_id = micro_step
def forward_handle_wait(fwd_wait_handles):
if fwd_wait_handles is not None:
for req in fwd_wait_handles:
req.wait()
def forward_async_comm(forward_micro_step_id, output_tensor):
forward_virtual_pp_rank = self._get_virtual_pp_rank(
forward_micro_step_id, forward=True
)
self.set_virtual_pipeline_rank(forward_virtual_pp_rank)
# determine whether to recv input tensor from upstream
recv_prev = True
if self.is_pipeline_first_stage(ignore_virtual=True):
next_forward_virtual_pp_rank = (
self._get_virtual_pp_rank(
forward_micro_step_id + 1, forward=True
)
)
if next_forward_virtual_pp_rank == 0:
# next chunk is the first chunk, not need to pre recv an input tensor
recv_prev = False
else:
next_forward_virtual_pp_rank = (
self._get_virtual_pp_rank(
forward_micro_step_id + 1, forward=True
)
)
# last iteration doesn't need recv from upstream
if micro_step == (steady_steps - 1):
recv_prev = False
if self.is_pipeline_last_stage(ignore_virtual=True):
output_tensor = _process_fwd_buffer(
forward_micro_step_id, output_tensor
)
# Send activation tensor to the next stage and receive activation tensor from the
# previous stage
(
input_tensor,
fwd_wait_handles,
) = self._p2p_helper.send_forward_recv_forward(
output_tensor,
recv_prev=recv_prev,
batch_p2p_comm=self._use_batch_p2p_comm,
overlap_p2p_comm=True,
skip_check_meta=not self.training,
)
_release_output(output_tensor)
return (
next_forward_virtual_pp_rank,
input_tensor,
fwd_wait_handles,
)
def backward_handle_wait(bwd_wait_handles):
if bwd_wait_handles is not None:
for req in bwd_wait_handles:
req.wait()
def backward_async_comm(
backward_micro_step_id, input_tensor_grad
):
if (
self._best_unbalanced_scheduler
and self.is_pipeline_last_stage(ignore_virtual=True)
):
cur_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id, forward=False
)
if cur_pp_rank != 0:
last_stage_recv_queue.append(
(backward_micro_step_id, cur_pp_rank)
)
# first stage doesn't send grad to upstream
backward_virtual_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id, forward=False
)
self.set_virtual_pipeline_rank(backward_virtual_pp_rank)
if self.is_pipeline_first_stage(ignore_virtual=True):
input_tensor_grad = _process_bwd_buffer(
backward_micro_step_id, input_tensor_grad
)
recv_next = True
if self.is_pipeline_last_stage(ignore_virtual=True):
if self._best_unbalanced_scheduler:
next_backward_virtual_pp_rank = (
self._get_virtual_pp_rank(
backward_micro_step_id + 1,
forward=False,
)
)
if self.is_pipeline_last_stage(ignore_virtual=True):
recv_next = _last_stage_need_recv_next(
backward_micro_step_id + 1
)
else:
next_backward_virtual_pp_rank = (
self._get_virtual_pp_rank(
backward_micro_step_id + 1,
forward=False,
)
)
if next_backward_virtual_pp_rank == (
self.num_model_chunks - 1
):
# next chunk is the last chunk, not need to pre recv an output tensor grad
recv_next = False
else:
next_backward_virtual_pp_rank = (
self._get_virtual_pp_rank(
backward_micro_step_id + 1,
forward=False,
)
)
(
output_tensor_grad,
bwd_wait_handles,
) = self._p2p_helper.send_backward_recv_backward(
input_tensor_grad,
recv_next=recv_next,
batch_p2p_comm=self._use_batch_p2p_comm,
overlap_p2p_comm=True,
)
return (
next_backward_virtual_pp_rank,
output_tensor_grad,
recv_next,
bwd_wait_handles,
)
# Package some closure functions and parameters into `P2PAsyncHandle`
# structure to simplify function parameter passing
p2p_async_handle = P2PAsyncHandle(
partial(
forward_handle_wait, fwd_wait_handles=fwd_wait_handles
),
partial(
forward_async_comm,
forward_micro_step_id=forward_micro_step_id,
),
partial(
backward_handle_wait, bwd_wait_handles=bwd_wait_handles
),
partial(
backward_async_comm,
backward_micro_step_id=backward_micro_step_id,
),
)
self._forward_backward_helper(
micro_dataset,
forward_micro_step_id,
backward_micro_step_id,
p2p_async_handle,
)
# Information that needs to be updated
next_forward_virtual_pp_rank = (
p2p_async_handle.next_forward_virtual_pp_rank
)
input_tensor = p2p_async_handle.input_tensor
fwd_wait_handles = p2p_async_handle.out_fwd_wait_handles
next_backward_virtual_pp_rank = (
p2p_async_handle.next_backward_virtual_pp_rank
)
output_tensor_grad = p2p_async_handle.output_tensor_grad
recv_next = p2p_async_handle.recv_next
bwd_wait_handles = p2p_async_handle.out_bwd_wait_handles
else:
backward_micro_step_id = micro_step
output_tensor, input_tensor_grad = (
self._forward_backward_helper(
micro_dataset,
forward_micro_step_id,
backward_micro_step_id,
)
)
if (
self._best_unbalanced_scheduler
and self.is_pipeline_last_stage(ignore_virtual=True)
):
cur_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id, forward=False
)
if cur_pp_rank != 0:
last_stage_recv_queue.append(
(backward_micro_step_id, cur_pp_rank)
)
# four directions comm
# send output tensor to downstream
# send input tensor grad to upstream
# recv input tensor from upstream
# recv output tensor grad from downstream
# last stage doesn't send rst to downstream
forward_virtual_pp_rank = self._get_virtual_pp_rank(
forward_micro_step_id, forward=True
)
self.set_virtual_pipeline_rank(forward_virtual_pp_rank)
if self.is_pipeline_last_stage(ignore_virtual=True):
output_tensor = _process_fwd_buffer(
forward_micro_step_id, output_tensor
)
# first stage doesn't send grad to upstream
backward_virtual_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id, forward=False
)
self.set_virtual_pipeline_rank(backward_virtual_pp_rank)
if self.is_pipeline_first_stage(ignore_virtual=True):
input_tensor_grad = _process_bwd_buffer(
backward_micro_step_id, input_tensor_grad
)
# determine whether to recv input tensor from upstream
recv_prev = True
next_forward_virtual_pp_rank = self._get_virtual_pp_rank(
forward_micro_step_id + 1, forward=True
)
if self.is_pipeline_first_stage(ignore_virtual=True) and (
next_forward_virtual_pp_rank == 0
):
# first pp stage and first virtual stage
recv_prev = False
# last iteration doesn't need recv from upstream
if micro_step == (steady_steps - 1):
recv_prev = False
# determine whether to recv grad from downstream
recv_next = True
if self._best_unbalanced_scheduler:
next_backward_virtual_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id + 1,
forward=False,
)
if self.is_pipeline_last_stage(ignore_virtual=True):
recv_next = _last_stage_need_recv_next(
backward_micro_step_id + 1
)
else:
next_backward_virtual_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id + 1, forward=False
)
if self.is_pipeline_last_stage(ignore_virtual=True) and (
next_backward_virtual_pp_rank
== (self.num_model_chunks - 1)
):
# last pp stage and last virtual stage
recv_next = False
(
input_tensor,
output_tensor_grad,
) = self._p2p_helper.send_forward_backward_recv_forward_backward(
output_tensor,
input_tensor_grad,
recv_prev=recv_prev,
recv_next=recv_next,
batch_p2p_comm=self._use_batch_p2p_comm,
skip_check_meta=not self.training,
)
# append input_tensor no matter none or not
self.input_tensors[next_forward_virtual_pp_rank].append(
input_tensor
)
# append output_tensor_grad no matter none or not
if self._best_unbalanced_scheduler:
if self.is_pipeline_last_stage(ignore_virtual=True):
if recv_next:
recv_next_virtual_pp_rank = _last_stage_recv_pp_rank(
backward_micro_step_id + 1
)
self.output_tensor_grads[
recv_next_virtual_pp_rank
].append(output_tensor_grad)
if (
next_backward_virtual_pp_rank
== self.num_model_chunks - 1
and recv_next_virtual_pp_rank
!= next_backward_virtual_pp_rank
):
self.output_tensor_grads[
self.num_model_chunks - 1
].append(None)
elif (
next_backward_virtual_pp_rank
== self.num_model_chunks - 1
):
self.output_tensor_grads[
self.num_model_chunks - 1
].append(None)
else:
self.output_tensor_grads[
next_backward_virtual_pp_rank
].append(output_tensor_grad)
else:
self.output_tensor_grads[next_backward_virtual_pp_rank].append(
output_tensor_grad
)
_release_output(output_tensor)
assert fwd_buffer_queue.empty(), "forward buffer should be empty"
if not static_scheduler:
_release_output(output_tensor)
# remaining backward steps
if not forward_only:
if self._overlap_p2p_comm and bwd_wait_handles is not None:
for wait_handles in bwd_wait_handles:
wait_handles.wait()
# no steady steps, which only occurs when accumulate_step == num_stage
if not steady_steps:
output_tensor_grad = self._p2p_helper.recv_backward(
self.is_pipeline_last_stage(),
batch_p2p_comm=self._use_batch_p2p_comm,
)
self.output_tensor_grads[self.num_model_chunks - 1].append(
output_tensor_grad
)
for micro_step in range(steady_steps, num_steps):
if static_scheduler:
virtual_pp_rank = self._get_virtual_pp_rank(
micro_step, forward=False
)
real_micro_step = self._backward_micro_step_counter[
virtual_pp_rank
]
self._backward_micro_step_counter[virtual_pp_rank] += 1
schedule += f"b{real_micro_step}_vp{virtual_pp_rank};"
logger.info(
f"backward step for {real_micro_step} with virtual pp rank {virtual_pp_rank}"
)
continue
if (
micro_step
< steady_steps + self.num_stages - 1 - self.stage_id
) and self.user_hooks_enabled:
self.bubble_hooks.run_hook()
# cooldown loop
self._record_stamp("B", micro_step, '"B"', forward=False)
input_tensor_grad = self._backward_step_helper(
micro_step, overlap_schedule_mode=self.overlap_schedule_mode
)
self._record_stamp("B", micro_step, '"E"', forward=False)
next_backward_virtual_pp_rank = self._get_virtual_pp_rank(
micro_step + 1,
forward=False,
)
if (
self._best_unbalanced_scheduler
and self.is_pipeline_last_stage(ignore_virtual=True)
):
cur_pp_rank = self._get_virtual_pp_rank(
micro_step, forward=False
)
if cur_pp_rank != 0:
last_stage_recv_queue.append((micro_step, cur_pp_rank))
recv_next = True
if self.is_pipeline_last_stage(ignore_virtual=True):
if self._best_unbalanced_scheduler:
recv_next = _last_stage_need_recv_next(micro_step + 1)
else:
if next_backward_virtual_pp_rank == (
self.num_model_chunks - 1
):
recv_next = False
if micro_step == (num_steps - 1):
recv_next = False
if self.is_pipeline_first_stage(ignore_virtual=True):
input_tensor_grad = _process_bwd_buffer(
micro_step, input_tensor_grad
)
# append output_tensor_grad no matter none or not
if self._best_unbalanced_scheduler:
if self.is_pipeline_last_stage(ignore_virtual=True):
output_tensor_grad = (
self._p2p_helper.send_backward_recv_backward(
input_tensor_grad,
recv_next=recv_next,
batch_p2p_comm=self._use_batch_p2p_comm,
)
)
if recv_next:
recv_next_virtual_pp_rank = (
_last_stage_recv_pp_rank(micro_step + 1)
)
self.output_tensor_grads[
recv_next_virtual_pp_rank
].append(output_tensor_grad)
else:
self.output_tensor_grads[
next_backward_virtual_pp_rank
].append(output_tensor_grad)
else:
self.output_tensor_grads[
next_backward_virtual_pp_rank
].append(
self._p2p_helper.send_backward_recv_backward(
input_tensor_grad,
recv_next=recv_next,
batch_p2p_comm=self._use_batch_p2p_comm,
)
)
else:
self.output_tensor_grads[
next_backward_virtual_pp_rank
].append(
self._p2p_helper.send_backward_recv_backward(
input_tensor_grad,
recv_next=recv_next,
batch_p2p_comm=self._use_batch_p2p_comm,
)
)
self._sync_overlap_grads()
for _ in range(self.stage_id):
self.bubble_hooks.run_hook()
if static_scheduler:
self._reset_counter()
return schedule
if self._enable_timer:
self.timers("allreduce_shared_weight_gradients").start()
self._layers.allreduce_shared_weight_gradients()
if self._enable_timer:
self.timers("allreduce_shared_weight_gradients").stop()
self._flush_records()
assert bwd_buffer_queue.empty(), "backward buffer should be empty"
if compute_loss:
# return loss if compute loss
if self._enable_timer:
self.timers("broadcast_final_loss").start()
with paddle.amp.auto_cast(enable=False):
train_loss_or_logits = self._broadcast_final_loss(
return_micro_batch_loss
)
if self._enable_timer:
self.timers("broadcast_final_loss").stop()
else:
# else just return logits without loss func calc
train_loss_or_logits = self.output_tensors.pop()
if self._clear_every_step_cache:
self._p2p_helper.clear_meta_cache()
self.timer_printer()
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
"[Pipeline details] End_forward_backward_step"
)
self.processed_steps += 1
self._check_user_hooks_status_at_step_end()
# reset dynamic meta counter
if self._dynamic_shape:
assert self._p2p_helper._dynamic_cnt == len(
self._p2p_helper._send_recv_meta_list
), "p2p dynamic_cnt should equal to send_recv_meta_list"
self._p2p_helper._dynamic_cnt = 0
return train_loss_or_logits
def train_batch(
self,
data,
optimizer,
lr_scheduler=None,
scaler=None,
loss_fn_idx=0,
return_micro_batch_loss=False,
):
"""
Execute one training batch with pipeline parallel interleaving schedule.
Performs forward/backward passes and optimizer update for a batch of data
using pipeline parallel with interleaved scheduling.
Args:
data: Input data for the batch
optimizer: Optimizer instance for parameter updates
lr_scheduler: Learning rate scheduler (optional)
scaler: Gradient scaler for mixed precision training (optional)
loss_fn_idx: Index of loss function to use (default: 0)
return_micro_batch_loss: Whether to return per-micro-batch losses (default: False)
Returns:
The computed training loss. If return_micro_batch_loss is True,
returns a tuple of (total_loss, micro_batch_losses).
Note:
- Handles both FP16/FP32 mixed precision training when scaler is provided
- Supports multiple loss functions through loss_fn_idx
- Uses interleaved pipeline parallel schedule for efficient training
"""
data = self._prepare_training(data, optimizer, lr_scheduler)
# check loss_fn_idx is valid and loss_fn exists
assert (
loss_fn_idx in range(len(self._layers._loss_fn))
and self._layers._loss_fn[loss_fn_idx] is not None
), f"loss function {loss_fn_idx} should exist to compute loss"
self.loss_fn_idx = loss_fn_idx
# interleave scheduler for pipeline parallel
train_loss = self.forward_backward_pipeline(
data, scaler, return_micro_batch_loss=return_micro_batch_loss
)
# optimizer
with paddle.amp.auto_cast(enable=False):
self._optimizer_step()
return train_loss
def eval_batch(
self, data, compute_loss=False, loss_fn_idx=0, return_host_tensor=False
):
self.user_hooks_enabled = False
# reset the virtual pp rank for each run
self.set_virtual_pipeline_rank(0)
self._layers.eval()
origin_compute_loss = self._compute_loss
self._compute_loss = compute_loss
origin_return_host_tensor = self._return_host_tensor
self._return_host_tensor = return_host_tensor
# check loss_fn_idx is valid and loss_fn exists
assert (
loss_fn_idx in range(len(self._layers._loss_fn))
and self._layers._loss_fn[loss_fn_idx] is not None
), f"loss function {loss_fn_idx} should exist to compute loss"
self.loss_fn_idx = loss_fn_idx
train_loss_or_logits = self.forward_backward_pipeline(
data, None, forward_only=True, compute_loss=compute_loss
)
self._init_buffers()
self._compute_loss = origin_compute_loss
self._return_host_tensor = origin_return_host_tensor
return train_loss_or_logits
def get_static_scheduler(self):
return self.forward_backward_pipeline(
data=None, scaler=None, static_scheduler=True
)
class PipelineParallelWithInterleaveFthenB(PipelineParallelWithInterleave):
def __init__(self, layers, hcg, strategy):
# Initialize the basic parameters of the parent class PipelineParallel
super().__init__(layers=layers, hcg=hcg, strategy=strategy)
# Whether to enable overlapped scheduling mode (disabled by default)
self.overlap_schedule_mode = False
def _get_scheduler_name(self):
return "PipelineParallelWithInterleaveFthenB"
def _init_user_bubble_hooks(self):
# (TODO:gexiao) support bubble hooks if needed
self.bubble_hooks = None
# self.bubble_hooks = PipelineHook()
# self.bubble_hooks.set_hooks_capacity(2 * self.num_stages - 2)
def _check_sanity(self):
assert framework.in_dynamic_mode(), (
"virtual pipeline stage with interleave only support eager dygraph mode"
)
assert self.num_stages > 2, (
"virtual pipeline must run under pp degree > 2"
)
def _get_virtual_pp_rank(self, micro_step, forward):
virtual_pp_stage = micro_step % (
self.accumulate_steps * self.num_model_chunks
)
virtual_pp_stage = virtual_pp_stage // self.accumulate_steps
if not forward:
virtual_pp_stage = self.num_model_chunks - virtual_pp_stage - 1
return virtual_pp_stage
def _overlap_comm_grads(self):
if not self._comm_overlap:
return
self._backward_step_count += 1
sync_step = self._backward_step_count - self.stage_id
if sync_step > 0 and sync_step % self.accumulate_steps == 0:
chunk_idx = self._virtual_pp_world_size - (
sync_step // self.accumulate_steps
)
for buffer in self._chunk_2_comm_buffers[chunk_idx]:
buffer.comm_grads()
if self.stage_id == 0:
return
if (
self._backward_step_count
== self.accumulate_steps * self._virtual_pp_world_size
):
for buffer in self._chunk_2_comm_buffers[0]:
buffer.comm_grads()
def _sync_overlap_grads(self):
if not self._comm_overlap:
return
expected_count = self.accumulate_steps * self._virtual_pp_world_size
assert self._backward_step_count == expected_count, (
f"backward step count should be equal to accumulate steps * virtual pp world size, "
f"but got {self._backward_step_count}, expected result is {expected_count}"
)
for buffers in self._chunk_2_comm_buffers.values():
for buffer in buffers:
buffer.scale_grads()
def forward_backward_pipeline(
self,
data,
scaler,
forward_only=False,
compute_loss=True,
return_micro_batch_loss=False,
):
self._reset_user_hooks_status()
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
"[Pipeline details] Start_forward_backward_step"
)
if not compute_loss:
assert forward_only, (
"compute_loss can only be set to False when forward_only is set to True"
)
# NOTE(shenliang03): Due to ring_exchange for pipeline with interleave, cache should be enabled
assert self._using_cache, (
"cache should be enabled for pipeline with interleave"
)
# init some attributes for this batch run
self.scaler = scaler
self.total_loss = None
self.micro_batch_id = 0
self._forward_only = forward_only
self.user_hooks_enabled = not self._forward_only
assert (
self.accumulate_steps == self.num_stages
or self.accumulate_steps % self.num_stages == 0
), (
f"accumulate_steps({self.accumulate_steps}) and num_stages({self.num_stages}) should be a multiple or accumulate_steps % num_stages == 0"
)
self._backward_step_count = 0
skip_steps = self.accumulate_steps - self.num_stages
send_recv_buffer_queue = queue.Queue()
self._init_buffers()
micro_dataset = self._wrap_data(data)
num_steps = self.accumulate_steps * self.num_model_chunks
self.set_virtual_pipeline_rank(0)
self.input_tensors[0].append(
self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(),
sync_recv=False,
batch_p2p_comm=self._use_batch_p2p_comm,
)
)
for micro_step in range(num_steps):
output_tensor = self._forward_step_helper(micro_dataset, micro_step)
# determine whether recv forward tensor or not
next_virtual_pp_rank = self._get_virtual_pp_rank(
micro_step + 1, forward=True
)
recv_prev = True
if self.is_pipeline_first_stage(ignore_virtual=True):
if next_virtual_pp_rank == 0:
# next chunk is the first chunk, not need to pre recv an input tensor
recv_prev = False
# last micro step, no next run
if micro_step == (num_steps - 1):
recv_prev = False
if self.is_pipeline_last_stage(ignore_virtual=True):
# last stage skip send/recv
if not self.is_pipeline_last_stage():
send_recv_buffer_queue.put(output_tensor)
if micro_step < skip_steps or (
self.is_pipeline_last_stage()
and micro_step % self.accumulate_steps >= skip_steps
):
output_tensor = None
else:
output_tensor = send_recv_buffer_queue.get()
input_tensor = self._p2p_helper.send_forward_recv_forward(
output_tensor,
recv_prev=recv_prev,
batch_p2p_comm=self._use_batch_p2p_comm,
skip_check_meta=not self.training,
)
self.input_tensors[next_virtual_pp_rank].append(input_tensor)
_release_output(output_tensor)
assert send_recv_buffer_queue.empty(), (
"send_recv buffer should be empty"
)
# remaining backward steps
if not forward_only:
self.output_tensor_grads[self.num_model_chunks - 1].append(
self._p2p_helper.recv_backward(
self.is_pipeline_last_stage(),
sync_recv=False,
batch_p2p_comm=self._use_batch_p2p_comm,
)
)
for micro_step in range(num_steps):
# cooldown loop
input_tensor_grad = self._backward_step_helper(micro_step)
next_backward_virtual_pp_rank = self._get_virtual_pp_rank(
micro_step + 1, forward=False
)
recv_next = True
if self.is_pipeline_last_stage(ignore_virtual=True):
if next_backward_virtual_pp_rank == (
self.num_model_chunks - 1
):
recv_next = False
if micro_step == (num_steps - 1):
recv_next = False
if self.is_pipeline_first_stage(ignore_virtual=True):
if not self.is_pipeline_first_stage():
send_recv_buffer_queue.put(input_tensor_grad)
if micro_step < skip_steps or (
self.is_pipeline_first_stage()
and micro_step % self.accumulate_steps >= skip_steps
):
input_tensor_grad = None
else:
input_tensor_grad = send_recv_buffer_queue.get()
self.output_tensor_grads[next_backward_virtual_pp_rank].append(
self._p2p_helper.send_backward_recv_backward(
input_tensor_grad,
recv_next=recv_next,
batch_p2p_comm=self._use_batch_p2p_comm,
)
)
assert send_recv_buffer_queue.empty(), (
"send_recv buffer should be empty"
)
self._sync_overlap_grads()
if self._enable_timer:
self.timers("allreduce_shared_weight_gradients").start()
self._layers.allreduce_shared_weight_gradients()
if self._enable_timer:
self.timers("allreduce_shared_weight_gradients").stop()
if compute_loss:
# return loss if compute loss
if self._enable_timer:
self.timers("broadcast_final_loss").start()
with paddle.amp.auto_cast(enable=False):
train_loss_or_logits = self._broadcast_final_loss(
return_micro_batch_loss
)
if self._enable_timer:
self.timers("broadcast_final_loss").stop()
else:
# else just return logits without loss func calc
train_loss_or_logits = self.output_tensors.pop()
if self._clear_every_step_cache:
self._p2p_helper.clear_meta_cache()
self.timer_printer()
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
"[Pipeline details] End_forward_backward_step"
)
self.processed_steps += 1
self._check_user_hooks_status_at_step_end()
return train_loss_or_logits
class OffloadQueue(queue.Queue):
def __init__(self, offload=False, *args, **kwargs):
super().__init__(*args, **kwargs)
self.offload = offload
def put(self, tensor, *args, **kwargs):
if self.offload and isinstance(
tensor, (paddle.Tensor, paddle.base.framework.core.eager.Tensor)
):
tensor_cpu = tensor.pin_memory()
tensor_cpu._share_buffer_to(tensor)
elif self.offload and isinstance(tensor, tuple):
for t in tensor:
if isinstance(
t, (paddle.Tensor, paddle.base.framework.core.eager.Tensor)
):
t_cpu = t.pin_memory()
t_cpu._share_buffer_to(t)
super().put(tensor, *args, **kwargs)
def get(self, *args, **kwargs):
tensor = super().get(*args, **kwargs)
if self.offload and isinstance(
tensor, (paddle.Tensor, paddle.base.framework.core.eager.Tensor)
):
tensor = tensor.to(paddle.base.framework._current_expected_place())
elif self.offload and isinstance(tensor, tuple):
for t in tensor:
if isinstance(
t, (paddle.Tensor, paddle.base.framework.core.eager.Tensor)
):
t_dev = t.to(
paddle.base.framework._current_expected_place()
)
t_dev._share_buffer_to(t)
return tensor
class VPPFhenBInBalancedMemory(PipelineParallelWithInterleaveFthenB):
def __init__(self, layers, hcg, strategy):
super().__init__(layers=layers, hcg=hcg, strategy=strategy)
self.overlap_schedule_mode = False
def _get_scheduler_name(self):
return "VPPFhenBInBalancedMemory"
def _init_user_bubble_hooks(self):
self.bubble_hooks = PipelineHook()
self.bubble_hooks.set_hooks_capacity(2 * self.num_stages - 2)
def forward_backward_pipeline(
self,
data,
scaler,
forward_only=False,
compute_loss=True,
return_micro_batch_loss=False,
):
self._reset_user_hooks_status()
if not compute_loss:
assert forward_only, (
"compute_loss can only be set to False when forward_only is set to True"
)
assert self._using_cache, (
"cache should be enabled for pipeline with interleave"
)
self.user_hooks_enabled = not forward_only
if forward_only:
return super().forward_backward_pipeline(
data,
scaler,
forward_only,
compute_loss,
return_micro_batch_loss,
)
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
"[Pipeline details] Start_forward_backward_step"
)
# init some attributes for this batch run
self.scaler = scaler
self.total_loss = None
self.micro_batch_id = 0
self._forward_only = forward_only
self._init_buffers()
backward_send_recv_buffer_queue = OffloadQueue(
offload=self._enable_offload_queue
)
forward_send_recv_buffer_queue = OffloadQueue(
offload=self._enable_offload_queue
)
skip_steps = self.accumulate_steps - self.num_stages
micro_dataset = self._wrap_data(data)
num_steps = self.accumulate_steps * self.num_model_chunks
# the whole pipeline is splited into 3 parse:
# startup_steps, steady_1f1b_steps, cooldown_steps
startup_steps = (
self.accumulate_steps * (self.num_model_chunks - 1)
+ self.num_stages
- self.stage_id
- 1
)
steady_1f1b_steps = self.accumulate_steps - (
self.num_stages - self.stage_id - 1
)
cooldown_steps = startup_steps
# Bubbles before startup_steps
for _ in range(self.stage_id):
if self.user_hooks_enabled:
self.bubble_hooks.run_hook()
self.set_virtual_pipeline_rank(0)
self.input_tensors[0].append(
self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(),
sync_recv=False,
batch_p2p_comm=self._use_batch_p2p_comm,
)
)
# In startup_steps, we send every output_tensor of last stage,
# to simplify the code logic of stage 1F1B.
for micro_step in range(startup_steps):
self._record_stamp("F", micro_step, '"B"', forward=True)
output_tensor = self._forward_step_helper(micro_dataset, micro_step)
self._record_stamp("F", micro_step, '"E"', forward=True)
next_forward_virtual_pp_rank = self._get_virtual_pp_rank(
micro_step + 1, forward=True
)
recv_prev = True
if self.is_pipeline_first_stage(ignore_virtual=True) and (
micro_step < self.num_stages - 1
):
recv_prev = False
input_tensor = self._p2p_helper.send_forward_recv_forward(
output_tensor,
recv_prev=recv_prev,
batch_p2p_comm=self._use_batch_p2p_comm,
skip_check_meta=not self.training,
)
if self.is_pipeline_first_stage(ignore_virtual=True):
if input_tensor is not None:
# stash the input_tensor and it will be used in the next chunk later
forward_send_recv_buffer_queue.put(input_tensor)
if next_forward_virtual_pp_rank == 0:
input_tensor = None
else:
# when a input_tensor is needed, get one from the queue
input_tensor = forward_send_recv_buffer_queue.get()
self.input_tensors[next_forward_virtual_pp_rank].append(
input_tensor
)
_release_output(output_tensor)
if self.is_pipeline_first_stage(ignore_virtual=True):
assert (
forward_send_recv_buffer_queue.qsize()
== num_steps - startup_steps - 1
), forward_send_recv_buffer_queue.qsize()
input_tensor_grad = None
for micro_step in range(steady_1f1b_steps):
first_iter = micro_step == 0
last_iter = micro_step == (steady_1f1b_steps - 1)
forward_micro_step_id = micro_step + startup_steps
backward_micro_step_id = micro_step
self._record_stamp("F", forward_micro_step_id, '"B"', forward=True)
output_tensor = self._forward_step_helper(
micro_dataset,
forward_micro_step_id,
check_is_last_chunk=True,
)
self._record_stamp("F", forward_micro_step_id, '"E"', forward=True)
if first_iter:
for _ in range(self.num_stages - self.stage_id - 1):
if self.user_hooks_enabled:
self.bubble_hooks.run_hook()
# NOTE: `send_forward_recv_backward` is intentionally unused to
# prevent hanging bugs in dynamic shape mode.
self._p2p_helper.send_forward(
output_tensor,
self.is_pipeline_last_stage(ignore_virtual=True),
batch_p2p_comm=self._use_batch_p2p_comm,
)
output_tensor_grad = self._p2p_helper.recv_backward(
self.is_pipeline_last_stage(ignore_virtual=True),
batch_p2p_comm=self._use_batch_p2p_comm,
)
# Unlike normal FthenB, in 1F1B steps, we recv output_tensor_grad
# for the current step, but not for the next step
cur_backward_virtual_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id, forward=False
)
self.output_tensor_grads[cur_backward_virtual_pp_rank].append(
output_tensor_grad
)
self._record_stamp(
"B", backward_micro_step_id, '"B"', forward=False
)
input_tensor_grad = self._backward_step_helper(
backward_micro_step_id
)
self._record_stamp(
"B", backward_micro_step_id, '"E"', forward=False
)
WeightGradStore.flush()
# stash the input_tensor_grad and it will be sent to ths last stage later
if self.is_pipeline_first_stage(ignore_virtual=True):
backward_send_recv_buffer_queue.put(input_tensor_grad)
if not last_iter:
if not WeightGradStore.funcs_queue.empty():
# NOTE: `send_backward_recv_forward` is intentionally unused to
# prevent hanging bugs in dynamic shape mode.
input_tensor, fw_wait_handles = (
self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(ignore_virtual=True),
batch_p2p_comm=self._use_batch_p2p_comm,
overlap_p2p_comm=True,
)
)
bw_wait_handles = self._p2p_helper.send_backward(
input_tensor_grad,
self.is_pipeline_first_stage(ignore_virtual=True),
batch_p2p_comm=self._use_batch_p2p_comm,
overlap_p2p_comm=True,
)
# Execute weight grad computation while P2P communication is in progress
WeightGradStore.pop()
# Wait for P2P communication to complete
if fw_wait_handles is not None:
for fw_wait_handle in fw_wait_handles:
fw_wait_handle.wait()
if bw_wait_handles is not None:
for bw_wait_handle in bw_wait_handles:
bw_wait_handle.wait()
next_forward_virtual_pp_rank = self._get_virtual_pp_rank(
forward_micro_step_id + 1, forward=True
)
if self.is_pipeline_first_stage(ignore_virtual=True):
input_tensor = forward_send_recv_buffer_queue.get()
self.input_tensors[next_forward_virtual_pp_rank].append(
input_tensor
)
else:
# NOTE: `send_backward_recv_forward` is intentionally unused to
# prevent hanging bugs in dynamic shape mode.
input_tensor = self._p2p_helper.recv_forward(
self.is_pipeline_first_stage(ignore_virtual=True),
batch_p2p_comm=self._use_batch_p2p_comm,
)
self._p2p_helper.send_backward(
input_tensor_grad,
self.is_pipeline_first_stage(ignore_virtual=True),
batch_p2p_comm=self._use_batch_p2p_comm,
)
next_forward_virtual_pp_rank = self._get_virtual_pp_rank(
forward_micro_step_id + 1, forward=True
)
if self.is_pipeline_first_stage(ignore_virtual=True):
input_tensor = forward_send_recv_buffer_queue.get()
self.input_tensors[next_forward_virtual_pp_rank].append(
input_tensor
)
else:
for _ in range(self.num_stages - self.stage_id - 1):
if self.user_hooks_enabled:
self.bubble_hooks.run_hook()
assert forward_send_recv_buffer_queue.qsize() == 0, (
forward_send_recv_buffer_queue.qsize()
)
next_backward_virtual_pp_rank = self._get_virtual_pp_rank(
steady_1f1b_steps, forward=False
)
# no more fwd, but we need to send the input_tensor_grad.
if self.is_pipeline_first_stage(ignore_virtual=True):
input_tensor_grad = backward_send_recv_buffer_queue.get()
if not WeightGradStore.funcs_queue.empty():
output_tensor_grad, wait_handles = (
self._p2p_helper.send_backward_recv_backward(
input_tensor_grad,
recv_next=True,
batch_p2p_comm=self._use_batch_p2p_comm,
overlap_p2p_comm=True,
)
)
# Execute weight grad computation while P2P communication is in progress
WeightGradStore.pop()
if wait_handles is not None:
for handle in wait_handles:
handle.wait()
self.output_tensor_grads[next_backward_virtual_pp_rank].append(
output_tensor_grad
)
else:
self.output_tensor_grads[next_backward_virtual_pp_rank].append(
self._p2p_helper.send_backward_recv_backward(
input_tensor_grad,
recv_next=True,
batch_p2p_comm=self._use_batch_p2p_comm,
)
)
# Flush any remaining deferred weight gradient computations
if not WeightGradStore.funcs_queue.empty():
raise AssertionError("WeightGradStore.funcs_queue should be empty")
WeightGradStore.clear()
# run cooldown
for micro_step in range(cooldown_steps):
backward_micro_step_id = micro_step + steady_1f1b_steps
self._record_stamp(
"B", backward_micro_step_id, '"B"', forward=False
)
input_tensor_grad = self._backward_step_helper(
backward_micro_step_id
)
self._record_stamp(
"B", backward_micro_step_id, '"E"', forward=False
)
next_backward_virtual_pp_rank = self._get_virtual_pp_rank(
backward_micro_step_id + 1, forward=False
)
# Flush deferred weight gradient computations to queue
WeightGradStore.flush()
recv_next = True
if backward_micro_step_id == (num_steps - 1):
recv_next = False
if self.is_pipeline_first_stage(ignore_virtual=True):
if not self.is_pipeline_first_stage():
backward_send_recv_buffer_queue.put(input_tensor_grad)
if (
self.is_pipeline_first_stage()
and backward_micro_step_id % self.accumulate_steps
>= skip_steps
):
# no need to send the input_tensor_grad anymore
input_tensor_grad = None
else:
input_tensor_grad = backward_send_recv_buffer_queue.get()
if not WeightGradStore.funcs_queue.empty():
output_tensor_grad, wait_handles = (
self._p2p_helper.send_backward_recv_backward(
input_tensor_grad,
recv_next=recv_next,
batch_p2p_comm=self._use_batch_p2p_comm,
overlap_p2p_comm=True,
)
)
# Execute weight grad computation while P2P communication is in progress
WeightGradStore.pop()
if wait_handles is not None:
for handle in wait_handles:
handle.wait()
self.output_tensor_grads[next_backward_virtual_pp_rank].append(
output_tensor_grad
)
else:
self.output_tensor_grads[next_backward_virtual_pp_rank].append(
self._p2p_helper.send_backward_recv_backward(
input_tensor_grad,
recv_next=recv_next,
batch_p2p_comm=self._use_batch_p2p_comm,
)
)
# Flush any remaining deferred weight gradient computations
if not WeightGradStore.funcs_queue.empty():
raise AssertionError(
"WeightGradStore.funcs_queue should be empty"
)
WeightGradStore.clear()
assert backward_send_recv_buffer_queue.empty(), (
"send_recv buffer should be empty"
)
# Bubbles after cooldown
for _ in range(self.stage_id):
if self.user_hooks_enabled:
self.bubble_hooks.run_hook()
# reset dynamic meta counter
if self._dynamic_shape:
assert self._p2p_helper._dynamic_cnt == len(
self._p2p_helper._send_recv_meta_list
), "p2p dynamic_cnt should equal to send_recv_meta_list"
self._p2p_helper._dynamic_cnt = 0
self._flush_records()
self._sync_overlap_grads()
if self._enable_timer:
self.timers("allreduce_shared_weight_gradients").start()
self._layers.allreduce_shared_weight_gradients()
if self._enable_timer:
self.timers("allreduce_shared_weight_gradients").stop()
if compute_loss:
# return loss if compute loss
if self._enable_timer:
self.timers("broadcast_final_loss").start()
with paddle.amp.auto_cast(enable=False):
train_loss_or_logits = self._broadcast_final_loss(
return_micro_batch_loss
)
if self._enable_timer:
self.timers("broadcast_final_loss").stop()
else:
# else just return logits without loss func calc
train_loss_or_logits = self.output_tensors.pop()
if self._clear_every_step_cache:
self._p2p_helper.clear_meta_cache()
self.timer_printer()
if self.processed_steps < g_profile_pipeline_details_steps:
profile_pipeline_details(
"[Pipeline details] End_forward_backward_step"
)
self.processed_steps += 1
self._check_user_hooks_status_at_step_end()
return train_loss_or_logits