diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index 0b4fec5ea..5eba898d2 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -2,6 +2,130 @@ **Status:** Populated during Plan 1 Task 6 (A.5 orphan audit). Updated on every commit per Invariant 7. +## 2026-05-07 — SP14 Layer C pre-flight: α machinery deletion targets + +Phase C.0 of separate-aux-trunk refactor (plan: `docs/superpowers/plans/2026-05-07-sp14-layer-c-separate-aux-trunk.md`). Establishes "before" state for atomic deletion in Phase C.7. + +### Kernel files slated for deletion (Phase C.7) +- `crates/ml/src/cuda_pipeline/alpha_grad_compute_kernel.cu` +- `crates/ml/src/cuda_pipeline/sp14_scale_wire_col_kernel.cu` + +### ISV slot constants slated for deletion (`crates/ml/src/cuda_pipeline/sp14_isv_slots.rs`) + +The plan named `VAR_AUX_INDEX`, `VAR_Q_INDEX`, `VAR_ALPHA_INDEX`, `ALPHA_RAW_INDEX`, `ALPHA_SMOOTHED_INDEX`, `GATE1_OPEN_STATE_INDEX`, `GATE2_OPEN_STATE_INDEX` as the 7 deletion targets. The actual constant names in the file (slot range [383..396), SP14 EGF allocation) are: + +``` +L38: pub const ALPHA_GRAD_RAW_VARIANCE_EMA_INDEX: usize = 390; // plan's VAR_ALPHA_INDEX +L41: pub const GATE1_OPEN_STATE_INDEX: usize = 391; // plan's GATE1_OPEN_STATE_INDEX +L44: pub const ALPHA_GRAD_RAW_INDEX: usize = 392; // plan's ALPHA_RAW_INDEX +L45: pub const ALPHA_GRAD_SMOOTHED_INDEX: usize = 393; // plan's ALPHA_SMOOTHED_INDEX +``` + +Additional α-machinery slots in the same file that feed `alpha_grad_compute_kernel` (also slated for deletion in C.7): + +``` +L30: pub const K_AUX_ADAPTIVE_INDEX: usize = 385; // adaptive sigmoid steepness (variance-driven) +L31: pub const K_Q_ADAPTIVE_INDEX: usize = 386; // adaptive sigmoid steepness (variance-driven) +L33: pub const BETA_RATE_LIMITER_ADAPTIVE_INDEX: usize = 387; // adaptive rate-limiter β +L36: pub const AUX_DIR_ACC_VARIANCE_EMA_INDEX: usize = 388; // plan's VAR_AUX_INDEX +L37: pub const Q_DISAGREEMENT_VARIANCE_EMA_INDEX: usize = 389; // plan's VAR_Q_INDEX +``` + +**Discrepancy from plan naming**: plan used 7 abbreviated names; actual file has 9 slots in the α-machinery group (K_AUX_ADAPTIVE, K_Q_ADAPTIVE, BETA_RATE_LIMITER_ADAPTIVE are structural intermediates also written by `alpha_grad_compute_kernel` and read by `sp14_scale_wire_col_kernel`). Additionally the plan named `GATE2_OPEN_STATE_INDEX` but no such constant exists — the SP14 EGF uses only a single Schmitt-trigger gate (`GATE1_OPEN_STATE_INDEX = 391`). The circuit-breaker slots `AUX_DIR_ACC_POST_OPEN_MIN_INDEX = 394` and `GRADIENT_HACK_LOCKOUT_REMAINING_INDEX = 395` belong to `gradient_hack_detect_kernel` (not to `alpha_grad_compute_kernel` directly) — Phase C.7 must decide whether to fold those into the deletion scope or retain alongside circuit-breaker kernel. + +**`ISV_TOTAL_DIM` discrepancy**: plan assumed `ISV_TOTAL_DIM = 443`; actual value is `444` (bumped 443→444 by SP15 Phase 1.3.b-followup, `DD_PERSISTENCE_MAX_INDEX = 443`). Slot 443 is occupied — no vacancy gap at 443. Phase C.7 new-slots for separate-aux-trunk must append at 444+. + +### Launch / consumer sites (`grep` results) + +#### `crates/ml/src/cuda_pipeline/alpha_grad_compute_kernel.cu` +- L1: filename header +- L100: `void alpha_grad_compute_kernel(` — kernel entry point +- L142: `const float VARIANCE_REF_AUX = 0.01f;` — mirrored from `sp14_isv_slots.rs:VARIANCE_REF_AUX` +- L147: `const float SCHMITT_BAND = 0.03f;` — mirrored from `sp14_isv_slots.rs:SCHMITT_BAND` +- L181: `const float k_aux = fmaxf(K_BASE_AUX / (1.0f + var_aux / VARIANCE_REF_AUX), K_MIN);` +- L187: `const float threshold_open = target + SCHMITT_BAND;` +- L188: `const float threshold_close = target - SCHMITT_BAND;` + +#### `crates/ml/src/cuda_pipeline/sp14_scale_wire_col_kernel.cu` +- L1: filename header +- L38: `void sp14_scale_wire_col_kernel(` — kernel entry point + +#### `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs` +- L68: `pub const VARIANCE_REF_AUX: f32 = 0.01;` +- L80: `pub const SCHMITT_BAND: f32 = 0.03;` +- L133: `// \`alpha_grad_compute_kernel.cu\` and \`gradient_hack_detect_kernel.cu\`,` (layout-lock comment) + +#### `crates/ml/src/cuda_pipeline/gradient_hack_detect_kernel.cu` +- L15: `// Action on detection: force gate1_open_state = 0 (close gate); set` +- L17: `// force-closed by this kernel regardless of what alpha_grad_compute_kernel` +- L21: `// End of each epoch (after alpha_grad_compute_kernel has been called for` +- L41: `// alpha_grad_compute_kernel (B.4). The circuit breaker ALSO WRITES to` +- L43: `// alpha_grad_compute_kernel → gradient_hack_detect_kernel within the same` +- L65: `const float SCHMITT_BAND = 0.03f;` +- L102: `// The threshold_open = target + SCHMITT_BAND mirrors alpha_grad_` +- L104: `const float threshold_open = target + SCHMITT_BAND;` + +Note: `gradient_hack_detect_kernel.cu` has hard dependencies on `alpha_grad_compute_kernel`'s ISV outputs (`GATE1_OPEN_STATE_INDEX = 391`, `AUX_DIR_ACC_POST_OPEN_MIN_INDEX = 394`, `GRADIENT_HACK_LOCKOUT_REMAINING_INDEX = 395`). Phase C.7 must delete or replace both kernels atomically. + +#### `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs` +- L939: `/// \`sp14_alpha_grad_compute_kernel\` (\`SP14_ALPHA_GRAD_CUBIN\`).` (field doc) +- L940: `exp_sp14_alpha_grad_compute_kernel: CudaFunction,` (struct field) +- L1976: `let exp_sp14_alpha_grad_compute_kernel = {` (load in `new()`) +- L1983: `m.load_function("alpha_grad_compute_kernel")` +- L1985: `"sp14-β: alpha_grad_compute_kernel load (collector): {e}"` +- L2475: `exp_sp14_alpha_grad_compute_kernel,` (struct literal) +- L4683: `// 3. SP14 alpha_grad_compute (pure ISV state machine)` (comment in per-step body) +- L4881: `.launch_builder(&self.exp_sp14_alpha_grad_compute_kernel)` (production launch) +- L4890: `"sp14-β: alpha_grad_compute t={t}: {e}"` (error path) + +#### `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` +- L669: `/// input-gate scalar. Loaded from \`alpha_grad_compute_kernel.cubin\`.` (field doc) +- L671: `include_bytes!(concat!(env!("OUT_DIR"), "/alpha_grad_compute_kernel.cubin"));` (`SP14_ALPHA_GRAD_CUBIN` static) +- L694: `/// the EGF pearl design. Loaded from \`sp14_scale_wire_col_kernel.cubin\`.` (field doc) +- L696: `include_bytes!(concat!(env!("OUT_DIR"), "/sp14_scale_wire_col_kernel.cubin"));` (`SP14_SCALE_WIRE_COL_CUBIN` static) +- L6535: `/// Loaded from \`alpha_grad_compute_kernel.cubin\`.` (field doc) +- L6536: `sp14_alpha_grad_compute_kernel: CudaFunction,` (struct field) +- L6560: `/// \`sp14_scale_wire_col_kernel\`, and finally the first SH2 columns` (field doc) +- L6572: `/// \`sp14_scale_wire_col_kernel.cubin\`. Launched immediately after` (field doc) +- L6578: `sp14_scale_wire_col_kernel: CudaFunction,` (struct field) +- L8565: `/// \`alpha_grad_compute_kernel\` writes the live gate output ∈ [0, 1].` (method doc) +- L8568: `pub(crate) fn launch_sp14_scale_wire_col(&self, d_dir_qaux_concat_ptr: u64)` (method definition) +- L8575: `.launch_builder(&self.sp14_scale_wire_col_kernel)` (launch) +- L8585: `.map_err(|e| MLError::ModelError(format!("sp14_scale_wire_col: {e}")))?;` (error path) +- L8635: `/// alpha_grad_compute_kernel (B.4), which reads slot 383 (and 389` (method doc) +- L8699: `pub(crate) fn launch_sp14_alpha_grad_compute(` (method definition) +- L8704: `"launch_sp14_alpha_grad_compute: isv_signals_dev_ptr must be allocated"` (assert) +- L8708: `.launch_builder(&self.sp14_alpha_grad_compute_kernel)` (launch) +- L12137: `self.launch_sp14_scale_wire_col(self.sp14_d_dir_qaux_concat.raw_ptr())?;` (call site 1 — `launch_cublas_forward`) +- L18754: `let sp14_alpha_grad_compute_kernel = sp14_alpha_module` (load in `compile_training_kernels`) +- L18755: `.load_function("alpha_grad_compute_kernel")` +- L18756: `.map_err(|e| MLError::ModelError(format!("alpha_grad_compute_kernel: {e}")))?;` +- L18793: `let sp14_scale_wire_col_module = stream.context()` (load in `compile_training_kernels`) +- L18795: `.map_err(|e| MLError::ModelError(format!("sp14 scale_wire_col cubin: {e}")))?;` +- L18796: `let sp14_scale_wire_col_kernel = sp14_scale_wire_col_module` +- L18797: `.load_function("sp14_scale_wire_col_kernel")` +- L18798: `.map_err(|e| MLError::ModelError(format!("sp14_scale_wire_col_kernel: {e}")))?;` +- L22696: `sp14_alpha_grad_compute_kernel,` (struct literal) +- L22705: `sp14_scale_wire_col_kernel,` (struct literal) +- L29100: `self.launch_sp14_scale_wire_col(self.sp14_d_dir_qaux_concat.raw_ptr())?;` (call site 2 — target forward path) + +#### `crates/ml/src/trainers/dqn/trainer/training_loop.rs` +- L4205: `// and \`launch_sp14_alpha_grad_compute\` MOVED from this` (redirect comment — production launches now in collector) +- L5268: `// MUST run AFTER all per-step alpha_grad_compute launches in the` (ordering comment) +- L5275: `// state consumed by NEXT epoch's alpha_grad_compute via the` (ordering comment) +- L8102: `"sp14_gate1_open_state" => {` (diagnostic CLI reset handler — reads `GATE1_OPEN_STATE_INDEX`) + +#### `crates/ml/src/trainers/dqn/mod.rs` +- L442: `// ramp passed to \`launch_sp14_alpha_grad_compute\`. Both the counter` (comment only) + +#### `crates/ml/src/cuda_pipeline/batched_backward.rs` +- L1760: `// with \`K = SH2 + 1\`. The caller then runs \`sp14_scale_wire_col_kernel\`` (comment only) + +### Preserved (NOT deleted) +- `q_disagreement_*` ISV slots [383..390) — `Q_DISAGREEMENT_SHORT_EMA_INDEX=383`, `Q_DISAGREEMENT_LONG_EMA_INDEX=384`, `Q_DISAGREEMENT_VARIANCE_EMA_INDEX=389`: diagnostic-only consumer in HEALTH_DIAG, producer kernel `q_disagreement_update_kernel.cu` retained (it feeds the HEALTH_DIAG signal chain, independent of the α-gated wire column being deleted). +- `dir_concat_qaux_kernel.cu` (Coupling A: forward feature wire from `save_h_s2`/`tg_h_s2_buf` into `sp14_d_dir_qaux_concat`) — survives unchanged. The 6 `launch_sp14_dir_concat_qaux` call sites at `gpu_dqn_trainer.rs:12086, 25632, 25701, 27754, 27944, 29042` remain live; this kernel feeds the separate-aux-trunk's input representation in Phase C.1+. +- `gradient_hack_detect_kernel.cu` — Phase C.7 scope decision deferred: this kernel reads `GATE1_OPEN_STATE_INDEX` and `AUX_DIR_ACC_POST_OPEN_MIN_INDEX` written by `alpha_grad_compute_kernel`; if the α gate is deleted its inputs become undefined. Phase C.7 must either delete `gradient_hack_detect_kernel` atomically alongside `alpha_grad_compute_kernel`, or replace its inputs with equivalent signals from the new separate-aux-trunk architecture. + SP7 cadence fix — migrate SP5 Pearl 2 budget + SP7 loss-balance controller from `process_epoch_boundary` to per-step `submit_aux_ops` (2026-05-07): **Bug audit finding #2** (post-`train-d2b2s` diagnostic, mirroring SP14 B.11 commit `200f05fce` and the 4-producer batch in `5608b866b`). The 7th instance of Pattern 1 cadence staleness fixed in this branch since `train-v8ztm`. **Diagnostic**: `launch_sp5_pearl_2_budget` (Pearl 2 IQN budget + flatness producer; writes `ISV[BUDGET_IQN_BASE..198)` + `ISV[FLATNESS_BASE..210)`) and `launch_loss_balance_controller` (SP7 controller; writes `ISV[BUDGET_CQL_BASE..194)` + `ISV[BUDGET_C51_BASE..194)` + 6 LB variance/active slot blocks) were both launching from `process_epoch_boundary` (`training_loop.rs:4312, 4336` pre-fix), firing ONCE per epoch. Their outputs feed the per-step dispatch chain at `compute_adaptive_budgets` → `launch_lb_budget_dispatch` (`fused_training.rs:3631`) which resolves them into `lb_budget_effective_buf` for the captured `apply_c51_budget_scale` (`fused_training.rs:1941`) and `apply_cql_saxpy` (`fused_training.rs:2599..2603`) consumers — every replay before the next epoch boundary saw `(steps_per_epoch − 1)`-step-stale flatness/budget state. Same failure mode SP14 B.11 documented for `ALPHA_GRAD_SMOOTHED`: the dispatch kernel runs per-step (so the EFFECTIVE buffer updates every replay), but the underlying ISV signal it resolves was per-epoch, so the entire loss-balance budget system has been operating on stale-flatness state for the duration of training. **Fix (atomic, preserves Pearl 2 → SP7 dependency)**: Pearl 2 budget launch moved to `submit_aux_ops` immediately after the `launch_moe_lambda_eff_update` chain (line ~2086) and before the "2. Gather Q(s, a_taken)" IQL block (line ~2094). SP7 loss-balance controller follows immediately (reads `ISV[FLATNESS_BASE]` populated by Pearl 2 — same-stream serial ordering enforces the dependency). Both launchers use pre-loaded `CudaFunction` fields (`pearl_2_budget_kernel` at `gpu_dqn_trainer.rs:18437`, `loss_balance_controller_kernel` at `gpu_dqn_trainer.rs:18591`, `apply_pearls_ad_kernel` already shared with the rest of the SP4 Wiener-EMA chain) — graph-capture safe per `pearl_no_host_branches_in_captured_graph`. Captured into `aux_child` per `capture_training_graph`, identical replay semantics to the SP14 B.11 chain. `training_loop.rs:4312, 4336` deleted; replaced with redirect comment per the SP14 B.11 precedent. **Out-of-scope dependencies (FLAGGED for follow-up, not migrated atomically per `feedback_no_partial_refactor` scope-tightening)**: (1) Pearl 2 reads `ISV[Q_VAR_PER_BRANCH_BASE..226)` written by `launch_sp5_pearl_1_atom` (`training_loop.rs:4298`) and `ISV[NOISY_SIGMA_BASE..214)` written by `launch_sp5_pearl_3_sigma` (`training_loop.rs:4305`) — both still per-epoch; at fold start, this means Pearl 2 reads sentinel/cold-start `Q_VAR`/`SIGMA` for the first per-step launches until `process_epoch_boundary` fires Pearl 1/3 at the END of the first epoch. **Strict improvement** over the prior all-per-epoch placement (where Pearl 2 ITSELF was also stale until end of first epoch); migrating Pearl 1 + Pearl 3 requires their own atomic commit with the rest of the SP5 producer chain, since Pearl 1's `q_branch_stats` reads `q_out_buf` populated by `populate_q_out` (already per-step in the new placement region), and Pearl 3 reads `ATOM_V_HALF` (Pearl 1 output) + `BRANCH_ENTROPY` (also Pearl 1) — Pearl 1 → Pearl 3 → Pearl 2 is the canonical chain. (2) SP7 controller reads `ISV[LB_MAX_BUDGET_CQL_BASE..326)` + `ISV[LB_MAX_BUDGET_C51_BASE..330)` populated by `launch_max_budget_compute` (SP8 Fix 36 at `training_loop.rs:4324`, KEPT per-epoch in this commit). `launch_max_budget_compute` in turn reads `ISV[TRAIN_ACTIVE_FRAC_INDEX=321]` populated by `launch_train_active_frac_compute` which reads `monitoring_summary[5..17)` populated by per-epoch `monitoring_reduce` — migrating it requires the monitoring pair too. Acceptable staleness offset for the SP7-pair migration: cap is one-epoch-old at worst and Pearl A bootstraps from sentinel-0 at fold reset, controller has its own internal bootstrap so still functional. **Cold-start ordering**: `compute_adaptive_budgets` at line ~1916 still fires BEFORE the migrated SP7 controller at line ~2143 within the same step, so step N's controller output lands in step N+1's dispatch (1-step delay by construction — same offset as the per-epoch placement, just at step granularity). At step 0 of each fold, the dispatch reads sentinel `BUDGET_*_BASE` values; the controller's host-resolved IQN/ENS bootstrap path (`compute_adaptive_budgets` lines 3640-3644 with `BASE_IQN=0.11` floor + `ENS_BOOTSTRAP_BUDGET=0.02`) and the existing SP7 controller bootstrap branch handle this case — pre-existing behavior, not a regression. **State-reset registry coverage** (verified at `state_reset_registry.rs`): `FLATNESS_BASE` line 586, `BUDGET_C51_BASE` line 566, `BUDGET_CQL_BASE` line 576, `LB_MAX_BUDGET_CQL_BASE` line 671, `LB_MAX_BUDGET_C51_BASE` line 676 — all relevant slots have `FoldReset` sentinel-0 entries. **Pre-loaded CudaFunction verification**: both launchers source from struct fields (`pearl_2_budget_kernel: CudaFunction` at gpu_dqn_trainer.rs:6354, `loss_balance_controller_kernel: CudaFunction` at gpu_dqn_trainer.rs:6364), populated in `compile_training_kernels` via single `load_cubin` + `load_function` calls — no per-call resolution, capture-safe. **Verification**: `SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check -p ml --lib` clean (dev profile; only pre-existing unused-var warnings unrelated to this change). `cargo check -p ml --tests --all-targets` blocked by the in-flight SP15 Wave 5 follow-up's incomplete struct-field set on `GpuExperienceCollector` (`exp_sp15_dd_state_kernel` etc. — orthogonal to this change, see audit entry below). `cargo test -p ml --test sp14_oracle_tests --release -- --ignored --nocapture` 7/7 GPU tests pass on RTX 3050 Ti (`alpha_grad_adaptive_beta`, `alpha_grad_schmitt_hysteresis`, `dir_concat_qaux_correct`, `gradient_hack_circuit_breaker_fires`, `q_disagreement_all_hold_no_contribution`, `q_disagreement_empty_batch_preserves_ema`, `q_disagreement_k4_k2_mapping`) — these don't directly cover the Pearl 2 / SP7 controller producer chain, but they exercise the broader SP14 EGF chain whose ISV producers share the apply_pearls_ad_kernel infrastructure, and a regression in launch ordering or Wiener-state offsets would surface as ISV-state divergence. **L40S validation pending** consolidation of the in-flight `train-bqtnd` (aux stop-gradient pair `872bd7392` + `411a30473`) result. Expected behavior post-fix: per-step `lb_budget_effective_buf` resolution sees fresh per-step controller output instead of frozen-per-epoch values; loss-balance budget should track the in-step gradient-norm-ratio signal at the cadence the SP7 controller was designed for (mirrors the EGF chain's per-step EMA evolution post-SP14 B.11). **Files modified**: `crates/ml/src/trainers/dqn/fused_training.rs` (+59 lines: 2 launches + audit-comment block in `submit_aux_ops` immediately after the producer-cadence batch's MoE chain); `crates/ml/src/trainers/dqn/trainer/training_loop.rs` (−24 / +23 lines: deletion of `launch_sp5_pearl_2_budget` + `launch_loss_balance_controller` from `process_epoch_boundary` + replacement audit comment; `launch_max_budget_compute` retained at the same call site per scope-tightening note above). SP14 aux-head stop-gradient — regime head closes the pair (2026-05-07): mirrors the same-day next-bar stop-gradient fix at commit `872bd7392` for the K=5 regime CE head (`aux_regime_backward` at `crates/ml/src/cuda_pipeline/aux_heads_kernel.cu:756-766` pre-fix, lines 756-770 post-fix replaced with zero-write). The regime head has the IDENTICAL architectural pattern as the next-bar head: same `h_s2 → w1 → ELU(h_post) → w2 → softmax → CE` topology, same `dh_s2_out` SAXPY back to trunk that conflicts with Q-loss for h_s2 representation control. The next-bar fix's audit comment block already documented this regime head as a "candidate for the same fix once next-bar validation lands"; today's next-bar fix has been validated by passing the 7 GPU oracle tests, so we apply the same mechanical fix to regime now (zero-fill the dh_s2 write block) rather than waiting for the L40S validation cycle. The `dW1_partial` / `db1_partial` / `dW2_partial` / `db2_partial` writes (Step 2 of regime backward, before the dh_s2 step) are UNCHANGED — regime head's own params still receive correct CE-loss gradients via the per-sample partials. Verified mechanically: the dh_s2 write block in regime backward is the LAST step of the kernel (no downstream reads inside the kernel), and `dh_s2_out` is consumed only by the trunk SAXPY accumulation step (same as next-bar's `dh_s2_out` consumer pattern — `grep "dh_s2"` in `crates/ml/src/cuda_pipeline/` confirms). The label dtype difference (regime: `unsigned char` 5-class vs next-bar: `int` ±1/0) is at the input side (`label[b]` cast to int in the d_logits computation at Step 0); it does not interact with the dh_s2 zero-fill since dh_s2 sits at the OUTPUT side of the gradient chain and is determined entirely by `sh_dh_pre` (which becomes write-only-to-zero, ignoring upstream label). Combined with `872bd7392`, this completes today's trunk-isolation pair: both auxiliary heads (next-bar K=2 binary direction + regime K=5 multi-class) now train their own w1/b1/w2/b2 from CE loss without pulling on shared h_s2; Q-loss is the sole shaping force on h_s2. Also updated next-bar's NOTE comment block to reflect that regime is no longer "deferred" (was: "candidate for the same fix once next-bar validation lands"; now: "received the SAME fix in the immediately-following commit"). **Verification**: `SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check -p ml --tests --all-targets` clean; `SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --test sp14_oracle_tests --release -- --ignored --nocapture` 7/7 GPU tests pass on RTX 3050 Ti — these don't directly cover `aux_regime_backward` Step 3 but they exercise the broader EGF chain and a regression in CE math (which would also affect dW1/dW2 partials) would surface as ISV-state divergence. Pure "set output to 0" — no math regression in oracle tests is expected. **Validation pending L40S re-dispatch**: regime CE loss should not climb during training (analogous to the next-bar `aux next_bar_mse` improvement expected from `872bd7392`); regime accuracy should at minimum approach 1/K=0.20 (5-class random baseline) and ideally above it if h_s2 carries regime-relevant signal. If regime accuracy stays flat at the bootstrap value, the deeper diagnosis is that h_s2 has no regime-relevant signal — separate-trunk follow-up (Option 2, deferred). Files modified: `crates/ml/src/cuda_pipeline/aux_heads_kernel.cu` (zero-fill dh_s2 write in `aux_regime_backward` Step 3 + 28-line audit-comment block; updated 4-line NOTE in `aux_next_bar_backward`'s Step 3 comment).