AsyncTensorRLHF / src /trainer /trainer_worker.py
tahamajs's picture
Initial release of AsyncTensorRLHF: High-Throughput Asynchronous RLHF with Tensor-Native Rewards
e791b16 verified
Raw History Blame Contribute Delete
4.06 kB
# Trainer worker implementation (Ray actor or standalone worker)
from typing import Any, List, Optional
import torch
try:
import ray
ray_remote = ray.remote(num_gpus=1)
except ImportError:
ray = None
def ray_remote(cls):
return cls
from ..buffer.replay_buffer import BoundedReplayBuffer, Experience
from .ppo_loss import compute_ppo_loss, compute_m2po_loss
from .grpo_loss import compute_grpo_loss
@ray_remote
class TrainerWorker:
"""Ray actor or standalone worker that samples experiences and runs PPO/M2PO/GRPO updates.
Arguments
---------
model_path: str or torch.nn.Module, optional
Path to model or model module.
buffer: Any
Shared replay buffer or actor handle.
use_grpo: bool, default False
Whether to train with GRPO loss.
use_m2po: bool, default False
Whether to train with M2PO loss.
device: str, optional
Device to use ('cuda' or 'cpu').
"""
def __init__(
self,
model_path: Optional[Any] = None,
buffer: Optional[Any] = None,
use_grpo: bool = False,
use_m2po: bool = False,
device: Optional[str] = None,
lr: float = 1e-4,
):
self.buffer = buffer
self.use_grpo = use_grpo
self.use_m2po = use_m2po
self.device = torch.device(
device if device is not None else ("cuda" if torch.cuda.is_available() else "cpu")
)
if isinstance(model_path, torch.nn.Module):
self.model = model_path.to(self.device)
else:
self.model = torch.nn.Sequential(
torch.nn.Linear(128, 128),
torch.nn.ReLU(),
torch.nn.Linear(128, 128),
).to(self.device)
self.optimizer = torch.optim.AdamW(self.model.parameters(), lr=lr)
self.version = 0
def step(self, batch_size: int = 64) -> Optional[float]:
"""Perform a single training step."""
if self.buffer is None:
return None
# Sample from buffer (actor handle or local instance)
if hasattr(self.buffer, "sample"):
if hasattr(self.buffer.sample, "remote") and ray is not None:
exps: List[Experience] = ray.get(self.buffer.sample.remote(batch_size))
else:
exps: List[Experience] = self.buffer.sample(batch_size)
else:
return None
if not exps:
return None
# Pad or stack tensors
max_prompt = max(e.prompt_ids.shape[-1] for e in exps)
max_gen = max(e.generated_ids.shape[-1] for e in exps)
padded_old_logprobs = []
padded_advantages = []
for e in exps:
lp = e.log_probs.to(self.device)
if lp.shape[-1] < max_gen:
lp = torch.cat([lp, torch.zeros(max_gen - lp.shape[-1], device=self.device)])
padded_old_logprobs.append(lp)
r = float(e.reward)
padded_advantages.append(torch.full((max_gen,), r, device=self.device))
old_log_probs = torch.stack(padded_old_logprobs)
advantages = torch.stack(padded_advantages)
# Compute simulated policy log probs with small perturbation for optimization
dummy_in = torch.randn(len(exps), 128, device=self.device)
rep = self.model(dummy_in)
policy_log_probs = old_log_probs + 0.01 * rep.mean(dim=-1, keepdim=True)
if self.use_grpo:
loss = compute_grpo_loss(
policy_log_probs.unsqueeze(0),
old_log_probs.unsqueeze(0),
advantages.unsqueeze(0),
)
elif self.use_m2po:
loss = compute_m2po_loss(policy_log_probs, old_log_probs, advantages)
else:
loss = compute_ppo_loss(policy_log_probs, old_log_probs, advantages)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
self.version += 1
return float(loss.item())
def get_version(self) -> int:
return self.version