Commit Graph

231 Commits

Author SHA1 Message Date
jgrusewski
52ea2ac36b feat(ml): ReplayBufferType::GpuPrioritized variant with full dispatch
New variant delegates to GpuReplayBuffer for all operations. Sample
returns BatchSample with gpu_batch populated (empty CPU vecs). Added
update_priorities_gpu() for tensor-based priority updates, as_gpu_buffer()
for direct access from trainer. Existing tests unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:57:14 +01:00
jgrusewski
0939552a16 feat(ml): proportional + rank-based PER sampling and GPU priority update
Proportional: cumsum + binary search over alpha-weighted priorities.
Rank-based: sort descending, rank probabilities 1/rank^alpha, cumsum sample.
Priority update: |td_error|^alpha + epsilon scatter into buffer.
All methods return GpuBatch with IS weights normalized by max.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:57:14 +01:00
jgrusewski
c92691556b feat(ml): GpuReplayBuffer::insert_batch with ring buffer wrapping
Slice-scatter based insertion handles wrap-around by splitting into
tail + head copies. New experiences get max_priority. Tests verify
correctness at capacity boundary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:57:14 +01:00
jgrusewski
59eddb99f6 feat(ml): GpuReplayBuffer core struct with ring buffer storage
Pre-allocates all experience tensors on device at init (~47 MB for 100K
buffer). Ring buffer cursor and beta annealing state tracked CPU-side.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:57:14 +01:00
jgrusewski
0fc65717f2 feat(ml): add GpuBatch struct and gpu_batch field to BatchSample
GPU-resident batch tensors allow compute_gradients() to skip CPU→GPU
transfer when sampling from the GPU replay buffer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:57:14 +01:00
jgrusewski
c64022f819 fix(hyperopt): remove double win_rate multiplication in DQN trial summary
PerformanceMetrics::from_trades() already returns win_rate as percentage
(e.g. 57.78), but TRIAL_SUMMARY and backtest details log lines multiplied
by 100 again, producing values like 5778%. Remove the extra * 100.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:47:35 +01:00
jgrusewski
ca8b77f63a feat(dqn): wire gradient explosion and checkpoint save metrics into trainer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:34:41 +01:00
jgrusewski
2642286c86 feat(hyperopt): record elapsed_seconds metric in all hyperopt binaries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:10:02 +01:00
jgrusewski
ddacfbbff1 feat(supervised): record learning rate and epoch duration metrics
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:10:00 +01:00
jgrusewski
afa506495f feat(ppo): record entropy, KL, advantage, explained variance, epoch duration metrics
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:09:59 +01:00
jgrusewski
47f8c6109d feat(dqn): record Q-value, gradient norm, epoch duration, buffer size metrics
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:09:58 +01:00
jgrusewski
b08b67a5cd feat(metrics): register 17 new Prometheus metrics with verbose gating
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:56:51 +01:00
jgrusewski
85725abd8f feat(monitoring): per-epoch Prometheus metrics + CI scrape plumbing
- DQN/PPO trainers now record epoch, loss, val_loss, batch/s every epoch
- CI training jobs get Prometheus scrape annotations via runner overrides
- Allow Prometheus (foxhunt namespace) to scrape foxhunt-ci pods
- Skip no-model metrics in monitoring service session grouping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:10:52 +01:00
jgrusewski
2f193dca6d fix(ml): GPU experience collector gate — support hybrid distributional+dueling
The GPU experience collector gate checked only `dqn.dueling_q_network`
(plain dueling), but with both `use_dueling: true` AND `use_distributional: true`
(the defaults), DQN creates hybrid `dist_dueling_q_network` instead, leaving
the plain dueling fields as None. This meant the GPU collector never initialized
despite curiosity being enabled.

Fix: Add else-if fallback to check `dist_dueling_q_network`/`dist_dueling_target_network`
when plain dueling fields are None. Same fix applied to the weight sync site.

Also fix test_train_with_empty_data_completes_gracefully: reduce to 5 epochs with
early stopping disabled. The debug-mode async state machine is large enough that
empty-data epochs run ~180ms each (vs ~3ms in release), triggering both plateau
and patience-based early stopping. The test purpose is crash-freedom, not timing.

2497 tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:32:41 +01:00
jgrusewski
bb133d6619 feat(monitoring): add monitoring service for live training metrics over gRPC
New monitoring_service bridges Prometheus training/GPU metrics to gRPC,
enabling `fxt train monitor` TUI and web-dashboard live updates without
direct Prometheus dependency on clients.

Components:
- 6 new hyperopt Prometheus metrics (common::metrics::training_metrics)
- monitoring_service: Prometheus-to-gRPC bridge (port 50057)
- fxt train monitor: live TUI + --once snapshot mode
- web-gateway: REST endpoint + WS training_progress poller
- K8s deployment manifest + CI integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:07:42 +01:00
jgrusewski
1a62060dab fix(clippy): resolve all workspace clippy warnings
Fix pre-existing and new clippy issues across 5 files:
- service_auth.rs: .to_string() → .to_owned() on &str, backtick doc items
- streams.rs: backtick doc items, allow cognitive_complexity on reconnect_loop
- main.rs: .to_owned(), allow infinite_loop, drop must_use, rename shadow
- start.rs: eliminate shadow_reuse on endpoint binding
- enhanced_ml.rs: remove unnecessary f64 cast

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:06:25 +01:00
jgrusewski
3491749927 feat(web-gateway): integrate monitoring service for live training metrics
Wire monitoring_service gRPC into web-gateway: proto compilation,
config/state/client plumbing, REST endpoint at /api/monitoring/live-metrics,
and a 3s polling bridge that broadcasts training_progress over WebSocket.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:50:19 +01:00
jgrusewski
c4693403d5 feat(ml): wire hyperopt Prometheus metrics into training binaries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:25:56 +01:00
jgrusewski
354cd5c116 feat(metrics): add 6 hyperopt Prometheus metrics to training_metrics
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:18:50 +01:00
jgrusewski
148d4cbb43 feat(ml): ungate GPU experience collector + async PER pre-sampling
Enable curiosity module by default (weight 0.0→0.1) to satisfy the
three-way gate (dueling + target + curiosity) that was blocking the
GPU experience collector CUDA kernel. This eliminates the 30-40% CPU
experience collection phase that was the main GPU idle bottleneck.

Additional changes:
- BatchSample API: train_step/compute_gradients now accept
  Option<BatchSample> instead of Option<Vec<Experience>>, preserving
  PER importance-sampling weights and indices through pre-sampling.
- Async PER pre-sampling: train_step_single_batch and
  train_step_with_accumulation now pre-sample from the replay buffer
  using a READ lock before acquiring the WRITE lock for GPU training.
  This separates CPU sampling (~250μs) from GPU forward/backward (~3ms).
- Delete dead EpochPrefetcher: the binary (train_baseline_rl.rs) already
  implements fold prefetching with background thread + mpsc channel +
  GPU double-buffering, making the trainer's EpochPrefetcher redundant.
- Hyperopt DQN bounds: curiosity_weight min 0.0→0.01 so PSO can never
  fully disable curiosity (which would re-gate the GPU collector).

10 files changed, -195 net lines. 2497 tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:14:14 +01:00
jgrusewski
4b2f9e8133 fix(ml): GPU-aware PSO thread cap — 3→7 parallel trials on L40S
The auto-detect heuristic used cpus/2 ("smart cap"), designed for
CPU-bound workloads.  DQN/PPO trials are GPU-bound — each rayon
thread submits CUDA kernels and waits on cudaDeviceSynchronize(),
using minimal CPU.  cpus-1 is the correct cap.

On L40S-1-48G (8 vCPU): 3 threads → 7 threads (2.3× more trials).
Also bumps CI CPU limit 7500m→8000m to expose all 8 cores.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 19:55:54 +01:00
jgrusewski
64ca8f97ce fix(observability): dedicated OTLP runtime for sync training binaries
The batch span processor needs a tokio runtime for gRPC transport and
periodic flush. Async services already have one via #[tokio::main], but
sync training binaries (hyperopt, train, evaluate) don't.

Previous approach (making binaries async with #[tokio::main]) caused
"Cannot start a runtime from within a runtime" panics because the ML
crate's internal code creates its own tokio runtimes for block_on().

New approach: build_otel_tracer() detects runtime context via
Handle::try_current(). If absent, it creates a dedicated 1-worker
multi-thread runtime stored in a process-lifetime OnceLock. The worker
thread actively polls the OTLP batch export task.

Reverts training binaries to sync fn main() so internal runtime creation
(hyperopt adapters, DQN/PPO trainers) continues working as before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 18:54:10 +01:00
jgrusewski
77f8ce0b3a feat(ml): GPU Phase 3 — production hotpath elimination
14 commits eliminating GPU→CPU synchronization violations across
all 10 ML model trainers and ensemble inference pipeline:

P0 Training: GPU-accumulated loss in Liquid/TLOB/TFT/Diffusion,
batched gradient norms in TFT/TLOB/Diffusion (N syncs → 1)

P1 Training+Inference: KAN spline GPU-native forward, PPO metrics
batching (4→1), ensemble predict_raw() trait + GPU aggregation,
CudaStreamPool + StreamAwareEnsemble for CUDA multi-stream

P2 Minor: Mamba2 hyperopt GPU tensor creation, DQN GPU argmax

29 files changed, +1895/-425 lines. 2502 tests pass, 0 clippy warnings.
2026-03-02 18:48:04 +01:00
jgrusewski
fcf87a5f72 fix(ml): add tokio runtime to training binaries for OTLP export
All 6 training binaries (hyperopt_baseline_rl, hyperopt_baseline_supervised,
train_baseline_rl, train_baseline_supervised, evaluate_baseline,
evaluate_supervised) used sync fn main() but the OTLP batch exporter
requires a tokio runtime (tonic/hyper-util gRPC transport). This caused
an immediate panic on CI when OTEL_EXPORTER_OTLP_ENDPOINT was set.

Fix: #[tokio::main(flavor = "current_thread")] on all 6 binaries.
Also fix pre-existing clippy warnings (shadow, let_underscore_must_use,
doc_markdown, cognitive_complexity, integer_division, unsafe_code).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 18:16:37 +01:00
jgrusewski
4c57f3d704 fix(ml): DQN single-step action selection uses GPU argmax
Replace CPU-side argmax (to_vec1 + iter enumerate max_by) with
GPU-native argmax(D). Single u32 scalar extraction instead of
full Q-value vector transfer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 18:03:20 +01:00
jgrusewski
129c2f19b8 fix(ml): mamba2 hyperopt tensors created on GPU instead of CPU
Replace &Device::Cpu with &self.device for input and target tensor
creation in Mamba2 hyperopt adapter. Avoids unnecessary CPU→GPU
transfer during hyperparameter optimization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 18:03:10 +01:00
jgrusewski
85138e2fbd feat(ml): add StreamAwareEnsemble with per-model CUDA streams
Stream-aware ensemble that runs models on separate CUDA streams
for true GPU-level parallelism. Falls back to rayon on CPU.
Uses CudaStreamPool for synchronization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:59:12 +01:00
jgrusewski
550944ebf2 feat(ml): add CudaStreamPool for multi-stream ensemble inference
CUDA stream pool with CPU no-op fallback. Foundation for
StreamAwareEnsemble that runs models on separate CUDA streams.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:52:45 +01:00
jgrusewski
58dc95cf54 feat(ml): InferenceEnsemble GPU-aggregated prediction — N syncs → 1
Use predict_raw() to collect raw GPU tensors from adapters. Stack,
sigmoid, weighted-sum on GPU before single extraction. Falls back
to CPU path for adapters without tensor output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:46:21 +01:00
jgrusewski
78f5ead601 feat(ml): implement predict_raw() for 5 scalar-output ensemble adapters
Override predict_raw() in TGGN, TLOB, KAN, xLSTM, Diffusion adapters
to return raw GPU tensors. Enables GPU-side ensemble aggregation
instead of per-model CPU extraction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:46:09 +01:00
jgrusewski
f8cec1d6f9 feat(ml): KAN spline GPU-native forward — eliminate GPU→CPU→GPU roundtrip
Replace clamped.to_vec1() CPU loop with GPU-native floor/ceil/frac
operations. Removes 3 tensor transfers per forward pass (N floats
down + 3*N up). All index computation now stays on GPU.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:41:14 +01:00
jgrusewski
3aab43614a feat(ml): PPO metrics batch extraction — 4 GPU syncs → 1
Stack var_returns, var_residuals, mean_reward, var_reward into single
tensor before extraction. Uses broadcast_sub for GPU-native mean
centering instead of scalar round-trip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:41:11 +01:00
jgrusewski
c91995e41d feat(ml): add RawPrediction + predict_raw() default to ModelInferenceAdapter
Backward-compatible trait extension. Default predict_raw() wraps
predict() result with tensor: None. Adapters can override to return
raw GPU tensors for GPU-side ensemble aggregation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:41:08 +01:00
jgrusewski
e12cdfa005 feat(ml): diffusion trainer GPU-accumulated loss + batched grad norm
Replace per-param gradient norm extraction with batched Tensor::stack
pattern (N GPU syncs → 1). Remove per-batch loss scalar extraction
from backward(), defer to epoch-level accumulation. Apply GPU-accumulated
validation loss (stack + mean_all instead of per-batch to_scalar).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:30:19 +01:00
jgrusewski
4cf181c8fb feat(ml): TFT batched gradient norm — N GPU syncs → 1
Replace per-param gradient norm extraction loop with batched
Tensor::stack pattern. Single GPU→CPU sync instead of one per
parameter tensor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:30:10 +01:00
jgrusewski
7d2b56a3ad feat(ml): TLOB batched parameter norm — N GPU syncs → 1
Replace per-param calculate_gradient_norm() loop with batched
Tensor::stack pattern. Single GPU→CPU sync instead of one per
parameter tensor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:29:33 +01:00
jgrusewski
fd80dbc07e feat(ml): TFT trainer GPU-accumulated loss — eliminate per-batch to_vec0
Replace per-batch loss.to_vec0() in TFT training/validation loops
with GPU tensor accumulation. Single extraction per epoch + NaN guard
every 100 batches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:25:57 +01:00
jgrusewski
2c493f4305 feat(ml): liquid trainer GPU-accumulated loss — eliminate per-batch to_scalar
Replace per-batch .to_dtype(F64).to_scalar() in Liquid train/validate
with GPU tensor accumulation. Single extraction per epoch + NaN guard
every 100 batches. Removes ~100-500 GPU→CPU syncs per epoch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:25:56 +01:00
jgrusewski
851546b322 feat(ml): TLOB trainer GPU-accumulated loss — eliminate per-batch to_scalar
Replace per-batch loss.to_scalar() in TLOB train_epoch/validate_epoch
with GPU tensor accumulation. Single extraction per epoch + NaN guard
every 100 batches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:25:54 +01:00
jgrusewski
118ceca4d5 fix(ml): update evaluate_baseline for 45 factored actions
Both DQN and PPO eval paths used old 3-action index matching (0=Buy,
1=Sell, 2=Hold). Now uses FactoredAction.target_exposure() for
exposure-weighted returns and order-type-specific transaction costs.
PPO path had .to_int() which doesn't exist on FactoredAction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:24 +01:00
jgrusewski
18f98e17bb fix(ml): update PPO trainer reward and position tracking for 45 factored actions
compute_reward_pnl() took raw action_idx (0=Buy,1=Sell,2=Hold) — wrong
with 45-action FactoredAction encoding. Now takes &FactoredAction and
uses order-type-specific transaction costs. Position tracking uses
action.exposure instead of old 3-way index match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:24 +01:00
jgrusewski
9c3383420b fix(ml): update remaining PPO num_actions: 3 → 45 in hyperopt and benchmark
Hyperopt adapter and PPO benchmark still had num_actions: 3, which would
produce misconfigured PPO models when used with the 45-action FactoredAction
sampling path. Found by spec compliance review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00
jgrusewski
a41387c3f9 fix(ml): make IG completeness axiom test deterministic
Use a purely linear network (no ReLU) for the completeness axiom test.
IG on linear functions is mathematically exact, so the test is
deterministic regardless of random weight initialization. Tighten
tolerance from 20% to 1% (f32 rounding only). Keep ReLU network in
the basic test for non-linear verification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00
jgrusewski
2e15d1d834 fix: trading_service PPO type mismatch and IG test tolerance
- trading_service: PPO predict() used TradingAction match but act() now
  returns FactoredAction. Use target_exposure() mapped to 0-1 range.
- IG completeness axiom test: relax tolerance from 5% to 20% (random
  weights with ReLU non-linearity and trapezoidal rule can exceed 5%).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00
jgrusewski
63b2c6e72d fix(ml): align PPO action masking to canonical exposure*9+order*3+urgency layout
Was using direction=idx/15 (3 groups of 15: Buy/Sell/Hold) — incompatible
with DQN's FactoredAction encoding. Now uses exposure_idx=idx/9 (5 groups
of 9: Short100/Short50/Flat/Long50/Long100) matching FactoredAction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00
jgrusewski
512535076f feat(ml): wire PPO to 45 factored actions via FactoredAction
sample_action(), act(), act_with_log_prob(), greedy_action() now return
FactoredAction instead of TradingAction. Fixes the architectural disconnect
where num_actions=45 output neurons were sampled through a 3-action bottleneck.

TrajectoryStep.action and TrajectoryBatch.actions now use FactoredAction.
Added FactoredAction::from_legacy() for backward compatibility in tests.

Updated all PPO consumers: trainers/ppo.rs, hyperopt/adapters/ppo.rs,
validation/ppo_adapter.rs, benchmark/ppo_benchmark.rs.

2487 tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00
jgrusewski
f12336f16f refactor(ml): move FactoredAction to common/action.rs (canonical location)
Consolidates ExposureLevel, Urgency, FactoredAction from dqn/action_space.rs
and ppo/factored_action.rs into common/action.rs. Both dqn:: and ppo::
re-export for backward compatibility. Deletes ppo/factored_action.rs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00
jgrusewski
ca2468f5ae fix(ml): remove hidden fake feature importance from inference path
calculate_feature_importance() returned hardcoded fabricated scores with
wrong feature names on every inference. Replaced with honest empty map.
Real importance is computed on-demand via integrated gradients.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00
jgrusewski
764deb7ddb feat(ml): add integrated gradients for feature importance
Implements IntegratedGradients using Candle autograd (Var::from_tensor +
backward). Computes attributions by integrating input gradients along
interpolation path from baseline to input.

Verified via completeness axiom test: sum(attributions) ≈ F(x) - F(baseline).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:41:23 +01:00
jgrusewski
2fbb19d9df refactor(ml): rename real_data_loader to data_loader
The real_ prefix was misleading — there is no fake data loader.
Mechanical rename across 18 source files, no logic changes.

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