Final 58 compile errors fixed + GPU violation analysis: - 23 files across ml, ml-dqn, ml-supervised, services - flash_attention: CudaBlas + stream fields, GPU matmul/transpose - ensemble adapters (tggn, tlob, mamba2, tft, ppo): StreamTensor/GpuLinear - diffusion/xlstm/mamba trainable: StreamTensor ↔ GpuTensor conversion - hyperopt adapters: fixed API signatures - trainers (liquid, mamba2): checkpoint save via safetensors - benchmarks: fixed to_scalar, RainbowAgent API - services: MlDevice, PPO::load_checkpoint, Mamba2SSM constructor Host-side softmax/distributional functions analyzed — operating on legitimately-downloaded small output tensors at computation endpoints. Not GPU violations (cold paths, <50 elements). FULL WORKSPACE: 0 errors, 56 warnings, 0 candle dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
180 lines
6.2 KiB
Markdown
180 lines
6.2 KiB
Markdown
# GPU-Resident Training Pipeline Refactor
|
|
|
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Eliminate ALL GPU→CPU→GPU roundtrips from the ML training pipeline. Data stays GPU-resident from collection through forward/backward/optimizer. Only scalar metrics come to CPU.
|
|
|
|
**Architecture:** Experience batches use `CudaSlice<f32>` / `GpuTensor` fields (not `Vec<f32>`). `model.update()` accepts GPU-resident data directly. Delete all CPU-side batch conversion functions.
|
|
|
|
**Tech Stack:** cudarc 0.19, CudaSlice, GpuTensor, cuBLAS
|
|
|
|
---
|
|
|
|
## Current Roundtrips (what to eliminate)
|
|
|
|
| # | Path | Data/epoch | Fix |
|
|
|---|------|-----------|-----|
|
|
| 1 | PPO: GPU kernel → `Vec<f32>` (PpoExperienceBatch) → `Vec<Vec<f32>>` (TrajectoryBatch) → GPU re-upload | 123 MB | CudaSlice fields, delete conversion |
|
|
| 2 | DQN: GPU kernel → `Vec<f32>` (ExperienceBatch) — parallel `GpuExperienceBatch` exists but may not be primary | 1.95 GB | Unify on GpuExperienceBatch |
|
|
| 3 | PPO: GPU softmax → `to_vec1()` → CPU rand sampling per step | 16K syncs | GPU sampling kernel or batch |
|
|
|
|
---
|
|
|
|
## Task 1: PPO Experience Batch GPU-Resident
|
|
|
|
**Files:**
|
|
- Modify: `crates/ml/src/cuda_pipeline/gpu_ppo_collector.rs`
|
|
- Modify: `crates/ml/src/trainers/ppo.rs`
|
|
- Modify: `crates/ml-ppo/src/ppo.rs`
|
|
|
|
### Subtask 1a: Change PpoExperienceBatch to GPU-resident
|
|
|
|
- [ ] **Step 1:** In `gpu_ppo_collector.rs`, change `PpoExperienceBatch` fields:
|
|
```rust
|
|
// BEFORE:
|
|
pub struct PpoExperienceBatch {
|
|
pub states: Vec<f32>, // downloaded from GPU
|
|
pub actions: Vec<i32>,
|
|
pub log_probs: Vec<f32>,
|
|
pub advantages: Vec<f32>,
|
|
pub returns: Vec<f32>,
|
|
pub done_flags: Vec<i32>,
|
|
pub n_episodes: usize,
|
|
pub timesteps: usize,
|
|
}
|
|
|
|
// AFTER:
|
|
pub struct PpoExperienceBatch {
|
|
pub states: CudaSlice<f32>, // stays on GPU
|
|
pub actions: CudaSlice<i32>,
|
|
pub log_probs: CudaSlice<f32>,
|
|
pub advantages: CudaSlice<f32>,
|
|
pub returns: CudaSlice<f32>,
|
|
pub done_flags: CudaSlice<i32>,
|
|
pub n_episodes: usize,
|
|
pub timesteps: usize,
|
|
pub state_dim: usize,
|
|
pub stream: Arc<CudaStream>,
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2:** In `collect_experiences()`, remove ALL `memcpy_dtoh` calls. Return the GPU buffers directly.
|
|
|
|
- [ ] **Step 3:** Add `PpoExperienceBatch::download()` method for debugging/checkpoint only:
|
|
```rust
|
|
impl PpoExperienceBatch {
|
|
pub fn download_states(&self) -> Result<Vec<f32>, MLError> {
|
|
self.stream.clone_dtoh(&self.states).map_err(...)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Subtask 1b: Delete cpu_batch_to_trajectory_batch
|
|
|
|
- [ ] **Step 4:** In `ppo.rs`, DELETE `gpu_batch_to_trajectory_batch()` function entirely.
|
|
|
|
- [ ] **Step 5:** Change `train_gpu()` to pass `PpoExperienceBatch` directly to model.update():
|
|
```rust
|
|
// BEFORE:
|
|
let batch = collector.collect_experiences(...)?;
|
|
let training_batch = gpu_batch_to_trajectory_batch(&batch); // GPU→CPU conversion
|
|
let (policy_loss, value_loss) = model.update(&mut training_batch)?; // CPU→GPU re-upload
|
|
|
|
// AFTER:
|
|
let batch = collector.collect_experiences(...)?; // stays on GPU
|
|
let (policy_loss, value_loss) = model.update_gpu(&batch)?; // pure GPU
|
|
```
|
|
|
|
### Subtask 1c: Add PPOAgent::update_gpu()
|
|
|
|
- [ ] **Step 6:** In `crates/ml-ppo/src/ppo.rs`, add `update_gpu()` method:
|
|
```rust
|
|
pub fn update_gpu(&mut self, batch: &PpoExperienceBatch) -> Result<(f32, f32), MLError> {
|
|
// Forward pass directly on CudaSlice data — no upload needed
|
|
// Loss computation on GPU
|
|
// Backward + optimizer step on GPU
|
|
// Only readback: 2 scalar losses
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 7:** Compile check: `SQLX_OFFLINE=true cargo check -p ml-ppo -p ml`
|
|
|
|
---
|
|
|
|
## Task 2: DQN Experience Batch Unification
|
|
|
|
**Files:**
|
|
- Modify: `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs`
|
|
- Modify: `crates/ml/src/trainers/dqn/trainer/train_step.rs`
|
|
|
|
### Subtask 2a: Audit which batch type is primary
|
|
|
|
- [ ] **Step 1:** Search for all `ExperienceBatch` vs `GpuExperienceBatch` usage:
|
|
```bash
|
|
rg "ExperienceBatch\b" crates/ml/ --type rust -n | grep -v "Gpu"
|
|
rg "GpuExperienceBatch" crates/ml/ --type rust -n
|
|
```
|
|
|
|
- [ ] **Step 2:** If `ExperienceBatch` (CPU) is primary in training loop:
|
|
- Change training loop to use `GpuExperienceBatch` instead
|
|
- Delete `ExperienceBatch` struct OR keep only for checkpoint/debugging
|
|
|
|
- [ ] **Step 3:** If `GpuExperienceBatch` is already primary:
|
|
- Delete `ExperienceBatch` struct
|
|
- Remove `collect_experiences()` CPU download path
|
|
- Keep only `collect_experiences_gpu()` path
|
|
|
|
### Subtask 2b: Verify fused CUDA trainer uses GPU batch
|
|
|
|
- [ ] **Step 4:** Check that `GpuDqnTrainer::train_step()` takes CudaSlice/Tensor directly (not Vec<f32>)
|
|
- [ ] **Step 5:** Compile check
|
|
|
|
---
|
|
|
|
## Task 3: PPO Action Sampling on GPU
|
|
|
|
**Files:**
|
|
- Modify: `crates/ml/src/trainers/ppo.rs` (CPU fallback path)
|
|
- Modify: `crates/ml/src/cuda_pipeline/gpu_ppo_collector.rs` (GPU path — already correct)
|
|
|
|
- [ ] **Step 1:** Verify GPU collection path already samples actions on-device (should be in kernel)
|
|
- [ ] **Step 2:** For CPU fallback path: batch-sample N steps at once instead of per-step sync:
|
|
```rust
|
|
// BEFORE (per-step):
|
|
for step in 0..num_steps {
|
|
let probs = actor.forward(&state)?;
|
|
let probs_vec = probs.to_vec1::<f32>()?; // GPU SYNC per step
|
|
let action = sample_from_probs(&probs_vec);
|
|
}
|
|
|
|
// AFTER (batched):
|
|
let all_probs = actor.forward_batch(&all_states)?; // single forward
|
|
let all_probs_vec = all_probs.to_vec2::<f32>()?; // single download
|
|
for (step, probs) in all_probs_vec.iter().enumerate() {
|
|
let action = sample_from_probs(probs);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3:** Compile check
|
|
|
|
---
|
|
|
|
## Task 4: Verify Supervised Models Are Clean
|
|
|
|
- [ ] **Step 1:** Confirm TFT, Mamba2, Liquid, TLOB, KAN, xLSTM, Diffusion training loops only do scalar readbacks
|
|
- [ ] **Step 2:** No changes expected — just audit and document
|
|
|
|
---
|
|
|
|
## Execution Strategy
|
|
|
|
Task 1 (PPO) is the biggest win (123 MB/epoch eliminated). Tasks 2-3 can follow.
|
|
|
|
**Recommended: 2 agents**
|
|
- Agent A: Task 1 (PPO GPU-resident batch)
|
|
- Agent B: Task 2 (DQN batch unification)
|
|
- Task 3 after both complete
|
|
- Task 4 is read-only audit
|
|
|
|
**Estimated: 2-4 hours with 2 agents.**
|