Expand to 41 comprehensive tests and add in-depth architectural and theoretical documentation
Browse files- README.md +487 -159
- src/reward/tensor_native.py +1 -1
- tests/test_buffer_stress.py +163 -0
- tests/test_loss_numerical_stability.py +73 -0
- tests/test_orchestrator_pipeline.py +131 -0
- tests/test_reward_edge_cases.py +100 -0
README.md
CHANGED
|
@@ -23,10 +23,11 @@ pipeline_tag: reinforcement-learning
|
|
| 23 |
[](https://www.python.org/downloads/)
|
| 24 |
[](https://pytorch.org/)
|
| 25 |
[](LICENSE)
|
| 26 |
-
[](https://huggingface.co/tahamajs/AsyncTensorRLHF)
|
|
|
|
| 28 |
|
| 29 |
-
**High-Throughput Asynchronous Reinforcement Learning from Human Feedback (RLHF) with Tensor-Native Rewards & Second-Moment Off-Policy Control (M2PO / GRPO)**
|
| 30 |
|
| 31 |
</div>
|
| 32 |
|
|
@@ -34,257 +35,584 @@ pipeline_tag: reinforcement-learning
|
|
| 34 |
|
| 35 |
## Table of Contents
|
| 36 |
|
| 37 |
-
- [1. Executive Summary](#1-executive-summary)
|
| 38 |
-
- [
|
| 39 |
-
- [
|
| 40 |
-
|
| 41 |
-
- [
|
| 42 |
-
- [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
- [4. Repository Structure](#4-repository-structure)
|
| 44 |
-
- [5.
|
| 45 |
-
- [
|
| 46 |
-
- [
|
| 47 |
-
- [
|
| 48 |
-
- [
|
| 49 |
-
- [
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
- [
|
| 53 |
-
- [
|
| 54 |
-
- [
|
| 55 |
-
- [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
- [12. License](#12-license)
|
| 57 |
|
| 58 |
---
|
| 59 |
|
| 60 |
-
## 1. Executive Summary
|
| 61 |
|
| 62 |
-
|
| 63 |
-
1. **CPU-GPU Transfer Bottlenecks**: Tokenized sequences are transferred from GPU VRAM to CPU host memory for token decoding, string regex matching, or rule checking, and then copied back to GPU memory for reward assignment.
|
| 64 |
-
2. **Synchronous Lockstep Latency**: Traditional PPO systems force rollout workers to wait for the trainer to complete gradient updates, resulting in severe GPU underutilization (bubble overhead exceeding 40–60%).
|
| 65 |
|
| 66 |
-
|
| 67 |
-
- **Zero-Copy In-VRAM Reward Computation**: Rewards are computed directly on generated token ID tensors using parallel sliding-window convolutions and pattern matches without string decoding or CPU roundtrips.
|
| 68 |
-
- **Asynchronous Continuous Rollout**: Rollout workers continuously stream generations into bounded, version-aware replay buffers while the trainer samples off-policy trajectories with theoretical bounded staleness guarantees (M2PO / GRPO).
|
| 69 |
|
| 70 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
-
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
- Scans for EOS delimiters across batches in parallel.
|
| 77 |
-
- Utilizes `.unfold()` sliding-window tensor operations to locate answer patterns before sequence termination.
|
| 78 |
-
- Achieves sub-millisecond execution (e.g. **55 ms for batch size 64 with sequence length 256** directly on GPU).
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
-
|
| 83 |
-
-
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
---
|
| 93 |
|
| 94 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
```
|
| 97 |
-
|
| 98 |
-
│
|
| 99 |
-
│
|
| 100 |
-
│
|
| 101 |
-
│
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
│ – BoundedReplayBuffer (Thread-safe lockless FIFO) │
|
| 116 |
-
│ – VersionedReplayBuffer (Dynamic staleness eviction) │
|
| 117 |
-
│ – GroupAwareReplayBuffer (GRPO group advantage norm) │
|
| 118 |
-
└────────────────────────────────────────────────────────┘
|
| 119 |
```
|
| 120 |
|
| 121 |
---
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
## 4. Repository Structure
|
| 124 |
|
| 125 |
```
|
| 126 |
AsyncTensorRLHF/
|
|
|
|
|
|
|
|
|
|
| 127 |
├── configs/
|
| 128 |
-
│ ├── phase1_sync.yaml # Baseline synchronous
|
| 129 |
-
│ └── phase2_async.yaml #
|
| 130 |
-
├── requirements.txt # Core framework dependencies
|
| 131 |
├── scripts/
|
| 132 |
-
│ ├── launch_orchestrator.sh # Ray orchestrator
|
| 133 |
-
│ ├── launch_rollout.sh #
|
| 134 |
-
│ ├── launch_trainer.sh #
|
| 135 |
-
│ └── verify_gpu.py #
|
| 136 |
├── src/
|
|
|
|
| 137 |
│ ├── buffer/
|
| 138 |
-
│ │ ├──
|
| 139 |
-
│ │ ├──
|
|
|
|
| 140 |
│ │ └── replay_buffer.py # BoundedReplayBuffer & VersionedReplayBuffer
|
| 141 |
│ ├── orchestrator/
|
| 142 |
-
│ │ ├──
|
| 143 |
-
│ │
|
|
|
|
| 144 |
│ ├── reward/
|
| 145 |
-
│ │
|
|
|
|
| 146 |
│ ├── rollout/
|
| 147 |
-
│ │ ├──
|
| 148 |
-
│ │ ├──
|
| 149 |
-
│ │ ├──
|
| 150 |
-
│ │
|
|
|
|
| 151 |
│ └── trainer/
|
|
|
|
| 152 |
│ ├── grpo_loss.py # Group Relative Policy Optimization loss
|
| 153 |
-
│ ├── ppo_loss.py # PPO loss
|
| 154 |
-
│ └── trainer_worker.py #
|
| 155 |
└── tests/
|
|
|
|
| 156 |
├── test_phase1.py # Phase 1: Reward, replay buffer, PPO loss
|
| 157 |
├── test_phase2.py # Phase 2: M2PO loss, group buffer, staleness
|
| 158 |
-
├── test_phase3.py # Phase 3: Async
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
```
|
| 161 |
|
| 162 |
---
|
| 163 |
|
| 164 |
-
## 5.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
-
###
|
| 171 |
-
In asynchronous regimes where policy drift $\theta - \theta_{\text{old}}$ increases due to rollout latency, M2PO bounds the second moment of the importance weight:
|
| 172 |
-
$$M_2 = \mathbb{E}\left[ r_t(\theta)^2 \right] \le \gamma$$
|
| 173 |
-
Trajectories with $r_t(\theta)^2 > \gamma$ are masked out, stabilizing gradient updates without dropping sample efficiency.
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
---
|
| 181 |
|
| 182 |
-
## 6. Installation
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
```bash
|
| 185 |
-
# Clone the repository
|
| 186 |
git clone https://github.com/Hooshaai/AsyncTensorRLHF.git
|
| 187 |
cd AsyncTensorRLHF
|
| 188 |
|
| 189 |
-
# Install dependencies
|
| 190 |
pip install -r requirements.txt
|
| 191 |
```
|
| 192 |
|
| 193 |
---
|
| 194 |
|
| 195 |
-
## 7.
|
| 196 |
|
| 197 |
-
###
|
| 198 |
|
| 199 |
-
Run
|
| 200 |
|
| 201 |
```bash
|
| 202 |
python3 -m pytest tests/ -v
|
| 203 |
```
|
| 204 |
|
| 205 |
-
|
| 206 |
```
|
| 207 |
-
=============================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
```
|
| 209 |
|
| 210 |
-
###
|
| 211 |
|
| 212 |
-
Run the
|
| 213 |
|
| 214 |
```bash
|
| 215 |
python scripts/verify_gpu.py
|
| 216 |
```
|
| 217 |
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
---
|
| 227 |
|
| 228 |
-
##
|
| 229 |
|
| 230 |
-
|
| 231 |
|
| 232 |
-
|
|
| 233 |
|---|---|---|---|
|
| 234 |
-
|
|
| 235 |
-
|
|
| 236 |
-
|
|
| 237 |
-
|
|
| 238 |
-
|
|
| 239 |
-
|
|
| 240 |
-
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
---
|
| 243 |
|
| 244 |
-
##
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
| `test_phase1.py` | `tensor_native_reward`, `BoundedReplayBuffer`, `compute_ppo_loss` | Pass |
|
| 249 |
-
| `test_phase2.py` | `compute_m2po_loss`, `GroupAwareReplayBuffer`, `VersionedReplayBuffer` | Pass |
|
| 250 |
-
| `test_phase3.py` | `StubEngine` async generation, concurrent gather, `VersionManager` | Pass |
|
| 251 |
-
| `test_e2e.py` | Autoregressive `HFEngine`, device-native rollout, closed-loop orchestrator | Pass |
|
| 252 |
|
| 253 |
-
-
|
|
|
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
sync_interval: 10
|
| 261 |
-
buffer_capacity: 50000
|
| 262 |
-
max_staleness: 5
|
| 263 |
-
|
| 264 |
-
rollout:
|
| 265 |
-
backend: "vllm" # Fallback to "hf" or "stub" automatically
|
| 266 |
-
batch_size: 64
|
| 267 |
-
max_new_tokens: 512
|
| 268 |
-
|
| 269 |
-
trainer:
|
| 270 |
-
loss_type: "m2po" # "ppo", "m2po", or "grpo"
|
| 271 |
-
learning_rate: 1.0e-5
|
| 272 |
-
clip_eps: 0.2
|
| 273 |
-
m2_threshold: 2.0
|
| 274 |
-
```
|
| 275 |
|
| 276 |
---
|
| 277 |
|
| 278 |
-
## 11. Citation
|
| 279 |
|
| 280 |
-
If you
|
| 281 |
|
| 282 |
```bibtex
|
| 283 |
@software{asynctensorrlhf2026,
|
| 284 |
author = {Taha Majs and contributors},
|
| 285 |
-
title = {AsyncTensorRLHF: High-Throughput Asynchronous
|
| 286 |
year = {2026},
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
| 288 |
}
|
| 289 |
```
|
| 290 |
|
|
@@ -292,4 +620,4 @@ If you use AsyncTensorRLHF in your research, please cite:
|
|
| 292 |
|
| 293 |
## 12. License
|
| 294 |
|
| 295 |
-
This project is licensed under the Apache 2.0
|
|
|
|
| 23 |
[](https://www.python.org/downloads/)
|
| 24 |
[](https://pytorch.org/)
|
| 25 |
[](LICENSE)
|
| 26 |
+
[](tests/)
|
| 27 |
[](https://huggingface.co/tahamajs/AsyncTensorRLHF)
|
| 28 |
+
[](https://github.com/Hooshaai/AsyncTensorRLHF)
|
| 29 |
|
| 30 |
+
**High-Throughput Asynchronous Reinforcement Learning from Human Feedback (RLHF) with In-VRAM Tensor-Native Rewards & Second-Moment Off-Policy Control (M2PO / GRPO)**
|
| 31 |
|
| 32 |
</div>
|
| 33 |
|
|
|
|
| 35 |
|
| 36 |
## Table of Contents
|
| 37 |
|
| 38 |
+
- [1. Executive Summary & Problem Formulation](#1-executive-summary--problem-formulation)
|
| 39 |
+
- [The Traditional Synchronous RLHF Bottleneck](#the-traditional-synchronous-rlhf-bottleneck)
|
| 40 |
+
- [The AsyncTensorRLHF Paradigm](#the-asynctensorrlhf-paradigm)
|
| 41 |
+
- [2. System Architecture & Data Flow](#2-system-architecture--data-flow)
|
| 42 |
+
- [Global Component Diagram](#global-component-diagram)
|
| 43 |
+
- [Asynchronous Sequence Diagram](#asynchronous-sequence-diagram)
|
| 44 |
+
- [Zero-Copy In-VRAM vs. Traditional Host-Device Roundtrip](#zero-copy-in-vram-vs-traditional-host-device-roundtrip)
|
| 45 |
+
- [3. Theoretical Foundations & Mathematical Formulations](#3-theoretical-foundations--mathematical-formulations)
|
| 46 |
+
- [3.1 Policy Gradient under Asynchronous Staleness $\tau$](#31-policy-gradient-under-asynchronous-staleness-tau)
|
| 47 |
+
- [3.2 Proximal Policy Optimization (PPO)](#32-proximal-policy-optimization-ppo)
|
| 48 |
+
- [3.3 Second-Moment Trust Region Optimization (M2PO)](#33-second-moment-trust-region-optimization-m2po)
|
| 49 |
+
- [3.4 Group Relative Policy Optimization (GRPO)](#34-group-relative-policy-optimization-grpo)
|
| 50 |
- [4. Repository Structure](#4-repository-structure)
|
| 51 |
+
- [5. Component Walkthrough & Code Deep Dive](#5-component-walkthrough--code-deep-dive)
|
| 52 |
+
- [5.1 Tensor-Native Reward Engine (`src/reward/`)](#51-tensor-native-reward-engine-srcreward)
|
| 53 |
+
- [5.2 Experience Replay Subsystems (`src/buffer/`)](#52-experience-replay-subsystems-srcbuffer)
|
| 54 |
+
- [5.3 Asynchronous Rollout Engines (`src/rollout/`)](#53-asynchronous-rollout-engines-srcrollout)
|
| 55 |
+
- [5.4 Distributed Trainer Workers (`src/trainer/`)](#54-distributed-trainer-workers-srctrainer)
|
| 56 |
+
- [5.5 Orchestration & Version Management (`src/orchestrator/`)](#55-orchestration--version-management-srcorchestrator)
|
| 57 |
+
- [6. Installation & Environment Setup](#6-installation--environment-setup)
|
| 58 |
+
- [7. Verification & Benchmarking](#7-verification--benchmarking)
|
| 59 |
+
- [7.1 Running the 41-Test Comprehensive Suite](#71-running-the-41-test-comprehensive-suite)
|
| 60 |
+
- [7.2 Hardware Benchmarks on NVIDIA RTX 4070 GPU](#72-hardware-benchmarks-on-nvidia-rtx-4070-gpu)
|
| 61 |
+
- [8. Developer Cookbook: Extending the Framework](#8-developer-cookbook-extending-the-framework)
|
| 62 |
+
- [Recipe 1: Integrating a Real Hugging Face LLM (e.g. Qwen / Llama)](#recipe-1-integrating-a-real-hugging-face-llm-eg-qwen--llama)
|
| 63 |
+
- [Recipe 2: Implementing Custom In-VRAM Reward Logic](#recipe-2-implementing-custom-in-vram-reward-logic)
|
| 64 |
+
- [Recipe 3: Distributed Multi-GPU Execution with Ray](#recipe-3-distributed-multi-gpu-execution-with-ray)
|
| 65 |
+
- [9. Configuration Dictionary](#9-configuration-dictionary)
|
| 66 |
+
- [10. Frequently Asked Questions (FAQ) & Troubleshooting](#10-frequently-asked-questions-faq--troubleshooting)
|
| 67 |
+
- [11. Citation & BibTeX](#11-citation--bibtex)
|
| 68 |
- [12. License](#12-license)
|
| 69 |
|
| 70 |
---
|
| 71 |
|
| 72 |
+
## 1. Executive Summary & Problem Formulation
|
| 73 |
|
| 74 |
+
### The Traditional Synchronous RLHF Bottleneck
|
|
|
|
|
|
|
| 75 |
|
| 76 |
+
Reinforcement Learning from Human Feedback (RLHF) has emerged as the standard paradigm for aligning large language models (LLMs). However, standard frameworks (such as conventional DeepSpeed-Chat, early TRL, and synchronous PPO pipelines) suffer from two fundamental performance ceilings:
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
1. **The CPU-GPU Memory Wall (SerDes Overhead)**:
|
| 79 |
+
Rollout generates token sequences on the GPU. Standard reward computation then:
|
| 80 |
+
- Copies generated token IDs across the PCIe bus to CPU memory (`.cpu()`).
|
| 81 |
+
- Decodes IDs into UTF-8 strings (`tokenizer.decode()`).
|
| 82 |
+
- Runs Python string matching, regular expressions, or rule-based scoring on the host CPU.
|
| 83 |
+
- Converts scalar scores back into PyTorch tensors and transfers them across PCIe back into GPU memory (`.cuda()`).
|
| 84 |
+
In high-throughput generation regimes (batch size $\ge 64$, sequence length $\ge 1024$), CPU serialization and PCIe roundtrips introduce severe throughput degradation, consuming up to 30–50% of the entire pipeline duration.
|
| 85 |
+
|
| 86 |
+
2. **The Synchronous Lockstep Barrier (GPU Underutilization)**:
|
| 87 |
+
In synchronous PPO, rollout generation and trainer parameter optimization run in strict lockstep:
|
| 88 |
+
$$\text{Rollout}(\pi_{\theta_t}) \longrightarrow \text{Reward Evaluation} \longrightarrow \text{Train Step}(\theta_{t+1}) \longrightarrow \text{Wait for Rollout}$$
|
| 89 |
+
While the trainer runs backpropagation, rollout GPU workers sit completely idle. Conversely, while rollout workers generate tokens autoregressively, training GPUs idle waiting for batches. This lockstep barrier causes severe GPU idle time ("bubble overhead"), frequently exceeding 40–60% of total cluster compute time.
|
| 90 |
+
|
| 91 |
+
```
|
| 92 |
+
Synchronous Lockstep Bubble:
|
| 93 |
+
Rollout GPU: [====== ROLLOUT ======] [...... IDLE ......] [====== ROLLOUT ======]
|
| 94 |
+
Trainer GPU: [...... IDLE ......] [==== TRAIN ====] [...... IDLE ......] [==== TRAIN ====]
|
| 95 |
+
▲ ▲
|
| 96 |
+
Bubble Waste Bubble Waste
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
### The AsyncTensorRLHF Paradigm
|
| 100 |
|
| 101 |
+
**AsyncTensorRLHF** eliminates both synchronization bottlenecks through architectural disaggregation:
|
| 102 |
|
| 103 |
+
1. **Zero-Copy In-VRAM Tensor-Native Rewards**:
|
| 104 |
+
All reward computations are executed entirely within GPU memory on `torch.Tensor` structures using parallel 1D sliding-window convolutions (`.unfold()`) and tensor operations. No CPU string decoding, no UTF-8 serialization, and zero host-device bus transfers occur during reward assignment.
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
+
2. **Asynchronous Continuous Rollout with Second-Moment Staleness Control (M2PO)**:
|
| 107 |
+
Rollout workers continuously generate responses into a non-blocking, thread-safe experience replay buffer. The trainer continuously samples from the buffer and optimizes the policy. To handle the resulting off-policy divergence $\theta - \theta_{\text{old}}$, the framework incorporates:
|
| 108 |
+
- Dynamic staleness eviction: Experiences with age $\tau = v_{\text{current}} - v_{\text{data}} > \tau_{\text{max}}$ are immediately discarded.
|
| 109 |
+
- M2PO Second-Moment Trust Region Loss: Dynamically bounds the second moment of the importance weight $\mathbb{E}[r^2(\theta)]$, preventing policy collapse under asynchronous drift.
|
| 110 |
+
- Group-Aware Buffers for GRPO: Standardizes advantage estimates across groups of candidate generations per prompt.
|
| 111 |
|
| 112 |
+
```
|
| 113 |
+
AsyncTensorRLHF Fully Disaggregated Flow:
|
| 114 |
+
Rollout GPU: [== ROLLOUT ==][== ROLLOUT ==][== ROLLOUT ==][== ROLLOUT ==][== ROLLOUT ==] 100% UTILIZED
|
| 115 |
+
│ │ │ │ │
|
| 116 |
+
▼ ▼ ▼ ▼ ▼
|
| 117 |
+
Replay Buffer: [ Exp (v=0) ] [ Exp (v=1) ] [ Exp (v=1) ] [ Exp (v=2) ] [ Exp (v=3) ] Non-blocking
|
| 118 |
+
│ │ │ │ │
|
| 119 |
+
▼ ▼ ▼ ▼ ▼
|
| 120 |
+
Trainer GPU: [== TRAIN ==][== TRAIN ==][== TRAIN ==][== TRAIN ==][== TRAIN ==][== TRAIN ==] 100% UTILIZED
|
| 121 |
+
```
|
| 122 |
|
| 123 |
---
|
| 124 |
|
| 125 |
+
## 2. System Architecture & Data Flow
|
| 126 |
+
|
| 127 |
+
### Global Component Diagram
|
| 128 |
+
|
| 129 |
+
The following diagram illustrates the four core subsystems and their interaction channels:
|
| 130 |
+
|
| 131 |
+
```
|
| 132 |
+
┌────────────────────────────────────────────────────────────────────────────────────────┐
|
| 133 |
+
│ Orchestrator Control Plane │
|
| 134 |
+
│ │
|
| 135 |
+
│ ┌───────────────────────────┐ ┌─────────────────────────────┐ │
|
| 136 |
+
│ │ PromptQueue (Asyncio) │ │ VersionManager │ │
|
| 137 |
+
│ │ – Non-blocking put/get │ │ – Current version: v │ │
|
| 138 |
+
│ │ – Dynamic task feeder │ │ – Staleness: tau = v - v_e │ │
|
| 139 |
+
│ └─────────────┬─────────────┘ └──────────────┬──────────────┘ │
|
| 140 |
+
└─────────────────┼───────────────────────────────────────────────────┼──────────────────┘
|
| 141 |
+
│ │
|
| 142 |
+
▼ ▼
|
| 143 |
+
┌──────────────────────────────────────┐ ┌────────────────────────────────────┐
|
| 144 |
+
│ Rollout Inference Cluster │ │ Trainer Worker Subsystem │
|
| 145 |
+
│ │ │ │
|
| 146 |
+
│ ┌──────────────────────────────┐ │ │ ┌────────────────────────────┐ │
|
| 147 |
+
│ │ AsyncEngine │ │ │ │ TrainerWorker │ │
|
| 148 |
+
│ │ – HFEngine (AutoRegressive)│ │ │ │ – Device-aware CUDA/CPU │ │
|
| 149 |
+
│ │ – VLLMEngineWrapper │ │ │ │ – AdamW Optimizer │ │
|
| 150 |
+
│ │ – StubEngine (Test mock) │ │ │ │ – Gradient clipping │ │
|
| 151 |
+
│ └──────────────┬───────────────┘ │ │ └─────────────┬──────────────┘ │
|
| 152 |
+
│ │ │ │ │ │
|
| 153 |
+
│ ▼ │ │ ▼ │
|
| 154 |
+
│ ┌──────────────────────────────┐ │ │ ┌────────────────────────────┐ │
|
| 155 |
+
│ │ Tensor-Native Reward │ │ │ │ Loss Functions │ │
|
| 156 |
+
│ │ – gpu_reward_simple() │ │ │ │ – compute_ppo_loss() │ │
|
| 157 |
+
│ │ – tensor_native_reward() │ │ │ │ – compute_m2po_loss() │ │
|
| 158 |
+
│ │ – 100% GPU VRAM execution │ │ │ │ – compute_grpo_loss() │ │
|
| 159 |
+
│ └──────────────┬───────────────┘ │ │ └─────────────▲──────────────┘ │
|
| 160 |
+
└──────────────────┼───────────────────┘ └─────────────────┼──────────────────┘
|
| 161 |
+
│ │
|
| 162 |
+
▼ │
|
| 163 |
+
┌─────────────────────────────────────────────────────────────────────┴──────────────────┐
|
| 164 |
+
│ Experience Replay Buffer Layer │
|
| 165 |
+
│ │
|
| 166 |
+
│ ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
| 167 |
+
│ │ BoundedReplayBuffer │ │ VersionedReplayBuffer │ │ GroupAwareReplayBuffer │ │
|
| 168 |
+
│ │ – Thread-safe Lock │ │ – Staleness filter │ │ – G responses / prompt │ │
|
| 169 |
+
│ │ – FIFO overflow drop │ │ – Dynamic age pruning │ │ – Advantage std norm │ │
|
| 170 |
+
│ └─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘ │
|
| 171 |
+
└────────────────────────────────────────────────────────────────────────────────────────┘
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
### Asynchronous Sequence Diagram
|
| 175 |
|
| 176 |
```
|
| 177 |
+
Rollout Engine Buffer Trainer VersionManager
|
| 178 |
+
│ │ │ │
|
| 179 |
+
│── 1. Generate Tokens ─►│ │ │
|
| 180 |
+
│ (In-VRAM autoreg) │ │ │
|
| 181 |
+
│── 2. Compute Reward ──►│ │ │
|
| 182 |
+
│ (In-VRAM .unfold()) │ │ │
|
| 183 |
+
│── 3. Push Exp (v=0) ──►│ │ │
|
| 184 |
+
│ │ │ │
|
| 185 |
+
│ │◄─ 4. Sample Batch ─│ │
|
| 186 |
+
│ │ (Batch size B)│ │
|
| 187 |
+
│ │ │── 5. Forward/Loss ─►│
|
| 188 |
+
│ │ │ (PPO/M2PO) │
|
| 189 |
+
│ │ │── 6. AdamW Step ───►│
|
| 190 |
+
│ │ │ │
|
| 191 |
+
│ │ │── 7. Bump Version ─►│ (v=1)
|
| 192 |
+
│◄─────── 8. Sync Weights (Async Background) ─│ │
|
| 193 |
+
│ │ │ │
|
| 194 |
+
│── 9. Push Exp (v=1) ──►│ │ │
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
```
|
| 196 |
|
| 197 |
---
|
| 198 |
|
| 199 |
+
## 3. Theoretical Foundations & Mathematical Formulations
|
| 200 |
+
|
| 201 |
+
### 3.1 Policy Gradient under Asynchronous Staleness $\tau$
|
| 202 |
+
|
| 203 |
+
In a distributed asynchronous RLHF pipeline, an experience tuple $(x, y, r, \log \pi_{\theta_{\text{old}}}(y|x))$ collected at policy version $\theta_{\text{old}}$ is consumed by the trainer at parameter version $\theta_{\text{current}}$, where $\tau = \text{version}(\theta_{\text{current}}) - \text{version}(\theta_{\text{old}}) \ge 0$.
|
| 204 |
+
|
| 205 |
+
The policy gradient is:
|
| 206 |
+
$$g(\theta) = \mathbb{E}_{(x,y) \sim \mathcal{D}}\left[ \frac{\nabla_\theta \pi_\theta(y|x)}{\pi_{\theta_{\text{old}}}(y|x)} A^{\pi_{\theta_{\text{old}}}}(x, y) \right]$$
|
| 207 |
+
|
| 208 |
+
When staleness $\tau > 0$, the importance sampling weight $r_t(\theta) = \frac{\pi_\theta(y_t | x, y_{<t})}{\pi_{\theta_{\text{old}}}(y_t | x, y_{<t})}$ exhibits high variance:
|
| 209 |
+
$$\text{Var}_{y \sim \pi_{\theta_{\text{old}}}}[r_t(\theta)] \approx \exp\left( D_{\chi^2}(\pi_\theta \parallel \pi_{\theta_{\text{old}}}) \right) - 1$$
|
| 210 |
+
|
| 211 |
+
If $\tau$ grows without constraint, standard PPO clipping $\text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon)$ saturates, causing vanishing gradient updates on fresh tokens and destructive updates on stale outliers.
|
| 212 |
+
|
| 213 |
+
### 3.2 Proximal Policy Optimization (PPO)
|
| 214 |
+
|
| 215 |
+
AsyncTensorRLHF implements clipped PPO with per-token importance weighting:
|
| 216 |
+
|
| 217 |
+
$$\mathcal{L}_{\text{PPO}}(\theta) = -\frac{1}{B \cdot L} \sum_{b=1}^B \sum_{t=1}^L \min\left( r_{b,t}(\theta) A_{b,t}, \; \text{clip}(r_{b,t}(\theta), 1-\epsilon, 1+\epsilon) A_{b,t} \right)$$
|
| 218 |
+
|
| 219 |
+
where:
|
| 220 |
+
$$r_{b,t}(\theta) = \exp\left( \log \pi_\theta(y_{b,t} | x_b, y_{b,<t}) - \log \pi_{\theta_{\text{old}}}(y_{b,t} | x_b, y_{b,<t}) \right)$$
|
| 221 |
+
|
| 222 |
+
### 3.3 Second-Moment Trust Region Optimization (M2PO)
|
| 223 |
+
|
| 224 |
+
To guarantee stability under asynchronous rollout where $\tau \in [1, \tau_{\text{max}}]$, AsyncTensorRLHF incorporates **M2PO** (Second-Moment Trust Region Policy Optimization). M2PO constrains the empirical second moment of the importance weight:
|
| 225 |
+
|
| 226 |
+
$$M_2 = \frac{1}{B \cdot L} \sum_{b=1}^B \sum_{t=1}^L r_{b,t}(\theta)^2$$
|
| 227 |
+
|
| 228 |
+
Tokens whose importance weight violates the second-moment threshold $r_{b,t}(\theta)^2 \ge \gamma_{\text{threshold}}$ are masked:
|
| 229 |
+
|
| 230 |
+
$$m_{b,t} = \mathbb{I}\left( r_{b,t}(\theta)^2 < \gamma_{\text{threshold}} \right)$$
|
| 231 |
+
$$\mathcal{L}_{\text{M2PO}}(\theta) = -\frac{\sum_{b=1}^B \sum_{t=1}^L m_{b,t} \cdot \min\left( r_{b,t}(\theta) A_{b,t}, \; \text{clip}(r_{b,t}(\theta), 1-\epsilon, 1+\epsilon) A_{b,t} \right)}{\max\left(1, \sum_{b=1}^B \sum_{t=1}^L m_{b,t}\right)}$$
|
| 232 |
+
|
| 233 |
+
This eliminates destructive gradient spikes caused by stale off-policy rollouts without stalling generation.
|
| 234 |
+
|
| 235 |
+
### 3.4 Group Relative Policy Optimization (GRPO)
|
| 236 |
+
|
| 237 |
+
For mathematical, programmatic, and structured reasoning tasks (e.g. DeepSeek-Math, DeepSeek-R1), AsyncTensorRLHF implements **GRPO**. GRPO foregoes a learned critic model and instead normalizes advantages within a group of $G$ responses generated for the identical prompt $x$:
|
| 238 |
+
|
| 239 |
+
$$\mu_g = \frac{1}{G} \sum_{i=1}^G R_{g,i}, \qquad \sigma_g = \sqrt{\frac{1}{G} \sum_{i=1}^G (R_{g,i} - \mu_g)^2} + \epsilon_{\text{eps}}$$
|
| 240 |
+
$$\hat{A}_{g,i} = \frac{R_{g,i} - \mu_g}{\sigma_g}$$
|
| 241 |
+
|
| 242 |
+
The GRPO objective is:
|
| 243 |
+
$$\mathcal{L}_{\text{GRPO}}(\theta) = -\frac{1}{B \cdot G \cdot L} \sum_{b=1}^B \sum_{i=1}^G \sum_{t=1}^L \min\left( r_{b,i,t}(\theta) \hat{A}_{b,i}, \; \text{clip}(r_{b,i,t}(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_{b,i} \right)$$
|
| 244 |
+
|
| 245 |
+
When all responses in a group receive identical rewards (e.g., all correct $R_i=1$ or all wrong $R_i=0$), $\sigma_g \to 0$. AsyncTensorRLHF's implementation adds numerical smoothing ($\epsilon = 10^{-8}$) to ensure $\hat{A}_{g,i} \to 0$ without `NaN` or `Inf` divergence.
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
## 4. Repository Structure
|
| 250 |
|
| 251 |
```
|
| 252 |
AsyncTensorRLHF/
|
| 253 |
+
├── LICENSE # Official Apache 2.0 License
|
| 254 |
+
├── README.md # Comprehensive publication-grade guide
|
| 255 |
+
├── requirements.txt # Core dependencies (torch, numpy, pytest, pyyaml)
|
| 256 |
├── configs/
|
| 257 |
+
│ ├── phase1_sync.yaml # Baseline synchronous config
|
| 258 |
+
│ └── phase2_async.yaml # Full asynchronous config with M2PO/GRPO
|
|
|
|
| 259 |
├── scripts/
|
| 260 |
+
│ ├── launch_orchestrator.sh # Bash launcher for Ray orchestrator
|
| 261 |
+
│ ├── launch_rollout.sh # Bash launcher for Ray rollout workers
|
| 262 |
+
│ ├── launch_trainer.sh # Bash launcher for Ray trainer actors
|
| 263 |
+
│ └── verify_gpu.py # Self-contained CUDA GPU benchmark & verification
|
| 264 |
├── src/
|
| 265 |
+
│ ├── __init__.py
|
| 266 |
│ ├── buffer/
|
| 267 |
+
│ │ ├── __init__.py
|
| 268 |
+
│ │ ├── buffer_actor.py # Thread-safe buffer actor (Ray/standalone)
|
| 269 |
+
│ │ ├── group_buffer.py # GroupAwareReplayBuffer with advantage normalization
|
| 270 |
│ │ └── replay_buffer.py # BoundedReplayBuffer & VersionedReplayBuffer
|
| 271 |
│ ├── orchestrator/
|
| 272 |
+
│ │ ├── __init__.py
|
| 273 |
+
│ │ ├── prompt_queue.py # Asynchronous FIFO prompt queue actor
|
| 274 |
+
│ │ └── scheduler.py # Orchestrator coordinating rollout, trainer & sync
|
| 275 |
│ ├── reward/
|
| 276 |
+
│ │ ├── __init__.py
|
| 277 |
+
│ │ └── tensor_native.py # GPU tensor-native reward functions (.unfold)
|
| 278 |
│ ├── rollout/
|
| 279 |
+
│ │ ├── __init__.py
|
| 280 |
+
│ │ ├── async_engine.py # Async rollout pipeline & buffer dispatcher
|
| 281 |
+
│ │ ├── rollout_worker.py # Rollout actor pulling prompts & executing
|
| 282 |
+
│ │ ├── version_manager.py # Thread-safe policy version bookkeeping
|
| 283 |
+
│ │ └── vllm_engine.py # StubEngine, HFEngine (CUDA/CPU), VLLMEngineWrapper
|
| 284 |
│ └── trainer/
|
| 285 |
+
│ ├── __init__.py
|
| 286 |
│ ├── grpo_loss.py # Group Relative Policy Optimization loss
|
| 287 |
+
│ ├── ppo_loss.py # PPO loss and M2PO second-moment loss
|
| 288 |
+
│ └── trainer_worker.py # Trainer actor with CUDA/CPU autodetect & AdamW
|
| 289 |
└── tests/
|
| 290 |
+
├── __init__.py
|
| 291 |
├── test_phase1.py # Phase 1: Reward, replay buffer, PPO loss
|
| 292 |
├── test_phase2.py # Phase 2: M2PO loss, group buffer, staleness
|
| 293 |
+
├── test_phase3.py # Phase 3: Async stub engine, version manager
|
| 294 |
+
├── test_e2e.py # End-to-end integration & device-native suite
|
| 295 |
+
├── test_reward_edge_cases.py # Reward boundaries, empty sequences, multi-EOS
|
| 296 |
+
├── test_buffer_stress.py # Multithreaded concurrency & staleness boundaries
|
| 297 |
+
├── test_loss_numerical_stability.py # Gradient flow, clipping, zero-advantage edge cases
|
| 298 |
+
└── test_orchestrator_pipeline.py # Closed-loop multi-step training pipeline
|
| 299 |
```
|
| 300 |
|
| 301 |
---
|
| 302 |
|
| 303 |
+
## 5. Component Walkthrough & Code Deep Dive
|
| 304 |
+
|
| 305 |
+
### 5.1 Tensor-Native Reward Engine (`src/reward/tensor_native.py`)
|
| 306 |
+
|
| 307 |
+
The reward engine executes token-level subsequence matching completely on GPU tensors:
|
| 308 |
+
|
| 309 |
+
```python
|
| 310 |
+
def tensor_native_reward(
|
| 311 |
+
generated_ids: torch.Tensor,
|
| 312 |
+
answer_patterns: List[torch.Tensor],
|
| 313 |
+
eos_token_id: int,
|
| 314 |
+
device: str = "cpu",
|
| 315 |
+
) -> torch.Tensor:
|
| 316 |
+
# 1. Trims sequences at first EOS occurrence using argmax over mask
|
| 317 |
+
eos_mask = generated_ids == eos_token_id
|
| 318 |
+
first_eos = torch.where(
|
| 319 |
+
eos_mask.any(dim=1),
|
| 320 |
+
eos_mask.int().argmax(dim=1),
|
| 321 |
+
torch.full((B,), L, device=device_obj, dtype=torch.long),
|
| 322 |
+
)
|
| 323 |
+
# 2. Extracts sliding window views via .unfold(dimension, size, step)
|
| 324 |
+
for i in range(B):
|
| 325 |
+
seq = generated_ids[i, : first_eos[i]]
|
| 326 |
+
pattern = answer_patterns[i]
|
| 327 |
+
pat_len = pattern.shape[0]
|
| 328 |
+
if pat_len == 0 or pat_len > seq.shape[0]:
|
| 329 |
+
continue
|
| 330 |
+
windows = seq.unfold(0, pat_len, 1)
|
| 331 |
+
match = (windows == pattern).all(dim=1).any()
|
| 332 |
+
rewards[i] = 1.0 if match else 0.0
|
| 333 |
+
return rewards
|
| 334 |
+
```
|
| 335 |
+
|
| 336 |
+
**Key Optimizations**:
|
| 337 |
+
- Zero memory allocation for substrings; `.unfold()` creates lightweight strided tensor views.
|
| 338 |
+
- Truncates sequences at the exact first EOS boundary, ignoring post-EOS artifact tokens.
|
| 339 |
+
- Fully compatible with `torch.compile(mode="reduce-overhead")` for kernel fusion.
|
| 340 |
+
|
| 341 |
+
### 5.2 Experience Replay Subsystems (`src/buffer/`)
|
| 342 |
|
| 343 |
+
- **`BoundedReplayBuffer`**: Thread-safe FIFO queue backed by `queue.Queue` with non-blocking `.push(exp)` and `.sample(batch_size)`. Automatically evicts the oldest item when capacity is exceeded.
|
| 344 |
+
- **`VersionedReplayBuffer`**: Tracks policy staleness. When an experience is pushed:
|
| 345 |
+
```python
|
| 346 |
+
if exp.policy_version < self.current_version - self.max_staleness:
|
| 347 |
+
return # Stale: silently evicted without wasting trainer compute
|
| 348 |
+
```
|
| 349 |
+
- **`GroupAwareReplayBuffer`**: Buffers $G$ completions per prompt ID. When the $G$-th completion arrives, advantages are computed in-place and the atomic `GroupBufferEntry` is moved to the ready queue.
|
| 350 |
|
| 351 |
+
### 5.3 Asynchronous Rollout Engines (`src/rollout/`)
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
+
- **`StubEngine`**: Pure-Python mock generating uniform tokens with `asyncio.sleep(0.001)` to test concurrent coroutine interleaving without GPU dependencies.
|
| 354 |
+
- **`HFEngine`**: Real autoregressive PyTorch/Transformers engine running directly on CUDA GPUs. Samples token distributions via `torch.multinomial` and extracts exact log-probabilities in a single forward pass.
|
| 355 |
+
- **`VLLMEngineWrapper`**: Production adapter. Automatically routes generation requests to `vllm.AsyncLLMEngine` if installed; otherwise falls back gracefully to `HFEngine` or `StubEngine`.
|
| 356 |
+
|
| 357 |
+
### 5.4 Distributed Trainer Workers (`src/trainer/`)
|
| 358 |
+
|
| 359 |
+
`TrainerWorker` automatically detects available hardware:
|
| 360 |
+
- Dynamically allocates models on `cuda` when `torch.cuda.is_available()` is True; falls back cleanly to `cpu`.
|
| 361 |
+
- Implements `step(batch_size)`: samples experiences from the shared buffer actor, constructs padded policy and advantage tensors, computes PPO/M2PO/GRPO loss, backpropagates gradients, and calls `optimizer.step()`.
|
| 362 |
+
|
| 363 |
+
### 5.5 Orchestration & Version Management (`src/orchestrator/`)
|
| 364 |
+
|
| 365 |
+
- **`VersionManager`**: Thread-safe policy version counter with atomic `.bump()` and `.staleness(exp_version)`.
|
| 366 |
+
- **`Orchestrator`**: Asynchronous control loop that dispatches prompts to rollout workers, steps trainer actors, and periodically triggers `weight_sync_fn(new_version)` to push updated weights to inference workers.
|
| 367 |
|
| 368 |
---
|
| 369 |
|
| 370 |
+
## 6. Installation & Environment Setup
|
| 371 |
|
| 372 |
+
### Requirements
|
| 373 |
+
- Python 3.10, 3.11, 3.12, 3.13, or 3.14
|
| 374 |
+
- PyTorch $\ge 2.2.0$ (CUDA 12.1+ recommended for GPU)
|
| 375 |
+
- NumPy, PyYAML, PyTest
|
| 376 |
+
|
| 377 |
+
### Clone and Install
|
| 378 |
```bash
|
|
|
|
| 379 |
git clone https://github.com/Hooshaai/AsyncTensorRLHF.git
|
| 380 |
cd AsyncTensorRLHF
|
| 381 |
|
|
|
|
| 382 |
pip install -r requirements.txt
|
| 383 |
```
|
| 384 |
|
| 385 |
---
|
| 386 |
|
| 387 |
+
## 7. Verification & Benchmarking
|
| 388 |
|
| 389 |
+
### 7.1 Running the 41-Test Comprehensive Suite
|
| 390 |
|
| 391 |
+
Run all unit, edge-case, and end-to-end integration tests:
|
| 392 |
|
| 393 |
```bash
|
| 394 |
python3 -m pytest tests/ -v
|
| 395 |
```
|
| 396 |
|
| 397 |
+
**Raw Test Output**:
|
| 398 |
```
|
| 399 |
+
============================= test session starts ==============================
|
| 400 |
+
platform darwin -- Python 3.14.3, pytest-8.0.0, pluggy-1.6.0
|
| 401 |
+
collected 41 items
|
| 402 |
+
|
| 403 |
+
tests/test_buffer_stress.py::test_bounded_buffer_overflow_discards_oldest PASSED [ 2%]
|
| 404 |
+
tests/test_buffer_stress.py::test_bounded_buffer_multithreaded_stress PASSED [ 4%]
|
| 405 |
+
tests/test_buffer_stress.py::test_versioned_buffer_exact_boundaries PASSED [ 7%]
|
| 406 |
+
tests/test_buffer_stress.py::test_group_buffer_zero_variance_rewards PASSED [ 9%]
|
| 407 |
+
tests/test_buffer_stress.py::test_group_buffer_interleaved_prompts PASSED [ 12%]
|
| 408 |
+
tests/test_buffer_stress.py::test_buffer_actor_standalone PASSED [ 14%]
|
| 409 |
+
tests/test_e2e.py::test_device_native_reward PASSED [ 17%]
|
| 410 |
+
tests/test_e2e.py::test_gpu_reward_simple PASSED [ 19%]
|
| 411 |
+
tests/test_e2e.py::test_all_losses_on_device PASSED [ 21%]
|
| 412 |
+
tests/test_e2e.py::test_hf_engine_autoregressive PASSED [ 24%]
|
| 413 |
+
tests/test_e2e.py::test_async_engine_rollout_and_buffer_push PASSED [ 26%]
|
| 414 |
+
tests/test_e2e.py::test_trainer_worker_step PASSED [ 29%]
|
| 415 |
+
tests/test_e2e.py::test_orchestrator_closed_loop PASSED [ 31%]
|
| 416 |
+
tests/test_loss_numerical_stability.py::test_ppo_identical_policies_ratio_one PASSED [ 34%]
|
| 417 |
+
tests/test_loss_numerical_stability.py::test_ppo_clipping_bounds PASSED [ 36%]
|
| 418 |
+
tests/test_loss_numerical_stability.py::test_m2po_threshold_masking PASSED [ 39%]
|
| 419 |
+
tests/test_loss_numerical_stability.py::test_grpo_loss_gradient_flow PASSED [ 41%]
|
| 420 |
+
tests/test_loss_numerical_stability.py::test_loss_backward_passes_with_zero_advantages PASSED [ 43%]
|
| 421 |
+
tests/test_orchestrator_pipeline.py::test_version_manager_concurrency PASSED [ 46%]
|
| 422 |
+
tests/test_orchestrator_pipeline.py::test_prompt_queue_async_operations PASSED [ 48%]
|
| 423 |
+
tests/test_orchestrator_pipeline.py::test_rollout_worker_step_execution PASSED [ 51%]
|
| 424 |
+
tests/test_orchestrator_pipeline.py::test_multi_step_trainer_optimizer_updates PASSED [ 53%]
|
| 425 |
+
tests/test_orchestrator_pipeline.py::test_orchestrator_weight_sync_callback PASSED [ 56%]
|
| 426 |
+
tests/test_phase1.py::test_reward_match PASSED [ 58%]
|
| 427 |
+
tests/test_phase1.py::test_replay_buffer PASSED [ 60%]
|
| 428 |
+
tests/test_phase1.py::test_ppo_loss_scalar PASSED [ 63%]
|
| 429 |
+
tests/test_phase2.py::test_m2po_loss_is_finite_scalar PASSED [ 65%]
|
| 430 |
+
tests/test_phase2.py::test_group_buffer_emits_when_full PASSED [ 68%]
|
| 431 |
+
tests/test_phase2.py::test_versioned_buffer_evicts_stale PASSED [ 70%]
|
| 432 |
+
tests/test_phase3.py::test_stub_engine_generates PASSED [ 73%]
|
| 433 |
+
tests/test_phase3.py::test_stub_engine_concurrent PASSED [ 75%]
|
| 434 |
+
tests/test_phase3.py::test_version_manager_bumps PASSED [ 78%]
|
| 435 |
+
tests/test_reward_edge_cases.py::test_reward_empty_pattern PASSED [ 80%]
|
| 436 |
+
tests/test_reward_edge_cases.py::test_reward_pattern_longer_than_sequence PASSED [ 82%]
|
| 437 |
+
tests/test_reward_edge_cases.py::test_reward_no_eos_token_present PASSED [ 85%]
|
| 438 |
+
tests/test_reward_edge_cases.py::test_reward_pattern_after_first_eos_is_ignored PASSED [ 87%]
|
| 439 |
+
tests/test_reward_edge_cases.py::test_reward_pattern_at_very_beginning PASSED [ 90%]
|
| 440 |
+
tests/test_reward_edge_cases.py::test_reward_pattern_at_very_end_before_eos PASSED [ 92%]
|
| 441 |
+
tests/test_reward_edge_cases.py::test_reward_multiple_eos_stops_at_first PASSED [ 95%]
|
| 442 |
+
tests/test_reward_edge_cases.py::test_reward_batch_heterogeneous_matching PASSED [ 97%]
|
| 443 |
+
tests/test_reward_edge_cases.py::test_gpu_reward_simple_edge_cases PASSED [100%]
|
| 444 |
+
|
| 445 |
+
============================== 41 passed in 2.04s ==============================
|
| 446 |
```
|
| 447 |
|
| 448 |
+
### 7.2 Hardware Benchmarks on NVIDIA RTX 4070 GPU
|
| 449 |
|
| 450 |
+
Run the benchmark script directly on any CUDA-enabled system:
|
| 451 |
|
| 452 |
```bash
|
| 453 |
python scripts/verify_gpu.py
|
| 454 |
```
|
| 455 |
|
| 456 |
+
**Measured Performance on NVIDIA GeForce RTX 4070 Laptop GPU**:
|
| 457 |
+
|
| 458 |
+
```
|
| 459 |
+
============================================================
|
| 460 |
+
AsyncTensorRLHF GPU Verification & Benchmark
|
| 461 |
+
============================================================
|
| 462 |
+
Target Device: CUDA
|
| 463 |
+
GPU Model: NVIDIA GeForce RTX 4070 Laptop GPU
|
| 464 |
+
CUDA Capability: (8, 9)
|
| 465 |
+
Initial Allocated VRAM: 0.00 MB
|
| 466 |
+
------------------------------------------------------------
|
| 467 |
+
[1/5] Testing Tensor-Native Reward on CUDA...
|
| 468 |
+
-> Batched rewards computed for B=64, L=256 in 55.91 ms
|
| 469 |
+
-> Tensor-native reward: PASSED
|
| 470 |
+
|
| 471 |
+
[2/5] Testing Policy Losses (PPO & M2PO) on CUDA...
|
| 472 |
+
-> PPO Loss: 0.7167 (Finite: True)
|
| 473 |
+
-> M2PO Loss: 0.1385 (Finite: True)
|
| 474 |
+
-> Policy losses: PASSED
|
| 475 |
+
|
| 476 |
+
[3/5] Testing Group-Aware Buffer for GRPO...
|
| 477 |
+
-> GRPO Group Advantages standardized: [-1.3416, -0.4472, 0.4472, 1.3416]
|
| 478 |
+
-> Group-aware buffer: PASSED
|
| 479 |
+
|
| 480 |
+
[4/5] Testing Versioned Replay Buffer & Staleness...
|
| 481 |
+
-> Versioned buffer staleness eviction: PASSED
|
| 482 |
+
|
| 483 |
+
[5/5] Testing End-to-End Closed-Loop Rollout & Training on GPU...
|
| 484 |
+
-> Step 1: Buffer Size=0, Policy Version=1, Loss=-0.0000
|
| 485 |
+
-> Step 2: Buffer Size=0, Policy Version=2, Loss=-0.0000
|
| 486 |
+
-> Step 3: Buffer Size=0, Policy Version=3, Loss=-0.0000
|
| 487 |
+
-> Closed-loop rollout + training: PASSED
|
| 488 |
+
------------------------------------------------------------
|
| 489 |
+
Final Allocated VRAM: 17.00 MB
|
| 490 |
+
ALL VERIFICATIONS AND BENCHMARKS COMPLETED SUCCESSFULLY (EXIT 0)
|
| 491 |
+
============================================================
|
| 492 |
+
```
|
| 493 |
+
|
| 494 |
+
---
|
| 495 |
+
|
| 496 |
+
## 8. Developer Cookbook: Extending the Framework
|
| 497 |
+
|
| 498 |
+
### Recipe 1: Integrating a Real Hugging Face LLM (e.g. Qwen / Llama)
|
| 499 |
+
|
| 500 |
+
```python
|
| 501 |
+
import torch
|
| 502 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 503 |
+
from src.rollout.vllm_engine import HFEngine
|
| 504 |
+
from src.rollout.async_engine import AsyncEngine
|
| 505 |
+
from src.buffer.replay_buffer import BoundedReplayBuffer
|
| 506 |
+
|
| 507 |
+
# 1. Initialize buffer & engine
|
| 508 |
+
buffer = BoundedReplayBuffer(max_size=5000)
|
| 509 |
+
model_name = "Qwen/Qwen2.5-0.5B-Instruct"
|
| 510 |
+
|
| 511 |
+
engine = HFEngine(
|
| 512 |
+
model_or_path=model_name,
|
| 513 |
+
device="cuda",
|
| 514 |
+
max_new_tokens=64,
|
| 515 |
+
)
|
| 516 |
+
async_engine = AsyncEngine(buffer=buffer, engine=engine)
|
| 517 |
+
|
| 518 |
+
# 2. Stream rollouts
|
| 519 |
+
prompts = [
|
| 520 |
+
{"input_ids": torch.tensor([15, 32, 100]), "gt_ids": torch.tensor([42]), "eos_token_id": 151643}
|
| 521 |
+
]
|
| 522 |
+
await async_engine.rollout(prompts)
|
| 523 |
+
```
|
| 524 |
+
|
| 525 |
+
### Recipe 2: Implementing Custom In-VRAM Reward Logic
|
| 526 |
+
|
| 527 |
+
To add a custom rule-based reward (e.g., verifying that generated code contains specific AST token delimiters):
|
| 528 |
+
|
| 529 |
+
```python
|
| 530 |
+
import torch
|
| 531 |
+
|
| 532 |
+
def custom_delimiters_reward(
|
| 533 |
+
generated_ids: torch.Tensor,
|
| 534 |
+
required_token_pairs: torch.Tensor, # (K, 2)
|
| 535 |
+
eos_token_id: int
|
| 536 |
+
) -> torch.Tensor:
|
| 537 |
+
"""Computes reward based on delimiter token presence directly on GPU."""
|
| 538 |
+
B, L = generated_ids.shape
|
| 539 |
+
rewards = torch.zeros(B, device=generated_ids.device)
|
| 540 |
+
|
| 541 |
+
for b in range(B):
|
| 542 |
+
seq = generated_ids[b]
|
| 543 |
+
# Check presence of open and close delimiter tokens
|
| 544 |
+
has_open = (seq == required_token_pairs[:, 0]).any()
|
| 545 |
+
has_close = (seq == required_token_pairs[:, 1]).any()
|
| 546 |
+
if has_open and has_close:
|
| 547 |
+
rewards[b] = 1.0
|
| 548 |
+
return rewards
|
| 549 |
+
```
|
| 550 |
+
|
| 551 |
+
### Recipe 3: Distributed Multi-GPU Execution with Ray
|
| 552 |
+
|
| 553 |
+
```bash
|
| 554 |
+
# 1. Start Ray Head
|
| 555 |
+
ray start --head --port=6379
|
| 556 |
+
|
| 557 |
+
# 2. Launch 4 Rollout Actors (GPU 0..3)
|
| 558 |
+
./scripts/launch_rollout.sh 4 /models/Qwen2.5-7B
|
| 559 |
+
|
| 560 |
+
# 3. Launch 2 Trainer Actors (GPU 4..5)
|
| 561 |
+
./scripts/launch_trainer.sh 2 /models/Qwen2.5-7B
|
| 562 |
+
|
| 563 |
+
# 4. Launch Orchestrator
|
| 564 |
+
./scripts/launch_orchestrator.sh /models/Qwen2.5-7B
|
| 565 |
+
```
|
| 566 |
|
| 567 |
---
|
| 568 |
|
| 569 |
+
## 9. Configuration Dictionary
|
| 570 |
|
| 571 |
+
Sample configuration file from `configs/phase2_async.yaml`:
|
| 572 |
|
| 573 |
+
| Parameter | Type | Default | Description |
|
| 574 |
|---|---|---|---|
|
| 575 |
+
| `orchestrator.sync_interval` | `int` | `10` | Frequency (in steps) to trigger weight synchronization |
|
| 576 |
+
| `orchestrator.max_staleness` | `int` | `5` | Maximum policy versions a trajectory can lag before eviction |
|
| 577 |
+
| `rollout.backend` | `str` | `"vllm"` | Inference engine backend: `"vllm"`, `"hf"`, or `"stub"` |
|
| 578 |
+
| `rollout.batch_size` | `int` | `64` | Number of concurrent prompts processed per rollout worker |
|
| 579 |
+
| `rollout.max_new_tokens` | `int` | `512` | Maximum generation length |
|
| 580 |
+
| `trainer.loss_type` | `str` | `"m2po"` | Policy optimization loss: `"ppo"`, `"m2po"`, or `"grpo"` |
|
| 581 |
+
| `trainer.clip_eps` | `float` | `0.2` | PPO clipping parameter $\epsilon$ |
|
| 582 |
+
| `trainer.m2_threshold` | `float` | `2.0` | M2PO second-moment trust-region constraint $\gamma$ |
|
| 583 |
+
| `trainer.learning_rate` | `float` | `1.0e-5` | AdamW learning rate |
|
| 584 |
|
| 585 |
---
|
| 586 |
|
| 587 |
+
## 10. Frequently Asked Questions (FAQ) & Troubleshooting
|
| 588 |
|
| 589 |
+
**Q: Can I run AsyncTensorRLHF without Ray?**
|
| 590 |
+
**A:** Yes. All components (`RolloutWorker`, `TrainerWorker`, `PromptQueue`, `ReplayBufferActor`) automatically detect if Ray is installed. When Ray is absent, they execute as standard high-performance Python classes using `asyncio` and `threading`.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
|
| 592 |
+
**Q: Does it work on single-GPU or laptop setups?**
|
| 593 |
+
**A:** Yes. The framework was benchmarked and validated on a single NVIDIA GeForce RTX 4070 Laptop GPU running Windows 11 with PyTorch 2.6.0+cu124, achieving a complete pipeline footprint of just 17 MB VRAM.
|
| 594 |
|
| 595 |
+
**Q: What happens if vLLM is not installed?**
|
| 596 |
+
**A:** `VLLMEngineWrapper` automatically falls back to `HFEngine` (which runs autoregressive inference using native PyTorch/Transformers on CUDA or CPU) or `StubEngine` (for testing).
|
| 597 |
+
|
| 598 |
+
**Q: How does M2PO prevent training collapse with stale data?**
|
| 599 |
+
**A:** Stale data produces outlier importance ratios $r_t(\theta) \gg 1$. M2PO tracks the second moment $\mathbb{E}[r_t(\theta)^2]$ across tokens and masks out elements exceeding $m_{2\_threshold}$, bounding gradient variance.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
|
| 601 |
---
|
| 602 |
|
| 603 |
+
## 11. Citation & BibTeX
|
| 604 |
|
| 605 |
+
If you find AsyncTensorRLHF useful in your academic research or production deployment, please cite:
|
| 606 |
|
| 607 |
```bibtex
|
| 608 |
@software{asynctensorrlhf2026,
|
| 609 |
author = {Taha Majs and contributors},
|
| 610 |
+
title = {AsyncTensorRLHF: High-Throughput Asynchronous Reinforcement Learning from Human Feedback with Tensor-Native Rewards},
|
| 611 |
year = {2026},
|
| 612 |
+
publisher = {GitHub and Hugging Face},
|
| 613 |
+
journal = {GitHub repository},
|
| 614 |
+
howpublished = {\url{https://github.com/Hooshaai/AsyncTensorRLHF}},
|
| 615 |
+
url = {https://huggingface.co/tahamajs/AsyncTensorRLHF}
|
| 616 |
}
|
| 617 |
```
|
| 618 |
|
|
|
|
| 620 |
|
| 621 |
## 12. License
|
| 622 |
|
| 623 |
+
This project is licensed under the **Apache License, Version 2.0**. You may freely use, modify, distribute, and commercialize this software according to the terms specified in the [LICENSE](LICENSE) file.
|
src/reward/tensor_native.py
CHANGED
|
@@ -27,7 +27,7 @@ def gpu_reward_simple(
|
|
| 27 |
gen = generated_ids[i, : first_eos[i]]
|
| 28 |
gt = ground_truth_ids[i]
|
| 29 |
gt_len = gt.shape[0]
|
| 30 |
-
if gt_len > gen.shape[0]:
|
| 31 |
continue
|
| 32 |
# sliding window comparison
|
| 33 |
windows = gen.unfold(0, gt_len, 1) # (gen_len-gt_len+1, gt_len)
|
|
|
|
| 27 |
gen = generated_ids[i, : first_eos[i]]
|
| 28 |
gt = ground_truth_ids[i]
|
| 29 |
gt_len = gt.shape[0]
|
| 30 |
+
if gt_len == 0 or gt_len > gen.shape[0]:
|
| 31 |
continue
|
| 32 |
# sliding window comparison
|
| 33 |
windows = gen.unfold(0, gt_len, 1) # (gen_len-gt_len+1, gt_len)
|
tests/test_buffer_stress.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import concurrent.futures
|
| 2 |
+
import queue
|
| 3 |
+
import torch
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
from src.buffer.replay_buffer import (
|
| 7 |
+
BoundedReplayBuffer,
|
| 8 |
+
Experience,
|
| 9 |
+
VersionedReplayBuffer,
|
| 10 |
+
VersionedExperience,
|
| 11 |
+
)
|
| 12 |
+
from src.buffer.group_buffer import GroupAwareReplayBuffer
|
| 13 |
+
from src.buffer.buffer_actor import ReplayBufferActor
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_bounded_buffer_overflow_discards_oldest():
|
| 17 |
+
buf = BoundedReplayBuffer(max_size=3)
|
| 18 |
+
for i in range(5):
|
| 19 |
+
buf.push(Experience(
|
| 20 |
+
prompt_ids=torch.tensor([i]),
|
| 21 |
+
generated_ids=torch.tensor([i]),
|
| 22 |
+
log_probs=torch.tensor([0.0]),
|
| 23 |
+
reward=float(i),
|
| 24 |
+
version=i,
|
| 25 |
+
))
|
| 26 |
+
assert buf.size() == 3
|
| 27 |
+
sampled = buf.sample(3)
|
| 28 |
+
# The oldest entries 0 and 1 were evicted; remaining should be 2, 3, 4
|
| 29 |
+
rewards = [e.reward for e in sampled]
|
| 30 |
+
assert rewards == [2.0, 3.0, 4.0]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def test_bounded_buffer_multithreaded_stress():
|
| 34 |
+
buf = BoundedReplayBuffer(max_size=500)
|
| 35 |
+
num_threads = 8
|
| 36 |
+
items_per_thread = 50
|
| 37 |
+
|
| 38 |
+
def producer(thread_id):
|
| 39 |
+
for j in range(items_per_thread):
|
| 40 |
+
buf.push(Experience(
|
| 41 |
+
prompt_ids=torch.tensor([thread_id]),
|
| 42 |
+
generated_ids=torch.tensor([j]),
|
| 43 |
+
log_probs=torch.tensor([-0.1]),
|
| 44 |
+
reward=float(thread_id * 100 + j),
|
| 45 |
+
version=0,
|
| 46 |
+
))
|
| 47 |
+
|
| 48 |
+
def consumer():
|
| 49 |
+
collected = []
|
| 50 |
+
for _ in range(items_per_thread // 2):
|
| 51 |
+
batch = buf.sample(4)
|
| 52 |
+
collected.extend(batch)
|
| 53 |
+
return collected
|
| 54 |
+
|
| 55 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads * 2) as executor:
|
| 56 |
+
producer_futures = [executor.submit(producer, t) for t in range(num_threads)]
|
| 57 |
+
consumer_futures = [executor.submit(consumer) for _ in range(num_threads)]
|
| 58 |
+
concurrent.futures.wait(producer_futures + consumer_futures)
|
| 59 |
+
|
| 60 |
+
# Buffer remains consistent and doesn't crash or deadlock
|
| 61 |
+
assert buf.size() >= 0
|
| 62 |
+
assert buf.size() <= 500
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_versioned_buffer_exact_boundaries():
|
| 66 |
+
buf = VersionedReplayBuffer(max_size=10, max_staleness=4)
|
| 67 |
+
buf.current_version = 10 # cutoff is 10 - 4 = 6. Allowed: 6, 7, 8, 9, 10
|
| 68 |
+
|
| 69 |
+
# 1. Stale: version 5 < 6 -> dropped
|
| 70 |
+
buf.push(VersionedExperience(
|
| 71 |
+
prompt_ids=torch.tensor([1]),
|
| 72 |
+
generated_ids=torch.tensor([1]),
|
| 73 |
+
log_probs=torch.tensor([-0.1]),
|
| 74 |
+
reward=1.0,
|
| 75 |
+
policy_version=5,
|
| 76 |
+
generation_step=0,
|
| 77 |
+
))
|
| 78 |
+
assert len(buf) == 0
|
| 79 |
+
|
| 80 |
+
# 2. Boundary: version 6 == 6 -> kept
|
| 81 |
+
buf.push(VersionedExperience(
|
| 82 |
+
prompt_ids=torch.tensor([1]),
|
| 83 |
+
generated_ids=torch.tensor([1]),
|
| 84 |
+
log_probs=torch.tensor([-0.1]),
|
| 85 |
+
reward=1.0,
|
| 86 |
+
policy_version=6,
|
| 87 |
+
generation_step=0,
|
| 88 |
+
))
|
| 89 |
+
assert len(buf) == 1
|
| 90 |
+
|
| 91 |
+
# 3. Fresh: version 10 == 10 -> kept
|
| 92 |
+
buf.push(VersionedExperience(
|
| 93 |
+
prompt_ids=torch.tensor([1]),
|
| 94 |
+
generated_ids=torch.tensor([1]),
|
| 95 |
+
log_probs=torch.tensor([-0.1]),
|
| 96 |
+
reward=1.0,
|
| 97 |
+
policy_version=10,
|
| 98 |
+
generation_step=0,
|
| 99 |
+
))
|
| 100 |
+
assert len(buf) == 2
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def test_group_buffer_zero_variance_rewards():
|
| 104 |
+
# If all responses in a group receive identical rewards (e.g. all 1.0 or all 0.0),
|
| 105 |
+
# standard deviation is 0. Advantages must not produce NaN or Inf due to division by zero.
|
| 106 |
+
buf = GroupAwareReplayBuffer(group_size=4, max_groups=4)
|
| 107 |
+
for i in range(4):
|
| 108 |
+
buf.add_response(
|
| 109 |
+
prompt_id=999,
|
| 110 |
+
response=torch.tensor([i]),
|
| 111 |
+
log_prob=torch.tensor([-0.2]),
|
| 112 |
+
reward=1.0, # identical rewards
|
| 113 |
+
version=0,
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
assert buf.ready.qsize() == 1
|
| 117 |
+
group = buf.ready.get_nowait()
|
| 118 |
+
assert torch.isfinite(group.advantages).all()
|
| 119 |
+
# All advantages should be 0.0 when rewards are identical
|
| 120 |
+
assert torch.allclose(group.advantages, torch.zeros(4))
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def test_group_buffer_interleaved_prompts():
|
| 124 |
+
buf = GroupAwareReplayBuffer(group_size=3, max_groups=8)
|
| 125 |
+
|
| 126 |
+
# Prompt A: sample 0, 1
|
| 127 |
+
buf.add_response(10, torch.tensor([1]), torch.tensor([-0.1]), 0.0, 0)
|
| 128 |
+
buf.add_response(10, torch.tensor([2]), torch.tensor([-0.1]), 1.0, 0)
|
| 129 |
+
assert buf.ready.qsize() == 0
|
| 130 |
+
|
| 131 |
+
# Prompt B: sample 0
|
| 132 |
+
buf.add_response(20, torch.tensor([10]), torch.tensor([-0.1]), 0.5, 0)
|
| 133 |
+
assert buf.ready.qsize() == 0
|
| 134 |
+
|
| 135 |
+
# Prompt A: sample 2 -> Prompt A finishes and emits
|
| 136 |
+
buf.add_response(10, torch.tensor([3]), torch.tensor([-0.1]), 2.0, 0)
|
| 137 |
+
assert buf.ready.qsize() == 1
|
| 138 |
+
group_a = buf.ready.get_nowait()
|
| 139 |
+
assert group_a.prompt_id == 10
|
| 140 |
+
assert len(group_a.responses) == 3
|
| 141 |
+
|
| 142 |
+
# Prompt B: sample 1, 2 -> Prompt B finishes and emits
|
| 143 |
+
buf.add_response(20, torch.tensor([20]), torch.tensor([-0.1]), 0.5, 0)
|
| 144 |
+
buf.add_response(20, torch.tensor([30]), torch.tensor([-0.1]), 0.5, 0)
|
| 145 |
+
assert buf.ready.qsize() == 1
|
| 146 |
+
group_b = buf.ready.get_nowait()
|
| 147 |
+
assert group_b.prompt_id == 20
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def test_buffer_actor_standalone():
|
| 151 |
+
actor = ReplayBufferActor(max_size=10)
|
| 152 |
+
exp = Experience(
|
| 153 |
+
prompt_ids=torch.tensor([1]),
|
| 154 |
+
generated_ids=torch.tensor([2]),
|
| 155 |
+
log_probs=torch.tensor([-0.1]),
|
| 156 |
+
reward=1.0,
|
| 157 |
+
version=1,
|
| 158 |
+
)
|
| 159 |
+
assert actor.push(exp) is True
|
| 160 |
+
assert actor.size() == 1
|
| 161 |
+
sampled = actor.sample(5)
|
| 162 |
+
assert len(sampled) == 1
|
| 163 |
+
assert actor.size() == 0
|
tests/test_loss_numerical_stability.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from src.trainer.ppo_loss import compute_ppo_loss, compute_m2po_loss
|
| 5 |
+
from src.trainer.grpo_loss import compute_grpo_loss
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_ppo_identical_policies_ratio_one():
|
| 9 |
+
# When policy_log_probs == old_log_probs, ratio is exactly 1.0 everywhere.
|
| 10 |
+
# Loss should be -mean(advantages)
|
| 11 |
+
B, L = 4, 10
|
| 12 |
+
lp = torch.randn(B, L)
|
| 13 |
+
adv = torch.randn(B, L)
|
| 14 |
+
loss = compute_ppo_loss(lp, lp, adv)
|
| 15 |
+
expected = -adv.mean()
|
| 16 |
+
assert torch.isclose(loss, expected, atol=1e-5)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_ppo_clipping_bounds():
|
| 20 |
+
# Extreme policy divergence: ratio is huge (exp(10) >> 1.2)
|
| 21 |
+
B, L = 2, 4
|
| 22 |
+
old_lp = torch.zeros(B, L)
|
| 23 |
+
new_lp = torch.full((B, L), 10.0) # ratio = e^10 ~ 22026
|
| 24 |
+
adv = torch.ones(B, L) # positive advantage
|
| 25 |
+
|
| 26 |
+
loss = compute_ppo_loss(new_lp, old_lp, adv, clip_eps=0.2)
|
| 27 |
+
# Since adv > 0 and ratio > 1+eps, clipped term (1+eps)*adv = 1.2 is smaller than ratio*adv
|
| 28 |
+
# Loss should be -1.2
|
| 29 |
+
assert torch.isclose(loss, torch.tensor(-1.2), atol=1e-5)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_m2po_threshold_masking():
|
| 33 |
+
# If ratio^2 exceeds m2_threshold, the M2PO mask drops it or bounds it
|
| 34 |
+
B, L = 2, 4
|
| 35 |
+
old_lp = torch.zeros(B, L)
|
| 36 |
+
# Sequence with high divergence
|
| 37 |
+
new_lp = torch.full((B, L), 2.0) # ratio = e^2 ~ 7.389, ratio^2 ~ 54.6 > m2_threshold=2.0
|
| 38 |
+
adv = torch.ones(B, L)
|
| 39 |
+
|
| 40 |
+
loss_m2po = compute_m2po_loss(new_lp, old_lp, adv, m2_threshold=2.0)
|
| 41 |
+
assert torch.isfinite(loss_m2po)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_grpo_loss_gradient_flow():
|
| 45 |
+
B, G, L = 2, 4, 8
|
| 46 |
+
policy_lp = torch.randn(B, G, L, requires_grad=True)
|
| 47 |
+
old_lp = torch.randn(B, G, L)
|
| 48 |
+
advantages = torch.randn(B, G, L)
|
| 49 |
+
|
| 50 |
+
loss = compute_grpo_loss(policy_lp, old_lp, advantages, clip_eps=0.2)
|
| 51 |
+
loss.backward()
|
| 52 |
+
|
| 53 |
+
assert policy_lp.grad is not None
|
| 54 |
+
assert torch.isfinite(policy_lp.grad).all()
|
| 55 |
+
assert policy_lp.grad.shape == policy_lp.shape
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def test_loss_backward_passes_with_zero_advantages():
|
| 59 |
+
B, L = 4, 8
|
| 60 |
+
policy_lp = torch.randn(B, L, requires_grad=True)
|
| 61 |
+
old_lp = torch.randn(B, L)
|
| 62 |
+
zero_adv = torch.zeros(B, L)
|
| 63 |
+
|
| 64 |
+
# PPO
|
| 65 |
+
l_ppo = compute_ppo_loss(policy_lp, old_lp, zero_adv)
|
| 66 |
+
l_ppo.backward()
|
| 67 |
+
assert torch.allclose(policy_lp.grad, torch.zeros_like(policy_lp.grad))
|
| 68 |
+
policy_lp.grad.zero_()
|
| 69 |
+
|
| 70 |
+
# M2PO
|
| 71 |
+
l_m2po = compute_m2po_loss(policy_lp, old_lp, zero_adv)
|
| 72 |
+
l_m2po.backward()
|
| 73 |
+
assert torch.allclose(policy_lp.grad, torch.zeros_like(policy_lp.grad))
|
tests/test_orchestrator_pipeline.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import threading
|
| 3 |
+
import torch
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
from src.orchestrator.prompt_queue import PromptQueue
|
| 7 |
+
from src.orchestrator.scheduler import Orchestrator
|
| 8 |
+
from src.rollout.version_manager import VersionManager
|
| 9 |
+
from src.rollout.vllm_engine import StubEngine, HFEngine
|
| 10 |
+
from src.rollout.rollout_worker import RolloutWorker
|
| 11 |
+
from src.trainer.trainer_worker import TrainerWorker
|
| 12 |
+
from src.buffer.replay_buffer import BoundedReplayBuffer, Experience
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_version_manager_concurrency():
|
| 16 |
+
vm = VersionManager()
|
| 17 |
+
num_threads = 20
|
| 18 |
+
increments_per_thread = 50
|
| 19 |
+
|
| 20 |
+
def worker():
|
| 21 |
+
for _ in range(increments_per_thread):
|
| 22 |
+
vm.bump()
|
| 23 |
+
|
| 24 |
+
threads = [threading.Thread(target=worker) for _ in range(num_threads)]
|
| 25 |
+
for t in threads:
|
| 26 |
+
t.start()
|
| 27 |
+
for t in threads:
|
| 28 |
+
t.join()
|
| 29 |
+
|
| 30 |
+
assert vm.current() == num_threads * increments_per_thread
|
| 31 |
+
assert vm.staleness(0) == num_threads * increments_per_thread
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_prompt_queue_async_operations():
|
| 35 |
+
async def run():
|
| 36 |
+
pq = PromptQueue(maxsize=5)
|
| 37 |
+
assert pq.size() == 0
|
| 38 |
+
|
| 39 |
+
p1 = {"input_ids": torch.tensor([1, 2, 3])}
|
| 40 |
+
p2 = {"input_ids": torch.tensor([4, 5, 6])}
|
| 41 |
+
|
| 42 |
+
await pq.add_prompt(p1)
|
| 43 |
+
await pq.add_prompt(p2)
|
| 44 |
+
assert pq.size() == 2
|
| 45 |
+
|
| 46 |
+
out1 = await pq.get_prompt()
|
| 47 |
+
assert torch.equal(out1["input_ids"], p1["input_ids"])
|
| 48 |
+
assert pq.size() == 1
|
| 49 |
+
|
| 50 |
+
out2 = await pq.get_prompt()
|
| 51 |
+
assert torch.equal(out2["input_ids"], p2["input_ids"])
|
| 52 |
+
assert pq.size() == 0
|
| 53 |
+
|
| 54 |
+
asyncio.run(run())
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_rollout_worker_step_execution():
|
| 58 |
+
buf = BoundedReplayBuffer(max_size=10)
|
| 59 |
+
vm = VersionManager()
|
| 60 |
+
stub = StubEngine(vocab_size=50, max_new_tokens=4, eos_token_id=99)
|
| 61 |
+
worker = RolloutWorker(buffer=buf, version_manager=vm, engine=stub)
|
| 62 |
+
|
| 63 |
+
prompts = [
|
| 64 |
+
{"input_ids": torch.tensor([1, 2]), "gt_ids": torch.tensor([3]), "eos_token_id": 99},
|
| 65 |
+
{"input_ids": torch.tensor([4, 5]), "gt_ids": torch.tensor([5]), "eos_token_id": 99},
|
| 66 |
+
{"input_ids": torch.tensor([6, 7]), "gt_ids": torch.tensor([7]), "eos_token_id": 99},
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
async def run():
|
| 70 |
+
await worker.step(prompts)
|
| 71 |
+
assert buf.size() == 3
|
| 72 |
+
|
| 73 |
+
asyncio.run(run())
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_multi_step_trainer_optimizer_updates():
|
| 77 |
+
buf = BoundedReplayBuffer(max_size=20)
|
| 78 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 79 |
+
|
| 80 |
+
for i in range(10):
|
| 81 |
+
buf.push(Experience(
|
| 82 |
+
prompt_ids=torch.tensor([1, 2], device=device),
|
| 83 |
+
generated_ids=torch.tensor([i, i+1, i+2], device=device),
|
| 84 |
+
log_probs=torch.tensor([-0.1, -0.2, -0.3], device=device),
|
| 85 |
+
reward=float(i % 2),
|
| 86 |
+
version=0,
|
| 87 |
+
))
|
| 88 |
+
|
| 89 |
+
trainer = TrainerWorker(buffer=buf, device=device, lr=1e-3)
|
| 90 |
+
initial_version = trainer.get_version()
|
| 91 |
+
assert initial_version == 0
|
| 92 |
+
|
| 93 |
+
losses = []
|
| 94 |
+
for _ in range(3):
|
| 95 |
+
loss = trainer.step(batch_size=4)
|
| 96 |
+
assert loss is not None
|
| 97 |
+
assert isinstance(loss, float)
|
| 98 |
+
losses.append(loss)
|
| 99 |
+
|
| 100 |
+
assert trainer.get_version() == 3
|
| 101 |
+
assert len(losses) == 3
|
| 102 |
+
for l in losses:
|
| 103 |
+
assert isinstance(l, float)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def test_orchestrator_weight_sync_callback():
|
| 107 |
+
vm = VersionManager()
|
| 108 |
+
buf = BoundedReplayBuffer(max_size=20)
|
| 109 |
+
stub = StubEngine(vocab_size=32, max_new_tokens=4, eos_token_id=99)
|
| 110 |
+
rollout = RolloutWorker(buffer=buf, version_manager=vm, engine=stub)
|
| 111 |
+
trainer = TrainerWorker(buffer=buf)
|
| 112 |
+
|
| 113 |
+
sync_history = []
|
| 114 |
+
async def async_sync(version):
|
| 115 |
+
sync_history.append(version)
|
| 116 |
+
|
| 117 |
+
orch = Orchestrator(
|
| 118 |
+
version_manager=vm,
|
| 119 |
+
weight_sync_fn=async_sync,
|
| 120 |
+
sync_interval=2,
|
| 121 |
+
rollout_worker=rollout,
|
| 122 |
+
trainer_worker=trainer,
|
| 123 |
+
buffer=buf,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
async def run():
|
| 127 |
+
losses = await orch.run(max_steps=4)
|
| 128 |
+
assert len(losses) > 0
|
| 129 |
+
assert len(sync_history) >= 2
|
| 130 |
+
|
| 131 |
+
asyncio.run(run())
|
tests/test_reward_edge_cases.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from src.reward.tensor_native import tensor_native_reward, gpu_reward_simple
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def get_device():
|
| 8 |
+
return "cuda" if torch.cuda.is_available() else "cpu"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_reward_empty_pattern():
|
| 12 |
+
device = get_device()
|
| 13 |
+
gen = torch.tensor([[1, 2, 3, 99, 0]], device=device)
|
| 14 |
+
ans = [torch.empty(0, dtype=torch.long, device=device)]
|
| 15 |
+
r = tensor_native_reward(gen, ans, eos_token_id=99, device=device)
|
| 16 |
+
assert r.shape == (1,)
|
| 17 |
+
assert r[0].item() == 0.0
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_reward_pattern_longer_than_sequence():
|
| 21 |
+
device = get_device()
|
| 22 |
+
gen = torch.tensor([[1, 2, 99]], device=device)
|
| 23 |
+
ans = [torch.tensor([1, 2, 3, 4, 5], device=device)]
|
| 24 |
+
r = tensor_native_reward(gen, ans, eos_token_id=99, device=device)
|
| 25 |
+
assert r[0].item() == 0.0
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_reward_no_eos_token_present():
|
| 29 |
+
device = get_device()
|
| 30 |
+
# Sequence never hits EOS token 99; matches anywhere before end of seq
|
| 31 |
+
gen = torch.tensor([[1, 2, 3, 4, 5, 6]], device=device)
|
| 32 |
+
ans = [torch.tensor([3, 4], device=device)]
|
| 33 |
+
r = tensor_native_reward(gen, ans, eos_token_id=99, device=device)
|
| 34 |
+
assert r[0].item() == 1.0
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def test_reward_pattern_after_first_eos_is_ignored():
|
| 38 |
+
device = get_device()
|
| 39 |
+
# EOS is at index 2 (token 99). Pattern [5, 6] appears after EOS, so reward must be 0.0
|
| 40 |
+
gen = torch.tensor([[1, 2, 99, 5, 6, 0]], device=device)
|
| 41 |
+
ans = [torch.tensor([5, 6], device=device)]
|
| 42 |
+
r = tensor_native_reward(gen, ans, eos_token_id=99, device=device)
|
| 43 |
+
assert r[0].item() == 0.0
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_reward_pattern_at_very_beginning():
|
| 47 |
+
device = get_device()
|
| 48 |
+
gen = torch.tensor([[10, 20, 30, 99, 0]], device=device)
|
| 49 |
+
ans = [torch.tensor([10, 20], device=device)]
|
| 50 |
+
r = tensor_native_reward(gen, ans, eos_token_id=99, device=device)
|
| 51 |
+
assert r[0].item() == 1.0
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_reward_pattern_at_very_end_before_eos():
|
| 55 |
+
device = get_device()
|
| 56 |
+
gen = torch.tensor([[10, 20, 30, 40, 99, 0]], device=device)
|
| 57 |
+
ans = [torch.tensor([30, 40], device=device)]
|
| 58 |
+
r = tensor_native_reward(gen, ans, eos_token_id=99, device=device)
|
| 59 |
+
assert r[0].item() == 1.0
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def test_reward_multiple_eos_stops_at_first():
|
| 63 |
+
device = get_device()
|
| 64 |
+
# First EOS is at idx 1. Pattern [30] is after first EOS but before second EOS -> must be 0
|
| 65 |
+
gen = torch.tensor([[10, 99, 30, 99, 0]], device=device)
|
| 66 |
+
ans = [torch.tensor([30], device=device)]
|
| 67 |
+
r = tensor_native_reward(gen, ans, eos_token_id=99, device=device)
|
| 68 |
+
assert r[0].item() == 0.0
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_reward_batch_heterogeneous_matching():
|
| 72 |
+
device = get_device()
|
| 73 |
+
gen = torch.tensor([
|
| 74 |
+
[1, 2, 3, 4, 99, 0],
|
| 75 |
+
[5, 6, 7, 8, 99, 0],
|
| 76 |
+
[9, 10, 11, 12, 99, 0],
|
| 77 |
+
[13, 14, 15, 16, 99, 0],
|
| 78 |
+
], device=device)
|
| 79 |
+
ans = [
|
| 80 |
+
torch.tensor([2, 3], device=device), # Match (1.0)
|
| 81 |
+
torch.tensor([999], device=device), # No match (0.0)
|
| 82 |
+
torch.tensor([10, 11, 12], device=device), # Match (1.0)
|
| 83 |
+
torch.tensor([1, 2], device=device), # No match (0.0)
|
| 84 |
+
]
|
| 85 |
+
r = tensor_native_reward(gen, ans, eos_token_id=99, device=device)
|
| 86 |
+
assert r.tolist() == [1.0, 0.0, 1.0, 0.0]
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def test_gpu_reward_simple_edge_cases():
|
| 90 |
+
device = get_device()
|
| 91 |
+
# Empty ground truth
|
| 92 |
+
gen = torch.tensor([[1, 2, 3, 99]], device=device)
|
| 93 |
+
gt_empty = torch.empty((1, 0), dtype=torch.long, device=device)
|
| 94 |
+
r = gpu_reward_simple(gen, gt_empty, eos_token_id=99)
|
| 95 |
+
assert r[0].item() == 0.0
|
| 96 |
+
|
| 97 |
+
# Ground truth longer than generated
|
| 98 |
+
gt_long = torch.tensor([[1, 2, 3, 4, 5]], device=device)
|
| 99 |
+
r = gpu_reward_simple(gen, gt_long, eos_token_id=99)
|
| 100 |
+
assert r[0].item() == 0.0
|