Source code for rlightning.weights.weight_transfer_manager

import gc
from collections import defaultdict
from typing import Dict, List, Sequence, Union

import ray
import torch
from ray.actor import ActorHandle

from rlightning.utils.distributed.comm_context import CommContext
from rlightning.utils.distributed.group_initializer import ParallelMode
from rlightning.utils.logger import get_logger
from rlightning.utils.utils import InternalFlag
from rlightning.weights.utils import tensor_state_dict_to_numpy
from rlightning.weights.weight_buffer import CPUWeightBuffer

from .weight_buffer import WeightBuffer

logger = get_logger(__name__)


[docs] class WeightTransferManager: def __init__(self, buffer_strategy: str): self.buffer_strategy = buffer_strategy
[docs] def clear_cache(self): gc.collect() torch.cuda.empty_cache()
[docs] def record_gpu_memory(self, record_name: str): allocated_memory = torch.cuda.memory_allocated() max_reserved_memory = torch.cuda.max_memory_reserved() logger.debug(f"Allocated Memory: {allocated_memory/1024/1024/1024} GB {record_name}") logger.debug(f"Max Reserved Memory: {max_reserved_memory/1024/1024/1024} GB {record_name}")
def _group_params_by_dtype(self, state_dict: Dict[str, Dict]): params_by_dtype = defaultdict(list) metadata_by_dtype = defaultdict(list) for module_name, module_state_dict in state_dict.items(): for name, param in module_state_dict.items(): dtype = param.dtype params_by_dtype[dtype].append(param) metadata_by_dtype[dtype].append( { "module_name": module_name, "name": name, "shape": param.shape, "numel": param.numel(), } ) return params_by_dtype, metadata_by_dtype def _flat_tensor_to_state_dict( self, params_by_dtype: Dict[torch.dtype, torch.Tensor], metadata_by_dtype: Dict[torch.dtype, Sequence], to_numpy: bool = False, ): state_dict = {} for dtype, metadata_list in metadata_by_dtype.items(): flat_tensor = params_by_dtype[dtype] offset = 0 for meta in metadata_list: if meta["module_name"] not in state_dict: state_dict[meta["module_name"]] = {} n_param = meta["numel"] param_flat = flat_tensor.narrow(0, offset, n_param).view(meta["shape"]) if to_numpy: if dtype == torch.bfloat16: state_dict[meta["module_name"]][meta["name"]] = param_flat.to("cpu").view(torch.uint16).numpy() else: state_dict[meta["module_name"]][meta["name"]] = param_flat.to("cpu").numpy() else: state_dict[meta["module_name"]][meta["name"]] = param_flat offset += n_param return state_dict
[docs] def send_weights_cpu(self, state_dict: Dict[str, Dict[str, torch.Tensor]], cpu_weight_buffer: ActorHandle): """ Sends weights to the CPU weight buffer actor. """ cpu_state_dict = tensor_state_dict_to_numpy(state_dict) if isinstance(cpu_weight_buffer, ActorHandle): cpu_state_dict = ray.put(cpu_state_dict) ray.get(cpu_weight_buffer.add_state_dict.remote(cpu_state_dict)) else: cpu_weight_buffer.add_state_dict(cpu_state_dict)
[docs] def send_weights(self, state_dict: Dict[str, Dict[str, torch.Tensor]], eval_policies: List[ActorHandle]): """Broadcasts the state dict to the eval policies. This function is called on the TrainPolicy under sync mode. It dispatches to the correct sending strategy based on the eval policies' configuration. Args: state_dict (Dict[str, Dict[str, torch.Tensor]]): A dict of module state dicts. eval_policies (List[ActorHandle]): A list of Actors which related to policies for evaluation Raises: ValueError: Invalid buffer strategy """ if self.buffer_strategy == "None": # Broadcast weights layer by layer context_group = CommContext().get_group(ParallelMode.WEIGHT_TRANSFER) # The sender is always ranks_in_group[0] (train policy's global rank). src_rank = CommContext().get_ranks_in_group(ParallelMode.WEIGHT_TRANSFER)[0] num_layers = sum(len(module_state_dict) for module_state_dict in state_dict.values()) cur_layer = 0 for module_name, module_state_dict in state_dict.items(): for name, param in module_state_dict.items(): cur_layer += 1 shape, dtype = param.shape, param.dtype refs = [ eval_policy.sync_weights_layer_by_layers.remote( module_name, name, dtype, shape, is_last=(cur_layer == num_layers) ) for eval_policy in eval_policies ] torch.distributed.broadcast(param.data, src=src_rank, group=context_group) ray.get(refs) else: # 1. Group parameters by dtype params_by_dtype, metadata_by_dtype = self._group_params_by_dtype(state_dict) # 2. Asynchronously call recv_weights on all eval policies to get them ready if not InternalFlag.REMOTE_EVAL: # single process mode for eval_policy in eval_policies: eval_policy.recv_weights_local(state_dict) return refs = [eval_policy.recv_weights.remote(metadata_by_dtype) for eval_policy in eval_policies] # 3. Send weights from trainer. The corresponding receive logic is on the policy. if self.buffer_strategy in ["Double", "Shared"]: self._send_weights_full(params_by_dtype) elif self.buffer_strategy == "Sharded": self._send_weights_sharded(params_by_dtype) else: raise ValueError(f"Invalid buffer strategy: {self.buffer_strategy}") ray.get(refs)
[docs] def recv_weights(self, weight_buffer, metadata_by_dtype): """ Receive weights from the train policy, grouped by dtype. Delegates the buffering and application logic to the configured WeightBuffer. This method is called by policies that do NOT use the shared buffer actor. """ if self.buffer_strategy in ["Double", "Shared"]: self._recv_weights_full(weight_buffer, metadata_by_dtype) elif self.buffer_strategy == "Sharded": self._recv_weights_sharded(weight_buffer, metadata_by_dtype) else: raise ValueError(f"Invalid buffer strategy: {self.buffer_strategy}")
[docs] def recv_weights_local(self, weight_buffer, state_dict): """ Receive weights directly for single process mode (non-remote). """ if isinstance(weight_buffer, ray.actor.ActorHandle): weight_buffer.clear.remote() else: weight_buffer.clear() if isinstance(weight_buffer, ray.actor.ActorHandle): assert self.buffer_strategy == "Shared", " Only Shared buffer strategy support ray actor" ray.get(weight_buffer.add_state_dict.remote(state_dict)) else: if isinstance(weight_buffer, CPUWeightBuffer): state_dict = tensor_state_dict_to_numpy(state_dict) weight_buffer.add_state_dict(state_dict)
def _send_weights_full(self, params_by_dtype): """ Sends full weight tensors for the double-buffer strategy. """ context_group = CommContext().get_group(ParallelMode.WEIGHT_TRANSFER) # Use the global rank of the sender (train, always at position 0 in the group). src_rank = CommContext().get_ranks_in_group(ParallelMode.WEIGHT_TRANSFER)[0] sorted_dtypes = sorted(list(params_by_dtype.keys()), key=lambda x: str(x)) work_handles = [] for dtype in sorted_dtypes: params = params_by_dtype[dtype] flat_params = torch.nn.utils.parameters_to_vector(params).contiguous() # Use asynchronous broadcast so that trainer and all evals can pipeline the transfers work = torch.distributed.broadcast(flat_params, src=src_rank, group=context_group, async_op=True) work_handles.append(work) # Synchronize all outstanding broadcasts to guarantee completion before we exit. for work in work_handles: work.wait() def _recv_weights_full( self, weight_buffer: Union[WeightBuffer, ActorHandle], metadata_by_dtype: Dict[torch.Tensor, Sequence], ): """Implementation for the double buffer strategy. Receives full weights and applies them directly. Args: weight_buffer (Union[WeightBuffer, ActorHandle]): Weight buffer or an actor of that. metadata_by_dtype (Dict[torch.Tensor, Sequence]): Metadata grouped by dtype for reconstructing flattened tensors back to per-module state dicts. """ context_group = CommContext().get_group(ParallelMode.WEIGHT_TRANSFER) # The sender is always ranks_in_group[0] (train policy's global rank). src_rank = CommContext().get_ranks_in_group(ParallelMode.WEIGHT_TRANSFER)[0] # prevent peak memory usage three times memory of weights if isinstance(weight_buffer, ActorHandle): weight_buffer.clear.remote() else: weight_buffer.clear() sorted_dtypes = sorted(list(metadata_by_dtype.keys()), key=lambda x: str(x)) received_flat_tensors = {} work_handles = {} for dtype in sorted_dtypes: metadata_list = metadata_by_dtype[dtype] numel_total = sum(m["numel"] for m in metadata_list) flat_weights = torch.empty(numel_total, dtype=dtype, device="cuda") # Asynchronous receive side of broadcast work = torch.distributed.broadcast(flat_weights, src=src_rank, group=context_group, async_op=True) received_flat_tensors[dtype] = flat_weights work_handles[dtype] = work # Wait for all broadcasts to finish for work in work_handles.values(): work.wait() # Add state dict to weight buffer if isinstance(weight_buffer, ActorHandle): assert self.buffer_strategy == "Shared", " Only Shared buffer strategy support ray actor" state_dict = self._flat_tensor_to_state_dict(received_flat_tensors, metadata_by_dtype, to_numpy=True) state_dict = ray.put(state_dict) ray.get(weight_buffer.add_state_dict.remote(state_dict)) else: to_numpy = isinstance(weight_buffer, CPUWeightBuffer) state_dict = self._flat_tensor_to_state_dict(received_flat_tensors, metadata_by_dtype, to_numpy=to_numpy) weight_buffer.add_state_dict(state_dict) def _send_weights_sharded(self, params_by_dtype): """ Sends sharded weights for the sharded-buffer strategy. """ context_group = CommContext().get_group(ParallelMode.WEIGHT_TRANSFER) # The sender (train) is always at position 0 in the WEIGHT_TRANSFER ranks list. src_rank = CommContext().get_ranks_in_group(ParallelMode.WEIGHT_TRANSFER)[0] total_world_size = CommContext().get_world_size(ParallelMode.WEIGHT_TRANSFER) eval_world_size = total_world_size - 1 sorted_dtypes = sorted(list(params_by_dtype.keys()), key=lambda x: str(x)) for dtype in sorted_dtypes: params = params_by_dtype[dtype] flat_params = torch.nn.utils.parameters_to_vector(params) # Pad tensor to be divisible by eval_world_size(只考虑eval policies) numel = flat_params.numel() padded_numel = numel if padded_numel % eval_world_size != 0: padding_size = eval_world_size - (padded_numel % eval_world_size) padding = torch.zeros(padding_size, dtype=flat_params.dtype, device=flat_params.device) flat_params = torch.cat([flat_params, padding]) # 为eval policies创建分片 eval_param_shards = list(flat_params.chunk(eval_world_size, dim=0)) # 为源rank 0(train policy)创建一个dummy分片 这个分片不会被使用,但scatter操作需要它 dummy_shard = torch.empty_like(eval_param_shards[0]) # 构建完整的scatter_list:[train_shard, eval_shard1, eval_shard2, ...] param_shards = [dummy_shard] + eval_param_shards output_tensor = torch.empty_like(dummy_shard) # 执行scatter操作 torch.distributed.scatter( tensor=output_tensor, scatter_list=param_shards, src=src_rank, group=context_group, async_op=False, ) def _recv_weights_sharded(self, weight_buffer, metadata_by_dtype): """ Implementation for the sharded buffer strategy. Receives only a slice of the weights from the trainer. """ context_group = CommContext().get_group(ParallelMode.WEIGHT_TRANSFER) # prevent peak memory usage three times memory of weights weight_buffer.clear() # The trainer is always at position 0 in the WEIGHT_TRANSFER ranks list. trainer_rank = CommContext().get_ranks_in_group(ParallelMode.WEIGHT_TRANSFER)[0] total_world_size = CommContext().get_world_size(ParallelMode.WEIGHT_TRANSFER) eval_world_size = total_world_size - 1 # 只有eval policies参与权重分片 sorted_dtypes = sorted(list(metadata_by_dtype.keys()), key=lambda x: str(x)) for dtype in sorted_dtypes: is_last = dtype == sorted_dtypes[-1] metadata_list = metadata_by_dtype[dtype] numel_total = sum(m["numel"] for m in metadata_list) # Pad to be divisible by eval_world_size(只考虑eval policies) padded_numel_total = numel_total if padded_numel_total % eval_world_size != 0: padded_numel_total += eval_world_size - (padded_numel_total % eval_world_size) numel_shard = padded_numel_total // eval_world_size shard_tensor = torch.empty(numel_shard, dtype=dtype, device="cuda") # 接收端不需要提供scatter_list torch.distributed.scatter( tensor=shard_tensor, scatter_list=None, src=trainer_rank, group=context_group, async_op=False, ) weight_buffer.add_shard(dtype, shard_tensor, metadata_list, numel_total, is_last)