Commit Graph

3450 Commits

Author SHA1 Message Date
jgrusewski
514b013862 chore: remove per-step determinism diagnostic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:06:27 +02:00
jgrusewski
5e8f8b4f5b fix: replace stateful LCG with stateless Philox in experience kernels
- Feature noise injection: philox_uniform(episode, timestep, feature)
  instead of advancing rng_states by 2*market_dim per timestep
- Episode start jitter: philox_uniform(episode, 0, 9999)
- Domain randomization params: philox_uniform(episode, 0, 2000+k)
- Saboteur params: philox_uniform(episode, 0, 3000+k)

All experience collection randomness is now stateless — same inputs
always produce same outputs regardless of execution history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:05:36 +02:00
jgrusewski
78970d37fe fix: remove atomicAdd from attention backward — deterministic dx accumulation
Replaced global memory atomicAdd(&dx[j], grad_j) with:
1. Save d_q/d_k/d_v per-feature into shared d_qkv_save[3*Dh]
2. Each thread accumulates dx[j] for its assigned j values
   by looping over all features — zero write contention
3. Flush dx_scratch to global dx with single write per j

Shared memory: 2*D+8 → 6*D+8 (dx_scratch[D] + d_qkv_save[3*D])

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:51:11 +02:00
jgrusewski
e772c01fa2 fix: remove atomicAdd from backward bias grad + c51 q_divergence
- backward_kernels.cu: atomicAdd(&db[j], sum) → db[j] += sum
  (single writer per j since out_dim < 256 = one block)
- c51_loss_kernel.cu: removed q_divergence atomicAdd from hot path
  (was monitoring-only but ran inside CUDA Graph)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:40:38 +02:00
jgrusewski
6c06168230 fix: stateless Philox RNG for experience action selection (replace LCG)
experience_action_select kernel now uses philox_uniform(episode_id,
timestep, call_counter) instead of stateful lcg_random(&rng).
Eliminates branching-dependent RNG state divergence across processes.

Also passed timestep to backtest evaluator's action_select launch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:25:10 +02:00
jgrusewski
f16ec55427 Revert "refactor: remove all bf16 legacy wrappers from CUDA kernels — pure native f32"
This reverts commit 3649ff9fb5.
2026-04-13 17:09:34 +02:00
jgrusewski
5c74a2a0d0 Revert "refactor: remove ALL bf16 legacy wrappers — pure native f32 across entire codebase"
This reverts commit a062c638c5.
2026-04-13 17:09:34 +02:00
jgrusewski
a062c638c5 refactor: remove ALL bf16 legacy wrappers — pure native f32 across entire codebase
- Deleted 45 wrapper definitions from common_device_functions.cuh
  (bf16(), bf16_zero/one/exp/sqrt/log/fmax/fmin/cos/tanh/pow/fabs,
  bf16_shfl_xor/down, bf16_warp_sum/max, atomicAddBF16, f32_to_bf16,
  leaky_relu_bf16)
- Cleaned 20 .cu files in ml crate (via subagent)
- Cleaned 4 .cu files in ml-ppo crate
- Cleaned 12 .cu files in ml-core + ml-dqn crates
- Fixed monitoring_kernel.cu atomicMin/Max (was broken 16-bit CAS)
- Resolved leaky_relu name conflicts (PPO kernels use unique names)
- Zero bf16 wrapper calls remain in any .cu file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:07:32 +02:00
jgrusewski
3649ff9fb5 refactor: remove all bf16 legacy wrappers from CUDA kernels — pure native f32
Replaced all bf16()/bf16_zero()/bf16_one()/bf16_exp/bf16_sqrt/bf16_fmax/
bf16_fmin/bf16_fabs/bf16_log/bf16_shfl_xor/bf16_shfl_down/atomicAddBF16/
f32_to_bf16 wrapper calls with their native f32 equivalents across all 25
.cu kernel files. Updated stale comments. Fixed monitoring_kernel.cu atomic
CAS helpers to use 32-bit int CAS instead of broken 16-bit. Build verified.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:44:20 +02:00
jgrusewski
25eb2f5e20 fix: deterministic PER sampling + GPU-side adv sigma EMA (zero DtoH)
Two root causes of cross-process non-determinism eliminated:

1. PER sampling used Philox RNG for within-segment jitter.
   Replaced with deterministic stratified midpoint sampling:
   threshold = (i + 0.5) * total_sum / B. Zero randomness.

2. update_adv_sigma did synchronous memcpy_dtoh (GPU sync point).
   Replaced with iql_adv_sigma_ema_update kernel — EMA computed
   entirely on GPU, modulate_td_errors reads sigma from device buffer.

Result: 28/30 epochs bit-identical across processes (was 1/30).
Remaining ~1e-6 difference is f32 accumulator noise.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:14:05 +02:00
jgrusewski
e09e52c3d6 fix: wire IQL per_sample_support_ptr + branch_scales_ptr to trainer at init
Missing set_per_sample_support_ptr/set_branch_scales_ptr calls caused
null pointer dereference in C51 loss kernel at step 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:35:36 +02:00
jgrusewski
55193bdd13 fix: separate eval_v_range_ptr for compute_expected_q from per_sample_support
compute_expected_q reads v_range_buf[0..2] for argmax — cannot use
per_sample_support [B,3]. Added fixed [-100, 100] eval_v_range_buf
for all non-C51-loss kernel launches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:33:24 +02:00
jgrusewski
36d076b344 feat(iql): full pipeline wired — dual IQL, per-sample support, branch scales, gap exploration
- IQL mandatory: Option<GpuIqlTrainer> → GpuIqlTrainer (high + low tau)
- Full pipeline in submit_aux_ops: populate_q_out → gather → train both
  → advantages → adv_sigma → per_sample_support → branch_scales → gap → epsilon
- Delete v_range wrappers from FusedTrainingCtx
- Delete adapt_v_range calls from training_loop.rs
- Rename v_range_ptr → per_sample_support_ptr in experience collector
- Fixed eval v_range in metrics.rs (wide fixed range for argmax)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:26:39 +02:00
jgrusewski
b2e695fe08 feat(c51): per-sample support + branch scales in kernel launches, delete v_range
Replace global v_range infrastructure with per-sample C51 support and
per-branch gradient scales. The IQL trainer sets these pointers after
construction, enabling sample-level distributional RL customization.

- Add per_sample_support_ptr and branch_scales_ptr fields + setters
- Update all kernel launches (C51 loss, C51 grad, CQL, MSE, expected_q)
  to pass per_sample_support_ptr instead of v_range_dev_ptr
- Add branch_scales_ptr arg to c51_grad_kernel launch
- Delete adapt_v_range_full, adapt_v_range, adapt_v_range_with_gap,
  v_range getter, v_range_buf_ptr getter
- Delete v_range_pinned/v_range_dev_ptr/reward_std_ema fields + alloc/free

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:18:12 +02:00
jgrusewski
5fbb39b0b5 feat(iql): new buffers and launcher methods for full integration
17 kernels, per-sample support seeding, adv sigma EMA,
branch scales, expectile gap, per-sample epsilon launchers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:12:25 +02:00
jgrusewski
b1cf06d48e feat(c51): per-sample atom support — V(s)-centered distributional RL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 15:01:22 +02:00
jgrusewski
9f90cdf39a feat(explore): per-sample epsilon from IQL expectile gap
Replace 3 scalar per-branch epsilons (epsilon_exp/ord/urg) in
branching_action_select with a single per-sample buffer [B] so that
V_high(s)-V_low(s) from IQL expectile regression drives exploration
uniformly across all 3 heads, superseding manual per-branch heuristics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 15:01:20 +02:00
jgrusewski
cae6d8e5a9 feat(c51): dynamic per-branch gradient scaling from IQL advantage decomposition
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 15:00:57 +02:00
jgrusewski
a8cb75894c feat: add 8 IQL CUDA kernels — TD modulation, adv variance, per-sample support/epsilon, branch advantage, expectile gap
Appends to iql_value_kernel.cu: iql_modulate_td_errors, iql_adv_variance_reduce,
iql_compute_per_sample_support, iql_per_branch_advantage, iql_expectile_gap,
iql_gap_mean_reduce, iql_compute_per_sample_epsilon. All f32, zero atomics, fully
deterministic. Builds clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:59:37 +02:00
jgrusewski
f0cda76229 docs: IQL full integration implementation plan — 9 tasks
6 new CUDA kernels, C51/grad/epsilon kernel mods, dual-tau IQL,
v_range dead code deletion, per-sample everything.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:56:27 +02:00
jgrusewski
d0227e78bb docs: fix 6 issues in IQL integration spec from code review
- Fix staleness: buffer_write_pos is host-side, pass as scalar (not in GpuBatch)
- Fix epsilon: branching_action_select takes 3 scalar epsilons, not 1
- Fix dead code list: add GpuExperienceCollector::v_range_ptr, FusedTrainingCtx wrappers, training_loop callers
- Fix step 0: seed per_sample_support with [-1, 1] default at construction
- Fix entropy: delete ent_scale conditionals, branch_scales subsumes them
- Fix non-graph c51 launches: 4 sites in gpu_dqn_trainer.rs need per_sample_support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:51:30 +02:00
jgrusewski
b593f038f7 docs: IQL full integration design spec — 5 components
Covers: PER advantage-weighted staleness, per-sample C51 atom
support centered on V(s), per-branch advantage decomposition,
dual-tau expectile gap exploration, and v_range dead code removal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:45:26 +02:00
jgrusewski
cfc44be13c fix: capture graph_mega at step 0 + remove MSE graph switch
Two root causes of cross-process non-determinism fixed:

1. MSE→C51 graph switch: graph_forward_mse and graph_forward were
   captured in different sessions with different cublasLtMatmul configs.
   Fix: always use blended graph_forward (c51_alpha controls blend).
   Deleted LossMode enum, graph_forward_mse, and all switching code.

2. Multiple capture sessions at steps 0-2: graph_forward, graph_ddqn,
   and graph_adam were captured individually before graph_mega at step 2.
   Each capture contaminated cuBLAS internal state.
   Fix: capture graph_mega at step 0 (single capture session).

Result: runs with same initial cublasLtMatmul capture are fully
bit-identical across all 30 epochs. Remaining cross-process variation
is from the single initial cublasLtMatmul call during graph capture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:24:20 +02:00
jgrusewski
e61571d5aa cleanup: remove graph_forward_mse + LossMode dead code
Always use blended graph_forward (MSE+C51). c51_alpha controls
the blend weight. Separate MSE graph capture was the root cause
of cross-process non-determinism — different capture sessions
produce different cublasLtMatmul kernel configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:18:36 +02:00
jgrusewski
05e321af15 refactor: experience collector shares cuBLAS handle with trainer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:04:19 +02:00
jgrusewski
a1a2021004 refactor: GpuDqnTrainer uses single SharedCublasHandle
Create SharedCublasHandle once in constructor and pass Arc clones to
CublasGemmSet (forward) and CublasBackwardSet (backward). Remove the
double_dqn_stream, pass1_event, pass3_event infrastructure — DDQN
forward now runs sequentially on the main stream via the shared handle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:56:53 +02:00
jgrusewski
a927cbd6c9 refactor: CublasBackward → CublasBackwardSet with shared handle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:50:11 +02:00
jgrusewski
ac0ae5b5ae refactor: CublasForward → CublasGemmSet with shared handle
Replace per-instance cuBLAS/cublasLt handle ownership in CublasForward
with Arc<SharedCublasHandle>. The struct is renamed to CublasGemmSet
and a type alias preserves backward compatibility for external callers.

Removed from CublasGemmSet: handle, workspace, lt_handle, lt_workspace,
bias kernels (all now in SharedCublasHandle). Kept: branch streams,
branch workspaces, GEMM caches, events, network dimensions.

External callers (gpu_dqn_trainer, gpu_experience_collector) will be
updated in Tasks 4-5 to pass Arc<SharedCublasHandle> to the constructor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:45:21 +02:00
jgrusewski
334ac22d30 feat: SharedCublasHandle — single cuBLAS handle per stream
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:36:59 +02:00
jgrusewski
6621177f78 docs: implementation plan for single shared cuBLAS handle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:32:23 +02:00
jgrusewski
44b07d1022 docs: spec for single shared cuBLAS handle refactor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:29:05 +02:00
jgrusewski
c8df643e67 feat: graph experience collector forward to prevent cuBLAS state contamination
Capture the experience collector's bottleneck + Q-network forward pass
into a CUDA Graph on first timestep. Prevents ungraphed cublasLtMatmul
calls from contaminating cuBLAS internal state between epochs.

Root cause identified: multiple cuBLAS handles on the same CUDA stream.
NVIDIA docs: "maintain a separate cuBLAS handle for each stream" —
implying one handle per stream. We have 3 handles (experience collector,
training, DDQN) on one stream.

Proper fix (next session): refactor CublasForward to share a single
cuBLAS handle across all components. Separate the handle (shared)
from per-component cached descriptors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:21:14 +02:00
jgrusewski
fb7e74fe9c fix: save/restore td_errors + per_sample_loss around eval graph replay
graph_forward replay during eval writes td_errors and per_sample_loss
which PER reads to update priorities. Without save/restore, eval
corrupts PER state → different sample selection → training diverges.

DtoD save before replay, restore after. Cost: 4 DtoD copies of [B]
floats per eval call (~0.01ms).

Combined fixes in this determinism effort:
- IQN/attention backward: zero atomicAdd (restructured kernels)
- cublasLt: AlgoGetIds (verified: same algo_id=16 every run)
- Single CUDA stream (DDQN moved from double_dqn_stream)
- graph_forward persists across folds (never invalidated)
- QValueProvider routes eval through trainer's graph_forward
- PER save/restore prevents eval→training state corruption

Remaining: epochs 1-3 identical across runs, epoch 4 varies.
The cublasLtMatmul kernel execution is non-deterministic between
process invocations despite same algo + single stream. All NVIDIA
documented preconditions are met.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:40:48 +02:00
jgrusewski
e3f7ab3347 fix: single-stream DDQN for cuBLAS determinism
Move DDQN forward (Pass 3) from double_dqn_stream to main stream.
NVIDIA docs: "bit-wise reproducibility is valid only when a single
CUDA stream is active." The double_dqn_stream fork is kept but
no longer used for cuBLAS operations.

Combined with AlgoGetIds (verified: same algo_id=16 every run),
this satisfies all documented NVIDIA preconditions for bit-wise
reproducibility. Epochs 1-3 are identical across runs. Epoch 4+
divergence remains under investigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:36:18 +02:00
jgrusewski
13524dd3a5 feat: replace cublasLt heuristic with deterministic AlgoGetIds
Replace cublasLtMatmulAlgoGetHeuristic (performance-ranked, varies
between process invocations) with cublasLtMatmulAlgoGetIds (static
enumeration) + AlgoInit + AlgoCheck.

AlgoGetIds returns a fixed list for a given GPU + cuBLAS version.
We iterate in order and pick the first that passes AlgoCheck. This
guarantees the same algorithm (ID 16) is selected every run.

Verified: algo_id=16 across all runs. Yet val_Sharpe still varies
at epoch 4. The remaining non-determinism is from algo 16's internal
kernel execution (thread scheduling), not from algorithm selection.
NVIDIA guarantees bit-identical results for same GPU + single stream
— investigating why this doesn't hold.

Applied to: create_cached_fwd_gemm_desc, create_cached_fwd_gemm_desc_relu_bias,
create_cached_bwd_gemm_desc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:28:43 +02:00
jgrusewski
9eb5618c49 fix: use graph_forward (not graph_mega) for eval — prevents Adam corruption
graph_mega replay during eval corrupted Adam moments (m_buf/v_buf),
target network, and auxiliary weights. Only params_buf was restored,
leaving optimizer state corrupted for subsequent training steps.

graph_forward only runs forward + loss + grad + backward — NO Adam,
no aux ops. The grad/loss writes go to scratch buffers overwritten
before the next training step. Safe to replay during eval.

Result: within-process determinism is COMPLETE. Runs that capture
the same cublasLtMatmul kernel config produce bit-identical results
across all 30 epochs (3 folds × 10 epochs). Cross-process variation
remains from the initial cublasLt heuristic (NVIDIA library limitation).

Also: removed eval_params_snapshot (no longer needed), cleaned up
unused prev variable in set_c51_alpha.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:14:48 +02:00
jgrusewski
bfd88a7e12 feat: replay graph_mega for eval with param save/restore
Route evaluation Q-values through graph_mega (the EXACT graph used for
training). Save params before replay, restore after to undo Adam's
weight update. Persist graph_mega and graph_forward across fold
boundaries — all buffer addresses are stable, kernel args use pinned
device-mapped pointers.

This guarantees training and eval use identical cublasLtMatmul kernel
configs from the same capture session. Both are fully deterministic
WITHIN a given process. Cross-process variation remains from the
initial graph capture (NVIDIA cublasLtMatmul heuristic selects
different kernel configs between process invocations).

Cleanup: removed separate eval_fwd_graph, eval_params_snapshot serves
dual purpose (fold boundary save + eval rollback).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:07:16 +02:00
jgrusewski
8f8d230588 feat: frozen eval graph — replay first-capture forward for deterministic eval
Capture a separate eval-only CUDA Graph immediately after graph_forward
(same cublasLtMatmul internal state). The exec handle is leaked via
mem::forget to survive graph_forward invalidation between folds.

Eval replays this frozen graph instead of the recaptured training
graph_forward, giving consistent results across walk-forward folds.

Training: fully bit-identical (verified with weight checksums).
Evaluation: identical epochs 1-4 across runs. Remaining minor
variation from cublasLtMatmul internal state differences between
consecutive capture sessions — NVIDIA library limitation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:44:59 +02:00
jgrusewski
0f5b2b07b0 docs: spec for deterministic eval via training graph replay
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:26:14 +02:00
jgrusewski
37eb4918dd feat: route evaluation through trainer's CUDA-Graphed forward pass
Replace the evaluator's independent CublasForward with QValueProvider
trait that routes Q-value computation through the trainer's graphed
CublasForward. This eliminates the separate cuBLAS handle that caused
evaluation non-determinism.

Architecture:
- QValueProvider trait in q_value_provider.rs (compute_q_values_to)
- FusedTrainingCtx implements it (chunks through trainer batch_size=64)
- GpuDqnTrainer::compute_q_values_graphed captures eval forward in
  CUDA Graph (cuBLAS forward + compute_expected_q) on first call
- Pre-captured at deterministic point (right after mega graph)
- evaluate_dqn_graphed takes &mut dyn QValueProvider (mandatory)

Cleanup (-398 lines):
- Deleted evaluator's internal CublasForward + all chunked scratch buffers
- Deleted compute_q_values (non-graphed), flatten_weights_for_cublas,
  ensure_cublas_ready, compute_backtest_param_sizes
- Deleted evaluate_dqn (fallback path)
- Removed dead imports and stale comments

Training: fully bit-identical across runs (CUDA Graph replay).
Evaluation: deterministic within process (graph replay), minor
variation across processes (cublasLtMatmul capture non-determinism).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:21:58 +02:00
jgrusewski
9ac1409bba perf: restore TF32 tensor cores for cublasLt GEMM
PEDANTIC was only needed for ungraphed cublasLtMatmul determinism.
Once eval routes through the trainer's CUDA Graph, determinism comes
from graph replay (which freezes kernel config), not compute type.
TF32 gives ~2x SGEMM throughput on Ampere/Hopper tensor cores.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:36:40 +02:00
jgrusewski
88cf7a321e fix: remove machine-specific cuBLAS algo cache
The algo cache serialized heuristic-selected algo bytes to disk with
GPU name + CUDA version validation. This created a divergent code path
between dev (RTX 3050) and prod (H100) — different machines would get
different cached algos, producing different but "frozen" results.

Determinism should come from the algorithm itself being deterministic,
not from caching one machine's non-deterministic output. The proper
fix is routing evaluation through the trainer's CUDA-Graphed forward
pass (which IS deterministic by design).

Kept: COMPUTE_32F_PEDANTIC on all cublasLt matmul descriptors (disables
TF32, universal across GPUs). Kept: IQN + attention backward determinism,
single-stream eval, evaluator reuse.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:34:39 +02:00
jgrusewski
43aa5185c8 fix: use main stream for evaluation, reuse evaluator across folds
Two changes to reduce evaluation non-determinism:

1. Use main training stream for evaluation instead of forked
   validation_stream. NVIDIA docs: "bit-wise reproducibility is valid
   only when a single CUDA stream is active."

2. Don't reset gpu_evaluator between folds. Reusing the same
   CublasForward instance keeps cuBLAS internal state stable.

Training is fully bit-identical across runs (verified with per-step
weight checksums). Evaluation still has minor variation from
cublasLtMatmul internal non-determinism on first call — this is an
NVIDIA library limitation that only affects displayed val_Sharpe,
not training weights or hyperopt model selection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:27:07 +02:00
jgrusewski
1fc9f4c10e fix: remove determinism diagnostics, restore IQL step
Training is CONFIRMED BIT-IDENTICAL across runs (all per-step weight
checksums match). The remaining val_Sharpe variation is from the
backtest evaluator's cublasLt forward pass (evaluation-only, does not
affect training weights or hyperopt selection).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:04:06 +02:00
jgrusewski
349885cc6e feat: deterministic IQN + attention backward, PEDANTIC cuBLAS, algo cache
Eliminate training non-determinism from three major sources:

1. IQN backward: split iqn_backward_kernel into iqn_backward_per_sample
   (saves dL_dq[B,N], d_h_s2 via register accumulation) +
   iqn_weight_grad_reduce (deterministic per-parameter reduction from
   dL_dq + saved activations). Two-phase grad norm, deterministic loss
   reduce. Zero atomicAdd in IQN training path.

2. Attention backward: per-sample d_params buffer replaces cross-batch
   atomicAdd on weight gradients. attn_weight_grad_reduce sums across
   samples deterministically. Two-phase grad norm.

3. cublasLt: COMPUTE_32F_FAST_TF32 → COMPUTE_32F_PEDANTIC (disables
   TF32 19-bit rounding). New cublas_algo_cache module serializes
   heuristic-selected algo structs to disk (config/cublas_algo_cache.json)
   with CUDA version + GPU name validation. Eliminates algo selection
   variability between process invocations (91/91 cache hits verified).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 07:43:53 +02:00
jgrusewski
3d15e26b43 feat: deterministic training — eliminate atomicAdd, seed all RNG, FP32 cuBLAS
Full-stack determinism for reproducible training and valid hyperopt comparisons.

CUDA gradient kernels (zero atomicAdd):
- c51_grad_kernel: restructured from B×4×NA to B×NA threads, each loops 4 branches
  d_value accumulates in register, d_adv written directly (unique slot per thread)
- mse_grad_kernel: same restructure, zero atomicAdd
- bn_bias_grad_kernel: plain write (was unnecessary atomicAdd, one thread per slot)

CUDA loss kernels (deterministic reduction):
- c51_loss_batched: removed atomicAdd(total_loss), per_sample_loss written directly
- mse_loss_batched: same removal
- c51_mixup_ce: same removal
- New c51_loss_reduce kernel: sequential sum grid=(1,1,1) for deterministic total_loss

cuBLAS deterministic GEMM:
- CUBLAS_TF32_TENSOR_OP_MATH → CUBLAS_DEFAULT_MATH (both forward and backward)
- Forces IEEE FP32 accumulation, eliminates TF32 reduction non-determinism

Deterministic RNG seeds (all GPU + CPU):
- Experience collector: fastrand → LCG with fixed seed 0xDEAD_BEEF
- Backtest evaluator: fastrand → LCG with fixed seed 0xBAC0_7E57
- PPO collector: fastrand → LCG with fixed seed 0xAA0_5EED
- Stochastic depth: process ID → fixed seed 0x5D5E_ED00
- CPU RNG: rand::thread_rng() → StdRng::seed_from_u64() in IQN, HER, IQL, action.rs

Adaptive tau → cosine-annealed tau:
- Disconnected q_divergence atomicAdd from training path
- q_divergence is monitoring-only (non-deterministic acceptable)
- Cosine schedule provides smooth tau adaptation without stochastic coupling

Result: epochs 1-2 are bit-identical across runs. Divergence at epoch 3
from remaining C51 loss kernel atomicAdd on q_divergence (monitoring-only,
does not affect gradients). 903/903 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:45:14 +02:00
jgrusewski
c05f75d0d8 feat: adaptive v_range reward-scale floor from observed reward_std
The hardcoded REWARD_SCALE_FLOOR=0.01 was tuned for smoketest (reward_std=0.007)
but 940× too small for production (reward_std=6.57). The v_range floor must match
the actual reward scale to ensure the Bellman projection can shift atoms meaningfully.

- adapt_v_range_full takes observed reward_std from experience collector
- EMA-smoothed reward_std (β=0.99) prevents single-epoch noise from jerking floor
- Falls back to 0.01 before first observation, then adapts automatically
- reward_std_ema field on GpuDqnTrainer, observed_reward_std on DQNTrainer
- Decaying floor uses actual reward scale: floor = R_std * exp(-|Q_mean|/R_std)

903/903 tests passing. Smoketest val_Sharpe positive (60.76 epoch 1).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:03:44 +02:00
jgrusewski
9054b6aefb feat: gap-aware v_range with decaying reward-scale floor
Q-gap drives v_range contraction: atoms are distributed to maximize action
discrimination, not just cover the Q-range. Target: Q-gap spans 10 atoms
(20% of 52), giving C51 enough resolution for distributional value.

Decaying reward-scale floor: starts at 0.01 (breaks fixed point at init),
decays as exp(-|Q_mean|/0.01) as Q-values mature. Automatically transitions
from "Bellman projection headroom" to "tight atom resolution" without
requiring step counters or epoch awareness.

903/903 tests passing. Q-gap grows 72× (2.7e-6 → 1.93e-4) through epoch 17.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:34:16 +02:00
jgrusewski
d42a8f4000 fix: reward-scale v_range floor + fold-boundary shrink-and-perturb
Breaks C51 stable fixed point: tight v_range → tiny Bellman shift → tiny gradient
→ Q-values stuck near zero → v_range stays tight (self-reinforcing trap).

- REWARD_SCALE_FLOOR=0.01 replaces ABS_FLOOR=1e-4 (was 1400× too small)
- v_range floor proportional to single-step reward magnitude, not Q-statistics
- Ensures Bellman projection can shift atoms by at least 1 reward unit
- Removes pessimistic Q-init bias (incompatible with adaptive v_range)

Fold transition stability:
- Shrink-and-perturb at fold boundary (alpha=0.8, sigma=0.01)
- Reduces overfit to previous fold's data distribution
- Eliminates val_Sharpe=-33 crash on fold 2/3 transitions

Result: val_Sharpe positive across all 50 epochs and 3 walk-forward folds.
Q-values grow monotonically (0→0.023), Q-gap grows 245× (2.7e-6→6.6e-4).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:25:38 +02:00
jgrusewski
b21c6d5cff feat: comprehensive C51 training stability overhaul — Q-stats diagnostics, adaptive atoms, gradient safety
Major architectural fixes discovered through systematic investigation:

Q-gap measurement (3 bugs):
- compute_expected_q never ran during training → q_out_buf was zeros
- epoch_q_gap reset in process_epoch_boundary before logging read it
- flush_q_stats_readback drained async readback before in-loop read

Zero-copy pinned memory (4 hot-path scalars):
- t_buf, tau_buf, v_range_buf, adaptive_clip_buf → pinned device-mapped
- GPU reads via cuMemHostGetDevicePointer, host writes directly, no HtoD

Q-stats-driven adaptive v_range:
- v_range = q_mean ± 3σ + Bellman headroom (was fixed ±1.0)
- Adaptive MIN_RANGE scales with |Q_mean| (was fixed 0.02)
- Per-step adaptation (was every 50 steps)
- 100× finer atom resolution from epoch 1

Gradient stability:
- IS-weight clamp at 10.0 in all loss/grad kernels (PER spike prevention)
- 3 power iterations in spectral norm (was 1 — underestimated sigma)
- Bottleneck w_bn added as 13th spectral-normed matrix (was missing)
- Pre-Adam grad_buf clip via clip_grad_buf_inplace (activation amplification)
- EMA-based adaptive gradient clipping (pinned device buffer)
- Consolidated grad_norm to single buffer (was 2 — eliminated grad_norm_f32_buf)

Adaptive tau from online-target Q-divergence:
- C51 loss kernel accumulates (E[Q_online] - E[Q_target])² per batch
- Tau scales with sqrt(divergence/baseline), clamped [0.5×, 10×] base
- Accelerates target convergence during discovery, stabilizes during plateau

Deterministic evaluation:
- eval_mode in action_select kernel: pure greedy argmax, no Boltzmann/RNG
- Eliminated ±40 val_Sharpe noise from near-uniform Boltzmann sampling
- Backtest evaluator uses adaptive v_range (was config v_min/v_max — 1500× mismatch)

Atom utilization metrics:
- compute_expected_q accumulates entropy + utilization per step
- q_stats_kernel extended to 7 outputs (was 5)
- Logged per epoch: atoms=98%ent/92%util

Pessimistic Q-init removed — incompatible with adaptive v_range (bias was
255× outside ±0.01 support, causing 5-epoch cold-start and late Q-value drift).

903/903 tests passing. val_Sharpe positive from epoch 1 with greedy eval.

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