Second half of Phase B4b. Wires the trade-outcome label column through
the replay buffer (struct field, allocation, scatter on insert, gather
on sample, direct-to-trainer pointer + setter) and connects the
trainer's aux_to_label_buf to receive per-batch sampled labels.
Completes the end-to-end producer → ring → trainer i32 path the K=3
sparse CE consumer reads.
Replay buffer changes (crates/ml-dqn/src/gpu_replay_buffer.rs):
- New struct fields: aux_outcome_labels (capacity-sized ring),
sample_aux_outcome_labels (mbs-sized fallback gather), trainer_aux_
outcome_labels_ptr (direct-path destination)
- New aux_outcome_labels_ptr field on GpuBatchPtrs
- insert_batch signature: new aux_outcome arg between aux_conf_in and
bs. Scatters via existing K-generic scatter_insert_i32 (same kernel
the K=2 aux_sign_labels uses).
- sample_proportional direct gather when trainer_aux_outcome_labels_ptr
!= 0; fallback gather otherwise. Mirrors aux_sign_labels direct/
fallback semantic exactly.
- New setter set_trainer_aux_outcome_labels_ptr mirrors
set_trainer_aux_conf_ptr.
Collector emission:
- New aux_outcome_labels field on GpuExperienceBatch
- Populated at end of collect_experiences_gpu via dtod_clone_i32 from
Phase B4b-1's per-(env, t) producer scratch.
Trainer wireup:
- aux_to_label_buf_ptr() accessor on GpuDqnTrainer (mirrors
aux_nb_label_buf_ptr)
- trainer_aux_to_label_buf_ptr() delegating accessor on
FusedTrainingCtx
- New set_trainer_aux_outcome_labels_ptr call in training_loop at the
same site where set_trainer_buffers + set_trainer_aux_conf_ptr fire.
Two call sites updated (lines ~835 + ~2857).
- insert_batch call in training_loop passes &gpu_batch.aux_outcome
_labels as new arg.
Test fixtures updated: 4 smoke test files + 3 unit-test fixtures in
gpu_replay_buffer.rs alloc zero-init aux_outcome i32 arg.
End-to-end chain complete:
trade_outcome_label_kernel (A2)
→ collector per-step launch (B3)
→ collector emission (B4b-1)
→ replay-buffer insert + scatter (B4b-2)
→ PER sample + direct gather (B4b-2)
→ trainer aux_heads_forward.loss_reduce (B4) reads sparse {-1,0,1,2}
→ trainer aux_heads_backward (B4) computes per-sample partials
→ Adam SAXPY (B1+B4) updates W1, b1, W2, b2 at [163..167)
The "degraded predict-Profit-everywhere" cold-start from Phase B4 is
resolved. K=3 head trains on real sparse trade-outcome labels.
Verification:
- cargo check -p ml clean (21 warnings, none new).
- cargo test -p ml --lib → 1016/0 on clean runs; pre-existing
NoisyLinear flake still surfaces ~30-50% of runs (unrelated to
vNext work — see ebc1b1502 / 20e1aea27 commit notes).
Remaining vNext work:
- B5: input concat 256 → 262 with plan_params
- Phase C: 3-slot state assembly (state[121..124])
- Phase D: 12-weight W atom-shift (4 actions × 3 outcomes)
- Phase E: dW backward + Adam for W[4, 3]
- Phase F: validation smoke at β=0.5 structural prior
Audit: docs/dqn-wire-up-audit.md Phase B4b-2 section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The B0 audit (commit 62ab8ed85) under-counted insert_batch test
callers as 2 (1 production + 1 in-file unit test). Surfaced during
B1.0 implementation when cargo check --workspace --tests failed
with 5 arity-mismatch errors after B0's signature change.
Root cause: B0 audit's grep filter was `grep -v test` and didn't
enumerate crates/ml/src/trainers/dqn/smoke_tests/ (compiled as
part of the lib's test binary, not behind #[cfg(test)]) nor
crates/ml/tests/.
Sites fixed (zero-init i32 alloc, threaded through):
- crates/ml/src/trainers/dqn/smoke_tests/training_stability.rs:152, 196
- crates/ml/src/trainers/dqn/smoke_tests/performance.rs:142
- crates/ml/src/trainers/dqn/smoke_tests/gpu_residency.rs:75
- crates/ml/tests/gpu_per_integration_test.rs:125
No behavior change — the column carries zero data and no consumer
reads it pre-B1.1. B1.1 lands the producer kernel that fills with
-1/0/1 from the 30-bar price trajectory.
Process correction documented in docs/dqn-wire-up-audit.md
"B0.1 cascade-gap fix-up" subsection: future B-series audits must
run cargo check --workspace --tests before claiming cardinality
completeness.
Build: cargo check --workspace --tests clean.
Tests: cargo test -p ml --lib compiles + passes (GPU tests
#[ignore]-gated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously test_training_throughput_measurement and test_real_data_single_epoch
only asserted loss.is_finite() on the final metric. That passes on trivial
zeros, on huge-but-finite NaN-disguised values, and on any regression that
doesn't produce literal NaN — giving effectively no signal.
test_training_throughput_measurement now asserts:
- loss finite AND non-negative
- epochs_trained >= 1
- throughput floor: epochs_per_sec > 0.05 (i.e. each epoch < 20s on the
RTX 3050 Ti; catches accidental CPU fallback or kernel CPU-pinning).
Documented as a conservative local floor; CI may tighten.
- avg_q_value present, finite, |avg_q| < 1e6 (rules out finite-but-huge
NaN propagation)
test_real_data_single_epoch now asserts:
- loss finite, non-negative, and < 1e8 (a real DQN loss of 0.0 is a
sign-bug or accumulation-bug tell; huge-but-finite rules out NaN
propagation)
- epochs_trained >= 1
- avg_q_value finite and |avg_q| < 1e6
Why these are safe:
- Bounds are chosen from observed smoke runs with 2-3 orders of margin.
- Passes locally in 1.58s and 10.24s respectively.
- Designed to flag regressions, not true production-scale deviations.
Verified PASS on laptop (RTX 3050 Ti).
Remove pub state_dim field from DQNConfig and GpuReplayBufferConfig; remove the
state_dim field from GpuExperienceCollector. Replace all reads with
ml_core::state_layout::STATE_DIM (and STATE_DIM_PADDED for cuBLAS-padded
strides). Checkpoint loading now validates saved state_dim against the
constant and hard-errors on mismatch. GpuAttentionConfig.state_dim is a
distinct attention-feature dim and is left untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix f32→bf16 type mismatches in 7 ml test files:
- gpu_action_selector, mod.rs, signal_adapter, gpu_residency,
gradient_budget, performance, training_stability
- Convert Vec<f32> → Vec<half::bf16> for memcpy_htod
- Convert readback to bf16 then to f32
- Relax tolerances for BF16 precision
Remaining 57 failures: nvrtc stubs in ml-ppo cuda_nn + kernel name
mismatches + replay buffer type issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CUDA kernels: 36 of 37 compiled with __nv_bfloat16* (dt_kernels.cu remaining)
Rust types: ALL CudaSlice<f32> → CudaSlice<half::bf16> across ml + ml-core
Build: all kernels now compiled with common_device_functions.cuh (BF16 helpers)
BF16 math wrappers: bf16_sqrt, bf16_log, bf16_exp, bf16_pow, bf16_fabs, bf16_fmax,
bf16_fmin, bf16_cos, bf16_zero, bf16_one, bf16(), atomicAddBF16
NOT YET COMPILING — ml-core boundary errors (Vec<f32> → Vec<half::bf16>)
and dt_kernels.cu float*__nv_bfloat16 ambiguity remain.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove 6 eprintln!("[GPU-DEBUG]...") statements from gpu_dqn_trainer.rs,
constructor.rs, and elementwise.rs
- Delete log_gpu_memory() function and all 18 callers in smoke test files
- Remove check_err() drains from GpuDqnTrainer::new() and
GpuExperienceCollector::new() — root cause is fixed (per-context kernel cache)
- Keep check_err() after CUDA Graph capture (legitimate — drains event tracking errors)
- Fix broken import lines in smoke test files after sed cleanup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: `SMOKE_CUDA: OnceLock<MlDevice>` held a static Arc<CudaContext>
for the process lifetime. CudaSlice Drop from test N recorded errors on
this shared context's error_state, causing test N+1's bind_to_thread() to
fail with CUDA_ERROR_INVALID_VALUE.
Fix: create a fresh MlDevice per test (no static caching). Each test gets
its own CudaContext Arc with clean error_state.
Also: convert all GPU smoke tests from #[tokio::test] to synchronous #[test]
with explicit tokio::runtime::Builder::new_current_thread(). The runtime is
explicitly dropped between tests, ensuring all Arc<CudaContext> refs are freed.
Result: 5 of 6 sequential smoke tests now pass. The remaining 1 failure is
a real Candle GpuTensor bug: the replay buffer insertion path still uses
Candle's elementwise kernels, which cache CudaFunction handles that become
stale across test boundaries. Fix: eliminate Candle from replay buffer path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eliminate the 3 CPU roundtrips per sample_proportional() call (called
300x/epoch), which was the #1 performance bottleneck:
1. Replace memcpy_dtoh(cs_total) with DtoD copy to pre-allocated
total_sum_buf — total_sum never leaves GPU
2. Replace CPU rand() + memcpy_htod(thresholds) with GPU-resident
Philox PRNG kernel — thresholds generated directly on device
3. Replace memcpy_htod([0.0], max_weight) with memset_zeros —
async GPU memset, zero host staging
4. is_weights_f32 kernel now reads total_sum from GPU pointer
instead of scalar argument
Pre-allocate 13 PER sampling buffers on the GpuReplayBuffer struct
(thresholds, indices, gathered data, weights, max_weight, total_sum_buf,
rng_step counter). All intermediate computation uses these pre-allocated
buffers. Output GpuBatchSlices are DtoD-cloned for ownership transfer.
Add max_batch_size field to GpuReplayBufferConfig (defaults to 1024).
Delete the cs_total() method entirely.
Net result: zero memcpy_dtoh, zero memcpy_htod, zero CPU synchronization
points in the PER sampling hot path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests that load full 163K-bar training datasets take 30+ min in CI.
Mark them #[ignore] — run via nightly cron or manual trigger.
Affected: gpu_residency(2), training_stability(2), performance(2),
feature_coverage(1), ppo_benchmark(1), cache(1)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed #[ignore] from tests that have local infrastructure:
- 3 data_loader tests: auto-detect test_data/real/databento/ via workspace
- 3 memory_profiler tests: nvidia-smi at /usr/bin/nvidia-smi
- 4 benchmark tests (TFT, Mamba2, DQN, PPO): GPU + DBN data available
- 1 inference test: model loading (slow but should run)
- 3 DQN performance smoke tests: GPU available
PPO benchmark: fixed data_path to test_data/real/databento/6E.FUT
Sequential: added vars_mut() accessor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update all callers to match the new pure-cudarc APIs introduced by the
hive agent CudaSlice migration. Key changes:
- GpuTrainingGuard::new() now takes Arc<CudaStream>; callers use from_device()
- check_and_accumulate/qvalue_stats/qvalue_divergence take &CudaSlice<f32>
instead of &Tensor; callers convert via tensor_to_cuda_slice_f32()
- accumulate_q_value takes f32 scalar, returns () (no Result)
- GpuReplayBuffer::insert_batch gains batch_size arg, takes CudaSlice params
- signal_adapter functions take &Arc<CudaStream> (cudarc 0.17 Arc requirement)
- Add tensor_to_cuda_slice_u32() and cuda_f32_to_tensor() utility functions
- Replace CudaView usage with owned CudaSlice via tensor_to_cuda_slice_f32()
- Fix CudaStorage.device field access (was method call in older API)
- Fix borrow-after-move in copy_actions_out via scoped DtoD copy
Zero errors, zero warnings across lib + tests + examples + full workspace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: shmem_max_in_dim only included trunk dims (state_dim,
shared_h1, shared_h2) but not head dims (value_h, adv_h). When
hidden_dim_base=32 made the trunk narrow while heads stayed at 128,
the BF16 weight tile for branch output (255×128=32640 BF16 elements)
overflowed the shared memory region (12288 BF16 elements). On H100
the overflow landed in unused-but-mapped hardware shmem (silent
corruption). On RTX 3050 (48KB physical shmem) it hit unmapped
memory → CUDA_ERROR_ILLEGAL_ADDRESS.
Changes:
- gpu_dqn_trainer.rs: shmem_max_in_dim includes value_h/adv_h
- Remove all #[ignore] from smoke tests (feature_coverage,
training_stability, gpu_residency)
- Smoke tests use real .dbn data from test_data/ (hard error if missing)
- Remove synthetic_data() fallback — no fake data in tests
- GPU-direct DtoD training path (train_step_gpu, FusedTrainScalars)
- GPU-native PER priority update kernel (zero CPU readback)
- IQN dual-head integration (gpu_iqn_head.rs)
- BF16 dtype fixes across 6 model adapters
- Hyperopt 30D→31D (iqn_lambda)
- portfolio_transformer: unconditional BF16 (remove dead CPU branches)
- liquid/adapter: all tests use Cuda(0) directly
- Fix pre-existing gpu_kernel_parity_test.rs (stale args)
- Fix pre-existing evaluate_baseline.rs (removed fields)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove ALL #[cfg(feature = "cuda")] guards (~400+ occurrences)
- Remove ALL #[cfg_attr(not(feature = "cuda"), ignore)] test annotations (~250)
- Make cuda default feature in 9 ML crates (ml, ml-core, ml-dqn, ml-ppo, etc.)
- Convert nvrtc JIT compilation to precompiled nvcc (searchsorted, prefix_sum)
- Move compile_ptx_for_device() to ml-core for shared access
- Delete dead CPU code: multi_step.rs, self_supervised_pretraining.rs,
training_guard_gpu_tests.rs, CPU PER buffer paths, CPU Q-diagnostics
- Replace unwrap_or(Device::Cpu) with hard errors everywhere
- Remove dead is_cuda() else branches in DQN/PPO/hyperopt trainers
- Change config defaults from "cpu" to "cuda" (rainbow, tlob, pipeline)
- Port IQL value network to GPU kernel (5 CUDA entry points)
- Port HER goal relabeling to GPU kernel (warp-per-sample)
- Wire DSR GPU-to-CPU sync in training loop
- cfg!(feature = "cuda") → true in inference_validator
Zero warnings, zero errors across entire workspace.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cast input to weight dtype in DQN residual, rmsnorm, noisy_layers
- Set use_gpu=true in QNetworkConfig defaults and all config sites
- Resolve BF16 boundary mismatches in attention, curiosity, branching,
distributional_dueling across ml-dqn
- GPU-resident regime ops with BF16 boundary casts, eliminate .expect() in CUDA paths
- Eliminate all Device::Cpu fallbacks — GPU-only across 10 ML crates
- PPO: cast logits to F32 before softmax, cast batch tensors to training dtype
- Gradient collapse detection for RegimeConditionalDQN
- Wire halt_grad_collapse from CUDA guard kernel to halt training
- Dead neuron detection uses active network VarMap + squeeze factored readback
- Increment gradient_logging_step in GPU PER path
- Gradient collapse warmup guards use original buffer_size
- Cap training steps per epoch + tracing migration
- Replace Tensor::all() with sum_all() for pinned Candle compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The GPU PER replay buffer had a hardcoded 4 GB MAX_BYTES limit that
rejected the auto-sizer's 10M-entry proposal on H100 (80 GB VRAM).
Now per_max_buffer_bytes() computes 20% of total VRAM (min 1 GB) and
flows through OptimalReplayConfig → DQNConfig → GpuReplayBufferConfig
so both subsystems agree on the budget.
Also fixes misleading regime detection log (indices 211/219 → 40/41)
and renames dqn_config_2025 → dqn_default_config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>