docs: fix 5 chunk 2 re-review issues in CUDA backtest plan

- Task 9: memcpy_stod_inplace → memcpy_htod (cudarc 0.17 API)
- Task 9: Fix transposed actions_history layout — accumulate CPU-side
  in window-major [window][step] layout, upload once before metrics kernel
- Task 9: Replace non-existent Tensor::from_raw_buffer with download-
  and-reupload pattern (temporary, replaced by Task 11 GPU gather kernel)
- Task 10: DqnOptimizer → DQNTrainer (hyperopt adapter) (actual struct name)
- Task 10: internal_trainer type → &InternalDQNTrainer (avoids name conflict
  with adapter's own DQNTrainer alias)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-11 09:40:51 +01:00
parent 33bcc44bd4
commit 208cee4cf5

View File

@@ -1169,7 +1169,11 @@ pub struct GpuBacktestEvaluator {
actions_history_buf: CudaSlice<i32>, // [n_windows * max_len]
// Output
metrics_buf: CudaSlice<f32>, // [n_windows * 6]
metrics_buf: CudaSlice<f32>, // [n_windows * 10]
// CPU-side action history (window-major: [window][step])
// Accumulated during step loop, uploaded once before metrics kernel.
actions_history_cpu: Vec<i32>, // [n_windows * max_len]
// Config
n_windows: usize,
@@ -1280,6 +1284,7 @@ impl GpuBacktestEvaluator {
prices_buf, features_buf, window_lens_buf,
portfolio_buf, step_rewards_buf, step_returns_buf,
done_buf, actions_buf, actions_history_buf, metrics_buf,
actions_history_cpu: vec![0_i32; n_windows * max_len],
n_windows, max_len, feature_dim, config,
})
}
@@ -1308,9 +1313,17 @@ impl GpuBacktestEvaluator {
// For initial correctness, we use Candle narrow ops + small portfolio download.
// Use Candle narrow ops to gather from GPU features tensor
// without downloading the full buffer.
let features_tensor = Tensor::from_raw_buffer(
&self.features_buf, DType::F32,
&[self.n_windows, self.max_len, self.feature_dim], device,
// Wrap pre-uploaded CudaSlice as Candle Tensor.
// NOTE: `Tensor::from_raw_buffer` does not exist in Candle. Use the established
// pattern: allocate zeros tensor, extract CudaSlice via storage_and_layout(),
// then memcpy_dtod_async. Or simply download-and-reupload for this temporary path.
// This gather_states is replaced by a GPU gather kernel in Task 11 anyway.
let mut flat_feats = vec![0.0_f32; self.n_windows * self.max_len * self.feature_dim];
self.stream.memcpy_dtoh(&self.features_buf, &mut flat_feats)
.map_err(|e| MLError::ModelError(format!("features download: {e}")))?;
let features_tensor = Tensor::from_vec(
flat_feats,
(self.n_windows, self.max_len, self.feature_dim), device,
).map_err(|e| MLError::ModelError(format!("features tensor: {e}")))?;
// Narrow to current step: [n_windows, feat_dim]
@@ -1366,20 +1379,16 @@ impl GpuBacktestEvaluator {
let actions_vec: Vec<u32> = actions_tensor.to_vec1()?;
let actions_i32: Vec<i32> = actions_vec.iter().map(|&a| a as i32).collect();
// Upload actions
self.stream.memcpy_stod_inplace(&actions_i32, &mut self.actions_buf)
// Upload actions to GPU (cudarc 0.17: memcpy_htod, not memcpy_stod_inplace)
self.stream.memcpy_htod(&actions_i32, &mut self.actions_buf)
.map_err(|e| MLError::ModelError(format!("actions upload: {e}")))?;
// Copy actions to history buffer for metrics kernel trade counting.
// GPU-to-GPU copy at correct offset. cudarc memcpy_dtod with offset:
let history_offset = step * self.n_windows;
unsafe {
self.stream
.memcpy_dtod(
&self.actions_buf,
&mut self.actions_history_buf.slice(history_offset..history_offset + self.n_windows),
)
.map_err(|e| MLError::ModelError(format!("actions history copy: {e}")))?;
// Track actions in CPU-side history (window-major layout: [window][step]).
// The metrics kernel reads actions_history[w * max_len + i], so we must match
// that layout. A single contiguous DtoD copy can't scatter to strided offsets,
// so we accumulate on CPU and upload once before the metrics kernel.
for w in 0..self.n_windows {
self.actions_history_cpu[w * self.max_len + step] = actions_i32[w];
}
// NOTE: Task 11 replaces the CPU gather_states above with a GPU gather kernel
@@ -1422,7 +1431,11 @@ impl GpuBacktestEvaluator {
}
}
// 5. Launch metrics reduction kernel
// 5. Upload accumulated actions history for metrics kernel trade counting
self.stream.memcpy_htod(&self.actions_history_cpu, &mut self.actions_history_buf)
.map_err(|e| MLError::ModelError(format!("actions_history upload: {e}")))?;
// 6. Launch metrics reduction kernel
let shmem_bytes = (256 * 6 * 4) as u32; // 6 reduction arrays × 256 threads × f32
let metrics_config = LaunchConfig {
grid_dim: (self.n_windows as u32, 1, 1),
@@ -1535,9 +1548,9 @@ Wire `GpuBacktestEvaluator` into the DQN hyperopt adapter's backtest evaluation.
- Modify: `crates/ml/src/hyperopt/adapters/dqn.rs:2862-3165` (backtest evaluation section)
- Test: `SQLX_OFFLINE=true cargo test -p ml --lib -- hyperopt_dqn --no-capture`
- [ ] **Step 1: Add GPU evaluator field to DqnOptimizer**
- [ ] **Step 1: Add GPU evaluator field to DQNTrainer (hyperopt adapter)**
In `dqn.rs`, find the `DqnOptimizer` struct fields and add:
In `dqn.rs`, find the `DQNTrainer (hyperopt adapter)` struct fields and add:
```rust
/// GPU backtest evaluator (initialized on first use)
@@ -1549,14 +1562,14 @@ Initialize as `None` in the builder.
- [ ] **Step 2: Add GPU evaluation method**
Add a method to `DqnOptimizer`:
Add a method to `DQNTrainer (hyperopt adapter)`:
```rust
/// Run backtest evaluation on GPU (zero CPU roundtrips during eval).
#[cfg(feature = "cuda")]
fn evaluate_gpu(
&mut self,
internal_trainer: &DQNTrainer,
internal_trainer: &InternalDQNTrainer,
val_close_prices: &[f64],
window_size: usize,
stride: usize,