Commit Graph

3935 Commits

Author SHA1 Message Date
jgrusewski
af9373ced9 feat(D6/N6): ensemble as collapse oracle — pairwise Q-gap triggers plasticity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 20:41:25 +02:00
jgrusewski
087c21050a feat(D5/N5): information bottleneck — scalar penalty visible in HEALTH_DIAG
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 20:39:12 +02:00
jgrusewski
02f2f75f5e feat(D4/N4): counterfactual curriculum — cf_ratio scales with (1-health)
Replace hardcoded 0.5f flip probability in experience_env_step kernel with
dynamic cf_ratio = clamp(0.5 + 0.3*(1-health), 0.0, 1.0). Healthy (h=1)
keeps cf_ratio=0.5; collapsed (h=0) raises to 0.8 for more counterfactual
exposure to break collapse. Propagated via set_learning_health() each epoch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 20:36:43 +02:00
jgrusewski
7e345118f4 feat(D3/N3): plasticity injection — health-triggered shrink_perturb replaces periodic
Track consecutive epochs with health < 0.3 in `unhealthy_epoch_count`; after 3
consecutive unhealthy epochs, trigger shrink_and_perturb(alpha, sigma) and reset
the counter. Remove the periodic interval-based trigger (every N epochs). The
Phase 3 boundary trigger is intentionally kept as an independent mechanism.
last_plasticity_ready: Some(true) = accumulating, Some(false) = just triggered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 20:31:53 +02:00
jgrusewski
ebc1d9e9e2 feat(D2/N2): Q-gap barrier constraint — scalar loss visible in HEALTH_DIAG
Compute barrier_loss = 0.5 × max(0, 0.05×health − q_gap)² on the host
from cached q_gap_ema. Written to last_barrier_loss (already declared by
A4) and surfaced in HEALTH_DIAG `barrier=...`. Scalar-only / no gradient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 20:29:01 +02:00
jgrusewski
4911563b2a feat(D1/N1): temporal self-distillation — snapshot at high health, pull toward best during collapse
- Add q_snapshot.rs: SnapshotRing ring buffer (MAX_SNAPSHOTS=5), health/q_gap admission gate
- Add GpuDqnTrainer::maybe_snapshot_params() — DtoD copy of params_buf into snapshot slot
- Add GpuDqnTrainer::apply_distillation_gradient() — two ungraphed saxpy_f32_aux calls:
  grad += alpha * (params - best_snapshot), alpha = 0.1 * (1 - health), skipped when health >= 0.99
- Wire FusedTrainingCtx::maybe_snapshot_qnet() / apply_distillation() / last_distill_active()
- Call from process_epoch_boundary: snapshot when health >= 0.7, distill when health < 0.4
- No new CUDA kernel — reuses existing dqn_saxpy_f32_kernel (saxpy_f32_aux handle)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 20:26:36 +02:00
jgrusewski
8669b74d1f feat(C4/P4): temporal-coupled gamma — regime × health scales discount factor
Apply gamma_base + 0.005×(regime_stability−0.5) − 0.05×(1−health), clamped
to [0.9, 0.995], so stable regimes get longer-horizon credit assignment while
collapse (health=0) forces short-horizon local credit to break the collapse.

- GpuDqnTrainer: add last_gamma_eff field + apply_adaptive_gamma() method
- FusedTrainingCtx: add apply_adaptive_gamma() wrapper + last_gamma_eff() accessor
- training_loop: replace both set_adaptive_gamma call sites with apply_adaptive_gamma
- process_epoch_boundary: propagate last_gamma_eff into DQNTrainer for HEALTH_DIAG

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 20:18:06 +02:00
jgrusewski
8d088cbc38 feat(B4/G5): adaptive gradient budget — IQN/CQL/C51 scale with health & regime
IQN and CQL SAXPY contributions into grad_buf are now scaled by adaptive
budget factors derived from learning_health (ISV[12]) and regime_stability
(ISV[11]):
  iqn_budget = 0.10 + 0.30 × health  (0.10..0.40)
  cql_budget = 0.10 × (1−regime) × health  (0 at collapse, volatile+healthy → 0.10)
  ens_budget = 0.05  (constant)
  c51_budget = 1 − iqn − cql − ens  (C51 absorbs headroom at collapse → 0.85)

At collapse (health=0): IQN backed off to 10%, CQL off, C51 takes 85% —
stable directional learning when distributional components are unreliable.

Changes:
- GpuDqnTrainer: add 4 last_*_budget_eff fields (initialized to health=1 defaults)
- GpuDqnTrainer: add read_isv_health_and_regime() public accessor
- apply_iqn_trunk_gradient(): add iqn_budget param; scale = iqn_lambda × readiness × iqn_budget
- apply_cql_saxpy(): add cql_budget param; SAXPY alpha = cql_budget (was 1.0)
- FusedTrainingCtx: add compute_adaptive_budgets() — reads ISV, computes all 4, caches to trainer
- FusedTrainingCtx: add last_{iqn,cql,c51,ens}_budget_eff() accessors
- submit_aux_ops(): call compute_adaptive_budgets() once per step, thread to IQN/CQL sites
- training_loop.rs: B4/G5 propagation block for HEALTH_DIAG logging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 20:13:04 +02:00
jgrusewski
aaf6e70f7c feat(C1/P1): health-weighted PER priorities — boost diverse-action experiences during collapse
When learning_health < 0.8, the PER priority update switches from the
standard per_update_pa kernel to pow_alpha_diverse_f32, which multiplies
each priority by (1 + 2*(1-health)*|action - mean_action|). This rescues
the replay buffer's diversity signal during Q-collapse by surfacing
experiences whose action deviates from the batch mean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 20:04:38 +02:00
jgrusewski
e07c82976e feat(B3/G4): health-scaled Expected SARSA temperature — breaks collapse attractor
When health=1 (healthy): tau unchanged → sharp softmax → near-argmax target (deterministic).
When health=0 (collapsed): tau scales 6× → wide softmax → stochastic sampling breaks Q-collapse attractor.

- c51_loss_kernel.cu: add get_learning_health() helper, isv_signals as last param, tau_base → tau * factor
- gpu_dqn_trainer.rs: add last_sarsa_tau_factor field + init, launch_c51_loss → &mut self, host-side mirror, append isv_signals_dev_ptr arg
- fused_training.rs: add last_sarsa_tau_factor() accessor
- training_loop.rs: propagate SARSA tau factor from fused ctx for HEALTH_DIAG logging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 20:02:43 +02:00
jgrusewski
edc59ed6bd feat(B2/G3): health-coupled tau — floor at 0.01×(1-health) for collapse recovery
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 19:57:11 +02:00
jgrusewski
46666afef9 feat(B1/G2): uncertainty-gated CQL — cql_alpha = base × (1-regime) × health
Replace static cql_alpha with cql_alpha_eff computed from ISV signals:
health (ISV[12]) × (1 − regime_stability (ISV[11])) × base.
Collapse→0 (CQL off); volatile+healthy→full; stable+healthy→0.
Adds last_cql_alpha_eff field to GpuDqnTrainer (f32, init 0.0),
accessor on FusedTrainingCtx, and propagation into DQNTrainer for
HEALTH_DIAG logging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 19:52:01 +02:00
jgrusewski
1734ae7e34 feat(A4): extend HEALTH_DIAG with all effective values and novel placeholders
Adds 15 new last_* fields (all None) to DQNTrainer for B/C/D tasks to populate,
and replaces the A3 HEALTH_DIAG log line with the full format covering components,
effective hyperparams, and novel mechanism states.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 19:46:09 +02:00
jgrusewski
3e9a21d52b fix(A3): clarify docs and guard q_var against infinite sentinels 2026-04-20 19:43:24 +02:00
jgrusewski
6622d90906 feat(A3): compute LearningHealth per epoch, broadcast via ISV[12], HEALTH_DIAG logging
- Add HealthEmaTrackers struct (metrics.rs): EMA for q_gap/q_var/grad_norm with
  scalar grad_consistency proxy (successive grad_norm delta ratio)
- Add LearningHealth + HealthEmaTrackers + 5 last_* fields to DQNTrainer (mod.rs)
- Initialize new fields in constructor.rs
- Add write_isv_signal_at, read_atom_utilization, compute_q_spectral_gap to
  GpuDqnTrainer (gpu_dqn_trainer.rs) with coarse max/min spectral-gap proxy
- Forward same three methods on FusedTrainingCtx (fused_training.rs)
- Compute LearningHealth in process_epoch_boundary (training_loop.rs): reads
  per_branch_q_gap_ema, epoch min/max, atom_util, spectral_gap; emits HEALTH_DIAG log;
  broadcasts health_value to ISV[12] via write_isv_signal_at

Known proxy substitutions (documented in training_loop.rs comment):
  - grad_consistency: scalar proxy instead of full Adam vector cosine (per-component buffers)
  - spectral_gap: coarse max/min ratio on q_readback_pinned instead of SVD
  - ens_disagreement: 0.1 placeholder until D6/N6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 19:36:58 +02:00
jgrusewski
31f4f6314c docs(A2): add rationale for smoothstep thresholds and clamp bounds 2026-04-20 19:29:10 +02:00
jgrusewski
586f5c1058 feat(A2): LearningHealth module with 7-component composition + EMA 2026-04-20 19:25:15 +02:00
jgrusewski
1ad09f63c9 chore(A1): refresh ISV_DIM annotation comments 12→13
Follow-up to 9f3cbc77d — field-doc comments referenced the old value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:20:40 +02:00
jgrusewski
9f3cbc77d7 feat(A1): extend ISV_DIM 12→13, add LEARNING_HEALTH_INDEX constant 2026-04-20 19:18:22 +02:00
jgrusewski
ab7567875a plan: adaptive learning dynamics — 19 tasks across 5 phases
Implementation plan for unified LearningHealth system (all gems/pearls/novels):
- Phase A (A1-A4): ISV extension, LearningHealth module, training loop wiring,
  HEALTH_DIAG logging
- Phase B (B1-B4): G2 uncertainty-gated CQL, G3 health-coupled tau,
  G4 temp-continuous Expected SARSA, G5 adaptive gradient budget
- Phase C (C1-C4): P1 health-weighted PER priorities, P2/P3 subsumed by A3,
  P4 temporal-coupled gamma
- Phase D (D1-D8): N1 temporal self-distillation, N2 Q-gap barrier,
  N3 health-triggered plasticity, N4 CF curriculum, N5 information bottleneck,
  N6 ensemble oracle, N7 contrarian override, N8 meta-Q collapse predictor
- Phase E (E1-E2): collapse-recovery smoke test + L40S deployment verification

Spec: docs/superpowers/specs/2026-04-20-adaptive-learning-dynamics-design.md
Target: WinRate >55% by epoch 20, Q-gap stays above 0.1 (no collapse).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:12:41 +02:00
jgrusewski
c2f61a0129 spec: review fixes — labels, dedup, P2 as 7th component, WinRate 55%
- Layer 2 labeled as G2-G5, G5 explicit for gradient budget
- P2 spectral_gap_norm added as 7th component in composition (rebalanced weights)
- N6 ensemble oracle has explicit threshold (>0.8 triggers N3 immediately)
- N8 meta-Q wiring clarified: logged but not in composition until validated
- Files Changed deduplicated, paths qualified
- Success criteria: WinRate >55% (above random baseline ~25%)
- HEALTH_DIAG log line includes all 7 components

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:58:35 +02:00
jgrusewski
ff5e578bc3 spec: adaptive learning dynamics — LearningHealth + 4 gems + 4 pearls + 8 novels
Fixes Q-value collapse via unified LearningHealth signal that senses
training health (6 components) and continuously adapts:
- CQL regularization (regime + health gated)
- Gradient budget (IQN/CQL/Ens/C51 dynamic allocation)
- Tau target EMA (health-coupled)
- Expected SARSA temperature (continuous, no hardcoded threshold)

Plus 4 pearls (PER priorities, spectral detection, gradient consistency,
adaptive gamma) and 8 novels (self-distillation, barrier loss, plasticity
injection, CF curriculum, information bottleneck, ensemble oracle,
contrarian override, meta-Q network).

Core principle: training hyperparameters are OUTPUTS of the temporal
pipeline, not static schedules. The system meta-learns its own settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:55:26 +02:00
jgrusewski
6e0cb3d62c fix: CRITICAL — dense micro-reward + OFI embed read stale state positions
Task 3 refactored experience_state_gather (the WRITER) to use assemble_state()
with canonical layout (OFI at [42..62)). But env_step and ofi_embed_build_input
(the READERS) still hardcoded OFI at [66..84) — the OLD pre-refactor layout.

This meant 7 locations were reading MTF/portfolio features as if they were OFI:

1. Line 1745: dense micro-reward ofi_cur = state+66 → actually MTF[4]
2. Line 1778: book_aggression = state[82] → actually plan_isv region
3. Line 1983: ps[30..37] OFI delta storage for NEXT bar — storing MTF data
4. Lines 5833/5836/5838/5840: ofi_embed_build_input — feeds 18→10 MLP into
   Mamba2 temporal SSM and attention. Entire temporal pipeline was training
   on MTF features dressed as OFI.

Symptoms explained:
- WinRate=20.9% on validation (anti-correlated): dense micro-reward computes
  quality=sign_pos × garbage_MTF_deltas, systematically rewarding wrong direction
- mean_reward=+0.004 but Sharpe_raw=-0.0004: shaped reward exploits garbage
  signal, real portfolio loses money
- grad_norm=23560 at epoch 2: gradients chasing noise
- Q-value explosion to ±10 in one epoch: learning contradictions

Fix: replaced all hardcoded 66/74/82/83 with SL_OFI_START from state_layout.cuh.
Both reader kernels now use the same canonical layout as assemble_state().

Verified locally: smoke test passes, OFI_DIAG shows correct non-zero values
(raw_mean=-0.36, delta_mean=-0.21, log_dur=-0.23).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:30:25 +02:00
jgrusewski
904185004c feat: L40S GPU profile + auto-derive cuda-compute-cap from GPU pool
argo-train.sh now auto-selects cuda-compute-cap based on --gpu-pool:
  - ci-training-h100* → sm_90 (Hopper)
  - ci-training-l40s  → sm_89 (Ada Lovelace)

Added config/gpu/l40s.toml:
  - batch_size=4096 (between H100's 8192 and A100's 2048)
  - buffer_size=300K (scaled for 48GB VRAM)
  - gpu_timesteps_per_episode=2000 (bandwidth-limited)
  - gpu_n_episodes=2048 (scaled from H100's 4096)

GPU profile loader maps "L40S" → "l40s" (was "a100" fallback).

Also fixed pre-existing test drift: num_atoms=52 in h100.toml/a100.toml
was 51 in test expectations (padding alignment for C51 kernels).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:59:06 +02:00
jgrusewski
733b2c32ec test: add state layout match integration test
Verifies the canonical state layout at all 5 sections (market/OFI/MTF/portfolio/padding).
If any section drifts to the wrong offset, this test fails. Since training and
backtest share assemble_state() in state_layout.cuh, a passing test guarantees
both paths produce identical layouts.

Result: ✓ market=41 ofi=9 mtf=9 portfolio=5 padding=all-zero

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:35:41 +02:00
jgrusewski
882497caa4 fix: OFI_DIAG reads new canonical indices [42..62), remove state_dim from evaluate_baseline
- OFI_DIAG now reads positions [42..62) using OFI_START constant instead of
  hardcoded [66..74). Verified: raw_mean=0.0891, delta_mean=-0.0370,
  book_agg=0.4500, log_dur=-0.2303 (was all zeros before).
- Removed 3 stale state_dim field initializers from evaluate_baseline.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:33:42 +02:00
jgrusewski
66bc8d12e5 refactor: remove configurable state_dim — use STATE_DIM constant everywhere
Remove pub state_dim field from DQNConfig and GpuReplayBufferConfig; remove the
state_dim field from GpuExperienceCollector. Replace all reads with
ml_core::state_layout::STATE_DIM (and STATE_DIM_PADDED for cuBLAS-padded
strides). Checkpoint loading now validates saved state_dim against the
constant and hard-errors on mismatch. GpuAttentionConfig.state_dim is a
distinct attention-feature dim and is left untouched.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:02:51 +02:00
jgrusewski
10b90268d6 feat: delete backtest_gather_kernel, use shared assemble_state() for validation 2026-04-20 15:01:03 +02:00
jgrusewski
65ad9debbb refactor: experience_state_gather uses assemble_state() — fixes OFI/MTF collision
The kernel now writes to local arrays (market[], portfolio[], plan_isv[],
mtf[], ofi[]) then calls assemble_state() from state_layout.cuh to produce
the canonical layout. This eliminates the hardcoded ofi_start=66 that
collided with MTF features and ensures training/validation use identical
state vectors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:49:03 +02:00
jgrusewski
62650aa53b feat: add state_layout.cuh — CUDA header with layout constants and assemble_state()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:41:16 +02:00
jgrusewski
c60cd98a35 feat: add state_layout constants module — single source of truth for STATE_DIM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:39:30 +02:00
jgrusewski
bc8fde9a2b plan: unified state layout implementation — 8 tasks
Task 1: Rust constants (ml-core/state_layout.rs)
Task 2: CUDA header (state_layout.cuh + assemble_state())
Task 3: Refactor experience_state_gather to use shared assembly
Task 4: Delete backtest_gather_kernel, replace with shared function
Task 5: Remove configurable state_dim everywhere (131 call sites)
Task 6: Update OFI_DIAG indices + checkpoint validation
Task 7: Layout match integration test
Task 8: Deploy and verify on H100

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:36:03 +02:00
jgrusewski
1d8c82221c spec: unified state layout — fix train/validation mismatch
Training and validation use different kernels with different state layouts,
making generalization impossible. Single state_layout.cuh + one assembly
function + STATE_DIM as compile-time constant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:29:53 +02:00
jgrusewski
a7ef847922 cleanup: remove OFI diagnostics + dead bf16 scatter_insert kernel
Diagnostics served their purpose — confirmed OFI flows through full
pipeline on H100 (state_gather → PER → trainer). Remove:
- OFI host verify readback (upload_ofi_features)
- STATE_GATHER_DIAG pinned memory readback (timestep loop)
- Dead bf16 scatter_insert kernel (states are f32, was never called)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:18:04 +02:00
jgrusewski
7af0a228fb fix: PER insert used 1D kernel for 2D state matrices — model trained on garbage
scatter_insert_f32 is a 1D scalar kernel (5 args: dst, src, cursor, cap,
batch_size). insert_batch called it with 6 args for state matrices, passing
state_dim as batch_size. CUDA silently dropped the 6th arg (actual batch_size).

Result: only 96 floats (1 state row) inserted per experience batch into PER.
The model was training on ~99.99% uninitialized GPU memory. This bug affected
ALL state features, not just OFI — market features and portfolio were also
garbage in PER-sampled training batches.

Fix: added scatter_insert_f32_rows kernel (2D-aware, 6 args: dst, src,
cursor, cap, state_dim, batch_size) matching the existing scatter_insert
(bf16) pattern. States and next_states now use the row-aware kernel.

Verified locally: OFI_DIAG shows non-zero values through the full chain
(state_gather → env_step → PER insert → PER sample → trainer states_buf).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 13:57:23 +02:00
jgrusewski
bd0d90482d diag: pinned memory readback of batch_states after state_gather
Uses PinnedHostBuf + cuMemcpyDtoHAsync for state_gather diagnostic.
Reads batch_states[0..state_dim] to verify OFI at positions [66..84).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 13:31:16 +02:00
jgrusewski
4ddd6e1ffa diag: verify OFI host data before GPU upload + state_gather readback
Host-side check (zero-cost) before clone_htod to confirm data isn't
zero before it reaches the GPU. Fixes crash from previous diagnostic
(memcpy_dtoh size mismatch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 13:19:26 +02:00
jgrusewski
2f7828aefb diag: GPU readback after OFI upload + state_gather to trace H100 zero OFI
Temporary diagnostic: readback ofi_gpu[0..20] after upload and
batch_states[66..84] after first state_gather. OFI works locally on
RTX 3050 but shows zeros on H100 with identical v4 cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 13:10:16 +02:00
jgrusewski
602628f698 fix: reject legacy v2/v3 fxcache — force v4 rebuild with correct OFI
The PVC had a v2 cache that passed has_ofi and nonzero checks but contained
stale OFI data computed by an old binary. load_fxcache accepted v2/v3 for
backward compat, keeping the stale cache alive across every deploy.

v4 is the only valid version. Legacy loading code removed (-50 lines).
Argo ensure-fxcache will delete the v2 cache and rebuild from 148GB MBP-10.

Verified locally: v4 cache produces OFI_DIAG raw_mean=-0.2391 (non-zero).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 12:49:07 +02:00
jgrusewski
a240b7a8c4 fix: remove Option<> from ofi_gpu, remove default total_bars=10000
ofi_gpu is unconditional — wrapping in Option allowed silent NULL pointer
fallback to kernel. Now a plain CudaSlice<f32>.

total_bars default was 10,000 (a lie) — must come from actual data length.
Default changed to 0 with hard error if not set before collect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 12:23:07 +02:00
jgrusewski
e41c4909f1 fix: validate OFI data content, not just has_ofi flag — v2 cache had all-zero OFI
The v2 fxcache on PVC passed has_ofi=true validation (mbp10_dir was present
when built) but contained all-zero OFI data. The old binary set the flag
based on directory existence, not actual computed values.

Now counts non-zero OFI rows before accepting a cache — forces rebuild
when MBP-10 data is available but OFI content is all zeros.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 12:08:11 +02:00
jgrusewski
4ce2fb7d4b fix: make OFI unconditional, fix *8→*20 dimension bug, remove 882 lines dead code
OFI (Order Flow Imbalance) features are mandatory — the model is worthless
without MBP-10 order book data. Every silent fallback that degraded to zeros
has been converted to a hard error.

Critical fixes:
- build_batch_states used *8 instead of *20 for OFI dimensions — every
  walk-forward backtest was reading corrupted OFI features from adjacent memory
- precompute_features early exit skipped cache rebuild when stale v2/v3
  cache existed with has_ofi=false — now validates has_ofi before skipping
- 14 silent OFI fallback paths converted to hard errors across data loading,
  training loop, experience collector, state construction, metrics, hyperopt

Dead code removed (-751 lines):
- DoubleBufferedLoader (superseded by init_from_fxcache)
- GpuBufferPool (superseded by init_from_fxcache)
- DqnGpuData::upload legacy method (no OFI support)
- CPU training fallback path (CUDA always required)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 11:50:26 +02:00
jgrusewski
c5d84dcc78 fix: wire OFI features into state_gather kernel — OFI was always zero in replay buffer
The experience_state_gather kernel assembled states from market_features
+ portfolio but NEVER included OFI features. The ofi_gpu buffer was
uploaded to GPU but never passed to the gather kernel. Comment at line
693 said "appended after this kernel" but that code was never written.

Fix: add ofi_features + ofi_dim parameters to the kernel. Writes OFI
at state[66..84): raw_ofi(8) + delta_ofi(8) + book_aggression(1) +
log_duration(1). Now verified non-zero via diagnostic:
  OFI_DIAG: raw_mean=0.099, delta_mean=2.901, book_agg=-0.001, log_dur=0.526

This was THE fundamental data pipeline bug — the OFI embed MLP, Mamba2
enrichment, attention enrichment, and dense micro-reward all read zeros
for OFI features during training. Now they get real order flow data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 10:05:37 +02:00
jgrusewski
7b12df7950 fix: LN backward arg mismatch + save D-wide residual for dgamma
Two bugs in attention LN backward:

1. attn_layer_norm_bwd_dx: Rust passed 8 args (old monolithic kernel
   signature) but the split kernel takes 6. Args 2-3 (saved_input,
   projected_buf) shifted gamma/rstd/d_x/D/B → total corruption.
   Fixed: removed extra args, kernel now receives correct 6 args.

2. attn_layer_norm_bwd_dgamma_p1: passed saved_input (D+10 wide) as
   LN residual, but kernel indexes with D stride → OOB access.
   Fixed: save states→ln_residual [D,B] during forward via copy_f32
   kernel (graph-safe). Backward reads D-wide ln_residual correctly.

Root cause: Phase 2 split of monolithic LN kernel into 3 parts didn't
update the Rust arg lists. The attention widening (D→D+10) exposed the
mismatch as CUDA_ERROR_ILLEGAL_ADDRESS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 09:34:55 +02:00
jgrusewski
f0d6f1f4d2 feat: ISV-adaptive exploration — epsilon + noisy sigma modulated by regime
Volatile regime → higher epsilon + sigma (explore more, uncertain market)
Stable regime → lower epsilon + sigma (exploit learned Q-values)

Reads ISV regime_stability + volatility from pinned host memory at epoch
start. Zero GPU cost — pinned memory is readable from CPU without sync.

Also adds read_isv_regime() accessor to GpuDqnTrainer + FusedTrainingCtx.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 09:12:10 +02:00
jgrusewski
caa01070c8 feat: C51 atom warm-start + robust PopArt (median/IQR) from bitonic sort
Atom warm-start: bitonic sort rewards → quantile positions → write to
atom_positions_buf as initialization. Existing SGD optimizer refines.
Atoms start where reward mass actually is instead of uniform [-50,+50].

Robust PopArt: median/IQR normalization from sorted rewards replaces
Welford mean/var. More robust for bimodal distribution (many ±0.1
micro-rewards + few ±5.0 trade exits). Conditional: popart_robust=true.

Both reuse the same bitonic sort (~14ms per epoch, amortized).
gather_quantiles kernel extracts positions. extract_median_iqr reads
Q25/median/Q75 from sorted buffer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 09:06:23 +02:00
jgrusewski
7923d6ff24 feat: ISV-adaptive micro-reward weights — regime routing adjusts reward composition
Volatile regime → price confirmation weighted higher (momentum matters)
Stable regime → book aggression + hold quality weighted higher
Regime transition → hold quality drops (don't hold through shifts)

Uses ISV signals[11] (regime_stability) and signals[2] (volatility)
already available in env_step_batch. Zero new parameters — the temporal
attention pipeline drives reward composition through ISV routing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:59:43 +02:00
jgrusewski
8394e17224 feat: wire config weights to kernel + revert C51 alpha + atom warm-start spec
Config weights wired end-to-end (5 files): price_confirm_weight,
book_aggression_weight, hold_quality_weight, micro_reward_temp now
parsed from [reward] TOML section → DQNHyperparameters → GpuExperienceConfig
→ kernel args. No more hardcoded magic numbers in micro-reward formula.

Reverted c51_alpha_max 1.0→0.5: full C51 collapsed atoms to 3% util
at epoch 30 (death spiral). MSE floor prevents atom collapse.

PopArt warmup 100→10: 100 batches = ~5 epochs unnormalized → unstable.

Added bitonic sort integration spec: 4 uses (atom warm-start, robust
PopArt, experience curriculum, top-K PER).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:51:42 +02:00
jgrusewski
8146613cfa fix: skip trivial Hold counterfactual + disable reward_noise + config weights
Counterfactual: Hold(1)/Flat(3) direction mirror maps to self — trivial.
Now falls through to magnitude CF for Hold/Flat instead of wasting a
replay buffer slot on same-action same-reward experiences.

reward_noise_scale: 0.05→0.0 (dense micro-rewards are already noisy,
adding 5% label noise destroys the per-bar signal).

Added micro-reward weight config fields (price_confirm_weight=0.5,
book_aggression_weight=0.3, hold_quality_weight=0.2, micro_reward_temp=3.0,
holding_cost_rate=0.0001) — defined in TOML, kernel plumbing next session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:22:56 +02:00