Commit Graph

187 Commits

Author SHA1 Message Date
jgrusewski
2e84fd35d4 feat(sp5): Task A8 — Pearl 1-ext per-branch num_atoms — Layer A complete
Final SP5 Layer A producer. Per-branch C51 num_atoms derived from
per-branch ATOM_V_HALF (Pearl 1, Task A1). Threshold cascade:
  v_half < 0.1  → 64 atoms (narrow Q, high resolution)
  v_half < 1.0  → 32 atoms (moderate)
  v_half ≥ 1.0  → 16 atoms (wide Q, modest resolution)

4 ISV slots [ATOM_NUM_ATOMS_BASE=274..278). Pearls A+D smooths the
discrete output during transitions; Layer B's atoms_update consumer
rounds to nearest valid count.

producer_step_scratch_buf grew 203 → 207. wiener_state_buf already
at 543 (sized at A1 for entire SP5 block).

StateResetRegistry: 1 new FoldReset entry (sp5_atom_num_atoms).

atoms_update consumer migration deferred to Layer B.

LAYER A COMPLETE. 8 producers + 3 auxiliary kernels (q_branch_stats,
grad_cosine_sim, q_skew_kurtosis) populating 110 ISV slots [174..286)
with 4 cross-fold-persistent slots carved out (Kelly).

Refs: SP5 spec 6e6e0fa11 line 1020

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:36 +02:00
jgrusewski
fd1426a398 feat(sp5): Task A7 — Pearl 8 per-direction trail-stop distance
Per-direction trail-stop distance derived from the current bar's ATR.
Short[270] and Long[272] receive 2× denormalized ATR (industry-standard
2×ATR trail); Hold[271] and Flat[273] receive EPS_CLAMP_FLOOR=1.0
(no trail-stop fires for those directions; floor prevents zero).

4 ISV slots [TRAIL_DIST_PER_DIR_BASE=270..274). ATR is read from
features[bar_idx × market_dim + 9] (ATR_NORM column) and denormalized
via the canonical fxcache scheme:
  atr_abs = max(0.01, exp(atr_norm × 16 − 7))
Constants 16, 7, 0.01 are Invariant 1 anchors from the existing
fxcache normalization in experience_kernels.cu:2701.

Layer A simplification: Short and Long share the same current-bar ATR
value. The spec implied direction-conditional ATR via per-trade-event
aggregation, but that requires infrastructure not yet in place. The
4-slot ISV layout preserves the consumer contract so a future Layer C
extension can populate truly direction-specific values without
breaking Layer B's check_trailing_stop reads.

producer_step_scratch_buf grew 199 → 203 (1 new constant
SCRATCH_PEARL_8_TRAIL=199). wiener_state_buf stays at 543 (sized at
A1 for entire SP5 block).

StateResetRegistry: 1 new FoldReset entry (sp5_trail_dist_per_dir).
Pearl A sentinel 0 → bootstrap on fold boundary's first launch.

check_trailing_stop consumer migration deferred to Layer B.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 23:59:22 +02:00
jgrusewski
f40ccc16a7 fix(sp5): Task A6 — close two minor review findings
Combined spec/quality review caught two minor issues in the Pearl 6
commit. Both are mechanical fixes; no behavior change.

1. Test 12 (pearl_6_kelly_within_fold_ewma_blend) was missing an
   explicit assertion for slot 282 (TRADE_VAR_SMOOTH_IDX). The test
   setup initialized tvar_i32 and the launcher passed it through the
   kernel's parameter slot, but no assert! ever fired against the
   resulting ISV value. With n_envs=1, the kernel's
   `(kelly_count > 1) ? variance : 0.0f` branch returns 0 (no
   cross-env variance possible with 1 env), so EWMA blend yields
   0.99 × 0.5 + 0.01 × 0.0 = 0.495. Added the missing assertion to
   close the within-fold coverage gap for s==2.

2. pearl_6_kelly_kernel.cu:136 doc comment said the slot computes
   "standard deviation of per-env Kelly fractions" but the code
   actually computes `ksum_sq / kelly_count` — i.e. the variance
   (second moment), not the standard deviation. The slot name
   TRADE_VAR_SMOOTH_INDEX correctly indicates variance; the comment
   was wrong. Updated comment to match: 'variance of per-env Kelly
   fractions' with explicit note that this is the second moment,
   NOT std-dev (no sqrtf applied).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:31:37 +02:00
jgrusewski
5b4cdec4ff feat(sp5): Task A6 — Pearl 6 cross-fold-persistent Kelly cap signals
Adds `pearl_6_kelly_kernel.cu` (single-block 6-thread kernel reading
portfolio_state directly) to populate ISV[280..286) with Bayesian-prior
Kelly fraction, conviction identity, trade variance, cumulative sample
count (max-semantics), win-rate and loss-rate EMAs. EWMA α=0.01 is an
Invariant 1 structural anchor for cross-fold inertia.

Key design departure from A1-A5: ISV[280..286) are intentionally EXEMPT
from the StateResetRegistry and from apply_pearls/Pearls A+D. portfolio_state
has WindowReset lifecycle (resets at EVERY window boundary, not just fold),
making cross-fold persistence essential. The max()-semantics for sample_count
(s==3) ensure the cumulative count never decreases across any boundary.
Wiener offsets [525..543) that naive formula would produce are intentionally
unused; in-kernel EWMA replaces external wiener bootstrap.

Verification: cargo check -p ml --offline clean (11 pre-existing warnings,
no new). sp5_producer_unit_tests --no-run clean. Two GPU tests (12, 13)
validate EWMA blend and cross-fold persistence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 23:22:06 +02:00
jgrusewski
b1ef312a40 feat(sp5): Task A5 — Pearl 5 per-branch IQN τ schedule GPU producer (Layer A)
Two new CUDA kernels land as SP5 Layer A additive producers feeding ISV[250..270)
with per-branch IQN quantile-τ schedules derived from Q-distribution skew.

q_skew_kurtosis_update: single-block 4-thread, reads save_q_online[B×13],
two-pass central moments → skew clamped [-3,+3] + ex_kurt clamped [-3,+30];
writes scratch[171..179). EPS_DIV=1e-12 (Invariant 1 anchor). No atomicAdd.

pearl_5_iqn_tau_update: single-block 4-thread, shifts symmetric default τ
{0.05,0.25,0.5,0.75,0.95} by skew×SKEW_SHIFT=0.05, clamps to [0.01,0.99];
writes scratch[179..199). SKEW_SHIFT and envelope are Invariant 1 anchors.

launch_sp5_pearl_5_iqn_tau fires both kernels + 20 apply_pearls_ad calls
(ALPHA_META=1e-3) → ISV[IQN_TAU_BASE=250..270). SP5_SCRATCH_TOTAL 171→199.

StateResetRegistry +1 FoldReset entry (sp5_iqn_tau, ISV[250..270)).
training_loop.rs wired after Pearl 4 with tracing::warn on error.
Two GPU-only unit tests (10+11): zero-skew symmetric default + left-skew
floor clamp. Module docstring updated A1-A5.

No consumer migration — Layer A additive only per spec.
cargo check -p ml --offline clean (11 pre-existing warnings, none new).
cargo test --no-run clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 22:50:06 +02:00
jgrusewski
d4d12abab2 docs(sp5): close two minor review findings before A5
Two minor code-review nits accumulated across A1-A4 reviews; closing
them in a single docs/comment-only commit before Layer A continues.

1. tests/sp5_producer_unit_tests.rs module docstring (A4 review):
   the file header still claimed it covered only A1's two kernels
   even though A2/A3/A4 had each appended their tests. Updated header
   to enumerate all 9 tests across A1-A4 (+ 1 grad_cosine_sim test
   landed in 4da6b34d7) so a reader cold to the file can locate each
   producer's coverage without scanning the body.

2. gpu_dqn_trainer.rs:2854 stale field comment `[B, TOTAL_ACTIONS(11)]`
   (A1 review): comment was left at 11 since the pre-4-direction
   layout but the actual slot count is 13 (4 dir + 3 mag + 3 ord + 3
   urg). Updated to `[B, TOTAL_ACTIONS(13)]` with the per-branch
   breakdown inline so the field declaration matches the SP5 producer
   docstrings (e.g. q_branch_stats_kernel comments at line 277, 282).

Comment-only changes; no behavior change. cargo check + cargo test
--no-run both clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:35:18 +02:00
jgrusewski
4da6b34d78 test(sp5): Task A4 — add grad_cosine_sim_update unit test
Code-quality review caught that pearl_4_adam_hparams_kernel had direct
GPU tests but the auxiliary grad_cosine_sim_kernel had none. Tests 7
and 8 launched the hparams kernel with pre-baked cosine inputs,
never exercising the per-group reduction (dot + 2× L2 norm sums) or
the writeback (grad_curr → grad_prev for next step's comparison).
The writeback is load-bearing for cross-step cosine evolution — if
broken, every subsequent step's cosine_sim is computed against a stale
or mis-aligned previous gradient.

Adds Test 9 `grad_cosine_sim_per_group_dot_norm_and_writeback` with
8 analytically-known synthetic group cases:
  group 0: curr=prev=e1               → cos=+1, |c|=1
  group 1: curr=e1, prev=e2           → cos= 0 (orthogonal)
  group 2: curr=e1, prev=-e1          → cos=-1 (antiparallel; raw)
  group 3: curr=prev=(3,4,0,0)        → cos=+1, |c|=5
  group 4: curr=0, prev=e1            → cos= 0, |c|=0 (cold-start curr)
  group 5: curr=e1, prev=0            → cos= 0, |c|=1 (Pearl A sentinel)
  group 6,7: same as group 0

NO CPU oracle — expected values are unit-vector cosines from the
kernel formula. Writeback verified by reading grad_prev_buf after
the kernel runs and asserting bit-identity with grad_curr_buf for
all 32 elements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:30:02 +02:00
jgrusewski
67dd414cb3 feat(sp5): Task A4 — Pearl 4 per-group Adam β1/β2/ε
Per-param-group Adam hyperparameters from per-group gradient
direction-stability EMA + L2 norm. 8 SP4 param groups × 3 hparams = 24
ISV slots [226..250).

Two-kernel chain:
  grad_cosine_sim_update — per-group cosine_sim of curr vs prev grads
                          + L2 norm; writes back grad_curr → grad_prev
                          for next step's comparison.
  pearl_4_adam_hparams_update — β1/β2/ε from cosine + l2_norm.

Structural envelopes (Invariant 1 anchors per spec line 88):
  β1 ∈ [0.85, 0.95]; β2 ∈ [0.99, 0.9995]; ε ∈ [1e-10, 1e-6]

ALPHA_META migration (Option A — atomic shared-contract migration per
feedback_no_partial_refactor): apply_pearls_ad_kernel.cu and the
launch_apply_pearls Rust wrapper now take alpha_meta as a parameter.
All 45 existing call sites (SP4 + SP5 producers) pass the prior default
1.0e-3 explicitly via crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META.
Pearl 4's apply_pearls calls pass 5.0e-4 (half the default per spec
line 89) to limit β change rate.

Theoretical caveat: adaptive β breaks Adam's constant-β convergence
proof. Mitigations: structural envelopes + halved ALPHA_META + Pearls
A+D smoothing. Fall-back path defined: revert + ε-only adaptive
variant if Layer C destabilization observed.

New mapped-pinned buffer: grad_prev_buf_per_group [TOTAL_PARAMS] for
cosine-sim previous-step direction storage. Mapped-pinned i32 mirror
of grad_buf layout.

producer_step_scratch_buf grew 131 → 171 (40 new outputs: 8 cosine_sim
+ 8 l2_norm + 24 β1/β2/ε). wiener_state_buf already at 543 (A1 sized
for entire SP5 block).

StateResetRegistry: 4 new FoldReset entries: sp5_adam_beta1,
sp5_adam_beta2, sp5_adam_eps, sp5_grad_prev_buf. All sentinel 0 →
bootstrap to envelope midpoint via Pearls A+D + cosine_sim=0 fall-back
on first step of new fold.

Adam-launcher consumer migration deferred to Layer B.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 22:18:10 +02:00
jgrusewski
71a0275f54 test(sp5): Task A3 — assert budget sum-to-1 invariant
Code-quality review caught that the two Pearl 2 unit tests verified each
output (c51, iqn, cql, ens) against its analytical expected value within
1% relative tolerance, but did NOT assert the structural invariant
`c51 + iqn + cql + ens ≈ 1.0` that the kernel maintains by construction
(`ens = max(0, 1 - iqn - c51 - cql)`).

A coefficient typo (e.g. BASE_IQN=0.111 instead of 0.11) would produce
individually-plausible per-component values that all still pass the
relative checks while quietly summing to 0.97 or 1.04. The sum check
catches that class of regression.

Adds `assert!((c51+iqn+cql+ens - 1.0).abs() < 1e-4)` inside both
per-branch loops in the flat-regime and sharp-regime tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:46:40 +02:00
jgrusewski
4b2093a627 feat(sp5): Task A3 — Pearl 2 per-branch loss budget
Per-branch C51/IQN/CQL/Ens loss budgets driven by per-branch flatness =
var(Q[b]) / (σ[b]² + EPS_DIV). IQN dominates when flat, C51 yields
proportionally. CQL stays gated by existing regime_stability allocator.

Reads Q_VAR_PER_BRANCH (222..226 from Pearl 1's q_branch_stats) AND
NOISY_SIGMA (210..214 from Pearl 3 — must run AFTER both).

20 ISV slots (BUDGET_C51[190..194), BUDGET_IQN[194..198),
BUDGET_CQL[198..202), BUDGET_ENS[202..206), FLATNESS[206..210)).
StateResetRegistry: 5 new FoldReset entries.

producer_step_scratch_buf grew 111 → 131 (20 new outputs).
wiener_state_buf already at 543 (A1 sized for entire SP5 block).

Loss budget consumer migration deferred to Layer B.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 21:38:43 +02:00
jgrusewski
ab3e17f4a2 feat(sp5): Task A2 — Pearl 3 per-branch NoisyNet sigma
Per-branch sigma scales with per-branch Q magnitude (v_half from Pearl 1's
ATOM_V_HALF, populated in A1). SIGMA_FRACTION adapts via entropy-deficit
controller targeting 70% of max action entropy (target_entropy =
log(n_actions[b]) * 0.7).

8 ISV slots (NOISY_SIGMA[210..214), SIGMA_FRACTION[214..218)). Reuses
BRANCH_ENTROPY (218..222) from Task A1's q_branch_stats_kernel.

producer_step_scratch_buf grew 103 -> 111 (8 new outputs). wiener_state_buf
already at 543 (A1 sized for entire SP5 block).

NoisyLinear consumer migration deferred to Layer B.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 21:16:11 +02:00
jgrusewski
3b4ce05465 fix(sp5): Task A1 — pearl_1 clip_rate denominator must be per-branch
Code-quality review caught: pearl_1_atom_kernel.cu computed
`clip_rate = atoms_clip_count[b] / (batch_size × 13)` using the
total-actions=13 sum across all branches. The headroom controller's
TARGET_CLIP_RATE=0.01 is calibrated against the per-branch clip event
rate. Using a denominator 3.25–4.3× larger than the per-branch
denominator made the controller systematically under-responsive — at
actual 1% per-branch clipping it would read ~0.23–0.31% and contract
headroom toward the 2.0 floor instead of holding the target.

Currently masked: `atoms_clip_count` is zero in Layer A (Layer B's
atoms_update_kernel migration is what populates it). Without this fix,
Layer B would inherit a silently-wrong formula that converges to the
floor rather than the target rate.

Fix: plumb `action_counts[4]` ({4, 3, 3, 3}) through the kernel
signature and use `batch_size × action_counts[b]` as the per-branch
denominator. Matches q_branch_stats_kernel's existing parameter pattern.

Test #2 launcher updated to allocate action_counts_buf and pass its
dev_ptr through the new kernel parameter slot. Audit doc entry added
per Invariant 7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:04:12 +02:00
jgrusewski
db0b603127 fix(sp5): Task A1 — MappedF32Buffer.len is a public field, not a method
Test sp5_producer_unit_tests.rs:212 called `scratch_buf.len()` but `len`
is declared as `pub len: usize` on MappedF32Buffer (mapped_pinned.rs:210),
not a method. `cargo test --no-run` failed with E0599; library + cubin
build was unaffected.

One-character fix: `scratch_buf.len()` → `scratch_buf.len`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:55:18 +02:00
jgrusewski
0b0f3b21e1 feat(sp5): Task A1 — Pearl 1 atom-span + Q-stats GPU producers (Layer A)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:46:50 +02:00
jgrusewski
88ae74ca73 fix(sp4): #260 — eliminate SP1-era 1e6 × isv multipliers via ISV-driven producers
Two SP1-Phase-C cuBLAS backward sanitisers at gpu_dqn_trainer.rs:7615
(bw_d_h_s2 input) and :20679 (d_value_logits + d_adv_logits) used
hardcoded `1e6 × signal.max(1.0)` formulas — outside SP4 mechanisms
1/2/5/6/9/10 but flagged by feedback_isv_for_adaptive_bounds review as
the last remaining hardcoded multipliers in the cuBLAS-backward
sanitiser consumer path after Layer A/B closed out the rest of SP4.

Migrated to the proper SP4 pattern:
  - 2 new SP4 ISV slots [171, 172) extending [131..171) → [131..173):
    BW_D_H_S2_BOUND_INDEX=171 (single-buffer p99) and
    Q_DIR_GRAD_BOUND_INDEX=172 (multi-sub-buffer p99 over
    d_value_logits ∪ d_adv_logits, n_sub=2).
  - 2 new producer kernels:
    bw_d_h_s2_p99_kernel.cu (mirrors h_s2_p99 single-buffer pattern)
    q_dir_grad_p99_kernel.cu (mirrors param_group_oracle multi-sub
    convention via shared sp4_histogram_p99_multi<256> template;
    reuses oracle_subbuf_table_buf + oracle_subbuf_counts_buf).
  - Pearls A+D wired via existing apply_pearls_ad_kernel chained on
    same stream (GPU-only, graph-capture-compatible).
  - Consumers read ISV[slot].max(EPS_CLAMP_FLOOR) directly.

Buffer growth (single source of truth in gpu_dqn_trainer.rs):
  SP4_PRODUCER_COUNT 69 → 71, SP4_WIENER_TOTAL_FLOATS 207 → 213.
  ISV_TOTAL_DIM 171 → 173. LAYOUT_FINGERPRINT_SEED extended (existing
  checkpoints will fail-fast at load — re-train required).

StateResetRegistry: 2 new FoldReset entries (sp4_bw_d_h_s2_bound,
sp4_q_dir_grad_bound) + 2 matching reset_named_state dispatch arms;
both halves of Pearl A's sentinel contract reset together per
feedback_no_partial_refactor.

Unit tests: 2 new GPU-gated tests in sp4_producer_unit_tests.rs
exercising (single-buffer Pearl-A→Pearl-D convergence, multi-sub-
buffer p99 vs analytical |N(0,1)|). All 16 GPU tests pass on local
RTX 3050 Ti (rel-err 0.00000 / 0.00526).

Behaviour change: bound is now Pearls-smoothed p99 of the actual
gradient distribution, tighter than the pre-existing 1e6 ceiling but
appropriate-scale for observed values. Smoke validation must verify
F0/F1/F2 still converge.

Refs: task #260, baseline commit 1c150d190 (Layer A + B + fix-up +
L40S smoke pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:41:29 +02:00
jgrusewski
1389d1c810 refactor(sp4): GPU-only Pearls A+D — eliminate all host-side compute paths
Layer A L40S smoke smoke-test-v9kjv revealed CUDA Graph capture failure
at aux_label_scale_ema mid-step host sync inside aux_heads_forward Step 2b
(CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED). Per feedback_no_cpu_compute_strict
and pearl_cold_path_no_exception_to_gpu_drives: CPU compute path is
strictly forbidden — any formula (EMA / reduction / Pearls A+D / adaptive α)
belongs on GPU regardless of frequency.

This commit migrates ALL 11 Pearls A+D applications + 1 inline application
to a single GPU apply_pearls_ad_kernel:
  - 5 SP4 producers (target_q, atom_pos×4, param_group_oracle, grad_norm, h_s2)
  - 6 A13 retrofits (h_s2_rms, aux_heads_loss, moe_expert_util,
                     vsn_mask, iqn_quantile, reward_component)
  - 1 inline aux_label_scale block in aux_heads_forward Step 2b

Eliminates: stream.synchronize() + host_ptr read_volatile + host arithmetic
            + host_ptr write_volatile pattern from all 11 launchers + 1 inline.

apply_pearls_to_slot host helper deleted. pearls_ad_update kept as
test-only reference implementation (also retained for the post-Adam
pearl_c_post_adam_engagement_check host-side diagnostic which reduces
mapped-pinned i32 counters outside any captured graph). New GPU unit
test asserts kernel output matches reference within fp32 ULP for 5
representative cases plus a multi-slot batch sanity check.

The refactor is graph-capture-compatible by construction: the kernel
runs single-thread in the same stream as the producer kernel; no host
synchronisation needed between producer and applicator.

Build clean (cargo check --workspace), 6 host sp4_wiener_ema tests pass,
13 SP4 GPU tests + 1 new oracle test pass on RTX 3050 Ti. L40S smoke
re-validation deferred to A17 redo.

Refs: smoke-test-v9kjv graph-capture failure (terminated), commit 4c231fa81.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:57:57 +02:00
jgrusewski
4c231fa812 refactor(sp4): A13 code-quality pass — DRY helper, named constant, doc fix, param cleanup
Addresses 5 IMPORTANT items from A13 code-quality review:

1. apply_pearls_to_slot helper extracted into sp4_wiener_ema.rs.
   Collapses ~30 lines of read_volatile / pearls_ad_update / write_volatile
   per-slot block into a single unsafe fn. 12 launchers now consume the
   helper (5 SP4 producers A5-A9, 6 A13 retrofits A13.0-A13.5, plus the
   inline label-scale block in aux_heads_forward Step 2b — including 2
   apply_pearls closures inside multi-slot launchers and the cross-boundary
   GpuExperienceCollector consumer). Pearl C rate_deficit site untouched
   (different mapped-pinned buffer + Rust ema array, doesn't share the
   helper's pointer-based contract).

2. REWARD_COMPONENT_COUNT named constant added next to
   REWARD_POPART_EMA_INDEX in gpu_dqn_trainer.rs. Replaces 3 hardcoded `6`
   literals across collector launcher (block_dim, Pearls A+D loop) and
   training_loop fold-reset range arithmetic. Mirrors MOE_NUM_EXPERTS /
   SL_NUM_FEATURE_GROUPS invariant-guard pattern with debug_assert_eq! in
   the collector launcher. Kernel literal `6` retained (allows nvcc full
   unroll); kernel comment now documents the host-side invariant.

3. SP4_PRODUCER_COUNT, SP4_WIENER_FLOATS_PER_SLOT, SP4_WIENER_TOTAL_FLOATS
   promoted from fn-local consts inside `pub fn new` to module-level
   `pub const`s in gpu_dqn_trainer.rs, re-exported via cuda_pipeline::mod.
   All 13 redeclarations in tests/sp4_producer_unit_tests.rs replaced
   with single `use ml::cuda_pipeline::SP4_PRODUCER_COUNT;` import. Future
   buffer growth requires single-file edit.

4. _ema_alpha_unused: f32 caller-compat shim removed from 6 retrofitted
   launchers (launch_h_s2_rms_ema, launch_aux_heads_loss_ema,
   launch_vsn_mask_ema, launch_moe_expert_util_ema, launch_iqn_quantile_ema,
   launch_reward_component_ema_inplace) and the FusedTrainingCtx proxy.
   All callers in training_loop.rs + fused_training.rs updated to drop
   the unused argument per feedback_no_legacy_aliases (no soft-deprecated
   wrappers; rename all call sites directly). Doc-comments updated from
   "α dropped per SP4 — argument preserved so callers compile unchanged"
   to "α derived adaptively from per-slot Pearls A+D Wiener state — see
   sp4_wiener_ema::pearls_ad_update".

5. Stale doc reference fixed at gpu_aux_heads.rs:391 — comment referenced
   non-existent launch_label_scale_ema_with_pearls function. Now correctly
   points at the inline Pearls A+D block in GpuDqnTrainer::aux_heads_forward
   Step 2b (which now consumes apply_pearls_to_slot).

Pure refactor — no spec or behaviour change. Same kernel launches, same
Pearls A+D semantics, same ISV slot writes.

cargo check -p ml --offline clean (12 pre-existing warnings, no new);
cargo test -p ml --lib --offline sp4_wiener_ema 7/7 passing
(6 originals + apply_pearls_to_slot_pearl_a_bootstrap_path);
cargo test -p ml --lib --offline state_reset_registry 3/3 passing.

Refs: A13 review (commits aada419de..c5add566d).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 08:42:40 +02:00
jgrusewski
4f82b74a51 feat(sp4): Task A13.5 — retrofit reward_component_ema with Pearls A+D + cross-boundary wiring + orphan deletion
Replaces the kernel's hardcoded `ema_alpha` with the shared `pearls_ad_update`
host-side helper, wires the host-side update across the trainer/collector
boundary (mirrors the A14/A15 Pearl C wiring path), and deletes the
trainer's orphan `launch_reward_component_ema` per
`feedback_wire_everything_up.md`.

Kernel + collector launcher:
  - Kernel `reward_component_ema` signature: drops `(isv_out, ema_alpha,
    isv_reward_base_slot)` for `(scratch_buf, scratch_first_index=63)`.
    Single-block 6-thread (one per component); each writes mean|r_c| to
    `scratch_buf[scratch_first_index + c]` with `__threadfence_system()`.
  - 6 ISV slots wired with Pearls A+D: ISV[63..69) (Wiener offsets
    189..207 — last slots in the post-A13 207-float wiener_state_buf).
  - `GpuExperienceCollector::launch_reward_component_ema_inplace` now:
    launches kernel → syncs stream → applies Pearls A+D in 6-iteration
    loop → memsets reward_components_per_sample to zero (preserves
    original behaviour). Degenerate-zero short-circuit per slot covers
    the always-zero placeholder components (c=2 trail, c=5 bonus) plus
    cold-start.

Cross-boundary wiring (mirrors A14/A15 precedent):
  - 4 new fields on `GpuExperienceCollector`:
    `reward_component_pearls_{wiener_host_ptr, scratch_dev_ptr,
    scratch_host_ptr, isv_pinned_ptr}` — all NULL/0 until wired.
  - New setter `set_reward_component_pearls_buffers(...)` on collector.
  - New accessors on `GpuDqnTrainer`: `wiener_state_buf_host_ptr()`,
    `producer_step_scratch_buf_dev_ptr()`,
    `producer_step_scratch_buf_host_ptr()`, `isv_signals_pinned_ptr()`.
  - New wire helper `FusedTrainingCtx::wire_reward_component_pearls_buffers`
    pulls all 4 pointers from trainer, pushes into collector.
  - Wired once in `training_loop.rs::init_gpu_experience_collector`
    immediately after `set_curiosity_pearl_c_buffers`.

Orphan deletion (per `feedback_wire_everything_up.md`):
  - Removed `GpuDqnTrainer::launch_reward_component_ema` (zero call sites
    pre-deletion).
  - Removed trainer-side `reward_component_ema_kernel: CudaFunction`
    field (zero consumers post-launcher-deletion).
  - Removed cubin loader + struct-init line.
  - `REWARD_COMPONENT_EMA_CUBIN` static remains because the collector
    still loads from it.

Tests:
  - `sp4_reward_component_ema_writes_step_obs_via_pearl_a_then_converges_pearl_d`:
    drives kernel with N=128 reward_components where r[i*6+c] = sign(i) ×
    (c+1), asserts each slot ∈ ±1e-5 of (c+1), non-target slots remain 0,
    Pearl A bootstrap + Pearl D convergence verified.
  - The cross-boundary wiring path exercised in production by integration
    smoke harness; this kernel-direct test isolates kernel signature +
    Pearls A+D semantics.
  - `cargo test -p ml --lib sp4_wiener_ema --offline` 6/6 passing.

Per `feedback_no_atomicadd.md`,
`feedback_no_htod_htoh_only_mapped_pinned.md`,
`feedback_no_partial_refactor.md`, `feedback_wire_everything_up.md`.
Build: `cargo check -p ml --lib --tests --offline` clean (11 pre-existing
warnings, no new warnings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:27:50 +02:00
jgrusewski
5f800fe5c8 feat(sp4): Task A13.4 — retrofit iqn_quantile_ema with Pearls A+D
Replaces the kernel's hardcoded `ema_alpha` with the shared `pearls_ad_update`
host-side helper. 4 ISV slots retrofit (off-median IQN quantiles).

  - Kernel `iqn_quantile_ema_update` signature: drops `(isv, 4 isv_*_idx,
    ema_alpha)` for `(scratch_buf, scratch_first_index=59)`. Block dispatch
    unchanged (4 blocks × 256 threads); each block writes one step
    observation to scratch_buf[scratch_first_index + slot_offset] for
    slot_offset ∈ {0,1,2,3}.
  - 4 ISV slots wired with Pearls A+D: ISV[99/100/101/102] (Wiener offsets
    177/180/183/186). Median tau_idx=2 intentionally skipped (already
    surfaced via greedy-Q).
  - Wrapper `GpuDqnTrainer::launch_iqn_quantile_ema(..., _ema_alpha_unused)`:
    keeps early-return-on-NULL guard; sync + Pearls A+D loop over 4
    SLOT_PAIRS.

Behavior: stationary signals converge to the same value at adaptive rate.
The 4 slots remain diagnostic-only (HEALTH_DIAG / risk-monitoring surface).

Tests: `sp4_iqn_quantile_ema_writes_step_obs_via_pearl_a_then_converges_pearl_d`
drives kernel with B=32 Q=5 TBA=12 controlled q surface where
Q[a, b*Q+tau_idx] = (tau_idx+1)*0.7. Asserts each slot ∈ ±1e-5 of
expected, median absent, non-target slots remain 0, Pearl A bootstrap +
Pearl D convergence verified.

Per `feedback_no_atomicadd.md`,
`feedback_no_htod_htoh_only_mapped_pinned.md`. Build: `cargo check -p ml
--lib --tests --offline` clean (11 pre-existing warnings, no new warnings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:18:55 +02:00
jgrusewski
0e9d69787d feat(sp4): Task A13.3 — retrofit vsn_mask_ema with Pearls A+D
Replaces the kernel's hardcoded `ema_alpha` with the shared `pearls_ad_update`
host-side helper. 6 ISV slots retrofit (one per VSN feature group).

  - Kernel `vsn_mask_ema_update` signature: drops `(isv, isv_first_index,
    ema_alpha)` for `(scratch_buf, scratch_first_index=53)`. Per-group mean
    writes to `scratch_buf[53..59)`. Single `__threadfence_system()` after
    all 6 groups write.
  - 6 ISV slots wired with Pearls A+D: ISV[105..111) (Wiener offsets
    159..177). Wiener offset for group g: (53+g)*3.
  - Wrapper `GpuDqnTrainer::launch_vsn_mask_ema(_ema_alpha_unused)`: sync
    + Pearls A+D loop over the 6 groups.
  - `debug_assert_eq!(SL_NUM_FEATURE_GROUPS, 6)` guards against group-count
    changes silently breaking the contiguous scratch layout.

Behavior: stationary signals converge to the same value at adaptive rate.
The 6 slots stay semantically identical (per-group mean of VSN softmax
mask); HEALTH_DIAG mirror + VSN focus monitor consume them unchanged.

Tests: `sp4_vsn_mask_ema_writes_step_obs_via_pearl_a_then_converges_pearl_d`
drives kernel with B=128 num_groups=6 mask `mask[b,g]=(g+1)/21` (rows sum
to 1, per-group mean = (g+1)/21). Asserts each slot ∈ ±1e-5, non-target
slots remain 0; verifies Pearl A bootstrap + Pearl D convergence.

Per `feedback_no_atomicadd.md`,
`feedback_no_htod_htoh_only_mapped_pinned.md`. Build: `cargo check -p ml
--lib --tests --offline` clean (11 pre-existing warnings, no new warnings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:15:31 +02:00
jgrusewski
04fcbd27cf feat(sp4): Task A13.2 — retrofit moe_expert_util_ema with Pearls A+D
Replaces the kernel's hardcoded `alpha` with the shared `pearls_ad_update`
host-side helper. 9 ISV slots retrofit: 8 per-expert util + 1 gate entropy.

  - Kernel `moe_expert_util_ema_update` signature: drops `(isv,
    isv_util_base, isv_entropy_index, alpha)` for `(scratch_buf,
    scratch_idx_util_base=44, scratch_idx_entropy=52)`.
  - Single-block single-thread; computes per-expert col_mean in a single
    forward pass (collapses prior dual-pass), writes each to scratch slots
    [44..52) and Shannon entropy to slot 52.
  - 9 ISV slots wired with Pearls A+D: ISV[118..126) (per-expert util,
    Wiener offsets 132..156) + ISV[126] (gate entropy, Wiener offset 156).
  - Launcher `gpu_moe_head.rs::launch_expert_util_ema`: drops alpha + isv
    args; takes scratch_dev_ptr + 2 scratch_idx args.
  - Wrapper `GpuDqnTrainer::launch_moe_expert_util_ema(_ema_alpha_unused)`:
    sync + Pearls A+D loop over 9 slots via `apply_pearls(scratch_idx,
    isv_idx)` closure.
  - `debug_assert_eq!(MOE_NUM_EXPERTS, 8)` guards against K changes
    silently breaking the contiguous scratch layout.

Behavior: stationary signals converge to the same value at adaptive rate.
The 9 slots stay semantically identical (per-expert col_mean, gate
entropy); HEALTH_DIAG mirror + the adaptive λ controller
`moe_lambda_eff_update` (which reads ISV[MOE_GATE_ENTROPY_EMA_INDEX])
continue to consume them unchanged.

Tests: `sp4_moe_expert_util_ema_writes_step_obs_via_pearl_a_then_converges_pearl_d`
drives kernel with B=128 K=8 perfect-uniform gate → analytical col_mean=1/8,
entropy=ln(8)≈2.0794. Asserts slot values ∈ ±1e-6/1e-5, non-target slots
remain 0; verifies Pearl A bootstrap + Pearl D convergence.

Per `feedback_no_atomicadd.md`,
`feedback_no_htod_htoh_only_mapped_pinned.md`. Build: `cargo check -p ml
--lib --tests --offline` clean (11 pre-existing warnings, no new warnings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:12:22 +02:00
jgrusewski
74ed2f5008 feat(sp4): Task A13.1 — retrofit aux_heads_loss + aux_label_scale with Pearls A+D
Both kernels in `aux_heads_loss_ema_kernel.cu` retrofit in the same commit
per `feedback_no_partial_refactor.md` (single shared cubin, single producer
family).

  - Kernel `aux_heads_loss_ema_update`: writes nb_loss/rg_loss scalars to
    `producer_step_scratch_buf[41..43)` (slots 41=next-bar, 42=regime).
  - Kernel `aux_label_scale_ema_update`: writes mean(|label|) to
    `producer_step_scratch_buf[43]`.
  - 3 ISV slots wired with Pearls A+D: ISV[113] (Wiener 123..126),
    ISV[114] (Wiener 126..129), ISV[117] (Wiener 129..132).
  - Launcher `AuxHeadsForwardOps::launch_loss_ema`: drops isv_dev_ptr +
    isv_*_index + ema_alpha; takes scratch_dev_ptr + 2 scratch_idx args.
  - Launcher `AuxHeadsForwardOps::launch_label_scale_ema`: same retrofit
    pattern with a single scratch_idx arg.
  - Wrapper `GpuDqnTrainer::launch_aux_heads_loss_ema(_ema_alpha_unused)`:
    sync + Pearls A+D loop over both slots.
  - `aux_heads_forward` mid-step launch (Step 2b — runs BEFORE
    next_bar_loss_reduce + backward consume ISV[117]) gains inline sync +
    Pearls A+D update so consumers see the up-to-date scale this step.

Behavior: stationary signals converge to the same value at adaptive rate
(Pearl D's α* derived from per-slot signal-vs-noise variance);
non-stationary signals respond Wiener-optimally faster. Slots stay
semantically identical (next-bar MSE, regime CE, label-scale mean_abs);
only the EMA blending logic changes.

Tests:
  - `sp4_aux_heads_loss_ema_writes_step_obs_via_pearl_a_then_converges_pearl_d`:
    nb_loss=0.5, rg_loss=1.2 stationary, both slots converge within 1%
    after 1000 observations.
  - `sp4_aux_label_scale_ema_writes_step_obs_via_pearl_a_then_converges_pearl_d`:
    B=256 mixed-sign ±3.0 labels (mean_abs=3.0), step_obs ∈ ±1e-4 of
    analytical mean, Pearl A bootstrap + Pearl D convergence verified.

Per `feedback_no_atomicadd.md`,
`feedback_no_htod_htoh_only_mapped_pinned.md`,
`feedback_no_partial_refactor.md`. Build: `cargo check -p ml --lib --tests
--offline` clean (11 pre-existing warnings, no new warnings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:08:55 +02:00
jgrusewski
aada419de3 feat(sp4): Task A13.0 — retrofit h_s2_rms_ema with Pearls A+D + buffer growth
Replaces the kernel's hardcoded `ema_alpha` with the shared `pearls_ad_update`
host-side helper (Task A3). Buffer growth in same commit so subsequent A13.x
retrofits land on a stable scratch/Wiener layout.

  - Kernel `h_s2_rms_ema_update` signature: `(h_s2, B, SH2, scratch_buf,
    scratch_idx)`. Reduces RMS = sqrt(sum_sq/(B*SH2)) via the existing
    256-thread shmem tree (no atomicAdd) and writes step_obs to
    `producer_step_scratch_buf[40]` with `__threadfence_system()`.
  - Launcher `launch_h_s2_rms_ema(_ema_alpha_unused: f32)` syncs the stream,
    then applies Pearls A+D host-side via zero-copy mapped-pinned reads/
    writes of `isv_signals_pinned[H_S2_RMS_EMA_INDEX=96]` and
    `wiener_state_buf[120..123)` (= scratch slot 40 × 3). Degenerate-zero
    short-circuit before mutating ISV/Wiener state.
  - Buffer growth: `SP4_PRODUCER_COUNT 47 → 69` (40 SP4 + 29 Task A13
    retrofit producers); `wiener_state_buf 141 → 207` floats;
    `producer_step_scratch_buf` grows to 69 entries.
  - Stable-layout doc-comments updated: `producer_step_scratch_buf` field
    comment, `launch_sp4_target_q_p99` slot-table comment, 5 launcher
    `wiener_offset + 2 < 141` safety comments (now `< 207`),
    `reset_sp4_wiener_state` doc + body comment, and
    `state_reset_registry.rs::sp4_wiener_state` description.
  - Test cubin reference `SP4_PRODUCER_COUNT: usize = 47` in
    `tests/sp4_producer_unit_tests.rs` updated to 69 across all 6
    occurrences.

Behavior: stationary signals converge to the same RMS at adaptive rate
(Pearl D's α* derived from per-slot signal-vs-noise variance);
non-stationary signals respond Wiener-optimally faster. Cold-path producer
with no consumer-facing change beyond the EMA mechanism — slot 96 is read
by `mag_concat_qdir`'s adaptive-scale path and stays semantically identical
(RMS of `save_h_s2`).

Tests: new `sp4_h_s2_rms_ema_writes_step_rms_via_pearl_a_then_converges_pearl_d`
unit test (`#[ignore]`-gated for GPU) drives the production kernel kernel-
direct on a constant-5.0 stationary signal of B=4 SH2=64, asserts
step_rms ≈ analytical RMS=5.0 ± 1e-4, asserts non-target scratch slots
remain 0, then exercises Pearl A bootstrap (returns step_rms directly +
seeds x_lag) and Pearl D convergence (1000 stationary observations →
x_mean within 1% of 5.0).

Per `feedback_no_atomicadd.md`, `feedback_no_htod_htoh_only_mapped_pinned.md`,
`feedback_no_partial_refactor.md`. Build: `cargo check -p ml --lib --tests
--offline` clean (11 pre-existing warnings, no new warnings); `cargo test
-p ml --lib sp4_wiener_ema --offline` 6/6 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 02:03:50 +02:00
jgrusewski
8f93c11525 fix(sp4): Task A7 fix-up #2 — Curiosity sub-buffer support, all 8 groups wired
Curiosity stores params/grads/Adam state as 4 non-contiguous sub-buffers
(w1, b1, w2, b2 — each its own CudaSlice<f32>). The single-pointer kernel
signature couldn't describe them. The first fix-up wired 4 of 5 aux
groups; Curiosity (group 7) was deferred.

This commit extends the param_group_oracle kernel to accept a sub-buffer
table (mapped-pinned u64 ptr-arrays + i32 counts), with `n_sub=1` for
groups 0-6 (existing behavior) and `n_sub=4` for Curiosity. Pass A/B/C
(p99 histograms) iterate sub-buffers via a new
`sp4_histogram_p99_multi<BLOCK_SIZE>` template that mirrors the original
three-pass structure but loops sub-buffers within the max-reduce + binning
passes; Pass 3 (cumulative-from-top) divides by `total_count`. Pass D
(4-way reduce) iterates sub-buffers in the accumulator loop; Pass E
(L1 trunk only) reads `grads_ptrs[0]` (group 0 has n_sub=1 by construction).

`Sp4ParamGroupBufs` redefined from a flat quartet to `Vec<Sp4SubBuffer>`.
Convenience constructors `Sp4ParamGroupBufs::single(...)` and
`Sp4ParamGroupBufs::empty()` keep call sites ergonomic; `total_count()`
for kernel arg. Switched Copy → Clone since the descriptor now owns a Vec.
`SP4AuxBuffers` extended with `curiosity` field (5-tuple from 4-tuple).

Added 16 public accessors to GpuCuriosityTrainer (grad + Adam state) and
8 to CuriosityWeightSet (weights + lengths) for the 4 sub-buffers ×
4 signal types.

`oracle_subbuf_table_buf: MappedU64Buffer` (4×4 = 16 u64s) and
`oracle_subbuf_counts_buf: MappedI32Buffer` (4 i32s) allocated at
construction. Launcher overwrites entries [0..n_sub) per group launch
via volatile writes; zeros the unused tail (defence-in-depth so a kernel
bug reading past `n_sub` lands on count=0 no-op). Inter-launch
`stream.synchronize()` added so the next iteration's host writes don't
race with the in-flight kernel's coalesced loads from the persistent
table. Cold-path producer; per-launch sync cost is negligible vs the
kernel work.

`build_sp4_aux_buffers` signature changed: takes
`Option<&CuriosityWeightSet>` and `Option<&GpuCuriosityTrainer>` since
Curiosity state lives outside FusedTrainingCtx (owned by
GpuExperienceCollector). Layer B's training-loop caller threads them
in from `collector.curiosity_weight_set()` + the collector's
`curiosity_trainer` field; both must be Some together (caller
responsibility — they live on the same collector). Passing None for
either yields an empty curiosity descriptor and the launcher silently
skips group 7.

`param_group_buffers` return type changed from
`Option<(u64, u64, u64, u64, usize, i32, i32)>` to
`Option<(Sp4ParamGroupBufs, i32, i32)>`. All groups now return Some(...)
(Curiosity included); None reserved for forward-compat.

GPU test extended: group 7 exercises 4 sub-buffers of distinct shapes
[1024, 32, 1024, 32] (w1/b1/w2/b2-like sizes scaled to keep test runtime
small while still exercising the multi-sub-buffer iteration), each with
its own seed offset so distinct sub-buffers have distinct distributions
— catches buffer-mixup bugs in the kernel's sub-buffer iteration.
Reference computation builds union vectors and computes p99/WD_RATE
over the union. Test launcher refactored to `&[TestSubBuffer]` slice
matching the production kernel's table-packing layout.

All 8 SP4 param-groups now produce real outputs in Layer A. The
launcher's `count == 0` short-circuit retained for the optional aux-
trainer fallback (init failures on gpu_iqn / gpu_attention / curiosity).

`MappedU64Buffer` gained a manual Debug impl (warn missing_debug_impls)
for parity with MappedU32Buffer.

cargo check -p ml --lib --tests clean. Workspace clean. Layer B can now
safely consume all 8 ISV[WEIGHT_BOUND/ADAM_M_BOUND/ADAM_V_BOUND/WD_RATE[group]]
slots.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:39:32 +02:00
jgrusewski
a72468666f feat(sp4): Task A9 — h_s2_p99 producer for ISV[H_S2_BOUND=169]
New separate producer kernel reading save_h_s2 [B × SH2] via
sp4_histogram_p99<256>. Output → ISV[169] via host-side Pearls A+D
applied at scratch slot 39. Mirrors Task A5's pattern exactly.

NB: distinct from existing h_s2_rms_ema_update (slot 96) which is a
different signal (RMS, not p99). Task A13 will retrofit that existing
kernel to also use Pearls A+D, in a separate commit.

GPU test verifies p99 ≈ 2.576 for 4096 |N(0,1)| samples within 5%
tolerance (rel_err = 0.336% on local RTX 3050 Ti) and exercises Pearl
A's sentinel branch (first observation replaces sentinel directly,
x_lag seeds to step_p99, both variances stay zero).

No consumer wired yet. Behaviour unchanged. cargo check --lib --tests clean.
6/6 SP4 producer GPU tests passing locally (A4 + A5 + A6 + A7 + A8 + A9).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:50:44 +02:00
jgrusewski
611d2663cb feat(sp4): Task A8 — grad_norm_p99 producer (degenerate single-element)
grad_norm_buf is single-element f32; p99 == the element. Kernel copies
to scratch slot 38 with __threadfence_system; host applies Pearls A+D
to update ISV[GRAD_CLIP_BOUND_INDEX=168]. Trivial pattern — establishes
the launcher template for Mech 6's eventual ISV read in Layer B.

GPU test verifies first-observation Pearl A replacement (input=42.0 →
scratch[38]=42.0 bit-identical → new_x_mean=42.0, x_lag=42.0, both
variances stay zero) and that all non-target scratch slots remain 0.

No consumer wired yet. Behaviour unchanged. cargo check --lib --tests clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:45:21 +02:00
jgrusewski
4f13e2ca37 feat(sp4): Task A7 — Pearl B fused per-param-group statistics oracle
Single kernel per group reads (params, grads, adam_m, adam_v) once and
fuses 5 passes (4 for non-trunk):
  Pass A: WEIGHT_BOUND[group] = p99(|params|)  via sp4_histogram_p99
  Pass B: ADAM_M_BOUND[group] = p99(|adam_m|)  via sp4_histogram_p99
  Pass C: ADAM_V_BOUND[group] = p99(|adam_v|)  via sp4_histogram_p99
  Pass D: WD_RATE[group] = |Σ w·g| / max(Σ w², ε_div); side: mean|g|, mean|w|
  Pass E (group 0 only): L1_LAMBDA[trunk] via gradient-direction entropy
   deficit × (mean|g|/mean|w|) magnitude scale

Launcher loops over 8 groups (DQN trunk/value/branches, IQN, IQL hi/lo,
attn, curiosity), one launch per group. Phase 2 applies Pearls A+D to
each of the 4-5 outputs per group via host-side pearls_ad_update.

Producer-scratch slots 5..38 reserved for this task's 33 outputs:
[5..13)=WEIGHT, [13..21)=ADAM_M, [21..29)=ADAM_V, [29..37)=WD_RATE, 37=L1.

Three of eight groups wired (DQN trunk/value/branches); five aux groups
(IQN, IQL hi/lo, attn, curiosity) skip silently because their backing
trainers live on FusedTrainingCtx, not GpuDqnTrainer. Wiring those
requires either threading buffer pointers through the launcher signature
or hoisting the launcher onto FusedTrainingCtx — both follow-up changes
scoped beyond Task A7. Documented in launcher + param_group_buffers
docstrings.

Per-group GPU tests verify outputs within 5% rel_err (p99 quantization)
or 2% rel_err (analytical formulas). Local RTX 3050 Ti: max WEIGHT
rel_err=0.589%, max ADAM_M=0.589%, max ADAM_V=0.554%, max WD_RATE=0.001%
across all 8 group shapes; group 0 L1 within tolerance.

Pearl B 4× memory-bandwidth reduction vs naive 4 separate per-group
producer kernels: each buffer read once for all 4-5 outputs.

No consumer wired yet — Adam kernels still take hardcoded weight_decay
+ weight_clamp_max_abs config args. Behavior unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:34:19 +02:00
jgrusewski
b4c28811ab feat(sp4): Task A6 — atom_pos_p99 producer × 4 branches
Single kernel parameterized by branch slice. Launcher loops 0..4,
launching once per branch with distinct scratch slot (1..5). Phase 2
applies Pearls A+D host-side per branch, writing to ISV[ATOM_POS_BOUND[
branch]] + wiener_state. Same end-to-end pattern as Task A5.

GPU test verifies per-branch independence: 4 scales of |N(0,1)| samples
(1×, 10×, 100×, 1000×) → 4 distinct p99 values within 5% tolerance.

No consumer wired yet. Behavior unchanged. cargo check --lib --tests clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:07:07 +02:00
jgrusewski
cd037084b2 feat(sp4): Task A5 — target_q_p99 producer kernel + Pearls A/D wire-up
First end-to-end SP4 producer. Kernel reads denoise_target_q_buf,
computes p99(|target_q|) via sp4_histogram_p99<256>, writes step_obs
to producer_step_scratch_buf[0] with __threadfence_system. Launcher
syncs, applies Pearls A+D via pearls_ad_update (zero-copy mapped-pinned
reads of ISV[TARGET_Q_BOUND_INDEX=131] + wiener_state_buf[(131-base)*3]),
writes new x_mean back to ISV + state back to wiener_state. Cold-path
launch (no captured graph in this task; Task A10 may move to captured).

Producer-step-scratch slot 0 reserved for TARGET_Q_BOUND (stable layout
documented in launcher comment for Tasks A6-A11 to extend).

GPU unit test verifies kernel writes step_p99 ≈ p99(|N(0,1)|) within
5% rel_err on 4096 Box-Muller samples, then exercises Pearl A's
sentinel branch (sentinel ISV + zero Wiener state → first-observation
replacement). Helper asserts non-target scratch slots stay zero.

No consumer wired yet — Mech 1's clamp still uses 10 × Q_ABS_REF.max(1.0).
Behavior unchanged. cargo check --lib --tests clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 22:53:27 +02:00
jgrusewski
7540df1ecf feat(sp4): Task A4 — linear-histogram p99 device function + GPU unit test
Header-only `__device__` function `sp4_histogram_p99<BLOCK_SIZE>(buf, count)`
returning p99(|buf|) via three-pass single-block algorithm:
  Pass 1: block-wide max-reduce → step_max (0.0 short-circuit on degenerate)
  Pass 2: linear-spaced [0, step_max] binning into 256 bins via per-warp
          tiles (no atomicAdd; lane-collisions cost <0.012% precision)
  Pass 3: cumulative-from-top → p99 = bin upper-edge

Returns 0.0 for degenerate step_max=0 (caller skips ISV update). Linear
spacing chosen over log because SP4 producers care about resolution at
the top of the distribution — top bin width = step_max/256 ≈ 0.39%, well
within the 1% quantile precision budget.

Wrapper kernel `sp4_histogram_p99_test_kernel` exposes the device fn for
Rust testing; mapped-pinned scalar output with __threadfence_system() so
host read_volatile sees the result (no memcpy_dtoh). Build.rs
registration mirrors thompson_test_kernel.cu pattern + adds
rerun-if-changed for the .cuh header.

Unit test: 4096 deterministic |N(0,1)| Box-Muller samples, sorted
ground-truth p99 ≈ 2.576 z-score one-tailed, asserts rel_err < 5%
against device output. GPU-gated (#[ignore]). Local L40S run:
true_p99=2.59758, computed_p99=2.59858, rel_err=0.039% — passes.

No producer wired yet — header is library code included only by the
test wrapper; SP4 Tasks A5-A9 add the magnitude-bound producer kernels
that #include "sp4_histogram_p99.cuh" directly. Behaviour unchanged.
cargo check --lib --tests clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 22:26:07 +02:00
jgrusewski
673b04a8d4 perf(checkpoint): async best-ckpt serialize via spawn_blocking + mapped-pinned param snapshot
Best-checkpoint save (val Sharpe improvement, ~30% of epochs in
convergent runs) blocked the epoch loop for 20-40s on each improvement:
serialize_model() chained N (~26) per-tensor DtoH downloads via
GpuTensor::to_host → memcpy_dtoh, each forcing an implicit stream sync
on a busy training stream. The DtoH chain also violates
feedback_no_htod_htoh_only_mapped_pinned.md (only cuMemHostAlloc
DEVICEMAP allowed for CPU↔GPU paths).

Plan B:

- Introduce snapshot_model_to_pinned (mod.rs): allocate one
  MappedF32Buffer sized for all named weight slices concatenated,
  cuMemcpyDtoDAsync each slice into the buffer's device pointer
  (which aliases the host page), single stream sync, copy bytes out
  to a Send + 'static Vec<u8>. One sync per snapshot, replaces N.

- serialize_snapshot_bytes (mod.rs): pure-CPU safetensors construction
  from CheckpointSnapshot. Static — callable without &self, so the
  worker can move the snapshot across thread boundary.

- handle_epoch_checkpoints_and_early_stopping on val-Sharpe
  improvement: save_best_gpu_params (DtoD, fast) + snapshot to pinned
  + tokio::task::spawn_blocking the safetensors construction +
  checkpoint_callback invocation. JoinHandle parked on
  pending_checkpoint_handles. Training loop continues immediately.

- await_pending_checkpoint_handles drains in-flight workers at
  training end (success branch + early-stop branches) and before
  any synchronous cold-path checkpoint write to keep disk ordering
  deterministic.

- F bound on train / train_walk_forward / train_fold_from_slices
  gains + 'static so the callback can be moved into the worker.
  All public callers already use 'static-compatible move closures
  (test fixtures with shared mutable state migrate to Arc<Mutex<T>>).
  Internal pipeline uses CheckpointCallbackHandle =
  Arc<std::sync::Mutex<Box<dyn FnMut + Send + 'static>>> so the
  same callback flows through multi-fold walk-forward into every
  fold's worker.

- serialize_model itself rewritten via the snapshot path: the
  no-DtoH rule now holds across ALL checkpoint paths (best, periodic,
  early-stop, plateau-exhausted). The pre-existing GpuTensor::to_host
  path is no longer reachable from the DQN trainer.

The audit's spec called for an mpsc channel(1) drop-old worker, but
the multi-fold + &mut F pre-existing API made the simpler
fire-and-forget spawn_blocking pattern a cleaner fit (Mutex
serialises any concurrent invocations; Vec<JoinHandle> drain at end
guarantees disk writes complete before the trainer returns). Same
overlap benefit (training rolls while serialize+disk run on a
blocking thread); upper bound on in-flight work is one-per-improved-
epoch which approximates the spec's depth=1 in realistic training
runs.

Per feedback_no_partial_refactor: every site that constructs a
checkpoint payload migrated in lockstep — best-improvement uses
the worker; periodic / plateau-exhausted / early-stop call the
shared Arc<Mutex<F>> handle inline. All paths read params via
snapshot_model_to_pinned, so the no-DtoH rule applies uniformly.
Test fixtures (8 .rs files) updated for the + 'static bound (move
closures + cloned PathBufs / Arc<Mutex<T>> for shared mutable state).

Verified: SQLX_OFFLINE=true cargo check --workspace --tests clean
(warnings unchanged from baseline). cargo test -p ml --lib --no-run
clean. No fingerprint change.

Wire-up audit entry extended with Plan B file:line edit sites
(rides under the same Async-validation overlap section started by
the companion Plan A commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:54:00 +02:00
jgrusewski
aa56cd7069 fix(ofi): align OFI window to bar t's formation interval — kill 31/32-slot lookahead
OFI features at bar t were computed over [close(bar_t), close(bar_{t+1}))
— bar t+1's formation period — so 31 of 32 OFI dims at the policy's
input for bar t carried next-bar microstructure data. Per audit
`docs/lookahead-bias-audit-2026-04-28.md` §3, this is a DEFINITE leak
contaminating both training inputs and validation backtest.

Fix: shift the window backward to (close(bar_{t-1}), close(bar_t)] so
OFI at bar t uses only data accumulated during bar t's own formation.
Mirrors the correct `log_bar_duration` convention at
precompute_features.rs:567-575 (audit's smoking-gun citation).

Per feedback_no_partial_refactor, both write sites migrated in lockstep:
- crates/ml/examples/precompute_features.rs:441-475 (precompute path)
- crates/ml/src/trainers/dqn/data_loading.rs:396-448 (DBN fallback)

FXCACHE_VERSION bumped 6 → 7. PVC auto-regen via schema-hash check on
next deploy. Local regen:

  ./target/release/examples/precompute_features \
      --data-dir test_data/futures-baseline \
      --mbp10-data-dir test_data/futures-baseline-mbp10 \
      --trades-data-dir test_data/futures-baseline-trades \
      --output-dir test_data/feature-cache \
      --symbol ES.FUT --data-source mbp10 --yes

Regression test added: tests/ofi_features_test.rs::
test_ofi_window_alignment_uses_formation_interval validates the new
partition_point predicates against a synthetic 5-bar dataset, including
an explicit anti-regression assertion that bar t+1 snapshots are NEVER
picked up for bar t.

dqn-wire-up-audit.md updated to reflect new contract for
`trainers/dqn/data_loading.rs`. Audit report
`docs/lookahead-bias-audit-2026-04-28.md` checked in alongside the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 09:23:32 +02:00
jgrusewski
2d0e6b6bdc feat(moe): adaptive load-balance λ via gate-entropy ISV controller
Replace static moe_lambda=0.01 with a GPU-driven controller that scales
λ inversely with observed gate-entropy: λ_eff = λ_floor + λ_max_extra ×
max(0, ent_target − ent_ema)/ent_target, where ent_ema is the existing
ISV[126] producer (Phase 2 task 2.4 wire-up).

Motivation: the L40S validation run train-multi-seed-fgg9x exposed
single-expert specialization in fold 1 (expert 4 → 81% util, 7/8
experts < 5%) under static λ=0.01. The load-balance push was too weak
to prevent winner-take-all once the gate had a strong preference.
Adaptive λ rises proportional to the entropy deficit, restoring
diversity pressure when the gate concentrates.

Per pearl_blend_formulas_must_have_permanent_floor: λ_floor remains a
permanent minimum (= old static 0.01) so the controller never weakens
below baseline. Per feedback_isv_for_adaptive_bounds: signal flows
through ISV[128] (new MOE_LAMBDA_EFF_INDEX); consumer kernel reads
the slot at runtime; no DtoH on hot path.

Wiring (feedback_no_partial_refactor):
  • new kernel moe_lambda_eff_update (single-block cold-path cadence,
    matches kelly_cap_update / cql_alpha_seed_update precedent)
  • new ISV slot 128, ISV_TOTAL_DIM 127 → 129
  • layout_fingerprint_seed updated (schema_hash bumps; PVC fxcache will
    auto-regen on next deploy)
  • moe_load_balance_loss kernel takes (isv_signals, isv_lambda_eff_idx)
    instead of float lambda — matches mag_concat_qdir's ISV-read pattern
  • config: pub moe_lambda field replaced with moe_lambda_floor (0.01),
    moe_lambda_max_extra (0.09), moe_entropy_target_frac (0.7)
  • state-reset registry entry — ISV[128] resets to floor at fold boundary
  • HEALTH_DIAG aux_moe line gains λ_eff field for observability
  • hyperopt adapter (DQNHyperparameters) migrated to new knobs
  • constructor bootstraps ISV[128]=floor + ISV[118..127)=uniform 1/K +
    ISV[126]=ln(K) so cold-start matches legacy byte-for-byte

Smoke validates: magnitude_distribution still passes/fails identically
to HEAD (eq=0.586 same as pre-change eq=0.592 — unrelated Kelly cap
behaviour per project_magnitude_eval_collapse_kelly_capped.md).
HEALTH_DIAG fold 3 confirms controller live: ent decays 1.381 → 0.746
across epochs 7-19, λ_eff rises 0.0146 → 0.0539 monotonically.

Unit test moe_lambda_eff_update_writes_correct_values exercises 5
regimes (above-target / at-target / half-collapse / full-collapse /
deeply-above-target) on the GPU kernel — all pass within 1e-5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 09:10:31 +02:00
jgrusewski
95965dd11b feat(moe): moe_expert_util_ema_update ISV producer kernel + test
Single-thread cold-path-cadence kernel writes 8 per-expert utilization
EMAs + 1 gate-entropy EMA into ISV slots [118..127), α=0.05. Same shape
as h_s2_rms_ema_update / aux_heads_loss_ema_update.

GPU-resident, CPU read-only per pearl_cold_path_no_exception_to_gpu_drives.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:47:34 +02:00
jgrusewski
bae9d7a7d8 feat(moe): moe_load_balance_loss kernel + correctness tests
Two-step design (per-k contribution then K-element reduce) avoids
atomicAdd per feedback_no_atomicadd.md. Tests verify CPU reference
match and uniform-gate minimum (loss = λ).

Backward gradient lands in Phase 3 (wire-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:45:00 +02:00
jgrusewski
9eea7f2977 feat(moe): moe_mixture_backward + moe_dgate_reduce kernels + FD test
Two-stage backward: moe_mixture_backward computes de_k = g · dh_s2 in
one kernel; moe_dgate_reduce computes dg via shmem reduction over c.

FD test verifies analytic backward matches numerical for B=2, K=4, C=32
within 1e-3 tolerance (perturbation 1e-3).

All test data flows via mapped pinned buffers (no HtoD/HtoH).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:42:29 +02:00
jgrusewski
3d17c2a93c feat(moe): moe_mixture_forward kernel + Rust wrapper + unit test
Single-thread-per-(b,c) kernel, no atomicAdd, capture-friendly.
CPU-reference test verifies correctness for B=4, K=8, C=256 within 1e-6
tolerance.

Test wrapper uses mapped pinned memory exclusively
(feedback_no_htod_htoh_only_mapped_pinned.md): allocate MappedF32Buffer,
write CPU-side via host_ptr, kernel reads via dev_ptr, output via host_ptr
read after stream sync. NO memcpy_stod / memcpy_dtov anywhere.

GpuMoeHead struct in crates/ml/src/cuda_pipeline/gpu_moe_head.rs follows
existing cuda_pipeline head pattern (cubin loaded once, kernel handles
cached). Subsequent kernels (moe_mixture_backward,
moe_load_balance_loss, moe_expert_util_ema_update) extend the same
struct.

Spec: docs/superpowers/specs/2026-04-27-moe-regime-redesign-design.md §6.2.
Plan: docs/superpowers/plans/2026-04-27-moe-regime-redesign.md Task 2.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:33:41 +02:00
jgrusewski
8de65265ee refactor(dqn): strip dead use_* flags + tighten count_bonus API
Atomic removal of 5 boolean feature flags from DQNConfig per
`feedback_no_feature_flags.md`, all of which gated dead or redundant code.
(use_iqn already removed in da632446c.)

- `use_dueling`, `use_distributional`, `use_noisy_nets`, `use_branching`:
  pure-cosmetic always-on flags. Only metadata strings remained.
  4 `if true /* always on */` blocks in dqn.rs collapsed to live arms;
  dead else arms (legacy non-branching code paths, 5-flat ExposureLevel)
  deleted.
- `use_count_bonus`: redundant with `count_bonus_coefficient` (the live
  numeric kill-switch). Recording made unconditional; coefficient is the
  single dial.
- `use_cvar_action_selection`: dead post-use_iqn cleanup (only consumers
  were inside the deleted IQN arms). cvar_alpha retained for cuda_pipeline
  CVaR loss usage.

count_bonus.rs API tightened: `bonuses_branched_f32(&self,
exp_out: &mut [f32], ord_out: &mut [f32], urg_out: &mut [f32])` write-into
API alongside the existing Vec<f64> form (kept for tests). Production
DQN::get_count_bonuses_branched() returns fixed `([f32; 4], [f32; 3],
[f32; 3])` arrays — allocation-free, f32 throughout, fed directly to GPU
action selector via stack-allocated buffers.

Test 0.F outputs bit-identical vs pre-cleanup (sigma_C51, argmax,
Thompson counts) — confirms legacy use_* arms were unreachable as
expected.

`docs/dqn-wire-up-audit.md` updated per Invariant 7.

Precondition for the MoE regime redesign per
`docs/superpowers/specs/2026-04-27-moe-regime-redesign-design.md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:52:23 +02:00
jgrusewski
da632446ce refactor(dqn): strip use_iqn feature flag + dead legacy iqn_network
`use_iqn` is exactly the `use_/enable_` boolean banned by
`feedback_no_feature_flags.md`. It gated dead code: production training
runs IQN unconditionally through `cuda_pipeline/gpu_iqn_head.rs` +
`iqn_dual_head_kernel`, properly wired into the branching architecture
with the `FIXED_TAUS` 5-quantile schedule. Nothing in the cuda_pipeline
production path ever read `use_iqn` or `iqn_network`.

The legacy `iqn_network: Option<QuantileNetwork>` field on `DQN` was a
parallel CPU-side network from a pre-branching era, structurally
unreachable in production: every consumer was gated behind the
`if true /* use_branching: always on */` arm at `q_values_for_batch`,
so the IQN else-if at L1822 was dead code. Training optimised
`iqn_network` parameters in isolation; inference never read them. That's
the train/inference mismatch the L283 comment ("IQN trains base
q_network but inference uses dist_dueling network (zero gradients)")
was working around by **disabling** the feature instead of fixing the
inference path. Per `feedback_no_quickfixes.md` + `feedback_no_hiding.md`
the fix is to remove the dead path entirely.

Strip:
- `DQNConfig::use_iqn` field + parses + checkpoint hash + metadata.
  `dqn.use_iqn` / `dqn.iqn_embedding_dim` / `dqn.iqn_num_quantiles` /
  `dqn.iqn_kappa` from older checkpoints are silently dropped on load
  (same pattern used for `use_dueling`). `iqn_lambda` stays — the
  cuda_pipeline dual head consumes it as the IQN aux loss weight.
- 3 vestigial config fields (`iqn_num_quantiles`, `iqn_kappa`,
  `iqn_embedding_dim`) — never read in production; kernel-side macros
  (`IQN_NUM_QUANTILES = 5`, embed_dim 64, kappa 1.0) are the actual
  config.
- 4 default builders (`Default`, `aggressive`, `conservative`,
  `emergency_safe_defaults`) drop the 4 IQN-related fields each.
- `DQN::iqn_network` field + initialisation block in `new_with_stream`.
- 6 conditional gates in `select_action`, `select_action_with_confidence`,
  `select_action_inference`, `q_values_for_batch` — all collapse to the
  live (branching or standard-Q) arm.
- `DQN::get_state_embedding` (only consumed by deleted IQN paths).
- The entire `crates/ml-dqn/src/quantile_regression.rs` module (392 LOC)
  + its 2 lib.rs exports. Nothing outside `ml-dqn` ever imported it
  (the `quantile_huber_loss` reference in `gpu_iqn_head.rs` is a CUDA
  kernel name string, unrelated to this Rust module).

Downstream call sites:
- `crates/ml/src/trainers/dqn/{config,fused_training,trainer/constructor}.rs`:
  drop `iqn_num_quantiles` / `iqn_embedding_dim` / `iqn_kappa` references
  off `DQNConfig`; substitute kernel-fixed literals (64, 1.0) where
  `GpuDqnTrainConfig` / `GpuIqnConfig` still expect them.
- `crates/ml/examples/evaluate_baseline.rs`: drop two `iqn_num_quantiles`
  hyperparam reads (their `..DQNConfig::default()` fallbacks now stand
  alone).
- `crates/ml/tests/dqn_action_collapse_fix_test.rs`: drop the
  `assert!(!config.use_iqn, "...gradient dead zone")` and the explicit
  `config.use_iqn = false` setter; the dead-zone pathology is now
  structurally impossible.
- `crates/ml/tests/dqn_inference_test.rs`: drop `config.use_iqn = false`.
- `services/trading_service/src/services/dqn_model.rs`: drop the
  `iqn={}` debug-log field.
- `crates/ml/src/trainers/dqn/distributional_q_tests.rs`: ship Test 0.F
  (Plan A Task 8 #186) — converged-checkpoint extraction harness +
  `MappedF32Buffer` (mapped pinned f32 mirror) + `compute_sigma_c51_test`
  kernel handle. The structural assertions panic on the local 5-epoch
  smoke checkpoint as designed (under-converged: sigma_C51 spread ~1.4%
  across directions, P(active)=0.4330 >= 0.20). Docstring rewritten to
  drop `use_iqn=false` framing and the legacy "Tier-B-prime" caveat;
  Tier-A version is GPU-integration-only per
  `feedback_no_cpu_forwards.md` (CPU is read-only).

`docs/dqn-wire-up-audit.md` updated per Invariant 7.

Build: `cargo check --workspace --tests` clean (0 errors).
Test: `cargo test -p ml --lib distributional_q_tests::test_0f
       -- --ignored --nocapture` produces bit-identical sigma_C51 /
       argmax / Thompson values vs pre-removal — confirms branching
       forward path is the same code post-cleanup as pre-cleanup
       (legacy `use_iqn` arms were unreachable, as expected).

Net: 12 files, +458 / -785 lines (327 LOC deleted).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:12:13 +02:00
jgrusewski
d64adc14f5 refactor(dqn): f64 → f32 for kernel-facing hyperparams
Eliminates the f64→f32 cudarc ABI trap (feedback_cudarc_f64_f32_abi.md,
task #82) at the type level: hyperparameters consumed by CUDA kernels
now live as f32 in Rust, cast once at the TOML/PSO ingest boundary
instead of at every kernel call site.

Structs changed:
  - DQNHyperparameters (crates/ml/src/trainers/dqn/config.rs) —
    ~85 scalar fields migrated from f64 → f32. Covers all
    kernel-facing scalars: reward weights (w_pnl/w_dd/w_idle,
    dd_threshold, cea_weight, micro_reward_*, price_confirm_weight,
    book_aggression_weight, hold_quality_weight), exploration
    (epsilon_* and the 4 branch mults, noisy_sigma_*, count_bonus,
    noise_sigma, q_gap_threshold), distributional RL (v_min, v_max,
    reward_scale, iqn_lambda, qr_kappa, spectral_*,
    gradient_collapse_multiplier), fill simulation (5 fill_*
    fields), risk/Kelly (kelly_fractional, kelly_max_fraction,
    max_leverage, max_position_absolute, minimum_profit_factor),
    ensemble/curiosity (curiosity_weight,
    curiosity_q_penalty_lambda, ensemble_*, beta_*, variance_cap),
    anti-LR (anti_lr_*, adversarial_dd_threshold,
    beta_penalty_strength), walk-forward (wf_*),
    experience (avg_spread, transaction_cost_multiplier,
    holding_cost_rate, churn_penalty_scale, contract_multiplier,
    margin_pct, tick_size, bars_per_day, cash_reserve_percent),
    misc kernel scalars (gamma, tau, huber_delta, q_clip_*,
    shrink_perturb_*, regime_replay_decay, per_alpha,
    per_beta_start, dt_target_return, etc.). Also
    `noisy_epsilon_floor: Option<f32>` and
    `count_bonus_coefficient: Option<f32>`.
  - `computed_v_min` / `computed_v_max` now return f32.
  - `compute_max_position` returns f32 (f64 internally for the
    notional division).

Fields preserved as f64 (precision-sensitive, NOT kernel-facing
scalars — per task spec and feedback_cudarc_f64_f32_abi.md):
  - `learning_rate` — tested at 1e-10 tolerance; f32 rounds to
    2e-12 for a 1e-5 LR.
  - `entropy_coefficient` — tested at 1e-9 tolerance.
  - `weight_decay` — tiny 1e-5..1e-3 range.
  - `adam_epsilon` — 1e-8 default; f32 preserves denorms here but
    paired with weight_decay/learning_rate for symmetry.
  - `gradient_clip_norm: Option<f64>` — grad norms are f64
    accumulators by project convention.
  - `min_loss_improvement_pct`, `q_value_floor` — early-stopping
    long-horizon stats.
  - `cql_alpha` — flows into DQNConfig (ml-dqn) still f64.
  - `min_learning_rate`, `lr_min` — paired with learning_rate.
  - Family intensity scalars (6 `*_intensity` fields) — f64 PSO
    search space; the intensity applies via `as f32` at each call
    site in `apply_family_scaling`.

No checkpoint format change: DQNHyperparameters has
`#[derive(Debug, Clone)]` only (not Serialize/Deserialize), so the
TOML-ingest path is the only serde boundary and already casts
explicitly via `hp.field = v as f32;` in
`DqnTrainingProfile::apply_to`. PSO hyperopt bounds stay f64 in
`SearchSpaceSection` and cast at the adapter boundary.

Call-site impact:
  - ~30 `as f32` casts removed from hot paths (fused_training
    FusedConfig builder, training_loop kernel launches,
    constructor DQNConfig builder, action.rs GPU action selector,
    trainer/mod.rs WF config). Kernels now receive the hp field
    directly via `&hp.x`.
  - ~75 `as f32` casts added at the `apply_to` / hyperopt-adapter
    ingest boundary — the single conversion point.
  - Cross-crate contracts (DQNConfig in ml-dqn, PortfolioTracker
    in ml-core, GAECalculator, DropoutScheduler, NoisySigmaScheduler,
    KellyOptimizerConfig, RewardConfig) retain their f64 signatures;
    ml calls cast at the boundary with `f64::from(hp.x)` so the
    contract is explicit and greppable.

Two test-side adjustments:
  - `test_kelly_fields_are_public` now asserts `f32` for
    kelly_fractional / kelly_max_fraction (these migrated).
  - `test_early_stopping_termination` uses `f64::from(...)` to
    preserve the f64 threshold computation against the now-f32
    `gradient_collapse_multiplier`.

Verified:
  - `SQLX_OFFLINE=true CARGO_INCREMENTAL=0 RUSTC_WRAPPER=sccache
    cargo check --workspace` — clean, zero new warnings.
  - `cargo check --workspace --tests` — clean.
  - `cargo test -p ml --lib training_profile::tests` — all 18
    migrated tests pass. The one pre-existing failure
    (`test_production_profile_applies_all_sections` n_steps
    mismatch, 1 vs 5) reproduces on stashed baseline, so it is
    unrelated to this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 01:55:14 +02:00
jgrusewski
79578bbaf6 feat(fxcache): OFI_DIM 20→32, persist 12 real-math microstructure signals,
fix kernel-read gap

Adds 12 features to the DQN input pipeline:
 - 10 MicrostructureState::snapshot()[0..10] slots that were previously computed
   every bar and then discarded before reaching fxcache: ofi_trajectory,
   realized_variance, hawkes_intensity, book_pressure (weighted 10-level),
   spread_dynamics, aggression_ratio, queue_depletion_asymmetry,
   order_count_flux, intra_bar_momentum, regime_score.
 - 2 TLOB-novel slots derived directly from Mbp10Snapshot:
   order_count_imbalance = (Σbid_ct − Σask_ct) / Σ(bid_ct + ask_ct),
   microprice_residual = (weighted_mid − mid) / mid.

Also fixes a production gap: ofi_acceleration (slot 18) and
toxicity_gradient (slot 19) were persisted to fxcache via OFI_DIM=20
but the OFI embed kernel (experience_kernels.cu:6146-6173) only read
[0..18), silently discarding them every bar. Kernel extended to
consume full SL_OFI_DIM=32.

Dimension bumps (all 8-aligned):
  OFI_DIM          20 → 32
  FXCACHE_VERSION   4 → 5  (invalidates existing caches; regen via
                            precompute_features)
  STATE_DIM        96 → 104
  PADDING_DIM       4 → 0  (OFI expansion consumed padding, still 8-aligned)
  STATE_DIM_PADDED 128 (unchanged)
  OFI_EMBED_IN     18 → 32 (MLP input width; W/grad/Adam/m/v buffers
                            resized in lockstep via named constants)

fxcache regen results (175874 bars ES.FUT 2024-Q1):
  deltas_nonzero:       175781 / 175874  (99.9 percent)
  book_aggression:      102137 / 175874  (58.1 percent)
  microstructure[20-30): 175874 / 175874 (100 percent)
  tlob_novel[30-32):    133615 / 175874  (76.0 percent)

Compile status: SQLX_OFFLINE=true CARGO_INCREMENTAL=0 cargo check
  --workspace --tests passes cleanly (0 errors, pre-existing warnings
  only).

Test results:
  fxcache roundtrip (unit + integration): PASS (4+6 tests)
  magnitude_distribution smoke: ran through epoch 1 successfully
    (OFI_DIAG fires, state_dim=104 confirmed, feature_dim=74 in
    validation kernel); epoch 2 OOM on local RTX 3050 Ti (4 GB) —
    expected hardware limit from state_dim growth. Full 20-epoch run
    requires L40S/H100 CI verification.
  multi_fold_convergence smoke: not verified locally (same VRAM
    ceiling applies). L40S/H100 CI verification required.

The new slots follow the existing OFICalculator/MicrostructureState
pattern and consume signals already computed by ml-features — no new
crate, no ONNX, no stubs. All 12 sources were audited against their
implementation before persistence; every slot traces back to real
Mbp10Snapshot or MicrostructureState math.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:36:34 +02:00
jgrusewski
c5045c009e cleanup: delete dead DQN apply_accumulated_gradients + honest TFT error
Two ghost-feature fixes from the pre-L40S cleanup audit (tasks #66
and #68 tracked internally).

### #66 — delete apply_accumulated_gradients (dead code from removed path)

The agent-audit confirmed this function is residue from a prior
Candle-gradient-tracking training path that was REMOVED (see the
now-deleted test crates/ml/tests/test_var_source_gradients.rs which
was `#[ignore]`'d with the comment "Candle gradient tracking removed
-- DQN/PPO use custom CUDA backward passes"). Evidence:

- `DQN::optimizer: Option<GpuAdamW>` is always `None` — never
  initialised anywhere in the codebase.
- `DQN` uses `OwnedGpuLinear` + `NoisyLinear` with standalone
  `CudaSlice<f32>` BY DESIGN (see comment at branching.rs:988-989
  "this layout exists to avoid a GpuVarStore intermediate"). There
  is no GpuVarStore to feed GpuAdamW.
- Zero external callers for `DqnTrainer::apply_accumulated_gradients`,
  `DQN::apply_accumulated_gradients`, `RegimeConditionalDQN::
  apply_accumulated_gradients`, or the various `optimizer_vars()`
  wrappers.
- The only test exercising this path was `#[ignore]`'d with the
  "Candle gradient tracking removed" rationale.
- Production training goes through the fused CUDA trainer
  (`trainers/dqn/fused_training.rs`) which applies gradients into a
  flat `params_buf` — a separate, live path.

Deleted: 6 functions across 3 files + the stale test. Preserving a
ghost that has zero callers, zero initialisation path, and a design
direction explicitly chosen AWAY from its premise is not "keep and
wire" — it's accumulating fiction. Per feedback_no_functionality_
removal.md the rule preserves FUNCTIONAL features; this wasn't one.

### #68 — TFT honest error message

Audit found TFT is architecturally incompatible with GpuAdamW in its
current form (not a wiring gap, an architectural absence):

- `TemporalFusionTransformer` has no GpuVarStore, no `parameters()`,
  no `named_parameters()` accessor
- Internal layers use `StreamLinear` which has NO `backward()`
- `TFTModel` trait only exposes `forward()`, `get_config()`,
  `clear_cache()` — no gradient accessor
- `TemporalFusionTransformer::train()` runs forward + accumulates
  loss but never calls backward or optimizer step
- `TrainableTFT` adapter's `backward()` explicitly returns "not
  supported — use TFTTrainer::train() instead"

The previous error string "TFT optimizer not yet migrated to
GpuAdamW" implied 99% done and a simple constructor wiring would
finish it. Actual gap: GpuVarStore threading through 5+ layers +
StreamLinear→GpuLinear migration + backward ops for softmax
attention / layer norm 3D / quantile monotonicity chain / stack +
trait extension for var_store accessor + train-loop gradient
assembly. ~3-7 day dedicated architectural project.

New error message states this explicitly so no one wastes time
chasing an "almost migrated" fiction. Full-lift tracked separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:43:38 +02:00
jgrusewski
a5c3d73d9e cleanup: declarative rewrites for ml-tests and trading-engine TODOs
- ml/tests/dqn_training_pipeline_test.rs: the inline TODO speculated
  about a future `load_checkpoint` hook; loader round-trip coverage
  already lives in dqn_checkpoint_tests. Reword to point there.
- ml/tests/ppo_lstm_training_loop_tests.rs: the assertion on
  `hidden_state_manager.is_some()` is the public-surface proxy for
  "LSTM path active"; deeper introspection isn't exposed. Say so.
- ml/tests/ppo_recurrent_integration_tests.rs: the test is already
  `#[ignore]`d; rewrite the inline TODO as a description of the
  missing `from_varbuilder` constructors on LSTMPolicyNetwork /
  LSTMValueNetwork.
- risk/risk_engine.rs: VarEngine receives a default asset-class
  config because the schema-to-config conversion is not wired.
  Reword the TODO to describe that plainly.
- trading_engine/types/errors.rs: `common::ConversionError` does
  not exist; keep ConversionError local and drop the aspirational
  re-export comment.
- trading_engine/tests/audit_persistence_tests.rs: describe why
  the query assertion only checks the Ok shape (row-to-event
  mapping not wired) rather than pointing at a nonexistent line
  number.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:44:05 +02:00
jgrusewski
199feff4db fix(cuda): per-stream cublasLt handles (Option C) — 10× determinism improvement on cuBLAS path
Replaces SharedCublasHandle (one lt_handle rebound across streams) with
PerStreamCublasHandles (one lt_handle per CUDA stream). Implements
NVIDIA's cuBLAS §2.1.4 remediation #1 — documented fix for concurrent-
stream non-determinism.

Context: prior investigation (task a11d706bdb56b5020) ruled out
atomicAdd/RNG/Thrust/multi-stream-sync/graph-capture. Option B (commit
bb399b635) fixed algo-selection determinism via AlgoGetIds+Init+Check
but HEALTH_DIAG still varied 1.5-2.5% at epoch 0. Context7 query of
NVIDIA docs identified shared-handle-across-streams as the remaining
cause even with user-owned workspaces and CUBLAS_WORKSPACE_CONFIG=:4096:8.

Fix:
  * PerStreamCublasHandles replaces SharedCublasHandle — HashMap<raw
    cu_stream ptr, lt_handle>, per-stream workspace registry.
  * Hot-path accessor lt_handle_for(stream) returns handle for that
    stream; creates on first use.
  * Pre-registers iqn_stream, attn_stream, 4 forward+backward branch
    streams before CUDA Graph capture (avoids mid-capture hashmap insert).
  * Deleted set_stream rebind dance.
  * 11 files migrated; ~250-350 LOC refactor. TF32 preserved everywhere.
  * Option B's DeterministicAlgoSelector unchanged; selector is
    stateless-per-handle and composes cleanly with per-stream handles.

Determinism validation at HEAD (3 runs, RTX 3050):
                            pre-C variance  post-C variance
  c51                       1.7%            0.16%    (10×)
  grad_ratio_mag_dir        1.5%            0.14%    (10×)
  grad_abs[mag]             1e-4 rel        3e-5 rel (bit-identical to 5 sig figs)
  g12_predictive            1e-6 rel        7e-6 rel (bit-identical to 6 sig figs)
  grad_abs[dir]             2.5%            8.7%     (UNCHANGED — residual)

The residual non-determinism at grad_abs[dir] is localized to the
direction-branch backward path. Magnitude-branch is bit-identical across
runs; direction-branch is not. The two branches use different backward
code paths — dir-branch has a non-cuBLAS source (candidate: atomicAdd in
a direction-specific reducer, or branch-stream finish-order dependency
on downstream atomic accumulation). Follow-up task will root-cause and
fix that residual.

Per feedback_fix_aggressively.md: shipping this partial determinism win
now so subsequent investigation has a clean baseline.

Wall-clock delta: <1% (within noise).
2026-04-22 14:44:46 +02:00
jgrusewski
66bc8d12e5 refactor: remove configurable state_dim — use STATE_DIM constant everywhere
Remove pub state_dim field from DQNConfig and GpuReplayBufferConfig; remove the
state_dim field from GpuExperienceCollector. Replace all reads with
ml_core::state_layout::STATE_DIM (and STATE_DIM_PADDED for cuBLAS-padded
strides). Checkpoint loading now validates saved state_dim against the
constant and hard-errors on mismatch. GpuAttentionConfig.state_dim is a
distinct attention-feature dim and is left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:29:05 +02:00
jgrusewski
b2fc2bbe40 fix: pre-existing smoke_test_real_data missing curiosity_weight arg
3 call sites to GpuExperienceCollector::new() were missing the 12th
argument (curiosity_weight). Pre-existing test issue surfaced during
Task 4 verification. Fixed by passing 0.0 (disabled in tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:02:51 +02:00
jgrusewski
063fd27166 feat: target_dim 4→6 + spec v5 with pearls (bar duration, book CoM, retrospective hold)
target_dim expansion: adds raw_open (OHLCV) and mid_price_open
(MBP-10 midpoint at bar formation) to fxcache targets. FXCACHE_VERSION
2→3 for auto-rebuild. Legacy v2 files handled with close-price fallback.

Spec v5 adds 3 pearls:
- Bar duration encoding in Mamba2 (continuous-time SSM awareness)
- Order book center of mass from all 10 MBP-10 levels (aggression signal)
- Retrospective hold quality bonus (teaches exit timing)

Plus: Hold action (4th direction), DSR Sharpe EMA fix, counterfactual
magnitude/order sign fix, MFT mid-price mark-to-market.

OFI embed MLP now 18→10 (was 16→8). Mamba2 width SH2+10 (was SH2+8).
Attention width D+10 (was D+8).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 23:47:04 +02:00
jgrusewski
81771d7920 feat(tick): OFI_DIM 8→20 — atomic update across 15 files
fxcache: OFI_DIM=20 (pub const), RECORD_F64_COUNT=66, 272 bytes/bar.
constructor: state_dim 74→86 (aligned 88) with OFI.
experience collector: ofi_dim detection 8→20.
All [f64; 8] → [f64; 20]. Existing 8 features preserved at 0-7.
New 12 features zero-padded until precompute is extended.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 00:21:15 +02:00
jgrusewski
05e321af15 refactor: experience collector shares cuBLAS handle with trainer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:04:19 +02:00
jgrusewski
ad5c2b7ded refactor: exp collector zero-copy — direct pointer into trainer params_buf
Dead code sweep (167 lines deleted):
- online_params_flat (synced but never read by forward)
- online_params_f32 (never synced, always zeros — ROOT CAUSE of garbage Q-values)
- DuelingWeightSet/BranchingWeightSet/CuriosityWeightSet/RmsNormWeightSet fields
- sync_weights_flat, sync_weights_f32, flatten_online_weights methods
- sync_gpu_weights epoch-boundary DtoD copy

Zero-copy replacement:
- trainer_params_ptr: u64 points directly into trainer's params_buf
- param_sizes computed from real config (bottleneck_dim=16, market_dim=42)
- CublasForward with correct s1_input_dim=54 for bottleneck
- Bottleneck forward activated (bn_hidden + tanh + concat)
- Fused context init moved before Phase 2 (experience collection)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:10:25 +02:00