17 Commits

Author SHA1 Message Date
jgrusewski
75e94858c5 feat(sp13): B1.0 — ISV[117] retirement + scale-free MSE bridge
Retires ISV[117]=AUX_LABEL_SCALE_EMA_INDEX together with its producer
kernel (aux_label_scale_ema_update), launch site, backward pass-through,
StateResetRegistry entry, HEALTH_DIAG snapshot field, and unit test.

Why: labels at the data layer are z-normalised, so the
mean(|label|) EMA tracked by ISV[117] sits at ~1.0 empirically.
Dividing by max(scale, 1e-6) before the residual `(pred - label)`
reduces to `(pred - label)` within rounding. The divisor was a
defensive scaffold from when the data layer carried mixed-scale
labels (1e-3 log returns vs 5000 raw prices); z-normalisation made
that scaffold redundant.

This is a numerical bridge, NOT the final fix. B1.1 lands on top:
- Aux head 1→2 dim (next-bar regression → 2-class direction logit)
- MSE → CE loss flip
- aux_dir_acc reads softmax over the 2 logits
- aux_pred_to_isv_tanh rewrite as logit-diff
- Producer kernel that fills aux_sign_labels with real -1/0/1 from
  the 30-bar price trajectory (B0 plumbing currently zero-init)
- dqn_param_layout fingerprint bump (head dim changes)
- aux_b1_diag HEALTH_DIAG metric
- 17+ GPU oracle unit tests

Cascade (atomic per feedback_no_partial_refactor):
- aux_heads_kernel.cu: aux_next_bar_loss_reduce + aux_next_bar_backward
  drop `isv` + `isv_label_scale_index` params; residual is (pred - label)
- aux_heads_loss_ema_kernel.cu: aux_label_scale_ema_update kernel deleted
- gpu_aux_heads.rs: kernel field/loader + launch_label_scale_ema +
  isv_* args from next_bar_loss_reduce / backward_next_bar all dropped
- gpu_dqn_trainer.rs: Step 2b producer launch + ISV slot uses dropped;
  AUX_LABEL_SCALE_EMA=117 line retained in fingerprint seed
  (no fingerprint bump in B1.0; B1.1 will bump on head-dim flip)
- gpu_health_diag.rs + health_diag.rs: aux_label_scale snapshot field
  dropped; aux block 4→3 floats, downstream offsets shift down by 1,
  WORD_TOTAL 150→149, snapshot_size_is_stable test 150*4 → 149*4
- health_diag_kernel.cu: WORD_AUX_LABEL_SCALE removed, downstream
  offsets shift, static_assert(WORD_TOTAL == 149)
- state_reset_registry.rs: isv_aux_label_scale_ema FoldReset dropped
- training_loop.rs: reset_named_state arm + HEALTH_DIAG read +
  aux line label_scale field all dropped
- sp4_producer_unit_tests.rs: load_aux_label_scale_ema_kernel helper +
  sp4_aux_label_scale_ema_writes_step_obs_via_pearl_a_then_converges_pearl_d
  test dropped

Hard rules upheld:
- feedback_no_partial_refactor: every consumer of ISV[117] migrates
  atomically — kernel + Rust orchestrator + producer launch + backward
  + HEALTH_DIAG + reset registry + unit test all in this commit
- feedback_no_stubs: not a stub — divisor is removed at every site,
  not aliased through a 1.0_const shim
- feedback_no_legacy_aliases: no legacy AUX_LABEL_SCALE_EMA_INDEX → 1.0
  alias function
- feedback_no_hiding: doc comments forward to B1.1 explicitly; no
  underscore suppression or #[allow(dead_code)]

Build: cargo check --workspace --tests clean.
Tests: snapshot_size_is_stable passes at 149*4=596 bytes.
       cargo test -p ml --lib + cargo test -p ml-dqn --lib compile.

Net delta: 10 files, −288 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:52:13 +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