From a92abedbae70da05e3971a9e988b601fbb668d7d Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sun, 3 May 2026 00:50:18 +0200 Subject: [PATCH] sp7(isv): allocate 16 ISV slots for loss-balance controller state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LB_DIFF_VAR_CQL_BASE=297, LB_SAMPLE_VAR_CQL_BASE=301, LB_DIFF_VAR_C51_BASE=305, LB_SAMPLE_VAR_C51_BASE=309. Bumps SP5_SLOT_END 297 → 313 and SP5_PRODUCER_COUNT 123 → 139. Helper fns mirror the existing budget_*/flatness/q_var_per_branch pattern. Layout fingerprint extended. slot_layout_no_overlaps_and_total_correct asserts the new total. Audit doc Fix 31 stub added; will be extended commit-by-commit through Tasks 2-9 of the SP7 plan. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/ml/src/cuda_pipeline/sp5_isv_slots.rs | 60 ++++++++++++++------ docs/dqn-wire-up-audit.md | 31 ++++++++++ 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/crates/ml/src/cuda_pipeline/sp5_isv_slots.rs b/crates/ml/src/cuda_pipeline/sp5_isv_slots.rs index 3f63afe82..08f6ecf22 100644 --- a/crates/ml/src/cuda_pipeline/sp5_isv_slots.rs +++ b/crates/ml/src/cuda_pipeline/sp5_isv_slots.rs @@ -167,7 +167,20 @@ pub const TRAINING_SHARPE_EMA_INDEX: usize = 294; pub const MAX_DD_EMA_INDEX: usize = 295; pub const LOW_DD_RATIO_INDEX: usize = 296; -pub const SP5_SLOT_END: usize = 297; +// ── SP7: Loss-balance controller state-tracker EMAs ────────────────── +// +// Per-branch (4 branches each: dir, mag, ord, urg) Wiener-α state for +// the 2 managed loss heads (CQL and C51). The kernel writes raw +// observations of (candidate − old_budget)² and (h_norm)² to the +// producer scratch buffer; `apply_pearls_ad_kernel` smooths them into +// these slots — same Pearl A sentinel-bootstrap + Pearl D Wiener-EMA +// as every other SP5 producer. +pub const LB_DIFF_VAR_CQL_BASE: usize = 297; // [4] (candidate_cql − old_cql)² EMA +pub const LB_SAMPLE_VAR_CQL_BASE: usize = 301; // [4] (cql_norm)² EMA +pub const LB_DIFF_VAR_C51_BASE: usize = 305; // [4] (candidate_c51 − old_c51)² EMA +pub const LB_SAMPLE_VAR_C51_BASE: usize = 309; // [4] (c51_norm)² EMA + +pub const SP5_SLOT_END: usize = 313; /// Wiener-buffer producer-count constant. Sizes `wiener_state_buf` via /// `(SP4_PRODUCER_COUNT + SP5_PRODUCER_COUNT) * SP4_WIENER_FLOATS_PER_SLOT`. @@ -202,8 +215,8 @@ pub const SP5_SLOT_END: usize = 297; /// 174..278 ∪ 280..297; the linear span grows 120 → 123. Three new /// training-metrics EMA outputs (sharpe / max_dd / low_dd_ratio) sit /// immediately after the D2 health composition block at slots 294..297. -pub const SP5_PRODUCER_COUNT: usize = 123; -// linear span = SP5_SLOT_END - SP5_SLOT_BASE = 297 - 174 = 123 wiener triples +pub const SP5_PRODUCER_COUNT: usize = 139; +// linear span = SP5_SLOT_END - SP5_SLOT_BASE = 313 - 174 = 139 wiener triples // unique-slot count = 121 (52 per-branch + 24 Adam + 20 IQN τ + 4 trail // + 4 num_atoms + 6 Kelly + 4 Layer D D1 PnL aggregation // + 4 Layer D D2 health composition @@ -229,6 +242,10 @@ pub const SP5_PRODUCER_COUNT: usize = 123; #[inline] pub const fn iqn_tau(b: usize, q: usize) -> usize { IQN_TAU_BASE + b * 5 + q } #[inline] pub const fn trail_dist_per_dir(d: usize) -> usize { TRAIL_DIST_PER_DIR_BASE + d } #[inline] pub const fn atom_num_atoms(b: usize) -> usize { ATOM_NUM_ATOMS_BASE + b } +#[inline] pub const fn lb_diff_var_cql(b: usize) -> usize { LB_DIFF_VAR_CQL_BASE + b } +#[inline] pub const fn lb_sample_var_cql(b: usize) -> usize { LB_SAMPLE_VAR_CQL_BASE + b } +#[inline] pub const fn lb_diff_var_c51(b: usize) -> usize { LB_DIFF_VAR_C51_BASE + b } +#[inline] pub const fn lb_sample_var_c51(b: usize) -> usize { LB_SAMPLE_VAR_C51_BASE + b } /// Layout fingerprint contribution. Appended to the existing /// LAYOUT_FINGERPRINT_SEED in gpu_dqn_trainer.rs so existing checkpoints @@ -244,7 +261,9 @@ pub const SP5_LAYOUT_FINGERPRINT_FRAGMENT: &str = PNL_TOTAL=286;PNL_MEAN=287;PNL_VAR=288;PNL_MAX_DD=289;\ HEALTH_SCORE=290;Q_GAP_NORM=291;Q_VAR_NORM=292;GRAD_NORM_NORM=293;\ TRAINING_SHARPE_EMA=294;MAX_DD_EMA=295;LOW_DD_RATIO=296;\ - ISV_TOTAL_DIM=297"; + LB_DIFF_VAR_CQL_BASE=297;LB_SAMPLE_VAR_CQL_BASE=301;\ + LB_DIFF_VAR_C51_BASE=305;LB_SAMPLE_VAR_C51_BASE=309;\ + ISV_TOTAL_DIM=313"; #[cfg(test)] mod tests { @@ -321,27 +340,34 @@ mod tests { slots.insert(TRAINING_SHARPE_EMA_INDEX); slots.insert(MAX_DD_EMA_INDEX); slots.insert(LOW_DD_RATIO_INDEX); + // SP7: loss-balance controller state-tracker EMAs (b in 0..4) + for b in 0..4 { + slots.insert(lb_diff_var_cql(b)); + slots.insert(lb_sample_var_cql(b)); + slots.insert(lb_diff_var_c51(b)); + slots.insert(lb_sample_var_c51(b)); + } - // 1. Exactly 121 unique slots. - assert_eq!(slots.len(), 121, "expected 121 unique slots, got {}", slots.len()); + // 1. Exactly 137 unique slots. + assert_eq!(slots.len(), 137, "expected 137 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 = 296. - assert_eq!(*slots.iter().max().unwrap(), 296); + // 3. Max slot is SP5_SLOT_END - 1 = 312. + assert_eq!(*slots.iter().max().unwrap(), 312); // 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..297} — no holes (other than the + // 5. Set equals {174..278} ∪ {280..313} — no holes (other than the // carve-out gap 278..280), no overlaps. Layer D D1 extended the // upper end 286 → 290; D2 extended it 290 → 294; D3 extends it - // 294 → 297. - let expected: HashSet = (174..278).chain(280..297).collect(); + // 294 → 297; SP7 extends it 297 → 313. + let expected: HashSet = (174..278).chain(280..313).collect(); assert_eq!(slots, expected, - "slot set does not match expected {{174..278}} ∪ {{280..297}}"); + "slot set does not match expected {{174..278}} ∪ {{280..313}}"); } #[test] @@ -368,11 +394,11 @@ mod tests { // is cross-fold-persistent — the contracts must remain disjoint). assert!(PNL_TOTAL_INDEX > LOSS_RATE_SMOOTH_INDEX); // SP5_SLOT_END must reflect the post-D3 end-of-block. - assert_eq!(SP5_SLOT_END, 297); + assert_eq!(SP5_SLOT_END, 313); // SP5_PRODUCER_COUNT is the wiener-buffer linear span (slot-range // width including the 2-slot carve-out gap), NOT the unique-slot // count. See SP5_PRODUCER_COUNT docstring for the rationale. - assert_eq!(SP5_PRODUCER_COUNT, 123); + assert_eq!(SP5_PRODUCER_COUNT, 139); assert_eq!(SP5_PRODUCER_COUNT, SP5_SLOT_END - SP5_SLOT_BASE); } @@ -391,7 +417,7 @@ mod tests { // 4-slot block is internally contiguous. assert_eq!(GRAD_NORM_NORM_INDEX - HEALTH_SCORE_INDEX, 3); // SP5_SLOT_END must reflect the post-D3 end-of-block. - assert_eq!(SP5_SLOT_END, 297); + assert_eq!(SP5_SLOT_END, 313); // SP5_PRODUCER_COUNT linear-span check matches the new end. assert_eq!(SP5_PRODUCER_COUNT, SP5_SLOT_END - SP5_SLOT_BASE); } @@ -412,12 +438,12 @@ mod tests { assert_eq!(MAX_DD_EMA_INDEX - TRAINING_SHARPE_EMA_INDEX, 1); assert_eq!(LOW_DD_RATIO_INDEX - MAX_DD_EMA_INDEX, 1); // SP5_SLOT_END must reflect the new end-of-block (3-slot grow). - assert_eq!(SP5_SLOT_END, 297); + assert_eq!(SP5_SLOT_END, 313); // SP5_PRODUCER_COUNT linear-span check matches the new end. The // wiener buffer must cover the entire linear span — including the // 2-slot carve-out gap (278..280) and the Pearl 6 reserved-but- // unused 6-float block (slots 280..286 don't call apply_pearls). - assert_eq!(SP5_PRODUCER_COUNT, 123); + assert_eq!(SP5_PRODUCER_COUNT, 139); assert_eq!(SP5_PRODUCER_COUNT, SP5_SLOT_END - SP5_SLOT_BASE); } } diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index 8d5e482f6..873155170 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -3856,3 +3856,34 @@ This is a pure structural migration — orthogonal to Bug-1 contract drift, but References: Stale-B closes the last ⚠ Stale row from Fix 29's audit. `feedback_no_partial_refactor` (single launcher migrated alongside kernel signature change in one commit), `feedback_no_functionality_removal` (PNL_VS_TARGET / PNL_VS_STOP slots preserved — only their data source corrected; the audit's "drop the slots" alternative explicitly rejected), `feedback_no_hiding` (no fallback to z-normed reads remaining; kernel either gets real raw_close from `prices` or falls back to `unrealized=0` when `prices==NULL`, matching the existing `have_close=false` semantics for callers without OHLC data — same as pre-fix behaviour for the smoke-test paths), `feedback_no_cpu_compute_strict` n/a (zero new host-side compute), `feedback_no_htod_htoh_only_mapped_pinned` already satisfied (`prices_buf` is `MappedF32Buffer` post the prereq commit `4d966e62f`). **Fix 30 closure**: all four ⚠ Stale rows (#12 Stale-C, #13 Stale-B, #14/#15 Stale-A) and the one ❓ Ambiguous row (#18 Ambiguous-A) from Fix 29's deferred follow-ups are now resolved. Bug-1 contract drift triage is complete; the production hot path was already clean post-Fix-29's host-side `vol_normalizer` deletion, and the val/HPO/seed-phase derived metrics now read raw_close from the correct source on every path. + +### Fix 31: SP7 loss-balance controller — adaptive CQL/C51 budget ratio (2026-05-03, IN PROGRESS) + +**The bug.** At production scale (50-epoch baseline at HEAD `2fb7d7f57`, +smoke `train-multi-seed-n4qv2` ep4-7) `grad_split_bwd cql=4.13` vs +`iqn=0.07` (≈60×); magnitude Q-values within 0.003 of each other; +`eval_dist eq=1.000` (full eval-collapse to Quarter via argmax). Root +cause: per-branch CQL budget hardcoded to 0 by Pearl 2 and floored to +0.02 in `compute_adaptive_budgets`. The "regime_stability allocator" +docstring at `fused_training.rs:3409` was never implemented. + +**The fix.** New GPU controller `loss_balance_controller_kernel` writes +adaptive per-branch budgets to the same ISV slots Pearl 2 used to +zero. Math and rationale in +`docs/superpowers/specs/2026-05-03-sp7-loss-balance-controller-design.md`. + +**Layer A landing (this commit, 2026-05-03)**: 16 new ISV slots +`LB_DIFF_VAR_CQL_BASE` (297..301), `LB_SAMPLE_VAR_CQL_BASE` (301..305), +`LB_DIFF_VAR_C51_BASE` (305..309), `LB_SAMPLE_VAR_C51_BASE` (309..313). +`SP5_SLOT_END` 297→313; `SP5_PRODUCER_COUNT` 123→139. Wiener-buffer +linear-span test asserts the new total. Layout fingerprint string +extended. No producer kernel yet — that arrives in the next commit. + +**Subsequent landings** (this entry will be extended commit-by-commit): +- T2: state-reset registry entries. +- T3: kernel `.cu` file. +- T4: build.rs cubin manifest. +- T5: trainer struct + launcher fn. +- T6: Pearl 2 contract change (drop CQL/C51/ENS args). +- T7: launch site + sentinel-aware consumer + stale doc. +- T9–T10: smoke + 50-epoch verification.