Prerequisite for the deferred Fix 30 Stale-B kernel-side fix
(`backtest_plan_kernel.cu` raw_close source). The DtoD-via-pinned
pre-commit guard (`scripts/pre-commit-hook.sh::check_no_dtod_via_pinned`,
commit `5275932f4`) blocks any commit that stages `gpu_backtest_evaluator.rs`
against the file's 5 pre-existing `clone_to_device_*_via_pinned` callers
that landed before the guard. Stale-B has to stage this file (to thread
`prices_buf` into the `backtest_plan_state_isv` launcher), so the
migration must land first as its own atomic commit per
`feedback_no_partial_refactor`.
Migration scope. Single file, four buffer fields:
- prices_buf CudaSlice<f32> → MappedF32Buffer [n*max_len*4]
- features_buf CudaSlice<f32> → MappedF32Buffer [n*max_len*feat_dim]
- portfolio_buf CudaSlice<f32> → MappedF32Buffer [n*8] (kernel-mutated)
- window_lens_buf CudaSlice<i32> → MappedI32Buffer [n]
Init sites (lines ~651-664 post-edit). 4 `clone_to_device_*_via_pinned`
calls replaced with `MappedF32Buffer::new(host.len())` +
`write_from_slice(host)`. No memcpy_dtod_async + stream.synchronize()
pair at construction — mapped-pinned coherence makes the kernel see
host writes after the next stream-sync barrier.
Reset site (`reset_evaluation_state`, line ~1485 post-edit). In-place
`self.portfolio_buf.write_from_slice(&portfolio_init)` replaces the
prior buffer-replacement
`self.portfolio_buf = clone_to_device_f32_via_pinned(...)`. No alloc
churn per epoch; the device pointer stays stable across resets which
matches MappedF32Buffer's intended use.
Consumer sites (17 kernel arg passes). `arg(&self.X_buf)` →
`arg(&self.X_buf.dev_ptr)` so the launcher receives the device pointer
the kernel expects. Sites: launch_gather (×2), launch_gather_chunk,
launch_env_step, env_batch_kernel chunked path, plan_state_isv kernel,
metrics_kernel. CUdeviceptr (u64) is passed by reference exactly as
metrics_dev_ptr already does at the metrics launch site (~line 2497).
Kernel-mutated MappedF32Buffer precedent. portfolio_buf is mutated by
the env_step kernel every step. The same file's `plan_diag_buf` (a
MappedF32Buffer) is also kernel-written via dev_ptr (lines ~1230-1256)
and host-read via host_ptr — direct precedent. The IQN τ migration
(commit `facbf76eb`) confirms cuMemHostAlloc DEVICEMAP for kernel
reads. The mapped_pinned.rs module docstring states explicitly:
"Kernels write through dev_ptr (with __threadfence_system())".
What this change touches:
- crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs
Field types (lines 299-321), init block (lines 641-665), reset
(lines 1462-1471), 17 kernel arg passes across 6 launchers.
- docs/dqn-wire-up-audit.md
Fix 30 Stale-B paragraph extended with the prereq commit summary.
Stale-B itself remains DEFERRED (kernel-side fix lands next).
Verification:
- SQLX_OFFLINE=true cargo check -p ml --offline (44.38s) clean.
- SQLX_OFFLINE=true cargo build -p ml --release --offline
--features cuda (53.78s) clean.
- grep -n "clone_to_device_.*_via_pinned\|upload_.*_via_pinned"
crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs returns
zero hits.
- grep -n "self\.\(prices\|features\|window_lens\|portfolio\)_buf"
shows every kernel-arg site followed by `.dev_ptr`. The only
non-`.dev_ptr` references are the field declarations, the
constructor moves into `Self { ... }`, and the
`write_from_slice` calls in `reset_evaluation_state`.
- Pre-commit DtoD-via-pinned guard now passes on staging this file.
Eliminates the last 5 `_via_pinned` callers in
`gpu_backtest_evaluator.rs`. Per
`feedback_no_htod_htoh_only_mapped_pinned`, `feedback_no_partial_refactor`
(every consumer of a field-type change migrates in one commit),
`feedback_no_hiding` (no `--no-verify`; the migration IS the fix the
guard is asking for).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>