Files
paddlepaddle--paddle/python/paddle/distributed/auto_parallel/fully_shard_fusion.py
T
2026-07-13 12:40:42 +08:00

810 lines
30 KiB
Python

# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections import OrderedDict
from dataclasses import dataclass, field
from enum import Enum
import numpy as np
import paddle
import paddle.distributed as dist
from paddle.autograd import PyLayer
from paddle.distributed.fleet.utils.tensor_fusion_helper import (
align,
alignment,
get_current_device_type,
)
# Global registry for fsdp_context
_g_fsdp_context = None
def register_fsdp_context(context):
global _g_fsdp_context
_g_fsdp_context = context
def get_fsdp_context():
return _g_fsdp_context
class BufferState(Enum):
# Buffer status for lazy double buffer mechanism
#
# State transitions:
# FREED ──all_gather──> USING ──computation done──> READY ──release──> FREED
# ^ │
# │ (reuse) │
# └────────────────────────────┘
FREED = 1 # Released, buffer data is sharded, tmp_buffer not allocated
USING = 2 # Unsharded and actively in use
READY = 3 # Unsharded, marked for lazy release, can be reused
SYNCING = 4 # Communication in progress
@dataclass
class BufferGroup:
params: list = field(default_factory=list)
dtype: object = None
trainable: bool = None
fsdp_unit_id: int = None
is_tie: bool = False
is_expert_param: bool = False
fsdp_group: object = None
params_buffer: 'TensorFusionBuffer' = None
grads_buffer: 'TensorFusionBuffer' = None
params_use_sum: int = 0
params_use_cnt: int = 0
grads_use_sum: int = 0
grads_use_cnt: int = 0
def _dtensor_from_local(local_tensor, mesh, placements):
global_dims = list(local_tensor.shape)
for idx, placement in enumerate(placements):
if placement.is_shard():
global_dims[placement.get_dim()] = (
global_dims[placement.get_dim()] * mesh.shape[idx]
)
place = paddle.framework._current_expected_place()
place = paddle.framework._get_paddle_place(place)
return paddle.Tensor(
local_tensor,
dims=global_dims,
process_mesh=mesh,
placements=placements,
place=place,
)
class TensorFusionBuffer:
def __init__(
self,
group_id,
params,
fsdp_degree,
dtype,
is_params=False,
main_grad_dtype=None,
):
# Calculate total buffer size needed (with padding)
self.group_id = group_id
self.fsdp_degree = fsdp_degree
self.dtype = dtype
self.main_grad_dtype = (
main_grad_dtype if main_grad_dtype is not None else dtype
)
self.total_buffer_size = 0
self.param_offsets = {}
self.tmp_data_buffer = None
self.comm_task = None
self.trainable = params[0].trainable
for param in params:
self.param_offsets[param.name] = self.total_buffer_size
self.total_buffer_size += self.get_padded_size(param)
if is_params:
# Create fused params_buffer
# TODO(lizhenxing): Build full params_buffer on CPU and only move shards to GPU to minimize mem peaks
self.data_buffer = paddle.zeros(
shape=[self.total_buffer_size],
dtype=dtype,
)
# Use BufferState enum instead of is_shard boolean, initial state is FREED (sharded)
self.status = BufferState.FREED
for param in params:
offset = self.param_offsets[param.name]
stop_gradient = param.stop_gradient
local_shape = param._local_shape
param.stop_gradient = True
param._local_value().flatten_()
paddle.assign(
param._local_value(),
self.data_buffer._slice(
offset,
offset + param._numel(),
),
)
param._clear_data()
param.stop_gradient = stop_gradient
param._local_value().get_tensor()._set_dims(local_shape)
paddle.device.cuda.empty_cache()
mesh = dist.auto_parallel.get_mesh()
curr_global_rank = paddle.distributed.get_rank()
if curr_global_rank in mesh.process_ids:
total_nums = self.data_buffer.shape[0]
num_of_pieces = mesh.shape[0]
piece_len = (total_nums + num_of_pieces - 1) // num_of_pieces
rank_relative = mesh.process_ids.index(curr_global_rank)
start = rank_relative * piece_len
end = min(start + piece_len, total_nums)
self.data_buffer = paddle.slice(
self.data_buffer, [0], [start], [end]
).clone()
# Init params_buffer attr
self.data_buffer.name = "fuse_params_" + str(group_id)
self.data_buffer.stop_gradient = params[0].stop_gradient
self.data_buffer.optimize_attr = params[0].optimize_attr
else:
# Create fused grads_buffer with shard
self.data_buffer = paddle.zeros(
shape=[self.total_buffer_size // self.fsdp_degree],
dtype=self.main_grad_dtype,
)
# Register get_main_grad method for each param, returns view_slice of grad_buffer
for param in params:
if param.trainable:
param._fusion_buffer = self
param._param_offsets = self.param_offsets
def get_grad_from_tmp_buf(param):
tmp_buffer = param._fusion_buffer.get_tmp_buffer()
offset = param._param_offsets[param.name]
main_grad = paddle._C_ops.view_slice(
tmp_buffer,
offset,
offset + param._numel(),
)
return main_grad
param.get_main_grad = get_grad_from_tmp_buf.__get__(param)
def get_padded_size(self, param):
size = np.prod(param.shape)
align_size = (
alignment[get_current_device_type()]
// align[param.dtype]
* self.fsdp_degree
)
return ((size + align_size - 1) // align_size) * align_size
def get_tmp_buffer(self):
# Reuse tmp_buffer if exists, else create
if self.tmp_data_buffer is None:
self.tmp_data_buffer = paddle.zeros(
shape=[self.total_buffer_size], dtype=self.dtype
)
return self.tmp_data_buffer
def clear_tmp_buffer(self):
if self.tmp_data_buffer is not None:
self.tmp_data_buffer._clear_data()
self.tmp_data_buffer = None
# paddle.device.cuda.empty_cache()
class FSDPBufferManager:
def __init__(
self, model, mesh, fsdp_unit_layers=None, moe_layers_name=None
):
self.model = model
self._fsdp_group = mesh.get_group("dp")
self.main_grad_dtype = paddle.float32
# Get EP group if "ep" dimension exists in mesh
if "ep" in mesh.dim_names:
self._ep_fsdp_group = mesh.get_group("ep")
else:
self._ep_fsdp_group = self._fsdp_group
topk = None
if hasattr(self.model, 'config') and hasattr(
self.model.config, 'num_experts_per_tok'
):
topk = self.model.config.num_experts_per_tok
# Layer types to wrap as FSDP sharding layers
# Note: 'Qwen3VLTextDecoderLayer' is temporary; fleet models all use 'TransformerLayer'
self.fsdp_unit_layers = fsdp_unit_layers or [
'TransformerLayer',
'Qwen3VLTextDecoderLayer',
'Qwen3MoeDecoderLayer',
]
# Layer types to identify MoE expert layers
self.moe_layers_name = moe_layers_name or [
'StandardMLPExpert',
]
# Get tie_param_name if using tie_weights
self.tie_param_name = None
# Note: need add get_input_embeddings in fleet modeling
# if hasattr(self.model, "get_input_embeddings"):
# self.tie_param_name = self.model.get_input_embeddings().weight.name
# Create buffer_groups
grouped_params, group_is_expert = self._build_groups()
self.buffer_groups = []
self.param_to_buffer_id = {}
# Create params_buffer, grads_buffer with groups
for gid, params in grouped_params.items():
is_expert = group_is_expert.get(gid, False)
# Use EP group for expert params, DP group for regular params
fsdp_group = self._ep_fsdp_group if is_expert else self._fsdp_group
params_buffer = TensorFusionBuffer(
gid,
params,
fsdp_group.nranks,
params[0].dtype,
is_params=True,
)
if not params[0].stop_gradient:
grads_buffer = TensorFusionBuffer(
gid,
params,
fsdp_group.nranks,
params[0].dtype,
main_grad_dtype=self.main_grad_dtype,
)
else:
grads_buffer = None
if is_expert:
_params_use_sum = topk
_grads_use_sum = topk
else:
_params_use_sum = len(params)
_grads_use_sum = len(params)
self.buffer_groups.append(
BufferGroup(
params=params,
dtype=params[0].dtype,
trainable=params[0].trainable,
is_expert_param=is_expert,
fsdp_group=fsdp_group,
params_buffer=params_buffer,
grads_buffer=grads_buffer,
params_use_sum=_params_use_sum,
params_use_cnt=0,
grads_use_sum=_grads_use_sum,
grads_use_cnt=0,
)
)
for param in params:
self.param_to_buffer_id[param.name] = gid
def _build_groups(self):
parameters = self.model.parameters()
grouped_params = OrderedDict()
group_is_expert = {}
curr_gid = 0
param_to_unit_id = {}
for unit_id, module in enumerate(self.model.modules()):
if type(module).__name__ in self.fsdp_unit_layers:
for param in module.parameters():
param_to_unit_id[param.name] = unit_id
if type(module).__name__ in self.moe_layers_name:
for param in module.parameters():
param.is_moe_param = True
temp_groups = []
for param in parameters:
name = param.name
is_expert = getattr(param, "is_moe_param", False)
if is_expert:
continue
is_tie = (
self.tie_param_name is not None and name == self.tie_param_name
)
param_attrs = {
"dtype": param.dtype,
"trainable": param.trainable,
"fsdp_unit_id": param_to_unit_id.get(name),
"is_tie": is_tie,
"is_expert_param": is_expert,
}
found_group = False
for param_group in temp_groups:
if (
param_group.dtype == param_attrs["dtype"]
and param_group.trainable == param_attrs["trainable"]
and param_group.fsdp_unit_id == param_attrs["fsdp_unit_id"]
and param_group.is_tie == param_attrs["is_tie"]
and param_group.is_expert_param
== param_attrs["is_expert_param"]
):
param_group.params.append(param)
found_group = True
break
# Create new group if no matching
if not found_group:
temp_groups.append(BufferGroup(params=[param], **param_attrs))
def group_sort_key(group):
priority = 0 if group.is_tie else (1 if not group.trainable else 2)
return (
priority,
group.fsdp_unit_id
if group.fsdp_unit_id is not None
else float('inf'),
)
sorted_groups = sorted(temp_groups, key=group_sort_key)
# For each sorted parameter group, buffer them by execution order
for param_group in sorted_groups:
cur_params = param_group.params
if len(cur_params) == 0:
continue
for p in cur_params:
grouped_params.setdefault(curr_gid, []).append(p)
group_is_expert[curr_gid] = param_group.is_expert_param
curr_gid += 1
return grouped_params, group_is_expert
class FSDPCommManager:
def __init__(
self,
buffer_manager,
enable_overlap=True,
double_buffer_limit=2,
):
self.buffer_manager = buffer_manager
self.enable_overlap = enable_overlap
self.grad_reduce_queue = []
# for double buffer mechanism config
self.double_buffer_limit = double_buffer_limit
self.buffer_cnt_in_using = 0
self._need_zero_grads = True
def _release_one_buffer_if_needed(self):
# Release a buffer with the READY status if needed
while self.buffer_cnt_in_using >= self.double_buffer_limit:
found = False
for group in self.buffer_manager.buffer_groups:
if group.params_buffer.status == BufferState.READY:
group.params_buffer.status = BufferState.FREED
group.params_buffer.clear_tmp_buffer()
self.buffer_cnt_in_using -= 1
found = True
break
if not found:
break
def _next_buffer_id(self, gid, is_backward):
# Get next buffer id for prefetch
if is_backward:
next_gid = gid - 1
# Search backward for trainable buffer_groups
while (
next_gid >= 0
and not self.buffer_manager.buffer_groups[
next_gid
].params_buffer.trainable
):
next_gid -= 1
return max(next_gid, 0)
else:
return min(gid + 1, len(self.buffer_manager.buffer_groups) - 1)
def all_gather_params(self, params, is_backward=False):
if len(params) == 0:
return
for param in params:
if hasattr(param, "is_moe_param"):
continue
gid = self.buffer_manager.param_to_buffer_id[param.name]
group = self.buffer_manager.buffer_groups[gid]
group.params_use_cnt += 1
params_buffer = group.params_buffer
# Use group-specific fsdp_group
fsdp_group = group.fsdp_group or self.buffer_manager._fsdp_group
# Double buffer: reuse buffer if status is READY
if params_buffer.status == BufferState.READY:
# Reuse: READY -> USING, no need to all_gather again
params_buffer.status = BufferState.USING
# Overlap prefetch comm
if self.enable_overlap:
next_gid = self._next_buffer_id(gid, is_backward)
next_group = self.buffer_manager.buffer_groups[next_gid]
next_params_buffer = next_group.params_buffer
next_fsdp_group = (
next_group.fsdp_group or self.buffer_manager._fsdp_group
)
if next_params_buffer.status == BufferState.FREED:
# Check double_buffer_limit before prefetch
self._release_one_buffer_if_needed()
next_params_buffer.status = BufferState.SYNCING
tmp_buffer_prefetch = next_params_buffer.get_tmp_buffer()
next_params_buffer.comm_task = (
paddle.distributed.all_gather(
tmp_buffer_prefetch,
next_params_buffer.data_buffer,
group=next_fsdp_group,
sync_op=False,
)
)
self.buffer_cnt_in_using += 1
# Wait for async comm to complete: SYNCING -> USING
if params_buffer.status == BufferState.SYNCING:
params_buffer.status = BufferState.USING
params_buffer.comm_task.wait()
params_buffer.comm_task = None
tmp_buffer = params_buffer.get_tmp_buffer()
# Do all_gather in sync: FREED -> USING
if params_buffer.status == BufferState.FREED:
fsdp_group.process_group.all_gather(
params_buffer.data_buffer, tmp_buffer
).wait()
params_buffer.status = BufferState.USING
self.buffer_cnt_in_using += 1
# Bind the unsharded param to the real param
offset = params_buffer.param_offsets[param.name]
tmp_param = paddle._C_ops.view_slice(
tmp_buffer,
offset,
offset + param._numel(),
)
tmp_param.get_tensor()._set_dims(param.shape)
tmp_param = _dtensor_from_local(
tmp_param,
param.process_mesh,
param.placements,
)
param.get_tensor()._share_data_with(tmp_param.get_tensor())
def shard_params(self, params, is_backward=False):
affected_gids = set()
for param in params:
if hasattr(param, "is_moe_param"):
continue
gid = self.buffer_manager.param_to_buffer_id.get(param.name)
group = self.buffer_manager.buffer_groups[gid]
stop_gradient = param.stop_gradient
local_shape = param._local_shape
param._clear_data()
param.stop_gradient = stop_gradient
param._local_value().get_tensor()._set_dims(local_shape)
affected_gids.add(gid)
for gid in affected_gids:
group = self.buffer_manager.buffer_groups[gid]
if group.params_buffer.status == BufferState.USING:
group.params_buffer.status = BufferState.READY
def reduce_scatter_grads(self, param):
if self._need_zero_grads:
self._need_zero_grads = False
for group in self.buffer_manager.buffer_groups:
if group.grads_buffer is not None:
group.grads_buffer.data_buffer.zero_()
gid = self.buffer_manager.param_to_buffer_id.get(param.name)
group = self.buffer_manager.buffer_groups[gid]
group.grads_use_cnt += 1
fsdp_group = group.fsdp_group or self.buffer_manager._fsdp_group
param.main_grad = None
if group.grads_use_cnt == group.grads_use_sum:
group.grads_use_cnt = 0
# reduce_scatter from tmp_grad_buffer into grads_buffer
grads_buffer = group.grads_buffer
# Grad queue mechanism: wait and release completed reduce_scatter async tasks
self._wait_for_grad_comm()
tmp_buffer = grads_buffer.get_tmp_buffer()
shard_size = grads_buffer.data_buffer.shape[0]
grad_buffer_shard = tmp_buffer._slice(0, shard_size)
if self.enable_overlap:
# Comm grads async and check all comm_task before optimizer update
grads_buffer.comm_task = paddle.distributed.reduce_scatter(
grad_buffer_shard,
tmp_buffer,
op=paddle.distributed.ReduceOp.SUM,
group=fsdp_group,
sync_op=False,
)
# Add async task to queue
self.grad_reduce_queue.append(grads_buffer)
else:
paddle.distributed.reduce_scatter(
grad_buffer_shard,
tmp_buffer,
op=paddle.distributed.ReduceOp.SUM,
group=fsdp_group,
sync_op=False,
).wait()
grads_buffer.data_buffer.add_(grad_buffer_shard)
grads_buffer.clear_tmp_buffer()
def _wait_for_grad_comm(self, queue_limit=2):
# Wait for async reduce_scatter tasks to complete and release resources
# queue_limit: max queue size, default use 2, 0 means wait for all
while len(self.grad_reduce_queue) > queue_limit:
grads_buffer = self.grad_reduce_queue.pop(0)
if grads_buffer.comm_task is not None:
grads_buffer.comm_task.wait()
grads_buffer.comm_task = None
tmp_buffer = grads_buffer.get_tmp_buffer()
shard_size = grads_buffer.data_buffer.shape[0]
grad_buffer_shard = tmp_buffer._slice(0, shard_size)
grads_buffer.data_buffer.add_(grad_buffer_shard)
grads_buffer.clear_tmp_buffer()
def _finish_grads_sync(self):
# Wait for all async reduce_scatter tasks, call before optimizer.step()
self._wait_for_grad_comm(queue_limit=0)
def _reset_params_buffer_status(self):
for group in self.buffer_manager.buffer_groups:
params_buffer = group.params_buffer
if params_buffer.status in (BufferState.READY, BufferState.USING):
# Clear stale tmp_buffer to force re-all_gather with updated data_buffer
params_buffer.clear_tmp_buffer()
params_buffer.status = BufferState.FREED
if self.buffer_cnt_in_using > 0:
self.buffer_cnt_in_using -= 1
class FusionBackwardHook(PyLayer):
@staticmethod
def forward(ctx, *inputs, layer, comm_manager, recursive=False):
ctx.layer = layer
ctx.comm_manager = comm_manager
ctx.recursive = recursive
return inputs if len(inputs) > 1 else inputs[0]
@staticmethod
def backward(ctx, *args):
trainable_params = []
for param in ctx.layer.parameters(include_sublayers=ctx.recursive):
if param.trainable:
trainable_params.append(param)
ctx.comm_manager.all_gather_params(trainable_params, is_backward=True)
return args
class FusionForwardHook(PyLayer):
@staticmethod
def forward(ctx, *inputs, layer, comm_manager, recursive=False):
ctx.layer = layer
ctx.comm_manager = comm_manager
ctx.recursive = recursive
return inputs
@staticmethod
def backward(ctx, *args):
ctx.comm_manager.shard_params(
ctx.layer.parameters(include_sublayers=ctx.recursive),
is_backward=True,
)
return args
class FullyShardFusion:
def __init__(
self, model, mesh, fsdp_unit_layers=None, moe_layers_name=None
):
self.model = model
self.mesh = self._check_mesh(mesh)
self._shard_all_params()
self.buffer_manager = FSDPBufferManager(
self.model, self.mesh, fsdp_unit_layers, moe_layers_name
)
self.comm_manager = FSDPCommManager(self.buffer_manager)
self.register_tensor_fusion_hooks(self.model)
register_fsdp_context(self)
def _check_mesh(self, mesh, pp_idx=0):
if "pp" in mesh.dim_names:
mesh = mesh.get_mesh_with_dim("pp", pp_idx)
return mesh
def _shard_all_params(self):
def shard_layer_param(layer):
for param_name in list(layer._parameters.keys()):
param = getattr(layer, param_name)
if param is not None:
param_placements = [
dist.Replicate() for _ in range(len(self.mesh.shape))
]
if not param.is_dist():
param = dist.shard_tensor(
param, self.mesh, param_placements
)
setattr(layer, param_name, param)
for name, layer in self.model.named_sublayers(include_self=True):
shard_layer_param(layer)
def comm_sync_and_reset_status(self):
self.comm_manager._finish_grads_sync()
self.comm_manager._reset_params_buffer_status()
self.comm_manager._need_zero_grads = True
# Reset main_grad for all trainable parameters
for param in self.model.parameters():
if param.trainable:
param.main_grad = None
def register_tensor_fusion_hooks(self, model):
def _pre_forward_hook(sublayers, recursive=False):
comm_manager = self.comm_manager
@paddle.autograd.no_grad()
def all_gather_comm(*_):
comm_manager.all_gather_params(
sublayers.parameters(include_sublayers=recursive)
)
return all_gather_comm
def _post_forward_hook(sublayers, recursive=False):
comm_manager = self.comm_manager
@paddle.autograd.no_grad()
def shard_comm(*_):
comm_manager.shard_params(
sublayers.parameters(include_sublayers=recursive)
)
return shard_comm
def _update_main_grad_hook(param):
comm_manager = self.comm_manager
@paddle.autograd.no_grad()
def comm_hook(grad):
if grad is not None and grad._is_initialized():
# Share mem with grads_tmp_buffer
_main_grad = param.get_main_grad()
_main_grad.get_tensor()._set_dims(grad._local_shape)
param.main_grad = _dtensor_from_local(
_main_grad,
grad.process_mesh,
grad.placements,
)
param.main_grad._local_value().copy_(grad._local_value())
grad._clear_data()
comm_manager.shard_params([param], is_backward=True)
comm_manager.reduce_scatter_grads(param)
return comm_hook
def _post_backward_hook(param):
param.main_grad = None
if hasattr(param, "get_main_grad"):
param._register_grad_hook(_update_main_grad_hook(param))
for param in model.parameters():
if param.trainable:
_post_backward_hook(param)
def _register_recursive(layer):
is_unit = (
type(layer).__name__ in self.buffer_manager.fsdp_unit_layers
)
if is_unit:
# For FSDP Unit, register recursive hooks and stop recursion
layer.register_forward_pre_hook(
_pre_forward_hook(layer, recursive=True)
)
layer.register_forward_post_hook(
_post_forward_hook(layer, recursive=True)
)
self._register_fusion_layer_hooks(layer, recursive=True)
return
if layer.parameters(include_sublayers=False):
layer.register_forward_pre_hook(
_pre_forward_hook(layer, recursive=False)
)
layer.register_forward_post_hook(
_post_forward_hook(layer, recursive=False)
)
self._register_fusion_layer_hooks(layer, recursive=False)
for child in layer.children():
_register_recursive(child)
_register_recursive(model)
def _register_fusion_layer_hooks(self, layer, recursive=False):
def _forward_post_hook(layer, inputs, outputs):
if isinstance(outputs, dict):
for key, value in outputs.items():
if (
isinstance(value, paddle.Tensor)
and not value.stop_gradient
):
outputs[key] = FusionBackwardHook.apply(
value,
layer=layer,
comm_manager=self.comm_manager,
recursive=recursive,
)
return outputs
elif isinstance(outputs, tuple):
result = FusionBackwardHook.apply(
*outputs,
layer=layer,
comm_manager=self.comm_manager,
recursive=recursive,
)
if not isinstance(result, tuple):
result = (result,)
return result
else:
return FusionBackwardHook.apply(
outputs,
layer=layer,
comm_manager=self.comm_manager,
recursive=recursive,
)
def _forward_pre_hook(layer, inputs):
return FusionForwardHook.apply(
*inputs,
layer=layer,
comm_manager=self.comm_manager,
recursive=recursive,
)
layer.register_forward_post_hook(_forward_post_hook)
# Register an additional hook for tie_weights shard_params
for param in layer.parameters(include_sublayers=False):
if param.name == self.comm_manager.buffer_manager.tie_param_name:
layer.register_forward_pre_hook(_forward_pre_hook)