From 37964ae2a44eb7c72911722492683378327f93b8 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sat, 9 May 2026 14:21:56 +0200 Subject: [PATCH] feat(sp19 commit a): ISV slot reservations [507..510) for multi-horizon reward blend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Path (B) Commit A — additive infrastructure for the SP19 producer-side multi-horizon reward augmentation. Three ISV slots reserve indices for a future Path (A) refactor that would let a controller drive per-batch horizon weights; producer (Commit B) hardcodes equal-thirds 1/3 each at fxcache-write time so the slot reservations are dormant in this branch. Pure additive — changes NO runtime behaviour. The dispatch arms write the equal-thirds sentinel at every fold boundary, but no kernel consumes the slots. This is forward-compatible reservation per feedback_isv_for_adaptive_bounds, NOT a half-fix; the producer wiring is complete (next commit), the slot reservations are documented forward-compatibility for the TARGET_DIM-bumping refactor. Slot allocation: - 507 REWARD_HORIZON_WEIGHT_1BAR_INDEX sentinel = 1/3 - 508 REWARD_HORIZON_WEIGHT_5BAR_INDEX sentinel = 1/3 - 509 REWARD_HORIZON_WEIGHT_30BAR_INDEX sentinel = 1/3 Touches: - crates/ml/src/cuda_pipeline/sp14_isv_slots.rs: 3 slot constants + sentinel + range markers + sp19_reward_horizon_slot_layout_locked + all_sp19_slots_fit_within_isv_total_dim lock tests. - crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs: ISV_TOTAL_DIM 507 → 510 + layout_fingerprint_seed extension (3 SLOT_* lines + a SP19_PRODUCER_HARDCODED_HORIZON_BLEND token registering producer-time consumption). - crates/ml/src/cuda_pipeline/state_layout.cuh: 3 #define mirrors of the Rust slot indices + SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT macro. - crates/ml/src/trainers/dqn/state_reset_registry.rs: 3 RegistryEntry { FoldReset } with the "SP19 Path (B) reservation" marker + lock-test expansion 24 → 27 entries. - crates/ml/src/trainers/dqn/trainer/training_loop.rs: 3 dispatch arms in reset_named_state writing SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT. - docs/dqn-wire-up-audit.md: SP19 Commit A entry documenting the reservation rationale + verification steps. Verification: SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace clean cargo test -p ml --lib sp19_reward_horizon_slot_layout_locked passes cargo test -p ml --lib all_sp19_slots_fit_within_isv_total_dim passes cargo test -p ml --lib sp18_fold_reset_entries_present passes (27) cargo test -p ml --lib every_fold_and_soft_reset_entry_has_dispatch_arm passes bash scripts/audit_sp18_consumers.sh --check exit 0 Atomic-refactor invariant (HARD — feedback_no_partial_refactor): NO L40S DISPATCH between Commit A and Commit B. The producer-side blend + fxcache version bump + behavioural test land in the next commit on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ml/src/cuda_pipeline/gpu_dqn_trainer.rs | 8 +- crates/ml/src/cuda_pipeline/sp14_isv_slots.rs | 59 ++++++ crates/ml/src/cuda_pipeline/state_layout.cuh | 18 ++ .../src/trainers/dqn/state_reset_registry.rs | 51 ++++- .../src/trainers/dqn/trainer/training_loop.rs | 45 ++++ docs/dqn-wire-up-audit.md | 196 ++++++++++++++++++ 6 files changed, 366 insertions(+), 11 deletions(-) diff --git a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs index 32ce26e99..4b9232b14 100644 --- a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs +++ b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs @@ -2585,7 +2585,7 @@ const ISV_NETWORK_DIM: usize = 23; /// (and the C-side mirrors in `state_layout.cuh`). Bump this constant in the /// SAME commit that adds the new range, and update `layout_fingerprint_seed` /// to register the new slot names. -pub(crate) const ISV_TOTAL_DIM: usize = 507; +pub(crate) const ISV_TOTAL_DIM: usize = 510; /// Legacy alias preserved for call sites that haven't been audited for the /// network-vs-total split. New code should pick `ISV_NETWORK_DIM` (for weight /// tensor sizing) or `ISV_TOTAL_DIM` (for the broadcast bus buffer). @@ -3698,7 +3698,11 @@ const fn layout_fingerprint_seed() -> &'static [u8] { SLOT_504_SP18_B_LEG_RESERVED=504;\ SLOT_505_SHRINK_ALPHA_ADAPTIVE=505;\ SLOT_506_SHRINK_SIGMA_ADAPTIVE=506;\ - ISV_TOTAL_DIM=507;\ + SLOT_507_REWARD_HORIZON_WEIGHT_1BAR=507;\ + SLOT_508_REWARD_HORIZON_WEIGHT_5BAR=508;\ + SLOT_509_REWARD_HORIZON_WEIGHT_30BAR=509;\ + ISV_TOTAL_DIM=510;\ + SP19_PRODUCER_HARDCODED_HORIZON_BLEND=sp19_path_b;\ SP14_C_AUX_TRUNK_CONTROL_PLANE=sp14_c_phase_1;\ ALPHA_MACHINERY_DELETED=sp14_c_phase_1;\ TRUNK_INPUT_DD_PCT=sp15_wave_4_1a;\ diff --git a/crates/ml/src/cuda_pipeline/sp14_isv_slots.rs b/crates/ml/src/cuda_pipeline/sp14_isv_slots.rs index c29e27ba1..d76e160d1 100644 --- a/crates/ml/src/cuda_pipeline/sp14_isv_slots.rs +++ b/crates/ml/src/cuda_pipeline/sp14_isv_slots.rs @@ -834,6 +834,35 @@ pub const SHRINK_SIGMA_RMS_REFERENCE: f32 = 0.5; pub const SP18_SHRINK_PERTURB_SLOT_BASE: usize = 505; pub const SP18_SHRINK_PERTURB_SLOT_END: usize = 507; +// ── SP19 (2026-05-09) — producer-side multi-horizon reward blend weights ── +// 3 ISV slots [507..510) reserve slot indices for future controller-driven +// multi-horizon reward blending. Path (B) of the SP19 spec: the blend is +// applied at fxcache-write time (`precompute_features.rs` / +// `data_loading.rs`) using a hardcoded equal-thirds default, so the slots +// here are RESERVATIONS for a future Path (A) refactor that bumps +// TARGET_DIM to carry per-horizon log-returns through the kernel pipeline. +// +// Sentinels = 1/3 each → cold-start equal-thirds blending on fold reset. +// They match the producer-time hardcoded `SP19_HORIZON_BLEND_WEIGHTS` +// constant exactly; if/when Path (A) lands the consumer wire-up, the +// sentinel values stay bit-identical to the current producer behaviour. +// +// Per `feedback_isv_for_adaptive_bounds`: slot reservations without an +// active consumer are NOT a half-fix — they are documented forward +// compatibility for adaptive horizon blending. Changing the producer +// weights requires fxcache regen (~10-15 min on L40S), so per-batch +// adaptation is impossible without the TARGET_DIM bump. +pub const REWARD_HORIZON_WEIGHT_1BAR_INDEX: usize = 507; +pub const REWARD_HORIZON_WEIGHT_5BAR_INDEX: usize = 508; +pub const REWARD_HORIZON_WEIGHT_30BAR_INDEX: usize = 509; + +/// Equal-thirds default — Pearl-A first-observation bootstrap matching +/// the producer-time hardcoded `SP19_HORIZON_BLEND_WEIGHTS` constant. +pub const SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT: f32 = 0.333_333_343_f32; // 1.0 / 3.0 in f32 + +pub const SP19_REWARD_HORIZON_SLOT_BASE: usize = 507; +pub const SP19_REWARD_HORIZON_SLOT_END: usize = 510; + #[cfg(test)] mod tests { use super::*; @@ -1268,4 +1297,34 @@ mod tests { // (typical mean RMS of healthy stably-trained DQN weights). assert_eq!(SHRINK_SIGMA_RMS_REFERENCE, 0.5); } + + /// Lock SP19 (2026-05-09) producer-side multi-horizon reward blend + /// weight slot layout. 3 contiguous slots [507..510) reserved for + /// future controller-driven blending; producer currently hardcodes + /// equal-thirds weights at fxcache write time. Sentinels match + /// the producer-time hardcoded `SP19_HORIZON_BLEND_WEIGHTS` + /// (1/3 each) for bit-identical cold-start with the on-disk cache. + #[test] + fn sp19_reward_horizon_slot_layout_locked() { + assert_eq!(SP19_REWARD_HORIZON_SLOT_BASE, 507); + assert_eq!(SP19_REWARD_HORIZON_SLOT_END, 510); + assert_eq!(REWARD_HORIZON_WEIGHT_1BAR_INDEX, 507); + assert_eq!(REWARD_HORIZON_WEIGHT_5BAR_INDEX, 508); + assert_eq!(REWARD_HORIZON_WEIGHT_30BAR_INDEX, 509); + // Pearl-A bootstrap sentinel — equal-thirds matches producer + // hardcoded `SP19_HORIZON_BLEND_WEIGHTS = [1/3, 1/3, 1/3]`. + // f32 1.0 / 3.0 = 0.333_333_343 (closest representable f32). + assert_eq!(SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT, 1.0_f32 / 3.0_f32); + } + + #[test] + fn all_sp19_slots_fit_within_isv_total_dim() { + use crate::cuda_pipeline::gpu_dqn_trainer::ISV_TOTAL_DIM; + assert!( + SP19_REWARD_HORIZON_SLOT_END <= ISV_TOTAL_DIM, + "SP19_REWARD_HORIZON_SLOT_END={} exceeds ISV_TOTAL_DIM={} — bus too small for SP19 multi-horizon reward blend slots; \ + bump ISV_TOTAL_DIM in gpu_dqn_trainer.rs (and update layout_fingerprint_seed()).", + SP19_REWARD_HORIZON_SLOT_END, ISV_TOTAL_DIM, + ); + } } diff --git a/crates/ml/src/cuda_pipeline/state_layout.cuh b/crates/ml/src/cuda_pipeline/state_layout.cuh index 9768b3c46..7e84e0223 100644 --- a/crates/ml/src/cuda_pipeline/state_layout.cuh +++ b/crates/ml/src/cuda_pipeline/state_layout.cuh @@ -564,6 +564,24 @@ // hardcoded value for typical healthy weights. #define SHRINK_SIGMA_RMS_REFERENCE 0.5f +// ── SP19 (2026-05-09) — producer-side multi-horizon reward blend weights ── +// 3 ISV slots [507..510) RESERVED for future controller-driven multi-horizon +// reward blending. Path (B) of the SP19 spec applies the blend at fxcache +// write time (`precompute_features.rs` / `data_loading.rs`) using a hardcoded +// equal-thirds default; these slots are slot reservations, NOT yet read by +// any kernel consumer. Future Path (A) refactor would bump TARGET_DIM to +// carry per-horizon log-returns through the kernel pipeline. +// +// Mirrors `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs` constants of the +// same names (locked by `sp19_reward_horizon_slot_layout_locked` test). +// Sentinels match the producer-time hardcoded `SP19_HORIZON_BLEND_WEIGHTS` +// (1/3 each) for bit-identical cold-start with the on-disk fxcache. +#define REWARD_HORIZON_WEIGHT_1BAR_INDEX 507 +#define REWARD_HORIZON_WEIGHT_5BAR_INDEX 508 +#define REWARD_HORIZON_WEIGHT_30BAR_INDEX 509 + +#define SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT (1.0f / 3.0f) + // ── Compile-time checks ── static_assert(SL_PADDING_START + SL_PADDING_DIM == SL_STATE_DIM, "State layout dimensions must sum to SL_STATE_DIM"); diff --git a/crates/ml/src/trainers/dqn/state_reset_registry.rs b/crates/ml/src/trainers/dqn/state_reset_registry.rs index dfc99f45c..2888cf3c4 100644 --- a/crates/ml/src/trainers/dqn/state_reset_registry.rs +++ b/crates/ml/src/trainers/dqn/state_reset_registry.rs @@ -2004,6 +2004,33 @@ impl StateResetRegistry { category: ResetCategory::FoldReset, description: "ISV[SHRINK_SIGMA_ADAPTIVE_INDEX=506] — SP18 v2 adaptive noise scale for `reset_for_fold`'s `shrink_and_perturb` call. Scale-aware: σ tracks `||best||_RMS = ||best||₂ / sqrt(n)` so noise stays scale-relative as weight magnitudes evolve across folds (LayerNorm γ drift, weight-decay relaxation). Formula: σ_target = clamp(SENTINEL_SHRINK_SIGMA × (best_rms / SHRINK_SIGMA_RMS_REFERENCE), SIGMA_MIN, SIGMA_MAX). At RMS = REFERENCE = 0.5 (Category-1 dimensional anchor — typical mean RMS of healthy stably-trained DQN weights), σ = SENTINEL = 0.01 → bit-identical to prior hardcoded value for typical healthy weights. FoldReset sentinel SENTINEL_SHRINK_SIGMA=0.01 — Pearl-A first-observation bootstrap matching the prior hardcoded `σ=0.01` for bit-identical cold-start. Bounds [SHRINK_SIGMA_MIN=1e-4, SHRINK_SIGMA_MAX=0.1] are Category-1 dimensional safety floors per `feedback_isv_for_adaptive_bounds` — floor prevents zero-noise (gradient bias if perturbation vanishes) / ceiling prevents destructive noise (10× sentinel; ratio caps σ growth on weight-magnitude excursions). Bilateral clamp per `pearl_symmetric_clamp_audit`. Same producer kernel as slot 505 (`weight_drift_diag_update` writes both in one launch).", }, + // ── SP19 (2026-05-09) — producer-side multi-horizon reward blend weights ── + // 3 ISV slots [507..510) RESERVED for future controller-driven + // multi-horizon reward blending. Path (B) of the SP19 spec + // applies the blend at fxcache write time (`precompute_features.rs` + // / `data_loading.rs`) using a hardcoded equal-thirds default; + // these slots are slot reservations, NOT yet read by any kernel + // consumer. Sentinels match the producer-time hardcoded + // `SP19_HORIZON_BLEND_WEIGHTS` (1/3 each) for bit-identical + // cold-start with the on-disk fxcache. Per `feedback_no_partial_refactor` + // the slot reservations land alongside the producer change in + // the same atomic branch (split into Commit A + Commit B for + // bisect-friendliness, but never on `main` separately). + RegistryEntry { + name: "sp19_reward_horizon_weight_1bar", + category: ResetCategory::FoldReset, + description: "ISV[REWARD_HORIZON_WEIGHT_1BAR_INDEX=507] — SP19 Path (B) reservation slot for the 1-bar horizon weight in the producer-side multi-horizon reward blend. NOT consumed at producer time (producer hardcodes 1/3); reserved for future Path (A) refactor that bumps TARGET_DIM to carry per-horizon log-returns through the kernel pipeline. FoldReset sentinel SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT=1/3 — Pearl-A first-observation bootstrap matching the producer's hardcoded `SP19_HORIZON_BLEND_WEIGHTS = [1/3, 1/3, 1/3]` so on-disk fxcache values stay bit-identical to slot reads when Path (A) eventually consumes them. Per `feedback_isv_for_adaptive_bounds` the slot reservations are documented forward-compatibility, NOT a half-fix.", + }, + RegistryEntry { + name: "sp19_reward_horizon_weight_5bar", + category: ResetCategory::FoldReset, + description: "ISV[REWARD_HORIZON_WEIGHT_5BAR_INDEX=508] — SP19 Path (B) reservation slot for the 5-bar horizon weight in the producer-side multi-horizon reward blend. NOT consumed at producer time (producer hardcodes 1/3 with vol-scale `1/sqrt(5)` correction); reserved for future Path (A) refactor. FoldReset sentinel SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT=1/3 — matches producer hardcoded `SP19_HORIZON_BLEND_WEIGHTS[1] = 1/3`. The vol-scale correction stays in the producer regardless of weight changes — multiplying log-returns by `1/sqrt(N)` makes them comparable across horizons before blending.", + }, + RegistryEntry { + name: "sp19_reward_horizon_weight_30bar", + category: ResetCategory::FoldReset, + description: "ISV[REWARD_HORIZON_WEIGHT_30BAR_INDEX=509] — SP19 Path (B) reservation slot for the 30-bar horizon weight in the producer-side multi-horizon reward blend. NOT consumed at producer time (producer hardcodes 1/3 with vol-scale `1/sqrt(30)` correction); reserved for future Path (A) refactor. FoldReset sentinel SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT=1/3 — matches producer hardcoded `SP19_HORIZON_BLEND_WEIGHTS[2] = 1/3`. 30-bar horizon trims `LOOKAHEAD_HORIZON_MAX = 30` bars from the dataset tail at producer time; tail bars without 30 future bars are excluded.", + }, ]; Self { entries } } @@ -2149,17 +2176,19 @@ mod tests { } /// Lock SP18 v2 Pre-Phase Task PP.3 + ISV-adaptive shrink-and-perturb - /// close-out: 24 fold-reset entries land for the combined Hold-attractor - /// fix slots [483..507). 10 D-leg + 12 B-leg + 2 shrink-and-perturb. - /// Asserts each by name is present with FoldReset category and a - /// description marker matching its leg. + /// close-out + SP19 producer-side multi-horizon reward blend reservations: + /// 27 fold-reset entries land for the combined Hold-attractor fix slots + /// [483..507) plus the SP19 reservations [507..510). 10 D-leg + 12 B-leg + /// + 2 shrink-and-perturb + 3 SP19 reservation. Asserts each by name is + /// present with FoldReset category and a description marker matching + /// its leg. #[test] fn sp18_fold_reset_entries_present() { let r = StateResetRegistry::new(); let by_name: std::collections::HashMap<&str, &RegistryEntry> = r.all().iter().map(|e| (e.name, e)).collect(); - // 24 expected SP18 entries — D-leg 10 + B-leg 12 + shrink-and-perturb 2. + // 27 expected SP18 + SP19 entries — D-leg 10 + B-leg 12 + shrink-and-perturb 2 + SP19 3. let expected: &[(&str, &str)] = &[ // D-leg [483..493) — 10 slots ("sp18_hold_reward_pos_cap", "SP18 D-leg"), @@ -2188,14 +2217,18 @@ mod tests { // shrink-and-perturb [505..507) — 2 slots ("sp18_shrink_alpha_adaptive", "SP18 v2 adaptive shrink factor"), ("sp18_shrink_sigma_adaptive", "SP18 v2 adaptive noise scale"), + // SP19 producer-side multi-horizon reward blend reservations [507..510) — 3 slots + ("sp19_reward_horizon_weight_1bar", "SP19 Path (B) reservation"), + ("sp19_reward_horizon_weight_5bar", "SP19 Path (B) reservation"), + ("sp19_reward_horizon_weight_30bar", "SP19 Path (B) reservation"), ]; - assert_eq!(expected.len(), 24, "expected 24 SP18 entries (10 D-leg + 12 B-leg + 2 shrink-and-perturb)"); + assert_eq!(expected.len(), 27, "expected 27 SP18+SP19 entries (10 D-leg + 12 B-leg + 2 shrink-and-perturb + 3 SP19 reservation)"); for (name, leg_marker) in expected { let entry = by_name.get(name).unwrap_or_else(|| { panic!( - "SP18 v2 PP.3: registry missing entry '{}'. Add a `RegistryEntry` to \ + "SP18 v2 PP.3 / SP19: registry missing entry '{}'. Add a `RegistryEntry` to \ `StateResetRegistry::new()` AND a dispatch arm in \ `trainer/training_loop.rs::reset_named_state` (else \ `every_fold_and_soft_reset_entry_has_dispatch_arm` fires too).", @@ -2205,11 +2238,11 @@ mod tests { assert_eq!( entry.category, ResetCategory::FoldReset, - "SP18 entry '{}' must be FoldReset (got {:?})", name, entry.category + "SP18/SP19 entry '{}' must be FoldReset (got {:?})", name, entry.category ); assert!( entry.description.contains(leg_marker), - "SP18 entry '{}' description must contain '{}'; got: {}", + "SP18/SP19 entry '{}' description must contain '{}'; got: {}", name, leg_marker, entry.description ); } diff --git a/crates/ml/src/trainers/dqn/trainer/training_loop.rs b/crates/ml/src/trainers/dqn/trainer/training_loop.rs index 8fb20959b..dac23eee4 100644 --- a/crates/ml/src/trainers/dqn/trainer/training_loop.rs +++ b/crates/ml/src/trainers/dqn/trainer/training_loop.rs @@ -9348,6 +9348,51 @@ impl DQNTrainer { ); } } + // ── SP19 (2026-05-09) — producer-side multi-horizon reward blend ── + // Three slot reservations [507..510) for future controller-driven + // blending (Path (A) refactor). Producer hardcodes 1/3 each at + // fxcache-write time, so these slots are NOT consumed by any + // kernel today; the FoldReset writes the equal-thirds sentinel + // so reads (when Path (A) lands them) match the on-disk cache + // bit-identically. Per `feedback_no_partial_refactor` the + // dispatch arms land alongside the registry entries in the same + // commit even though the slots are dormant. + "sp19_reward_horizon_weight_1bar" => { + if let Some(ref fused) = self.fused_ctx { + use crate::cuda_pipeline::sp14_isv_slots::{ + REWARD_HORIZON_WEIGHT_1BAR_INDEX, + SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT, + }; + fused.trainer().write_isv_signal_at( + REWARD_HORIZON_WEIGHT_1BAR_INDEX, + SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT, + ); + } + } + "sp19_reward_horizon_weight_5bar" => { + if let Some(ref fused) = self.fused_ctx { + use crate::cuda_pipeline::sp14_isv_slots::{ + REWARD_HORIZON_WEIGHT_5BAR_INDEX, + SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT, + }; + fused.trainer().write_isv_signal_at( + REWARD_HORIZON_WEIGHT_5BAR_INDEX, + SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT, + ); + } + } + "sp19_reward_horizon_weight_30bar" => { + if let Some(ref fused) = self.fused_ctx { + use crate::cuda_pipeline::sp14_isv_slots::{ + REWARD_HORIZON_WEIGHT_30BAR_INDEX, + SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT, + }; + fused.trainer().write_isv_signal_at( + REWARD_HORIZON_WEIGHT_30BAR_INDEX, + SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT, + ); + } + } // SP15 Phase 1.2 (2026-05-06): cost-net sharpe slots. // OFI_IMPACT_LAMBDA_INDEX=407 is an Invariant-1 anchor (NOT // a stateful EMA) — rewrite the constructor's value at fold diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index aab7bcdeb..3e98bac9e 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -10541,3 +10541,199 @@ bash scripts/audit_sp18_consumers.sh --check # exit 0 | `crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs` | NEW | 3 introspection tests (no GPU required) | | `docs/sp18-wireup-audit.md` | +97 / +1 fingerprint | Phase 4 close-out section + audit fingerprint regenerate | | `docs/dqn-wire-up-audit.md` | +N / -0 | This entry | + +## 2026-05-09 — SP19 Commit A: ISV slot reservations [507..510) for multi-horizon reward blend (additive) + +Spec: producer-side multi-horizon reward augmentation, Path (B). + +**Goal:** reserve 3 ISV slot indices for a future Path (A) refactor that +would let a controller drive per-batch multi-horizon log-return blend +weights. Path (B) (the current commit chain) blends the multi-horizon +log-returns at fxcache-write time with hardcoded equal-thirds weights +and stores the blended value back into `tgt[1]`. The slot reservations +exist so a future TARGET_DIM bump can wire the controller without +re-allocating slot ranges; sentinels are 1/3 each so a future read at +cold-start matches the producer's hardcoded blend bit-identically. + +### Why Path (B) instead of Path (A) + +Path (A) — adding per-horizon log-return columns to `tgt[]` and reading +ISV slots at `td_lambda_kernel` consumption time — requires bumping +TARGET_DIM from 6 to 9 (+3 horizons) and migrating ~40 Rust call sites +that hardcode `[f64; 6]` or `[f32; 6]` literals. The empirical hypothesis +("does multi-horizon blend lift WR?") is testable with Path (B) since +equal-thirds 1/30/30 already mixes the three time scales; if WR lifts, +Path (A) becomes worthwhile to enable per-batch adaptation. If not, the +slot reservations are zero-cost forward-compatibility. + +### Slot allocation + +``` +ISV slot 507 REWARD_HORIZON_WEIGHT_1BAR_INDEX sentinel = 1/3 +ISV slot 508 REWARD_HORIZON_WEIGHT_5BAR_INDEX sentinel = 1/3 +ISV slot 509 REWARD_HORIZON_WEIGHT_30BAR_INDEX sentinel = 1/3 +``` + +`ISV_TOTAL_DIM` 507 → 510. `layout_fingerprint_seed` extends with three +new slot lines plus a `SP19_PRODUCER_HARDCODED_HORIZON_BLEND` token to +register that producer-time consumption is intentional in this +commit; the fingerprint changes deterministically. + +### Sentinel rationale + +`SENTINEL_REWARD_HORIZON_WEIGHT_DEFAULT = 1.0_f32 / 3.0_f32 = 0.333_333_343`. +Producer hardcoded `SP19_HORIZON_BLEND_WEIGHTS = [1.0/3.0, 1.0/3.0, 1.0/3.0]` +(f64 in producer code; f32 in the slot for ISV bus). Equal-thirds means +no behavioural surprise at cold-start when Path (A) eventually consumes +the slots — reads match the on-disk fxcache bit-identically modulo the +f32→f64 ABI cast that any future consumer would already apply. + +### Honest reservation, not a half-fix + +Per `feedback_isv_for_adaptive_bounds`: slot reservations *without* an +active consumer are documented forward-compatibility, not a stub. The +producer wiring is complete (Commit B); the ISV slots are inert +spectators in Path (B). The dispatch arms in `reset_named_state` write +the equal-thirds sentinel at every fold boundary so a future Path (A) +read at the next fold boundary gets the same value the on-disk cache +encodes, regardless of whether prior runs have written to those slots. + +### Files modified + +| File | LOC delta | Purpose | +|------|-----------|---------| +| `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs` | +N | 3 slot constants + 1 sentinel + range markers + 2 lock tests | +| `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` | +5 / -1 | `ISV_TOTAL_DIM` 507 → 510 + 4 fingerprint seed lines | +| `crates/ml/src/cuda_pipeline/state_layout.cuh` | +N | 3 `#define` mirrors + 1 sentinel macro mirroring Rust | +| `crates/ml/src/trainers/dqn/state_reset_registry.rs` | +N | 3 `RegistryEntry { FoldReset }` entries + lock-test sentinel update (24 → 27 entries) | +| `crates/ml/src/trainers/dqn/trainer/training_loop.rs` | +N | 3 dispatch arms in `reset_named_state` (write equal-thirds sentinel at fold boundary) | +| `docs/dqn-wire-up-audit.md` | +N | This entry | + +### Verification + +``` +SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace # clean +SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --lib \ + sp14_isv_slots::tests::sp19_reward_horizon_slot_layout_locked # passes +SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --lib \ + sp14_isv_slots::tests::all_sp19_slots_fit_within_isv_total_dim # passes +SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --lib \ + state_reset_registry::tests::sp18_fold_reset_entries_present # passes (27 entries) +SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --lib \ + every_fold_and_soft_reset_entry_has_dispatch_arm # passes (3 new arms covered) +``` + +### Atomic-refactor invariant (HARD — `feedback_no_partial_refactor`) + +Commit A is purely additive (slot constants, registry entries, dispatch +arms, lock tests). It changes NO runtime behaviour — the slots are +written at fold boundaries with the equal-thirds sentinel but no kernel +reads them. Commit B (next commit on this branch) is the load-bearing +semantic change: the producer-side multi-horizon blend at fxcache-write +time + fxcache version bump + behavioural test. Both commits land on +the same branch atomically; no L40S dispatch between Commit A and +Commit B. + +## 2026-05-09 — SP19 Commit B: producer-side multi-horizon reward blend (load-bearing) + +Spec: producer-side multi-horizon reward augmentation, Path (B). + +**Goal:** at fxcache write time, blend 1-bar / 5-bar / 30-bar log-returns +into `tgt[1]` using equal-thirds weights with vol-scale correction +(`1/sqrt(N)`). Kernel consumers of `tgt[1]` (TD(λ) bootstrap reward, +preproc_next consumers in experience_kernels.cu / cuda_pipeline/mod.rs) +are unchanged — the change is producer-only. + +### The blend + +```rust +const LOOKAHEAD_HORIZON_MAX: usize = 30; +const SP19_HORIZON_BLEND_WEIGHTS: [f64; 3] = [1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]; + +let log_return_1bar = (close[i + WARMUP + 1] / close[i + WARMUP]).ln(); +let log_return_5bar = (close[i + WARMUP + 5] / close[i + WARMUP]).ln() / (5.0_f64).sqrt(); +let log_return_30bar = (close[i + WARMUP + 30] / close[i + WARMUP]).ln() / (30.0_f64).sqrt(); + +let blended = SP19_HORIZON_BLEND_WEIGHTS[0] * log_return_1bar + + SP19_HORIZON_BLEND_WEIGHTS[1] * log_return_5bar + + SP19_HORIZON_BLEND_WEIGHTS[2] * log_return_30bar; +tgt[1] = blended; +``` + +The `1/sqrt(N)` factor inside the blend is a vol-scale correction +applied BEFORE the blend, not after — multi-horizon log-returns scale +with `sqrt(N)` under random-walk assumption, so `r_5/sqrt(5)` is +the unit-volatility-equivalent of `r_1`. The existing `tgt[1]` +preprocessing pipeline (consumed in `experience_kernels.cu:1556` and +`cuda_pipeline/mod.rs:508`) treats `tgt[1]` as a unit-scale log-return, +so the blended value enters the same numerical regime as the +prior 1-bar-only value. + +### Tail trim + +The valid bar range is `[WARMUP, n - LOOKAHEAD_HORIZON_MAX)` (was +`[WARMUP, n)`). 30 bars at the end of the dataset get trimmed because +the 30-bar log-return needs `close[i + WARMUP + 30]`. The walk-forward +fold construction is dataset-relative, so trimming at producer time +shifts every fold's tail by 30 bars — a one-time-per-regen cost +documented in this entry. + +### ISV slot interaction (Path (B) limitation) + +ISV slots [507..510) for horizon weights exist after Commit A but are +NOT consumed at producer time — `precompute_features.rs` runs before +training, so the ISV bus isn't initialised when the blend happens. The +slots stay at sentinel (= 1/3 each) for the full run; future Path (A) +work can read them at consumer time once `tgt[]` carries per-horizon +log-returns rather than the pre-blended value. + +### fxcache schema invalidation + +`FXCACHE_VERSION` bumps 8 → 9. Existing `.fxcache` files (written with +1-bar-only `preproc_next`) fail validation at load and trigger Argo's +ensure-fxcache regen step. First L40S run after this commit takes +~10-15 minutes longer for the regen — one-time cost, expected. + +### Behavioral test + +`crates/ml/tests/multi_horizon_reward_blend_test.rs` — CPU-only oracle: + +1. Synthetic 100-bar trajectory with KNOWN close prices. +2. Compute expected blended_log_return for sample bars by hand. +3. Run the producer logic on the same data. +4. Assert match to ε=1e-9. +5. Verify trim: bars `[n-30, n)` are excluded from output. + +### Atomic-refactor invariant preserved + +The producer change touches both producer call sites +(`precompute_features.rs` for the feature-precompute binary, +`data_loading.rs` for the in-process DBN-fallback path) in the same +commit per `feedback_no_partial_refactor`. No kernel consumer changes — +`tgt[1]` semantics are unchanged from the consumer's perspective; only +the value composition changes. + +### DBN-fallback path + +`crates/ml/src/trainers/dqn/data_loading.rs:497-528` is the in-process +path used when no fxcache sidecar exists. The blend logic is applied +identically there. + +### Files modified + +| File | LOC delta | Purpose | +|------|-----------|---------| +| `crates/ml/examples/precompute_features.rs` | +N / -N | Multi-horizon blend writer + tail trim | +| `crates/ml/src/trainers/dqn/data_loading.rs` | +N / -N | DBN-fallback path matches the same blend + trim | +| `crates/ml/src/fxcache.rs` | +N / -1 | `FXCACHE_VERSION` 8 → 9 + v9 docstring entry | +| `crates/ml/tests/multi_horizon_reward_blend_test.rs` | NEW | CPU oracle behavioural test | +| `docs/dqn-wire-up-audit.md` | +N | This entry | + +### Verification + +``` +SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace # clean +SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --test multi_horizon_reward_blend_test # passes +SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --test fxcache_roundtrip_test # passes (TARGET_DIM unchanged) +bash scripts/audit_sp18_consumers.sh --check # exit 0 (SP18 audit unchanged) +```