| |
|
|
| import queue |
| import threading |
| from dataclasses import dataclass |
| from typing import List, Dict |
| import torch |
|
|
|
|
| @dataclass |
| class GroupBufferEntry: |
| prompt_id: int |
| responses: List[torch.Tensor] |
| log_probs: List[torch.Tensor] |
| rewards: List[float] |
| policy_version: int |
| advantages: torch.Tensor = None |
| is_complete: bool = False |
|
|
|
|
| class GroupAwareReplayBuffer: |
| """Buffers groups of G responses per prompt before exposing them to the trainer. |
| |
| - `group_size` defines G. |
| - When a group is complete, advantages are computed and the entry is placed on a ready queue. |
| - Trainer consumes from the ready queue. |
| """ |
|
|
| def __init__(self, group_size: int = 8, max_groups: int = 512): |
| self.group_size = group_size |
| self.pending: Dict[int, GroupBufferEntry] = {} |
| self.ready = queue.Queue(maxsize=max_groups) |
| self.lock = threading.Lock() |
|
|
| def add_response( |
| self, |
| prompt_id: int, |
| response: torch.Tensor, |
| log_prob: torch.Tensor, |
| reward: float, |
| version: int, |
| ): |
| with self.lock: |
| if prompt_id not in self.pending: |
| self.pending[prompt_id] = GroupBufferEntry( |
| prompt_id=prompt_id, |
| responses=[], |
| log_probs=[], |
| rewards=[], |
| policy_version=version, |
| ) |
| entry = self.pending[prompt_id] |
| entry.responses.append(response) |
| entry.log_probs.append(log_prob) |
| entry.rewards.append(reward) |
|
|
| if len(entry.responses) == self.group_size: |
| |
| rewards_tensor = torch.tensor(entry.rewards, device=response.device) |
| mean_r = rewards_tensor.mean() |
| std_r = rewards_tensor.std(unbiased=False) + 1e-8 |
| entry.advantages = (rewards_tensor - mean_r) / std_r |
| entry.is_complete = True |
| |
| try: |
| self.ready.put_nowait(entry) |
| except queue.Full: |
| try: |
| self.ready.get_nowait() |
| except queue.Empty: |
| pass |
| self.ready.put_nowait(entry) |
| del self.pending[prompt_id] |
|
|
| def sample_ready(self) -> GroupBufferEntry: |
| """Retrieve a completed group; returns None if none available.""" |
| try: |
| return self.ready.get_nowait() |
| except queue.Empty: |
| return None |
|
|