refactor(sp5): Task A0 quality fix-up — rustdoc + HashSet overlap test

Address two code-quality review items on commit 6dcaf1a1c:

1. Convert file-level + Pearl-section comments from `//` to rustdoc
   (`//!` module-level + `///` on first constant of each section).
   Matches sp4_isv_slots.rs style; SP5 module now `cargo doc`-discoverable
   as peer of SP4.

2. Replace spot-check assertions in slot_layout_no_overlaps_and_total_correct
   with a HashSet enumerating every slot reachable through every accessor.
   Asserts exactly 110 unique slots, min=174, max=285, the 2-slot carve-out
   gap (278, 279) absent, and the set equals {174..278} ∪ {280..286}.
   Test now actually verifies the no-overlaps invariant its name promised.

Also adds SP5 section to docs/isv-slots.md (Invariant 7 audit-doc update
required by pre-commit hook for cuda_pipeline component changes).

No constant values, accessor signatures, or fingerprint string changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-01 20:20:53 +02:00
parent 6dcaf1a1c9
commit f91098e456
2 changed files with 136 additions and 40 deletions

View File

@@ -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<usize> = (174..278).chain(280..286).collect();
assert_eq!(slots, expected, "slot set does not match expected {{174..278}} {{280..286}}");
}
#[test]

View File

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