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:
@@ -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;\
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user