Files
2025-11-06 10:59:57 +08:00

155 lines
5.8 KiB
Python

import copy
from typing import Any, Dict
import ray
import ray.util.collective as cc
import torch
import torch.distributed.distributed_c10d as c10d
from packaging.version import Version
def ray_broadcast_object(obj: Any, src: int = 0, device=None, group_name: str = "default") -> Any:
rank = cc.get_rank(group_name)
if rank == src:
if Version(torch.__version__) >= Version("2.3.0"):
obj_tensor, size_tensor = c10d._object_to_tensor(obj, device=device, group=None)
elif Version(torch.__version__) >= Version("1.13.0"):
obj_tensor, size_tensor = c10d._object_to_tensor(obj, device=device)
else:
obj_tensor, size_tensor = c10d._object_to_tensor(obj)
obj_tensor = obj_tensor.to(device)
size_tensor = size_tensor.to(device)
else:
size_tensor = torch.empty(1, dtype=torch.int64, device=device)
cc.broadcast(size_tensor, src, group_name)
if rank != src:
obj_tensor = torch.empty(size_tensor.item(), dtype=torch.uint8, device=device)
cc.broadcast(obj_tensor, src, group_name)
if rank != src:
if Version(torch.__version__) >= Version("2.3.0"):
obj = c10d._tensor_to_object(obj_tensor, size_tensor.item(), group=None)
else:
obj = c10d._tensor_to_object(obj, size_tensor.item())
return obj
def ray_broadcast_tensor_dict(
tensor_dict: Dict[str, torch.Tensor],
src: int = 0,
device=None,
group_name: str = "default",
backend: str = "nccl",
offload_to_cpu: bool = False,
pin_memory: bool = False,
) -> Dict[str, torch.Tensor]:
rank = cc.get_rank(group_name)
if tensor_dict is None:
tensor_dict = {}
if rank == src:
metadata = []
for k, v in tensor_dict.items():
metadata.append((k, v.shape, v.dtype))
else:
metadata = None
metadata = ray_broadcast_object(metadata, src, device, group_name)
for k, shape, dtype in metadata:
if rank == src:
if offload_to_cpu:
tensor = tensor_dict[k].to(device)
else:
tensor = tensor_dict[k]
else:
tensor = tensor_dict.get(k, torch.zeros(shape, dtype=dtype, device=device, pin_memory=pin_memory))
if backend == "gloo" and dtype == torch.bfloat16:
# Gloo does not support bfloat16, convert to float16
tensor = tensor.view(torch.float16)
cc.broadcast(tensor, src, group_name)
if backend == "gloo" and dtype == torch.bfloat16:
# Convert back to bfloat16 if it was converted to float16
tensor = tensor.view(torch.bfloat16)
if rank != src:
if offload_to_cpu:
tensor_dict[k] = tensor.cpu()
else:
tensor_dict[k] = tensor
return tensor_dict
@ray.remote
class SharedVariableActor:
def __init__(self, number_of_readers: int = 0, buffer_size_limit: int = 1000):
self.data_queue = []
self.data_uid = 0
self.number_of_readers = number_of_readers
self.queue_size = 0
self.signals = {}
self.process_locks = {}
self.signal_procs_meet_count = {}
self.buffer_size_limit = buffer_size_limit
def pickup_rollout_task(self, num_tasks: int):
"""
use queue size to control whether producers should generating new rollouts or wait
for consumer to consumer more data. if queue size is less than threshold,
it means consumer is consuming data fast enough, so producers can generate new rollouts.
if queue size is greater than threshold, it means consumer is consuming data slowly,
so producers should wait for consumer to consume more data.
Any free producer can pick up the task to generate rollout then increase the queued_data_size
to prevent other producer to pick up the task redundantly, Note it is not the real
queue length as data may still be generating
"""
ret = False
if self.queue_size < (self.buffer_size_limit / max(0.1, self.signals.get("sample_utilization", 1.0))):
ret = True
self.queue_size += num_tasks
return ret
def append_data(self, data):
self.data_queue.append([self.data_uid, data, 0]) # [data_uid, data, access_count]
self.data_uid += 1
return True
def get_data(self, data_uid: int):
# for multi-process data reading
if not self.data_queue:
# no data in the queue, return None
return None
to_pop_index = None
ret = None
for i, (uid, data, access_count) in enumerate(self.data_queue):
if uid == data_uid:
# found the data with the given uid
self.data_queue[i][2] += 1
ret = copy.deepcopy(data)
if self.data_queue[i][2] == self.number_of_readers:
to_pop_index = i
break
if to_pop_index is not None:
# remove the data from the queue if it has been accessed by all readers
self.data_queue.pop(to_pop_index)
self.queue_size -= data["input_ids"].size(0)
return ret
def acquire_process_lock(self, key: str):
# atomic lock for process
if key not in self.process_locks:
self.process_locks[key] = 1 # locked
return 0
if self.process_locks[key] == 0:
self.process_locks[key] = 1 # lock the process
return 0
else:
return 1
def release_process_lock(self, key: str):
# atomic unlock for process
assert self.process_locks.get(key, 0) == 1, f"Releasing a process lock {key} that is not locked."
self.process_locks[key] = 0
def set_signal(self, key: str, signal: str):
self.signals[key] = signal
def get_signal(self):
return self.signals