Files
ray-project--ray/release/llm_tests/serve/rlhf_utils.py
T
2026-07-13 13:17:40 +08:00

181 lines
6.5 KiB
Python

import torch
import ray
import requests
from typing import Optional
from transformers import AutoModelForCausalLM
def stateless_init_process_group(master_address, master_port, rank, world_size, device):
"""Create a stateless process group for NCCL communication.
vLLM provides StatelessProcessGroup to create a process group
without considering the global process group in torch.distributed.
"""
from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator
from vllm.distributed.utils import StatelessProcessGroup
pg = StatelessProcessGroup.create(
host=master_address, port=master_port, rank=rank, world_size=world_size
)
pynccl = PyNcclCommunicator(pg, device=device)
return pynccl
class WorkerExtension:
"""Extension class for vLLM workers to enable weight updates.
This class is inherited by vLLM workers when worker_extension_cls is set.
It provides methods for initializing NCCL process groups and receiving
weight updates from an external trainer.
"""
def init_weight_update_group(
self, master_address, master_port, rank_offset, world_size
):
"""Initialize the NCCL process group for weight synchronization."""
from vllm.distributed.parallel_state import get_world_group
rank = get_world_group().rank + rank_offset
self.model_update_group = stateless_init_process_group(
master_address,
master_port,
rank,
world_size,
self.device,
)
def update_weight(self, name, dtype_name, shape):
"""Receive a weight tensor broadcast from the trainer."""
dtype = getattr(torch, dtype_name)
weight = torch.empty(shape, dtype=dtype, device="cuda")
self.model_update_group.broadcast(
weight, src=0, stream=torch.cuda.current_stream()
)
self.model_runner.model.load_weights(weights=[(name, weight)])
del weight
def check_weights_changed(self):
"""Check if weights have been updated to zero (for testing)."""
weights_updated = True
for name, p in self.model_runner.model.named_parameters():
weights_updated = weights_updated and torch.allclose(p, torch.zeros_like(p))
return weights_updated
@ray.remote(num_gpus=1)
class TrainerActor:
"""Simulates a trainer that updates model weights via RLHF.
This actor:
1. Loads the same model as the inference engine
2. Sets up an NCCL process group with all inference workers
3. Broadcasts weight updates to all workers
"""
def __init__(self, model_id: str, base_url: str):
self.model_id = model_id
self._base_url = base_url
self.weight_sync_group = None
self.model = AutoModelForCausalLM.from_pretrained(model_id)
self.model.to("cuda:0")
def setup_weight_sync_group(
self,
tp_size: int,
num_replicas: int,
):
"""Set up the NCCL process group between trainer and inference workers.
Args:
tp_size: Tensor parallel size of each replica
num_replicas: Number of inference replicas
"""
import concurrent.futures
from vllm.utils.network_utils import get_ip, get_open_port
# World size = 1 trainer + (tp_size * num_replicas) inference workers
world_size = 1 + (tp_size * num_replicas)
rank_offset = 1 # Inference workers start at rank 1
master_address = get_ip()
master_port = get_open_port()
print(
f"Setting up weight sync group: master={master_address}:{master_port}, "
f"world_size={world_size}"
)
# Use ThreadPoolExecutor to run both operations concurrently
# One thread calls the HTTP endpoint, another initializes local NCCL
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
# Start HTTP call to init weight update group on inference workers
http_future = executor.submit(
self._call_collective_rpc_sync,
"init_weight_update_group",
[master_address, master_port, rank_offset, world_size],
)
# Initialize trainer's side of the process group (rank 0)
nccl_future = executor.submit(
stateless_init_process_group,
master_address,
master_port,
0,
world_size,
torch.device("cuda:0"),
)
# Wait for both to complete
self.weight_sync_group = nccl_future.result(timeout=120)
http_result = http_future.result(timeout=120)
print(f"Weight sync group initialized. HTTP response: {http_result}")
def update_weights(self):
"""Zero out all weights and broadcast to inference workers.
In a real RLHF loop, this would broadcast the actual trained weights.
For testing, we zero out the weights to verify the sync worked.
"""
import concurrent.futures
# Use a single ThreadPoolExecutor for all parameters to avoid
# creating/destroying many thread pools (one per parameter)
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
for name, p in self.model.named_parameters():
# Zero out weights for testing
p.data.zero_()
dtype_name = str(p.dtype).split(".")[-1]
# Start HTTP call to trigger update_weight on inference workers
http_future = executor.submit(
self._call_collective_rpc_sync,
"update_weight",
[name, dtype_name, list(p.shape)],
)
# Broadcast the tensor via NCCL
self.weight_sync_group.broadcast(
p, src=0, stream=torch.cuda.current_stream()
)
# Wait for HTTP call to complete before next parameter
http_future.result(timeout=60)
# Ensure all NCCL operations have completed
torch.cuda.synchronize()
def _call_collective_rpc_sync(
self, method: str, args: Optional[list] = None, kwargs: Optional[dict] = None
):
"""Call the /collective_rpc endpoint synchronously."""
url = f"{self._base_url}/collective_rpc"
data = {
"model": self.model_id,
"method": method,
"args": args or [],
"kwargs": kwargs or {},
}
response = requests.post(url, json=data, timeout=120)
return response.json()