Commit Graph

1059 Commits

Author SHA1 Message Date
jgrusewski
7ef92983f9 fix(clippy): apply cargo clippy --fix across workspace
Mechanical auto-fixes: redundant borrows, clone on Copy, or_insert_with,
single-char push_str, get(0) → first(), needless borrow, let_and_return.
150 files, no behavior changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:17:51 +01:00
jgrusewski
e6e31305a2 fix(ml-dqn): suppress unused grad_norm_tensor warning without cuda
Prefix with underscore since it's only consumed in #[cfg(feature = "cuda")] block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:33:18 +01:00
jgrusewski
2662d5cda2 fix(build): gate GPU PER calls behind #[cfg(feature = "cuda")]
is_gpu_prioritized() and grad_norm squeeze are only available with CUDA.
Without the gate, compile-services (CPU-only) fails with E0599.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:25:52 +01:00
jgrusewski
9a96691001 perf(dqn): eliminate all GPU→CPU roundtrips from training hot path
Replace per-step to_scalar()/to_vec1() readbacks with GPU-resident
tensor accumulation and single epoch-boundary sync. On H100 this
removes ~12-15 pipeline flushes per training step (~5μs each),
enabling full GPU saturation with zero CPU sync in the inner loop.

Key changes:
- GpuTrainResult: train_step() returns GPU scalar tensors (loss_gpu,
  grad_norm_gpu) instead of f32 — zero readback per step
- Regime conditional: train all 3 heads unconditionally with
  zero-masked weights (mathematical no-op) instead of 3-6
  to_scalar() mask count checks per step
- GPU PER: max_priority as GPU tensor with flush_max_priority(),
  delta-based priority update via index_add (no CPU dedup)
- Deferred diagnostics: NaN detection, CQL logging, Q-value
  estimation all moved to epoch boundary where pipeline is
  already synced
- Legacy CPU-readback wrappers removed entirely

9 files changed, +393/-295 lines. 520 DQN tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:05:18 +01:00
jgrusewski
18166a9e6f perf(gpu): zero-roundtrip DQN experience collection via cuMemcpyDtoDAsync
Eliminate GPU→CPU→GPU roundtrip in the experience collection hot path.
Kernel outputs (states, rewards, actions, dones) now stay on GPU via
device-to-device copy into candle Tensors. Only rewards + actions are
downloaded for monitoring metrics (~0.5MB vs ~3.14GB at 32K episodes).

- Extract launch_kernel() helper from collect_experiences() (DRY)
- Add collect_experiences_gpu() — DtoD path for GPU PER
- Add cuda_slice_to_tensor_f32/i32_to_u32 DtoD copy utilities
- GPU next_states via tensor narrow/cat/reshape (no CPU loop)
- Trainer selects GPU vs CPU path based on is_gpu_prioritized()
- stream.synchronize() barrier ensures cross-stream data visibility

0 warnings, 1266 tests pass (874 ml + 392 ml-dqn)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:05:18 +01:00
jgrusewski
21462d5f0b perf(dqn): eliminate GPU→CPU roundtrips from training hot path
Forward-port from worktree-gpu-hotpath-audit (2 commits):

1. Zero-roundtrip DQN experience collection via cuMemcpyDtoDAsync:
   - GpuExperienceCollector uses device-to-device copies for state tensors
   - Shared memory weight caching in GPU replay buffer
   - NaN priority clamping on GPU (no CPU readback)

2. Eliminate all GPU→CPU roundtrips from training loop:
   - GpuTrainResult: loss + grad_norm stay as GPU scalar tensors
   - Single to_scalar() readback at epoch boundary (not per step)
   - GPU-resident loss accumulation across training steps
   - RegimeConditional head selection via GPU tensor ops
   - Removed per-step NaN diagnostic checks (now epoch-level)

Also removes dead `states_tensor` field from ComputeLossResult and
fixes redundant field name clippy warning.

Expected: ~15-20% training throughput improvement on H100 by
eliminating synchronous GPU→CPU transfers in the inner loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:04:36 +01:00
jgrusewski
e4a911941b chore: remove benchmark comment, trigger Docker rebuild + warm cache
Removes temporary benchmark comment. Docker changes from previous commit
will trigger image rebuilds. This run serves as warm-cache benchmark
after cold-cache run completes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:35:36 +01:00
jgrusewski
2933e0f014 chore: trigger full CI compile (cold PVC sccache benchmark)
Temporary comment to trigger detect-changes for both services and
training compile steps. Will be removed after benchmarking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:30:51 +01:00
jgrusewski
ff7d79b03a fix(gpu): resolve 3 pre-existing issues flagged by zen code review
1. PER duplicate index accumulation (HIGH): update_priorities_gpu()
   used index_add delta trick which accumulates deltas for duplicate
   indices, overshooting target priority. Now deduplicates the small
   indices tensor (batch_size=256, ~1KB) via HashMap before delta
   computation. Fast path (no dupes) reuses original tensors.

2. RegimeConditional silent experience drop (MEDIUM):
   insert_batch_tensors() returned Ok(()) for CPU replay buffers,
   silently discarding all GPU-collected experiences. Now converts
   tensors→Vec<Experience> via extracted helper (lazy, only on first
   CPU head encountered) and inserts into CPU buffer.

3. Unsafe direct indexing (LOW): gpu_experience_collector.rs used
   &states[s..e] in fallback paths, violating deny(indexing_slicing).
   Replaced with .get() safe bounds checking.

392/392 ml-dqn + 874/874 ml tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:53:16 +01:00
jgrusewski
5149c71444 feat(gpu): warp-cooperative DQN experience kernel for H100 (sm_90+)
Add warp-cooperative CUDA kernel for DQN experience collection that
exploits H100's improved warp shuffle throughput. On sm_90+, 32 threads
(1 warp) cooperate on each dot product via __shfl_xor_sync butterfly
reduction, cutting per-thread register pressure from ~5.5KB to ~200B
and enabling full occupancy on 132 SMs.

Key additions:
- 6 warp-cooperative device functions in common_device_functions.cuh:
  distributed/broadcast matvec (clean + NoisyNet), warp RMSNorm,
  warp_reduce_sum_all
- 4 TILE_LAYER_WARP_* macros using __syncwarp() for warp-level sync
- 3 warp forward passes: standard dueling, NoisyNet, C51 distributional
  (distributed heavy layers + broadcast atom layers for softmax)
- Full warp kernel: dqn_full_experience_kernel_warp with lane-0
  simulation, strided state scatter, warp-shuffle curiosity gather
- Host-side compute capability detection (sm_major >= 9) with
  automatic fallback to standard 256-thread kernel on older GPUs
- cuCtxSetLimit(STACK_SIZE, 16KB) for standard kernel safety

874/874 tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:44:59 +01:00
jgrusewski
f9b6cdb923 perf(gpu): H100 optimizations — dynamic NOISY_MAX_DIM, 32768 episodes, scratch aliasing
Three zen-validated optimizations for H100 GPU experience collection:

1. Dynamic NOISY_MAX_DIM: inject via NVRTC as max(state_dim, shared_h1,
   shared_h2). On H100 with SHARED_H1=512, noise now covers all 512 input
   dims instead of truncating at 256. Fixes exploration correctness bug.

2. MAX_EPISODES_LIMIT raised 4096→32768: H100 (132 SMs) can now run up to
   32768 episodes per kernel launch = 128 blocks = ~97% SM utilization
   (was 16 blocks = 12%). All 3 trainer.rs caps updated to match.

3. Scratch array aliasing: scratch_v and scratch_a now alias scratch1
   memory after shared layers complete, saving 1024 bytes per thread.
   Per-thread local memory reduced from ~6.5KB to ~5.5KB.

Zen validation confirms: kernel is safe and correct for H100 deployment.
Remaining architectural improvements (warp-cooperative matvec, batched
GEMM) deferred to future optimization phase.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:44:35 +01:00
jgrusewski
6b3bb26c05 fix(gpu-per): clamp priorities after index_add to prevent NaN weights
index_add with duplicate indices double-applies deltas — e.g. if index i
is sampled twice with delta -0.5, priority goes 1.0 + (-0.5) + (-0.5) = 0.0,
causing NaN in importance sampling weights. Post-clamp to [epsilon, 1e6]
ensures priorities stay positive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:34:29 +01:00
jgrusewski
9502c1e22c fix(gpu): dynamic SHMEM_MAX_IN_DIM prevents shared memory overflow on H100
Root cause: SHMEM_MAX_IN_DIM was hardcoded to 256, but on H100 with
hidden_dim_base=2048, SHARED_H1=SHARED_H2=512. The cooperative tile
loader wrote 64×512=32768 floats into shmem allocated for 64×256=16384,
causing CUDA_ERROR_INVALID_VALUE on kernel launch.

Fixes:
- Make SHMEM_MAX_IN_DIM injectable via NVRTC #define (was hardcoded)
- Compute max(state_dim, shared_h1, shared_h2) and inject at compile time
- Use dynamic value for host-side shmem_bytes allocation
- Shrink eps_out[NOISY_MAX_DIM] → eps_out[SHMEM_TILE_ROWS] (saves 768B/thread)
- Force smoke tests to Device::Cpu (CUDA driver sensitivity to binary layout)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 23:31:35 +01:00
jgrusewski
dd075ce02a fix(gpu): cap auto-scaled episode count at GPU buffer limit (4096)
optimal_n_episodes() returns 8192 on H100 (132 SMs) but the GPU
experience collector pre-allocates buffers for MAX_EPISODES_LIMIT=4096.
Exceeding that caused "Episode count exceeds allocated buffer size"
and silent fallback to CPU. Now all 3 auto-scaling sites clamp at 4096.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:43:34 +01:00
jgrusewski
befa7e2f55 perf(gpu): shared memory weight caching, GPU reductions, fix staging flush dim
CUDA kernel: cooperative_load_tile() + matvec_leaky_relu_shmem() cache weight
matrices in shared memory (SRAM), reducing HBM traffic 32x (1.76TB→55GB).
Block size increased to 256 threads for better SHMEM utilization.

Trainer hot-path: replace to_vec2 full-tensor CPU readbacks with GPU-native
flatten_all/min/max/mean_all (4 scalar readbacks). Merge two Q-diagnostic
forward passes into one. Deferred max_priority flush (1 GPU→CPU sync/epoch
instead of ~8/batch).

Fix StagedGpuBuffer::flush() dimension mismatch: used raw experience
state.len() (45) instead of gpu.state_dim() (48 aligned), causing
slice_scatter crash [500,48] vs [n,45] when CPU experiences flush into
GPU-aligned replay buffer.

Fix hardcoded state_dim=48 in optimal_n_episodes() — now uses dynamic
state_dim from network config (correct for OFI-enabled 56-dim states).

Dynamic episode auto-scaling guard: only auto-scale when gpu_n_episodes≥128
(production), respecting explicit test overrides (gpu_n_episodes=2).

Tests: 874 ml + 392 ml-dqn = 1266 pass, 0 fail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:24:33 +01:00
jgrusewski
3865354d8f perf(gpu): dynamic episode scaling + BF16 cross-entropy stabilization
GPU experience collector:
- Add GpuHardwareInfo with SM count detection from device name lookup
  (H100=132, A100=108, L40S=142, RTX 4090=128, etc.)
- optimal_n_episodes() scales to GPU: sm_count × 2 warps × 32 threads,
  capped by 15% free VRAM budget, 256-aligned for block scheduling
- Remove hardcoded .min(256) cap in trainer — auto-scales from 128 to 8192
- MAX_EPISODES_LIMIT raised from 256 to 4096

Numerical stability:
- BF16 cross-entropy epsilon: 1e-8 → 1e-4 (below BF16 precision floor
  1e-8 rounds to zero, making log-stabilization a no-op → -inf → NaN)
- Add log_probs.clamp(-20, 0) guard against -inf × 0 = NaN in loss

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:16:47 +01:00
jgrusewski
fb18e0f1dc fix(dqn): prevent BF16 softmax overflow, increase batch size for H100
BF16's 7-bit mantissa overflows on exp(50+) in softmax → Inf → NaN.
Cast distributional logits to F32 before softmax in both dueling and
rainbow network heads, then cast back to original dtype.

Increase default batch_size 128→1024 (standard), 128→256 (conservative),
512→2048 (aggressive) to saturate H100 tensor cores. AutoBatchSizer
already caps to VRAM ceiling for smaller GPUs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:05:42 +01:00
jgrusewski
e89fbc2b4d fix(metrics): wire training pod scraping to Grafana dashboard
Three root causes for "no metrics" on the training dashboard:

1. Dashboard template variables ($model, $fold) sourced from
   foxhunt_training_current_epoch which isn't emitted until the first
   epoch completes. Switch to foxhunt_training_step which fires from
   step 500 onward.

2. train.sh pod template missing Prometheus annotations
   (prometheus.io/scrape, port, path). Also add the
   app.kubernetes.io/component label to the eval manifest so
   evaluation pods are discoverable too.

3. DQN and PPO trainers only called set_epoch() at the END of each
   epoch. Move the call to the TOP of the epoch loop so the gauge
   exists from the first training iteration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:02:29 +01:00
jgrusewski
ee12f1d9e4 perf(gpu): zero-sync gradient clipping — eliminate all GPU→CPU barriers from hot path
clip_grad_norm now returns a GPU-resident Tensor instead of (f64, f64),
keeping backward → clip → optimizer.step fully pipelined on GPU with
zero cuStreamSynchronize stalls. The unconditional multiply trick
(scale = min(max_norm/(norm+eps), 1.0)) avoids the conditional branch
that previously required reading the norm to CPU.

Key changes:
- gradient_utils::clip_grad_norm: return Tensor, unconditional GPU multiply
- Handle BF16 mixed-precision via per-gradient to_dtype cast
- adam.rs: backward_step_with_monitoring returns Tensor (zero sync)
- gradient_accumulation: delegate to gradient_utils (DRY, same GPU path)
- dqn.rs: single sync boundary after ALL GPU work queued

1746 tests passing (ml-core 286, ml-dqn 388, ml-ppo 198, ml 874).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:26:45 +01:00
jgrusewski
db65eb56a5 perf(gpu): eliminate GPU→CPU sync barriers from training hot path
clip_grad_norm: accumulate squared norms on GPU-resident scalar, single
to_scalar() at end (was 16-20 per-param syncs per step × 2917 steps/epoch).

check_gradients_finite: same GPU-accumulation pattern, single sync.

gpu_replay_buffer sample_proportional/rank_based: generate random targets
via rand(0,1)*total_sum on GPU, normalize weights via broadcast_div
(eliminates 2 to_vec0 syncs per sample call).

gpu_replay_buffer update_priorities_gpu: replace CPU loop of 50 individual
slice_scatter calls with single batched index_add delta trick.

dqn NaN detection (every 500 steps): accumulate 3 NaN counts on GPU,
single to_scalar for total; detailed breakdown only if NaN found.

dqn dead neuron detection (every 1000 steps): accumulate dead count on
GPU-resident scalar (was to_vec0 per parameter tensor, ~16-20 syncs).

Net: ~22 GPU→CPU syncs + 50 micro-kernels per training step → 2 syncs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:02:16 +01:00
jgrusewski
0355fb17bc perf(metrics): expose step-level DQN metrics to Prometheus gauges
Move Q-value stats, gradient norm, and training step from tracing::info!
(stdout-only, block-buffered, invisible mid-epoch) to Prometheus gauges
(atomic set, scrapable every 15s). Downgrade formatted step logs to
debug! level to eliminate allocation/serialization overhead in the hot path.

New gauge: foxhunt_training_step (current step within epoch)
Updated: foxhunt_training_q_value_mean/max, foxhunt_training_gradient_norm
now update every 500 steps instead of only at epoch boundaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:44:29 +01:00
jgrusewski
7751f7615a fix(ci): unblock CPU service builds and fix H100 BF16 regime classification
Three fixes validated by 20/20 hyperopt trials on H100 (zero OOM):

1. Workspace default-features: ml-core, ml-dqn, ml-ppo, ml-supervised
   workspace deps now have default-features=false. Prevents cudarc
   (which requires nvcc) from leaking into CPU service builds via
   Cargo feature unification. CI compile-services was failing with
   "Failed to execute nvcc: No such file or directory" (exit 101).

2. BF16 comparison fix: Candle's gt()/le() don't support BF16 operands.
   Cast ADX/CUSUM features to F32 before threshold comparison in
   regime classification. Previous approach (cast threshold to BF16)
   failed due to Candle broadcast_as reverting dtype.

3. CI pipeline: expand ML change detection to all 14 sub-crates,
   add component:compile labels for sccache network policy matching,
   bump training runtime to CUDA 12.6 + Ubuntu 24.04 (glibc 2.39).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:11:04 +01:00
jgrusewski
379c0bee37 fix(dqn): cast F32 constants to input dtype for BF16 mixed precision on H100
On H100 with BF16 mixed precision enabled, Candle's comparison ops (gt,
lt, le, ge) and arithmetic ops require matching dtypes. F32 threshold
constants compared against BF16 state tensors caused every training step
to fail with "dtype mismatch in cmp, lhs: BF16, rhs: F32".

Fixes:
- regime_conditional.rs: ADX/CUSUM thresholds cast to adx.dtype()
- quantile_regression.rs: Huber kappa, zero, and half tensors match
  input dtype (both quantile_huber_loss variants)
- rainbow_network.rs: LeakyReLU slope and ELU alpha/one tensors cast
  to x.dtype()
- continuous_ppo.rs: Huber delta/half tensors cast to abs_diff.dtype()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:13:46 +01:00
jgrusewski
fe223b3843 fix(dqn): eliminate OOM in hyperopt by gating GPU features on small GPUs
Three root causes fixed:
1. GPU PER + experience collector (cudarc) created dual CUDA allocator
   fragmentation on GPUs ≤8 GB, making training impossible even at
   batch_size=1. Added `use_gpu_replay_buffer` config flag; both GPU PER
   and experience collector now disabled when VRAM ≤8192 MB.

2. Search space bounds were WIDENED instead of capped — max_batch_size
   returning 4096 replaced the original 512 upper bound, and
   max_hidden_dim_base_full returning 3072 replaced the original 1024.
   Fixed with min() to only narrow, never widen.

3. VRAM estimator assumed GPU features always active, overcharging when
   they're disabled on small GPUs. Now conditional: when
   replay_buffer_capacity=0 (proxy for GPU PER disabled), collector/cudarc
   costs are zero and fragmentation multiplier drops from 3× to 1.5×.

Additional small-GPU guard: GPUs ≤8 GB get clamped search space
(batch≤128, hidden≤512, atoms≤51, buffer≤50K) to fit 3 regime heads
+ C51 + noisy nets + dueling in limited VRAM.

Validated: 7/7 trials complete on RTX 3050 Ti 4 GB, zero OOM, best trial
Sharpe 9.8 with 50.6% win rate. Previous runs had 100% OOM failure rate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 16:35:38 +01:00
jgrusewski
06d0a8d618 Merge branch 'feature/dqn-branching'
perf(dqn): GPU-native branching action decomposition (eliminates cudaSync stall)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:36:01 +01:00
jgrusewski
1b9f09fce1 perf(dqn): eliminate GPU→CPU roundtrip in branching action decomposition
Replace `actions_tensor.to_vec1()` → CPU loop → `Tensor::from_vec` with
GPU-native `decompose_actions_batch_gpu` using F32 affine+floor arithmetic.
Removes the last `cudaDeviceSynchronize` stall in the branching training
hot path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:26:44 +01:00
jgrusewski
3a37454ea5 Merge branch 'feature/dqn-branching' 2026-03-09 13:17:50 +01:00
jgrusewski
89c3fb89d9 feat(dqn): Branching DQN with full GPU Rainbow parity (7 fixes)
Bring Branching Dueling Q-Network (Tavakoli 2018) to full Rainbow parity
with the existing GPU hotpath. 3 independent advantage heads (exposure=5,
order=3, urgency=3) decompose the 45-action space into learnable branches.

H1 - CUDA fallback: gate GpuExperienceCollector when use_branching=true
     (fused kernel hardcodes NUM_ACTIONS=5, incompatible with 45 factored)
H2 - Per-branch C51 distributional: each branch outputs [batch, n_d, atoms]
     log-softmax, loss = avg of D cross-entropies vs projected Bellman target
M1 - NoisyNet: MaybeNoisyLinear enum in branch heads, reset_noise/disable_noise
     wired through select_action, compute_loss, and set_eval_mode
M2 - Regime-conditional IS weights: Trending=1.2, Ranging=0.8, Volatile=0.6
     applied to branching loss via ADX/CUSUM features at state[40:41]
M3 - State dim alignment: align_dim_for_tensor_cores() in from_dqn_params()
     for H100 HMMA dispatch (8-byte alignment)
L1 - Fill simulator: splitmix64 replaces golden ratio hash (chi-squared tested)
L2 - Hyperopt 29D: branch_hidden_dim [64,256] added to PSO search space

Config plumbing: branch_hidden_dim, v_min/v_max/num_atoms, use_distributional,
use_noisy, noisy_sigma_init all flow from DQNConfig → BranchingConfig.

10 files, +3207/-125 lines, 33 branching tests + 387 ml-dqn + 284 ml-core pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:17:06 +01:00
jgrusewski
7f3066e695 feat(ml): enable BF16 mixed precision by default on CUDA
training_dtype() now returns BF16 on all CUDA devices, enabling full
tensor-core utilization. F32 is used only at boundaries (scalar
extraction, loss computation, softmax). VRAM estimator updated to
account for BF16 byte sizes, C51/QR atoms, dueling streams, NoisyNet
param doubling, and GPU PER buffer pre-allocation.

Changes:
- mixed_precision.rs: training_dtype() returns BF16 on CUDA
- curiosity.rs: F32 cast before scalar extraction
- network.rs: F32 output at NetworkLayers forward boundary
- traits.rs: estimate_trial_vram_mb_full() with BF16-aware sizing
- dqn.rs adapter: uses full estimator with worst-case architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:48:55 +01:00
jgrusewski
c0c44a5f17 feat(dqn): GPU-native regime classification with 42-dim feature vector
Expand FeatureVector from 40 to 42 dimensions by including ADX(14) at
index 40 and CUSUM direction at index 41 from the existing CPU feature
extraction pipeline. This eliminates proxy-based regime classification
and enables GPU-native regime detection via tensor narrow/comparison ops.

Key changes:
- extraction.rs: wire RegimeADXFeatures + RegimeCUSUMFeatures into
  extract_current_features_v2(), output 42 features per bar
- regime_conditional.rs: classify_regime_masks_gpu() creates per-regime
  mask tensors entirely on GPU (ADX > 0.25 = trending, |CUSUM| > 0.7 =
  volatile, else ranging). Zero CPU roundtrip in training hot path.
- trainer.rs/config.rs: state_dim 43→45 (no OFI), 51→53 (with OFI),
  aligned dims unchanged (48/56). GPU batch insertion for all 3 heads.
- CUDA header: MARKET_DIM 40→42
- walk_forward.rs: FEATURE_DIM 40→42
- 42 files updated, all [f64;40]→[f64;42] propagated across workspace

Test results: ml=874/0, ml-dqn=354/0, ml-features=282/0, ml-core=274/0
Real data GPU smoke tests: 7/7 passed (OHLCV + OFI + trade enrichment)
Hyperopt baseline RL: 2 trials completed on local RTX 3050 Ti

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:16:05 +01:00
jgrusewski
d12273e164 fix(dqn): GPU PER monitoring funcs used empty CPU experiences, causing cuBLAS crash
Four monitoring functions (estimate_avg_q_value_with_early_stopping,
collect_qvalue_statistics, compute_q_gap_for_epoch, compute_per_action_q_values)
iterated over batch_sample.experiences to build CPU tensors for forward passes.
When GPU PER (GpuPrioritized) is active, experiences is always vec![] — all data
lives on GPU tensors in gpu_batch. This created zero-element tensors with non-zero
shapes, triggering CUBLAS_STATUS_INVALID_VALUE on the next forward pass.

Fix: all four functions now check for gpu_batch.states and use it directly,
falling back to CPU experiences only for non-GPU buffers. Also removes debug
eprintln probes, wires new_on_device for DQN/RegimeConditionalDQN construction,
and adds GPU-aware smoke tests (33 pass, 874 total, 0 failures).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 10:47:46 +01:00
jgrusewski
0f45537e6a perf(dqn): GPU searchsorted kernel for PER sampling via CustomOp2
Eliminates the CPU roundtrip in PER binary search: previously the
cumsum tensor (~400KB for 100K buffer) was downloaded to CPU, searched
in a loop, then indices uploaded back. Now a CUDA kernel runs one
thread per target with O(log n) binary search — zero DMA.

- Add searchsorted_kernel.cu (40-line CUDA binary search kernel)
- Implement SearchSorted as Candle CustomOp2 with CPU fallback
- Wire into sample_proportional() and sample_rank_based()
- Fix all clippy warnings in gpu_replay_buffer.rs (const fn, shadow,
  doc backticks, to_owned, div_ceil, module_name_repetitions)
- Fix wildcard_enum_match_arm in mixed_precision.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 08:33:28 +01:00
jgrusewski
5191fa022a perf(ml): replace remaining CPU tensor allocations with GPU-native ops
Eliminate all remaining from_vec(vec![const; N]) patterns and CPU
tensor roundtrips in the training hot path:

- gamma discount: from_vec → Tensor::full (DQN + distributional C51)
- C51 atoms: from_vec(computed Vec) → arange+affine, 4 instances
  across forward pass and C51 loss paths (online/standard/double-DQN)
- IQN cosine embedding indices: from_vec → arange+reshape
- IQN uniform quantiles: from_vec → arange+affine (τ_i = (i+0.5)/N)
- PPO clip epsilon: from_vec+ones+sub/add+tensor_clamp → scalar clamp
  (eliminates 6 intermediate tensor allocations, 2 call sites)
- Dead neuron detection: flatten+to_vec1+CPU loop → abs().le().sum_all()
  (single scalar per parameter tensor instead of full weight transfer)

548 tests pass (350 ml-dqn + 198 ml-ppo), ml crate compiles clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:22:39 +01:00
jgrusewski
801781a472 perf(ml): eliminate CPU tensor ops from GPU training hot path
Replace expensive CPU↔GPU data transfers with GPU-native operations:

- check_gradients_finite: sum_all() scalar readback (4B per tensor)
  replaces flatten_all()+to_vec1() that copied entire gradients to CPU
  (up to 200MB per step across 9 call sites in DQN+PPO)
- Huber loss: affine() replaces 3× Tensor::from_vec(vec![const; N])
  CPU Vec allocations in the inner loss computation loop
- update_priorities_gpu: per-index slice_scatter (~1KB DMA) replaces
  full-buffer to_vec1()+from_vec() roundtrip (1.2MB DMA per step)
- PER sampling: single from_vec + GPU to_dtype replaces duplicate
  from_vec calls and CPU type conversion roundtrips
- RegimeConditional: log warning on silent GPU batch drops

822 tests pass (350 ml-dqn, 274 ml-core, 198 ml-ppo), 0 clippy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:12:54 +01:00
jgrusewski
b616d024ad fix(dqn): IQN GPU PER weights, staged GPU buffer, CUDA default in all ML crates
Three fixes for GPU PER hot path:

1. IQN quantile loss used empty CPU weights Vec instead of GPU-resident
   weights_tensor_cached — caused CUDA_ERROR_ILLEGAL_ADDRESS from
   uninitialized GPU memory. Now uses cached GPU tensor matching C51
   and standard DQN paths.

2. GpuPrioritized add()/add_batch() replaced with StagedGpuBuffer:
   add() stages on CPU (Vec::push, zero GPU ops), sample() batch-flushes
   staging→GPU in one DMA before sampling. Production path (insert_batch_tensors)
   bypasses staging entirely — GPU→GPU with zero CPU.

3. All 9 ML sub-crates default to cuda feature so `cargo test -p ml-dqn`
   exercises GPU code paths on CUDA workstations. CI service crates use
   default-features=false, unaffected.

Test results: 350 passed (was 343+7 failed), 0 failed, 1 ignored.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 22:31:49 +01:00
jgrusewski
e35ead5f3e fix(dqn): wire GPU PER into hot path, fix IS-weight ordering and IQN CVaR sort
- Wire GpuReplayBuffer into DQN constructor with OOM fallback to CPU PER
- Fix P0: GPU PER priorities never updated in single-batch train_step()
  (result.indices was empty CPU Vec; GPU tensors td_errors_gpu/indices_gpu
  were silently dropped)
- Fix IS-weight ordering: apply weights AFTER Huber loss, not before
  (weighting before nonlinear Huber shifts quadratic/linear regime boundary)
- Fix IQN CVaR: add .contiguous() before sort_last_dim (341/341 tests pass)
- Defer loss scalar readback to after backward pass (piggyback on grad flush)
- Add next_states to ExperienceBatch with episode-aware shift computation
- Add insert_batch_tensors() for direct GPU tensor insertion into replay buffer
- Make Q-value estimation periodic (every 50 steps) to reduce forward passes
- Fix CPU fallback path types (u8 action, i32 fixed-point reward, timestamp)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 21:50:26 +01:00
jgrusewski
7e5af20373 fix(ci): make CUDA non-default in 5 remaining ml sub-crates
ml-supervised, ml-ensemble, ml-labeling, ml-explainability, ml-hyperopt
all had default = ["cuda"] which pulled cudarc into the CPU services
build, causing compile-services to fail with "nvcc not found".

Changed all to default = [] and wired ml/Cargo.toml cuda feature to
propagate to all 8 sub-crates (was only 3: ml-core, ml-dqn, ml-ppo).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:35:55 +01:00
jgrusewski
eec5494344 fix(dqn): tighten v_range, replace val_loss proxy, add per-action Q diagnostics
- v_range search bounds [5,30] → [2,10] to match EMA-normalized reward
  range [-0.45,+0.32] (max discounted return ≈1.2)
- Replace val_loss proxy (max_Q - reward)^2 with Sharpe-based validation
  metric: returns -Sharpe so lower = better, economically meaningful
- Add compute_per_action_q_values(): samples 200 states from replay
  buffer, logs per-action Q averages (S100/S50/Flat/L50/L100) at each
  epoch — exposes whether agent differentiates exposure actions
- Remove dead max_q_values computation (leftover from old proxy)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:29:17 +01:00
jgrusewski
3d68e9feaf fix(ci): make CUDA non-default to unblock CPU service builds
- Remove cuda from default features in ml-core, ml-dqn, ml-ppo, ml
- Propagate cuda feature from ml → ml-core/ml-dqn/ml-ppo
- CI compile-training already uses --features ml/cuda explicitly
- Fix MaxDD log format: {:.1}% → {:.3}% (was rounding 0.033% to 0.0%)
- Suppress unused_labels/unused_variables warnings for cfg(cuda) code
- Add CALLBACK_ENDPOINT env to ml-training-service deployment
- Fix Grafana active_workers query to use sum() with fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:12:03 +01:00
jgrusewski
9c249e2a27 fix(dqn): symmetric distributional support + near-zero output init to unfreeze Q-values
Root cause: C51 distributional DQN Q-values frozen at ~7.0 for all 30 epochs.
Asymmetric hyperopt support (v_min=-6.7, v_max=17.6) caused init Q ≈ midpoint
= 5.48, creating a self-reinforcing equilibrium that gradient signal couldn't
escape. Additionally, log_q_values() queried the wrong (untrained) network.

Three fixes:
1. Enforce symmetric support in hyperopt: v_min=-v_range, v_max=+v_range
   (midpoint always 0, Q starts unbiased)
2. Near-zero init for distributional output layers (value_out, advantage_out):
   0.01× Xavier scale → softmax ≈ uniform → Q ≈ midpoint regardless of support
3. Fix log_q_values() to use self.forward() (unified dispatch through
   dist_dueling → dueling → standard) instead of self.q_network.forward()

Tests: 350 ml-dqn + 841 ml + 3 ml-core = 1194 passed, 0 failed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 18:11:22 +01:00
jgrusewski
e89fce5eb2 fix(cuda): per-thread EMA reward normalization in GPU experience kernel
GPU kernel produced raw percentage PnL rewards (~3.5e-4) which were
~10,000x smaller than Q-values (~7.0), making rewards invisible in the
Bellman backup. Q-values froze at initialization.

Port the CPU RewardNormalizer algorithm (EMA mean/variance, z-score
normalization, [-3,3] clamp) directly into the CUDA kernel hot path
as per-thread register state. Each thread maintains its own running
mean/variance with configurable decay rate (reward_norm_alpha, default
0.01 = ~100-step window). EMA resets on episode boundaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 16:49:11 +01:00
jgrusewski
1f1ba40eaa fix: resolve rebase artifacts — import paths and MLError variant
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:21:46 +01:00
jgrusewski
b95ee9d336 refactor(ml): remove dead code across 8 sub-crates — delete 742 lines
Remove unused struct fields, dead methods, unreachable code across
ml-dqn, ml-supervised, ml-features, ml-ppo, ml-checkpoint, ml-labeling,
ml-observability, and ml-universe. Gate test-only infra behind #[cfg(test)].

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:17:22 +01:00
jgrusewski
58f4f26113 refactor(ml): extract regime-detection, explainability, paper-trading
- ml-regime-detection (1.2K lines): feature_classifier, hmm modules.
  Depends on ml-core + ml-dqn (RegimeType). 23 tests passing.

- ml-explainability (329 lines): integrated_gradients module.
  Depends on ml-core + candle-core. 4 tests passing.

- ml-paper-trading (389 lines): broker, pnl_tracker modules.
  Depends on ml-ensemble (TradeAction, TradeSignal). 8 tests passing.

Total: 21 sub-crates extracted from ml monolith.
ml reduced from ~260K to ~90K lines (65% extracted).
All tests: 841 ml + 35 in new sub-crates = 876 passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:17:22 +01:00
jgrusewski
4676fe79e2 refactor(ml): extract observability, stress-testing, security into sub-crates
- ml-observability (1.2K lines): alerts, dashboards, metrics modules.
  Depends on ml-core + common (ModelType). 4 tests passing.

- ml-stress-testing (1.3K lines): load_generator, market_simulator,
  performance_analyzer modules. Depends on ml-core + common + config.
  5 tests passing.

- ml-security (1.4K lines): anomaly_detector, prediction_validator
  modules. Depends on ml-core + ml-ensemble (EnsembleDecision,
  ModelVote, TradingAction). 17 tests passing.

Total: 18 sub-crates extracted from ml monolith.
Workspace: 0 errors, ml tests 876 + 26 in new sub-crates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:17:22 +01:00
jgrusewski
b7597a7543 refactor(ml): extract universe, backtesting, asset-selection into sub-crates
- ml-universe (1.7K lines): correlation, liquidity, momentum, volatility
  modules. Only depends on ml-core (MLError, PRECISION_FACTOR).
  6 tests passing.

- ml-backtesting (1.1K lines): action_loader, barrier_backtest, report
  modules. Only depends on ml-core (MLError). Discovered and included
  previously undeclared report.rs module. 10 tests passing.

- ml-asset-selection (1.2K lines): scorer, selector modules with
  AssetClass, AssetUniverse, PredictabilityScorer, ActiveSetSelector.
  Only depends on ml-core (MLError). 33 tests passing.

All three replaced with thin facade re-exports in ml — existing
`use ml::universe::*` / `use ml::backtesting::*` /
`use ml::asset_selection::*` paths continue to work.

Total: 15 sub-crates extracted from ml monolith.
Workspace: 0 errors, ml tests 902 passed + 49 in sub-crates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:17:22 +01:00
jgrusewski
aa19b42255 refactor(ml): move UnifiedTrainable to ml-core + delete 6K dead deployment code
- Move UnifiedTrainable trait, TrainingMetrics, CheckpointMetadata, and
  checkpoint helpers from ml to ml-core (zero new dependencies — ml-core
  already had candle-core + serde_json)
- Wrap Mamba2SSM in Mamba2TrainableAdapter to satisfy orphan rule (trait
  in ml-core, type in ml-supervised — all other 9 models already used
  wrapper pattern)
- Make Mamba2SSM::validate() pub for cross-crate adapter access
- Delete 5 permanently disabled deployment modules (cfg(any()) — never
  compiled): registry, hot_swap, validation, monitoring, endpoints
  (-5,842 lines)
- Delete 2 undeclared dead files in training/: dqn_trainer.rs,
  transformer_trainer.rs (-138 lines)
- Fix pre-existing compute_loss test shape mismatch in mamba adapter

UnifiedTrainable in ml-core unblocks future trainers/ extraction (17.8K
lines) since model-specific trainers can now depend on ml-core for the
trait without pulling in the full ml monolith.

14 files changed, +128 -6,497 (net -6,369 lines)
Tests: 274 ml-core + 948 ml = 1,222 passed, 0 failed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:17:22 +01:00
jgrusewski
d313486dc2 refactor(ml): split monolith into 9 sub-crates + delete dead code
Extract 9 new sub-crates from the ml monolith to enable parallel
compilation across the workspace:

New crates (this commit):
- ml-features (282 tests): feature engineering, 21 modules
- ml-labeling (45 tests): triple barrier, meta-labeling, fractional diff
- ml-ensemble (116 tests): ensemble coordination, voting, confidence
- ml-hyperopt (47 tests): core PSO/TPE optimizer, parameter space
- ml-checkpoint (41 tests): checkpoint persistence, compression, signing
- ml-regime (68 tests): CUSUM, Bayesian changepoint, regime classification
- ml-data-validation (67 tests): FDR correction, CPCV, data quality
- ml-risk (33 tests): neural VaR, Kelly criterion, circuit breakers
- ml-validation (43 tests): statistical validation, walk-forward, DSR

Extended existing crates:
- ml-dqn: added evaluation/ (backtesting engine, metrics, reports)
  and checkpoint implementation
- ml-supervised: added checkpoint implementations
- ml-core: added shared types needed by new sub-crates

Pattern: each module in ml/ becomes a thin facade (pub use subcrate::*)
with bridge modules staying in ml for cross-model adapter code.

Dead code deleted (~7K lines):
- 13 undeclared files in microstructure/ (never compiled)
- 7 undeclared files + tests/ in risk/ (never compiled)
- parquet_io, cache_service, cache_storage, minio_integration (unused)
- extraction_wave_d_impl.rs (bare fn outside impl block)

All 2,746 sub-crate tests + 951 ml tests pass.
Full workspace builds clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:17:22 +01:00
jgrusewski
3db7f4828b refactor(ml): extract 8 supervised models into ml-supervised crate (task 8)
Move TFT, Mamba-2, Liquid, TGGN, TLOB, KAN, xLSTM, and Diffusion model
implementations to ml-supervised. Bridge files (UnifiedTrainable adapters,
Checkpointable impls) stay in ml. Delete AsyncDataLoader (replaced by
StreamingDbnLoader + simple .chunks() batching). Remove empty ml-infra
scaffold — the remaining ml modules are too tightly coupled for clean
extraction, so ml stays as the orchestration facade.

- ml-supervised: 234 tests, 0 failures
- ml: 1687 tests, 0 failures
- Workspace: 0 compilation errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:16:08 +01:00
jgrusewski
58d0f535df refactor(ml): extract PPO module into ml-ppo crate (task 7)
Move 24 PPO source files + flow_policy/ from ml into standalone
ml-ppo crate. The ml crate's ppo module is now a thin re-export layer
(`pub use ml_ppo::*`) plus two bridge files (trainable_adapter.rs,
stress_testing.rs) that depend on ml-internal types.

Key changes:
- ml-ppo: 25 modules (incl flow_policy subdir), 198 tests, standalone
- ml: depends on ml-ppo, re-exports via ppo/mod.rs
- Import rewrites: crate::common::action → ml_core::action_space,
  crate::dqn::{mixed_precision,xavier_init} → ml_core::*,
  crate::gradient_accumulation → ml_core::gradient_accumulation
- ml tests: 1929 pass (down from 2127 — 198 moved to ml-ppo)
- Workspace: 0 errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:16:08 +01:00