Commit Graph

3900 Commits

Author SHA1 Message Date
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
jgrusewski
9d4c9efa05 cleanup: remove legacy Sequential q_network + tune config for dense reward
Removed the legacy non-branching Sequential q_network and target_network
from DQNAgent. These were never used (branching+dueling always active)
but allocated VRAM and ran noise resets every step. -190 lines.

Config tuning for dense micro-reward system:
- n_steps: 5→1 (TD(0), micro-rewards cancel over n>1)
- tau: 0.007→0.01 (faster target tracking for TD(0))
- c51_alpha_max: 0.5→1.0 (full C51, PopArt handles normalization)
- curiosity_weight: 0.1→0.0 (dense micro-reward replaces curiosity)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:17:34 +02:00
jgrusewski
429967dcd1 feat: precompute OFI deltas + book aggression + bar duration in fxcache
FXCACHE_VERSION 3→4. Three precomputed features added to OFI region:
- ofi[8..16): temporal deltas (ofi[bar] - ofi[bar-1]) for 8 features
- ofi[16]: book aggression (MBP-10 10-level center-of-mass asymmetry)
- ofi[17]: log bar duration (imbalance bar formation time, normalized)

Previously 10 of 18 OFI embed MLP inputs were zero. Now all 18 have
real data: raw_ofi(8) + delta_ofi(8) + book_aggression(1) + log_duration(1).

Added read_state_sample() diagnostic for GPU state verification.
Legacy v3 fxcache handled by zeroing new slots (v2→v4 graceful upgrade).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:11:03 +02:00
jgrusewski
f961b6ad64 fix: disable rank normalization + n_steps 5→1 + per_alpha 0.6→0.3
Three signal-killing issues fixed:

1. Rank normalization DISABLED — was double-normalizing with PopArt,
   destroying magnitude difference between micro-rewards (±0.1) and
   trade exits (±5.0). PopArt alone preserves relative magnitude.

2. n_steps 5→1 (TD(0)) — dense micro-rewards alternate ±0.1 each bar.
   With n=5, they cancel out over 5 bars. TD(0) preserves the per-bar
   signal that the temporal pipeline needs to learn from.

3. per_alpha 0.6→0.3 — lower = more uniform PER sampling. Dense micro-
   rewards have tiny TD-errors (easy to predict), so high alpha ignores
   them. Lower alpha ensures micro-reward experiences get sampled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 08:06:36 +02:00
jgrusewski
353c8d81ab tune: revert LR 2e-5 → 1e-5 — too aggressive with architectural changes
Linear scaling rule (2x batch → 2x LR) doesn't hold for DQN+PER with
major architectural changes (Hold action, OFI embed, wider attention).
The model needs stability to learn the new action space, not speed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 07:53:29 +02:00
jgrusewski
afb37b3a31 fix: zero-init W_O and OFI embed weights — prevent trunk feature corruption
Attention W_O was Xavier-initialized, injecting ~sqrt(D) magnitude noise
into the residual connection. With the DtoD→copy_f32 fix making attention
active (was a no-op before), random W_O corrupted all branch head inputs
→ MaxDD=100% on validation.

Fix: W_O zero-init → attention starts as identity (output ≈ h_s2).
W_Q/K/V stay Xavier (they project to SDP space, not the residual).

OFI embed weights also zero-init → Mamba2 history starts as [h_s2; 0]
and attention input starts as [h_s2; 0]. No noise injection at init.

Both learn from zero as gradients shape them toward useful patterns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 07:50:44 +02:00
jgrusewski
d625ca28e8 fix: q_readback buffer 12→total_actions (13 with Hold action)
Hardcoded 12 Q-values in pinned readback buffer and per-branch Q-gap
slice caused panic with b0=4 (Hold action: 4+3+3+3=13 total actions).
Now uses dynamic total_actions from config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 01:39:20 +02:00
jgrusewski
8022fb96eb feat: OFI embed MLP backward — full gradient flow from Mamba2 + attention
Accumulates d_ofi_embed from Mamba2 (d_h_history extract) and attention
(d_input_scratch extract). ReLU mask → cuBLAS dW GEMM → 2-phase bias
reduce → Adam update on 190 params. Contiguous param buffer for Adam.

Gradient flow complete: micro-reward → C51/MSE loss → backward through
trunk → Mamba2 d_h_history → extract d_ofi_embed + attention d_input →
extract d_ofi_embed → accumulate → MLP backward → Adam → updated OFI
embedding weights.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 01:33:41 +02:00
jgrusewski
aad17fa869 feat: attention enrichment D→D+10 + backward gradient exposure
Widens W_Q/W_K/W_V from [D,D] to [D,D+10]. OFI embed (10-dim) is
concatenated to attention input via attn_concat_input kernel. Cross-
feature attention now sees order flow dynamics alongside trunk features.

Backward: d_input_scratch widened to [D+10,B]. extract_attn_ofi_grad
kernel extracts the OFI gradient portion. d_ofi_embed_attn buffer
exposed via accessor for Task 8 gradient accumulation.

W_O stays [D,D], LayerNorm stays D-wide — only input projection is
affected. SDP operates on Q/K/V which remain [D,B].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 01:17:16 +02:00
jgrusewski
36d02b8f3c feat: Mamba2 d_h_history backward — gradient flows to OFI embed MLP
2 new cuBLAS GEMMs: d_h_history = W_A^T @ d_gate + W_B^T @ d_x.
Extracts last 10 dims as d_ofi_embed_mamba2, accumulated across K=8
timesteps. This enables the OFI embed MLP to learn from temporal
patterns discovered by the Mamba2 SSM.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 01:03:31 +02:00
jgrusewski
227d5cb5fb feat: Mamba2 history enrichment SH2→SH2+10 — temporal scan learns OFI momentum
h_history widened from [B, K, SH2] to [B, K, SH2+10]. OFI embed (10-dim)
appended per timestep. W_A/W_B projections grow to [SH2+10, STATE_D].
W_C stays [SH2, STATE_D] — operates on scan output, not history input.

The SSM temporal scan now sees order flow momentum, book aggression, and
bar duration alongside trunk features. Enables learning sequential
patterns in microstructure dynamics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:56:00 +02:00
jgrusewski
6575eea8da feat: OFI embed MLP forward (18→10 cuBLAS) — learned order flow compression
Extracts [raw_ofi(8); delta_ofi(8); book_aggression(1); log_duration(1)]
= 18-dim from state vector, compresses to 10-dim via cuBLAS SGEMM +
bias+ReLU. Xavier init. Runs in training forward path before Mamba2
and attention, making the embedding available for temporal enrichment.

190 trainable params (18×10 + 10 bias). ~0.05ms per step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:49:18 +02:00
jgrusewski
9b61e46a71 feat: Hold action — backtest kernel, epsilon_greedy, action selector, defaults
Completes the Hold action implementation across remaining files:
- backtest_env_kernel.cu: Hold skips trade execution, fixes Flat
  encoding from dir=1→dir=3, fixes actual_dir re-encoding
- epsilon_greedy_kernel.cu: 5-action exposure→4-action direction
- gpu_action_selector.rs: count bonuses [f32;5]→[f32;4]
- gpu_iqn_head.rs: default branch_0_size 3→4
- dqn.rs: default num_actions 7→4
- All test assertions and doc comments updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:40:56 +02:00
jgrusewski
b800591c22 feat: 4th direction Hold action — keep position unchanged, zero cost
Action space 81→108: direction(4) × magnitude(3) × order(3) × urgency(3).
Short(0), Hold(1), Long(2), Flat(3). Hold keeps current position with
zero transaction cost, giving the model a "do nothing" option.

Changes: trade_physics.cuh (Hold returns current_position), env_step
(Hold skips trade execution), action.rs (ExposureLevel::Hold variant),
config (branch_0_size 3→4), all match arms updated across 11 files.

Counterfactual mirror: Short↔Long, Hold↔Hold, Flat↔Flat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:29:24 +02:00
jgrusewski
115623dbc4 fix: replace 11 memcpy_dtod_async with graph-safe copy_f32 kernel
memcpy_dtod_async is NOT captured by CUDA Graph — silently skipped
during replay. This caused:
- Attention reading stale trunk data (h_s2→scratch copy skipped)
- Attention output never written back (scratch→h_s2 copy skipped)
- Backward gradients using stale logits (d_logits staging skipped)
- Vaccine comparison using stale reference (prev_grad snapshot skipped)

Attention has been effectively a NO-OP since CUDA Graph was introduced.

Fix: new copy_f32 kernel in graph_utility_kernels.cu (same pattern as
existing mamba2_copy_enriched). 11 call sites replaced across
gpu_dqn_trainer.rs and fused_training.rs. graph_safe_copy_f32() and
graph_safe_copy_f32_on() helpers for single-stream and multi-stream.

Non-graph-captured DtoD copies (checkpoint, init, eval) left as-is.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:28:41 +02:00
jgrusewski
55d70b7cc8 feat: dense micro-reward + DSR fix + counterfactual sign fix
Dense micro-reward: OFI momentum × price confirmation (MBP-10 mid-price
mark-to-market) × adaptive cost tolerance (capital_ratio × Sharpe_ema)
+ book aggression + retrospective hold quality bonus. Replaces flat
-0.0001 holding cost. Scale: micro_reward_scale=0.1.

DSR Sharpe EMA: was hardcoded price_change_dsr=0.0 — adaptive cost
tolerance was permanently floored at 0.1. Now uses actual per-bar
returns from ps[PREV_CLOSE_SLOT].

Counterfactual: cf_cycle==1 (magnitude) and cf_cycle==2 (order) now
undo do_flip before computing CF reward, then re-apply. Previously
2/3 of counterfactual experiences had wrong sign when do_flip=true.

Rank normalization threshold: 0.001 → 1e-5 for dense micro-rewards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 00:03:51 +02:00
jgrusewski
c2c1802e58 fix: walk-forward purge gap 100→2000 bars (~1 trading day)
100 bars = ~1 hour of imbalance bars. ES futures regimes persist 4-8
hours. Val_Sharpe was artificially inflated by regime autocorrelation
bleeding through the too-short purge gap. 2000 bars = ~1 full trading
day, ensuring validation data is from a different regime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 23:58:46 +02:00
jgrusewski
de8a139494 fix: wire OFI pipeline + PORTFOLIO_STRIDE 30→38 + fix concat_ofi indices
3 pre-existing bugs fixed:
- ofi_gpu uploaded but never passed to env_step — now wired via
  full_batch_states param (OFI at state[66..74) accessible in kernel)
- concat_ofi_features read indices 42/45 (portfolio features) instead
  of 66/69 (actual OFI location in 96-dim state vector)
- PORTFOLIO_STRIDE expanded 30→38 for prev-OFI delta storage (ps[30..37])

Added PREV_CLOSE_SLOT=6 and PREV_MID_SLOT=22 for MFT mark-to-market.

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 23:47:04 +02:00
jgrusewski
f7cf02f363 plan: OFI momentum + dense micro-reward — 9 tasks, full gradient flow
9-task implementation plan covering:
- fxcache target_dim 4→6 (raw_open + mid_price_open from MBP-10)
- OFI data pipeline fix (ofi_gpu wiring, indices 42→66, PORTFOLIO_STRIDE 30→38)
- Dense micro-reward (OFI momentum × price confirm × adaptive cost tolerance)
- OFI embed MLP (16→8 cuBLAS) with full backward gradient flow
- Mamba2 history enrichment (SH2→SH2+8) + d_h_history backward
- Attention enrichment (D→D+8) + d_input_scratch exposure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 23:21:24 +02:00
jgrusewski
9f67cb0e6e spec: OFI momentum v4 — adaptive cost tolerance + fxcache auto-rebuild
Adds performance-adaptive cost penalty: profitable models (high Sharpe,
capital_ratio > 1) get lower effective trading costs, allowing more
frequent trading when alpha justifies it. Losing models face maximum
cost penalty, forcing selectivity.

Documents fxcache auto-rebuild: FXCACHE_VERSION 2→3 triggers automatic
cache regeneration via existing ensure-fxcache Argo step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 23:15:04 +02:00
jgrusewski
be977fa5a1 spec: OFI momentum v3 — add raw_open + mid_price_open from MBP-10
target_dim 4→6: adds raw_open (OHLCV) and mid_price_open (MBP-10
best bid/ask midpoint at bar formation) to the targets buffer.

Micro-reward now uses real intra-bar move (close - open) / atr
instead of close-to-close approximation. Also adds spread cost
awareness via |open - mid| penalty. Requires fxcache recomputation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 23:10:27 +02:00
jgrusewski
19808041aa spec: OFI momentum v2 — addresses all 12 review findings
Major changes from v1:
- Fix OFI data pipeline (ofi_gpu never wired, indices 42→66)
- Expand PORTFOLIO_STRIDE 30→38 for prev-OFI storage
- Add Mamba2 d_h_history backward (2 new cuBLAS GEMMs)
- Expose attention d_input_scratch for gradient flow
- Pre-compute OFI deltas in experience collection (state[74..82])
- Lower rank normalization threshold to 1e-5 for micro-rewards
- Use close-to-close return instead of unavailable open_price

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 23:06:46 +02:00
jgrusewski
67dedee49d spec: OFI momentum enrichment + dense micro-reward design
Dense per-bar reward using order flow momentum × price confirmation.
OFI embedding MLP (16→8 via cuBLAS) feeds into Mamba2 history AND
attention input. Replaces sparse exit-only reward with continuous
temporal signal for the SSM and attention heads to learn from.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 22:52:58 +02:00
jgrusewski
4f6b4540c5 fix: wire PopArt normalization + counterfactual double-negation bug
PopArt: normalize_rewards_popart_inplace was implemented but never
called from run_full_step. Without it, rank-normalized rewards [-1,+1]
spread across C51 atom range [-50,+50] — only 2% of atoms used,
near-zero C51 gradient. Now called before forward pass (Phase 0a).

Counterfactual: when do_flip=true AND cf_cycle==0 (directional mirror),
cf_reward = -reward double-negated (reward already flipped at line 1895).
Fix: cf_reward = do_flip ? reward : -reward. Affects ~1/6 of
counterfactual experiences that were teaching wrong directional signal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 17:56:42 +02:00
jgrusewski
ff62c7968a fix: gate conviction reward scaling by readiness — was killing gradient signal
The plan head's conviction output (sigmoid, [0,1]) scales rewards.
At init, conviction ≈ 0.1 (Xavier random weights through sigmoid),
which multiplied ALL rewards by 0.1 — killing the gradient signal.
The model couldn't learn meaningful Q-values, defaulted to uniform
random action selection → 1.7M trades on 4M bars → val_Sharpe -1000.

Fix: only apply conviction scaling when readiness ≥ 0.5 (plan head
mature). Before that, conviction defaults to 1.0 (identity). This
matches the existing readiness gate on position sizing (line 1416).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 17:30:52 +02:00
jgrusewski
ff7bbc7dd9 fix: remove cuStreamSynchronize — one-step lag is fine for atom_stats
The async pinned readback design intentionally reads previous epoch's
values. Epoch 1 shows zeros (lag), epoch 2+ shows real atom stats.
cuStreamSynchronize kills GPU pipeline — removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:53:48 +02:00
jgrusewski
65604db516 fix: add cuStreamSynchronize before pinned atom_stats read
populate_q_out + q_stats_reduce launch async kernels that write to
pinned device-mapped memory. The CPU was reading immediately — before
the GPU finished writing. Added cuStreamSynchronize to ensure the
kernel results are visible before the pinned memory read.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:51:46 +02:00
jgrusewski
a0b52b7876 fix: revert atom_stats from graph-captured replay_forward_ungraphed
atomicAdd inside CUDA graph replay causes non-determinism and is
unnecessary. The only correct path: reduce_current_q_stats() calls
populate_q_out() outside the graph to compute atom_stats cleanly.
Graph-captured path stays NULL for both atom_stats and q_var.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:44:23 +02:00
jgrusewski
d596cbbc20 fix: call populate_q_out in reduce_current_q_stats for atom_stats
The graph-captured forward pass never calls compute_expected_q (it
works directly on logits for C51 loss). So atom_stats_buf was never
written during training. Now reduce_current_q_stats() calls
populate_q_out() first — runs compute_expected_q outside the graph
to populate atom_stats before q_stats_reduce reads them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:41:45 +02:00
jgrusewski
f359905fe6 fix: wire atom_stats in replay_forward_ungraphed — second NULL call site
The first fix only patched populate_q_out(). The graph-captured path
uses replay_forward_ungraphed() which had its own null_atom_stats=0.
This is the call site that actually runs during training.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:32:55 +02:00
jgrusewski
64d0eb8bfe fix: wire atom_stats_buf + q_var to populate_q_out — C51 atom utilization was always 0%
compute_expected_q was called with null_atom_stats=0 (NULL pointer),
so the kernel skipped atom entropy/utilization accumulation entirely.
atom_stats_buf existed but was never passed. This disabled:
- G4 adaptive gamma annealing (uses atom_utilization)
- Homeostatic regularizer atom_util observable
- ISV atom utilization signal

Now passes atom_stats_buf.raw_ptr() with a cuMemsetD32 zero before
each call (kernel uses atomicAdd). Also passes q_var_buf_trainer for
per-action Q-variance computation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 16:11:39 +02:00
jgrusewski
28384836a7 tune: batch 8192→16384, lr 1e-5→2e-5, tau 0.005→0.007
Linear scaling rule: 2x batch → 2x LR to maintain effective update
magnitude. Tau increased to compensate for fewer steps/epoch (target
network tracks faster). H100 VRAM: 41GB free at B=8192, B=16384 adds
~4GB — easily fits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 15:38:31 +02:00
jgrusewski
a54f9b8e40 perf: reward_rank_normalize O(N²) → bitonic sort O(N log²N) + fix warnings
reward_rank_normalize: 10.6s single call replaced with bitonic sort
pipeline — compute_abs_sharpe + bitonic_sort_step × O(log²N) passes +
scatter_rank. Target: <50ms for 4M elements.

Fix: bias_grad_reduce partials buffer undersized — max_out_dim now
includes state_dim and absolute floor of 512.

Cleanup: suppress 3 compiler warnings (unused vars, dead fields).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 15:10:11 +02:00
jgrusewski
c274c43f27 perf: bias_grad_reduce_f32 + curiosity_bias_grad — 2-phase shared-memory
bias_grad_reduce_f32_kernel: 28K calls × 0.24ms = 6.6s total (11.4% GPU).
Serial batch loop → 2-phase shared-memory block reduce + final reduce.

curiosity_bias_grad_reduce: 4 calls × 134ms = 539ms (0.9% GPU).
Same serial pattern → same 2-phase fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 14:54:33 +02:00
jgrusewski
a79b8761cf perf: cuMemsetD8Async → cuMemsetD32Async for 4x memset bandwidth
All gradient buffer clears now use cuMemsetD32Async (u32-wide writes)
instead of cuMemsetD8Async (byte-wide). 4x memory bandwidth utilization
for the ~33 memset calls per training step. Size params converted from
bytes to f32 element count (.num_bytes() → .len()).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 14:15:45 +02:00