Mirror the DSR (Differential Sharpe Ratio) and n-step return
accumulation added by Task 4 to the scalar kernel into the warp
kernel (dqn_full_experience_kernel_warp). DSR/n-step state runs
on lane 0 only, matching the existing reward computation pattern.
All accumulators are reset on episode boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Differential Sharpe Ratio reward shaping and n-step return
accumulation to dqn_full_experience_kernel (scalar path). DSR replaces
EMA normalization when use_dsr=1; n-step ring buffer accumulates
discounted returns for effective_n>1. Both are reset on episode
boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Append third CUDA kernel to epsilon_greedy_kernel.cu for Branching DQN:
3 independent epsilon-greedy heads (exposure/5, order/3, urgency/3)
compose to factored action index 0-44 (exposure*9 + order*3 + urgency).
Add Rust composition tests verifying full 45-action coverage and
round-trip decomposition correctness.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add backticks to type names in doc comments (doc_markdown)
- Mark eligible functions as const fn (missing_const_for_fn)
No behavior changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
module_name_repetitions: PpoConfig in ppo module is conventional ML naming.
Renaming breaks every import across the workspace.
integer_division: Basis point calculations, batch size math, combinatorial
formulas — truncation is intentional. Float conversion would introduce bugs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
Ubuntu 24.04 minimal doesn't include passwd (useradd/groupadd).
Debian bookworm-slim had it by default. Required for foxhunt user.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cargo defaults to /proc/cpuinfo CPU count (32 host cores) but each
compile pod is cgroup-limited to 14 CPU request. Without this, both
pods spawn 32 threads each (64 total) causing heavy throttling on
the 32-core POP2 node. Setting CARGO_BUILD_JOBS=14 per pod avoids
context-switch overhead and uses cgroup-aware parallelism.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Align all 4 Docker images with local dev environment:
- ci-builder: CUDA 12.4.1/Ubuntu 22.04 → 12.9.1/Ubuntu 24.04
- ci-builder-cpu: Debian bookworm → Ubuntu 24.04 (+ explicit rustup)
- foxhunt-runtime: Debian bookworm → Ubuntu 24.04
- foxhunt-training-runtime: CUDA 12.6.3 → 12.9.1, nvrtc 12-6 → 12-9
All images now have glibc 2.39, matching the local build machine.
Binaries compiled locally or in CI will run in any of these containers
without GLIBC_2.3x version mismatch errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Switch sccache backend from S3 (MinIO) to persistent local storage.
Two 20Gi PVCs (sccache-cpu, sccache-cuda) on scw-bssd-retain eliminate
network I/O for cache reads/writes. Both compile steps always run on
the same node so RWO is sufficient.
Removes 13 S3 env vars per compile step, replaces with SCCACHE_DIR=/sccache.
Updates ci-pipeline-template, compile-and-train-template, kustomization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
Argo Events labels workflows with events.argoproj.io/trigger, not
workflows.argoproj.io/workflow-template. Fixed find_ci_workflow() so
ci-train preset can actually find CI pipeline runs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New `ci-train` preset monitors the Argo CI workflow for a commit,
polls until completion, then auto-submits the training job (defaults
to hyperopt). Prints Prometheus/log links during monitoring.
Usage: ./infra/scripts/train.sh ci-train --model dqn [--commit SHA]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
Remove phantom foxhunt_training_total_epochs reference (never registered)
and replace with foxhunt_training_step from the recent metrics commit.
Add full coverage for all 52 Tier-1 Prometheus metrics across 5 new rows:
RL diagnostics (Q-overestimation, PPO entropy/KL/advantage), training
health (NaN/grad explosion/feature errors, checkpoint ops, data load
latency), epoch returns, supervised eval (accuracy/precision/recall/F1),
and hyperopt details (mode, trial epoch, failed trials).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The train_baseline_rl binary expects --output-dir, not --output.
Also adds prometheus.io scrape annotations to job pod templates
so training metrics appear in Grafana.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>