Commit Graph

1286 Commits

Author SHA1 Message Date
jgrusewski
bf3a32d63a feat(sp11): A0 — allocate 20 ISV slots [340..360) + 20 reset entries
Pure infrastructure. No producer kernels, no consumer reads. Existing
training paths trace identically because no consumer reads slots [340..360)
yet. Layout-fingerprint bumped to ISV_TOTAL_DIM=360.

Spec: docs/superpowers/specs/2026-05-04-sp11-reward-as-controlled-subsystem.md
2026-05-04 00:55:07 +02:00
jgrusewski
e0d3abd9d2 plan(sp11): patch all 5 violations + 2 gaps from review
feedback_no_htod_htoh_only_mapped_pinned (tests not exempt):
  - All test fixtures converted from htod_copy/dtoh_sync_copy to
    MappedF32Buffer with host_slice / host_slice_mut access.
  - Novelty hash buffer + projection matrix changed from
    cudarc::CudaSlice<f32> + alloc_zeros to MappedF32Buffer.

feedback_no_cpu_forwards (CPU is read-only):
  - Projection matrix initialization changed from host-side StdRng +
    host_slice_mut writes to a one-shot GPU init kernel
    (novelty_simhash_proj_init_kernel) using Philox seeded from
    config.seed. No host RNG, no host writes.

feedback_no_cpu_compute_strict (saboteur multiplication):
  - B1 step 5 reverted from Rust-side `read_isv_slot * scale` to
    GPU-side: pass base scale + ISV pointer + slot index to the
    saboteur perturbation kernel; multiplication happens on-device.

feedback_trust_code_not_docs (grad-ratio terminology):
  - Spec §3.3.1 "per-component grad EMA" was wrong — SP4 grad-balancer
    is per-branch (4 slots), not per-reward-component. Renamed:
      reward_component_grad_ratio_compute_kernel
        → reward_component_mag_ratio_compute_kernel
      REWARD_COMPONENT_GRAD_RATIO_BASE
        → REWARD_COMPONENT_MAG_RATIO_BASE
    Source: existing REWARD_POPART_EMA_INDEX..+6 (per-component reward
    magnitude EMAs from SP4 reward_component_ema_kernel). Semantic
    equivalent for the controller's exploit/diversify blend.

feedback_no_stubs (dead parameter):
  - Removed `eps_div_idx_unused` from mag_ratio kernel signature.

Saboteur engagement: missing producer specified
  - Spec §3.3.1's two-reward-arrays formulation replaced with single
    `saboteur_delta_reward_buf` produced by the saboteur perturbation
    kernel itself (single reward computation, diff emitted as side
    output). Engagement kernel signature simplified to one input array.

PNL_REWARD_MAGNITUDE_EMA_INDEX (slot 359): producer wired
  - mag-ratio kernel mirrors `isv[REWARD_POPART_EMA_INDEX]` to
    scratch_out[6]; chained apply_pearls_ad targets slot 359.

Replay sample kernel location specified
  - graph_utility_kernels.cu:71 (gather_f32_scalar). New sibling kernel
    `gather_replay_reward_with_curiosity` defined; replaces the existing
    scalar gather (no legacy alias per feedback_no_legacy_aliases).
    novelty_simhash_lookup runs before, novelty_simhash_update after.

A2 controller test placeholders → full GPU oracle assertions
  - Three controller tests (z=0 midpoint, weight renorm, saboteur clamp)
    have full mapped-pinned fixtures with assertions on weight sum,
    individual values, post-clamp bounds.

Plan now passes:
  - feedback_no_htod_htoh_only_mapped_pinned (tests + production)
  - feedback_no_cpu_forwards (CPU never writes/computes for GPU)
  - feedback_no_cpu_compute_strict (all multiplications GPU-side)
  - feedback_no_atomicadd (race-tolerated non-atomic, safety documented)
  - feedback_no_partial_refactor (Layer B atomic; saboteur kernel sig
    change touches all callers in the same commit)
  - feedback_no_stubs (no dead parameters)
  - feedback_trust_code_not_docs (corrected spec terminology)
  - feedback_wire_everything_up (every new field has init + producer)
  - feedback_no_legacy_aliases (old gather_f32_scalar replaced, deleted)

1447 lines, +268 from previous version.
2026-05-04 00:40:36 +02:00
jgrusewski
d8b44e0829 plan(sp11): implementation plan for reward-as-controlled-subsystem
Task-by-task plan for the SP11 spec at HEAD 9395b983c. Three layers:

Layer A (3 commits, additive infrastructure, no behavior change):
  A0 — 20 ISV slots [340..360) + 20 reset entries + novelty-hash arm
  A1 — 3 canary producers (val_sharpe_delta, saboteur_engagement,
       reward_component_grad_ratio) with Pearls A+D chained
  A2 — controller kernel + SimHash novelty buffer; HEALTH_DIAG sp11_reward

Layer B (1 atomic commit, ~750 LOC):
  B1 — every consumer migrates: cf_weight (mse + c51), audit-discovered
       shaping sites, saboteur multiplier, replay-time curiosity bonus,
       novelty hash lookup+update scheduled at replay

Layer C (validation + close-out):
  C1-C2 — local + L40S smoke; T10 3×3×50 full validation
  C3-C5 — Fix 39 audit doc, pearl_reward_as_controlled_subsystem, MEMORY.md
  C6 — merge to main

Plan provides exact file paths, kernel signatures, GPU oracle test
skeletons, commit messages, and validation pass criteria from spec
§9.1-9.3. ~1550 LOC total estimate; ~3 hours subagent work plus
validation wall-clock (smoke ~25 min, T10 ~4 hr).

Spec: docs/superpowers/specs/2026-05-04-sp11-reward-as-controlled-subsystem.md
2026-05-04 00:27:28 +02:00
jgrusewski
9395b983cc spec(sp11): fix all 13 review issues — straight-up implementation
Critical bugs fixed:
1. Z-score formula: was mean(Δ)/mean(|Δ|), bounded to [-1,+1] — sigmoid
   never saturated, controller stuck in [0.27, 0.73]. Now true Z-score:
   delta_ema / sqrt(delta_var_ema) with two-pass var. Renamed canary
   slot 351 from VAL_SHARPE_STD_EMA to VAL_SHARPE_VAR_EMA.
2. Component weight renormalization added — Σweights = 1 enforced
   post-floor so PopArt's reward-magnitude EMA stays uncontaminated.
3. SABOTEUR_MIN bound violation — engagement-floor (0.1) was silently
   pulling output below 0.5 stated minimum. Added post-multiplication
   clamp to [SABOTEUR_MIN, SABOTEUR_MAX].
4. ratios[] undeclared — now __shared__ float ratios[6] block-loaded
   once from ISV.
5. §3.6 slot count off-by-five (15 → 20). All 20 slots get FoldReset
   entries; cold-start window behavior specified explicitly (no NaN path).

Architectural fixes:
6. Q-overconfidence concern addressed in scope — controller's
   REWARD_CF_WEIGHT_INDEX covers CQL conservatism. No SP11′ deferral;
   if T10 fails, extend controller outputs (e.g., target-update τ).
7. Curiosity recompute-at-replay specified (§3.5.1) — replay buffer
   stores base reward only; curiosity added per-tuple at replay time
   against current visit-count and current ISV. Eliminates stale-signal
   replay contamination that would worsen ep1-overfitting.

Smaller fixes:
8. Novelty signal specified — SimHash 42×16 projection → 16-bit code,
   1M-slot per-bucket count table, 1/sqrt(1+count). Hash table lives
   in state-reset registry (cleared at fold boundary).
9. Saboteur engagement operationally defined — per-bar
   |reward_with_saboteur − reward_without_saboteur| > EPS_ENGAGEMENT,
   block tree-reduce, EMA'd. EPS_ENGAGEMENT = 0.01 × PnL_EMA.
10. Duplicate sentence in §7 component-starvation paragraph removed.
11. Validation criteria strengthened (§9): aggregation across 9
    trajectories specified; primary metric = median peak-epoch ≥ 10
    (baseline median peak-epoch = 1); secondary = drop-from-peak ≤ 5%
    by ep20; tertiary = ep20 mean ≥ baseline ep1 mean. §9.3 fix-forward
    response codified — no rollback.
12. Phases split per project pattern — Layer A additive (3 commits:
    slots, canaries, controller), Layer B atomic consumer migration
    (1 commit, including replay-time curiosity), Layer C validation +
    close-out. No falsification gate; smoke is validation only.
13. Pearls A+D applied to controller outputs (§3.4.1) — chained
    apply_pearls_ad_kernel after controller writes scratch, smoothed
    values land in ISV. Consumers read smoothed slots.

Constants surviving (Invariant-1 anchors only, all rate-not-regime):
EPS_DIV, WEIGHT_HARD_FLOOR, SABOTEUR_MIN/MAX, CURIOSITY_PERMANENT_FRACTION,
CURIOSITY_BOUND_FRACTION, WEIGHT_FLOOR_FRACTION, ENGAGEMENT_FLOOR.

Spec is now full straight-up implementation; smoke validates Layer A
infrastructure and Layer B consumers, T10 validates SP11 success metric.
~1550 LOC across 3 commits in Layer A + 1 atomic commit in Layer B +
close-out in Layer C.
2026-05-04 00:20:25 +02:00
jgrusewski
d49fbbe4e2 spec(sp11): reframe curiosity as feature + add permanent floor
User correction: curiosity is the *fix* for the ep1-peak overfitting
pathology, not a hazard to defend against. Reframed §7 from "Risks"
to "Design notes" — curiosity bound is a signal-relative scale, not
a defensive cap.

Fix the contradiction this exposed in the formula: previous
`curiosity_pressure = stagnant_or_worse * curiosity_bound` went to
zero when improving, which would cancel the always-on exploration
the §7 narrative now relies on. Replace with permanent-floor pattern
per pearl_blend_formulas_must_have_permanent_floor:

  curiosity_floor    = 0.2 * curiosity_bound  (CURIOSITY_PERMANENT_FRACTION)
  curiosity_dynamic  = stagnant_or_worse * curiosity_bound
  curiosity_pressure = max(curiosity_dynamic, curiosity_floor)

Now curiosity is always ≥ 20% of bound (anti-overfitting baseline)
and rises toward the bound when stagnant (stagnation breaker).
Updated unit-test guidance to assert pressure > 0 even at z=+10.

CURIOSITY_PERMANENT_FRACTION=0.2 added to Invariant-1 fraction list.
Saboteur-rising-with-improvement reframed as adversarial-load feature
rather than over-stress risk.
2026-05-04 00:08:39 +02:00
jgrusewski
a72a23e6b1 spec(sp11): reward as controlled subsystem — fully ISV-driven
Brainstorm spec for SP11. Resolves the policy-stagnation pathology
surfaced in T10 train-multi-seed-xkjkb seed-0 ep0-14: model finds a
stable fixed point at ep1 (peak val sharpe 80.61), then OVERFITS to
it across remaining epochs (decline 80.61 → 70.58). Q-values grow but
val performance declines because reward function has no improvement
pressure.

Architecture (every input ISV-driven):
  - Z-score-driven adaptation (no hardcoded "improving" threshold):
      improvement_z = val_sharpe_delta_ema / max(val_sharpe_std_ema, EPS)
  - 10 ISV outputs: 6 component weights + curiosity_pressure +
    saboteur_intensity_mult + adaptive weight_floor + curiosity_bound
  - 5 ISV canaries: val_sharpe_delta + val_sharpe_std (Z-score noise
    estimate) + 6 per-component grad ratios + saboteur engagement +
    PnL magnitude EMA (signal-relative curiosity bound)
  - 4 new producer kernels (controller + 3 canary computers)
  - Audit + migrate hardcoded cf_weight=0.3 in mse_loss_kernel.cu:318
    and c51_loss_kernel.cu:789, plus other shaping multipliers
  - NEW reward dimension: curiosity bonus, bounded by PnL magnitude

Per pearl_controller_anchors_isv_driven: every threshold replaced with
sigmoid(z) — no constants encode "what counts as improving". Per
pearl_blend_formulas_must_have_permanent_floor: every weight has
adaptive floor preventing zero-out. Per pearl_engagement_rate_self_
correction: saboteur intensity self-corrects via engagement rate canary.
Per pearl_cold_start_exit_signal_or: improvement signal OR'd from
multiple canaries so single-signal-failure doesn't stall controller.

New pearl authored alongside spec: pearl_reward_as_controlled_subsystem
— meta-principle that every reward path degree of freedom is a unified
controller output. Subsumes controller-anchor pearl at the reward layer.

Scope: 20 ISV slots, 4 producer kernels, audit + migration of
hardcoded reward shaping constants, 1 atomic commit.
~1300-1700 LOC; ~2.5-3 hours subagent work.

Success metric: val_sharpe[ep20] > val_sharpe[ep1] (Fix 33-38 baseline
peaked at ep1; SP11 should shift peak later as model continues
learning).
2026-05-04 00:04:12 +02:00
jgrusewski
e580c1388c fix(log): epoch summary Return uses scientific notation, fixes overflow display
`total_return` from financials.rs:80-94 is log-space cumulative growth
across every per-bar step_return. With ~4M step_returns in a fold-
convergence run, even sub-bps positive bars compound to absurd
magnitudes (observed: 1.93e37%) when displayed as `{:+.2}%`. Math is
correct; display needs scientific notation.

Surfaced in T10 train-multi-seed-xkjkb seed-0 ep3 epoch summary while
SP10 chain validates structural fixes. Cosmetic-only change; no
training-path impact. Audit doc updated with Cosmetic 38.1 entry.
2026-05-03 23:31:11 +02:00
jgrusewski
a5457b0dc7 Revert "fix(log): epoch summary Return uses scientific notation, fixes overflow display"
This reverts commit 368454788a.
2026-05-03 23:28:17 +02:00
jgrusewski
368454788a fix(log): epoch summary Return uses scientific notation, fixes overflow display
`total_return` from financials.rs:80-94 is the log-space cumulative growth
across every per-bar step_return: `exp(sum(ln(max(1+r, 1e-10)))) - 1`.

With ~4M per-bar step returns in a fold-convergence run, even sub-bps
positive bars compound to absurd magnitudes (observed: 1.93e37%) when
displayed as `{:+.2}%`. The math is correct; the display is broken.

Switching to `{:+.3e}%` shows the same magnitude compactly across the
full dynamic range without hiding the number. Comment added explaining
the underlying compute's HFT-inappropriate semantic so a future reader
knows the value is correct-but-meaningless rather than buggy.

Surfaced in T10 train-multi-seed-xkjkb seed-0 ep3 epoch summary while
the SP10 chain validates structural fixes. Cosmetic-only change; no
training-path impact.
2026-05-03 23:27:34 +02:00
jgrusewski
920a2d0219 fix(sp10): unconditional Thompson selector + ISV-driven temperature (Fix 38)
T10 train-multi-seed-khr7c (commit 8a25b330f, post-Fix-37) showed val-Flat-
collapse persisting at the eval-side selector despite all SP9 controller
fixes — dir_entropy=0 / trade_count=1 in 214,654 bars because
experience_action_select branched on eval_mode and used argmax(E[Q]) at
eval. With Hold's E[Q] ≈ 0 and directional E[Q] = ε (small edge minus tx
costs), argmax wins Hold deterministically every bar.

Per pearl_thompson_for_distributional_action_selection (amended): the
rollout SELECTOR is unconditional Thompson at all times (training AND
eval); argmax is reserved for the Bellman TARGET Q computation only
(DDQN target). Per pearl_controller_anchors_isv_driven: the temperature
on the Thompson sample is ISV-driven from the SP9 intent_eval_divergence
canary. Per pearl_blend_formulas_must_have_permanent_floor: MIN_TEMP=0.5
is the permanent-stochasticity floor — the eval selector is NEVER fully
deterministic.

Atomic commit per feedback_no_partial_refactor:

* 1 new ISV slot @ [339..340) (EVAL_THOMPSON_TEMP_INDEX); ISV_TOTAL_DIM
  339 → 340; SP5_PRODUCER_COUNT 165 → 166; layout fingerprint updated
* 1 new scratch slot @ [265..266) (SCRATCH_SP10_THOMPSON_TEMP);
  SP5_SCRATCH_TOTAL 265 → 266
* intent_eval_divergence_compute_kernel.cu extended with 2 params
  (divergence_target_isv_index, scratch_temp_idx) + new compute branch
  temp = clamp(divergence/div_target, 0.5, 2.0); existing scratch_idx
  renamed scratch_div_idx for semantic clarity
* experience_action_select: if (eval_mode) { argmax(E[Q]) } DELETED;
  unconditional temperature-blended Thompson installed:
  q_eff[d] = E[Q][d] + temp · (q_sample[d] − E[Q][d]); defensive clamp
  to [0.5, 2.0] for cold-start before producer first observation. Other
  branches (mag/ord/urg) keep their existing eps-greedy/Boltzmann logic
  per pearl_thompson §3 exemption
* state_layout.cuh: new ISV_EVAL_THOMPSON_TEMP_IDX 339 define
* gpu_dqn_trainer.rs::launch_intent_eval_divergence_compute extended
  with 2 kernel args + second apply_pearls_ad_kernel chain to smooth
  the temperature into ISV[339]
* New FoldReset entry sp10_eval_thompson_temp + dispatch arm in
  reset_named_state writing sentinel 0; Pearl A's first-observation
  replacement fires on the new fold's first producer launch
* Test renamed test_eval_action_select_eval_argmax_picks_best →
  test_eval_action_select_thompson_picks_proportionally; ISV buffer
  setup with EVAL_THOMPSON_TEMP_INDEX=1.0 (pure Thompson); assertions
  updated to ≥ 70% best-direction wins (was ≥ 99% under deterministic
  argmax) and < 100% (selector is sampling)
* Pearl pearl_thompson_for_distributional_action_selection §4 amended;
  MEMORY.md index entry updated
* Audit doc Fix 38 entry

Verification:
* SQLX_OFFLINE=true cargo check -p ml — clean (only pre-existing 18
  warnings; no new errors or warnings introduced by Fix 38)
* SQLX_OFFLINE=true cargo test -p ml --lib state_reset — 4/4 pass
  including contract test every_fold_and_soft_reset_entry_has_dispatch_arm
* SQLX_OFFLINE=true cargo test -p ml --lib sp5_isv_slots — 10/10 pass
  including new sp10_thompson_temp_slot_above_sp9_block
* SQLX_OFFLINE=true cargo test -p ml --lib test_eval_action_select_thompson —
  pass (RTX 3050 Ti local; τ=1.0 with clear Q gap → P(Long) ≥ 0.70 < 1.0)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:42:25 +02:00
jgrusewski
6ae9bc0055 spec(sp10): unconditional Thompson selector + ISV-driven temperature
Brainstorm spec for SP10. Resolves the val-Flat-collapse pathology that
persisted through Fix 33-37: the eval-time argmax in experience_action_
select picks Hold deterministically every val bar (dir_entropy=0,
trade_count=1 in 214k bars) regardless of controller state.

Architecture:
  - Delete `if (eval_mode)` argmax branch in direction-selector kernel
  - Use temperature-blended Thompson: q_eff = E[Q] + τ × (Thompson - E[Q])
  - τ = clamp(intent_eval_divergence / divergence_target, 0.5, 2.0)
  - τ self-corrects: collapse → high τ; healthy → low τ; permanent 0.5 floor
  - Reuses SP9's intent_eval_divergence_compute_kernel (extended, not new)

Per pearl_controller_anchors_isv_driven: τ is ISV-driven, no
hardcoded constants beyond Invariant 1 numerical anchors (clamp range).
Per pearl_blend_formulas_must_have_permanent_floor: MIN_TEMP=0.5 ensures
eval ALWAYS has stochasticity.

The pearl_thompson_for_distributional_action_selection was about Bellman
TARGET argmax (selector/target symmetry). It does NOT prohibit Thompson
at the rollout selector. SP10 amends the pearl to clarify.

Scope: 1 ISV slot, kernel modification (no new kernel — extend SP9's
producer), 1 consumer kernel rewrite, test update, pearl amendment,
audit doc Fix 38. ~300-500 LOC, single atomic commit per
feedback_no_partial_refactor.
2026-05-03 22:24:14 +02:00
jgrusewski
8a25b330fc fix(sp9): targets as Invariant-1 anchors + divergence sentinel handling
Two targeted fixes to the SP9 Kelly cold-start warmup floor (Fix 37,
commit `48a8b9ee7`) for quirks observed in smoke-test-wrwkz on a
5-epoch L40S smoke. Atomic per `feedback_no_partial_refactor`.

**Quirk 1 — EMA target saturation:** ep1 HEALTH_DIAG showed
`stat_count_tgt=419 div_tgt=105002 temp_tgt=1.0 conf [stat=1.000
bhv=0.333 tmp=1.000] floor=0.0`. Pearl A sentinel-bootstrap on the
3 EMA target updaters (`kelly_*_target_ema_kernel.cu`) caused the
first observation to replace the sentinel directly, so
`target = first_obs`, `current/target = 1.0`, `confidence = 1.0`,
`floor = base × (1 − 1.0) = 0` — defeating the cold-start mechanism.

The principled fix per `feedback_isv_for_adaptive_bounds.md`:
"sufficient" is defined in absolute terms via Invariant-1 numerical
anchors. The 3 target ISV slots become constructor-written constants
(written ONCE in trainer constructor, identical pattern to existing
`config.cql_alpha → ISV[CQL_ALPHA_INDEX=48]` from Plan 1 Task 12).

  - ISV[KELLY_SAMPLE_COUNT_TARGET_INDEX=333] = 100.0 (trades)
  - ISV[KELLY_DIVERGENCE_TARGET_INDEX=334]   = 2.0   (ratio)
  - ISV[KELLY_TEMPORAL_TARGET_INDEX=335]     = 5.0   (epochs)

**Quirk 2 — divergence ratio explosion:** ep1 showed
`divergence=70005`. Algebraically `intent_f / max(eval_f, 1e-6) =
0.07 / 1e-6 = 70 000` because `ISV[EVAL_DIST_F_INDEX=338]` was still
at Pearl A sentinel 0 before the first val window populated it.
Algebraically the warmup-floor kernel still derived
`behavioral_conf = 0` (correct semantic), but the synthetic 70 000
in HEALTH_DIAG masked the real signal flow.

Fix: explicit sentinel detection in
`intent_eval_divergence_compute_kernel.cu`:

  if (eval_f < SENTINEL_THRESHOLD=1e-5)
    divergence = SENTINEL_DIVERGENCE=1e6  // forces behavioral_conf=0
  else
    divergence = intent_f / eval_f

Same `behavioral_conf = 0` outcome but with explicit provenance —
HEALTH_DIAG now reads `divergence=1e6` until eval_f matures.

**Atomic structure:**

- DELETE 3 EMA target updater kernels (`.cu` files)
- DELETE 3 entries from `crates/ml/build.rs::kernels_with_common`
- DELETE 3 cubin static byte arrays + 3 struct fields + 3 cubin
  loaders + 3 fields in trainer construction tuple in
  `gpu_dqn_trainer.rs`
- DELETE 3 launches in `launch_sp9_kelly_warmup_floor` (chain
  shrinks 5 → 2: q_var_mag_ema + main warmup-floor)
- DELETE 3 scratch slots; SP5_SCRATCH_TOTAL 268 → 265;
  SCRATCH_SP9_EVAL_DIST_BASE slides 265 → 262
- ADD constructor-write of 3 Invariant-1 anchors (100.0, 2.0, 5.0)
  immediately before layout-fingerprint write
- UPDATE 3 dispatch arms in `reset_named_state` to rewrite the
  Invariant-1 anchors at fold boundary (NOT sentinel 0) — these
  are constants, not stateful EMAs
- UPDATE 3 registry descriptions to reflect Invariant-1 anchor
  semantic + smoke-test-wrwkz evidence
- UPDATE `intent_eval_divergence_compute_kernel.cu` with
  sentinel-detect branch
- ISV slot layout UNCHANGED (3 target slots @ ISV[333..336)
  remain); Wiener-buffer linear span (`SP5_PRODUCER_COUNT=165`)
  UNCHANGED — 3 wiener triples for deleted producers become
  reserved-unused like Pearl 6's [525..543) carve-out
- audit doc Fix 37.1 entry with full provenance + smoke evidence

**Verification:**

- `SQLX_OFFLINE=true cargo check -p ml` — clean (only pre-existing
  18 warnings; no new errors or warnings).
- `SQLX_OFFLINE=true cargo test -p ml --lib state_reset` — 4/4 pass
  including `every_fold_and_soft_reset_entry_has_dispatch_arm`.
- `SQLX_OFFLINE=true cargo test -p ml --lib sp5_isv_slots` — 9/9
  pass including `sp9_slots_contiguous_above_sp8_block`.

**Expected smoke signature post-fix:**

- floor: starts ≈ base_floor × 1.0, decays as confidence accumulates
- divergence: 1e6 until first val window, small (≤ 5) afterward
- conf: stat=count/100, bhv=0 until eval_dist matures, tmp=ep/5
- combined_conf reaches 1.0 only when at least one axis genuinely
  matures (typically temporal at epoch 5 in fold 0)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:36:11 +02:00
jgrusewski
48a8b9ee7c fix(sp9): Kelly cold-start ISV-driven warmup floor (Fix 37)
Per pearl_cold_start_exit_signal_or.md + pearl_controller_anchors_isv_driven.md:
the Kelly cap's `warmup_floor` and its release condition were both
regime-encoded constants. Single-axis statistical cold-start exit
(`total_trades >= 10` in trade_physics.cuh::kelly_position_cap) created
a chicken-and-egg deadlock: cap closed → no trades → no Kelly samples →
cap stays closed. T10 wsnc6 ep1-3 evidence (post-Fix 36):
intent_dist_f rising to 0.47 while eval_dist_f pinned to 0.00 with
kelly_f=0 across all epochs — train-vs-eval divergence as direct
symptom of the cold-start deadlock.

SP9 lifts the floor and its release condition fully onto ISV. The exit
condition is OR'd across statistical / behavioral / temporal axes per
the pearl — any single axis firing exits cold-start.

Atomic commit per feedback_no_partial_refactor:

- 9 new ISV slots @ [330..339):
  - KELLY_WARMUP_FLOOR_INDEX (330) — adaptive floor consumed by
    unified_env_step_core
  - Q_VAR_MAG_EMA_INDEX (331) — historical EMA baseline for the
    self-relative `base_floor` ratio (eliminates 0.5 hardcoded
    half-Kelly)
  - INTENT_EVAL_DIVERGENCE_INDEX (332) — behavioral-axis numerator
  - 3 EMA targets (sample_count / divergence / temporal)
  - 3 EVAL_DIST_{Q,H,F}_INDEX — replaces DtoH
    read_eval_intent_magnitude_distribution()
- ISV_TOTAL_DIM 330 → 339; SP5_PRODUCER_COUNT linear span 156 → 165
- 9 new scratch slots @ [259..268); SP5_SCRATCH_TOTAL 259 → 268
- 7 new producer kernels (single-block cold-path, chained through
  apply_pearls_ad_kernel for Pearl A bootstrap + Pearl D Wiener-α):
  - eval_intent_dist_compute_kernel.cu — GPU reduction of
    intent_mag_buf, replaces DtoH path per
    feedback_no_cpu_compute_strict.md
  - intent_eval_divergence_compute_kernel.cu
  - q_var_mag_ema_compute_kernel.cu
  - kelly_sample_count_target_ema_kernel.cu
  - kelly_divergence_target_ema_kernel.cu
  - kelly_temporal_target_ema_kernel.cu
  - kelly_warmup_floor_compute_kernel.cu — main producer combining
    all 8 ISV inputs
- consumer migration in trade_physics.cuh::unified_env_step_core:
  health_safety_sp9 = fmaxf(health_safety_sp5,
                            isv[SP9_KELLY_WARMUP_FLOOR_INDEX])
  threading through apply_kelly_cap's health_floor parameter; zero
  sentinel → no-op via fmaxf so existing path's behavior is preserved
- DELETE read_eval_intent_magnitude_distribution() in
  gpu_backtest_evaluator.rs (was DtoH read_all + host loop) per
  feedback_no_htod_htoh_only_mapped_pinned.md; replaced by
  intent_mag_dev_ptr_and_n() GPU accessor; metrics.rs:908 consumer
  migrated from DtoH to launch + ISV mapped-pinned read
- 9 new state-reset registry FoldReset entries + 9 dispatch arms in
  reset_named_state (Pearl A bootstrap on first observation)
- HEALTH_DIAG sp9_kelly_warmup line emits floor + divergence + 3
  confidence axes + 3 EMA targets per feedback_no_hiding
- audit doc Fix 37 entry with full provenance, files touched, pearls
  applied, verification

Constants: only KELLY_FLOOR_MIN_RATIO=0.25, KELLY_FLOOR_MAX_RATIO=1.0,
EPS_DIV=1e-6 — Invariant 1 numerical-stability anchors per
feedback_isv_for_adaptive_bounds.md. All thresholds, targets, and
the floor itself are signal-driven via ISV.

Pre-existing 18 cargo warnings unchanged. State-reset contract test
`every_fold_and_soft_reset_entry_has_dispatch_arm` passes for all 9
new entries; SP5 ISV slot layout test
`sp9_slots_contiguous_above_sp8_block` passes;
`SQLX_OFFLINE=true cargo check -p ml` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:56:34 +02:00
jgrusewski
38d2408604 spec(sp9): Kelly cold-start warmup floor — fully ISV-driven design
Brainstorm spec for SP9 (Fix 37). Resolves the val-Flat-collapse pathology
surfaced in T10 train-multi-seed-wsnc6 ep1-3: training intent_dist_f=0.47
but eval_dist_f=0.00 because Kelly cap pinned eval mag to Quarter, with
trade_count=1 in 214k bars (chicken-and-egg: cap closed → no trades → no
Kelly samples → cap stays closed).

Architecture per pearl_controller_anchors_isv_driven and
pearl_cold_start_exit_signal_or:

  final_kelly_f = max(measured_kelly_f, warmup_floor)
  warmup_floor  = base_floor × (1 − combined_confidence)
  base_floor    = clamp(q_var_mag / q_var_mag_ema, MIN, MAX)
  combined_confidence = max(statistical, behavioral, temporal)

All thresholds ISV-driven via Pearl D Wiener-α; only Invariant 1 numerical
anchors (clamp ranges, EPS) remain as constants.

Scope: 9 new ISV slots, 6 new GPU producer kernels including a
mandatory eval_dist GPU migration (eliminating DtoH readback at
gpu_backtest_evaluator.rs:1353 per feedback_no_cpu_compute_strict).
~1000-1200 LOC, single atomic commit per feedback_no_partial_refactor.

Pearls authored as part of brainstorm:
  - pearl_controller_anchors_isv_driven (regime-encoded constants)
  - pearl_cold_start_exit_signal_or (OR'd signals across statistical /
    behavioral / temporal axes)
2026-05-03 20:33:36 +02:00
jgrusewski
e0dbae3c99 fix(sp7): ISV-driven MAX_BUDGET via GPU train_active_frac canary (Fix 36)
Per pearl_controller_anchors_isv_driven.md: Fix 35 (CQL formula direction
flip) is necessary but insufficient — `const float MAX_BUDGET = 1.0f;` at
line 113 of loss_balance_controller_kernel.cu is regime-encoded the same
way the original CQL target_ratio formula was. Under healthy training
the cap is fine; under val-Flat-collapse it lets the controller saturate
budgets to 1.0 while the model is regressing into Flat.

The pearl prescribes the structural fix: pick the canary that fires
under the failure mode → train_active_frac (Long+Short / total during
training rollout). Lift it onto ISV with Pearls A+D smoothing; route it
through a producer kernel that derives per-(head, branch) cap via linear
interpolation FLOOR + active_frac × (CEIL - FLOOR); read the cap from
ISV in the controller kernel (no more hardcoded 1.0).

Atomic commit per feedback_no_partial_refactor:

- new kernel: train_active_frac_compute_kernel.cu — single-thread
  reduction over monitoring_summary[5..17), writes scratch[250]; chained
  apply_pearls_ad → ISV[TRAIN_ACTIVE_FRAC_INDEX=321]
- new kernel: loss_balance_max_budget_compute_kernel.cu — 8 threads
  (2 heads × 4 branches), reads ISV[TRAIN_ACTIVE_FRAC_INDEX], writes
  scratch[251..259); chained apply_pearls_ad ×8 →
  ISV[LB_MAX_BUDGET_{CQL,C51}_BASE..+4)
- consumer: loss_balance_controller_kernel.cu — replace
  `const float MAX_BUDGET = 1.0f;` with per-(head, branch) ISV reads,
  defensive Pearl A bootstrap clamp
- 9 new ISV slots @ [321..330); ISV_TOTAL_DIM 321 → 330;
  SP5_PRODUCER_COUNT linear span 147 → 156
- 9 new scratch slots @ [250..259); SP5_SCRATCH_TOTAL 250 → 259
- delete CPU train_active_frac compute (training_loop.rs:3392-3396) per
  feedback_no_cpu_compute_strict; delete host field
  `last_train_active_frac: f32`; replace with accessor reading from ISV
- 3 new state-reset registry entries (FoldReset; Pearl A bootstrap on
  first observation) + dispatch arms in reset_named_state
- audit doc: Fix 36 entry with full provenance, files touched, pearls
  applied, verification

Pre-existing 18 cargo warnings unchanged. State-reset contract test
`every_fold_and_soft_reset_entry_has_dispatch_arm` passes for all 3 new
entries; SP5 ISV slot layout test
`sp8_max_budget_slots_contiguous_and_above_activation_block` passes;
`SQLX_OFFLINE=true cargo check -p ml` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:26:01 +02:00
jgrusewski
65c3083dec fix(sp7): flip CQL target_ratio direction — death spiral on magnitude
`target_ratio[CQL][b] = ANCHOR_CQL_RATIO * (1 - flatness[b])` had the
sign inverted: it pushed CQL HIGH when Q was flat (the collapse state)
and LOW when Q had variance. Per pearl_2_budget_kernel.cu, `flatness =
var_q / σ²`, so flatness HIGH means Q has variance — exactly when
overconfidence-risk-driven CQL pressure should kick in.

Symptom (T10-v3 train-multi-seed-x7sl2 logs): Once IQN Fix 34 woke
IQN-mag/ord/urg branches and produced real Q-targets, the controller's
inverted formula sent cql_budget to MAX_BUDGET=1.0 saturation on every
branch, the conservative pull kept Q flat, the model collapsed to
single-Flat-action eval (val_active_frac=0, dir_entropy=0,
trade_count=1, sharpe=0).

Fix is one operational character: `(1 - flatness)` → `flatness`. C51
formula was already correctly aligned (`flatness * ANCHOR_C51_RATIO`)
and unchanged.

Per pearl_controller_anchors_isv_driven.md: the inverted formula was
masked for months because the IQN-dead regime (Fix 34) suppressed
cql_raw on mag/ord/urg, never letting the controller engage with the
inverted target. Fixing the IQN regime exposed the dormant formula bug.

ANCHOR_CQL_RATIO=2.0 and MAX_BUDGET=1.0 remain hardcoded; Fix 36 will
make those ISV-driven via a GPU train_active_frac canary signal per
the pearl's "pick the canary that fires under the pathology" rule.
2026-05-03 19:00:26 +02:00
jgrusewski
9b5296b2f0 fix(iqn): set_cached_target_h_s2 per branch — fixes silent 3/4 IQN branch dropout
execute_training_pipeline consumes cached_target_h_s2_ptr via .take(), so
the SP6 Pearl 5 per-branch loop (4 sequential iqn calls per step) only
worked for branch 0. Branches 1, 2, 3 saw cached_ptr=None, returned a
"non-fatal" Err logged as a warning, and silently skipped apply_iqn_
trunk_gradient — only the direction branch's IQN quantile signal ever
reached the trunk, for months.

Surfaced in T10-retry train-multi-seed-ksjcm logs as repeated:
  WARN: IQN parallel branch 1 step failed (non-fatal):
  WARN: IQN parallel branch 2 step failed (non-fatal):
  WARN: IQN parallel branch 3 step failed (non-fatal):
    cached_target_h_s2_ptr is None.

Pre-existing since SP6 Pearl 5 (P4.T3-era multi-quantile IQN per-branch
τ schedules) — masked by the non-fatal warning classification.

Likely explains:
  - cql_mag persistence at SP7 smoke (cql_mag=0.07 vs cql_dir=1.0):
    CQL was the only learning signal mag had because IQN-mag was dead.
  - SP7 c51 1000:1 controller suppression on mag/ord/urg may stabilize
    at non-collapse values once IQN signal returns to those branches.

Fix is local: re-set cached ptr inside both 4-branch loops (parallel arm
near line 2123, sequential arm near line 2308). The ptr is the same
value (target_h_s2 is per-step, not per-branch), but the .take()
contract requires set-per-call. Defensive single-shot semantic preserved
— if a future bug skips the set, IQN errors loud rather than reusing
stale ptr.

Per feedback_no_partial_refactor: SP6 changed the call pattern 1→4 per
step but didn't migrate the cache contract. Per feedback_no_hiding: the
non-fatal classification hid the structural bug; escalation to hard
error deferred to next commit after post-fix T10 validates the warnings
disappear.

Audit doc updated with Fix 34 entry.
2026-05-03 18:20:33 +02:00
jgrusewski
07f5ed5d74 fix(sp7): GPU dispatch for cql/c51 budget — eliminate capture-time host-branch freeze
The SP7 controller wrote real budgets to ISV[BUDGET_CQL_BASE/C51_BASE] and
the activation flag correctly transitioned 0→1, but downstream SAXPY ops
still saw exactly 0.02/0.05 every step. Root cause: compute_adaptive_budgets
ran host-side Rust with `if cql_active >= 0.5 { real } else { bootstrap }`,
captured into the aux_child CUDA Graph at step 0 of each fold (when
cql_active is FoldReset sentinel 0.0). The bootstrap branch resolves to
literal 0.02/0.05 and freezes into kernel arg buffers; ~1000 graph replays
per fold use frozen scalars regardless of runtime ISV state.

Same disease as SP4 host-side EMA elimination 2026-05-01. Fix:

- New consume_lb_budget_kernel.cu (8 threads, 1 block): reads ISV slots
  for activation flag + controller budget per (head, branch); applies
  if/else dispatch on-device; writes trunk_mean + per-branch correction
  to mapped-pinned scratch buf [10] = (cql_trunk + cql_corr×4 + c51_trunk
  + c51_corr×4). Same cubin houses dqn_{scale,saxpy}_f32_dev_ptr_kernel
  variants reading alpha from a device pointer (no kernel-arg freeze).
- Buffer + launcher + cubin loading + 4 dev-ptr accessor methods
  in gpu_dqn_trainer.rs.
- apply_c51_budget_scale / apply_cql_saxpy + per-branch variants take
  alpha_dev_ptr: u64 instead of scalar f32. The "skip when ≈ 1.0"
  optimization is dropped — the value lives on-device. Existing scalar
  dqn_{saxpy,scale}_f32_kernel are NOT modified — they remain reused
  by distill, VSN dW dilution, etc.
- compute_adaptive_budgets refactored to launch the dispatch kernel
  inline (captured along with consumers; replay-time fresh values
  every step) and stop returning host-resolved CQL/C51 scalars.
  IQN/ENS keep their host-side resolution (out-of-scope per change
  scope — no activation-flag dispatch).
- HEALTH_DIAG reads from lb_budget_effective_buf via new accessor
  methods on FusedTrainingCtx; dead caches on GpuDqnTrainer
  (last_{cql,c51}_budget_eff / _per_branch) deleted.
- Audit doc Fix 33. Memory pearl out-of-tree.

Bootstrap byte-equivalence preserved: kernel-arg constants
cql_bootstrap=0.02 / c51_bootstrap=0.05 remain byte-identical to the
prior host-side CQL_BOOTSTRAP_BUDGET / C51_BOOTSTRAP_BUDGET literals
and to loss_balance_controller_kernel.cu's COLD_START_FLOOR_* anchors.

Files: consume_lb_budget_kernel.cu (new), build.rs, gpu_dqn_trainer.rs,
fused_training.rs, training_loop.rs, dqn-wire-up-audit.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:54:36 +02:00
jgrusewski
14af4854fb fix(merge): resolve sp5→main merge fixups — DevicePtrMut import + adam_step 7-arg signature
After sp5 merge with -X theirs, two compile errors needed manual fix:
- gpu_her.rs lost the DevicePtrMut trait import
- gpu_tlob.rs Fix 20 regression test used the 4-arg adam_step signature
  before sp5's Pearl 4 added β1/β2/ε params.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:11:23 +02:00
jgrusewski
6c3a91c878 fix(sp7): wire raw CQL norm kernel reading cql_grad_scratch directly
The SP7 controller's CQL reference signal was reading cql_sx (post-SAXPY,
budget-scaled) which created a self-perpetuating deadlock at small
budget values: cql_sx_norm = budget × raw_grad → small budget → small
cql_sx → controller can't update → budget stays small.

The earlier offset 6 → 3 attempt failed because grad_decomp_launch_cql
was never effectively populating slot 3 — the snapshot pattern measures
‖grad_buf − snapshot‖, but apply_cql_gradient writes to cql_grad_scratch
(separate buffer), not grad_buf, so the snapshot delta is always 0.

This commit adds a real producer kernel cql_raw_norm_compute that reads
cql_grad_scratch directly and computes ‖raw_cql‖ over mag/dir/trunk
slices. Wired to fire AFTER apply_cql_gradient and BEFORE
apply_cql_saxpy, populating grad_decomp_result_pinned[3..6] with the
raw norm independent of cql_budget.

SP7 launcher updated to read from offset 3 (now: real raw CQL norm,
not the never-populated cql snapshot delta). HEALTH_DIAG label renamed
cql_sx → cql_raw to reflect the new contract; component index in the
cached grad_component_norms_* arrays switched 2 → 1.

The historical grad_decomp_launch_cql() call is removed — keeping it
would overwrite the slot with 0 after cql_raw_norm_compute fires. The
paired grad_decomp_snapshot_cql snapshot is left in place to scope the
diff to Path A; buffer cleanup (grad_snapshot_cql allocation +
grad_decomp_launch_cql definition) belongs in a follow-up commit per
feedback_no_partial_refactor.

Files: cql_raw_norm_kernel.cu (new, 97 LOC), build.rs,
gpu_dqn_trainer.rs (struct field + cubin static + load + launcher +
SP7 read offset 6→3), loss_balance_controller_kernel.cu (docstring +
arg comment), fused_training.rs (4 launch_cql_raw_norm call sites,
1 dead grad_decomp_launch_cql call removed), training_loop.rs
(HEALTH_DIAG label + index), audit doc Fix 31 SP7 Path A entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 14:55:40 +02:00
jgrusewski
6cf6f26ab9 diag(sp7): emit grad_decomp_pinned + lb_active_per_branch HEALTH_DIAG
The SP7 smoke at 237b3dbfb showed all 8 (head, branch) combinations
stuck at bootstrap across 5 epochs, despite Q-variance and gradient
norms being non-zero. The activation flag mechanism is monotonic per
fold (once active, stays active), so 8/8 inactive across all observed
epochs implies the kernel never hits the active path — but we can't
confirm whether that's because (a) grad_decomp_result_pinned reads
zero at SP7's epoch-boundary call site, or (b) something else.

Added two HEALTH_DIAG lines:
- grad_decomp_pinned — reads bytes [0..12,24..36,36..48] of the
  pinned buffer the SP7 kernel reads; surfaces iqn/cql_sx/c51 mag/
  dir/trunk norms exactly as the kernel sees them.
- lb_active_per_branch — reads LB_{CQL,C51}_ACTIVE_BASE+0..4 from
  ISV; surfaces the activation flag state per (head, branch).

Together these disambiguate "grad_decomp not populated at SP7 read
time" from "values populated but kernel cold-start branch fires for
other reasons".

Purely additive — no behavior change. Audit doc Fix 31 sub-bullet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:30:57 +02:00
jgrusewski
237b3dbfb0 fix(sp7): bump ISV_TOTAL_DIM 294→321 to match SP5_SLOT_END
SP7 T1 added 24 ISV slots (LB_DIFF_VAR_CQL_BASE=297 ... LB_C51_ACTIVE
through 321) without bumping ISV_TOTAL_DIM, leaving SP7's wiener-state
and activation slots out-of-bounds of the allocated pinned buffer
(294 * 4 = 1176 bytes; SP7 slots write to bytes 1188..1284). GPU
direct-pointer writes silently corrupted memory in the next page;
CPU read_isv_signal_at returned garbage in release builds.

This explains the SP7 smoke's confusing zero-budget-everywhere
observation: the activation flag was reading OOB memory, the consumer
saw inconsistent values, and the controller's actual ISV state was
never visible.

Bumped ISV_TOTAL_DIM to 321 (max valid index 320, covering
SP5_SLOT_END-1). Made pub(crate) so the new contract test can reference
it from sp5_isv_slots.rs. Updated layout_fingerprint_seed()'s slot
entries to include the previously missing D3 and SP7 slots
(TRAINING_SHARPE_EMA=294 through LB_C51_ACTIVE_BASE=317) and updated
ISV_TOTAL_DIM= literal to 321 in lockstep per feedback_no_partial_refactor.

Added contract test all_sp5_slots_fit_within_isv_total_dim to
permanently gate this class of bug at cargo test time. The test would
have caught SP7 T1 instantly; future slot allocations cannot regress
this.

Files: gpu_dqn_trainer.rs (ISV_TOTAL_DIM + comment + fingerprint),
sp5_isv_slots.rs (test), docs/dqn-wire-up-audit.md (Fix 31 sub-bullet).

Cargo check workspace clean. Cargo test ml --lib 935 passed (934
baseline + 1 new contract test); 16 pre-existing GPU-hardware failures
unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 12:45:59 +02:00
jgrusewski
bbf15ad690 diag(sp7): HEALTH_DIAG emit Q_VAR_PER_BRANCH signal
The SP7 controller's flatness gate reads per-branch Q-variance from
ISV[Q_VAR_PER_BRANCH_BASE=222..226) but that signal was not visible in
HEALTH_DIAG. The existing "var_q" in the main HEALTH_DIAG line is
realized step-return variance per magnitude bin from
gpu_experience_collector — a trade-outcome metric, semantically
distinct from per-branch Q-output variance.

Added one emit line immediately after cql_budget_per_branch:
  HEALTH_DIAG[E]: q_var_per_branch [dir=X.XXXX mag=X.XXXX ord=X.XXXX urg=X.XXXX]

Reads ISV[222..226) via read_isv_signal_at — the same slots written by
q_branch_stats_kernel.cu (scratch slot 2 per branch) and routed into ISV
by apply_pearls_ad_kernel in launch_sp5_pearl_1_atom. No new ISV slots,
no kernel change, no StateResetRegistry entry.

Class 2 signal (mag_concat_scale / q_rms): Option A infeasible — q_rms
is a per-sample register variable in mag_concat_qdir with no existing ISV
slot; h_s2_rms_ema at ISV[96] is the only available proxy. Option B
(new ISV slot) blocked pending explicit controller OK. See audit doc for
full stop-and-report rationale.

Files touched:
  crates/ml/src/trainers/dqn/trainer/training_loop.rs (+28 LOC)
  docs/dqn-wire-up-audit.md (+13 LOC)

[ISV slot decision: Option A reused existing Q_VAR_PER_BRANCH_BASE=222..226]
Cargo check workspace clean. State-reset contract test passes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 12:33:56 +02:00
jgrusewski
cb6a89714b sp7(controller): activation flag + Welford-α to escape bootstrap trap
Fixes the SP7 controller dormancy discovered in smoke-test-8556k:
per-branch budgets stuck at exactly bootstrap constants because the
kernel's cold_start_basis numerically equaled the consumer's bootstrap
fallback, AND the Wiener-α was clamped at ALPHA_FLOOR=1e-4 from step 1.

Architectural change:
- 8 new ISV slots LB_{CQL,C51}_ACTIVE_BASE per (head × branch).
  Activation flag is monotonic per fold, FoldReset on boundary.
- Kernel only sets active=1 when grads are populated AND on subsequent
  active-state computation; cold-start branch leaves active=0 (fresh
  branch) or holds prior budget steady (transient grad gate).
- Consumer dispatches on activation: bootstrap when active<0.5,
  controller verbatim when active>=0.5. No more spurious bootstrap
  when controller writes legitimate small values.
- Welford-α hybrid (max of 1/max(1,epoch_idx_in_fold) and Wiener-α)
  gives full update on first active step, falls off as 1/N until
  Wiener takes over with meaningful variance estimates. EPOCH_IDX_INDEX
  is the existing per-fold-reset counter (no new tuned constants).

State reset registry: 2 new sp7_lb_*_active FoldReset entries +
matching dispatch arms in reset_named_state. Contract test
(every_fold_and_soft_reset_entry_has_dispatch_arm) gates compile.

GPU unit test sp7_loss_balance_controller_activation_flag_transitions
exercises 3 transitions (cold start → both flags 0; active → both
flags 1 with controller-computed budget != bootstrap; transient grad-
gate → flags hold at 1, prior budget held verbatim). Passes on local
RTX 3050 Ti.

Audit doc: Fix 31 sub-bullet describing the activation-flag fix.
Memory pearl out-of-tree (controller will dispatch separately).

Touched:
  crates/ml/src/cuda_pipeline/sp5_isv_slots.rs
  crates/ml/src/cuda_pipeline/loss_balance_controller_kernel.cu
  crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs
  crates/ml/src/trainers/dqn/fused_training.rs
  crates/ml/src/trainers/dqn/state_reset_registry.rs
  crates/ml/src/trainers/dqn/trainer/training_loop.rs
  crates/ml/tests/sp5_producer_unit_tests.rs
  docs/dqn-wire-up-audit.md

Cargo check workspace clean. Cargo test ml --lib clean (incl. contract
test + 6 sp5_isv_slots tests). 16 pre-existing failures unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 11:34:21 +02:00
jgrusewski
07ac5ceea5 test(state_reset): contract test — every RegistryEntry has dispatch arm
Surfaced the SP7 T7 dispatch-arm bug at runtime (unknown-name panic at
fold boundary). This test enumerates RegistryEntry names from
StateResetRegistry::new() and asserts each has a match arm in
reset_named_state, catching the bug at `cargo test -p ml --lib`
instead of mid-training.

Source-introspection design — no production-code change. Test fails
fast if a future contributor adds a registry entry without the
corresponding dispatch arm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:31:02 +02:00
jgrusewski
d140ace635 docs(audit): T8 — memory pearl + MEMORY.md index landed (out-of-tree)
The pearl_loss_balance_controller memory pearl is at
~/.claude/projects/.../memory/pearl_loss_balance_controller.md (not in
git — user-memory subsystem). MEMORY.md index entry updated.

This audit-doc commit records the T8 landing for the Fix 31 chain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:18:53 +02:00
jgrusewski
023c62da28 test(magnitude_distribution): assert on EVAL_INTENT (pre-Kelly-cap), not EVAL_DIST
Pre-existing test bug: line 164 asserted `ef >= 0.05` (post-Kelly-cap
eval_dist), which gates on Kelly cold-start warmup completing within
the smoke's training horizon — not on whether the Q-head learned to
prefer Full magnitude. On the local-laptop smoke (1 quarter MBP-10),
Kelly warmup never completes, pinning eval_dist[Full] = 0 even when
Q(Full) > Q(Half) clearly.

Per `project_magnitude_eval_collapse_kelly_capped`, the diagnostic
split landed in #212: intent_dist measures policy learning, eval_dist
measures policy + Kelly-cap. Tests asserting on Q-learning success
must use intent_dist; the test was never updated.

Smoke now PASSES with intent_full=0.057 (intent_full=0.866 in the
prior re-run — both above the 0.05 threshold; Q(Full) is clearly
preferred when Kelly cap doesn't suppress it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:16:58 +02:00
jgrusewski
6e479c55c7 sp7(state): T2 bug-fix — add 4 missing reset_named_state dispatch arms
T2 (commit aa2854017) registered 4 SP7 ResetEntries:
  sp7_lb_diff_var_cql    → ISV[297..301)
  sp7_lb_sample_var_cql  → ISV[301..305)
  sp7_lb_diff_var_c51    → ISV[305..309)
  sp7_lb_sample_var_c51  → ISV[309..313)

But never added their dispatch arms in `reset_named_state`. At fold
boundary, the dispatcher panics with "unknown name 'sp7_lb_diff_var_cql'"
because every FoldReset entry must have a matching match arm.

Same shape as bug #281 (SP5 Layer A bug-fix). Surfaced by the SP7 T7
local sanity smoke (test_magnitude_distribution).

The 4 new arms mirror the existing `sp5_budget_cql` / `sp5_budget_c51`
template — `for b in 0..4 { write_isv_signal_at(BASE + b, 0.0); }`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:54:49 +02:00
jgrusewski
2de9a58922 sp7(wire): T7 polish — fix 2 stale docstrings around compute_adaptive_budgets
Two clarity-only edits surfaced by code review:
- M1: compute_adaptive_budgets header docstring claimed floors of
  "0.05 C51, 0.05 IQN, 0.02 CQL, 0.02 Ens" — stale post-T7 (IQN is now
  0.11, others are sentinel-aware bootstrap). Rewritten to reflect the
  SP7 contract: IQN keeps BASE_IQN=0.11 structural floor; C51/CQL/ENS
  use sentinel-aware bootstrap.
- M2: last_ens_budget_eff docstring misattributed ENS to "SP5 Pearl 2
  (flatness-modulated)" — Pearl 2 stopped writing BUDGET_ENS_BASE in
  T6. Rewritten to state ENS has no active controller driver in SP7;
  bootstrap-only.

Comment-only — zero runtime effect. cargo check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:27:36 +02:00
jgrusewski
55b8457fdf sp7(wire): launch loss-balance controller + sentinel-aware consumer (atomic)
Atomic per feedback_no_partial_refactor — launch site + consumer floor
change + stale doc are three halves of the same contract:

(a) launch_loss_balance_controller wired into the per-step pipeline
    after launch_sp5_pearl_2_budget (FLATNESS_BASE populated) and after
    backward (grad_decomp pinned slots populated).

(b) compute_adaptive_budgets replaces hard floors (0.02/0.05) with
    sentinel-aware bootstrap. The controller may now drive budgets near
    zero when the structural target says so. IQN keeps BASE_IQN=0.11
    structural floor (reference, can't be 0).

(c) All 4 stale "B4/G5" budget-eff docstrings rewritten to reflect
    actual ownership: last_iqn_budget_eff (Pearl 2 flatness),
    last_cql_budget_eff (SP7 controller), last_c51_budget_eff (SP7
    controller), last_ens_budget_eff (Pearl 2 flatness +
    sentinel-aware bootstrap). The previous claims (e.g.,
    "0.10×(1−regime)×health") were never implemented; per
    feedback_trust_code_not_docs, code wins over docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:14:30 +02:00
jgrusewski
d704ffd1bc docs(dqn): T6 polish — fix 4 stale Pearl 2 prose references in gpu_dqn_trainer.rs
Four comment/docstring sites still described the pre-SP7 Pearl 2 as
writing "20 floats / 5 outputs / c51/cql/ens" after the T6 kernel-
signature shrink. Updated to reflect the current 6-arg kernel writing
8 floats (IQN + flatness only) into scratch[115..119, 127..131).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 01:50:58 +02:00
jgrusewski
805a7429f4 sp7(pearl_2): contract change — drop CQL/C51/ENS args (atomic)
Kernel signature reduced from 9 to 6 args; pearl_2_budget_update now
writes only budget_iqn[4] and flatness[4]. Launcher migrated atomically:
arg list shrinks, apply_pearls smoothing loop shrinks from 5 slot-blocks
to 2.

SCRATCH_PEARL_2_C51, SCRATCH_PEARL_2_CQL, SCRATCH_PEARL_2_ENS deleted as
orphan constants per feedback_wire_everything_up. The corresponding
producer scratch slots (111..115, 119..127) become reserved-for-future
inside the buffer.

CQL/C51/ENS budget ownership now lives in the SP7 loss-balance
controller (loaded in T5; wired in T7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:46:06 +02:00
jgrusewski
40871595e8 sp7(trainer): T5 polish — rename cql_dev → cql_sx_dev + base_wiener annotation
Two clarity-only edits surfaced by code review:
- Rename `cql_dev` → `cql_sx_dev` in `launch_loss_balance_controller`.
  The launcher uses `grad_decomp_result_dev_ptr + 6 * f32_size` which
  is the cql_sx (post-budget) slot, not cql (pre-budget at offset 3).
  The variable name now reflects that.
- Add `// 213` inline annotation on `base_wiener_offset` to match the
  pattern used by all four sibling launchers (lines 11062, 11194, 11303).

No semantic impact — pointer arithmetic and parameter order unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:39:31 +02:00
jgrusewski
ab6fbcb668 sp7(trainer): load loss-balance controller kernel + launcher fn
LOSS_BALANCE_CONTROLLER_CUBIN static, kernel slot in GpuDqnTrainer,
6 SCRATCH_LB_* index constants (218..242), pub(crate) launch_loss_balance_controller
that runs the producer + 24 apply_pearls_ad smoothing launches.

SP5_SCRATCH_TOTAL bumped 218→242 with updated docblock and allocation
comment to match the 24 new slots.

Component pointers into grad_decomp_result_pinned: IQN at offset 0,
CQL_SX at offset 6, C51 at offset 9 (3-float layout per
launch_grad_decomp docstring).

Producer-only — call site in training_loop.rs lands at T7 atomically
with the consumer floor change and stale-doc deletion per
feedback_no_partial_refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:33:45 +02:00
jgrusewski
8e4d5cb45b sp7(build): compile loss_balance_controller_kernel cubin
Adds the kernel to the build.rs manifest after pearl_1_ext_num_atoms_kernel
and before the SP5 Layer D producers (matching the chronological SP5/SP7
producer ordering). nvcc-compiled cubin lands under
$OUT_DIR/loss_balance_controller_kernel.cubin and is loaded by
gpu_dqn_trainer.rs in T5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:23:38 +02:00
jgrusewski
21059320e2 sp7(cuda): T3 polish — comment-only clarifications
Three comment-only edits surfaced by code review:
- M1: Annotate `tid >= 8` guard with `// 8 = 2 heads × 4 branches`.
- M2: Header pseudocode `actual_ratio = h_norm / iqn_norm` clarified —
  the implementation correctly omits the inline `fmaxf` because the
  outer guard already enforces `iqn_n >= EPS_DIV`. Comment now states
  the invariant explicitly.
- M3: Header "No atomicAdd" line now cites `feedback_no_atomicadd` for
  consistency with sibling pearl kernels.

Comment-only — zero runtime effect. cargo check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:20:17 +02:00
jgrusewski
63226f17cb sp7(cuda): loss-balance controller kernel (producer-only)
Single-block 8-thread kernel: 2 heads (CQL, C51) × 4 branches. Reads
3-float views into grad_decomp_result_pinned for IQN/CQL_SX/C51, plus
FLATNESS_BASE for the per-branch target modulator. Writes new budgets +
raw Wiener observations (diff² and sample²) to producer scratch.

Per-branch flatness-modulated target ratio (CQL → ANCHOR×(1-flatness),
C51 → ANCHOR×flatness). Wiener-optimal α from per-branch EMA state
slots, clamped to [1e-4, 0.5]. Cold-start seeds at consumer-side
bootstrap (0.02 CQL, 0.05 C51) on first observation; sentinel-aware.

Producer-only commit; build.rs entry, kernel slot, launch site land in
subsequent tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:12:46 +02:00
jgrusewski
3f08c4d142 sp7(state): T2 polish — backfill commit SHA in audit doc T2 bullet
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:08:44 +02:00
jgrusewski
60804788eb sp7(state): T2 polish — descriptions reflect current state vs future T6/T7
Rewrite the three sp5_budget_c51/cql/ens registry descriptions to be
accurate at HEAD aa2854017: replace present-tense "driven by"/"no longer
writes" with future-tense "will be driven by"/"will stop writing", replace
non-existent C51_BOOTSTRAP_BUDGET/CQL_BOOTSTRAP_BUDGET/ENS_BOOTSTRAP_BUDGET
named constants with their actual anonymous .max() literal line references,
and apply the "(Pearl 2 will stop writing...)" parenthetical uniformly to all
three slots (cql was missing it). Audit doc T2 bullet updated accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:08:36 +02:00
jgrusewski
aa28540175 sp7(state): register loss-balance controller state slots + fix stale doc
4 new ResetEntries × 4 slots each cover ISV[297..313). Sentinel 0
triggers Pearl A first-observation bootstrap on fold boundary, matching
the existing Pearl 2/3/4/5/6/8 pattern.

Also: corrected the stale sp5_budget_cql description claiming a
non-existent "regime_stability allocator" (per
feedback_trust_code_not_docs); clarified sp5_budget_c51 / sp5_budget_ens
to reflect SP7 ownership.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:03:21 +02:00
jgrusewski
6e7690d82d sp7(isv): T1 polish — alignment + audit-doc stub softening
Minor item 1: Remove extra column-alignment spaces from the 4 SP7
constants (LB_DIFF_VAR_CQL_BASE, LB_SAMPLE_VAR_CQL_BASE,
LB_DIFF_VAR_C51_BASE, LB_SAMPLE_VAR_C51_BASE) so they match the
no-alignment style of surrounding SP5 constants (BUDGET_C51_BASE etc.).
Constant values (297, 301, 305, 309) and inline comments are unchanged.

Minor item 2: Soften the Fix 31 T7 stub in dqn-wire-up-audit.md from
"sentinel-aware consumer" to "sentinel-aware bootstrap with bootstrap
constants matching the kernel's cold-start basis (defined in T7)"
so the audit entry does not forward-reference specific constant names
before they land.

Cargo check: clean (zero warnings, zero errors).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:58:30 +02:00
jgrusewski
9822b2eea7 sp7(isv): refresh SP5_PRODUCER_COUNT docstring for T1 slot growth
Update stale (118) → 137 unique-slot count and [174..294) = 120 → [174..313) = 139
range in the SP5_PRODUCER_COUNT docstring. Update inline comment unique-slot count
121 → 137 with SP7 T1 breakdown entry. Append SP7 Task 1 history paragraph matching
the established Layer D D3 paragraph style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:55:09 +02:00
jgrusewski
a92abedbae sp7(isv): allocate 16 ISV slots for loss-balance controller state
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) <noreply@anthropic.com>
2026-05-03 00:50:18 +02:00
jgrusewski
bcbd16f8c5 plan(sp7): v2 — fix 5 self-review violations from v1
Critical re-review of v1 caught:

1. Task 6 retained Pearl 2 kernel signature with (void) no-op args
   "to save 3-site cascade" — that's the partial-refactor anti-pattern
   feedback_no_partial_refactor explicitly forbids. v2 does the full
   contract change: signature shrinks 9→6 args, launcher migrates
   atomically.

2. SCRATCH_PEARL_2_C51/_CQL/_ENS would become orphan constants —
   feedback_wire_everything_up says delete or wire. v2 deletes them in
   T6.

3. Audit-doc updates were separated into a single late commit — would
   fail check_audit_doc_updates pre-commit gate on T1-T6. v2 folds Fix
   31 stub into T1 and extends it commit-by-commit.

4. T5 referenced grad_decomp_*_result_dev_ptr field names that don't
   exist — actual layout is one shared 27-float pinned buffer with
   per-component byte offsets (iqn=0, cql_sx=24, c51=36). v2 uses
   pointer arithmetic from grad_decomp_result_dev_ptr.

5. Tasks 1, 2, 4 had "if file shape X then Y, otherwise Z" placeholder
   branches — writing-plans skill forbids these. v2 has concrete patches
   based on reading the actual files.

Also corrected: T2 fixes the stale "regime_stability allocator"
description in state_reset_registry.rs alongside the new entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:35:07 +02:00
jgrusewski
30f7032d12 plan(sp7): implementation plan for loss-balance controller
10 tasks: ISV slots → reset registry → kernel → build manifest → trainer
load+launcher → Pearl 2 retreat → atomic launch+consumer+stale-doc →
audit doc + memory pearl → L40S 5-epoch smoke verify → L40S 50-epoch
full validation.

Each task is producer-only or atomic-pair commits per
feedback_no_partial_refactor. Verification gates (cargo check, cuda
build, smoke acceptance criteria) embedded in each task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:20:33 +02:00
jgrusewski
8f484f3312 spec(sp7): v2 — flatness-gated targets + Wiener α + sentinel-bootstrap
Self-review found 4 violations of feedback_isv_for_adaptive_bounds in v1:
1. TARGET_CQL_OVER_IQN=2.0 hardcoded → flatness-modulated per-branch
   target_ratio = ANCHOR × (1 − flatness[b]). Realizes the original
   pearl_q_flatness_gates_conservatism brainstorm idea.
2. TARGET_C51_OVER_IQN=1.0 hardcoded → mirror direction:
   target_ratio = ANCHOR × flatness[b].
3. ALPHA_META=0.01 fixed → Wiener-optimal α from per-branch state-tracker
   EMAs (16 new ISV slots: diff_var/sample_var × 2 heads × 4 branches).
4. Cascaded floor cleanup: compute_adaptive_budgets line 3384 floors
   reads to 0.02/0.05 always, fighting the controller. Replaced with
   sentinel-aware bootstrap: cold-start uses BOOTSTRAP value, post-write
   takes ISV value verbatim.

Acceptance criteria rewritten to be ISV-relative where the corresponding
signal exists (q-spread vs NOISY_SIGMA, label_scale vs NOISY_SIGMA band).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:11:26 +02:00
jgrusewski
0808ebe3e5 spec(sp7): loss-balance controller — adapt CQL/C51 budgets to grad-norm ratio
Outcome-driven controller replacing Pearl 2's hardcoded-0 CQL budget and
floor-pinned C51 budget with multiplicative ratio adaptation. Targets
`grad_split_bwd cql/iqn = 2.0` and `c51/iqn = 1.0` per slice (trunk, dir,
mag). Slow EMA (α=0.01) consistent with existing controller pearls; Pearl
A sentinel-bootstrap on fold boundary; Pearl D Wiener-optimal smoothing.

Replaces ghost docstring at fused_training.rs:3409 ("0.10×(1−regime)×health"
formula was never implemented). Pearl 2 keeps owning IQN budget (reference
denominator) and FLATNESS_BASE (consumed by NoisyNet σ pearl); stops
writing CQL/C51/ENS slots so the new controller is the sole driver.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:03:14 +02:00
jgrusewski
2fb7d7f57c fix(data): Fix 30 Stale-B — backtest_plan_kernel raw_close from prices_buf (post-MappedF32Buffer migration)
Closes Fix 29 audit row #13 — the last ⚠ Stale row from the Bug-1
contract drift triage. Pre-fix, `backtest_plan_state_isv` extracted
raw_close as `features[bar*feat_dim + 0]`. Post Bug-1 (commit
`5a5dd0fed`) `features[..+0]` is z-normed log-return, NOT raw_close.
The resulting `equity = cash + position*raw_close` and
`unrealized = position*(raw_close - entry_price)` formulas mixed
z-normed-log-return as a dollar price, corrupting val plan_isv
slots [PNL_VS_TARGET] (slot 1) and [PNL_VS_STOP] (slot 2). Other
plan_isv slots (progress, conviction, drift, regime, remaining)
don't depend on raw_close and were correct pre-fix.

Resolution: route raw_close from the upload-once `prices` buffer
(layout `[n*max_len*4]` raw OHLC). Close is at column index 3 — the
same index `backtest_env_kernel.cu` already reads from for portfolio
mark-to-market (line 64 of that kernel; OHLC layout is canonical
across the val backtest path). Reading from `prices` aligns the val
plan_isv path with env_step's source-of-truth, eliminating the
mixed-units pathology end-to-end.

Sites fixed:
  - crates/ml/src/cuda_pipeline/backtest_plan_kernel.cu:74-100
    Kernel signature: `const float* features` and `int feat_dim`
    parameters dropped, replaced by `const float* prices` (the
    [n*max_len*4] raw OHLC buffer). Raw_close read becomes
    `prices[(w*max_len + current_step)*4 + 3]`. Multi-line comment
    block documents the Bug-1 origin and the env_step parity
    reference. The `bool have_close` / `prices != nullptr` guard
    semantics preserved — callers without OHLC data fall back to
    `unrealized = 0` exactly as pre-fix.
  - crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs:~1956
    Single launcher (`evaluate_dqn_graphed` chunk loop, the only
    invocation site of `plan_state_isv_kernel`) updated to pass
    `&self.prices_buf.dev_ptr` instead of `&self.features_buf.dev_ptr`
    and to drop the now-unused `feat_dim_i32` local + `.arg()` call.
    Inline comment explains the Bug-1 origin.
  - docs/dqn-wire-up-audit.md
    Stale-B row appended to Fix 30's table. The standalone Stale-B
    DEFERRED paragraph at the bottom replaced by the commit summary
    + the Fix 30 closure note (all 4 ⚠ Stale and 1  Ambiguous rows
    from Fix 29's deferred follow-ups now resolved).

Migration scope per `feedback_no_partial_refactor`: kernel signature
changed → every consumer migrates in the same commit. Single launcher;
verified via `grep -rn backtest_plan_state_isv` (only the kernel
definition + the gpu_backtest_evaluator launcher + load site appear).

Verification:
  - SQLX_OFFLINE=true cargo check -p ml --offline (43.68s) clean.
  - SQLX_OFFLINE=true cargo build -p ml --release --offline
    --features cuda (1m 30s) clean; cubin recompiled via nvcc.
  - Pre-commit DtoD-via-pinned guard passes (the prereq commit
    `4d966e62f` migrated `gpu_backtest_evaluator.rs`'s buffers to
    MappedF32Buffer, eliminating the 5 `_via_pinned` callers that
    had blocked any prior staging of this file).

Refs Fix 29 row #13. `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 or falls through to
`have_close=false` with `unrealized=0`, identical to pre-fix
smoke-test semantics where prices==NULL),
`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 migration),
`feedback_trust_code_not_docs` (the kernel comment said
"raw_close from features buffer" for months — accurate-when-written
pre-Bug-1, stale-after-Bug-1; verify-against-code disambiguates).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:56:00 +02:00
jgrusewski
4d966e62f4 refactor(cuda): migrate gpu_backtest_evaluator buffers to MappedF32Buffer (Fix 30 Stale-B prereq)
Prerequisite for the deferred Fix 30 Stale-B kernel-side fix
(`backtest_plan_kernel.cu` raw_close source). The DtoD-via-pinned
pre-commit guard (`scripts/pre-commit-hook.sh::check_no_dtod_via_pinned`,
commit `5275932f4`) blocks any commit that stages `gpu_backtest_evaluator.rs`
against the file's 5 pre-existing `clone_to_device_*_via_pinned` callers
that landed before the guard. Stale-B has to stage this file (to thread
`prices_buf` into the `backtest_plan_state_isv` launcher), so the
migration must land first as its own atomic commit per
`feedback_no_partial_refactor`.

Migration scope. Single file, four buffer fields:
  - prices_buf       CudaSlice<f32> → MappedF32Buffer  [n*max_len*4]
  - features_buf     CudaSlice<f32> → MappedF32Buffer  [n*max_len*feat_dim]
  - portfolio_buf    CudaSlice<f32> → MappedF32Buffer  [n*8] (kernel-mutated)
  - window_lens_buf  CudaSlice<i32> → MappedI32Buffer  [n]

Init sites (lines ~651-664 post-edit). 4 `clone_to_device_*_via_pinned`
calls replaced with `MappedF32Buffer::new(host.len())` +
`write_from_slice(host)`. No memcpy_dtod_async + stream.synchronize()
pair at construction — mapped-pinned coherence makes the kernel see
host writes after the next stream-sync barrier.

Reset site (`reset_evaluation_state`, line ~1485 post-edit). In-place
`self.portfolio_buf.write_from_slice(&portfolio_init)` replaces the
prior buffer-replacement
`self.portfolio_buf = clone_to_device_f32_via_pinned(...)`. No alloc
churn per epoch; the device pointer stays stable across resets which
matches MappedF32Buffer's intended use.

Consumer sites (17 kernel arg passes). `arg(&self.X_buf)` →
`arg(&self.X_buf.dev_ptr)` so the launcher receives the device pointer
the kernel expects. Sites: launch_gather (×2), launch_gather_chunk,
launch_env_step, env_batch_kernel chunked path, plan_state_isv kernel,
metrics_kernel. CUdeviceptr (u64) is passed by reference exactly as
metrics_dev_ptr already does at the metrics launch site (~line 2497).

Kernel-mutated MappedF32Buffer precedent. portfolio_buf is mutated by
the env_step kernel every step. The same file's `plan_diag_buf` (a
MappedF32Buffer) is also kernel-written via dev_ptr (lines ~1230-1256)
and host-read via host_ptr — direct precedent. The IQN τ migration
(commit `facbf76eb`) confirms cuMemHostAlloc DEVICEMAP for kernel
reads. The mapped_pinned.rs module docstring states explicitly:
"Kernels write through dev_ptr (with __threadfence_system())".

What this change touches:
  - crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs
    Field types (lines 299-321), init block (lines 641-665), reset
    (lines 1462-1471), 17 kernel arg passes across 6 launchers.
  - docs/dqn-wire-up-audit.md
    Fix 30 Stale-B paragraph extended with the prereq commit summary.
    Stale-B itself remains DEFERRED (kernel-side fix lands next).

Verification:
  - SQLX_OFFLINE=true cargo check -p ml --offline (44.38s) clean.
  - SQLX_OFFLINE=true cargo build -p ml --release --offline
    --features cuda (53.78s) clean.
  - grep -n "clone_to_device_.*_via_pinned\|upload_.*_via_pinned"
    crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs returns
    zero hits.
  - grep -n "self\.\(prices\|features\|window_lens\|portfolio\)_buf"
    shows every kernel-arg site followed by `.dev_ptr`. The only
    non-`.dev_ptr` references are the field declarations, the
    constructor moves into `Self { ... }`, and the
    `write_from_slice` calls in `reset_evaluation_state`.
  - Pre-commit DtoD-via-pinned guard now passes on staging this file.

Eliminates the last 5 `_via_pinned` callers in
`gpu_backtest_evaluator.rs`. Per
`feedback_no_htod_htoh_only_mapped_pinned`, `feedback_no_partial_refactor`
(every consumer of a field-type change migrates in one commit),
`feedback_no_hiding` (no `--no-verify`; the migration IS the fix the
guard is asking for).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:51:04 +02:00
jgrusewski
c4aa71b9df cleanup(cuda): Fix 30 Ambiguous-A — delete orphan phantom_liquidity_gbm
Closes Fix 29 audit row #18. The synthetic-data overlay kernel
`phantom_liquidity_gbm` in `dqn_utility_kernels.cu:1050-1096` was
flagged  Ambiguous because its writer wrote `log_ret` to
`market_features[bar*market_dim + 0]` and `+3` — the WRITER's
downstream consumer contract was unverifiable post Bug 1 (commit
`5a5dd0fed`) which redefined feature index 0 from raw log-return to
z-normed log-return.

Investigation result: zero callers in the entire codebase. The audit
grep `grep -rn "phantom_liquidity_gbm" crates/ml/src/ services/
crates/ bin/` returns ONLY the kernel definition itself — no
`load_function`, no `launch_builder`, no Rust-side launcher. The
kernel has been compiled but never invoked since at least early 2026.

Resolution: delete the kernel definition and replace with an
explanatory comment block. Per `feedback_no_hiding` an orphan kernel
with an ambiguous post-Bug-1 contract is exactly the failure mode the
rule warns against — a future re-wirer would have written
synthetic z-normed log-returns into a slot the production pipeline
expects raw log-returns at, re-introducing the very Bug-1 drift the
audit was triaging. Per `feedback_wire_everything_up` a kernel that
compiles but is unconsumed is either wired in the same commit or
deleted; this kernel was never wired since its writer existed.

The replacement comment block (23 lines) documents the design intent
(GBM-based synthetic-data augmentation) so any future re-introduction
has the spec available, plus the re-introduction conditions:
caller wiring must land in the same commit, and the writer/consumer
contract for `market_features[..+0]` (z-normed vs raw log-return)
must be explicit at the kernel boundary.

What this change touches:
  - crates/ml/src/cuda_pipeline/dqn_utility_kernels.cu:1037-1096
    47-line kernel definition + ══ comment header deleted; 23-line
    DELETED notice replaces it. Net -24 lines.
  - docs/dqn-wire-up-audit.md
    Fix 30 Ambiguous-A row appended with the dead-code-deleted
    resolution. Stale-B deferral note also added (separate paragraph)
    documenting the pre-existing `_via_pinned` guard block requires a
    dedicated `gpu_backtest_evaluator.rs` migration commit before the
    Stale-B kernel-side fix can land.

Verification:
  - SQLX_OFFLINE=true cargo build -p ml --release --offline
    --features cuda (1m 30s) clean; cubin recompiled via nvcc.
  - Pre-commit guards pass.
  - Final grep for `phantom_liquidity_gbm` returns only the comment
    block in the kernel file.

Refs Fix 29 row #18 deferred follow-up. `feedback_no_hiding`
(delete orphans, do not leave them as ambiguous landmines),
`feedback_wire_everything_up` (every module/feature/kernel built
must be wired to a production path or removed),
`feedback_no_functionality_removal` does NOT apply: a kernel with
zero callers is not a functional feature, and the audit doc retains
the design intent so any future re-introduction can rebuild the
mechanism with the correct post-Bug-1 contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:40:00 +02:00