# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import types from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import replace import paddle import paddle.distributed as dist from paddle.distributed.collective import Group from paddle.distributed.fleet.utils.log_util import logger from .resharder import ReadItem from .utils import ( get_target_tensor, slice_tensor, ) GROUPED_BATCH_SIZE = 10 class CommunicatorFactory: registry = {} @classmethod def register(cls, method, creator): cls.registry[method] = creator @classmethod def create(cls, comm_method, **kwargs): if comm_method not in cls.registry: raise ValueError( f"Unknown communication method '{comm_method}'. " f"Available: {list(cls.registry.keys())}" ) return cls.registry[comm_method](**kwargs) class AbstractCommunicator(ABC): @staticmethod def schedule_read_items( read_items: list[ReadItem], ) -> dict[str, list[ReadItem]]: order_rules = lambda read_item: ( read_item.tensor_name, read_item.src_rank, read_item.src_global_offset, read_item.dst_rank, read_item.dst_local_offset, read_item.dst_global_offset if read_item.dst_global_offset is not None else (), read_item.src_local_offset, read_item.slice_shape, read_item.file_name, read_item.dtype, ) # Step 1: Group by tensor_name tensor_groups = defaultdict(list) for item in read_items: tensor_groups[item.tensor_name].append(item) scheduled_items = defaultdict(list) # Step 2: For each tensor_name group, further group by all attributes except dst_rank for tensor_name, items in tensor_groups.items(): grouped_items = defaultdict(list) for item in items: key = ( item.src_global_offset, item.dst_global_offset, item.src_rank, item.dst_local_offset, item.src_local_offset, item.slice_shape, item.file_name, item.dtype, ) grouped_items[key].append(item) # Step 3: Combine items with the same key into a single ReadItem with all dst_ranks for key, grouped_item in grouped_items.items(): combined_dst_rank = [] for item in grouped_item: combined_dst_rank.extend(item.dst_rank) combined_dst_rank = sorted( set(combined_dst_rank) ) # Remove duplicates # Create a new ReadItem with combined dst_ranks scheduled_item = ReadItem( tensor_name=tensor_name, src_global_offset=key[0], dst_global_offset=key[1], dst_rank=tuple(combined_dst_rank), src_rank=key[2], dst_local_offset=key[3], src_local_offset=key[4], slice_shape=key[5], file_name=key[6], dtype=key[7], ) scheduled_items[tensor_name].append(scheduled_item) for key, items in scheduled_items.items(): scheduled_items[key] = sorted(items, key=order_rules) return dict(sorted(scheduled_items.items())) @staticmethod def split_read_items( read_items: list[ReadItem], ) -> (list[ReadItem], list[ReadItem]): local_read_items = [] comm_read_items = [] for item in read_items: assert len(item.dst_rank) == 1, ( "Before read_items is split, each ReadItem describes a communication task between one rank and another." ) if item.src_rank == item.dst_rank[0]: local_read_items.append(item) else: comm_read_items.append(item) return local_read_items, comm_read_items @staticmethod def process_local_copy_tasks( local_tasks, cur_rank, source_state_dict, target_state_dict ): """ Complete local copy tasks. """ logger.debug( f"Rank {cur_rank} starting local copy for {len(local_tasks)} tasks." ) for task in local_tasks: if task.src_rank != cur_rank: continue src_tensor = source_state_dict[task.file_name][task.tensor_name] dst_tensor = get_target_tensor(target_state_dict, task) src_chunk_tensor = slice_tensor( src_tensor, task.src_local_offset, task.slice_shape ) dst_chunk_tensor = slice_tensor( dst_tensor, task.dst_local_offset, task.slice_shape ) if src_chunk_tensor.place == dst_chunk_tensor.place: paddle.assign(src_chunk_tensor, dst_chunk_tensor) logger.debug(f"Local copy (same device) for task {task}.") else: tmp = ( src_chunk_tensor.cuda() if dst_chunk_tensor.place.is_gpu_place() else src_chunk_tensor.cpu() ) paddle.assign(tmp, dst_chunk_tensor) del tmp logger.debug(f"Local copy (cross device) for task {task}.") @abstractmethod def communicate(self, read_items, state, context): pass class BroadcastCommunicator(AbstractCommunicator): """ Communicator that uses broadcast operation for data transfer. """ def communicate(self, read_items, state, context): cur_rank = context['rank'] process_group = context['process_group'] source_state_dict = state['source_state_dict'] target_state_dict = state['target_state_dict'] local_read_items, comm_read_items = ( BroadcastCommunicator.split_read_items(read_items) ) logger.info(f"Generated {len(comm_read_items)} communication tasks.") logger.info(f"Generated {len(local_read_items)} local tasks.") BroadcastCommunicator.process_local_copy_tasks( local_read_items, cur_rank, source_state_dict, target_state_dict, ) logger.info( f"Rank {cur_rank} finished local copy and entered communication phase." ) comm_tasks = BroadcastCommunicator.schedule_read_items(comm_read_items) cnt = 0 total_task_len = len(comm_tasks) for tensor_name, read_items in comm_tasks.items(): cnt += 1 if cnt % 500 == 0 or cnt == total_task_len: logger.info( f"{cnt}/{total_task_len} tasks have been sent/received successfully!" ) source_tensors = {} destination_tensors = {} for item in read_items: logger.debug(f"Beginning to send/recv task {item}.") if item.src_rank == cur_rank: src_tensor = source_state_dict[item.file_name][ item.tensor_name ] if not src_tensor.place.is_gpu_place(): src_tensor = src_tensor.cuda() source_tensors[(tensor_name, item.file_name)] = src_tensor elif cur_rank in item.dst_rank: dst_tensor = get_target_tensor(target_state_dict, item) if not dst_tensor.place.is_gpu_place(): gpu_dst_tensor = dst_tensor.cuda() gpu_dst_tensor.need_cross_device_copy = True gpu_dst_tensor.target_tensor = dst_tensor destination_tensors[ (tensor_name, cur_rank, item.dst_global_offset) ] = gpu_dst_tensor else: gpu_dst_tensor = dst_tensor gpu_dst_tensor.target_tensor = dst_tensor destination_tensors[ (tensor_name, cur_rank, item.dst_global_offset) ] = dst_tensor for item in read_items: logger.debug(f"Beginning to send/recv task {item}.") if item.src_rank == cur_rank: src_tensor = source_tensors[(tensor_name, item.file_name)] src_chunk_tensor = slice_tensor( src_tensor, item.src_local_offset, item.slice_shape ) buffer_tensor = src_chunk_tensor.contiguous() elif cur_rank in item.dst_rank: dst_tensor = destination_tensors[ (tensor_name, cur_rank, item.dst_global_offset) ] dst_chunk_tensor = slice_tensor( dst_tensor, item.dst_local_offset, item.slice_shape ) buffer_tensor = paddle.zeros_like(dst_chunk_tensor) paddle.assign(dst_chunk_tensor, buffer_tensor) else: buffer_tensor = paddle.zeros(item.slice_shape, item.dtype) paddle.distributed.broadcast( buffer_tensor, src=item.src_rank, group=process_group ) if cur_rank in item.dst_rank: paddle.assign(buffer_tensor, dst_chunk_tensor) del buffer_tensor for dst_tensor in destination_tensors.values(): if getattr(dst_tensor, 'need_cross_device_copy', False): target_tensor = dst_tensor.target_tensor delattr(dst_tensor, "target_tensor") target_tensor.copy_(dst_tensor) else: target_tensor = dst_tensor.target_tensor delattr(dst_tensor, "target_tensor") paddle.assign(dst_tensor, target_tensor) del dst_tensor del source_tensors paddle.distributed.barrier(process_group) logger.info("All communication tasks completed.") class MultiGroupBroadcastCommunicator(AbstractCommunicator): """ Communicator that uses broadcast for data transfer across multiple communication groups. """ def __init__(self, worker_groups): if worker_groups is None: raise ValueError( "worker_groups must be specified when using multi_group_broadcast." ) self.worker_groups = worker_groups @staticmethod def schedule_read_items( comm_read_items: list[ReadItem], worker_groups: list[Group], ) -> list[list[ReadItem]]: group_members = {} name_to_groups = {} read_items = [] order_rules = lambda read_item: ( read_item.tensor_name, read_item.src_rank, read_item.src_global_offset, read_item.dst_rank, read_item.dst_local_offset, read_item.dst_global_offset if read_item.dst_global_offset is not None else (), read_item.src_local_offset, read_item.slice_shape, read_item.file_name, read_item.dtype, ) def _find_min_group(need_ranks, group_members, name_to_groups): min_group = None min_size = None for name, ranks in group_members.items(): if need_ranks <= ranks: if (min_size is None) or (len(ranks) < min_size): min_size = len(ranks) min_group = name_to_groups[name] assert min_group is not None, f"No group found for {need_ranks}!" return min_group for group in worker_groups: if len(group.ranks) <= 1: continue group_members[group.name] = set(group.ranks) name_to_groups[group.name] = group for read_item in comm_read_items: need_ranks = need_ranks = {*read_item.dst_rank, read_item.src_rank} group = _find_min_group( need_ranks, group_members, name_to_groups, ) read_items.append(replace(read_item, comm_group=group)) read_items = sorted(read_items, key=order_rules) def _build_group_conflict(group_members: dict[str, set]): member_to_groups = defaultdict(set) for g, members in group_members.items(): for m in members: member_to_groups[m].add(g) group_conflict = defaultdict(set) for group_set in member_to_groups.values(): for g1 in group_set: for g2 in group_set: if g1 != g2: group_conflict[g1].add(g2) return group_conflict def _dsatur_coloring(group_conflict: dict[str, set]) -> dict[str, int]: import heapq all_groups = sorted(group_conflict.keys()) sorted_conflict = {g: sorted(group_conflict[g]) for g in all_groups} color_map = {} neighbor_colors = {g: set() for g in all_groups} uncolored = set(all_groups) degree = {g: len(sorted_conflict[g]) for g in all_groups} heap = [] for g in all_groups: heapq.heappush(heap, (0, -degree[g], g)) saturation = dict.fromkeys(all_groups, 0) while uncolored: while True: _, _, node = heapq.heappop(heap) if node in uncolored: break used = neighbor_colors[node] color = 0 while color in used: color += 1 color_map[node] = color uncolored.remove(node) for neighbor in sorted_conflict[node]: if neighbor in uncolored: if color not in neighbor_colors[neighbor]: neighbor_colors[neighbor].add(color) saturation[neighbor] += 1 heapq.heappush( heap, ( -saturation[neighbor], -degree[neighbor], neighbor, ), ) return color_map def _assign_batches(tasks, group_color_map): batches = defaultdict(list) for t in tasks: g = t.comm_group.name batches[group_color_map[g]].append(t) return [ sorted(batches[c], key=order_rules) for c in sorted(batches) ] group_conflict = _build_group_conflict(group_members) group_color_map = _dsatur_coloring(group_conflict) results = _assign_batches(read_items, group_color_map) return results def communicate(self, read_items, state, context): cur_rank = context['rank'] process_group = context['process_group'] worker_groups = self.worker_groups source_state_dict = state['source_state_dict'] target_state_dict = state['target_state_dict'] local_read_items, comm_read_items = ( MultiGroupBroadcastCommunicator.split_read_items(read_items) ) logger.info(f"Generated {len(comm_read_items)} communication tasks.") logger.info(f"Generated {len(local_read_items)} local tasks.") MultiGroupBroadcastCommunicator.process_local_copy_tasks( local_read_items, cur_rank, source_state_dict, target_state_dict, ) results = MultiGroupBroadcastCommunicator.schedule_read_items( comm_read_items, worker_groups ) logger.info( f"Communication task scheduling completed, {len(results)} batches in total." ) for read_items in results: source_tensors = {} destination_tensors = {} for item in read_items: tensor_name = item.tensor_name if item.src_rank == cur_rank: src_tensor = source_state_dict[item.file_name][tensor_name] if not src_tensor.place.is_gpu_place(): src_tensor = src_tensor.cuda() source_tensors[(tensor_name, item.file_name)] = src_tensor elif cur_rank in item.dst_rank: dst_tensor = get_target_tensor(target_state_dict, item) if not dst_tensor.place.is_gpu_place(): gpu_dst_tensor = dst_tensor.cuda() gpu_dst_tensor.need_cross_device_copy = True gpu_dst_tensor.target_tensor = dst_tensor destination_tensors[ (tensor_name, cur_rank, item.dst_global_offset) ] = gpu_dst_tensor else: gpu_dst_tensor = dst_tensor gpu_dst_tensor.target_tensor = dst_tensor destination_tensors[ (tensor_name, cur_rank, item.dst_global_offset) ] = dst_tensor for item in read_items: logger.debug(f"Beginning to send/recv task {item}.") tensor_name = item.tensor_name if item.src_rank == cur_rank: src_tensor = source_tensors[(tensor_name, item.file_name)] src_chunk_tensor = slice_tensor( src_tensor, item.src_local_offset, item.slice_shape ) buffer_tensor = src_chunk_tensor.contiguous() elif cur_rank in item.dst_rank: dst_tensor = destination_tensors[ (tensor_name, cur_rank, item.dst_global_offset) ] dst_chunk_tensor = slice_tensor( dst_tensor, item.dst_local_offset, item.slice_shape ) buffer_tensor = paddle.zeros_like(dst_chunk_tensor) paddle.assign(dst_chunk_tensor, buffer_tensor) elif cur_rank in item.comm_group.ranks: buffer_tensor = paddle.zeros(item.slice_shape, item.dtype) else: buffer_tensor = None if cur_rank in item.comm_group.ranks: paddle.distributed.broadcast( buffer_tensor, src=item.src_rank, group=item.comm_group ) if cur_rank in item.dst_rank: paddle.assign(buffer_tensor, dst_chunk_tensor) del buffer_tensor for dst_tensor in destination_tensors.values(): if getattr(dst_tensor, 'need_cross_device_copy', False): target_tensor = dst_tensor.target_tensor delattr(dst_tensor, "target_tensor") target_tensor.copy_(dst_tensor) else: target_tensor = dst_tensor.target_tensor delattr(dst_tensor, "target_tensor") paddle.assign(dst_tensor, target_tensor) del dst_tensor del source_tensors paddle.distributed.barrier(process_group) logger.info("All communication tasks completed.") class SendRecvCommunicator(AbstractCommunicator): """ Communicator that uses send/recv operations for data transfer. The process is broken down into batches to manage memory and communication overhead. """ def __init__(self, use_group): self.use_group = use_group @staticmethod def schedule_read_items( read_items: list[ReadItem], ) -> dict[str, list[ReadItem]]: order_rules = lambda read_item: ( read_item.tensor_name, read_item.src_rank, read_item.src_global_offset, read_item.dst_rank, read_item.dst_local_offset, read_item.dst_global_offset if read_item.dst_global_offset is not None else (), read_item.src_local_offset, read_item.slice_shape, read_item.file_name, read_item.dtype, ) tensor_groups = defaultdict(list) for item in read_items: tensor_groups[item.tensor_name].append(item) return dict(sorted(tensor_groups.items())) def communicate(self, read_items, state, context): comm_tasks = SendRecvCommunicator.schedule_read_items(read_items) cur_rank = context['rank'] process_group = context['process_group'] source_state_dict = state['source_state_dict'] target_state_dict = state['target_state_dict'] total_items = sum(len(items) for items in comm_tasks.values()) processed_items = 0 for batch_data in self._process_batches( comm_tasks, cur_rank, source_state_dict ): received_slices = {} self._execute_p2p_ops( batch_data, cur_rank, use_group=self.use_group ) for item, tensor in batch_data.source_slices.items(): if item not in batch_data.local_copy_tasks: tensor._clear() received_slices.update(batch_data.target_slices) processed_items += len(batch_data.read_items) progress = processed_items / total_items * 100 logger.info( f"Batch communication completed. Progress: {processed_items}/{total_items} ({progress:.1f}%)." ) self._assign_received_data(received_slices, target_state_dict) for received_slice in received_slices.values(): received_slice._clear() del received_slices if self.use_group: paddle.distributed.barrier(process_group) logger.info("All communication tasks completed successfully.") def _process_batches(self, comm_tasks, cur_rank, source_state_dict): total_items = sum(len(items) for items in comm_tasks.values()) item_count = 0 batch_read_items = [] batch_source_slices = {} batch_target_slices = {} batch_local_copy_tasks = set() for tensor_name, read_items in comm_tasks.items(): tensors_to_clear = set() for item in read_items: item_count += 1 batch_read_items.append(item) if cur_rank == item.src_rank: src_tensor = source_state_dict[item.file_name][ item.tensor_name ] src_slice = ( slice_tensor( src_tensor, item.src_local_offset, item.slice_shape ) .cuda() .clone() ) batch_source_slices[item] = src_slice tensors_to_clear.add(src_tensor) if cur_rank in item.dst_rank: if cur_rank == item.src_rank: batch_local_copy_tasks.add(item) batch_target_slices[item] = batch_source_slices[item] else: dst_slice = paddle.zeros( item.slice_shape, dtype=item.dtype ) batch_target_slices[item] = dst_slice if ((item_count % GROUPED_BATCH_SIZE) == 0) or ( item_count == total_items ): batch_data = types.SimpleNamespace( read_items=batch_read_items, source_slices=batch_source_slices, target_slices=batch_target_slices, local_copy_tasks=batch_local_copy_tasks, ) yield batch_data batch_read_items = [] batch_source_slices = {} batch_target_slices = {} batch_local_copy_tasks = set() for tensor in tensors_to_clear: tensor._clear_to_zero_allocation() def _execute_p2p_ops(self, batch_data, cur_rank, use_group): p2p_ops = [] for item in batch_data.read_items: if item.src_rank == cur_rank: for rank in item.dst_rank: if rank != cur_rank: send_tensor = batch_data.source_slices[item] if use_group: p2p_ops.append( dist.P2POp(dist.isend, send_tensor, rank) ) else: dist.send(send_tensor, rank) if cur_rank in item.dst_rank and item.src_rank != cur_rank: recv_tensor = batch_data.target_slices[item] if use_group: p2p_ops.append( dist.P2POp(dist.irecv, recv_tensor, item.src_rank) ) else: dist.recv(recv_tensor, item.src_rank) if use_group and p2p_ops: logger.info( f"Starting batched send/recv for {len(p2p_ops)} P2P operations." ) reqs = dist.batch_isend_irecv(p2p_ops) for req in reqs: req.wait() logger.info("Batched send/recv finished.") def _assign_received_data(self, received_slices, target_state_dict): for item, received_slice in received_slices.items(): dest_tensor = get_target_tensor(target_state_dict, item) if not dest_tensor._is_initialized(): buffer = paddle.zeros_like(dest_tensor) buffer._share_buffer_to(dest_tensor) dest_slice = slice_tensor( dest_tensor, item.dst_local_offset, item.slice_shape ) if dest_slice.place != received_slice.place: received_slice = received_slice.to(dest_slice.place) paddle.assign(received_slice, dest_slice) CommunicatorFactory.register( "multi_group_broadcast", lambda worker_groups: MultiGroupBroadcastCommunicator(worker_groups), ) CommunicatorFactory.register( "send_recv", lambda **kwargs: SendRecvCommunicator(use_group=False) ) CommunicatorFactory.register( "grouped_send_recv", lambda **kwargs: SendRecvCommunicator(use_group=True) ) CommunicatorFactory.register( "broadcast", lambda **kwargs: BroadcastCommunicator() )