Files
foxhunt/docs/superpowers/plans/2026-03-24-remaining-gpu-features.md
jgrusewski a69174e99f fix: segment-based trade lifecycle + reversal P&L computation
- Trade detection now counts reversals (S100→L50) as completed segments
- old_pos_pnl saved before position update for correct reversal P&L
- realized_pnl writeback uses old position PnL on reversal bars
- 0% win rate persists — needs deeper investigation (likely tx cost interaction)

WIP: The trade_return formula produces correct sign for raw market moves,
but every trade still shows as a loss. Suspect tx costs on both entry AND
exit of each reversal segment exceed the 1-bar price movement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:57:04 +01:00

568 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Remaining GPU Features — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Wire and implement the 4 remaining unwired GPU features: multi-head attention integration, Decision Transformer CUDA kernels, ensemble training with diversity loss, and HER Future strategy with GPU episode boundaries.
**Architecture:** Each feature integrates into the existing fused CUDA training pipeline (`GpuDqnTrainer` + CUDA Graph). All kernels are GPU-native (zero CPU in hot path). Post-graph operations use the `EventTrackingGuard` RAII pattern for safe cudarc buffer access. Kernels are compiled via nvcc → cubin (cached at `/tmp/.cubin_cache/`).
**CUDA Graph Integration Strategy:** The CUDA Graph captures the full forward+loss+backward+Adam sequence. Features that modify intermediate activations (attention, ensemble heads) operate with a **1-step lag**: they modify `save_h_s2` AFTER graph replay, so the modification takes effect on the NEXT graph replay's unflatten step. This is the same pattern as the IQN trunk gradient and spectral normalization — standard in async gradient methods. The alternative (splitting the graph into trunk/head phases) is deferred until profiling shows the lag impacts convergence.
**VRAM Budget:**
| Feature | RTX 3050 (4GB) | H100 (80GB) |
|---------|---------------|-------------|
| Attention (params + activations) | ~0.5MB | ~2MB |
| Decision Transformer (params + context) | ~4MB | ~60MB |
| Ensemble (K=3 head copies) | ~1.5MB | ~6MB |
| HER episode tracking | ~0.5MB | ~4MB |
| **Total** | **~6.5MB** | **~72MB** |
**Tech Stack:** Rust (cudarc 0.19.3), CUDA C (nvcc/cubin), cuBLAS SGEMM
---
## File Structure
### New Files
| File | Responsibility |
|------|---------------|
| `crates/ml/src/cuda_pipeline/attention_backward_kernel.cu` | Attention backward pass: LayerNorm backward + output projection backward + attention weight gradients |
| `crates/ml/src/cuda_pipeline/dt_kernels.cu` | Decision Transformer forward: causal attention, softmax, positional encoding, token embedding, FFN, cross-entropy loss. Backward: softmax_backward, attention_backward (dQ/dK/dV), ffn_backward, layernorm_backward, embed_backward — 10 kernels total |
| `crates/ml/src/cuda_pipeline/ensemble_kernels.cu` | Ensemble: multi-head Q-aggregation, KL-divergence diversity loss, Thompson sampling action selection |
| `crates/ml/src/cuda_pipeline/her_episode_kernel.cu` | HER: episode boundary tracking, Future strategy donor sampling, episode-end detection |
### Modified Files
| File | Changes |
|------|---------|
| `crates/ml/src/cuda_pipeline/gpu_attention.rs` | Add `backward()`, `params_buf()`, Adam state; wire into `GpuDqnTrainer` |
| `crates/ml/src/cuda_pipeline/batched_forward.rs` | Add attention call between h_s2 and value/advantage heads |
| `crates/ml/src/cuda_pipeline/batched_backward.rs` | Add attention backward in the gradient chain |
| `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` | Conditional attention in forward/backward; ensemble head management |
| `crates/ml/src/cuda_pipeline/decision_transformer.rs` | Implement `pretrain_step()` with real CUDA kernels |
| `crates/ml/src/cuda_pipeline/gpu_her.rs` | Add Final/Future strategies, episode boundary buffer |
| `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs` | Add episode_ids to GpuExperienceBatch; fill via CUDA kernel (not CPU) |
| `crates/ml/src/trainers/dqn/fused_training.rs` | Wire attention, ensemble, HER strategies |
| `crates/ml/src/trainers/dqn/config.rs` | Add `use_attention: bool` to `DQNHyperparameters` (field exists in `DQNParams` hyperopt struct but not in the training config); wire from `DQNParams` during hyperopt conversion |
| `crates/ml/src/trainers/dqn/trainer/training_loop.rs` | Ensemble head rotation, DT pre-training phase, HER episode tracking |
| `crates/ml/src/hyperopt/adapters/dqn.rs` | Add DT pre-training epochs, ensemble params to search space |
---
## Task 1: Attention Forward Pass Integration
**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`
- Modify: `crates/ml/src/cuda_pipeline/gpu_attention.rs`
- Modify: `crates/ml/src/trainers/dqn/fused_training.rs`
- [ ] **Step 1: Add `GpuAttention` as optional field in `FusedTrainingCtx`**
In `fused_training.rs`, add to `FusedTrainingCtx`:
```rust
pub(crate) gpu_attention: Option<GpuAttention>,
```
Initialize it in `new()` when `hyperparams.use_attention` is true (similar to `gpu_iqn` initialization pattern at line 233).
- [ ] **Step 2: Wire attention forward BETWEEN trunk h_s2 and value/advantage heads**
In `gpu_dqn_trainer.rs`, add a method:
```rust
pub fn apply_attention_forward(
&self,
attention: &mut GpuAttention,
h_s2: &CudaSlice<f32>,
batch_size: usize,
) -> Result<(), MLError>
```
This copies `h_s2` into attention input, runs `attention.forward()`, then copies the attended output BACK into `save_h_s2` (in-place replacement). The CUDA Graph captures the trunk forward FIRST, then attention runs OUTSIDE the graph (same pattern as IQN/spectral norm), then value/advantage heads run.
- [ ] **Step 3: Call attention in `run_full_step()` after CUDA Graph replay**
After Step 2 (graph replay) and before Step 5 (IQN):
```rust
if let Some(ref mut attn) = self.gpu_attention {
let _evt_guard = EventTrackingGuard::new(self.stream.context());
self.trainer.apply_attention_forward(attn, ...)?;
}
```
- [ ] **Step 4: Compile check**
```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```
- [ ] **Step 5: Commit**
```bash
git add crates/ml/src/cuda_pipeline/gpu_attention.rs crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs crates/ml/src/trainers/dqn/fused_training.rs
git commit -m "feat: wire attention forward pass into DQN training (post-graph, frozen weights)"
```
---
## Task 2: Attention Backward Pass Kernel
**Files:**
- Create: `crates/ml/src/cuda_pipeline/attention_backward_kernel.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_attention.rs`
- [ ] **Step 1: Write the attention backward CUDA kernel**
The kernel computes `dL/d(h_s2_input)` given `dL/d(h_s2_attended)`:
```cuda
extern "C" __global__ void attention_backward_kernel(
const float* d_output, // [B, D] gradient from downstream
const float* states_input, // [B, D] saved input to attention
const float* params, // attention weights
float* d_input, // [B, D] gradient to upstream (trunk)
float* d_params, // [total_params] weight gradients (atomicAdd)
int B
);
```
The backward flows through:
1. LayerNorm backward → d_prenorm
2. Residual: d_prenorm splits into d_projection + d_residual
3. Output projection backward: d_projection → d_concat, dW_O, db_O
4. Multi-head attention backward → dQ, dK, dV per head
5. Q/K/V projection backward → d_input, dW_Q, dW_K, dW_V
Since the kernel is complex (matching the 237-line forward kernel), implement in phases:
- Phase A: Residual passthrough only (d_input = d_output) — gradient flows, weights frozen
- Phase B: Full backward with weight gradients (unfreezes attention weights)
Start with Phase A (trivial kernel, verifiable).
- [ ] **Step 2: Add `backward()` method to `GpuAttention`**
```rust
pub fn backward(
&self,
d_output: &CudaSlice<f32>,
d_input: &mut CudaSlice<f32>,
batch_size: usize,
) -> Result<(), MLError>
```
For Phase A: simply `memcpy_dtod(d_input, d_output)` — residual gradient passthrough.
- [ ] **Step 3: Wire backward into `apply_iqn_trunk_gradient`**
After attention forward rewrites `save_h_s2`, the IQN trunk gradient flows through the attention backward before reaching the trunk layers:
```rust
// Before trunk backward: d_h_s2 → attention_backward → d_h_s2_input
if let Some(ref attn) = self.gpu_attention {
attn.backward(&bw_d_h_s2, &mut bw_d_h_s2, batch_size)?;
}
```
- [ ] **Step 4: Compile check + local test**
```bash
SQLX_OFFLINE=true cargo check --workspace 2>&1 | tail -5
```
- [ ] **Step 5: Commit**
```bash
git add crates/ml/src/cuda_pipeline/attention_backward_kernel.cu crates/ml/src/cuda_pipeline/gpu_attention.rs
git commit -m "feat: attention backward (Phase A — residual passthrough, gradient flow verified)"
```
---
## Task 3: Decision Transformer CUDA Kernels
**Files:**
- Create: `crates/ml/src/cuda_pipeline/dt_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/decision_transformer.rs`
- [ ] **Step 1: Write the token embedding + positional encoding kernel**
```cuda
extern "C" __global__ void dt_embed_kernel(
const float* trajectories, // [B, T, state_dim+2] (return-to-go, state, action)
const float* W_embed, // [(state_dim+2), embed_dim]
const float* b_embed, // [embed_dim]
const float* pos_embed, // [T, embed_dim] (learned positional encoding)
float* output, // [B, T, embed_dim]
int B, int T, int input_dim, int embed_dim
);
```
Each thread handles one (batch, timestep, embed_dim_idx) tuple:
- Linear projection: `out[b][t][d] = sum_j(traj[b][t][j] * W[j][d]) + b[d]`
- Add positional: `out[b][t][d] += pos[t][d]`
- [ ] **Step 2: Write the causal self-attention kernel**
```cuda
extern "C" __global__ void dt_causal_attention_kernel(
const float* Q, // [B, T, embed_dim]
const float* K, // [B, T, embed_dim]
const float* V, // [B, T, embed_dim]
float* output, // [B, T, embed_dim]
int B, int T, int D, int num_heads
);
```
Per-head computation with causal mask:
- `attn[i][j] = Q[i] · K[j] / sqrt(D_h)` for j ≤ i (causal), -inf otherwise
- Softmax over j dimension
- `output[i] = sum_j(attn[i][j] * V[j])`
Block-per-sample, threads handle different timesteps. Shared memory for K/V caching.
- [ ] **Step 3: Write the FFN kernel (embed_dim → 4×embed_dim → embed_dim)**
```cuda
extern "C" __global__ void dt_ffn_kernel(
const float* input, // [B, T, embed_dim]
const float* W1, // [embed_dim, 4*embed_dim]
const float* b1, // [4*embed_dim]
const float* W2, // [4*embed_dim, embed_dim]
const float* b2, // [embed_dim]
float* output, // [B, T, embed_dim]
int B, int T, int D
);
```
GELU activation between layers. Residual connection: `output += input`.
- [ ] **Step 4: Write the cross-entropy loss kernel**
```cuda
extern "C" __global__ void dt_cross_entropy_kernel(
const float* logits, // [B, T, num_actions]
const int* target_actions, // [B, T]
float* per_sample_loss, // [B*T]
float* total_loss, // [1] atomicAdd
int B, int T, int num_actions
);
```
Per-token cross-entropy: `loss = -log(softmax(logits)[target_action])`.
- [ ] **Step 5: Implement `pretrain_step()` in decision_transformer.rs**
Wire the 5 forward kernels into a training loop. Then:
- [ ] **Step 5b: Write 5 backward CUDA kernels**
```cuda
// In dt_kernels.cu — add after forward kernels:
// dt_cross_entropy_backward_kernel: dL/d_logits = softmax - one_hot
// dt_ffn_backward_kernel: GELU backward + 2 linear backward
// dt_attention_backward_kernel: dQ,dK,dV from d_output (softmax Jacobian + causal mask)
// dt_layernorm_backward_kernel: chain through mean/variance
// dt_embed_backward_kernel: dW_embed from d_embedded
```
Each mirrors its forward kernel's thread/block layout. Total: 10 kernels.
- [ ] **Step 5c: Implement `pretrain_step()` in decision_transformer.rs**
1. **Forward:** Embed → {QKV → CausalAttn → LN → FFN → LN} × N → ActionHead → CE loss
2. **Backward:** CE backward → reverse chain through all N layers → embed backward
3. **Optimizer:** Reuse `dqn_grad_norm_kernel` + `dqn_adam_update_kernel` on DT params
DT action space = 9 exposure actions (branch_0 only, not full 81 factored).
- [ ] **Step 6: Compile check**
```bash
SQLX_OFFLINE=true cargo check -p ml 2>&1 | tail -5
```
- [ ] **Step 7: Commit**
```bash
git add crates/ml/src/cuda_pipeline/dt_kernels.cu crates/ml/src/cuda_pipeline/decision_transformer.rs
git commit -m "feat: Decision Transformer CUDA kernels (embed, causal attention, FFN, CE loss)"
```
---
## Task 4: Decision Transformer Pre-Training Loop
**Files:**
- Modify: `crates/ml/src/trainers/dqn/trainer/training_loop.rs`
- Modify: `crates/ml/src/trainers/dqn/config.rs`
- Modify: `crates/ml/src/hyperopt/adapters/dqn.rs`
- [ ] **Step 1: Add DT pre-training config fields**
In `DQNHyperparameters`:
```rust
pub dt_pretrain_epochs: usize, // 0 = disabled, 10-50 typical
pub dt_context_len: usize, // 20 timesteps
pub dt_embed_dim: usize, // 128
pub dt_num_layers: usize, // 3
pub dt_target_return: f64, // 2.0 (target Sharpe-like return)
```
- [ ] **Step 2: Add DT pre-training phase at training start**
In `training_loop.rs`, before the main training loop:
```rust
if self.hyperparams.dt_pretrain_epochs > 0 {
let dt = DecisionTransformer::new(stream, dt_config)?;
for epoch in 0..self.hyperparams.dt_pretrain_epochs {
// Build trajectory batches from training_data
// Run dt.pretrain_step(trajectories, target_actions, batch_size)
// Log DT loss
}
// Transfer DT's learned representations to DQN trunk (optional)
}
```
- [ ] **Step 3: Add DT params to hyperopt search space**
In `dqn.rs`, add bounds for `dt_pretrain_epochs`, `dt_context_len`, `dt_embed_dim`.
- [ ] **Step 4: Compile check + commit**
```bash
SQLX_OFFLINE=true cargo check --workspace
git add crates/ml/src/cuda_pipeline/ crates/ml/src/trainers/dqn/ crates/ml/src/hyperopt/ && git commit -m "feat: Decision Transformer pre-training phase in training loop"
```
---
## Task 5: Ensemble Multi-Head Q-Network
**Files:**
- Create: `crates/ml/src/cuda_pipeline/ensemble_kernels.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`
- Modify: `crates/ml/src/trainers/dqn/fused_training.rs`
- Modify: `crates/ml/src/trainers/dqn/config.rs`
- [ ] **Step 1: Write the ensemble aggregation kernel**
```cuda
extern "C" __global__ void ensemble_aggregate_kernel(
const float* head_q_values, // [K, B, num_actions] Q-values from K heads
float* mean_q, // [B, num_actions] mean across heads
float* var_q, // [B, num_actions] variance across heads
int K, int B, int num_actions
);
```
Computes mean and variance of Q-values across K ensemble heads for uncertainty estimation.
- [ ] **Step 2: Write the KL-divergence diversity loss kernel**
```cuda
extern "C" __global__ void ensemble_diversity_kernel(
const float* head_logits, // [K, B, num_atoms] per-head C51 logits
float* diversity_loss, // [1] total KL divergence (atomicAdd)
int K, int B, int num_atoms
);
```
For each pair of heads (i, j): `D_KL(p_i || p_j)` averaged over the batch. Uses hierarchical reduction (warp reduce → block reduce → atomicAdd per block, same pattern as `dqn_grad_norm_kernel`) to avoid single-address atomicAdd serialization. The diversity loss ENCOURAGES disagreement (subtracted from total loss).
- [ ] **Step 3: Implement multi-head architecture in GpuDqnTrainer**
The ensemble shares the TRUNK (W_s1, W_s2) but has K SEPARATE value + advantage heads:
```rust
pub struct EnsembleHeads {
heads: Vec<(DuelingWeightSet, BranchingWeightSet)>, // K head weight sets
}
```
During training (heads run OUTSIDE the CUDA Graph, same pattern as IQN/attention):
1. CUDA Graph: forward through shared trunk → h_s2 (using head 0's weights in the graph)
2. Post-graph: for each head k=1..K-1, run cuBLAS forward through head_k's value/advantage layers on `save_h_s2` (outside graph, with EventTrackingGuard)
3. Aggregate Q-values: mean ± std across K heads
4. C51 loss per head (head 0 inside graph, heads 1..K-1 outside)
5. Diversity regularization across heads
6. Backward: each head contributes trunk gradients via the existing IQN-style SGD/Adam correction
Note: The CUDA Graph remains captured for head 0 only. Heads 1..K-1 run as post-graph operations with their own cuBLAS forwards. This avoids re-capturing the graph for each head.
- [ ] **Step 4: Add ensemble config and wiring**
In `DQNHyperparameters`:
```rust
pub ensemble_count: usize, // K heads (default 1 = no ensemble)
pub ensemble_diversity_weight: f64, // 0.01 = mild diversity encouragement
```
In `FusedTrainingCtx::run_full_step()`:
- If `ensemble_count > 1`: run K forward passes through different heads, average for action selection, sum losses with diversity term.
- [ ] **Step 5: Compile check + commit**
```bash
SQLX_OFFLINE=true cargo check --workspace
git add crates/ml/src/cuda_pipeline/ crates/ml/src/trainers/dqn/ crates/ml/src/hyperopt/ && git commit -m "feat: ensemble multi-head Q-network with KL diversity loss"
```
---
## Task 6: HER Episode Boundary Tracking
**Files:**
- Create: `crates/ml/src/cuda_pipeline/her_episode_kernel.cu`
- Modify: `crates/ml/src/cuda_pipeline/gpu_her.rs`
- Modify: `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs`
- [ ] **Step 1: Add episode index buffer to experience output**
In `GpuExperienceBatch`, add:
```rust
pub episode_ids: CudaSlice<i32>, // [N * L] episode index per transition
```
Fill via a simple CUDA kernel `fill_episode_ids_kernel(int* ids, int L, int N)` where thread `i` writes `ids[i] = i / L`. Launched as `grid=(ceil(N*L/256)), block=256`. Zero CPU fill — pure GPU.
- [ ] **Step 2: Track episode boundaries in GPU replay buffer**
When inserting transitions, also store the episode ID:
```rust
pub episode_id_buf: CudaSlice<i32>, // [capacity] episode index per slot
```
This allows the HER kernel to find other transitions from the same episode.
- [ ] **Step 3: Write the episode-aware donor sampling kernel**
```cuda
extern "C" __global__ void her_sample_future_donors(
const int* episode_ids, // [capacity] episode index per slot
const int* source_indices, // [her_batch_size] which to relabel
int* donor_indices, // [her_batch_size] output: future donors
unsigned int* rng_states, // [her_batch_size] per-sample RNG
int capacity,
int her_batch_size
);
```
Uses a GPU-resident `episode_boundary_offsets` auxiliary buffer (updated at insertion time) for O(log(N)) binary search instead of O(capacity) linear scan. Each thread binary-searches the boundary buffer to find its episode's [start, end) range, then uniformly samples from `[source+1, end)`.
- [ ] **Step 4: Write the episode-end detection kernel (Final strategy)**
```cuda
extern "C" __global__ void her_find_episode_end(
const int* episode_ids, // [capacity]
const int* source_indices, // [her_batch_size]
int* end_indices, // [her_batch_size] output: last index in same episode
int capacity,
int her_batch_size
);
```
For each source: binary search or linear scan to find the last index with the same episode_id.
- [ ] **Step 5: Wire Future/Final strategies in `GpuHer`**
```rust
pub fn relabel_batch_gpu(
&mut self,
strategy: HerGpuStrategy,
episode_ids: &CudaSlice<i32>,
...
) -> Result<&HerBatch, MLError> {
let donors = match strategy {
HerGpuStrategy::Random => self.random_donors(her_batch_size),
HerGpuStrategy::Future => self.sample_future_donors(episode_ids, source_indices)?,
HerGpuStrategy::Final => self.find_episode_ends(episode_ids, source_indices)?,
};
self.relabel_kernel(source_indices, donors, ...)?;
Ok(&self.output)
}
```
- [ ] **Step 6: Compile check + commit**
```bash
SQLX_OFFLINE=true cargo check --workspace
git add crates/ml/src/cuda_pipeline/ crates/ml/src/trainers/dqn/ crates/ml/src/hyperopt/ && git commit -m "feat: HER Future/Final strategies with GPU episode boundary tracking"
```
---
## Task 7: Integration Test — Full Pipeline
**Files:**
- Modify: `crates/ml/src/trainers/dqn/trainer/training_loop.rs`
- [ ] **Step 1: Wire all 4 features into the training loop conditionally**
```rust
// Attention: after trunk forward, before heads
if hyperparams.use_attention { ... }
// DT pre-training: before main training loop
if hyperparams.dt_pretrain_epochs > 0 { ... }
// Ensemble: K heads with diversity loss
if hyperparams.ensemble_count > 1 { ... }
// HER: Future strategy with episode boundaries
if hyperparams.her_ratio > 0.0 && hyperparams.her_strategy == "future" { ... }
```
- [ ] **Step 2: Local test — all features enabled**
```bash
rm -rf /tmp/.cubin_cache/
SQLX_OFFLINE=true cargo build --release -p ml --example hyperopt_baseline_rl
SQLX_OFFLINE=true timeout 120 ./target/release/examples/hyperopt_baseline_rl \
--model dqn --phase fast --trials 1 --n-initial 1 --epochs 3 \
--data-dir test_data/futures-baseline --symbol ES.FUT \
--output /tmp/full_pipeline_test.json --seed 42
```
Expected: No NaN, no CUDA errors, train_loss < 50.
- [ ] **Step 3: Commit**
```bash
git add crates/ml/src/cuda_pipeline/ crates/ml/src/trainers/dqn/ crates/ml/src/hyperopt/ && git commit -m "feat: full pipeline integration — attention + DT + ensemble + HER Future"
```
---
## Task 8: H100 Submission
- [ ] **Step 1: Push to main**
```bash
git push origin main
```
- [ ] **Step 2: Submit H100 hyperopt run**
50 epochs, 20 trials, with all features enabled via config:
```toml
use_attention = true
dt_pretrain_epochs = 10
ensemble_count = 3
her_ratio = 0.3
her_strategy = "future"
```
- [ ] **Step 3: Monitor first trial**
Check: no NaN, no CUDA OOM, loss decreasing, Q-gap > 0.
---
## Dependency Graph
```
Task 1 (Attention Forward) → Task 2 (Attention Backward) → Task 7 (Integration)
Task 3 (DT Kernels) → Task 4 (DT Loop) → Task 7 (Integration)
Task 5 (Ensemble) → Task 7 (Integration)
Task 6 (HER Episodes) → Task 7 (Integration)
Task 7 (Integration) → Task 8 (H100)
```
Tasks 1-2, 3-4, 5, and 6 are INDEPENDENT and can be implemented in parallel.