Commit Graph

1205 Commits

Author SHA1 Message Date
jgrusewski
3696cb475b fix: update action reporting from 9-level to 7-level ExposureLevel
ShortSmall=0, ShortHalf=1, ShortFull=2, Flat=3, LongSmall=4, LongHalf=5,
LongFull=6. Updates all array sizes, bounds checks, names, and distribution
logic across gpu_monitoring, monitoring, training_loop, metrics, and financials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 12:12:18 +02:00
jgrusewski
1e133e4ca1 feat: enable PopArt + tau coupling + verify PER/Sharpe reset between folds
Task 3: Enable PopArt reward normalization (default true, all 3 TOML configs,
wiring confirmed in fused_training.rs submit_forward_ops_main()).
Task 4: Couple PopArt variance with tau reset — add prev_popart_var to
FusedTrainingCtx, read_popart_var() to GpuDqnTrainer, read_popart_variance()
+ should_reset_tau() to FusedTrainingCtx, tau reset injected at epoch boundary
in training_loop.rs after log_phase_timing().
Task 5: Add best_sharpe/best_epoch/best_val_loss reset to reset_for_fold() so
each walk-forward fold competes independently. Also wire v8 reward fields
(popart_enabled, micro_reward_scale, td_lambda, max_trace_length,
hindsight_fraction, hindsight_lookahead) through training_profile.rs apply_to().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:02:29 +02:00
jgrusewski
a401f03f9a fix: remove 30% Flat floor + fix magnitude masking — ABI exploration replaces it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 11:56:32 +02:00
jgrusewski
448b61d095 refactor: collapse 9-level to 7-level ExposureLevel — eliminate degenerate Flat variants
The 4-branch DQN (direction x magnitude) had 3 degenerate variants
(Short25, Flat, Long25) that all mapped to 0.0 target exposure when
direction=Flat, causing 82% Flat collapse. Collapse these into a
single Flat variant, giving 7 levels (ShortSmall/Half/Full, Flat,
LongSmall/Half/Full) and 63 total factored actions (7x3x3).

- ExposureLevel enum: 9 variants -> 7 (add direction/magnitude/from_dir_mag)
- FactoredAction: 81 -> 63 total actions, from_index/to_index updated
- DQN epsilon-greedy: use from_dir_mag() instead of dir*3+mag indexing
- DQN config: num_actions default 9 -> 7
- PPO action space: 45 -> 63 actions, action masking updated
- Signal adapter CUDA kernel: 5-bin -> 7-bin exposure aggregation
- All tests updated for new variant names and index ranges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:54:09 +02:00
jgrusewski
bdcc3b7d30 perf: cublasLt RELU_BIAS epilogue fusion for trunk + branch hidden GEMMs
Fuses GEMM + bias-add + ReLU into a single cublasLtMatmul kernel via
CUBLASLT_EPILOGUE_RELU_BIAS. Eliminates 7 separate add_bias_relu
kernel launches per forward pass (3 trunk + 4 branch hidden layers).

Creates separate cached descriptors with epilogue enabled at init time.
Falls back to separate kernels if the epilogue heuristic isn't available.
Bias pointer set dynamically per-call via set_matmul_desc_attribute.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:57:57 +02:00
jgrusewski
7c34003f03 perf+fix: cuBLAS descriptor caching, per-branch workspace, shrink-perturb fix
Performance:
- Cache cuBLAS descriptors: pre-create matmul_desc + layouts + algo at init.
  requestedAlgoCount=3 for better algorithm selection. Zero per-GEMM overhead.
- Per-branch workspace: 4 × 32MB separate workspace buffers for multi-stream
  branch dispatch. Eliminates workspace contention on parallel execution.

Training stability:
- Remove hardcoded shrink-perturb that fired every epoch on short runs (3-5 epochs).
  With epochs=5, interval = epochs/4 = 1 → fired every epoch, destroying epoch 1
  learned weights. This caused Sharpe to collapse from +0.60 to -0.29 after epoch 1.
- Phase 3 shrink-perturb now uses config values (was hardcoded alpha=0.9, sigma=0.01).
- The config-defined shrink_perturb_interval=20 now controls all shrink-perturb timing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:40:56 +02:00
jgrusewski
74f27d18f3 fix: revert workspace 16MB→32MB — H100 CUDA 13.0 CUBLAS_STATUS_NOT_SUPPORTED
The backward GEMM heuristic on H100 with CUDA 13.0 requires 32MB
workspace for TF32 algorithm selection. 16MB causes
CUBLAS_STATUS_NOT_SUPPORTED on dW_only (m=256,n=128,k=16384).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:19:35 +02:00
jgrusewski
1e5ed3bde1 perf: target branch parallelism + attention Q,K,V cache + workspace reduction
Three optimizations targeting the 345ms mega-graph bottleneck:

1. Target forward multi-stream branches (est -15ms):
   Replaced single aliased tg_h_b_scratch with 4 separate buffers.
   Target forward now uses fork-join multi-stream dispatch (same as
   online forward) — 4 branch GEMMs run in parallel instead of serial.

2. Attention Q,K,V projection caching (est -10ms):
   Forward kernel saves Q,K,V projections to a 14MB buffer.
   Backward kernel reads saved projections instead of recomputing
   from weights — eliminates 12 O(D) inner products per sample.

3. cuBLAS workspace reduction (32MB → 16MB):
   Sufficient for H100 TF32 tile sizes at our matrix dimensions.
   Reduces L2 cache pressure from workspace allocation.

Combined with IQN h_s2 reuse from prior commit: target 345ms → <300ms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:08:55 +02:00
jgrusewski
a93c35a614 perf: IQN reuses DQN Pass 2 target h_s2 — eliminates redundant trunk forward
IQN was recomputing target h_s2 via iqn_trunk_forward_kernel using the
same target weights on the same next_states that DQN Pass 2 already
computed into tg_h_s2_buf. Now IQN reads from the pre-computed buffer
via a DtoD copy (negligible) instead of re-running 2 cuBLAS GEMMs.

Expected: -15ms GPU time on H100 (batch=16384, shared_h2=256).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 09:53:26 +02:00
jgrusewski
3b2bf8d993 fix: remove in-graph sub-phase events (CUDA_ERROR_INVALID_VALUE)
Events recorded during CUDA graph capture cannot be queried with
cuEventElapsedTime after graph replay — the graph creates internal
copies. Reverted to original mega-graph capture structure.

Sub-graph timing fields kept in PhaseEvents for future use with
split-graph diagnostic mode or nsys profiling.

Also kept submit_loss_and_grad_ops() extraction and pub(crate) visibility
on launch_cublas_forward/backward for future per-phase graph splitting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 02:41:41 +02:00
jgrusewski
ef5be1b7a3 perf: add sub-graph phase events inside mega-graph capture
Split fwd_bwd timing into 5 sub-phases: spectral, forward, loss,
backward, aux. Events recorded during graph capture replay with the
graph on every step.

Also extract submit_loss_and_grad_ops() from submit_forward_ops_main()
and make launch_cublas_forward/backward pub(crate) for sub-graph timing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 02:31:03 +02:00
jgrusewski
a04fcb31f5 fix: cuEventElapsedTime → cuEventElapsedTime_v2 for CUDA 13.0 (H100)
CI builder uses CUDA 13.0 cudarc which exports _v2 suffix.
Local CUDA 12.x has both names but CI only resolves _v2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 02:06:58 +02:00
jgrusewski
a642fe5ad9 fix: deadlock in epoch boundary — remove cuStreamSynchronize, fix Q-diagnostics
Root cause: cuStreamSynchronize blocked the thread, preventing tokio
single-threaded runtime from progressing RwLock .write().await calls.

- Remove cuStreamSynchronize from training loop (stalls pipeline)
- Move log_phase_timing() after process_epoch_boundary (DtoH syncs stream)
- compute_epoch_q_diagnostics takes &mut DQNAgentType param (no re-lock)
- Disable Q-value gap diagnostics (deadlock in single-threaded runtime)
- smoke_trainer helpers: always set fxcache dir
- test_no_hang_single_epoch: use fxcache direct loading

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 01:35:13 +02:00
jgrusewski
7341491776 fix: fxcache always-on in smoke tests + cuStreamSynchronize before event timing
- smoke_trainer/smoke_trainer_with: always set with_feature_cache(feature_cache_dir())
- Add load_smoke_fxcache() + init_trainer_from_fxcache() helpers for direct loading
- test_no_hang_single_epoch: use fxcache directly (was slow DBN parsing)
- cuStreamSynchronize before log_phase_timing (was CUDA_ERROR_NOT_READY)
- Fix evaluation engine tests for 9-level ExposureLevel mapping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 01:11:20 +02:00
jgrusewski
ee6088eb53 fix: 4-branch inference indexing + GpuTensor→CudaSlice API boundaries
Bug fixes:
- select_action_inference: exposure = dir*3+mag (was using dir only),
  order = greedy[2] (was [1]), urgency = greedy[3] (was [2])
- select_action_with_confidence: same 4-branch indexing fix,
  random path 0..5 → dir*3+mag (9 exposure levels)

GpuTensor elimination at API boundaries:
- batch_branching_q_values returns (CudaSlice<f32>, ×3) via into_parts()
- batch_q_values, forward return CudaSlice<f32> directly
- batch_greedy/softmax_actions return Vec<u32> (no GpuTensor wrapping)
- Delete extract_cuda_f32! macro — callers use CudaSlice directly
- GpuActionSelector already uses raw CudaSlice — now the whole
  action selection chain matches

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:13:33 +02:00
jgrusewski
e265768cb3 cleanup: delete dead CPU replay buffers, DQNAgentType::Standard, add CUDA event profiling
- Delete 7 dead files: prioritized_replay.rs, prioritized_replay_staleness.rs,
  replay_buffer.rs, hindsight_replay.rs, rainbow_agent.rs, checkpoint.rs,
  strategy_dqn_bridge.rs (-4,760 lines)
- Rewrite replay_buffer_type.rs: ReplayBufferType enum → StagedGpuBuffer struct
  (GPU PER is the only replay path, no CPU fallbacks)
- Strip agent.rs to data types only (TradingState + AgentMetrics)
- Convert DQNAgentType from enum to struct wrapping RegimeConditionalDQN
  (Standard variant was never constructed, 43 dead match arms removed)
- Remove dead CPU methods: store_experience, fused_post_step,
  fused_post_step_no_ema, adaptive_buffer_resize, refresh_stale_per_priorities
- Add always-on CUDA event per-phase profiling (8 events, 4 phases:
  upload/fwd_bwd/adam/per_update) with Drop cleanup and error checking
  to identify 329ms/batch H100 bottleneck

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:13:09 +02:00
jgrusewski
74392e3744 fix(critical): 16-byte align weight pointers for cublasLtMatmul FAST_TF32
Root cause: weight tensors packed sequentially in the flat params buffer
had non-aligned start offsets when preceding tensors had odd element counts
(e.g. bias of 51 atoms = 204 bytes, 204 % 16 = 12). cublasLtMatmul with
CUBLAS_COMPUTE_32F_FAST_TF32 requires 16-byte aligned buffer pointers.

Fix: pad each tensor to 4-element boundary (16 bytes) in both
f32_weight_ptrs_from_base (pointer computation) and compute_total_params
(buffer allocation). Added align4() and padded_byte_offset() helpers,
fixed shrink_perturb skip range and bottleneck gradient offset.

Switched compute type: CUBLAS_COMPUTE_32F → CUBLAS_COMPUTE_32F_FAST_TF32
(forward + backward). Explicit TF32 tensor core path, required by cuBLAS
13.0 on H100 SM90.

Deleted dead bf16_weight_ptrs function.
19/19 smoke tests pass on RTX 3050.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:38:14 +02:00
jgrusewski
904a158df1 perf: eliminate f32 shadow buffers — GemmEx reads master params directly, saves 2x weight VRAM + copy overhead
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:52:29 +02:00
jgrusewski
6eda39b829 fix: 50-epoch overfitting test — zero hardcoded thresholds, all checks relative
Add last_epoch_loss metric. All assertions now compare the run's own metrics
against each other: convergence = loss stability (not divergence), overfitting =
best_oos_sharpe >= first_sharpe, OOS collapse = val_loss swing/magnitude ratio.

Walk-forward fold resets make epoch-1 loss unreliable for convergence comparison
(pre-trained weights), so we check stability instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:33:46 +02:00
jgrusewski
5fa877aaa8 fix: rewrite 50-epoch overfitting test with calculated bounds, add final_sharpe/final_val_loss metrics
- Add final_sharpe and final_val_loss to TrainingMetrics (last-epoch values)
- Rewrite test_walk_forward_no_overfitting_50_epochs: all checks are relative
  to the run's own metrics — no hardcoded thresholds
- Checks: finiteness, gradient health, OOS collapse ratio, IS→OOS gap ratio
- 19/19 smoke tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:25:50 +02:00
jgrusewski
42308b159f fix: resolve_data_dir helper for consistent relative path resolution
Extract shared resolve_data_dir() function — resolves relative data paths
against workspace root via CARGO_MANIFEST_DIR. Used for mbp10_data_dir
and trades_data_dir (was only mbp10 before, causing smoke test failures).

All 19/19 smoke tests pass with pure f32 pipeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:15:37 +02:00
jgrusewski
ddbad94329 cleanup: remove half crate dependency from entire workspace
half crate no longer needed — zero bf16 references remain.
Removed from: ml, ml-core, ml-dqn, ml-ppo, ml-supervised, workspace root.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:54:14 +02:00
jgrusewski
4842a04011 refactor: eliminate ALL bf16 from entire workspace — pure f32/TF32 pipeline
Complete bf16 elimination across all crates (ml, ml-core, ml-dqn, ml-ppo,
ml-supervised). Zero half::bf16, __nv_bfloat16, or CudaSlice<half::bf16>
references remain (verified by grep).

CUDA: All 60+ .cu kernels and .cuh headers converted to native float.
  - Half-precision intrinsics (__hmul, __hadd, __hdiv) → float operators
  - atomicAddBF16 → native atomicAdd(float)
  - bf16 wrapper functions → f32 identity passthroughs

Rust: All CudaSlice<half::bf16> → CudaSlice<f32> across 90+ files.
  - htod_f32_to_bf16/dtoh_bf16_to_f32 → htod_f32/dtoh_f32 (direct, no conversion)
  - Deleted bf16 mirror infrastructure (DuelingWeightSetBf16, alloc_bf16_mirror, etc.)
  - Renamed params_bf16→params_flat, d_value_logits_bf16→d_value_logits, etc.
  - Fixed .to_f32() sed damage on Decimal::to_f32() and rng.f32()

FxCache: Single f32 disk format (was bf16/f64 dual-version).
  - Deleted --bf16 CLI flag from precompute_features
  - PVC cache files need regeneration via precompute_features

TF32 tensor cores activated via cublasLtMatmul CUBLAS_COMPUTE_32F — no
explicit TF32 types needed. Storage is pure f32 everywhere.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:52:21 +02:00
jgrusewski
00bbe76b05 refactor: fxcache single f32 format — delete bf16 version, remove --bf16 CLI flag
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:45:49 +02:00
jgrusewski
d11b9870ff refactor: gpu_dqn_trainer — rename all bf16 fields/variables to f32, fix cast kernel names
- Rename f32_to_bf16_kernel → copy_kernel_a, bf16_to_f32_kernel → copy_kernel_b
- Rename bf16_states_buf → states_staging_buf, bf16_next_states_buf → next_states_staging_buf
- Convert staging buffers from CudaSlice<u16>/alloc_u16 → CudaSlice<f32>/alloc_f32
- Fix bias_bf16 → bias_f32 (remove redundant identity map)
- Rename launch_f32_to_bf16_params_cast → launch_f32_copy_params
- Rename launch_f32_to_bf16_target_cast → launch_f32_copy_target
- Rename cast_d_logits_to_bf16 → cast_d_logits_to_f32_staging
- Rename check_nan_bf16 → check_nan_f32_b, fix duplicate nan_check_f32_kernel field
- Fix dtod_from_bf16 → dtod_from_staging (now CudaSlice<f32>)
- Fix launch_bf16_to_f32 → launch_f32_copy (now f32→f32)
- Update all ~90 bf16 comments/error messages to f32
- Only remaining bf16 refs are CUDA kernel name strings (must match .cu source)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:28:06 +02:00
jgrusewski
0131b2904b refactor: rename all bf16 transfer functions → f32 across 19 files, delete legacy aliases
htod_f32_to_bf16 → htod_f32, clone_htod_f32_to_bf16 → clone_htod_f32,
dtoh_bf16_to_f32 → dtoh_f32. No wrappers — all 67 call sites renamed directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:11:50 +02:00
jgrusewski
95edfd024b refactor: gpu_weights all CudaSlice<half::bf16> → CudaSlice<f32>, delete bf16 mirror infrastructure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:11:09 +02:00
jgrusewski
8fa301dda8 refactor: mod.rs bf16 helpers → direct f32 transfers, legacy aliases for incremental migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:06:25 +02:00
jgrusewski
eb2c16acb9 refactor: eliminate __nv_bfloat16 from all 30 CUDA kernel files and shared header
Mechanical regex sweep: __nv_bfloat16 → float, __float2bfloat16(x) → (x),
__bfloat162float(x) → (x), removed cuda_bf16.h includes. Conversion kernels
(f32_to_bf16_kernel, bf16_to_f32_kernel) are now identity copies. Removed
duplicate overloads (matvec_leaky_relu, curiosity_inference) that became
identical after type substitution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:01:59 +02:00
jgrusewski
0662f9bbf8 refactor: rewrite common_device_functions.cuh — bf16 wrappers now pure f32 identity functions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:58:22 +02:00
jgrusewski
0fa5b993dd fix(proper): force cuMemAlloc_v2 in cudarc — cuMemAllocAsync incompatible with cublasLtMatmul on H100
Root cause: cuMemAllocAsync (stream-ordered memory pools) produces
memory that cublasLtMatmul rejects with CUBLAS_STATUS_NOT_SUPPORTED
on H100 (CUDA 13.0 driver) when the matmul runs on a different
stream than the allocation. Despite CUDA documentation stating
same-device async allocations should be accessible from all streams
with proper event synchronization, cublasLtMatmul disagrees.

Proof chain:
- C++ test with cudaMalloc: ALL dimensions pass on H100 ✓
- Rust test with cuMemAlloc: ALL dimensions pass on H100 ✓
- Rust test with cudarc alloc_zeros (cuMemAllocAsync): FAILS ✗
- Same dimensions, same handle, same workspace — only alloc differs

Fix: set has_async_alloc=false unconditionally in CudaContext::new().
This forces cuMemAlloc_v2 for ALL allocations. Zero performance impact
(allocations at construction time, not in hot loops).

Also: converted has_async_alloc from bool to AtomicBool for safety.
Removed diagnostic code (CUBLASLT_DIAG, test_lt_matmul_raw).
Reverted cuMemAlloc workspace hack (now uses normal alloc_zeros).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:33:50 +02:00
jgrusewski
8b9d8e0988 diag: test cublasLtMatmul with fresh cudaMalloc buffers before experience collector forward
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:29:01 +02:00
jgrusewski
171b9a241c diag: standalone cublasLtMatmul Rust test for H100 debugging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:06:05 +02:00
jgrusewski
45ff940e4b diag: disable multi-stream branch dispatch in f32 forward to test H100 cublasLtMatmul
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:29:04 +02:00
jgrusewski
06f9819f05 fix(critical): use cuMemAlloc for cublasLt workspace (cross-stream access)
cudarc's stream.alloc_zeros uses cuMemAllocAsync (stream-ordered).
These allocations are only accessible from the allocating stream.
When cublasLtMatmul runs on a forked BRANCH stream (multi-stream
branch dispatch), the stream-ordered workspace is inaccessible,
causing CUBLAS_STATUS_NOT_SUPPORTED on H100 at batch=4096.

Fix: allocate workspace via cuMemAlloc_v2 (synchronous, globally
accessible from all streams). This matches the C++ debug test
which uses cudaMalloc and works on H100 for all dimensions.

Also: add d_layout destroy (leaked handle cleanup).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:14:00 +02:00
jgrusewski
90826adad3 fix(critical): destroy d_layout in lt_matmul — leaked handle caused stale reuse
The forward lt_matmul destroyed a_layout, b_layout, c_layout, matmul_desc,
and matmul_pref — but NOT d_layout. The leaked handle caused cublasLt to
return stale descriptors on subsequent create calls. On H100 at batch=4096,
the dimension mismatch between the stale first-call layout and the actual
GEMM dimensions triggered CUBLAS_STATUS_NOT_SUPPORTED.

On RTX 3050 with small batches, the stale layout happened to be compatible
(oversized) for all subsequent GEMMs, masking the bug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:48:33 +02:00
jgrusewski
03c4f872e9 fix: convert remaining bf16 activation buffers in experience collector + evaluator
Experience collector: exp_h_s1, exp_h_s2, exp_h_v changed from
CudaSlice<half::bf16> to CudaSlice<f32>. These were unused legacy
fields but their bf16 type was inconsistent with the f32 pipeline.

Backtest evaluator: states_buf and chunked_states_buf padded to
state_dim_padded (pad128). gather_states kernel updated with padded_sd
parameter. DtoD copy uses padded row bytes.

C++ debug test confirms cublasLtMatmul works on H100 for all
dimensions including (128,4096,256) and (128,16384,256) with
both COMPUTE_32F and FAST_TF32.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:18:35 +02:00
jgrusewski
eda50b4eb5 fix(critical): pad backtest evaluator states buffer to state_dim_padded
Root cause of cublasLtMatmul CUBLAS_STATUS_NOT_SUPPORTED:
- states_buf allocated [batch * state_dim] (unpadded)
- cuBLAS forward GEMM reads with stride state_dim_padded (pad128)
- Buffer overflow: GEMM reads past buffer end

cublasLtMatmul validates buffer sizes against layout descriptors and
returns NOT_SUPPORTED for undersized buffers. cublasSgemm silently
read garbage — this was the source of the "parallel test congestion
errors" seen previously.

Fix:
- Allocate states_buf with state_dim_padded stride (3 allocation sites)
- gather_states kernel: add padded_sd parameter, write with padded stride
- DtoD copy: use padded row bytes
- Both gather_states call sites updated with padded_sd arg

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:31:35 +02:00
jgrusewski
b456691de5 fix: use CUBLAS_COMPUTE_32F for cublasLtMatmul (SM86 compat)
CUBLAS_COMPUTE_32F_FAST_TF32 returns NOT_SUPPORTED on RTX 3050 (SM86)
for certain (m,n,k) via cublasLtMatmul (e.g., 128×512×64). Plain
CUBLAS_COMPUTE_32F works for all dimensions. TF32 tensor cores are
still enabled via CUBLAS_TF32_TENSOR_OP_MATH math mode on the cuBLAS
handle — this activates TF32 on Ampere+ automatically when the
hardware supports the specific matrix dimensions.

Also: use sys::cublasLtMatmul directly (bypass result:: wrapper)
for clearer argument ordering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:33:36 +02:00
jgrusewski
754b695bc0 feat: migrate all GEMMs from cublasGemmEx to cublasLtMatmul
cublasLtMatmul passes workspace+stream per-call (no handle state),
eliminating the cublasSetStream workspace conflict that hung CUDA
Graph mega-capture on H100 with cublasGemmEx.

Forward (22 call sites via sgemm_f32/sgemm_f32_ldb):
- cublasLtMatmul with CUBLAS_COMPUTE_32F + per-call stream
- No more cublasSetStream for branch dispatch (stream passed directly)
- 32MB workspace unconditionally (TF32 HMMA needs it even on Ampere)

Backward (7 call sites via sgemm_f32):
- Same cublasLtMatmul pattern with flexible transa/transb
- Stream parameter threaded through backward_fc_layer methods

TF32 tensor cores enabled via CUBLAS_TF32_TENSOR_OP_MATH math mode
on the cuBLAS handle. CUBLAS_COMPUTE_32F (not FAST_TF32) used in
matmul descriptors for SM86 compatibility.

19/19 smoke tests pass (sequential). Ready for H100 graph_mega test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:11:51 +02:00
jgrusewski
d48cb89980 fix(critical): restore cuBLAS workspace after every cublasSetStream
cublasSetStream() resets workspace to the default pool, which uses
stream-ordered allocation — incompatible with CUDA Graph capture.
This caused cublasGemmEx TF32 to hang inside graph_mega on H100.

Fix:
- Store workspace raw pointer + size in CublasForward struct
- Call cublasSetWorkspace_v2 after every cublasSetStream (5 sites)
- Add 32MB workspace to CublasBackward (had none — used default pool)
- Increase forward workspace 4MB → 32MB (TF32 HMMA needs more)

Root cause from NVIDIA docs: "cublasSetStream() unconditionally
resets the cuBLAS library workspace back to the default workspace pool"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:07:24 +02:00
jgrusewski
1cd9d4039c perf: switch cublasSgemm → cublasGemmEx with CUBLAS_COMPUTE_32F_FAST_TF32
cublasSgemm was running on pure f32 CUDA cores (~60 TFLOPS on H100),
causing 15× slowdown vs the old bf16 tensor core path (77s vs 5s/epoch).

cublasGemmEx with CUBLAS_COMPUTE_32F_FAST_TF32 + CUBLAS_GEMM_DEFAULT_TENSOR_OP
uses TF32 tensor cores (~500 TFLOPS on H100) — 8× faster than pure f32.

The earlier NOT_SUPPORTED error was caused by bf16 weight pointers feeding
f32 GEMMs (stride mismatch), not API incompatibility. Now that all pointers
are f32, TF32 GemmEx works on both RTX 3050 (SM86) and H100 (SM90).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:53:15 +02:00
jgrusewski
30f3156369 diag: RECAPTURE_DIAG — per-buffer norms after graph recapture
Fires for 3 steps after any CUDA graph recapture. Reports:
- d_val/adv_norm: blended d_logits (C51 × alpha + MSE × (1-alpha))
- mse_val/adv_norm: MSE-only scratch buffers
- grad_norm: backward output grad_buf

Purpose: diagnose C51 grad_norm=0 on H100 at batch=16384 (epoch 2+).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:34:01 +02:00
jgrusewski
8329ca4187 fix(critical): eliminate ALL bf16 from training — pure f32/TF32 pipeline
The bf16 backward chain destroyed gradient precision at batch=16384 with
mean-reduced gradients (~6e-5). bf16's 8-bit mantissa couldn't represent
these values, producing zero weight gradients on H100.

This commit removes bf16 from the ENTIRE training pipeline:

Forward pass:
- cublasSgemm with CUBLAS_TF32_TENSOR_OP_MATH math mode (auto TF32 on Ampere+)
- f32 master weights used directly (no bf16 shadow for forward)
- All activation saves (h_s1, h_s2, h_v, h_b[0..3]) now f32
- States buffer f32 (pad_states_kernel outputs f32)
- Bias kernels: pure f32 (removed 5 bf16 variants)

Backward pass:
- Single cublasSgemm GEMM (was 6 variants: bf16, bf16_acc_f32, f32dy, etc.)
- f32 activations + f32 weights → no casts needed
- relu_mask_kernel reads f32 activation (was bf16)
- Removed: bf16 staging buffer, cast_dx_to_staging, all _f32dy duplicates

Backtest evaluator:
- All activation/state/weight buffers converted bf16→f32
- gather_states outputs f32 (kernel reads bf16 features, writes f32)
- Weight flattening: bf16→f32 conversion via kernel

Net: -1290 lines, +556 lines (734 lines removed)
Rule: bf16 is ONLY for stored weight tensors (spectral norm). Everything else is f32.
19/19 smoke tests pass. Gradient norms healthy (0.39-1.03).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 07:46:03 +02:00
jgrusewski
d25ade2087 feat: add f32 dY backward chain — no bf16 precision loss
New methods: backward_fc_layer_f32dy, backward_fc_layer_lda_f32dy,
launch_dw_only_f32dy, launch_dx_only_f32dy, launch_bias_grad_f32.
Uses gemmex_f32b_bf16a_acc_f32 (f32 dY + bf16 activations → f32 output).
backward_full now passes f32 dX directly between layers — no bf16 cast.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:30:40 +02:00
jgrusewski
a4fb15fce0 diag: per-step BUFFER_DIAG — d_value_logits, d_adv_logits, grad_buf norms
Logs L2 norms of critical buffers after forward+backward+aux, before Adam.
Fires on first 3 steps of each epoch only. Identifies exactly which buffer
goes zero on H100 — is it the loss gradient or the cuBLAS backward output?

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:30:57 +02:00
jgrusewski
d82519118b fix: align blend n_val/n_adv with allocation and cast dimensions
Blend used b*na but cast used b*pad32(na). Now both use the padded
size so scale/saxpy process the same element count as the cast.
Padding elements are zero — no behavioral change, just consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:07:04 +02:00
jgrusewski
49c8c1745e fix(critical): branch-major d_adv_logits layout + revert diagnostic hacks
Root cause of H100 gradient collapse: c51_grad, mse_grad, and cql_grad
kernels wrote d_adv_logits in sample-major interleaved layout [B, TBA]
but cuBLAS backward read it as branch-major [B*B0*NA | B*B1*NA | ...].
At batch=16384, the GEMM read cross-contaminated data from wrong branches,
producing zero weight gradients by symmetry cancellation at scale.

Fix: changed indexing in all 3 grad kernels to branch-major.
Reverted diagnostic hacks (ungraphed forward, forced diagnostics).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:05:46 +02:00
jgrusewski
9582fd7c73 fix(critical): cql_grad writes branch-major d_adv_logits layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:01:00 +02:00
jgrusewski
562a41483b fix(critical): mse_grad writes branch-major d_adv_logits layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:58:04 +02:00