Commit Graph

162 Commits

Author SHA1 Message Date
jgrusewski
d090685ca9 feat(phase-e-4-a-T16): vol-regime detection + cost-aware training
Adds two coupled interventions on the regime fragility exposed by
walk-forward CV (mean Sharpe +27 ± 56 at half-tick across 3 folds —
std-dev ≈ mean means the strategy is regime-dependent).

(1) Vol-EMA regime detector (new ISV slots 549/550)
- New alpha_regime_vol_update.cu kernel: per inference step, reads B
  parallel-env mid prices, computes cross-env mean squared log-return,
  and maintains ISV[549]=REGIME_VOL_EMA (Wiener-α with 0.4 floor +
  Pearl A bootstrap) and ISV[550]=REGIME_VOL_REF (slow tracker β=0.005,
  ≈200-step horizon).
- Block-tree-reduce (no atomicAdd), guards against zero/non-finite mids.

(2) Pre-emptive Kelly attenuation (modified stacker controller)
- stacker_threshold_controller.cu takes 3 new args: regime_vol_ema_idx,
  regime_vol_ref_idx, regime_scale_floor.
- Multiplies its reactive Sharpe-error Kelly output by
    regime_scale = clamp(vol_ref / vol_ema, 0.25, 1.0)
- Disabled when indices = -1 (backward-compatible smoke + kernel test).

(3) Cost-aware training (--train-cost-hi)
- alpha_compose_backtest --train-cost-hi: when > --train-cost, each
  training epoch samples cost ~ U[lo, hi] so the Q-network learns
  cost-conservative behaviour across the realistic ES range.

(4) Wiring
- alpha_compose_backtest --regime-scale enables both per-step regime
  kernel firing during eval AND the regime hookup in the per-episode
  controller call. Mapped-pinned mids buffers, all compute device-side.
- ExecutionEnv exposes current_mid() so the host gather reads the
  active snapshot mid per env without leaking the private cursor field.

Smoke + test sites pass -1/-1 for regime indices (backward compat).
Doc: docs/isv-slots.md ledger for slots 549/550.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 00:28:01 +02:00
jgrusewski
6e86e5b435 feat(phase-e-4-a): alpha_window_push circular-buffer kernel + GPU test
Phase E.4.A Task 6: tiny CUDA kernel that pushes a state[state_dim]
vector into slot `head_idx` of a circular window buffer
[K, state_dim]. Host tracks head_idx and zeros buffer on episode
reset. This is the GPU-side append primitive that the smoke
binary's per-step inference will call (T7) before Mamba2 over the
buffer (T8).

GPU smoke (alpha_window_push_circular_writes_to_indexed_slot):
writes state_a at slot 0, state_b at slot 2, verifies non-targeted
slots stay zero. PASS.

docs/isv-slots.md: documents kernel as slot-agnostic per
kernel-audit-doc hook requirement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 20:52:32 +02:00
jgrusewski
eb49e2a0f7 feat(alpha): Phase E.3 follow-up — C51 distributional Q + Thompson + L1-L10 depth + falsifications
C51 distributional Q-network with GPU Thompson selection borrowed
minimally from production (alpha_c51.cu: forward, project, grad,
expected_q, thompson_select kernels; ~260 lines). Uses Huber
negative-tail compression in projection per production
block_bellman_project_f. Action selection 100% GPU via mapped-pinned
i32 output + __threadfence_system + host volatile read (matches
gpu_training_guard MappedBuffer pattern).

Backtest result (2D sweep, 500 episodes per cell, 30 cells):
  cost=0    C51 +10.41 vs linear-Q -15.72  (+26pt, BEATS Phase 1d.4
                                            no-RL baseline +4.4 by 6pt)
  cost=0.125 C51 -13.81 vs -29.17  (+15pt closes half-tick gap)
Win rate at cost=0 best τ: linear-Q 0.008 → C51 0.552.

Calibration hypothesis vindicated; documented in
memory/pearl_c51_thompson_closed_phase_e3_gap.md.

Also in this commit (Phase E.3 follow-up cleanup):
- --pruned-actions falsified (2.4× worse Sharpe). Documented in
  memory/pearl_action_pruning_falsified.md.
- --real-spread falsified for ES futures (76% of bars at 1-tick floor).
- SnapshotRow bid_l/ask_l extended from [f32; 3] to [f32; 10].
  L4-L10 synthesized in this commit; real MBP-10 peek lands in E.4.A T5.
- docs/isv-slots.md updated per kernel-audit-doc hook requirement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 20:43:57 +02:00
jgrusewski
85792ed28a feat(alpha): stacker-threshold + Kelly-attenuation controller kernel
Phase E.2 Task 16. Engagement-rate self-correcting controller per
pearl_engagement_rate_self_correction. Single-block, single-thread
kernel; runs once per rollout-end boundary.

ISV slots driven:
  543  STACKER_THRESHOLD_INDEX        clamp [0, 0.5]  P-controller on rate
  545  TRADE_RATE_OBSERVED_EMA_INDEX  Pearl A+D floored Wiener-α
  546  STACKER_KELLY_ATTENUATION_INX  clamp [0.1, 1.0] P-controller on Sharpe

Reads ISV[544] TRADE_RATE_TARGET_INDEX (TrainingPersist anchor, set once
at training start).

Control law:
  observed = trade_count / max(decisions, 1)
  ISV[545] ← Pearl_A+D_floored(observed, prev, x_lag)
              [α* floor = 0.4 per pearl_wiener_alpha_floor_for_nonstationary;
               controller co-adapts with policy → need responsive EMA]
  err_rate = ISV[545] - ISV[544]
  ISV[543] ← clamp(0, 0.5, ISV[543] + k_threshold · err_rate)

  err_sharpe = rollout_sharpe - target_sharpe
  ISV[546] ← clamp(0.1, 1.0, prev_atten + k_atten · err_sharpe)
             where prev_atten = 1.0 if ISV[546] == 0.0 (sentinel-start)
             else ISV[546]

Wiener-α is INLINE (not via canonical apply_pearls_ad_kernel chain)
because the EMA is part of the control loop, not a separate diagnostic
slot. Lower latency, fewer kernels per step.

Floor at 0.1 on Kelly attenuation per
pearl_blend_formulas_must_have_permanent_floor — can't be 0, would
zero out position sizing permanently.

Pub launcher `launch_stacker_threshold_controller` in alpha_kernels.rs
with full safety asserts. Slot indices passed as i32 args (decouple
slot numbering from kernel).

GPU smoke test `stacker_threshold_controller_smoke_matches_hand_
computation` verifies 2-iteration sequence:
  Iter 1 (Pearl A):  observed=0.30 → ISV[545]=0.30; ISV[543]: 0.05 → 0.052
  Iter 2 (Pearl D):  observed=0.05 → ISV[545]=0.175 (α* hit floor 0.5)
                                     ISV[543]: 0.052 → 0.05275
Both within 1e-5 tolerance. Anchor slot 544 unchanged.

`cargo test -p ml --lib alpha_kernels`: 6 pass (compile witness + 5 GPU
smokes including this one) on RTX 3050 Ti in 2.18s.

Audit doc docs/isv-slots.md updated per Invariant 7.
2026-05-15 17:28:07 +02:00
jgrusewski
36ab50814e feat(alpha): alpha_linear_q kernels + launchers for Task 12 DQN smoke
Phase E.1 Task 12a. Three new CUDA kernels for the H=600 DQN smoke
(Task 12 proper) that lands in a follow-up commit:

  alpha_linear_q_forward_kernel    Q = X · W^T + b
  alpha_linear_q_grad_kernel       dW, db sparse MSE-TD over taken actions
  alpha_linear_q_sgd_step_kernel   element-wise params -= lr · grad

Architecture: single linear layer, no hidden layer. The Phase E state
vector has meaningful direct features (alpha_logit, spread_bps, position,
ofi_sum_5, …) so linear Q can capture real relations like Q[Buy] ∝
alpha_logit. If linear can't pass the kill-criteria gate, no architecture
upgrade will save it — and the smoke proceeds with NoisyNet escalation
per the plan.

Sparse gradient: only the taken action contributes (standard DQN TD
loss). No atomicAdd needed — one thread per (i, j) loops over the batch
and adds only when actions[b] == i.

GPU contract:
  - No host branches inside any kernel (graph-capture compatible)
  - No atomicAdd (per feedback_no_atomicadd)
  - All compute on GPU (forward, grad, weight update)
  - Tiny launch overhead — fits per-step (batch=64 forward = 576 threads,
    1 block; grad = 99 threads, 1 block)

Three pub(crate) Rust launchers in alpha_kernels.rs match the
launch_apply_pearls pattern. Cubin embedded via include_bytes!.

Smoke test `linear_q_forward_grad_sgd_round_trip_matches_hand_math`
exercises all three kernels end-to-end on a small (batch=2, state_dim=2,
n_actions=3) case with full hand-math:

  Forward:  Q = [[2.1, 3.2, 0.3], [4.1, 5.2, 0.3]] ✓
  Grad:     dW = [[-1.8, -2.7], [0.8, 1.0], [0, 0]]
            db = [-0.9, 0.2, 0] ✓
  SGD:      W' = [[1.18, 0.27], [-0.08, 0.90], [0, 0]]
            b' = [0.19, 0.18, 0.30] ✓

All within 1e-4 tolerance. `cargo test -p ml --lib alpha_kernels`:
5/5 pass on RTX 3050 Ti in 2.04s (compile witness + 4 GPU smokes).
Audit doc docs/isv-slots.md updated per Invariant 7.
2026-05-15 15:25:23 +02:00
jgrusewski
91d1a52b9c refactor(alpha): rename phase_e_* → alpha_* — system-scoped naming
The kill-criteria producer, Munchausen target kernel, Rust launchers,
fit/baseline binaries, and their output JSON artifacts are *durable
infrastructure* of the alpha trading system (live across Phase E/F/G/...),
not milestone-scoped to Phase E specifically. Aligns with the earlier
`phase_e_isv_slots.rs` → `alpha_isv_slots.rs` rename rationale.

What was renamed:

  Code files:
    crates/ml/src/cuda_pipeline/phase_e_kill_criteria.cu       → alpha_kill_criteria.cu
    crates/ml/src/cuda_pipeline/phase_e_munchausen_target.cu   → alpha_munchausen_target.cu
    crates/ml/src/cuda_pipeline/phase_e_kernels.rs             → alpha_kernels.rs
    crates/ml/examples/phase_e_fit_fill_model.rs               → alpha_fit_fill_model.rs
    crates/ml/examples/phase_e_random_baseline.rs              → alpha_random_baseline.rs

  Artifacts:
    config/ml/phase_e_fill_coeffs.json                         → alpha_fill_coeffs.json
    config/ml/phase_e_random_baseline.json                     → alpha_random_baseline.json

  Kernel function names:
    phase_e_kill_criteria_compute_kernel  → alpha_kill_criteria_compute_kernel
    phase_e_munchausen_target_kernel      → alpha_munchausen_target_kernel

  Rust launcher names:
    launch_phase_e_kill_criteria          → launch_alpha_kill_criteria
    launch_phase_e_munchausen_target      → launch_alpha_munchausen_target

  Static cubin names:
    PHASE_E_MUNCHAUSEN_TARGET_CUBIN       → ALPHA_MUNCHAUSEN_TARGET_CUBIN

Historical milestone tags in doc-comments ("Phase E.1 Task N (2026-05-15)")
are RETAINED — they record WHEN the work landed and what plan it
implemented, which doesn't change with the system-scoped rename.

Plus: ADDS the alpha_munchausen_target GPU smoke test in alpha_kernels.rs.
End-to-end validates the launcher + kernel against hand-computed expected
values: batch=2 with one terminal sample; expected targets [29.8, 1.1];
got match within 0.05 tolerance on RTX 3050 Ti. PROVES the Task 9/10
kernels actually run on GPU.

All affected references updated in:
  - build.rs (kernel compile list)
  - mod.rs (module registration)
  - state_reset_registry.rs (4 RegistryEntry descriptions for slots 539-542)
  - alpha_isv_slots.rs (slot table comment)
  - docs/isv-slots.md (audit-doc cross-references)

Verified:
  cargo test -p ml --lib alpha_kernels: 2/2 pass (including GPU smoke)
  cargo test -p ml --lib state_reset_registry: 10/10 pass
  cargo build -p ml --release --example alpha_fit_fill_model --example alpha_random_baseline: clean
2026-05-15 14:30:40 +02:00
jgrusewski
b1ba41d403 feat(alpha): Munchausen DQN target term kernel (Vieillard et al. 2020)
Phase E.1 Task 10. Standalone target-augmentation kernel:

  m            = α_m · max(τ · log π(a|s), log_clip_min)
  V_soft(s')   = max(Q_next) + τ · log Σ exp((Q_next − max) / τ)
  target       = r + m + γ · V_soft(s')   (terminal: r + m)

π(a|s) ∝ exp(Q_online(s, a) / τ) — softmax policy from the online net.
Munchausen bonus is implicit KL regularisation between successive policies;
soft-V replaces the hard max bootstrap with a τ-weighted softmax average.

Both softmaxes are computed via log-sum-exp with the max-trick. This is
essential at τ ≈ 0.03 where raw exp(Q/τ) would overflow f32 for any
Q-spread > 25 nats. The kernel is one-thread-per-batch-sample, no
atomicAdd, no host branches.

α_m, τ, log_clip_min are kernel args (not hard-coded), so a Phase E.2+
controller can ISV-drive them. Typical Vieillard values: α_m=0.9, τ=0.03,
log_clip_min=-1.0.

Does NOT touch any ISV slot — pure target augmentation.

Cubin: target/release/build/ml-*/out/phase_e_munchausen_target.cubin (12.8 KB).

Launcher integration is Task 11 (consumes target_out where the C51/MSE
loss kernels currently consume `r + γ · max_a' Q_target`). Audit doc
docs/isv-slots.md updated per Invariant 7.
2026-05-15 14:11:00 +02:00
jgrusewski
d493b729bf feat(alpha): phase_e_kill_criteria producer kernel (slots 539-542)
Phase E.0 Task 9. Single-block, single-thread CUDA producer that writes 4
raw scalar observations into a contiguous scratch_out[0..4] block:

  scratch_out[0] → ISV[539] Q_SPREAD_EMA          (kill: ≥ 0.05)
  scratch_out[1] → ISV[540] ACTION_ENTROPY_EMA    (kill: ≥ 0.5·ln(9) ≈ 1.10)
  scratch_out[2] → ISV[541] RETURN_VS_RANDOM_EMA  (kill: ≥ 0, i.e. ≥ random)
  scratch_out[3] → ISV[542] EARLY_Q_MOVEMENT_EMA  (kill: ≥ 0.01, learned)

Downstream `apply_pearls_ad_kernel` (n_slots=4) chained on the same stream
applies Pearl A first-observation bootstrap + Pearl D Wiener-α smoothing —
composes with the canonical val_sharpe_delta_compute_kernel pattern rather
than reimplementing Wiener math (per feedback_no_cpu_compute_strict — one
Wiener implementation in the codebase, the GPU one).

Inputs:
  q_values[batch, n_actions]   most-recent Q forward output (device)
  action_counts[n_actions]     empirical action histogram (device)
  scalar_inputs[3]             [rollout_R_mean, q_init_norm, q_early_norm]
                               via mapped-pinned (host writes between rollouts)
  isv                          reads slots 547 + 548 (random baseline
                               mean/std — populated once by Task 7c,
                               TrainingPersist)

No atomicAdd (per feedback_no_atomicadd), no host branches
(per pearl_no_host_branches_in_captured_graph), no CPU compute
(per feedback_cpu_is_read_only). __threadfence_system() before exit for
the chained Pearls applicator's visibility guarantee.

Cubin produced: target/release/build/ml-*/out/phase_e_kill_criteria.cubin
(16.8 KB).

Launcher integration is Task 11 (separate commit so this task can stand
alone for cubin validation). Audit doc docs/isv-slots.md updated per
Invariant 7.
2026-05-15 14:07:41 +02:00
jgrusewski
de64935b78 feat(sp22-vnext): Phase F-3 — K=3 CE EMA producer + ISV slot
Adds observability infrastructure for Phase F smoke validation: new
ISV slot AUX_TRADE_OUTCOME_CE_EMA_INDEX = 538 tracks the K=3 head's
batch-mean sparse cross-entropy EMA. Producer kernel registered +
cubin-built; launcher wireup follows in Phase F-3b commit.

Changes:
- sp22_isv_slots.rs: new pub const AUX_TRADE_OUTCOME_CE_EMA_INDEX = 538
- gpu_dqn_trainer.rs ISV_TOTAL_DIM: 538 → 539
- NEW kernel aux_outcome_ce_ema_kernel.cu: single-thread single-block
  direct-to-ISV EMA writer with Pearl A first-observation bootstrap.
  Fixed α=0.05 (slow-moving observability). NULL-tolerant + NaN-guarded.
- build.rs: kernel registered; cubin compiles (3.2 KB)

Why dedicated kernel (not extension of aux_heads_loss_ema_update):
The K=2/K=5 EMA writes through Pearls A+D's 2-stage producer scratch.
Extending it would require allocating a new scratch slot, threading
Pearls A+D mapping, and touching apply_pearls_ad_kernel. The K=3
head's CE is OBSERVABILITY ONLY in this commit — no Pearls A+D
adaptive α needed. Minimum-scope direct-to-ISV path. Re-routable
through Pearls A+D if K=3 CE later becomes a controller anchor.

Smoke validation signal interpretation:
- Cold-start: ISV[538] = 0.0 (FoldReset sentinel)
- First step with B_valid > 0: Pearl A bootstrap → ISV[538] = first CE
- Typical uniform-K=3 cold-start CE: ln(3) ≈ 1.098
- Learning signal: CE drops from ~1.098 toward 0.5-0.7 over epochs
- Falsification signal: CE pinned at ln(3) for many epochs → head
  can't learn (label noise / under-capacity / outcomes unconditional)

Phase F-3b follow-up:
- Trainer launcher call after aux_heads_loss_ema_update
- HEALTH_DIAG snap layout extension + console column
- Reset registry entry (FoldReset sentinel 0)

Verification:
- cargo check -p ml clean.
- cargo test -p ml --lib → 1016/0 green.

Audit: docs/dqn-wire-up-audit.md Phase F-3 section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:28:48 +02:00
jgrusewski
d2331f2f62 feat(sp22-vnext): Phase B5b — full plan-conditioning integration
The K=3 head's input is now [h_s2_aux (256) || plan_params (6)] = 262-
dim, matching the spec's intended architecture. Implements the input
concat in both the trainer's replay-batch path and the collector's
rollout-step path, with appropriate handling of the backward-side
stride mismatch.

NEW kernel strided_row_saxpy_kernel.cu: row-truncating SAXPY that
accumulates first n_cols_copy columns per row of src [B, src_cols]
into dst [B, dst_cols] scaled by alpha. Handles stride mismatch
(src_cols != dst_cols). Needed because backward emits dh_s2_aux_to_buf
[B, 262] but dh_s2_aux_accum [B, 256] only consumes first 256 cols.

PLAN_PARAM_DIM = 6 constant in gpu_aux_heads.rs.

Ops struct updates:
- AuxTradeOutcomeForwardOps gains concat_kernel + launch_concat()
- AuxTradeOutcomeBackwardOps gains strided_saxpy_kernel +
  launch_strided_row_saxpy()
- Both load new cubins in new()

Weight tensor resize:
- sizes[163] = H × (SH2 + PLAN_PARAM_DIM) = 128 × 262 = 33,536 floats
- fan_dims[163] = (H, SH2 + PLAN_PARAM_DIM)

Trainer changes:
- New aux_to_input_buf [B × 262] field
- aux_dh_s2_to_buf resized to [B × 262]
- aux_partial_to_w1 resized to [B × H × 262]
- max_aux_tensor_len bumped for param_grad_final scratch

Trainer forward (aux_heads_forward):
- Concat h_s2_aux + plan_params_buf → aux_to_input_buf
- forward() with SH2_TOTAL=262

Trainer backward (aux_heads_backward):
- backward() with SH2_TOTAL=262; reads aux_to_input_buf
- saxpy_f32_kernel SAXPY for dh_s2_aux REPLACED by
  launch_strided_row_saxpy: copies only first SH2=256 cols per row;
  trailing 6 cols (plan_params gradient) are dropped — STOP-GRAD on
  trade plan head from aux loss.

Collector forward (rollout):
- New exp_aux_to_input_buf [N × 262] field
- Concat with plan_params_ptr = 0 (NULL) → zero-fill trailing 6 cols
- forward() with SH2_TOTAL=262

Train/inference asymmetry (documented):
- Trainer: real plan_params from trade plan head output
- Collector: zeros (no trade plan launch in collector)
- The head is trained on real plan-conditional outcomes but queried
  at rollout time with plan_params=0. Phase B5b-2 follow-up would
  add a trade plan launch to the collector to resolve. Deferred —
  current state is functional, head still receives plan signal
  during training.

Stop-grad on plan_params: backward writes full [B, 262] gradient, but
strided SAXPY only copies first 256 cols. Trade plan head weights
NOT trained by K=3 aux loss in this commit.

Verification:
- cargo check -p ml clean.
- cargo test -p ml --lib → 1016/0 green.

Phase D next: 12-weight W atom-shift (4 actions × 3 outcomes).

Audit: docs/dqn-wire-up-audit.md Phase B5b section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:53:23 +02:00
jgrusewski
3286dc7dee feat(sp22-vnext): Phase C-1 — K=3 softmax → per-env 3-slot cache (producer)
First half of Phase C. Lands the producer side of the K=3 trade-
outcome aux head's state bridge: new kernel populates a per-env
3-slot cache from the K=3 softmax tile every rollout step. The
consumer side (state gather reading from this cache → state slots
[121..124)) lands in Phase C-2.

Mirrors the K=2 head's existing aux_softmax_to_per_env_kernel exactly
at K=3:
- K=2: prev_aux_dir_prob[env] = 2*softmax[env, 1] - 1 (recentered)
- K=3: prev_aux_outcome_probs[env, k] = softmax[env, k] for k in [0, 3)

Changes:
- state_layout.rs: 3 new constants AUX_OUTCOME_PROFIT_INDEX = 121,
  AUX_OUTCOME_STOP_INDEX = 122, AUX_OUTCOME_TIMEOUT_INDEX = 123.
  PROFIT_INDEX aliases AUX_DIR_PROB_INDEX (same value, different
  semantic). Phase C-2 flips slot 121's meaning from K=2's recentered
  p_up to K=3's p_Profit.
- aux_outcome_softmax_to_per_env_kernel.cu: new kernel + cubin.
- gpu_dqn_trainer.rs: new SP22_AUX_OUTCOME_SOFTMAX_TO_PER_ENV_CUBIN
  embed.
- gpu_experience_collector.rs: 2 new struct fields (cache buffer +
  kernel handle); cubin load + alloc in constructor; struct-init;
  per-step launch in rollout loop after K=3 forward.
- build.rs: kernel registered.

Encoding shift K=2 → K=3: K=2 used recentered [-1, +1] to match
"no signal = 0" baseline of every other slot. K=3 keeps raw softmax
probabilities [0, 1]. Cold-start sentinel 0.0 for all 3 slots =
"no prediction yet" (mask). The 3-slot natural distribution is more
informative than a scalar.

Dead-code status: producer populates cache every step but
experience_state_gather doesn't read from it yet — state slot 121
still receives K=2's prev_aux_dir_prob write. Phase C-2 swaps the
state gather's source from K=2 cache to K=3 cache (3-slot write).

Why split C into C-1 + C-2: experience_state_gather is a hot-path
kernel with many consumers. Updating it touches training collector,
eval-side backtest evaluator, Rust launcher arg list. C-2 lands that
as an atomic state-semantic flip; C-1 lands the GPU-side scaffolding
independently so the producer chain can be validated first.

Verification:
- cargo check -p ml clean.
- cargo test -p ml --lib → 1016/0 green.

Audit: docs/dqn-wire-up-audit.md Phase C-1 section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 02:26:39 +02:00
jgrusewski
68f0481a9e feat(sp22-vnext): Phase B5a — input concat kernel scaffolding
Lands the plan-conditioning concat kernel for the K=3 trade-outcome
forward as reusable scaffolding. Phase B5b (integration) is deferred
with rationale: Phase C (state slots) is more critical for testing the
K=3 head's effect on policy behavior, and can land independently of
the plan-conditioning refinement.

NEW kernel aux_to_input_concat_kernel.cu:
- Writes [B, SH2+P] from h_s2_aux [B, 256] || plan_params [B, 6]
- Pure GPU map; one thread per output element, no atomicAdd
- NULL-tolerant on plan_params (zeros trailing P cols when source
  unavailable, e.g., collector cold-start where the trade plan head
  doesn't run)
- Registered in build.rs; cubin compiles (5.7 KB). Dead code at this
  commit — no Rust launcher yet.

Why B5 is split + B5b deferred:

Full Phase B5 (integration) requires three coordinated changes:
1. Forward path: bump aux_to_fwd.forward() to SH2=262 + 262-dim input
2. Backward stride mismatch: backward emits dh_s2_aux_to_buf [B, 262],
   but dh_s2_aux_accum (input to aux trunk backward) is [B, 256]. A
   direct SAXPY mismatches row strides (262 vs 256) and corrupts the
   trunk's upstream gradient. Needs a strided-SAXPY kernel.
3. Collector-path plan_params unavailability: trade plan head only runs
   trainer-side. Workarounds: zero-fill, add trade plan to collector,
   or skip K=3 forward in collector. All have trade-offs.

Phase B5b would need (1) strided-SAXPY kernel and (2) collector
plan_params decision. Real work but NOT on the critical path for
testing the K=3 head's effect on WR.

Why Phase C should land first:

The K=3 head currently trains on real labels (post-B4b) but doesn't
influence policy behavior. Phase C wires the head's softmax into state
slots [121..124) = (p_Profit, p_Stop, p_Timeout), replacing the K=2
single-slot 121 = 2*p_up - 1. WITH Phase C the policy reads aux's
outcome predictions as state features → behavior changes → testable.

Without Phase C, validation runs would show "K=3 head trains and
converges" but predictions don't reach the policy → WR signal isn't
a function of K=3 at all. We'd be testing nothing.

Recommendation: skip the full B5 for now, do Phase C next, then Phase
D (atom-shift). Phase B5b (plan-conditioning) is a refinement we add
IF Phase C/D's no-plan-params version shows promise but plateaus
below the WR ≥ 0.55 target.

Audit: docs/dqn-wire-up-audit.md Phase B5a section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 02:19:27 +02:00
jgrusewski
9f4a25e623 feat(sp22-vnext): Phase A5 — aux_trade_outcome backward kernel (Phase A complete)
K=3 backward kernel that closes the forward → loss → backward chain for
the trade-outcome aux head. Mirrors `aux_next_bar_backward` (K=2 sibling)
line-for-line because gradient flow is K-independent: `d_logits = (softmax
− one_hot)/B_valid` propagated through `Linear → ELU → Linear` chain via
standard softmax-CE derivative.

Per-sample partials (caller reduces via existing K-generic `aux_param_
grad_reduce` kernel):
  dW1_partial [B, H=128, SH2=256], db1_partial [B, H]
  dW2_partial [B, K=3, H],         db2_partial [B, K=3]
  dh_s2_aux_out [B, SH2]

Mask handling: labels[b] == -1 zeros the K-vector → all downstream
partials zero (chain rule's multiplicative zero). All-skip batch produces
valid_count=0 → d_logits=0 for every row → zero gradients across the
board, no NaN.

Sparse-label gradient amplification: B_valid is typically ~1-5% of
nominal batch (trade-close events are rare), so inv_B = 1/B_valid is
much larger than the K=2 sibling's inv_B = 1/(~B). Per-trade-close
gradients have proportionally higher magnitude — correct credit
assignment (rare signal speaks louder) but Phase E's Adam may need
class-weighted CE or per-group LR tuning.

ELU backward via post-activation identity: f'(x) = (h_post > 0) ? 1 :
1 + h_post — recovers derivative without re-evaluating x_pre.

SP14 Phase C.5b separation preserved: reads h_s2_aux (aux trunk output),
writes dh_s2_aux_out SAXPYing into dh_s2_aux_accum. Q's encoder
structurally protected (aux_trunk_backward has no dx_in output).

Phase A5 (this commit) is dead code — no Rust launcher. Phase B will
land the full launcher chain (gpu_aux_heads.rs parallel ops struct,
collector struct fields for W1/W2/b1/b2/Adam-state/saved-tensors/dW-
partials, wireup in collect_experiences_gpu).

Cubin: aux_trade_outcome_backward_kernel.cubin (24.8 KB).

═══ Phase A complete ═══

A1: ISV slots (none needed — reuses padding 121-123)
A2: trade_outcome_label_kernel.cu (label producer)            26ce7ba69
A3: aux_trade_outcome_forward + save-for-backward wireup       07728f9ef
A4: aux_trade_outcome_loss_reduce_kernel.cu (sparse CE)        3ddcfb886
A5: aux_trade_outcome_backward_kernel.cu (this commit)

All 5 GPU kernels compile, cubins built, build.rs registered. Contract
chain composes end-to-end on the GPU side. Phase B (Rust launcher
chain + 262-dim input concat) is next.

Audit: docs/dqn-wire-up-audit.md Phase A5 section + Phase A summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:01:39 +02:00
jgrusewski
3ddcfb8868 feat(sp22-vnext): Phase A4 — aux_trade_outcome loss reduce kernel
K=3 sparse cross-entropy reduce over the trade-outcome softmax tile
produced by `aux_trade_outcome_forward` (Phase A3). Mirrors the K=2
sibling `aux_next_bar_loss_reduce` structurally: single-block shmem-tree
reduce, two parallel partial strips (loss_numer + valid_count) reduced
lockstep, fmaxf(p_tgt, 1e-30) numerical floor, fmaxf(valid, 1.0) all-
skip-batch guard, valid_count_out[1] save-for-backward.

Kept as SEPARATE kernel from the K=2 sibling:
- Diagnostic isolation (distinct HEALTH_DIAG slot, distinct cubin in
  profiles for clean per-loss-source attribution)
- Sparse-label semantic clarity (~95-99% mask=-1 vs ~50-100% valid for
  the K=2 next-bar head)
- Future per-class weighting headroom (Profit/Stop/Timeout 3:1-10:1
  imbalance will likely need class-weighted CE — surgical mod here
  without touching the K=2 head's contract)

Phase A4 (this commit) is dead code — no Rust launcher yet. Phase A5
lands backward; Phase B wires the full forward→loss→backward chain.

Discipline: feedback_no_atomicadd (single-block tree-reduce), feedback_
cpu_is_read_only (pure GPU), pearl_first_observation_bootstrap (sentinel
0 valid_count produces zero gradients gracefully on cold start).

Audit: docs/dqn-wire-up-audit.md Phase A4 section.
Cubin: aux_trade_outcome_loss_reduce_kernel.cubin (9.9 KB) compiles clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:41:59 +02:00
jgrusewski
07728f9efc feat(sp22-vnext): Phase A3 — aux_trade_outcome forward kernel + save-for-backward wireup
Phase A3 of the SP22 H6 vNext trade-outcome aux head (per
docs/plans/2026-05-14-sp22-h6-vNext-trade-outcome-aux.md). Two atomic pieces:

1. NEW kernel `aux_trade_outcome_forward_kernel.cu` — K=3 softmax aux head
   forward (Linear → ELU → Linear → stable softmax). Mirrors
   `aux_next_bar_forward` (K=2) but emits {Profit, Stop, Timeout} probs.
   Saved tensors {hidden_out, logits_out, softmax_out} ready for A4 (loss
   reduce) and A5 (backward). Dead code at this commit — no Rust launcher
   yet. Registered in build.rs::kernels_with_common, cubin verified.

2. Save-for-backward buffers `pnl_vs_target_at_close_per_env` +
   `pnl_vs_stop_at_close_per_env` ([alloc_episodes] f32 device-resident).
   Producer: `experience_env_step::segment_complete` writes the trade's
   realized P&L ratios vs profit_target / stop_loss at close (inline-
   computed from `pre_trade_position × (raw_close − entry_price) /
   (ps[PS_PLAN_PROFIT_TARGET] × prev_equity)`, symmetric-clamped to
   [-2, +2] per pearl_symmetric_clamp_audit — same formula as the
   sibling experience_state_gather's plan_isv[PLAN_ISV_PNL_VS_TARGET/_STOP]
   slots). Consumer (eventual A4/A5 wireup): trade_outcome_label_kernel
   classifies each close into {Profit, Stop, Timeout} via the ≥1.0
   threshold-hit predicate.

Wireup discipline per feedback_registry_entries_need_dispatch_arms:
- New struct fields on GpuExperienceCollector
- stream.alloc_zeros at construct site
- Kernel-launch .arg() threading at experience_env_step launch
- StateResetRegistry entries (FoldReset sentinel 0.0)
- training_loop::reset_named_state dispatch arms
- All 10 registry pin tests pass including
  every_fold_and_soft_reset_entry_has_dispatch_arm

Audit doc updated: docs/dqn-wire-up-audit.md Phase A3 section.

Next phases (per spec): A4 = aux_trade_outcome_loss_reduce (sparse CE,
mask=-1), A5 = aux_trade_outcome_backward, Phase B = 262-dim input
(h_s2_aux || plan_params), Phase C = 3-slot state assembly, Phase D =
12-weight W atom-shift, Phase E = dW + Adam, Phase F = validation smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:20:29 +02:00
jgrusewski
26ce7ba690 feat(sp22-vNext): Phase A2 — trade-outcome label producer kernel
First foundation kernel for the H6 vNext trade-outcome aux head.

Per-env classification at trade-close events into K=3 outcomes:
- 0 = Profit (pnl_vs_target >= 1.0)
- 1 = Stop (pnl_vs_stop >= 1.0)
- 2 = Timeout (neither threshold hit)

Sparse labels — most bars get -1 mask. Priority: Profit > Stop > Timeout.

Pure per-env map; no atomicAdd, no reduction. Launch: grid(ceil(N/256)),
block(256).

Registered in build.rs. Cubin compiles clean. Currently dead code —
launcher wireup comes in Phase A3+ commits.

See docs/plans/2026-05-14-sp22-h6-vNext-trade-outcome-aux.md for the
full vNext architecture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:55:34 +02:00
jgrusewski
5d163e0e1d feat(sp22): H6 Phase 3 α — adaptive W via dW backward + Adam (B9 Steps 8+11)
Step 8 — c51_aux_dw_kernel (new):
- Per-action block tree-reduce: grid=(4,1,1), block=(256,1,1). One block
  per W index a, tree-reduces dW[a] across batch via warp shuffle + shmem.
  Zero atomicAdd per pearl_no_atomicadd.
- Per-sample contributions:
    a == a_d: dW[a] += inv_batch × isw × (SP_b/dz) × state_121
    a == a*:  dW[a] += inv_batch × isw × (-γ(1-done)) × (SP_b/dz) × next_state_121

c51_loss_kernel forward — new scratch outputs:
- aux_target_a_dir_buf[B] (i32): saves best_next_a for d==0 after Step c
  sampling.
- aux_proj_logdiff_dir_buf[B] (f32): saves SP_b = Σ_n p_target_n ×
  (current_lp[upper_n] - current_lp[lower_n]) after Step d's projection
  via re-derivation of lower_n/upper_n (matching Huber compression +
  clamp arithmetic of block_bellman_project_f).

Step 11 — adam_w_aux_kernel (new):
- Standard Adam with bias correction, grid=(1,1,1), block=(4,1,1).
- Graph-capture-safe: lr via self.lr_dev_ptr pointer arg; step via
  self.ptrs.t_buf pointer arg (matches main Adam pattern). beta/eps
  as value args from sp5_isv_slots constants.
- Bias-correction denominator floored at 1e-30 to avoid /0.

Trainer wiring (submit_adam_ops):
- launch_c51_aux_dw + launch_adam_w_aux added right after
  launch_adam_update. Both inside the captured adam_child graph.
- New trainer fields: aux_target_a_dir_buf, aux_proj_logdiff_dir_buf,
  c51_aux_dw_kernel, adam_w_aux_kernel. Cubin statics SP22_C51_AUX_DW_CUBIN
  + SP22_ADAM_W_AUX_CUBIN added.

NULL-safety:
- aux_shift_active=false in c51_loss_kernel forward → both scratch
  buffers stay at alloc_zeros 0 → dW reads 0 → Adam W is a no-op.
- aux_target_a_dir_out / aux_proj_logdiff_dir_out are NULL-tolerant.

Deferred (deliberate scope):
- dL/dstate_121 backward (c51 → aux head): refinement, not correctness;
  aux head trains via own supervised CE loss.
- Phase C1 collector W ptr setter.
- Phase D (eval-side aux infrastructure).

Verification:
- cargo build -p ml --lib: 0 errors, 21 pre-existing warnings.
- nvcc full recompile clean (1m05s for sm_89 target).
- All forward atom-shift consumers + adaptive W backward + Adam now wired.

End-state: adaptive W trains from structural prior [-0.5, 0, +0.5, 0]
via projection log-diff gradient. Aux head trains independently via
supervised CE. Together they form learned cross-coupling from aux
direction predictions to dir-branch Q distribution shifts. Smoke can
now measure adaptive W's effect on WR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:11:40 +02:00
jgrusewski
195c051e7a feat(sp22): H6 Phase 3 α — atom-shift wired through compute_expected_q (B9 Step 5+6)
W structural prior init kernel (Step 5):
- aux_w_prior_init_kernel.cu: 4-thread one-time init writing W[a] =
  [-0.5, 0.0, +0.5, 0.0] (Short / Hold / Long / Flat). Hardcoded values
  in kernel — no HtoD per feedback_no_htod_htoh_only_mapped_pinned.md.
- build.rs registration + gpu_dqn_trainer.rs cubin static + handle field
  + new() launch after alloc_zeros for w_aux_to_q_dir.

compute_expected_q atom-shift (Step 6):
- experience_kernels.cu signature grows 4 args (w_aux, batch_states,
  aux_dir_prob_index, state_dim). NULL-safe — collapses to 0 shift
  when either pointer is NULL, bit-identical to pre-Phase-3-α.
- Per-action inner loop computes aux_atom_shift = w_aux[a] * state_121
  once per (b, a) for d==0 only. Inner z-loop applies z_val +=
  aux_atom_shift before all S/TZ/TZ²/TLM accumulators consume it.

Launcher updates (3 trainer sites + 1 collector site):
- populate_q_out: passes W + current states (online path).
- replay_forward_for_q_values: passes W + current states (online replay).
- compute_denoise_target_q: passes W + next_states (target on s'; W
  shared across online/target).
- gpu_experience_collector.rs: passes NULL W + NULL states until
  Phase C1 wires the trainer's W ptr through a setter.

Architectural notes:
- Action selection (every compute_expected_q call) now uses shifted
  atom positions for direction branch (d==0). Other branches stay
  bit-identical (shift = 0). Step 7 (c51_loss_kernel) + Step 8
  (c51_grad_kernel) will close the loop on training loss + W gradient
  in the same atomic commit to avoid the gradient mismatch trap
  per feedback_no_partial_refactor.md.
- Adam wireup for w_aux_to_q_dir lands at Step 11; until then W stays
  at structural prior values (no Adam step modifies it).

Verification:
- cargo check -p ml --lib: 0 errors, 21 pre-existing warnings
  (Phase 3b baseline parity).
- Audit doc updated with B9 Step 5+6 checkpoint entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:41:26 +02:00
jgrusewski
08fd5803c4 refactor(sp22): H6 Phase 3 α cleanup + runbook revision for atom-shift design
Same-session cleanup of Phase 3b scalar-bias residue (commit cb80b74ce's
additions that became architecturally redundant under the 2026-05-13
atom-shift revision). The scalar-bias α design is mathematically
ineffective in C51 distributional Q-learning (softmax-shift-invariance
→ W gradient = 0 → never trains). The revised design threads
`aux_atom_shift[b, a] = W[a] * state_121[b]` through compute_expected_q
+ c51_loss_kernel + c51_grad_kernel + mag_concat_qdir; dW + dstate
gradients integrate into c51_grad_kernel's projection backward.

Files
─────
- crates/ml/build.rs:
    Removed `aux_to_q_dir_bias_kernel.cu` + `aux_to_q_dir_bias_backward_kernel.cu`
    from kernels_with_common. Replaced with comment block documenting
    C51 softmax-invariance reason + spec/audit pointers.

- crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs:
    Removed `pub(crate) static SP22_AUX_TO_Q_DIR_BIAS_CUBIN` and
    `SP22_AUX_TO_Q_DIR_BIAS_BWD_CUBIN` static declarations.
    Removed 3 struct fields (aux_to_q_dir_bias_kernel,
    aux_to_q_dir_bias_backward_dw_kernel,
    aux_to_q_dir_bias_backward_dstate_kernel) and their new()
    loading blocks + struct construction entries.
    KEPT: w_aux_to_q_dir + adam_m_w_aux + adam_v_w_aux + dw_aux_buf
    (still needed for atom-shift Adam-trained W).
    Updated W doc-comment to describe atom-shift threading (was
    scalar-bias) and structural-prior initialization `[-0.5, 0.0,
    +0.5, 0.0]`.

- crates/ml/src/cuda_pipeline/aux_to_q_dir_bias_kernel.cu +
  crates/ml/src/cuda_pipeline/aux_to_q_dir_bias_backward_kernel.cu:
    Source files stay on disk as committed dead code (commit
    464bc5f7a history preserved). nvcc no longer compiles them
    (build.rs unregistered). No `include_bytes!` references remain.

- docs/plans/2026-05-13-sp22-h6-phase3-alpha-beta-runbook.md:
    Tasks A3/A4/A5 marked SUPERSEDED; A5 retains cleanup instructions.
    Task B9 Steps 4-10 rewritten as Steps 4 (cleanup), 5 (W structural
    prior init), 6 (compute_expected_q atom-shift threading),
    7 (c51_loss_kernel), 8 (c51_grad_kernel backward dW + dstate),
    9 (mag_concat_qdir), 10 (quantile_q_select/iqn_dual_head
    investigation), 11 (Adam wireup), 12 (verify + capture).
    Task C1 rewritten: collector passes 4 more args through
    existing compute_expected_q launcher (NO new kernel).

- docs/dqn-wire-up-audit.md:
    Cleanup-commit entry documenting the runbook revision and the
    code-cleanup actions.

Verification
────────────
- cargo check -p ml --features cuda: 0 errors, 21 pre-existing
  warnings (Phase 3b baseline parity).
- Build script no longer compiles the deleted-registration kernels.

Phase 3-final scope (~40-60 hr engineering — unchanged)
───────────────────────────────────────────────────────
Implementation per the revised runbook: atom-shift threading
through 4-5 kernels (compute_expected_q, c51_loss_kernel,
c51_grad_kernel, mag_concat_qdir, possibly quantile_q_select/
iqn_dual_head) + Adam wireup + collector launcher arg passing +
A2 eval-side + SP11 controller extension + HEALTH_DIAG telemetry
+ verification gates + atomic Phase F commit + smoke + verdict.

Refs
────
- docs/plans/2026-05-12-sp22-h6-phase3-alpha-beta.md (spec α section
  revised 2026-05-13 — commit 648078ce2)
- docs/plans/2026-05-13-sp22-h6-phase3-alpha-beta-runbook.md (runbook
  α tasks revised in this commit)
- 464bc5f7a (Phase A — α kernels added; now committed dead code)
- cb80b74ce (Phase 3b — α struct fields + cubin statics added; cleaned
  up in this commit)
- pearl_no_partial_refactor (atom-shift threading is the new atomic
  contract for direction-branch atom positions)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:24:15 +02:00
jgrusewski
464bc5f7a4 feat(sp22): H6 Phase 3 — Phase A foundation (WIP checkpoint, additive)
Phase A of the SP22 H6 Phase 3 implementation runbook
(docs/plans/2026-05-13-sp22-h6-phase3-alpha-beta-runbook.md). Purely
additive: new ISV slots + new kernel files + build.rs registration
+ state_reset_registry entries. Nothing in the running training or
eval pipeline consumes this infrastructure yet — the existing
6-component contract is untouched. Phase A is committable as a clean
WIP foundation; Phases B-F (7-component contract migration, α
plumbing, A2 eval-side wiring, verification, smoke) resume in a
future session.

Files
─────
- crates/ml/src/cuda_pipeline/sp22_isv_slots.rs (NEW)
    REWARD_AUX_ALIGN_EMA_INDEX = 536 — EMA of 7th reward component
    SP22_AUX_ALIGN_SCALE_INDEX  = 537 — SP11-controller scale_β

- crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs
    ISV_TOTAL_DIM bumped 536 → 538 (SP22 H6 Phase 3 +2 slots)
    Layout fingerprint extended with SLOT_536 + SLOT_537 entries

- crates/ml/src/cuda_pipeline/mod.rs
    pub mod sp22_isv_slots

- crates/ml/src/trainers/dqn/state_reset_registry.rs
    sp22_reward_aux_align_ema (FoldReset, sentinel 0)
    sp22_aux_align_scale       (FoldReset, sentinel 0)

- crates/ml/src/cuda_pipeline/aux_to_q_dir_bias_kernel.cu (NEW)
    α forward: Q_dir[b, a] += W_aux[a] * state_121[b]
    Reads state_121 from encoder INPUT (batch_states), bypassing
    the cold encoder weight for slot 121. No atomicAdd, no host
    branches, capture-safe.

- crates/ml/src/cuda_pipeline/aux_to_q_dir_bias_backward_kernel.cu (NEW)
    Two kernels in one .cu:
    - aux_to_q_dir_bias_backward_dw: per-action block tree-reduce
      computing dW[a] = Σ_b state_121[b] * dq_dir[b, a]
    - aux_to_q_dir_bias_backward_dstate: per-sample sum
      dstate_121[b] += Σ_a W[a] * dq_dir[b, a] (option-(i) gradient
      routing: both α and encoder paths get signal)

- crates/ml/build.rs
    Both new cubins registered in kernels_with_common (compiled
    successfully by nvcc; cubins exist at OUT_DIR/aux_to_q_dir_bias_*.cubin)

- docs/dqn-wire-up-audit.md
    Phase A entry documenting the checkpoint + remaining-work
    breakdown for next session.

Verification
────────────
- cargo check -p ml --features cuda: 0 errors, 21 pre-existing
  warnings (Phase 2 baseline parity)
- nvcc compiles both new cubins (3.6 KB fwd, 10.6 KB bwd)
- No runtime impact: no consumer wires the new infrastructure yet

Resumes in
──────────
Future session executes Phases B-F per the runbook:
- B: 11 tasks — 7-component contract migration cascade
- C: 1 task — α collector rollout-side wiring
- D: 7 tasks — A2 eval-side aux trunk + α + state-gather
- E: 7 tasks — verification gates
- F: 5 tasks — audit doc append + atomic commit (incl. this Phase A
  + Phase B-F changes) + push + smoke + verdict

Estimated remaining: ~28-43 hr engineering + ~37 min smoke wall-clock.

Phase B partial work (300-line diff for experience_kernels.cu + reward_component_ema_kernel.cu — 7-stride migration + β producer + preamble update) saved at /tmp/sp22-h6-phase3-b-partial.patch (local-only).

Refs
────
- docs/plans/2026-05-12-sp22-h6-phase3-alpha-beta.md (spec)
- docs/plans/2026-05-13-sp22-h6-phase3-alpha-beta-runbook.md (runbook)
- pearl_no_partial_refactor (Phase A is additive; safe to commit alone)
- pearl_no_atomicadd (backward block tree-reduce, no atomics)
- pearl_no_host_branches_in_captured_graph (kernel capture-safety)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:46:28 +02:00
jgrusewski
7fc9799343 feat(sp22): H6 Phase 1 — aux→policy state bridge (atomic)
Wires the aux head's per-env directional probability into policy STATE
slot 121 (AUX_DIR_PROB_INDEX = PADDING_START + 0), preserving the trunk-
separation invariant from `pearl_separate_aux_trunk_when_shared_starves`.
H1 (label horizon) confirmed aux learns 78% dir-acc within-fold at H=200
but the policy was walled off; this commit conducts that signal through
the state input with a one-step lag.

Mechanism (rollout-time, collector-only)
────────────────────────────────────────
- State[env, t] reads `prev_aux_dir_prob[env]` (= p_up from step t-1).
- After aux forward at step t, the new copy kernel writes
  `aux_softmax[env, 1]` → `prev_aux_dir_prob[env]` for step t+1.
- Cold-start + FoldReset seed the buffer to 0.5 (neutral; p_up = 50%)
  via the pure-GPU `fill_f32` kernel — no HtoD per
  `feedback_no_htod_htoh_only_mapped_pinned.md`.
- Launch sits in the same `isv_signals && trainer_params != 0` gate as
  the aux forward, so when aux is skipped the cache keeps its previous
  (sentinel or last-good) value instead of copying stale `alloc_zeros`.

Three state-gather kernels updated atomically (per
`feedback_no_partial_refactor.md`):
- `experience_state_gather` — training, reads `aux_dir_prob_per_env[i]`
- `backtest_state_gather` — eval (single-step), NULL → 0.5 (A3 fallback)
- `backtest_state_gather_chunk` — eval (chunked), NULL → 0.5 (A3 fallback)

`assemble_state` gained a 7th param `float aux_dir_prob` written to
`out[SL_PADDING_START + 0]`; the remaining 6 padding slots stay zero
for 8-alignment.

Phase 1 scope = training-side + eval A3 NULL fallback. A2 (aux trunk
forward in eval) is deferred per the runbook — gates on whether the
smoke moves WR off the 50.1–50.2% plateau.

New files
─────────
- crates/ml/src/cuda_pipeline/aux_softmax_to_per_env_kernel.cu

Modified
────────
- crates/ml-core/src/state_layout.rs           (+AUX_DIR_PROB_INDEX)
- crates/ml/src/cuda_pipeline/state_layout.cuh (+assemble_state param)
- crates/ml/src/cuda_pipeline/experience_kernels.cu
    (3 state-gather kernels + NULL-defensive sentinel)
- crates/ml/src/cuda_pipeline/gpu_experience_collector.rs
    (per-env buffer + 2 kernel handles + cold-start fill + copy launch
     + FoldReset re-fill + state-gather arg)
- crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs
    (NULL aux_dir_prob_per_env at all 3 launchers for A3)
- crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs
    (SP22_AUX_SOFTMAX_TO_PER_ENV_CUBIN static)
- crates/ml/src/cuda_pipeline/gpu_action_selector.rs
    (EPSILON_GREEDY_CUBIN → pub(crate) so collector reuses fill_f32)
- crates/ml/build.rs                            (register new kernel)
- docs/dqn-wire-up-audit.md
    (## 2026-05-12 — SP22 H6 implementation: Phase 1 entry)

Verification (all three gates clean, no smoke yet)
──────────────────────────────────────────────────
- cargo check -p ml --features cuda: 0 errors
- gpu_backtest_validation: 4/4 expected-passing tests still pass
  (the 2 PnL-assertion failures are pre-existing per the runbook)
- compute-sanitizer --tool=memcheck: 0 CUDA errors

Refs
────
- docs/plans/2026-05-12-sp22-h6-aux-policy-state-bridge.md
- docs/plans/2026-05-12-sp22-h6-next-session-prompt.md
- pearl_separate_aux_trunk_when_shared_starves
- pearl_first_observation_bootstrap (sentinel = 0.5 cold-start)
- feedback_no_htod_htoh_only_mapped_pinned (fill_f32 not HtoD)
- feedback_no_partial_refactor (3 state-gather kernels atomic)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:31:32 +02:00
jgrusewski
06dbb78ffc feat(sp20): Phase 3 Task 3.2 — Hold opportunity-cost dual emission (Component 2)
Lands the per-bar Hold opportunity-cost dual emission at
experience_env_step's per-bar branches (positioned-non-event ~3650 +
flat-non-event ~3737), atomically with the trade-close consumer
wiring at the segment_complete branch (~3289 — replaces the Phase 2
Task 2.2 forward-reference placeholder `hold_baseline = 0.0f`) per
`feedback_no_partial_refactor`.

Spec §4.2 dual-emission contract:

  per_bar_opp_cost = -aux_conf × cost_scale
  Path 1 (Hold-only):  r_micro/r_opp_cost += per_bar_opp_cost
                                            - ISV[HOLD_REWARD_EMA]
                       (centered for Q-target stability)
  Path 2 (always):     hold_baseline_buffer[env, t % 30] = per_bar_opp_cost
                       (uncentered, consumed at trade close)

  At trade close:
    hold_baseline = sp20_sum_hold_baseline_over_trade(buf, env, 30,
                                                      current_t,
                                                      segment_hold_time)
    alpha = R_event - hold_baseline    # replaces Phase 2 placeholder

The summation walks backwards `min(30, segment_hold_time)` slots from
`(current_t - 1) % 30` in env i's row of the per-env circular buffer.
The trade-close bar's segment_complete branch does NOT write to the
buffer, so the most recent per-bar write is at current_t - 1.

Layout decision (plan errata Gap 9): per-env stride
`[N_envs × HOLD_BASELINE_BUFFER_SIZE = 30]` row-major. The kernel is
per-env-parallel; a single global ring would interleave bars across
envs and break trade-attribution semantic.

Trade-open tracking decision (plan errata Gap 10): NO new state slot
needed. The existing `segment_hold_time` (= saved_hold_time captured
before PS_HOLD_TIME reset at experience_kernels.cu:2618) plus
current_t suffice — walk backwards in the buffer.

Replaces SP18 D-leg `compute_sp18_hold_opportunity_cost` calls at
experience_kernels.cu:3672 and :3783. The device function in
trade_physics.cuh:655 is RETAINED — still called by
sp18_hold_opp_test_kernel.cu (oracle test surface). Per
`feedback_no_stubs`: only production callers migrate; the helper
stays for the test surface.

New device helpers (sp20_hold_baseline.cuh):
  - sp20_compute_per_bar_aux_conf_k2(logits) — bit-identical to the
    formula in sp20_stats_compute_kernel.cu Pass A.
  - sp20_sum_hold_baseline_over_trade(buf, env, size, current_t,
    segment_hold_time) — walks backwards with modulo wrap-around.

New buffer + reset infrastructure:
  - GpuExperienceCollector.hold_baseline_buffer field
  - HOLD_BASELINE_BUFFER_SIZE = 30 constant (re-exported)
  - StateResetRegistry FoldReset entry + invariant test
  - reset_named_state dispatch arm

Six new kernel args appended to experience_env_step signature:
aux_logits_per_env, hold_baseline_buffer, hold_buffer_size, aux_k,
hold_cost_scale_idx, hold_reward_ema_idx. All NULL-tolerant.

HOLD_REWARD_EMA forward-reference: ISV[516] is updated by
sp20_emas_compute_kernel reading per_bar_hold_reward from
sp20_aggregate_inputs_kernel which currently emits 0.0f as a Phase
3.2 forward-ref placeholder (line 292). The parallel `sp20-phase-2-fix`
agent owns wiring the real producer; Task 3.2's centering math
references the ISV slot (not a hardcoded 0), so when the parallel
branch lands, EMA starts updating and centering becomes load-bearing
automatically. No additional Task 3.2 work needed post-merge.

Tests (sp20_hold_baseline_test.rs, 4 GPU oracle tests):
  1. aux_conf_k2_bounds_and_fixed_points — bounds, uniform/saturated/
     spec-warmup/spec-confident/symmetry fixed points.
  2. sum_hold_baseline_over_trade_indexing — basic indexing,
     hold_time>size clamp, defensive guards, modulo wrap-around.
  3. sum_hold_baseline_per_env_stride — env-stride correctness across
     row-major buffer.
  4. dual_emission_hold_vs_non_hold — full Path 1 + Path 2 contract,
     mirrors plan §3.2 reference fixture.

Verification:
  SQLX_OFFLINE=true cargo check -p ml --tests --features cuda  # green
  SQLX_OFFLINE=true cargo test -p ml --lib sp20                # 21/21 pass
  SQLX_OFFLINE=true cargo build -p ml                          # cubin compile

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 08:56:19 +02:00
jgrusewski
eaaab152fc feat(sp20): Phase 2 Task 2.1 — sp20_compute_event_reward + 8 GPU oracle tests
Lands the SP20 Component 1 reward math as a header-only `__device__
__forceinline__` function in a new `sp20_reward.cuh` header, plus the
GPU oracle test scaffold for the 4-quadrant truth table per spec §4.1.

* `sp20_reward.cuh` — `sp20_compute_event_reward(close_pnl,
  label_at_open_sign, loss_cap)` function. Returns bounded scalar
  `R_event ∈ [loss_cap, +1.0]`. 4-quadrant table:
    +1, +1 →  +1.0  (right reason, right outcome)
    +1, -1 →  +0.5  (wrong reason, right outcome)
    -1, -1 →  -0.5  (right reason, wrong outcome)
    -1, +1 →  loss_cap  (wrong reason, wrong outcome — ISV-driven)
     0, *  →   0.0  (no information — close_pnl == 0)

  Sentinel `label_at_open_sign == 0` (Task 2.0 contract) maps to
  dir_match=false ⇒ wrong-reason quadrant. Documented as the safer
  default in the header comment.

* `sp20_event_reward_test_kernel.cu` — standalone single-thread test
  wrapper, mirrors the `sp12_reward_math_test_kernel.cu` /
  `thompson_test_kernel.cu` pattern. Outputs to a mapped-pinned [1]
  f32 with `__threadfence_system()` for PCIe-visible coherence.

* `tests/sp20_event_reward_test.rs` — 8 GPU oracle tests
  (`#[ignore = "requires GPU"]`-gated):
    1. quadrant_right_reason_right_outcome
    2. quadrant_wrong_reason_right_outcome
    3. quadrant_right_reason_wrong_outcome
    4. quadrant_wrong_reason_wrong_outcome_loss_cap_at_minus_1
    5. quadrant_wrong_reason_wrong_outcome_loss_cap_at_minus_2
    6. zero_pnl_returns_zero
    7. sentinel_label_winning_trade_lands_shoulder (Task 2.0 contract)
    8. sentinel_label_losing_trade_lands_loss_cap (Task 2.0 contract)

* `experience_kernels.cu` — `#include "sp20_reward.cuh"` so Task 2.2's
  trade-close site replacement has the function in scope.

* `build.rs` — `sp20_event_reward_test_kernel.cu` cubin entry.

No production callers in this commit. Task 2.2 atomically:
  - replaces the SP12 v3 reward block at experience_kernels.cu:3216-3380
    with the SP20 4-quadrant reward;
  - adds the `is_close` consumer of `label_at_open_per_env[i]` (Task
    2.0's per-env scratch);
  - threads per-env alpha through the SP20 aggregation kernel,
    making `alpha_ema` non-zero post-trade-close (replacing the
    Phase 1.4 0.0 forward-reference placeholder).

Per `feedback_no_partial_refactor.md` the device function ships
BEFORE the production caller so Task 2.1's GPU oracle tests can
exercise the math in isolation; the partial refactor that breaks
the no-partial-refactor rule is "feature behind the function with
no caller", which this commit's tests resolve in scope.

Per `feedback_no_cpu_test_fallbacks.md` GPU oracle only — no CPU
reference impl. Per `feedback_no_htod_htoh_only_mapped_pinned.md`
all CPU↔GPU buffers are mapped-pinned (`MappedF32Buffer`).

Verification:
  SQLX_OFFLINE=true cargo check -p ml --tests --features cuda  # clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:59:49 +02:00
jgrusewski
235e838422 feat(sp20): Phase 1.4 (Path C) — kernel-arg refactor + aggregation kernel + wire-up
Atomic Phase 1.4 commit per `feedback_no_partial_refactor`. Wires the
SP20 fused-producer chain (Stats → Aggregate → EMAs → Controllers) into
GpuExperienceCollector's per-rollout-step path with one new aggregation
kernel + Phase 1.2 EMA kernel-signature refactor + production wire-up
+ tests + audit/spec/plan amendments, all in one commit.

## Path C decision rationale

The Phase 1.2 EMA kernel originally took 9 scalar value-args. The Phase
1.4 wire-up site (per-rollout-step in GpuExperienceCollector) needs to
feed per-env GPU-resident trade signals (trade_close_per_sample,
step_ret_per_sample, hold_at_exit_per_sample, packed factored
actions_out) into the kernel — forbidden via host sync
(`feedback_cpu_is_read_only`) and via memcpy_dtoh/htod
(`feedback_no_htod_htoh_only_mapped_pinned`). Path C refactors the EMA
kernel to take a device struct pointer (`SP20EmaInputs* ema_inputs`) +
adds an aggregation kernel that writes the struct on the GPU. Phase 1.2
had zero production consumers yet, so the sig change + Phase 1.4 wire-up
land atomically.

## What this commit contains

1. **EMA kernel signature refactor** (Phase 1.2 → Path C):
   `sp20_emas_compute_kernel.cu` swaps 9 scalar args for a single
   `const SP20EmaInputs* __restrict__ ema_inputs` device pointer.
   Math semantics bit-identical. Rust `EmaInputs` mirrored as
   `#[repr(C)]` byte-for-byte; new `pack_inputs_into_f32_view` helper
   for tests + collectors that fill the struct via a mapped-pinned
   f32-aliased buffer.

2. **New aggregation kernel** (`sp20_aggregate_inputs_kernel.cu`):
   Per-env arrays (sliced to current rollout step) + sp20_stats outputs
   (p50/std) + aux_dir_acc_reduce_kernel output [0] → SP20EmaInputs
   struct. Block tree-reduce (4 stripes × bdim × 4 bytes shmem); no
   atomicAdd. Aggregation rules per spec §4.5:
   - is_close = OR over envs
   - is_win = (≥ 0.5 of closed envs were wins)
   - trade_duration = round(mean(hold_at_exit) over closed envs)
   - action_is_hold = strict majority (count*2 > n_envs) HOLD
   - alpha = 0.0  (Phase 2 forward ref — reward kernel)
   - per_bar_hold_reward = 0.0  (Phase 3.2 forward ref — Hold-cost dual)
   - aux_logits_p50/std/dir_acc forwarded from upstream

3. **Production wire-up in GpuExperienceCollector**:
   4 kernel handles + 4 mapped-pinned buffers (struct fields, alloc,
   init); per-rollout-step launch sequence after env_step (step 5c):
   `Stats → Aggregate → EMAs → Controllers`. All on the same stream;
   stream-implicit producer→consumer ordering. Gated on
   `isv_signals_dev_ptr != 0 && trainer_params_ptr != 0` (matches the
   existing SP14-β EGF chain pattern).

4. **Fold-boundary reset**:
   3 new RegistryEntry records (sp20_ema_inputs_buf,
   sp20_emas_internal_buf, sp20_emas_obs_count_buf) + 13 new dispatch
   arms in training_loop.rs (10 SP20 ISV slot resets + 3 buffer resets).
   Closes the pre-existing `every_fold_and_soft_reset_entry_has_dispatch_arm`
   regression (was failing on fresh check after the SP20 ISV slot
   registrations landed in commit 4249ebc96).

5. **HEALTH_DIAG emit**:
   Per-epoch `HEALTH_DIAG[N]: sp20_isv [loss_cap=… alpha_ema=…
   wr_ema=… hold_cost_scale=… target_hold_pct=… hold_pct_ema=…
   hold_reward_ema=… n_step=… aux_conf_threshold=… aux_gate_temp=…]`
   right after the existing q_disagreement_diag emit.

6. **Tests** (5 test files, all green on RTX 3050 Ti sm_86):
   - sp20_emas_compute_test.rs (4 GPU oracle): updated to use the
     post-Path-C buffer-arg API (mapped-pinned f32-aliased struct).
   - sp20_aggregate_inputs_test.rs (NEW, 6 GPU oracle): aggregation
     rule coverage including the Phase 2 / Phase 3.2 placeholder
     contract.
   - sp20_phase1_4_wireup_test.rs (NEW, 2 GPU oracle): end-to-end
     4-kernel chain integration test.
   - sp20_stats_compute_test.rs (4 GPU oracle): unchanged, regression.
   - sp20_controllers_compute_test.rs (7 GPU oracle): unchanged,
     regression.
   - 18 lib unit tests across the SP20 launchers.

7. **Phase 2 / Phase 3.2 forward references**:
   The `alpha` (Phase 2) and `per_bar_hold_reward` (Phase 3.2) fields
   are emitted as 0.0 placeholders and documented at:
   - `sp20_aggregate_inputs_kernel.cu:46-52` (in-kernel docstring)
   - `sp20_aggregate_inputs.rs:46-49` (launcher docstring)
   - `dqn-wire-up-audit.md` "Phase 2 / Phase 3.2 forward references"
   These are NOT stubs — Phase 2 / 3.2 will replace the kernel's 0.0
   writes with real signals atomically with their respective producers.
   The original Task 2.3 (host-side aggregation) is **subsumed** by
   the GPU-side aggregation kernel.

## Hard rules

- `feedback_no_partial_refactor` — kernel sig change + aggregation
  kernel + production wire-up + tests + audit/spec/plan amendments
  in one atomic commit
- `feedback_no_atomicadd` — aggregation kernel uses block tree-reduce
- `feedback_no_cpu_compute_strict` — every aggregation lives on GPU
- `feedback_no_htod_htoh_only_mapped_pinned` — every Phase 1.4 buffer
  is mapped-pinned with `cuMemHostAlloc(DEVICEMAP)` reachable via
  `dev_ptr`; no memcpy_dtoh/htod
- `pearl_first_observation_bootstrap` — counter-based bootstrap
  preserved; placeholder writes (0.0) keep the ALPHA / HOLD_REWARD
  EMAs at 0.0 sentinel until Phase 2 / 3.2 wire real producers
- `pearl_no_host_branches_in_captured_graph` — every kernel is single-
  block; no host branches; safe to capture in the per-step CUDA Graph
- `pearl_tests_must_prove_not_lock_observations` — integration test
  asserts invariants (slots populate, bounds respected) rather than
  locked observed values

## Verification

- `cargo check -p ml --features cuda` clean
- 18 lib unit tests for SP20 launchers pass
- 23 GPU oracle tests across 5 test files pass on RTX 3050 Ti (sm_86)
- `every_fold_and_soft_reset_entry_has_dispatch_arm` regression test
  now passes (was failing pre-change)
- 14 baseline lib test failures unchanged (none introduced by this
  commit)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:49:07 +02:00
jgrusewski
5ded1cb4b9 feat(sp20): Phase 1.3 sp20_controllers_compute kernel
Component 5 / Kernel 2 of the SP20 fused-producer chain — the
deterministic derivation step. Reads the EMAs Phase 1.2's
sp20_emas_compute wrote (4 ISV + 4 internal scratch slots) and
derives 6 controller outputs into ISV slots:

  - LOSS_CAP (510)            = -1 - clamp((wr-0.50)/0.05, 0, 1)
  - HOLD_COST_SCALE (513)     two-sided ramp around TARGET_HOLD_PCT ±0.05
  - TARGET_HOLD_PCT (514)     = clamp(0.8 - aux_p50_ema*1.5, 0.1, 0.8)
  - N_STEP (517)              = clamp(round(trade_dur_ema), 1, 30) (f32)
  - AUX_CONF_THRESHOLD (518)  = clamp(aux_dir_acc_ema-0.50, 0.01, 0.20)
  - AUX_GATE_TEMP (519)       = max(aux_conf_std_ema, 0.01)

TARGET_HOLD_PCT computed before HOLD_COST_SCALE because the
HOLD_COST_SCALE controller reads tgt; implemented as a local f32
written to ISV then re-used for the controller (no redundant ISV
read-back). HOLD_COST_SCALE is the only output that READS its own
previous ISV value (in-place update); deadband path writes the
unchanged previous value verbatim per feedback_no_stubs.

Single-block, single-thread kernel — captureable in the per-step
CUDA Graph per pearl_no_host_branches_in_captured_graph. ISV +
internal pointers are mapped-pinned device pointers (existing
buffers from Phase 1.2); kernel emits __threadfence_system() for
PCIe-visible coherence.

Pearls + invariants honoured:
  - pearl_controller_anchors_isv_driven: every output's anchor /
    target / cap derives from EMAs; only spec-frozen ramp / clamp
    parameters are compile-time constants.
  - feedback_isv_for_adaptive_bounds: these 6 outputs ARE the
    adaptive bounds.
  - feedback_no_atomicadd, feedback_no_cpu_compute_strict,
    feedback_no_htod_htoh_only_mapped_pinned, feedback_no_stubs.
  - feedback_no_partial_refactor: kernel + launcher + tests + build
    entry land atomically; production wire-up lands in Phase 1.4.
  - pearl_tests_must_prove_not_lock_observations: 7 GPU oracle
    tests assert clamp boundaries, formula correctness, and
    bidirectional ramp behavior — NOT specific observed values.

Tests (all #[ignore = "requires GPU"], pass on RTX 3050 Ti sm_86):
  1. loss_cap_ramp_boundaries — 4 wr_ema sweeps incl. clamps.
  2. n_step_bounds — round + clamp [1, 30].
  3. aux_conf_threshold_bounds — clamp [0.01, 0.20].
  4. aux_gate_temp_floor — max(std, 0.01).
  5. target_hold_pct_inverse_relation — 0.8 − p50*1.5 with clamps.
  6. hold_cost_scale_two_sided_ramp — up/down/deadband + clamps.
  7. all_six_outputs_written_in_one_launch — guards missed writes.

Plus 3 launcher unit tests (slot range, slot uniqueness, ISV input
slot range).

Verification:
  SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check -p ml --features cuda
  SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml \
    --test sp20_controllers_compute_test --features cuda \
    -- --ignored --nocapture
  → 7/7 GPU oracle tests pass; 3/3 launcher unit tests pass.

Phase 1.4 will land the production caller atomically alongside
Kernel 1 + Kernel 3 launches on the same stream in order
Stats → EMAs → Controllers per the SP20 design.
2026-05-09 19:09:06 +02:00
jgrusewski
71aade18cf feat(sp20): Phase 1.2 sp20_emas_compute kernel
Component 5 / Kernel 1 of the SP20 design — central state-tracker
that updates 8 Wiener-α EMAs per training step:

  - 4 ISV slots:    ALPHA_EMA (511), WR_EMA (512),
                    HOLD_PCT_EMA (515), HOLD_REWARD_EMA (516)
  - 4 internal:     trade_duration_ema, aux_conf_p50_ema,
                    aux_conf_std_ema, aux_dir_acc_ema (private
                    scratch consumed by Phase 1.3 controllers)

Per-EMA i32 observation counters (mapped-pinned obs_count[8])
handle the pearl_first_observation_bootstrap sentinel transition
correctly even when 0.0 is a legitimate observation (e.g.,
first-loss WR=0). count==0 ⇒ replace, count>0 ⇒ Wiener-blend at
α = 0.4 (WIENER_ALPHA_FLOOR per
pearl_wiener_alpha_floor_for_nonstationary).

Phase 1.2 lands kernel + launcher + 5 tests (4 GPU oracle, 1
floor lock) + build entry + audit doc atomically per
feedback_no_partial_refactor. Production wire-up (Phase 1.4)
deferred — kernel is dead code until then.

Verified on RTX 3050 Ti (sm_86):
  - 4 GPU oracle tests pass: first_observation_replaces_sentinel,
    wiener_alpha_converges_to_long_run_mean,
    hold_reward_ema_gated_on_hold_bars,
    per_step_emas_fire_unconditionally
  - 4 launcher unit tests pass (constants + struct sanity)
  - 1 floor-lock test pass (WIENER_ALPHA_FLOOR == 0.4)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:57:30 +02:00
jgrusewski
de922c6a4a feat(sp20): Phase 1.1 sp20_stats_compute kernel
Component 5 / Kernel 3 of the SP20 fused-producer chain. Single-block
BLOCK=256 kernel reads `aux_logits [B, 3]` (the SP14-C aux head's
3-class direction logits) and emits `[aux_conf_p50, aux_conf_std]`
into a `MappedF32Buffer<2>`, where the per-row signal is
`aux_conf[i] = max_c softmax(logits[i, *])[c] - 1/3`.

p50 uses the inlined `sp4_histogram_p99` pattern (per-warp tile
binning + cumulative-from-bottom, no atomicAdd per
`feedback_no_atomicadd`); std uses two block tree-reductions sharing
one shmem tile sequentially. One fused kernel streams `aux_logits`
once for both stats per `pearl_fused_per_group_statistics_oracle`.

Phase 1.4 wires the production launch site atomically with the rest
of the SP20 reward chain per `feedback_no_partial_refactor`. This
commit lands kernel + Rust launcher + GPU oracle tests + build entry
+ audit-doc entry together so the kernel is independently verifiable
on RTX 3050 Ti (sm_86) and L40S (sm_89) before the EMA + controller
producers (Phase 1.2 + 1.3) reference its outputs.

Tests verify:
  - uniform logits → aux_conf = 0 → [p50, std] = [0, 0]
  - varied confidence (logit ramp 0 → 3) → matches CPU oracle
  - heterogeneous half-hot half-uniform → matches CPU oracle
  - empty batch → degenerate-guard writes [0, 0]

All 4 GPU oracle tests + 4 launcher unit tests pass on RTX 3050 Ti.
Test data uses per-row variance to avoid the
`pearl_sp4_histogram_warp_tile_undercount` lockstep-uniform trap
(concentrated values within one bin_width race the per-warp
non-atomic increments).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:43:11 +02:00
jgrusewski
5ea5aa9b8e feat(sp18 v2 P3.T1-T5): adaptive HOLD_REWARD_POS/NEG_CAP producer kernel
Lifts the Phase 2 caps from sentinel-driven cold-start (5.0/-10.0
fixed) to p99(|step_ret|) over Long/Short trade closes × 1.5 safety
factor with Wiener-optimal alpha blend. The Phase 2 consumer
(compute_sp18_hold_opportunity_cost) sees producer-driven slots
[483]/[484] from epoch 1 onward; sentinel branch in the consumer
remains as the cold-start fallback path (zero-trade-close epoch ⇒
producer early-returns ⇒ slots stay at sentinel ⇒ consumer falls
through to the +5/-10 macro defaults — bit-identical to pre-Phase-3).

Mirrors SP14 P0-A reward_cap_update_kernel structural template with
three differences: (1) filter (is_close && step_ret != 0) — both
winners AND losers (the consumer needs the magnitude scale of *all*
Long/Short closes); (2) Welford-derived Wiener-α (slots [487..493))
replaces fixed α=0.01, with floor at WELFORD_ALPHA_MIN=0.4 per
pearl_wiener_alpha_floor_for_nonstationary (the policy-realised
distribution is intrinsically non-stationary as the policy adapts);
(3) bounds [0.5, 50.0] (vs. position-side [1.0, 50.0]).

Atomic single-commit per feedback_no_partial_refactor:
- crates/ml/src/cuda_pipeline/hold_reward_cap_update_kernel.cu (NEW)
- crates/ml/build.rs cubin manifest entry
- HoldRewardCapUpdateOps in gpu_aux_trunk.rs (new struct + impl)
- HOLD_REWARD_CAP_UPDATE_CUBIN static + struct field +
  launch_hold_reward_cap_update method + constructor instantiation +
  field-init in gpu_dqn_trainer.rs (5 sites)
- Per-epoch boundary launch in training_loop.rs right AFTER
  launch_reward_cap_update (shared step_ret/trade_close source buffers,
  independent ISV slot pairs)
- HEALTH_DIAG[N]: hold_reward_cap [pos={:.4} neg={:.4} fire_rate={:.4}]
- 3 GPU oracle tests (T5 producer-drives-slots, Pearl-A REPLACE,
  no-closes preserves-isv) — all pass on local RTX 3050 Ti
- Phase 3 close-out sections in docs/sp18-wireup-audit.md and
  docs/dqn-wire-up-audit.md

Pearls applied: feedback_no_atomicadd, pearl_first_observation_bootstrap,
pearl_wiener_optimal_adaptive_alpha, pearl_wiener_alpha_floor_for_nonstationary,
pearl_no_host_branches_in_captured_graph, pearl_symmetric_clamp_audit,
pearl_audit_unboundedness_for_implicit_asymmetry (NEG = -2 × POS at
producer time, single source of truth), feedback_isv_for_adaptive_bounds,
pearl_fused_per_group_statistics_oracle.

Validation: cargo check --workspace clean; 3 GPU oracle tests pass on
local RTX 3050 Ti; scripts/audit_sp18_consumers.sh --check exits 0
(no fingerprint drift in tracked sections).

Plan: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md
Phase 4-5 (B-leg target-net forward + q_next replacement) follows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:05:03 +02:00
jgrusewski
e877e8aefe chore(sp18): archaeology cleanup — strip post-deletion markers + ISV_TOTAL_DIM giant docstring
Pure-comment follow-up to SP18 Phase 1 atomic deletion (3c318953a) and
Phase 2 device-fn introduction (8da8e2e58). Strips the "DELETED by SP18
Phase 1" markers + "RETIRED" docstrings + per-slot SP3→SP18 archaeology
that survived the focused deletion diff. Per feedback_no_legacy_aliases
("avoid backwards-compatibility hacks like removed-comment markers")
and CLAUDE.md ("don't add comments that explain what the code does"),
git history is the historical record — code shouldn't narrate its own
deletion.

Three categories cleaned:

1. ISV_TOTAL_DIM giant docstring in gpu_dqn_trainer.rs (Category 1):
   collapsed ~5000-char slot-by-slot SP3→SP18 history block into a
   13-line conceptual definition pointing readers at sp14_isv_slots.rs
   for the actual range definitions. Layout fingerprint seed
   (RESERVED_GAP_461_TO_468=SP18_P1_RETIRED;) is load-bearing and
   preserved.

2. RETIRED markers in sp14_isv_slots.rs + state_layout.cuh
   (Category 2): deleted "SP16 Phase 2 RETIRED 2026-05-09" header
   blocks, "HCS_* slots [462..468) RETIRED" markers, "SP13 [380..383)/
   SP16 [461..474) ALLOCATED but RETIRED per PP.4" prose, and the
   test-removal note before the surviving MHT block lock test.
   Tightened the SP16 T3 Wiener-α comment block to describe only the
   surviving MIN_HOLD_TEMPERATURE producer.

3. Stale doc-refs across the SP18 audit consumer set (Category 3):
   updated gpu_experience_collector.rs prose, sp5_isv_slots.rs SP13 v3
   description, gpu_dqn_trainer.rs slot-380 constructor block,
   state_reset_registry.rs HRC_*/MHT_*/TDB_* "mirrors slot N" refs +
   the registry-block introduction comment + the deleted-test marker
   at end of file, training_loop.rs launcher-call/diag-emit/
   dispatch-arms "DELETED" markers, build.rs cubin manifest "DELETED"
   marker, gpu_aux_trunk.rs trailing "HoldCostScaleUpdateOps DELETED"
   block, and tests/sp14_oracle_tests.rs leading "tests DELETED" marker.

Production-source grep verification (post-cleanup): zero matches for
HOLD_COST_SCALE_INDEX | HCS_TARGET | HCS_DIFF | HCS_PREV | HCS_SAMPLE
| HOLD_COST_CONTROLLER_GAIN | HOLD_COST_FLOOR_RATIO |
HOLD_COST_CEIL_RATIO | hold_cost_scale_update across crates/ml/src/
*.rs/*.cu/*.cuh.

Audit doc + script preserved per design:
- scripts/audit_sp18_consumers.sh grep patterns retain HOLD_COST_SCALE
  / HCS_* — those are the diagnostic tool.
- docs/sp18-wireup-audit.md historical sections retain references
  (audit doc IS the design-history record); fingerprint section
  regenerated to reflect post-cleanup hit counts (audit --check passes).
- docs/superpowers/specs/* + docs/superpowers/plans/* unchanged
  (spec/plan files retain HOLD_COST_SCALE references unchanged per
  feedback_trust_code_not_docs corollary — those are the past
  design-decision record).
- docs/dqn-wire-up-audit.md current-state header section gains a
  2026-05-09 archaeology-cleanup-pass entry citing the affected
  files + verification (per Pre-commit Invariant 7).

Verification:
- cargo check --workspace: clean (warnings only — pre-existing).
- cargo test -p ml --lib --no-run: compiles clean.
- cargo test --doc -p ml: 21 failures pre-existing (verified via stash;
  identical 21 failures on parent commit 8da8e2e58 — unrelated to
  this cleanup).
- bash scripts/audit_sp18_consumers.sh --check: passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:43:11 +02:00
jgrusewski
8da8e2e584 feat(sp18 v2 P2.T1-T5): structural Hold opportunity-cost device fn + 3-site migration (INTERIM STATE LIFTED)
Atomic introduction of `compute_sp18_hold_opportunity_cost` device function
in `trade_physics.cuh` + migration of the 3 placeholder-commented per-bar
Hold sites in `experience_kernels.cu` + CPU/GPU oracle tests + test-wrapper
cubin manifest entry — single atomic commit per `feedback_no_partial_refactor`.

Closes the SP18 INTERIM STATE introduced by Phase 1 (3c318953a). The reward
function is now structurally complete: no placeholder sites, no orphaned
consumers. Wire-up audit fingerprint stable; production-call count for the
new device fn = 3 (segment_complete + per-bar positioned-Hold + per-bar
flat-Hold), test-wrapper count = 1, definition count = 1.

Sites:
- trade_physics.cuh: new device fn after `compute_lump_sum_opp_cost`.
  Returns 0 for `dir_idx != DIR_HOLD`. Sign convention DD5(b) MIRRORED:
  positive next_log_return → negative Hold reward (opp cost paid for
  sitting out a winner); negative next_log_return → positive Hold reward
  (correctly avoided a loser). Asymmetric-capped via the existing
  `compute_asymmetric_capped_pnl` primitive against ISV[483]/[484]; output
  multiplied by `shaping_scale` (validation = 0 → returns 0). No new
  infrastructure beyond existing primitives.
- experience_kernels.cu Site 1 (segment_complete ~3143): unconditional
  add into `base_reward` BEFORE the SP12 asymmetric cap; preserves SP12
  cap interaction.
- experience_kernels.cu Site 2 (per-bar positioned-Hold ~3641):
  unconditional add into `r_micro` (rc[3]) — same component the deleted
  SP13/SP16 subtraction wrote to, preserving SP11 reward-subsystem
  controller signal attribution. `vol_proxy` recomputed locally from
  `features[bar_idx * market_dim + 9]` (per-bar branches are outside
  the segment_complete `vol_proxy` scope).
- experience_kernels.cu Site 3 (per-bar flat-Hold ~3760): unconditional
  add into `r_opp_cost` (rc[4]) — same component the deleted subtraction
  wrote to. `vol_proxy` recomputed locally with the same pattern.

Each site reads ISV[HOLD_REWARD_POS_CAP_INDEX=483] /
ISV[HOLD_REWARD_NEG_CAP_INDEX=484] with sentinel-or-out-of-bounds guard
falling through to SENTINEL_HOLD_REWARD_POS/NEG_CAP=+5/-10 macros. Bit-
identical to a fixed +5/-10 cap during cold-start and Pre-Phase-3 (Phase 3
lands the producer kernel `hold_reward_cap_update_kernel`).

Tests:
- T1 CPU oracle (sp18_hold_reward_oracle_tests.rs): 6 cases covering
  positive/negative log-return, dir-gate (early-exit for non-HOLD),
  positive saturation (clamps at +pos_cap), negative saturation (clamps
  at -neg_cap), shaping_scale=0 validation mode (returns 0).
- T2 GPU oracle (sp18_hold_reward_oracle_tests.rs::gpu): same 6 cases via
  `sp18_hold_opp_test_kernel.cu` wrapper; bit-equality to CPU at ε=1e-5.
- sp18_hold_opp_test_kernel.cu: single-thread single-block test wrapper
  mirroring `sp12_reward_math_test_kernel.cu` structure. Mapped-pinned
  f32 input/output per `feedback_no_htod_htoh_only_mapped_pinned`;
  __threadfence_system() for PCIe visibility before host read.
- build.rs: cubin manifest entry for sp18_hold_opp_test_kernel.cu.

Validation:
- SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace clean.
- cargo test -p ml --lib sp18: 4 SP18 lib tests pass (slot layouts,
  fold-reset entries-present).
- cargo test -p ml --lib state_reset_registry: 5 tests pass (including
  every_fold_and_soft_reset_entry_has_dispatch_arm).
- cargo test -p ml --test sp18_hold_reward_oracle_tests --features cuda
  -- --include-ignored: 7 tests pass (2 CPU + 5 GPU on local RTX 3050 Ti),
  including the new compute_sp18_hold_opportunity_cost_cpu_oracle and
  compute_sp18_hold_opportunity_cost_gpu_oracle.
- bash scripts/audit_sp18_consumers.sh --check: exit 0 (post-Phase-2
  fingerprint snapshotted in docs/sp18-wireup-audit.md).
- Wire-up audit: `grep -rn "compute_sp18_hold_opportunity_cost" crates/ml/`
  shows 1 device-fn definition + 3 production call sites + 1 test-wrapper
  kernel + CPU/GPU oracle tests. No orphans.

Audit fingerprint diff (Phase 1 → Phase 2):
- `Slot 380 (HOLD_COST_INDEX) consumers`: experience_kernels.cu hit count
  1 → 0 (placeholder-comment reference to HOLD_COST_INDEX removed; new
  Phase 2 calls reference HOLD_REWARD_POS/NEG_CAP_INDEX=483/484 instead);
  doc references in dqn-wire-up-audit.md grew by 2 lines (Phase 2
  close-out narrative). Total section count 42 → 43.
- All other sections unchanged — Phase 2 introduces forward-tracking
  references on the SP18-bound side (slots 483/484, new device fn), not
  the deleted-chain side.

Discipline:
- feedback_no_partial_refactor: device fn + 3 call sites + tests + cubin
  manifest in same commit; INTERIM STATE LIFTED in same commit.
- feedback_no_atomicadd: device fn is per-thread sample-local register
  arithmetic; test wrapper is single-thread single-block, no shared mem.
- feedback_no_htod_htoh_only_mapped_pinned: GPU oracle test uses
  MappedF32Buffer.
- feedback_isv_for_adaptive_bounds: pos/neg caps read from ISV[483]/[484]
  with sentinel fallback; Phase 3 producer `hold_reward_cap_update_kernel`
  drives them adaptively.
- feedback_wire_everything_up: device fn + 3 call sites + test wrapper +
  CPU/GPU oracle tests + cubin manifest all in same commit.
- pearl_one_unbounded_signal_per_reward: only `vol_normalized_return` is
  unbounded; everything else in [neg_cap, pos_cap] × shaping_scale.
- pearl_first_observation_bootstrap: sentinels (5.0/-10.0) match SP14
  P0-A REWARD_POS/NEG_CAP_ADAPTIVE for bit-identical cold-start.

Plan: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md
Phase 2 Tasks 2.1-2.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:16:49 +02:00
jgrusewski
3c318953a7 feat(sp18 v2 P1.T3-T6): atomic delete SP13/SP16 hold_cost_scale chain
Deletes the entire reactive Hold-cost controller chain from SP13 P0a / SP16
P2 / SP16 T3 atomically — replaced by the SP18 D-leg structural opportunity-
cost reward landing in Phase 2.

Per DD7(c): observation chain preserved (slots 380, 382). Slot 380 becomes
constructor-init-only diagnostic at HOLD_COST_BASE=0.005, never updated.

18 consumer sites deleted (8 from plan checklist + 10 from A1-A10 audit
expansion):

Production code:
  - 3 reward subtraction sites (replaced with Phase 2 placeholder comments)
  - hold_cost_scale_update_kernel.cu (file deleted)
  - HoldCostScaleUpdateOps struct in gpu_aux_trunk.rs (~120 lines)
  - launch_hold_cost_scale_update in gpu_dqn_trainer.rs (forwarder)
  - Host controller block in training_loop.rs (~60 lines)
  - Slot constants [461..468) in sp14_isv_slots.rs
  - HOLD_COST_CONTROLLER_GAIN/FLOOR/CEIL constants in sp13_isv_slots.rs
  - state_layout.cuh mirror constants for [461..468)
  - build.rs cubin manifest entry
  - 7 fold-reset registry entries + 7 dispatch arms

Tests:
  - sp14_oracle_tests.rs lines 2173-2926 (11 tests + helpers, ~750 lines)
    — these include_bytes! the deleted cubin and cannot survive
  - lock_sp18_v2_pp4_retired_chain test (deleted, no inverse-contract
    replacement — locking a deletion is pointless ceremony per user call)

Layout fingerprint: range [461..468) is RESERVED gap (SP14-C.1 pattern;
ISV_TOTAL_DIM stays at 507 for checkpoint compatibility). Layout fingerprint
seed updated atomically: HOLD_COST_SCALE + HCS_* slots replaced with
RESERVED_GAP_461_TO_468=SP18_P1_RETIRED marker.

INTERIM STATE: 3 reward sites are now missing the per-bar Hold cost. This
is forbidden to L40S-dispatch until Phase 2 lands the
compute_sp18_hold_opportunity_cost device fn that replaces the deleted
subtraction. The audit doc records this hold.

Validation: cargo check --workspace clean; cargo test -p ml --lib passes
(state_reset_registry::every_fold_and_soft_reset_entry_has_dispatch_arm,
sp16_t3_wiener_welford_slot_layout_locked, sp18_combined_slot_layout_locked,
sp18_shrink_perturb_slot_layout_locked, sp18_fold_reset_entries_present —
all OK). Audit fingerprint: per-bar Hold-cost subtraction sites count
13 → 0 (smoking gun for complete deletion).

Plan: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md
Audit: docs/sp18-wireup-audit.md (full A1-A10 expansion + post-deletion
       fingerprint)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:51:03 +02:00
jgrusewski
aab13a83f2 feat(sp18-v2): weight-drift HEALTH_DIAG diagnostic
Add per-epoch HEALTH_DIAG line measuring how far current online params
have drifted from the best-Sharpe checkpoint. The fold-transition
restore-best fix preserves peak weights across folds, but within-fold
edge-decay still happens (Adam keeps stepping after peak Sharpe). The
drift diagnostic surfaces the trajectory so operators can correlate
Sharpe-peak decay with parameter movement.

HEALTH_DIAG line:
  HEALTH_DIAG[N]: weight_drift [norm_l2={:.6} relative={:.6} branch_max={:.6}]

Where:
- norm_l2  = ||params_flat - best_params||₂            (absolute L2)
- relative = norm_l2 / max(||best_params||₂, EPS)      (relative drift)
- branch_max — currently mirrors `relative` (single-scalar form per spec).
  Per-branch breakdown is a deferred follow-up: branch heads are
  protected from S&P (skip_start..skip_end), so trunk drift dominates
  the L2 in any case.

Cold-start: when best_params_snapshot is None (first fold or any fold
without a Sharpe improvement yet), launcher emits [0.0, 0.0] directly
(kernel skipped, mapped-pinned host slice written from CPU).
Distinguishable from "snapshot matches current" via best_epoch elsewhere.

Kernel (weight_drift_diag_kernel.cu): single block × 256 threads. Two
block tree-reductions sharing one shmem tile sequentially:
  Pass 1: ||params - best||₂² over (params - best)
  Pass 2: ||best||₂²         over best
Thread 0 finalizes sqrt + EPS-floored ratio + writes via
__threadfence_system() for PCIe-visible coherence.

Per feedback_no_atomicadd — block tree-reduce only.
Per feedback_no_htod_htoh_only_mapped_pinned — output is
MappedF32Buffer<2>; cold-start bypass uses host_slice_mut.

Wire-up (atomic):
- crates/ml/build.rs: cubin manifest entry
- crates/ml/src/cuda_pipeline/weight_drift_diag_kernel.cu (NEW)
- crates/ml/src/trainers/dqn/fused_training.rs: field + constructor
  + launch_weight_drift_diag() + read_weight_drift_diag()
- crates/ml/src/trainers/dqn/trainer/mod.rs:
  DQNTrainer::read_weight_drift_diag() public wrapper (CPU-only path
  emits zeros)
- crates/ml/src/trainers/dqn/trainer/training_loop.rs: per-epoch
  HEALTH_DIAG emit adjacent to SP17 dueling block

GPU oracle test (crates/ml/tests/sp18_weight_drift_test.rs, NEW):
4 cases, all #[ignore = "requires GPU"]:
  1. weight_drift_matches_cpu_oracle_4096 — N=4096, ε=1e-5
  2. weight_drift_is_zero_when_params_equals_best
  3. weight_drift_eps_floor_when_best_is_zero (catches NaN/Inf edge)
  4. weight_drift_n_zero_emits_zeros (degenerate guard)
All 4 pass on local RTX 3050 Ti.

Pre-commit Invariant 7: docs/dqn-wire-up-audit.md updated with kernel
algorithm, wire-up table, cross-pearl invariants, and oracle-test
inventory.

Per:
- feedback_no_atomicadd (block tree-reduce in drift kernel)
- feedback_no_htod_htoh_only_mapped_pinned (MappedF32Buffer<2>)
- feedback_wire_everything_up (kernel + launcher + emit + test atomic)
- pearl_no_host_branches_in_captured_graph (cold-path, outside graph)
- feedback_no_partial_refactor (single atomic commit)
2026-05-09 01:47:48 +02:00
jgrusewski
27f8e332da plan(sp18 v2): Phase 0 Task 0.2 — B-leg V_SHARE trajectory + TD-error magnitude diagnostic
Adds the SP18 v2 Phase 0 B-leg observability scaffold per the plan's
Task 0.2 spec:

- New kernel `td_error_mag_ema_kernel.cu` (single-block × 256 threads):
  reads `td_errors_buf [B]` (already populated by C51/MSE loss for PER
  priority recomputation, post-train-step), block tree-reduces
  `mean(|td_errors[b]|)` (no atomicAdd), and EMA-blends into
  `ISV[TD_ERROR_MAG_EMA_INDEX=493]` via Pearl-A first-observation
  bootstrap (sentinel 0.0 → REPLACE) + fixed α=`WELFORD_ALPHA_MIN=0.4`
  per `pearl_wiener_alpha_floor_for_nonstationary`. The TDB_* Welford
  accumulators in slots [498..504) are RESERVED for the Phase 4
  q_next_target Wiener-α chain — not used in Phase 0.

- Cubin manifest entry in `crates/ml/build.rs` + `TD_ERROR_MAG_EMA_
  CUBIN` re-export in `gpu_dqn_trainer.rs`.

- `sp18_td_error_mag_ema_kernel` field on `GpuDqnTrainer` + cubin load
  on the trainer's stream + `launch_sp18_td_error_mag_ema_update()`
  cold-path launcher + `read_sp18_td_error_mag_ema()` convenience
  wrapper.

- New `sp18_v_share_history: [[f32; 4]; 5]` field on `DQNTrainer` —
  fixed-size ring buffer of the last 5 epochs of per-branch V_SHARE
  EMA readings (slots [478..482) per SP17 Phase 3.2). Initialised to
  `[[NaN; 4]; 5]`; epochs 0–3 emit `nan` as the slope and skip the
  ISV write; epoch 4 onward computes `(EMA[now] - EMA[now-4]) / 4`
  per branch and writes the dir-branch slope to
  `ISV[V_SHARE_TREND_DIAG_INDEX=496]`.

- Two new HEALTH_DIAG lines in `training_loop.rs` at the per-epoch
  boundary (right after the SP18 reward_decomp line):

    HEALTH_DIAG[N]: v_share_traj [dir_slope=X mag_slope=Y ord_slope=Z urg_slope=W]
    HEALTH_DIAG[N]: td_error_pre [magnitude_ema=X]

  V_SHARE slope is host-side computation against the ring buffer
  (`(now - now_m4) / 4` per branch). TD-error magnitude is post-blend
  ISV slot 493 read (producer fires inside `read_sp18_td_error_mag_
  ema()`). Pre-fix baseline for the B-DD9 ratio gate
  (`avg(|TD-error|) ratio post-fix / pre-fix ∈ [0.5, 5.0]`).

- New GPU oracle tests in `crates/ml/tests/sp18_hold_reward_oracle_
  tests.rs`:
  * `td_error_mag_ema_pearl_a_bootstrap` — synthetic td_errors with
    closed-form mean(|td|)=1.125; pre-populate slot at sentinel;
    assert post-launch slot equals the mean (Pearl-A direct-replace).
  * `td_error_mag_ema_blend_post_bootstrap` — synthetic td_errors
    with mean(|td|)=0.5; pre-populate slot at non-sentinel 1.0;
    assert blend equals `(1 - 0.4) × 1.0 + 0.4 × 0.5 = 0.8`.

Pure observability — no production-path consumer in this commit. No
reward changes, no Bellman target changes, no kernel modifications to
the action-selection or training paths. Per `feedback_no_partial_
refactor` the kernel + cubin manifest + buffer + launcher + ring
buffer + HEALTH_DIAG emit + GPU oracle tests all land atomically.

Verification:
  SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace clean.
  All 4 GPU oracle tests pass on RTX 3050 Ti (2.09s):
    reward_decomp_per_action_gpu_oracle, reward_decomp_empty_bin,
    td_error_mag_ema_pearl_a_bootstrap, td_error_mag_ema_blend_post_bootstrap.
  Existing slot lock + state_reset_registry tests still pass.

Plan: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md
      § Phase 0 Task 0.2.
Audit: docs/dqn-wire-up-audit.md § "SP18 v2 Phase 0 Task 0.2".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:18:15 +02:00
jgrusewski
15b50ac38f plan(sp18 v2): Phase 0 Task 0.1 — D-leg per-action reward decomposition diagnostic
Adds the SP18 v2 Phase 0 D-leg observability scaffold per the plan's
"Phase 0 — diagnostic emit (NO functional change)" task:

- New kernel `reward_decomp_diag_kernel.cu`: block tree-reduce
  (4 blocks × 256 threads, one block per direction-axis bin) reading
  `reward_components_per_sample [N×6]` + `actions_out [N]` and emitting
  5 per-bin stats (mean r_micro / mean r_opp_cost / mean r_popart /
  mean |reward| / fire_rate) into a 20-float row-major output. Bin
  order: Hold(0)→Long(1)→Short(2)→Flat(3); col order: micro→opp→
  popart→abs→fire. Empty-bin guard emits 0.0 (NOT NaN) per the
  consumer-side KILL CRITERION arithmetic contract.

- Cubin manifest entry in `crates/ml/build.rs` + `REWARD_DECOMP_DIAG_
  CUBIN` re-export in `gpu_dqn_trainer.rs`.

- 20-float `MappedF32Buffer sp18_reward_decomp_diag_buf` field on
  `GpuDqnTrainer` + accessor pair (`sp18_reward_decomp_diag_dev_ptr`
  for the writer-side launcher; `read_sp18_reward_decomp_diag` for the
  HEALTH_DIAG reader). Buffer is constructor-zeroed so cold-start
  HEALTH_DIAG emits a deterministic zero block.

- `sp18_reward_decomp_diag_kernel` field on `GpuExperienceCollector` +
  cubin load on the collector's stream + `launch_sp18_reward_decomp_
  diag(n, b1, b2, b3, out_dev_ptr)` launcher. Wired in
  `training_loop.rs` at the per-step boundary, BEFORE
  `launch_reward_component_ema_inplace` (which `memset_zeros` the
  source buffer after consuming it) per `pearl_canary_input_freshness_
  launch_order`.

- New per-epoch HEALTH_DIAG line emit at the existing per-epoch
  boundary (after the SP17 dueling line):

    HEALTH_DIAG[N]: reward_decomp [hold(micro=X opp=Y popart=Z abs=W
                    fire=F) long(...) short(...) flat(...)]

  Reads the mapped-pinned 20-float diag buffer directly via the
  collector→trainer host_ptr — no DtoH copy.

- New `crates/ml/tests/sp18_hold_reward_oracle_tests.rs`:
  * `reward_decomp_per_action_cpu_oracle` (CPU oracle pinning the
    per-bin reduction math against a 4-sample synthetic batch).
  * `reward_decomp_per_action_gpu_oracle` (GPU oracle, ignored unless
    `--ignored`; asserts kernel matches CPU oracle bit-for-bit within
    1e-6 f32 budget).
  * `reward_decomp_empty_bin_emits_zero_not_nan` (empty-bin contract
    guard).

Pure observability — no production-path consumer in this commit. No
reward changes, no Bellman target changes, no kernel modifications to
the action-selection or training paths. Per
`feedback_no_partial_refactor` the kernel + cubin manifest + buffer +
launcher + production wire-up + HEALTH_DIAG emit + GPU oracle test all
land atomically.

Verification:
  SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace clean.
  CPU oracle test passes; GPU oracle + empty-bin guard both pass on
  RTX 3050 Ti (2.13s).

Plan: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md
      § Phase 0 Task 0.1.
Audit: docs/dqn-wire-up-audit.md § "SP18 v2 Phase 0 Task 0.1".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:06:48 +02:00
jgrusewski
b6b17d46bb feat(sp17-3.2): V_share + advantage_clip_bound producers + extended emit
Phase 3.2 lands the remaining two SP17 dueling-Q diagnostic producers
atomically with kernel + launcher + Rust wrapper + extended HEALTH_DIAG
emit + GPU oracle tests per `feedback_wire_everything_up`.

V_share[d] = |E[V]| / (|E[V]| + |E[A_centered, picked]|)
  where picked = argmax_a Σ_z A_raw[i, a, z] (max-Q semantic — tractable
  per-batch without depending on actions_history_buf which is collector-
  time state stale relative to the cuBLAS forward at HEALTH_DIAG cadence).
  Pearl-A bootstrap (sentinel 0.5) + α=WELFORD_ALPHA_MIN=0.4 + bilateral
  [0, 1] clamp per `pearl_symmetric_clamp_audit`. 4 blocks × 256 threads.

advantage_clip_bound = p99(|A_centered|) × ADVANTAGE_CLIP_SAFETY_FACTOR=1.5
  via sp4_histogram_p99 (block tree-reduce + per-warp tile binning, NO
  atomicAdd per `pearl_fused_per_group_statistics_oracle`). EMA α=0.01
  slow per-fold + bilateral clamp [0.1, 100.0] per
  `pearl_symmetric_clamp_audit`. Pearl-A bootstrap (sentinel 1.0).
  Single block × 256 threads + flat |A_centered| scratch buffer
  (mapped-pinned, sized to B × Σ_d b_d × NA).

Observability-only — the actual clipping wire-up is Phase 5 follow-up.
The Phase 1 mean-zero contract (commits eabcf8d52..6f53d676f) makes
A_centered a meaningful signal; this commit observes it.

Extended HEALTH_DIAG line:

  HEALTH_DIAG[N]: dueling [v_share=(d=X m=Y o=Z u=W)]
                          [a_var=(d=A m=B o=C u=D)] [clip=K]

GPU oracle tests on RTX 3050 Ti (all pass, 13/13 SP17 tests):
- v_share_per_branch_matches_closed_form: synthetic V=2.0 + linear A
  per branch; closed-form V_share = 2/(2 + |K_d × (n_d-1)/2|);
  ε=1e-4. Pearl-A bootstrap REPLACES on first launch.
- advantage_clip_bound_tracks_p99_safety: synthetic A with action-
  dominant + per-(i,z) jitter (the jitter is REQUIRED — pathologically
  lockstep values undercount in sp4_histogram_p99's non-atomic warp
  tile binning per the kernel's documented "1/(256×32) loss for
  uniformly distributed signals" qualifier; concentrated values violate
  the assumption. Real |A_centered| in production is continuous, so
  this is a test-data-only effect.) ε=0.20 (jitter + linear histogram
  quantization).

Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md
      Phase 3.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:02:35 +02:00
jgrusewski
1e70cd5e59 feat(sp17-3.1): A_var_ema per-branch producer + HEALTH_DIAG emit
Phase 3 of SP17 dueling-Q identifiability — first of three diagnostic
producers landed atomically with kernel + launcher + Rust wrapper +
HEALTH_DIAG emit + GPU oracle test per `feedback_wire_everything_up`.

Per branch d ∈ {dir, mag, ord, urg}:
  Var_d = (1/(B × n_d × NA)) Σ_{i, a, z} (A[i, a, z] − mean_a A[*, z])²

Block tree-reduce (no atomicAdd, `feedback_no_atomicadd`); 4 blocks ×
256 threads. Pearl-A first-observation bootstrap (sentinel 0.0 →
REPLACE on first launch); steady-state α = WELFORD_ALPHA_MIN=0.4 per
`pearl_wiener_alpha_floor_for_nonstationary` — the structural-control
floor preserves catch-up bandwidth without storing 24 Welford
accumulator slots for a cold-path-cadence diagnostic.

Cold-path emit: single launch per HEALTH_DIAG cadence (epoch boundary)
right after `v_a_means`. New line:

  HEALTH_DIAG[N]: dueling [a_var=(d=X m=Y o=Z u=W)]

The line will be extended with V_share + advantage_clip_bound in
Phase 3.2, then finalised in Phase 3.3.

GPU oracle test on RTX 3050 Ti: synthetic A constructed so each branch
d has a closed-form Var(A_centered); kernel readback matches expected
value within ε=1e-4 (f32 rounding budget for ~8×n×51 accumulator
length).

Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md
      Phase 3.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:45:40 +02:00
jgrusewski
cc4746b48d plan(sp17): HEALTH_DIAG v_a_means baseline (PRE-CENTERING)
Adds per-epoch readout of the V/A breakdown so we can observe the
pre-SP17 baseline distribution before the identifiability projection
lands. Also instruments the kill criterion for Phase 0:

  HEALTH_DIAG[N]: v_a_means [v=X a_dir=Y a_mag=... a_ord=... a_urg=...]

If train-multi-seed-b5gmp's Q(Flat) over-attribution is V-driven, V
should be elevated (~0.4+) while a_dir is small. If V is balanced and
A is doing the work, dueling cannot help and we abort SP17.

Block-tree-reduce kernel \`v_a_means_diag_kernel\` (no atomicAdd per
\`feedback_no_atomicadd\`); MappedF32Buffer per
\`feedback_no_htod_htoh_only_mapped_pinned\`.

Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:52:35 +02:00
jgrusewski
1a3bcf97b8 feat(sp16-p2): adaptive Hold cost scale via ISV[461]
Per train-multi-seed-pfh9n post-mortem: observed_hold_rate climbed 0.25 → 0.52
across training while cost penalty (~0.006) was 100× smaller than per-bar
reward magnitudes (popart=0.97, cf=0.65). Hold action was effectively free,
allowing Q(Hold) to dominate via structural low-variance bias.

Fix: scale Hold cost adaptively. ISV[HOLD_COST_SCALE_INDEX=461] tracks:
  scale = clamp(1.0 + 24.0 × max(0, observed - target) / max(target, 0.01), 1.0, 25.0)

Effective cost at 100% overrun (observed=2× target): 0.006 × 25 = 0.15,
competitive with per-bar reward magnitudes (~0.01-0.1).
At/below target: scale = 1.0 (no extra penalty).

Pearl-A bootstrap + Welford slow EMA (α=0.05). Mirrors T1's
MIN_HOLD_TEMPERATURE pattern (same input signals: ISV[382] observed,
ISV[381] target).

Producer: hold_cost_scale_update_kernel.cu — single-thread cold-path,
per-epoch boundary, AFTER MIN_HOLD_TEMPERATURE in training_loop.rs.

Consumer migration (atomic per feedback_no_partial_refactor):
3 sites in experience_kernels.cu — segment_complete branch (line ~3089),
per-bar positioned-Hold branch (line ~3553), per-bar flat-Hold branch
(line ~3617). Cold-start fallback: scale=1.0 when slot ≤ 0 or
out-of-bounds (bit-identical pre-Phase-2 cost magnitude).

ISV_TOTAL_DIM: 461 → 462.

Behavioral tests (5/5 PASS on RTX 3050):
- sp16_phase2_hold_cost_scale_climbs_with_overrun
- sp16_phase2_hold_cost_scale_at_target_is_one
- sp16_phase2_hold_cost_scale_under_target_is_one
- sp16_phase2_hold_cost_scale_bounds_clamp
- sp16_phase2_hold_cost_scale_pearl_a_bootstrap

Regression: SP14 oracle suite 30/30 PASS, SP15 phase 1 oracle suite
36/36 PASS.

Instrumentation: HEALTH_DIAG[N]: hold_cost_scale_diag obs/tgt/norm/scale.

Per feedback_isv_for_adaptive_bounds + feedback_no_partial_refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:56:46 +02:00
jgrusewski
2c469af2f7 fixup(sp16-p1): correct stale build.rs comment + remove tautological test assertion
Code-quality review of 0426ce888 flagged two Important doc/test items:

1. crates/ml/build.rs lines 859-882 — comment described OLD slot-373
   dir-acc-skill mapping; replaced with short description of new
   slot-382/381 hold-rate-overrun mapping.

2. crates/ml/tests/sp14_oracle_tests.rs lines 1898-1903 — tautological
   `(A || ¬A)` assertion (dead test code); deleted. Preceding assert at
   1892-1897 already covers the real check.

No runtime impact — doc + test cleanup only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:34:01 +02:00
jgrusewski
0b9ea77dc4 fix(class-a-audit-batch-4b): plan_threshold floor adaptive + MIN_HOLD_TEMPERATURE ISV-driven
Per Class A audit-fix Batch 4-B (final 2 of 4 deferred items from
P1-wiring/P1-producer). Completes the 8-commit WR-plateau intervention
chain. Validation deferred to next L40S smoke.

Item 3: plan_threshold adaptive floor (Design Y - inline producer)
  - NEW slot PLAN_THRESHOLD_FLOOR_ADAPTIVE_INDEX=459
  - Writes slow-EMA shadow of `0.5 * readiness_ema` from inside the
    existing update kernel (no new file, no new launch).
  - Pearl-A first-observation bootstrap (sentinel 0.1 matches pre-fix
    hardcoded value for bit-identical cold-start) + Welford alpha=0.005
    slow EMA.
  - Bilateral clamp [0.05, 0.50] (probability units) per
    pearl_symmetric_clamp_audit.
  - Consumer reads isv[459] as the floor in the same launch's final
    fmaxf; cold-start sentinel REPLACES with threshold_target so the
    pre-fix `fmaxf(0.1, 0.5*ema)` semantic is preserved bit-identical
    for any readiness EMA above 0.20.

Item 4: MIN_HOLD_TEMPERATURE -> ISV-driven (driving signal: dir_acc skill)
  - NEW slot MIN_HOLD_TEMPERATURE_ADAPTIVE_INDEX=460
  - NEW kernel min_hold_temperature_update_kernel.cu (single-thread
    cold-path, per-epoch boundary launch).
  - Driving signal: dir_acc skill = clamp((short_ema - 0.5)/0.5, 0, 1)
    from ISV[AUX_DIR_ACC_SHORT_EMA_INDEX=373]. When committing skillfully
    (high dir_acc) -> temp HIGH (permissive). When at random baseline
    (~0.5) -> temp LOW (sharp commitment pressure). Substituted for
    the audit-spec's `dir_entropy_deficit` because no dir_entropy ISV
    slot exists - dir_acc skill is the closest semantically-equivalent
    signal that preserves the spec intent.
  - Pearl-A bootstrap (sentinel 50.0 matches the deleted
    MIN_HOLD_TEMPERATURE_START=50 anchor) + alpha=0.05 mid-cadence EMA.
  - Bounds [5, 50] (matches the deleted schedule range).
  - Decouples temperature from epoch number - the old schedule pinned
    LOW temp (sharp) at end of training, exactly when a WR-plateaued
    model needed forgiveness to escape.
  - DELETED: state_layout.cuh::MIN_HOLD_TEMPERATURE_{START, END, DECAY}
    #defines + training_loop.rs::min_hold_temperature_for_epoch helper
    function (kept docstring tombstone explaining the deletion). Both
    call sites migrated to the new ISV reader. Per
    feedback_no_legacy_aliases + feedback_no_partial_refactor.

ISV_TOTAL_DIM: 459 -> 461.

Cumulative WR-plateau fix series (final commit, #8):
- 8f218cab2 (Class C bug 1 + P0-B)
- 316db416b (P0-C MIN_HOLD_TARGET)
- 394de7d43 (P0-A REWARD_POS/NEG_CAP)
- c4b6d6ef2 (P1 wiring var_floor)
- 657972a4b (P0-A downstream DD penalty + MIN_HOLD_PENALTY_MAX)
- 87d597d5d (P1 producer Bayesian priors)
- 7e9a8f6ef (Batch 4-A DD saturation floor + legacy DELETE)
- this commit (Batch 4-B plan_threshold floor + MIN_HOLD_TEMPERATURE)

Verification:
  cargo check -p ml --tests --all-targets   PASS
  cargo test sp14 (lib)                     16/16 PASS (slot layout locks)
  cargo test sp15 (lib)                     2/2   PASS (no regression)
  cargo test state_reset_registry (lib)     4/4   PASS (dispatch coverage)
  cargo test sp14_oracle_tests (GPU)        24/24 PASS (8 new + 16 pre)
  cargo test sp15_phase1_oracle_tests (GPU) 36/36 PASS (no regression)

Per feedback_isv_for_adaptive_bounds + feedback_no_partial_refactor +
feedback_no_legacy_aliases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:13:54 +02:00
jgrusewski
7e9a8f6ef1 fix(class-a-audit-batch-4a): DD saturation floor adaptive + legacy DD path Case A
Per Class A audit-fix Batch 4-A (deferred from P1-wiring/P1-producer due to
audit-doc errors). Fixes 2 of 4 deferred items; Batch 4-B handles plan_threshold
floor + MIN_HOLD_TEMPERATURE in a separate commit.

Item 1: DD saturation floor (the upper end of the DD ramp at trade_physics.cuh:154
in apply_margin_cap, NOT line 548 as the audit doc claimed — that line is a
magnitude action constant; the actual saturation floor lives in apply_margin_cap)
  - NEW slot DD_SATURATION_FLOOR_ADAPTIVE_INDEX=458
  - Producer dd_saturation_floor_update_kernel.cu — p75(per-env DD_MAX) × 1.5
    via Welford `mean + Z_75 × sigma` estimator with `max(p75, mean)` robustness
    guard, mirrors P0-A REWARD_POS_CAP producer pattern (Pearl-A bootstrap +
    Welford α=0.01)
  - Cold-start fallback: 0.25f (DD_SATURATION_FLOOR_DEFAULT in state_layout.cuh)
  - Bounds: [0.10, 0.50] (Category-1 dimensional safety)
  - Distinct from SP15_DD_THRESHOLD_INDEX=421 (the SP15 quadratic DD-penalty
    *trigger* threshold, a *lower* bound; this slot is the *upper* end of the
    linear position-size scaling ramp dd_scale = max(0.05, 1.0 − dd_frac/floor))
  - Threaded `isv_signals_ptr` into `apply_margin_cap` with NULL-tolerant
    cold-start fallback to DD_SATURATION_FLOOR_DEFAULT
  - 4 oracle tests (Pearl-A bootstrap, no-DD guard, bounds clamp, Welford EMA)

Item 2: Legacy compute_drawdown_penalty path → Case A (DELETED)
  - Decision rationale: SP15's quadratic asymmetric DD penalty
    (compute_sp15_final_reward_kernel.cu:154 via sp15_dd_penalty helper) runs
    unconditionally as a post-modifier on the SP11-composed reward with
    ISV-driven λ_dd (slot 420) and DD threshold (slot 421). Layering the legacy
    linear-ramp penalty inside the SP11 composer on top of the SP15 quadratic
    creates double-counting of DD shaping — exactly the code-smell the Class A
    audit was designed to eliminate. Per `feedback_no_legacy_aliases.md` and
    `feedback_no_partial_refactor.md`.
  - Atomic deletion across:
      - `compute_drawdown_penalty` device function (trade_physics.cuh)
      - Single call site at experience_kernels.cu:3822
      - `dd_threshold` and `w_dd` kernel arguments
      - `w_dd` Rust config field (gpu_experience_collector.rs +
        trainers/dqn/config.rs DQNHyperparameters)
      - `w_dd` profile section + dispatch (training_profile.rs RewardSection,
        OptimizableParameterRanges, FixedRewardParameters, ParamLookup
        dispatch, profile→hyperparam mapping, test assertion)
      - `w_dd *= rki` risk-intensity multiplier (config.rs)
      - `w_dd` TOML keys (dqn-hyperopt.toml × 2, dqn-localdev.toml,
        dqn-production.toml, dqn-smoketest.toml)
      - Stale doc comments on hyperopt/adapters/dqn.rs + config.rs
        risk_intensity field
  - `config.dd_threshold` SURVIVES (still consumed by `launch_sp15_dd_state`
    as the dd_budget for DD_PCT scaling). Documented in field comment.

ISV_TOTAL_DIM: 458 → 459 (Item 1 adds 1 slot; Item 2 is pure deletion)

Cumulative WR-plateau fix series (this is commit 7):
- Class C bug 1 + P0-B (8f218cab2)
- P0-C (316db416b)
- P0-A (394de7d43)
- P1 wiring (c4b6d6ef2) — 1 of 4 wireable
- P0-A downstream (657972a4b)
- P1 producer (87d597d5d)
- audit-fix 4-A (this commit)

Verification: 16 sp14_oracle_tests pass (incl. 4 new), 36 sp15_phase1_oracle_tests
pass, 12 sp14_isv_slots layout tests pass, 4 state_reset_registry tests pass
(every-FoldReset-arm-has-dispatch contract holds), workspace cargo check clean.

Per feedback_isv_for_adaptive_bounds + feedback_no_partial_refactor +
feedback_no_legacy_aliases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:41:07 +02:00
jgrusewski
87d597d5d7 fix(class-a-p1-producer): adaptive Bayesian Kelly priors per-fold-end
Replace 4 hardcoded Bayesian priors in Kelly cap calculations
(prior_wins=2.0, prior_losses=2.0, prior_sum_wins=0.01,
prior_sum_losses=0.01) with ISV-driven slow-EMA values fed by a new
producer kernel that aggregates realized PS_KELLY_* fields across envs
from the same portfolio_state buffer kelly_cap_update_kernel reads
from at the same per-epoch boundary.

Slots [454..458):
- KELLY_PRIOR_WINS, KELLY_PRIOR_LOSSES,
  KELLY_PRIOR_SUM_WINS, KELLY_PRIOR_SUM_LOSSES

ISV_TOTAL_DIM bumps 454 -> 458.

Producer:
- kelly_bayesian_priors_update_kernel.cu (per-fold-end / per-epoch
  boundary, block-tree-reduce in shmem, no atomicAdd). Pearl-A
  first-observation bootstrap (sentinels 2.0/2.0/0.01/0.01 match
  pre-P1-Producer hardcoded values for bit-identical cold-start) +
  alpha=0.005 slow EMA. Bounds counts in [0.5, 100], sums in [0.001,
  1.0] per feedback_isv_for_adaptive_bounds. Launched RIGHT BEFORE
  launch_kelly_cap_update so that kernel sees the freshly-blended
  priors.

Consumers migrated atomically (same commit):
- kelly_cap_update_kernel.cu:39-42 -> ISV[454..458) with cold-start
  fallback via kelly_prior_or_default helper (range guard).
- trade_physics.cuh::kelly_position_cap:304-307 -> NULL-tolerant
  isv_signals_ptr threaded through apply_kelly_cap (single caller in
  unified_env_step_core line 898 already had the bus pointer; mirrors
  the existing kelly_f_smooth / kelly_warmup_floor_sp9 patterns).

State reset:
- 4 FoldReset registry entries (sp14_p1_kelly_prior_*) +
  4 dispatch arms in training_loop.rs::reset_named_state.

Oracle tests (sp14_oracle_tests.rs, GPU-gated #[ignore]):
- Pearl-A bootstrap (sentinel -> REPLACE with aggregated targets)
- No realized trades -> ISV preserved bit-exactly
- Bounds clamp on extreme aggregates
- Slow EMA blend after bootstrap

CPU tests passing:
- sp14_p1_kelly_prior_slot_layout_locked
- all_sp14_p1_slots_fit_within_isv_total_dim
- every_fold_and_soft_reset_entry_has_dispatch_arm (C.10 lesson)
- layout_fingerprint_bumps_after_sp14_wire

DEFERRED — Item 2 (MIN_HOLD_TEMPERATURE EMA): the audit-spec said
"MIN_HOLD_TEMPERATURE = 0.5f hardcoded somewhere" but the actual code
has MIN_HOLD_TEMPERATURE_{START=50.0f, END=5.0f, DECAY=20.0f} as a
PER-EPOCH ANNEALING SCHEDULE driven by min_hold_temperature_for_epoch
in training_loop.rs:68-73. The kernel takes T as a runtime scalar
specifically to enable Phase 2 ISV-driven lift "without recompiling
cubin" per the SP12 v3 design comment. The audit's claim of "0.5f
hardcoded" does not match reality; the right Phase 2 signal is a
separate spec decision and should not be guessed at per
feedback_no_quickfixes. Reporting back per the prompt's hard rule #8.

Per feedback_isv_for_adaptive_bounds + feedback_no_partial_refactor +
pearl_controller_anchors_isv_driven.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:38:51 +02:00
jgrusewski
394de7d434 feat(class-a-p0a): REWARD_POS/NEG_CAP → ISV-driven adaptive caps from realized return distribution
Per Class A audit ranking, the highest-suspected-impact fix for the
months-long WR-stuck-at-46-48% plateau across 11 superprojects.

Hardcoded REWARD_POS_CAP=+5.0f / REWARD_NEG_CAP=-10.0f
(state_layout.cuh:266-267) was structurally clipping the upper tail of
realized alpha. Controller signals (sharpe EMA, var_q, q_gap) all
derive from this CAPPED buffer, so the controller cannot select for
trades it cannot see. Selectivity gradient evaporates — small wins
clip to +5 alongside large wins also clipping to +5.

The state_layout comment lines 261-264 explicitly deferred Phase 2
(ISV-driven) "IF Phase 1 validation reveals adaptive need". Phase 1
has been running 11 SPs without budging WR — adaptive need revealed.

Architecture:
- 2 new ISV slots [452..454): REWARD_POS_CAP_ADAPTIVE,
  REWARD_NEG_CAP_ADAPTIVE.
- Producer kernel reward_cap_update_kernel.cu: block-tree-reduce
  Welford `mean + Z_99 × sigma` p99 estimator over winning realized
  returns + conservative `max(p99, max_win)` takeover, × 1.5 safety
  factor → POS cap. NEG cap = -2 × POS cap (preserves Kahneman 2:1
  asymmetry per pearl_audit_unboundedness_for_implicit_asymmetry —
  asymmetry stays, but moved from hardcoded scalar to producer-time
  multiplier; single source of truth, no consumer applies the 2× ratio
  itself).
- Pearl-A first-observation bootstrap from sentinel (5.0 / -10.0,
  matching pre-P0-A hardcoded values for bit-identical cold-start).
  Welford EMA α=0.01 thereafter (slow blend — reward distribution is
  the foundation of training and shouldn't move fast).
- Bounds: POS in [1, 50], NEG in [-100, -2] (Category-1 dimensional
  safety per feedback_isv_for_adaptive_bounds, NOT tuning).
- 3 consumer sites migrated atomically per
  feedback_no_partial_refactor: experience_kernels.cu:3112-3114
  (segment_complete cap), compute_sp15_final_reward_kernel.cu:163
  (Stage 4 helper invocation), sp15_reward_axis_helpers.cuh:211
  (sp15_apply_sp12_cap device fn signature change to take isv ptr).
- Cold-start fallback: when ISV slot at sentinel OR outside
  [REWARD_POS_CAP_MIN_BOUND=1, REWARD_POS_CAP_MAX_BOUND=50],
  consumers fall back to original macros (still defined in
  state_layout.cuh).
- 2 new device-ptr accessors on the experience collector
  (step_ret_per_sample_dev_ptr, trade_close_per_sample_dev_ptr) —
  reuses existing per-sample buffers; no new buffer allocated.
- Per-epoch boundary launch (cold path) in training_loop.rs alongside
  launch_aux_horizon_chain.
- Reset registry entries + dispatch arms in reset_named_state per the
  C.10 lesson (missing dispatch causes runtime crash).
- Layout fingerprint seed updated: ISV_TOTAL_DIM 452→454 +
  AUX_PRED_HORIZON_BARS=450 + AVG_WIN_HOLD_TIME_BARS=451 (previously
  missing from seed) + REWARD_POS_CAP_ADAPTIVE=452 +
  REWARD_NEG_CAP_ADAPTIVE=453.

Per feedback_isv_for_adaptive_bounds: every adaptive bound in ISV.

Verification:
- cargo check -p ml --tests --all-targets: clean (19 pre-existing
  warnings, 0 new).
- sp14_isv_slots tests: 8/8 pass (4 layout + 4 fits-within).
- sp14_oracle_tests with --features cuda --ignored: 8/8 pass (4
  existing q_disagreement/dir_concat + 4 new P0-A tests covering
  Pearl-A bootstrap, no-winning-trades preservation, bounds clamping
  to [1,50], Welford α=0.01 EMA blend).
- sp15_phase1_oracle_tests with --features cuda --ignored: 36/36 pass
  (no regression from sp15_apply_sp12_cap signature change).

Cumulative WR-plateau fix series:
- Class C bug 1 (8f218cab2): replay buffer intent→realized.
- Class A P0-B (8f218cab2): Kelly warmup floor wiring.
- Class A P0-C (316db416b): MIN_HOLD_TARGET adaptive.
- Class A P0-A (this commit): REWARD_POS/NEG_CAP adaptive — restores
  upper-tail alpha to the controller's view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:45:43 +02:00
jgrusewski
0e61de408f feat(sp14-c6): h_s2_aux_rms_ema producer — ISV[449] per-collector-step
Single-block 256-thread CUDA kernel computing RMS(h_s2_aux [B, SH2])
and EMA-blending the step observation into ISV[H_S2_AUX_RMS_EMA_INDEX=449]
directly. Pearl-A first-observation bootstrap embedded in kernel body
(sentinel 0.0 → replace); fixed α=0.05 EMA blend thereafter.

ISV slot 449 is outside the SP4/SP5 wiener buffer linear span so the
scratch+apply_pearls_ad_kernel path is not available — self-contained
Pearl-A logic mirrors the avg_win_hold_time_update_kernel precedent
(slot 451). No atomicAdd; shmem block-tree-reduce only. Launched after
aux_trunk_forward in the collector per-step hot path.

- h_s2_aux_rms_ema_kernel.cu — new CUDA kernel (81 lines)
- build.rs — cubin manifest entry
- gpu_dqn_trainer.rs — H_S2_AUX_RMS_EMA_CUBIN static
- gpu_aux_trunk.rs — HS2AuxRmsEmaOps struct + launch()
- gpu_experience_collector.rs — field + constructor + hot-path launch
- aux_trunk_oracle_tests.rs — h_s2_aux_rms_ema_pearl_a_bootstrap test
- dqn-wire-up-audit.md — Phase C.6 audit entry

cargo check -p ml --tests: clean (only pre-existing warnings)
Oracle test: 1 new test added (requires GPU to run)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 03:17:30 +02:00
jgrusewski
3b71d21834 feat(sp14-c): aux prediction horizon ISV-driven (multi-bar pivot)
Aux's original label was (p_{t+1} > p_t) — pure HFT-scale microstructure
noise that's unlearnable at our HFT-MFT trading frequency. Migrated to
(p_{t+H} > p_t) where H is read from ISV[AUX_PRED_HORIZON_BARS_INDEX=450].

Adaptive producer drives H from observed avg winning hold time:
- Pearl-A first-observation bootstrap: replace sentinel H=60 directly
  on first valid observation
- Steady-state Wiener-α EMA blend, slow (α=0.01) for stable horizon
  (no target-variance EMA available, fallback per
  pearl_wiener_optimal_adaptive_alpha)
- "No winning trades yet" guard keeps sentinel until first valid observation

Lookahead truncation: labels at t where t+H >= total_bars are masked
(sentinel -1, loss-reduce skips). The existing aux_next_bar_loss_reduce
in aux_heads_kernel.cu already supports the -1 mask convention via the
B_valid count — no new valid_mask parameter needed.

Step 5b finding: Case B — existing per-sample buffers
(hold_at_exit_per_sample, trade_profitable_per_sample) populated by
unified_env_step_core, but no aggregate ISV slot. Added new aggregator
slot AVG_WIN_HOLD_TIME_BARS_INDEX=451 + new producer kernel
avg_win_hold_time_update_kernel.cu (block-tree-reduce, no atomicAdd).
ISV_TOTAL_DIM bumped 450 → 452.

ATOMIC migration per feedback_no_partial_refactor: both label kernels
(aux_sign_label_kernel.cu trajectory + aux_sign_label_per_step_kernel.cu
per-rollout-step) migrated together to the new
(targets, bar_indices, isv, isv_h_idx, out_labels, total, total_bars)
signature. The lookahead host-passed scalar argument is removed; H is
read from ISV inside the kernel (broadcast value, single read per
thread, on-device clamp [1, 240]).

Producer chain (per-epoch boundary): new
GpuDqnTrainer::launch_aux_horizon_chain orchestrates
avg_win_hold_time_update → aux_horizon_update sequentially alongside
launch_kelly_cap_update at the existing epoch-boundary slot in
training_loop.rs.

Trunk math (C.2/C.3/C.4) unchanged — separate aux trunk is label-
agnostic. Validation in C.10 will use H=60 cold-start; the adaptive
producer drives H from real winning-trade observations.

Tests (8 oracle, 5 new + 3 preserved):
- aux_trunk_forward_matches_numpy_reference (C.3) ✓
- aux_trunk_backward_gradient_check (C.4) ✓
- aux_trunk_backward_does_not_write_dx (C.4) ✓
- aux_sign_label_h_bar_horizon (NEW) ✓
- aux_sign_label_lookahead_mask (NEW) ✓
- aux_horizon_pearl_a_bootstrap (NEW) ✓
- aux_horizon_converges_to_steady_target (NEW) ✓
- aux_horizon_holds_sentinel_with_no_winning_trades (NEW) ✓

8/8 pass on RTX 3050 Ti.

Phase C.4b of SP14 Layer C separate-aux-trunk refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:37:39 +02:00
jgrusewski
5d584dc751 feat(sp14-c): aux trunk backward kernel + gradient check + stop-grad invariant test
Backward propagates dh_s2_aux through w3/w2/w1 with block-tree-reduce
(no atomicAdd per feedback_no_atomicadd). Critical: kernel set does NOT
write dx_in — encoder gradient remains Q-shaped only. Stop-grad
invariant verified via parameter-list structural enforcement (kernels
literally cannot reference an `dx_in_out` pointer they don't accept) +
kernel source inspection that strips comments and asserts no `dx_in`
write pattern.

Three kernels in aux_trunk_backward_kernel.cu:
  - aux_trunk_bwd_dh_pre: per-sample, computes dh_aux2_pre [B, H2] +
    dh_aux1_pre [B, H1] using ELU' from POST-activation form
    (`(y > 0) ? 1 : (1 + y)` mirrors aux_elu_bwd_from_post in
    aux_heads_kernel.cu).
  - aux_trunk_bwd_dW_reduce: generic outer-product reduce
    `dW[k, j] = sum_b A[b, k] * B[b, j]`. One block per output
    element, shmem-tree reduce over batch. Used 3× (dW3, dW2, dW1).
  - aux_trunk_bwd_db_reduce: generic batch-reduce `db[j] = sum_b
    B[b, j]`. One block per output element. Used 3× (db3, db2, db1).

Memory-efficient: no per-sample partials (avoids B×163,072 floats for
production topology). Per-element reduction means O(P) blocks each
doing O(B) work in shmem.

Rust wrapper AuxTrunkBackwardOps in gpu_aux_trunk.rs orchestrates seven
launches in fixed sequence (capture-friendly, no host branches per
pearl_no_host_branches_in_captured_graph). All three CudaFunction
handles pre-loaded once at construction. Field added to GpuDqnTrainer
alongside aux_trunk_forward_ops; constructor mirrors C.3 pattern.

Tests (all pass on RTX 3050 Ti, sub-ULP forward, 1.33e-2 max rel-err
backward gradient at smallest sampled gradient):
  - aux_trunk_forward_matches_numpy_reference (C.3 — preserved).
  - aux_trunk_backward_gradient_check (NEW): central-difference
    numerical gradient at 16 sampled dW3 indices vs analytic from
    backward kernel. Loss = 0.5 * ||h_s2_aux||^2 so dh_s2_aux =
    h_s2_aux. EPS=1e-3, B=4, ENC=H1=H2=AUX=32 (33 forwards in ~2s).
    REL_TOL = 2e-2 (f32 finite-difference noise floor for
    small-gradient tail; production topology is dimension-independent
    given runtime args).
  - aux_trunk_backward_does_not_write_dx (NEW): reads kernel source,
    strips C-style comments (so design-discussion text mentioning
    `dx_in` doesn't false-positive), asserts no `dx_in` / `dx_in_out`
    symbol survives in code. Complements the structural enforcement
    (kernel signatures don't accept `dx_in_out` pointer).

Phase C.4 of SP14 Layer C separate-aux-trunk refactor. Module is
additive — wire-up into collector backward chain + Adam updates lands
in Phase C.5 (atomic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:16:37 +02:00
jgrusewski
cb6bca4629 feat(sp14-c): aux trunk forward kernel + Rust wrapper + oracle test
3-layer MLP forward (Linear→ELU→Linear→ELU→Linear). Pre-loaded
CudaFunction for graph-capture safety per pearl_no_host_branches_in_captured_graph.
Oracle test verifies bit-for-bit match against numpy reference within
1e-4 tol. Saves h_aux1 and h_aux2 to global memory for backward.

Phase C.3 of SP14 Layer C separate-aux-trunk refactor (plan:
docs/superpowers/plans/2026-05-07-sp14-layer-c-separate-aux-trunk.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:56:23 +02:00
jgrusewski
4f372a49a8 refactor(sp14-c): atomic α machinery deletion + aux trunk ISV slot allocation
Phase C.1 of SP14 Layer C separate-aux-trunk refactor. Single atomic
commit per feedback_no_partial_refactor and feedback_no_legacy_aliases —
no DEPRECATED stage.

Deleted (per C.0 audit a7a162d1f):
- alpha_grad_compute_kernel.cu
- sp14_scale_wire_col_kernel.cu
- gradient_hack_detect_kernel.cu (EGF circuit breaker — α-coupled)
- 10 ISV slot constants (K_AUX_ADAPTIVE, K_Q_ADAPTIVE,
  BETA_RATE_LIMITER_ADAPTIVE, AUX_DIR_ACC_VARIANCE_EMA,
  ALPHA_GRAD_RAW_VARIANCE_EMA, GATE1_OPEN_STATE, ALPHA_GRAD_RAW,
  ALPHA_GRAD_SMOOTHED, AUX_DIR_ACC_POST_OPEN_MIN,
  GRADIENT_HACK_LOCKOUT_REMAINING) plus 16 supporting sentinels and
  structural-anchor constants
- 31 reference sites across 7 files (collector, trainer, training_loop,
  fused_training, batched_backward, build.rs cubin manifest,
  state_reset_registry)
- 3 oracle tests (alpha_grad_adaptive_beta, alpha_grad_schmitt_hysteresis,
  gradient_hack_circuit_breaker_fires)

Added:
- 6 aux trunk control plane ISV slots [444..450):
  AUX_TRUNK_LR (444), AUX_TRUNK_BETA1 (445), AUX_TRUNK_BETA2 (446),
  AUX_TRUNK_EPS (447), AUX_TRUNK_GRAD_CLIP (448), H_S2_AUX_RMS_EMA (449)
- 6 reset registry entries (5 Invariant-1 anchors + Pearl-A first-
  observation EMA)
- 6 reset_named_state dispatch arms (mirrors SP5 Layer A pattern)
- ISV_TOTAL_DIM bumped 444 → 450

Preserved:
- dir_concat_qaux_kernel.cu (Coupling A: forward feature wire)
- q_disagreement_update_kernel.cu (diagnostic-only)
- q_disagreement_* ISV slots (383, 384, 389) — HEALTH_DIAG consumer

Build clean (cargo check -p ml --tests --all-targets, RTX 3050 Ti);
4 surviving oracle tests pass (dir_concat_qaux_correct + 3 q_disagreement_*).
ISV layout: deleted α slots left as RESERVED gap (NOT compacted) for
checkpoint fingerprint compatibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:32:40 +02:00
jgrusewski
296ba92282 feat(sp14-β): wire collector-side aux-head forward + label producer per-rollout-step
Step 3 of β migration: collector now runs aux_next_bar_forward on
rollout state every step. Label producer (new thin variant
aux_sign_label_per_step_kernel) derives sign(price[t+1] - price[t])
per env using bar = episode_starts[ep] + t. Aux predictions feed
the EGF kernel chain (step 4), NOT the Q-head's input (rollout
Q-head still sees raw h_s2, dir_qaux_concat_ptr remains 0u64).

Placement: AFTER captured forward graph, BEFORE expected_q kernel.
Same-stream serial ordering reads exp_h_s2_f32 populated by
forward_online_f32 inside the captured graph. Cold-start gated on
trainer_params_ptr != 0 to skip the test-scaffold path where the
trainer hasn't wired its params yet.

Files added: aux_sign_label_per_step_kernel.cu (66 lines).
Files modified: build.rs (+8 lines, register cubin),
gpu_experience_collector.rs (+106 lines: struct field, cubin static,
load in new(), per-step launch block).

Compile clean; sp14_oracle_tests 2/2 non-GPU pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:51:51 +02:00
jgrusewski
5b394f1035 feat(sp15-p1.3.b-followup): per-env DD redesign — Path A env-0-canonical → Path B per-env tile + reduction
Closes the Phase 1.3.b deferred per-env redesign per
feedback_no_partial_refactor. Path A (env-0-canonical, commit 132609724)
made every downstream consumer read env-0's DD context; production envs
each have their own DD trajectory, so when env-3 was at 30% DD and
env-0 was at ATH, the model learning from env-3's transitions saw
dd_pct=0 and silently skipped the recovery shaping. Path B threads
each env's actual DD context through the reward shaping atomically:

(1) dd_state_kernel reshape — grid [n_envs, 1, 1], one thread per env,
    writes 6 scalars per env to a new per-env tile dd_state_per_env
    [n_envs * 6]. No more ISV scalar writes.
(2) NEW dd_state_reduce_kernel — single-block tree-reduce (no
    atomicAdd; BLOCK=256 with strided initial pass for n_envs up to
    32768 on H100). Mean-aggregates per-env tile → 6 scalar ISV slots
    [401..407) for HEALTH_DIAG diagnostic. Max-aggregates DD_PERSISTENCE
    → new slot DD_PERSISTENCE_MAX_INDEX=443 for plasticity injection
    trigger (one shared advantage-head ⇒ global firing ⇒ max-aggregate
    is the only correct rule). ISV_TOTAL_DIM 443→444; SP15_SLOT_END
    443→444; SP15_SLOT_COUNT 46→47.
(3) compute_sp15_final_reward_kernel migration — per-(i,t) per-env DD
    lookup via env_id = (idx % (N*L)) / L. Helpers sp15_dd_asymmetric_reward
    and sp15_dd_penalty migrated to take dd_pct + dd_current as scalar
    parameters (the kernel reads from the per-env tile, threads scalars
    in). On-policy + CF threads at the same (i,t) read the SAME tile
    entry (one DD trajectory per env, shared across slot kinds).
(4) plasticity_injection_kernel migration — persistence read switched
    from ISV[404] (mean) to ISV[443] (max). One set of advantage
    weights ⇒ ANY env exceeding the threshold should arm the gate.
(5) Per-env tile owned by GpuExperienceCollector (not the trainer) —
    the collector knows alloc_episodes (= n_envs); the trainer's
    batch_size is a different quantity. Reset to zero via the
    sp15_dd_state_per_env registry-arm dispatch.
(6) Per-step launch order: dd_state → dd_state_reduce →
    alpha_split_producer → final_reward, all on the same stream
    (CUDA serialises producer→consumer without explicit event sync).
(7) HEALTH_DIAG semantic shift (documented breaking change): slots
    401-406 now report cross-env mean, not env-0 value. For n_envs=1
    smoke configs the mean equals env-0's value (bit-stable migration).
(8) Layout fingerprint break: added markers DD_PERSISTENCE_MAX=443;
    ISV_TOTAL_DIM=444; DD_STATE_PER_ENV=sp15_phase_1_3_b_followup.
    Pre-followup checkpoints will not load (greenfield OK per spec Q1).
(9) 6 oracle tests migrated + 1 NEW behavioral test
    `dd_state_per_env_diverge_independently` — two-env config (env-0
    in recovery, env-1 deepening) verifies independent trajectories
    + mean-aggregate + max-aggregate semantics.

Phase 1.3.b-followup-B (separate split): dd_trajectory_decreasing_kernel
+ per_insert_pa migration is NOT in scope. The current Wave 4.3 reads
ISV[439] at insert-batch time (epoch end) — applied uniformly to ALL
inserted transitions (a known pre-existing limitation). Proper fix
requires per-(env, t) trajectory buffer [N*L] + env_id-aware lookup
in per_insert_pa via env_id = (j % (N*L)) / L. That's a different
contract change; splitting preserves no-partial-refactor within each
migration.

Atomic per feedback_no_partial_refactor: all 5 consumers of single-env-
canonical DD slots (final_reward kernel + plasticity kernel +
HEALTH_DIAG diagnostic + 6 oracle tests + new behavioral test) migrate
to per-env tile lookup in this commit; the new DD_PERSISTENCE_MAX
ISV slot lands with its sole consumer (plasticity).

Verified: SQLX_OFFLINE=true cargo check -p ml --features cuda clean;
cargo check -p ml --features cuda --tests clean;
CUDA_COMPUTE_CAP=86 cargo test -p ml --test sp15_phase1_oracle_tests
--features cuda -- --ignored: 17/17 oracle tests green (2 dd_state
incl. new per-env behavioral + 5 plasticity + 3 dd_trajectory + 6
final_reward + 1 per_sampler); cargo test -p ml --features cuda --lib:
947 pass / 12 fail HOLDS the 483cef454 baseline (no new regressions).

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