- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>