diff --git a/crates/ml/src/cuda_pipeline/sp5_isv_slots.rs b/crates/ml/src/cuda_pipeline/sp5_isv_slots.rs index fd916a895..83965d0bc 100644 --- a/crates/ml/src/cuda_pipeline/sp5_isv_slots.rs +++ b/crates/ml/src/cuda_pipeline/sp5_isv_slots.rs @@ -1,28 +1,31 @@ -// crates/ml/src/cuda_pipeline/sp5_isv_slots.rs -// -// SP5 per-branch + per-group adaptation ISV slot allocation. -// -// All adaptive values live on ISV via Pearls A+D smoothing per -// `feedback_isv_for_adaptive_bounds`. Slot ranges: -// -// 174..226 Per-branch (4 branches × 13 quantities for Pearls 1/2/3 + shared signal) -// 226..250 Per-group Adam β1/β2/ε (8 groups × 3 hparams) -// 250..270 Per-branch IQN τ schedule (4 branches × 5 quantiles) -// 270..274 Per-direction trail distance (4 directions) -// 274..278 Per-branch num_atoms (4 branches) -// 280..286 Cross-fold-persistent Kelly slots (NOT in fold-reset registry) -// -// Total: 110 new SP5 ISV slots (52 + 24 + 20 + 4 + 4 + 6). +//! SP5 per-branch + per-group adaptation ISV slot allocation. +//! +//! All adaptive values live on ISV via Pearls A+D smoothing per +//! `feedback_isv_for_adaptive_bounds`. Slot ranges: +//! +//! 174..226 Per-branch (4 branches × 13 quantities for Pearls 1/2/3 + shared signal) +//! 226..250 Per-group Adam β1/β2/ε (8 groups × 3 hparams) +//! 250..270 Per-branch IQN τ schedule (4 branches × 5 quantiles) +//! 270..274 Per-direction trail distance (4 directions) +//! 274..278 Per-branch num_atoms (4 branches) +//! 278..280 Intentional 2-slot carve-out gap (boundary marker, not allocated) +//! 280..286 Cross-fold-persistent Kelly slots (NOT in fold-reset registry) +//! +//! Total: 110 new SP5 ISV slots (52 + 24 + 20 + 4 + 4 + 6). pub const SP5_SLOT_BASE: usize = 174; // ── Pearl 1: per-branch atom span (16 slots) ────────────────────────── +/// Adapts the atom distribution center per branch; tracks reward signal magnitude +/// so C51 atoms span the live return distribution rather than a fixed range. pub const ATOM_V_CENTER_BASE: usize = 174; // [4] pub const ATOM_V_HALF_BASE: usize = 178; // [4] pub const ATOM_HEADROOM_BASE: usize = 182; // [4] pub const ATOM_CLIP_RATE_BASE: usize = 186; // [4] // ── Pearl 2: per-branch loss budget (20 slots) ──────────────────────── +/// Adapts loss component budget weights per branch; balances C51/IQN/CQL/Ens +/// contributions based on measured gradient magnitudes. pub const BUDGET_C51_BASE: usize = 190; // [4] pub const BUDGET_IQN_BASE: usize = 194; // [4] pub const BUDGET_CQL_BASE: usize = 198; // [4] @@ -30,31 +33,44 @@ pub const BUDGET_ENS_BASE: usize = 202; // [4] pub const FLATNESS_BASE: usize = 206; // [4] // ── Pearl 3: per-branch NoisyNet σ (12 slots) ───────────────────────── +/// Adapts NoisyNet exploration noise per branch; scales σ to maintain +/// target branch entropy without manual schedule tuning. pub const NOISY_SIGMA_BASE: usize = 210; // [4] pub const SIGMA_FRACTION_BASE: usize = 214; // [4] pub const BRANCH_ENTROPY_BASE: usize = 218; // [4] // ── shared signal: per-branch Q variance (4 slots) ──────────────────── +/// Q-value variance per branch; shared diagnostic consumed by Pearls 1 and 2. pub const Q_VAR_PER_BRANCH_BASE: usize = 222; // [4] // ── Pearl 4: per-group Adam hyperparams (24 slots) ──────────────────── +/// Adapts Adam β1/β2/ε per param group using Wiener-optimal α (Pearl D); +/// replaces fixed Adam hparams with signal-driven estimates. pub const ADAM_BETA1_BASE: usize = 226; // [8 param groups] pub const ADAM_BETA2_BASE: usize = 234; // [8] pub const ADAM_EPS_BASE: usize = 242; // [8] // ── Pearl 5: per-branch IQN τ schedule (20 slots) ───────────────────── +/// Adapts IQN quantile schedule per branch; concentrates τ samples where +/// the distributional loss is highest. pub const IQN_TAU_BASE: usize = 250; // [4 branches × 5 quantiles] // ── Pearl 8: per-direction trail distance (4 slots) ─────────────────── +/// Adapts trailing stop distance per direction (Short/Hold/Long/Flat); +/// derived from per-direction return volatility on ISV. pub const TRAIL_DIST_PER_DIR_BASE: usize = 270; // [4 dirs: Short, Hold, Long, Flat] // ── Pearl 1-ext: per-branch num_atoms (4 slots) ─────────────────────── +/// Adapts C51 atom count per branch; extends Pearl 1 atom-span adaptation +/// with a dynamic resolution axis. pub const ATOM_NUM_ATOMS_BASE: usize = 274; // [4] // ── Pearl 6: cross-fold-persistent Kelly slots (6 slots) ────────────── -// NOT in fold-reset registry. Persist trade history across fold boundaries. -// Allocated in separate range 280..286 with intentional 2-slot gap from -// the rest of SP5 to make the carve-out structurally visible. +/// Cross-fold-persistent Kelly fraction and trade history signals. +/// NOT in fold-reset registry — these slots persist trade statistics +/// across fold boundaries. Allocated in a separate range 280..286 with +/// an intentional 2-slot gap (278, 279) from the per-fold block to make +/// the carve-out structurally visible in the layout. pub const KELLY_F_SMOOTH_INDEX: usize = 280; pub const CONVICTION_SMOOTH_INDEX: usize = 281; pub const TRADE_VAR_SMOOTH_INDEX: usize = 282; @@ -103,41 +119,81 @@ pub const SP5_LAYOUT_FINGERPRINT_FRAGMENT: &str = #[cfg(test)] mod tests { use super::*; + use std::collections::HashSet; #[test] fn slot_layout_no_overlaps_and_total_correct() { - // Per-branch block 174..226 (52 slots, contiguous) + let mut slots = HashSet::new(); + + // Pearl 1: atom span (b in 0..4) for b in 0..4 { - assert!(atom_v_center(b) >= 174 && atom_v_center(b) < 178); - assert!(atom_v_half(b) >= 178 && atom_v_half(b) < 182); - assert!(atom_headroom(b) >= 182 && atom_headroom(b) < 186); - assert!(atom_clip_rate(b) >= 186 && atom_clip_rate(b) < 190); - assert!(budget_c51(b) >= 190 && budget_c51(b) < 194); - assert!(noisy_sigma(b) >= 210 && noisy_sigma(b) < 214); - assert!(q_var_per_branch(b) >= 222 && q_var_per_branch(b) < 226); + slots.insert(atom_v_center(b)); + slots.insert(atom_v_half(b)); + slots.insert(atom_headroom(b)); + slots.insert(atom_clip_rate(b)); } - // Per-group Adam block 226..250 (24 slots) + // Pearl 2: loss budget (b in 0..4) + for b in 0..4 { + slots.insert(budget_c51(b)); + slots.insert(budget_iqn(b)); + slots.insert(budget_cql(b)); + slots.insert(budget_ens(b)); + slots.insert(flatness(b)); + } + // Pearl 3: NoisyNet σ (b in 0..4) + for b in 0..4 { + slots.insert(noisy_sigma(b)); + slots.insert(sigma_fraction(b)); + slots.insert(branch_entropy(b)); + } + // Shared signal: Q variance (b in 0..4) + for b in 0..4 { + slots.insert(q_var_per_branch(b)); + } + // Pearl 4: per-group Adam hparams (g in 0..8) for g in 0..8 { - assert!(adam_beta1(g) >= 226 && adam_beta1(g) < 234); - assert!(adam_beta2(g) >= 234 && adam_beta2(g) < 242); - assert!(adam_eps(g) >= 242 && adam_eps(g) < 250); + slots.insert(adam_beta1(g)); + slots.insert(adam_beta2(g)); + slots.insert(adam_eps(g)); } - // IQN τ block 250..270 (20 slots) + // Pearl 5: IQN τ schedule (b in 0..4, q in 0..5) for b in 0..4 { for q in 0..5 { - let s = iqn_tau(b, q); - assert!(s >= 250 && s < 270); + slots.insert(iqn_tau(b, q)); } } - // Trail + num_atoms 270..278 (8 slots) + // Pearl 8: trail distance (d in 0..4) for d in 0..4 { - assert!(trail_dist_per_dir(d) >= 270 && trail_dist_per_dir(d) < 274); - assert!(atom_num_atoms(d) >= 274 && atom_num_atoms(d) < 278); + slots.insert(trail_dist_per_dir(d)); } - assert_eq!(SP5_SLOT_END, 286); - // SP5_PRODUCER_COUNT counts only allocated slots: 110 - // 52 per-branch + 24 Adam + 20 IQN τ + 4 trail + 4 num_atoms + 6 Kelly = 110 - assert_eq!(SP5_PRODUCER_COUNT, 110); + // Pearl 1-ext: num_atoms (b in 0..4) + for b in 0..4 { + slots.insert(atom_num_atoms(b)); + } + // Pearl 6: cross-fold-persistent Kelly (6 scalar constants) + slots.insert(KELLY_F_SMOOTH_INDEX); + slots.insert(CONVICTION_SMOOTH_INDEX); + slots.insert(TRADE_VAR_SMOOTH_INDEX); + slots.insert(KELLY_SAMPLE_COUNT_INDEX); + slots.insert(WIN_RATE_SMOOTH_INDEX); + slots.insert(LOSS_RATE_SMOOTH_INDEX); + + // 1. Exactly 110 unique slots. + assert_eq!(slots.len(), 110, "expected 110 unique slots, got {}", slots.len()); + + // 2. Min slot is SP5_SLOT_BASE = 174. + assert_eq!(*slots.iter().min().unwrap(), 174); + + // 3. Max slot is SP5_SLOT_END - 1 = 285. + assert_eq!(*slots.iter().max().unwrap(), 285); + + // 4. Intentional carve-out gap (278, 279) is absent. + assert!(!slots.contains(&278), "slot 278 must be absent (carve-out gap)"); + assert!(!slots.contains(&279), "slot 279 must be absent (carve-out gap)"); + + // 5. Set equals {174..278} ∪ {280..286} — no holes, no overlaps. + let expected: HashSet = (174..278).chain(280..286).collect(); + assert_eq!(slots, expected, "slot set does not match expected {{174..278}} ∪ {{280..286}}"); } #[test] diff --git a/docs/isv-slots.md b/docs/isv-slots.md index 3c7cb8a89..af78e52dd 100644 --- a/docs/isv-slots.md +++ b/docs/isv-slots.md @@ -119,3 +119,43 @@ return the absolute slot index for a given group/branch. Producers and consumers will be wired in subsequent SP4 layer-B and layer-C tasks; until then the 40 slots are zero-initialized and reserved. + +## SP5: Per-branch + per-group adaptation layer (Task A0 reservation) + +SP5 Task A0 extends `ISV_TOTAL_DIM` from 173 → 286 by reserving 110 slots at +`[174..278) ∪ [280..286)`. An intentional 2-slot carve-out gap at `[278..280)` +separates the per-fold block from the cross-fold-persistent Kelly block. +Constants live in `crates/ml/src/cuda_pipeline/sp5_isv_slots.rs`. + +| Range | Base constant | Family | Pearl | Purpose | +|---|---|---|---|---| +| [174..178) | `ATOM_V_CENTER_BASE` | per-branch [4] | Pearl 1 | C51 atom distribution center | +| [178..182) | `ATOM_V_HALF_BASE` | per-branch [4] | Pearl 1 | C51 atom half-width | +| [182..186) | `ATOM_HEADROOM_BASE` | per-branch [4] | Pearl 1 | C51 atom headroom | +| [186..190) | `ATOM_CLIP_RATE_BASE` | per-branch [4] | Pearl 1 | C51 atom clip rate | +| [190..194) | `BUDGET_C51_BASE` | per-branch [4] | Pearl 2 | C51 loss budget weight | +| [194..198) | `BUDGET_IQN_BASE` | per-branch [4] | Pearl 2 | IQN loss budget weight | +| [198..202) | `BUDGET_CQL_BASE` | per-branch [4] | Pearl 2 | CQL loss budget weight | +| [202..206) | `BUDGET_ENS_BASE` | per-branch [4] | Pearl 2 | Ensemble loss budget weight | +| [206..210) | `FLATNESS_BASE` | per-branch [4] | Pearl 2 | Loss flatness diagnostic | +| [210..214) | `NOISY_SIGMA_BASE` | per-branch [4] | Pearl 3 | NoisyNet σ level | +| [214..218) | `SIGMA_FRACTION_BASE` | per-branch [4] | Pearl 3 | NoisyNet σ fraction | +| [218..222) | `BRANCH_ENTROPY_BASE` | per-branch [4] | Pearl 3 | Branch action entropy | +| [222..226) | `Q_VAR_PER_BRANCH_BASE` | per-branch [4] | shared | Q-value variance | +| [226..234) | `ADAM_BETA1_BASE` | per-group [8] | Pearl 4 | Adam β1 per param group | +| [234..242) | `ADAM_BETA2_BASE` | per-group [8] | Pearl 4 | Adam β2 per param group | +| [242..250) | `ADAM_EPS_BASE` | per-group [8] | Pearl 4 | Adam ε per param group | +| [250..270) | `IQN_TAU_BASE` | per-branch×quantile [4×5] | Pearl 5 | IQN τ schedule | +| [270..274) | `TRAIL_DIST_PER_DIR_BASE` | per-direction [4] | Pearl 8 | Trail stop distance | +| [274..278) | `ATOM_NUM_ATOMS_BASE` | per-branch [4] | Pearl 1-ext | C51 atom count | +| [278..280) | — | gap | — | Intentional carve-out (not allocated) | +| [280] | `KELLY_F_SMOOTH_INDEX` | scalar | Pearl 6 | Kelly fraction EMA (cross-fold) | +| [281] | `CONVICTION_SMOOTH_INDEX` | scalar | Pearl 6 | Conviction EMA (cross-fold) | +| [282] | `TRADE_VAR_SMOOTH_INDEX` | scalar | Pearl 6 | Trade variance EMA (cross-fold) | +| [283] | `KELLY_SAMPLE_COUNT_INDEX` | scalar | Pearl 6 | Kelly sample count (cross-fold) | +| [284] | `WIN_RATE_SMOOTH_INDEX` | scalar | Pearl 6 | Win rate EMA (cross-fold) | +| [285] | `LOSS_RATE_SMOOTH_INDEX` | scalar | Pearl 6 | Loss rate EMA (cross-fold) | + +Kelly slots `[280..286)` are NOT in the fold-reset registry. All other SP5 slots +are per-fold and reset at fold boundaries. Task A0 is reservation-only; producers +and consumers land in subsequent SP5 tasks.