feat(sp19 commit a): ISV slot reservations [507..510) for multi-horizon reward blend

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) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-09 14:21:56 +02:00
parent ce841eb56e
commit 37964ae2a4
6 changed files with 366 additions and 11 deletions

View File

@@ -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;\

View File

@@ -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,
);
}
}

View File

@@ -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");

View File

@@ -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
);
}

View File

@@ -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

View File

@@ -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)
```