Final piece of the SP13 Layer B chain. B1.1a flipped the aux head from K=1
MSE regression to K=2 softmax CE classification but aux_nb_label_buf was
zero-init — model was training on "all bars are class 0 (down)". B1.1b
lands the producer kernel that fills i32 -1/0/1 labels from the 30-bar
price trajectory, the replay direct-path 8th gather that carries those
labels into the trainer, and the experience collector hoist that ensures
bar_indices_pinned is always populated (producer + hindsight relabel both
consume it). Aux head finally trains on real classification signal.
Recovery commit: this completes a B1.1b agent dispatch that crashed
mid-edit. The implementer had landed ~95% of the cascade (kernel file,
build.rs, replay buffer signature + direct path, fused_training getter,
trainer accessor, both training_loop.rs callers, kernel field + cubin
loader on the experience collector) before being killed. The missing
pieces (experience collector launch + bar_indices_pinned hoist + 6
producer tests + audit doc) were completed manually post-crash and
verified end-to-end.
Three contracts (atomic single commit per feedback_no_partial_refactor):
1. NEW aux_sign_label_kernel.cu producer — pure per-thread O(1) map
reading targets[bar*6+2] (raw_close column) at bar and bar+lookahead,
writing -1 (skip if bar+lookahead >= total_bars), 0 (down/flat under
strict greater-than tie-break), or 1 (up). Replaces B0 alloc_zeros.
2. Replay direct-path 8th gather — set_trainer_buffers gains 8th arg
trainer_aux_sign_labels_ptr; direct branch in sample_proportional
adds gather_i32_scalar into the trainer ptr; fallback gather wrapped
in if !direct_to_trainer (avoids wasted DtoD). Direct-mode
GpuBatchPtrs return points aux_sign_labels_ptr at trainer ptr.
3. Experience collector bar_indices_pinned cpu-fill hoist — moved out
of if hindsight_fraction > 0.0 so producer + hindsight share it.
Files (9 total):
- crates/ml/src/cuda_pipeline/aux_sign_label_kernel.cu (NEW)
- crates/ml/build.rs (cubin registration)
- crates/ml/src/cuda_pipeline/gpu_experience_collector.rs (kernel
field + cubin loader + struct init + hoist + producer launch)
- crates/ml-dqn/src/gpu_replay_buffer.rs (8th arg + direct gather +
fallback skip + GpuBatchPtrs return)
- crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs (aux_nb_label_buf_ptr
accessor mirrors 6 existing trainer-buf accessors)
- crates/ml/src/trainers/dqn/fused_training.rs
(trainer_aux_sign_labels_buf_ptr getter)
- crates/ml/src/trainers/dqn/trainer/training_loop.rs (both
set_trainer_buffers callers updated)
- crates/ml/tests/sp13_layer_b_oracle_tests.rs (6 NEW producer tests)
- docs/dqn-wire-up-audit.md (B1.1b section)
Hard rules upheld:
- feedback_no_partial_refactor: every consumer of the 3 contracts
migrates atomically
- feedback_no_atomicadd: producer is pure map; no reductions
- feedback_cpu_is_read_only: producer GPU-only; only host work is
pre-existing bar_indices_pinned cpu-fill (hoisted unchanged)
- feedback_no_stubs: kernel output flows through real chain — ring
buffer → direct gather → aux_nb_label_buf → CE consumer
- feedback_no_legacy_aliases: 8-arg setter gets
#[allow(clippy::too_many_arguments)] not an alias shim
- feedback_no_htod_htoh_only_mapped_pinned: targets_buf and
bar_indices_pinned both pre-existing mapped-pinned
Build + test:
- cargo check --workspace clean (only pre-existing warnings)
- cargo check --workspace --tests clean
- 17 tests in sp13_layer_b_oracle_tests.rs:
- 2 CPU-only (fingerprint bump + HEALTH_DIAG snap stable) pass
- 15 GPU on RTX 3050 Ti pass (9 B1.1a + 6 new B1.1b producer):
aux_sign_label_monotone_up_all_ones
aux_sign_label_monotone_down_all_zeros
aux_sign_label_flat_all_zeros_strict_gt
aux_sign_label_last_30_bars_skip
aux_sign_label_boundary_first_valid_last_skip
aux_sign_label_multi_episode_per_episode_skip
Producer tests cover every edge case in the kernel:
- Monotone trajectories (label=1 / label=0 across all valid bars)
- Flat tie-break (strict greater-than means flat → 0)
- Skip sentinel for last lookahead bars
- First/last bar boundary (bar=0 valid, bar=L-1 skip)
- Multi-episode global skip semantics
Next: Smoke A — L40S 5-epoch validation of full SP13 stack
(P0a + P0b + B0 + B0.1 + B1.0 + B1.1a + B1.1b). Expected: aux head
trains on real K=2 softmax CE labels; aux_dir_acc_short_ema rises above
0.5 within first epoch (vs B1.1a degraded baseline at 0.5);
HEALTH_DIAG aux_b1_diag emits per-epoch with n_down/n_up/n_skip/mask_frac.
If aux_dir_acc_short_ema > 0.55 by epoch 5, B1.1b is validated and the
chain merges to main.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1054 lines
44 KiB
Rust
1054 lines
44 KiB
Rust
#![allow(unsafe_code)] // CUDA kernel launch + mapped-pinned memory.
|
||
|
||
//! SP13 Layer B Commit B1.1a (2026-05-05) GPU oracle tests.
|
||
//!
|
||
//! Validates the kernel rewrites that take the aux next-bar head from
|
||
//! K=1 MSE regression to K=2 softmax CE classification:
|
||
//! - `aux_next_bar_loss_reduce` (softmax + i32 labels → mean CE +
|
||
//! `B_valid` count; `-1` label = mask/skip)
|
||
//! - `aux_next_bar_backward` (softmax + i32 labels +
|
||
//! `B_valid` → per-sample dW/dh partials; masked rows zero)
|
||
//! - `aux_dir_acc_reduce_kernel` argmax over softmax-vs-i32-labels
|
||
//! (B1.1a-flipped — was f32 pred + f32 sign label)
|
||
//! - `aux_pred_to_isv_tanh_kernel` `mean(softmax[1] - softmax[0])`
|
||
//! into the SHARED ISV slot 375 (B1.1a-flipped — runtime tanh
|
||
//! retired; bound is structural)
|
||
//! - `LAYOUT_FINGERPRINT_CURRENT` regression — verifies the
|
||
//! fingerprint bumped from the pre-B1.1a value because the seed
|
||
//! names `PARAM_AUX_NB_W2 → PARAM_AUX_NB_W2_K2` (and `_B2`
|
||
//! equivalent) flipped.
|
||
//!
|
||
//! Per the B1.1a brief, this file covers tests 7-11 (CE loss
|
||
//! correctness), 12-13 (dir_acc B1.1a additions), 14-15 (isv_tanh
|
||
//! B1.1a additions), and 16+18 (fingerprint regression + HEALTH_DIAG
|
||
//! snap stability). Tests 1-6 (producer kernel) and 17 (end-to-end
|
||
//! round-trip) defer to B1.1b — they require the producer kernel +
|
||
//! replay direct path which aren't wired in B1.1a.
|
||
//!
|
||
//! Per `feedback_no_cpu_test_fallbacks.md`: GPU oracle only, no CPU
|
||
//! reference impl. Per `feedback_no_htod_htoh_only_mapped_pinned.md`:
|
||
//! every CPU↔GPU buffer is a `MappedF32Buffer` / `MappedI32Buffer`
|
||
//! (cuMemHostAlloc with DEVICEMAP|PORTABLE); zero `htod_copy`, zero
|
||
//! `dtoh_sync_copy`. Tests are NOT exempt from this rule.
|
||
//!
|
||
//! All GPU tests are `#[ignore = "requires GPU"]`-gated to match every
|
||
//! other GPU oracle test in this crate (sp4/sp5/sp11/sp12/sp13_phase0).
|
||
//! Run on a GPU host:
|
||
//!
|
||
//! SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 \
|
||
//! cargo test -p ml --test sp13_layer_b_oracle_tests --features cuda \
|
||
//! -- --ignored --nocapture
|
||
//!
|
||
//! The fingerprint regression test (test 16) is a pure-Rust const-eval
|
||
//! test — does NOT require a GPU and runs without `--ignored`.
|
||
|
||
#![cfg(feature = "cuda")]
|
||
|
||
use std::sync::Arc;
|
||
|
||
use cudarc::driver::{CudaContext, CudaFunction, CudaStream, LaunchConfig, PushKernelArg};
|
||
use ml::cuda_pipeline::mapped_pinned::{MappedF32Buffer, MappedI32Buffer};
|
||
|
||
// ── Test-only cubin handles ───────────────────────────────────────────────
|
||
//
|
||
// All three kernels live in the same `aux_heads_kernel.cubin` (loss +
|
||
// backward) plus the dedicated `aux_dir_acc_reduce_kernel.cubin` and
|
||
// `aux_pred_to_isv_tanh_kernel.cubin`. The cubins are emitted by
|
||
// `crates/ml/build.rs` into `OUT_DIR`; including them here mirrors the
|
||
// production loaders' include_bytes! pattern.
|
||
|
||
const SP13_AUX_HEADS_CUBIN: &[u8] =
|
||
include_bytes!(concat!(env!("OUT_DIR"), "/aux_heads_kernel.cubin"));
|
||
const SP13_AUX_DIR_ACC_REDUCE_CUBIN: &[u8] =
|
||
include_bytes!(concat!(env!("OUT_DIR"), "/aux_dir_acc_reduce_kernel.cubin"));
|
||
const SP13_AUX_PRED_TO_ISV_TANH_CUBIN: &[u8] =
|
||
include_bytes!(concat!(env!("OUT_DIR"), "/aux_pred_to_isv_tanh_kernel.cubin"));
|
||
/// SP13 Layer B Commit B1.1b: producer kernel cubin for the aux next-bar
|
||
/// sign label `[total] i32` ring column. Loaded by tests 1-6 below.
|
||
const SP13_AUX_SIGN_LABEL_CUBIN: &[u8] =
|
||
include_bytes!(concat!(env!("OUT_DIR"), "/aux_sign_label_kernel.cubin"));
|
||
|
||
// ── Stream + kernel loaders ──────────────────────────────────────────────
|
||
|
||
fn make_test_stream() -> Arc<CudaStream> {
|
||
let ctx = CudaContext::new(0).expect("CUDA context — is a GPU available?");
|
||
ctx.default_stream()
|
||
}
|
||
|
||
fn load_loss_reduce(stream: &Arc<CudaStream>) -> CudaFunction {
|
||
let module = stream
|
||
.context()
|
||
.load_cubin(SP13_AUX_HEADS_CUBIN.to_vec())
|
||
.expect("load aux_heads cubin");
|
||
module
|
||
.load_function("aux_next_bar_loss_reduce")
|
||
.expect("load aux_next_bar_loss_reduce function")
|
||
}
|
||
|
||
fn load_backward(stream: &Arc<CudaStream>) -> CudaFunction {
|
||
let module = stream
|
||
.context()
|
||
.load_cubin(SP13_AUX_HEADS_CUBIN.to_vec())
|
||
.expect("load aux_heads cubin");
|
||
module
|
||
.load_function("aux_next_bar_backward")
|
||
.expect("load aux_next_bar_backward function")
|
||
}
|
||
|
||
fn load_dir_acc(stream: &Arc<CudaStream>) -> CudaFunction {
|
||
let module = stream
|
||
.context()
|
||
.load_cubin(SP13_AUX_DIR_ACC_REDUCE_CUBIN.to_vec())
|
||
.expect("load aux_dir_acc_reduce cubin");
|
||
module
|
||
.load_function("aux_dir_acc_reduce_kernel")
|
||
.expect("load aux_dir_acc_reduce_kernel function")
|
||
}
|
||
|
||
fn load_isv_tanh(stream: &Arc<CudaStream>) -> CudaFunction {
|
||
let module = stream
|
||
.context()
|
||
.load_cubin(SP13_AUX_PRED_TO_ISV_TANH_CUBIN.to_vec())
|
||
.expect("load aux_pred_to_isv_tanh cubin");
|
||
module
|
||
.load_function("aux_pred_to_isv_tanh_kernel")
|
||
.expect("load aux_pred_to_isv_tanh_kernel function")
|
||
}
|
||
|
||
// ── CE loss reduce scaffold ──────────────────────────────────────────────
|
||
|
||
const AUX_BLOCK: u32 = 256;
|
||
|
||
/// Drive one launch of `aux_next_bar_loss_reduce` with the given softmax
|
||
/// tile + i32 labels. Returns `(mean_ce, b_valid)`.
|
||
fn run_loss_reduce(
|
||
stream: &Arc<CudaStream>,
|
||
f: &CudaFunction,
|
||
softmax: &[f32],
|
||
labels: &[i32],
|
||
k: i32,
|
||
) -> (f32, f32) {
|
||
let n = labels.len();
|
||
assert_eq!(
|
||
softmax.len(),
|
||
n * k as usize,
|
||
"softmax must be [B, K]; got {} for B={} K={}",
|
||
softmax.len(),
|
||
n,
|
||
k
|
||
);
|
||
|
||
let alloc_smx = (n * k as usize).max(1);
|
||
let alloc_lbl = n.max(1);
|
||
|
||
let softmax_buf = unsafe { MappedF32Buffer::new(alloc_smx) }
|
||
.expect("alloc softmax buffer (mapped-pinned)");
|
||
let labels_buf = unsafe { MappedI32Buffer::new(alloc_lbl) }
|
||
.expect("alloc labels buffer (mapped-pinned)");
|
||
if n > 0 {
|
||
softmax_buf.write_from_slice(softmax);
|
||
labels_buf.write_from_slice(labels);
|
||
}
|
||
|
||
let loss_buf = unsafe { MappedF32Buffer::new(1) }
|
||
.expect("alloc loss buffer (mapped-pinned)");
|
||
let valid_buf = unsafe { MappedF32Buffer::new(1) }
|
||
.expect("alloc valid_count buffer (mapped-pinned)");
|
||
|
||
let softmax_dev = softmax_buf.dev_ptr;
|
||
let labels_dev = labels_buf.dev_ptr;
|
||
let loss_dev = loss_buf.dev_ptr;
|
||
let valid_dev = valid_buf.dev_ptr;
|
||
let b_i32: i32 = n as i32;
|
||
|
||
unsafe {
|
||
stream
|
||
.launch_builder(f)
|
||
.arg(&softmax_dev)
|
||
.arg(&labels_dev)
|
||
.arg(&b_i32)
|
||
.arg(&k)
|
||
.arg(&loss_dev)
|
||
.arg(&valid_dev)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (AUX_BLOCK, 1, 1),
|
||
shared_mem_bytes: 2 * AUX_BLOCK * std::mem::size_of::<f32>() as u32,
|
||
})
|
||
.expect("launch aux_next_bar_loss_reduce");
|
||
}
|
||
stream.synchronize().expect("sync after aux_next_bar_loss_reduce");
|
||
|
||
(loss_buf.read_all()[0], valid_buf.read_all()[0])
|
||
}
|
||
|
||
// ── CE backward scaffold (test 10/11) ────────────────────────────────────
|
||
|
||
/// Drive one launch of `aux_next_bar_backward` with hand-crafted forward
|
||
/// state (uniform identity-ish weights so the test focuses on the
|
||
/// CE backward arithmetic, not the matmul). Returns the per-sample
|
||
/// `db2_partial [B, K]` slice (which equals `d_logits[b, kc]` directly
|
||
/// per the kernel's `db2_partial[b, kc] = sh_dlogits[kc]` write).
|
||
///
|
||
/// Hand-crafted forward state:
|
||
/// * h_s2[B, SH2] — zeros (so dh_s2 outputs zero; not under test)
|
||
/// * w1[H, SH2] — zeros
|
||
/// * w2[K, H] — zeros (so d_h_post = 0 → dW1/db1 = 0)
|
||
/// * hidden_post[B, H] — zeros (so ELU-bwd factor is 1.0)
|
||
///
|
||
/// With all-zero h_post and w2, the backward kernel's d_h_pre is zero,
|
||
/// so dW1, db1, dW2, dh_s2 are all zero. The interesting output is
|
||
/// `db2_partial[b, kc] = sh_dlogits[kc] = (softmax[b,kc] - one_hot)/B_valid`
|
||
/// for valid rows, zero for masked rows.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn run_backward_db2_only(
|
||
stream: &Arc<CudaStream>,
|
||
f: &CudaFunction,
|
||
softmax: &[f32],
|
||
labels: &[i32],
|
||
valid_count: f32,
|
||
b: usize,
|
||
sh2: usize,
|
||
k: i32,
|
||
) -> Vec<f32> {
|
||
let h: usize = 32; // AUX_HIDDEN_DIM
|
||
let kk = k as usize;
|
||
|
||
// Allocate all forward + partial buffers (mapped-pinned for parity
|
||
// with the production launcher; the kernel only reads dev_ptr).
|
||
let h_s2_buf = unsafe { MappedF32Buffer::new(b * sh2) }.unwrap();
|
||
let w1_buf = unsafe { MappedF32Buffer::new(h * sh2) }.unwrap();
|
||
let w2_buf = unsafe { MappedF32Buffer::new(kk * h) }.unwrap();
|
||
let hidden_post_buf = unsafe { MappedF32Buffer::new(b * h) }.unwrap();
|
||
let softmax_buf = unsafe { MappedF32Buffer::new(b * kk) }.unwrap();
|
||
let labels_buf = unsafe { MappedI32Buffer::new(b) }.unwrap();
|
||
let valid_buf = unsafe { MappedF32Buffer::new(1) }.unwrap();
|
||
|
||
let dw1_buf = unsafe { MappedF32Buffer::new(b * h * sh2) }.unwrap();
|
||
let db1_buf = unsafe { MappedF32Buffer::new(b * h) }.unwrap();
|
||
let dw2_buf = unsafe { MappedF32Buffer::new(b * kk * h) }.unwrap();
|
||
let db2_buf = unsafe { MappedF32Buffer::new(b * kk) }.unwrap();
|
||
let dh_s2_buf = unsafe { MappedF32Buffer::new(b * sh2) }.unwrap();
|
||
|
||
softmax_buf.write_from_slice(softmax);
|
||
labels_buf.write_from_slice(labels);
|
||
valid_buf.write_from_slice(&[valid_count]);
|
||
|
||
let h_s2_dev = h_s2_buf.dev_ptr;
|
||
let w1_dev = w1_buf.dev_ptr;
|
||
let w2_dev = w2_buf.dev_ptr;
|
||
let hpost_dev = hidden_post_buf.dev_ptr;
|
||
let softmax_dev = softmax_buf.dev_ptr;
|
||
let labels_dev = labels_buf.dev_ptr;
|
||
let valid_dev = valid_buf.dev_ptr;
|
||
let dw1_dev = dw1_buf.dev_ptr;
|
||
let db1_dev = db1_buf.dev_ptr;
|
||
let dw2_dev = dw2_buf.dev_ptr;
|
||
let db2_dev = db2_buf.dev_ptr;
|
||
let dhs2_dev = dh_s2_buf.dev_ptr;
|
||
let b_i32: i32 = b as i32;
|
||
let sh2_i32: i32 = sh2 as i32;
|
||
|
||
let smem_bytes = ((2 * h as u32) + (k as u32)) * std::mem::size_of::<f32>() as u32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(f)
|
||
.arg(&h_s2_dev)
|
||
.arg(&w1_dev)
|
||
.arg(&w2_dev)
|
||
.arg(&hpost_dev)
|
||
.arg(&softmax_dev)
|
||
.arg(&labels_dev)
|
||
.arg(&valid_dev)
|
||
.arg(&b_i32)
|
||
.arg(&sh2_i32)
|
||
.arg(&k)
|
||
.arg(&dw1_dev)
|
||
.arg(&db1_dev)
|
||
.arg(&dw2_dev)
|
||
.arg(&db2_dev)
|
||
.arg(&dhs2_dev)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (b as u32, 1, 1),
|
||
block_dim: (AUX_BLOCK, 1, 1),
|
||
shared_mem_bytes: smem_bytes,
|
||
})
|
||
.expect("launch aux_next_bar_backward");
|
||
}
|
||
stream.synchronize().expect("sync after aux_next_bar_backward");
|
||
|
||
db2_buf.read_all()
|
||
}
|
||
|
||
// ── dir_acc + isv_tanh helpers (mirror sp13_phase0 launchers) ───────────
|
||
|
||
fn run_dir_acc(
|
||
stream: &Arc<CudaStream>,
|
||
f: &CudaFunction,
|
||
softmax: &[f32],
|
||
labels: &[i32],
|
||
k: i32,
|
||
) -> [f32; 6] {
|
||
let n = labels.len();
|
||
let alloc_smx = (n * k as usize).max(1);
|
||
let alloc_lbl = n.max(1);
|
||
let softmax_buf = unsafe { MappedF32Buffer::new(alloc_smx) }.unwrap();
|
||
let labels_buf = unsafe { MappedI32Buffer::new(alloc_lbl) }.unwrap();
|
||
if n > 0 {
|
||
softmax_buf.write_from_slice(softmax);
|
||
labels_buf.write_from_slice(labels);
|
||
}
|
||
let out_buf = unsafe { MappedF32Buffer::new(6) }.unwrap();
|
||
let bdim: u32 = 256;
|
||
let b_i32: i32 = n as i32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(f)
|
||
.arg(&softmax_buf.dev_ptr)
|
||
.arg(&labels_buf.dev_ptr)
|
||
.arg(&b_i32)
|
||
.arg(&k)
|
||
.arg(&out_buf.dev_ptr)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (bdim, 1, 1),
|
||
shared_mem_bytes: 6 * bdim * std::mem::size_of::<i32>() as u32,
|
||
})
|
||
.expect("launch aux_dir_acc_reduce_kernel");
|
||
}
|
||
stream.synchronize().expect("sync after dir_acc");
|
||
let v = out_buf.read_all();
|
||
[v[0], v[1], v[2], v[3], v[4], v[5]]
|
||
}
|
||
|
||
fn run_isv_tanh(
|
||
stream: &Arc<CudaStream>,
|
||
f: &CudaFunction,
|
||
softmax: &[f32],
|
||
k: i32,
|
||
) -> f32 {
|
||
let n = if k > 0 { softmax.len() / k as usize } else { 0 };
|
||
let alloc = (n * k as usize).max(1);
|
||
let smx_buf = unsafe { MappedF32Buffer::new(alloc) }.unwrap();
|
||
if !softmax.is_empty() {
|
||
smx_buf.write_from_slice(softmax);
|
||
}
|
||
let isv_buf = unsafe { MappedF32Buffer::new(1) }.unwrap();
|
||
let bdim: u32 = 256;
|
||
let b_i32: i32 = n as i32;
|
||
let isv_off: i32 = 0;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(f)
|
||
.arg(&smx_buf.dev_ptr)
|
||
.arg(&b_i32)
|
||
.arg(&k)
|
||
.arg(&isv_off)
|
||
.arg(&isv_buf.dev_ptr)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (bdim, 1, 1),
|
||
shared_mem_bytes: bdim * std::mem::size_of::<f32>() as u32,
|
||
})
|
||
.expect("launch aux_pred_to_isv_tanh_kernel");
|
||
}
|
||
stream.synchronize().expect("sync after isv_tanh");
|
||
isv_buf.read_all()[0]
|
||
}
|
||
|
||
// ─── Test 7: CE loss single-row hand-crafted ──────────────────────────────
|
||
|
||
/// Single sample, K=2. softmax = [0.25, 0.75], label = 1 (up).
|
||
/// Expected loss = -log(0.75) ≈ 0.2877. valid_count = 1.
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn ce_loss_single_row_handcrafted() {
|
||
let stream = make_test_stream();
|
||
let f = load_loss_reduce(&stream);
|
||
let softmax = vec![0.25_f32, 0.75];
|
||
let labels = vec![1_i32];
|
||
let (loss, valid) = run_loss_reduce(&stream, &f, &softmax, &labels, 2);
|
||
let expected = (-0.75_f32.ln()) / 1.0;
|
||
assert!(
|
||
(loss - expected).abs() < 1e-5,
|
||
"expected loss = {expected}, got {loss}"
|
||
);
|
||
assert!(
|
||
(valid - 1.0).abs() < 1e-6,
|
||
"expected valid_count = 1.0, got {valid}"
|
||
);
|
||
}
|
||
|
||
// ─── Test 8: CE loss batch of 4 mixed ─────────────────────────────────────
|
||
|
||
/// 4 rows: 3 valid + 1 mask.
|
||
/// rows 0,1: softmax = [0.2, 0.8], label = 1 → CE = -log(0.8) each.
|
||
/// row 2: softmax = [0.9, 0.1], label = 0 → CE = -log(0.9).
|
||
/// row 3: softmax = [0.5, 0.5], label = -1 → masked, contributes 0.
|
||
/// Expected: B_valid = 3; mean = (2*-ln(0.8) + -ln(0.9)) / 3.
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn ce_loss_batch_mixed() {
|
||
let stream = make_test_stream();
|
||
let f = load_loss_reduce(&stream);
|
||
let softmax = vec![
|
||
0.2_f32, 0.8,
|
||
0.2, 0.8,
|
||
0.9, 0.1,
|
||
0.5, 0.5,
|
||
];
|
||
let labels = vec![1_i32, 1, 0, -1];
|
||
let (loss, valid) = run_loss_reduce(&stream, &f, &softmax, &labels, 2);
|
||
let numer = 2.0 * (-0.8_f32.ln()) + (-0.9_f32.ln());
|
||
let expected = numer / 3.0;
|
||
assert!(
|
||
(loss - expected).abs() < 1e-5,
|
||
"expected mean CE = {expected}, got {loss}"
|
||
);
|
||
assert!(
|
||
(valid - 3.0).abs() < 1e-6,
|
||
"expected valid_count = 3.0, got {valid}"
|
||
);
|
||
}
|
||
|
||
// ─── Test 9: CE loss all-skip batch ───────────────────────────────────────
|
||
|
||
/// 4 rows all masked → B_valid = 0; loss = 0/max(0,1) = 0 (no NaN).
|
||
/// `valid_count_out` reports the actual count (0.0).
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn ce_loss_all_skip_returns_zero_no_nan() {
|
||
let stream = make_test_stream();
|
||
let f = load_loss_reduce(&stream);
|
||
let softmax = vec![0.5_f32; 8]; // 4 rows, all balanced
|
||
let labels = vec![-1_i32; 4];
|
||
let (loss, valid) = run_loss_reduce(&stream, &f, &softmax, &labels, 2);
|
||
assert!(
|
||
loss.is_finite(),
|
||
"expected finite loss on all-mask batch, got {loss}"
|
||
);
|
||
assert!(
|
||
loss.abs() < 1e-6,
|
||
"expected loss = 0.0 on all-mask batch, got {loss}"
|
||
);
|
||
assert!(
|
||
valid.abs() < 1e-6,
|
||
"expected valid_count = 0.0 on all-mask batch, got {valid}"
|
||
);
|
||
}
|
||
|
||
// ─── Test 10: CE backward single-row ──────────────────────────────────────
|
||
|
||
/// Single row, K=2. softmax = [0.3, 0.7], label = 1 (up).
|
||
/// Expected `db2_partial[0, :] = (softmax - one_hot(1)) / B_valid`
|
||
/// = ([0.3, 0.7] - [0, 1]) / 1
|
||
/// = [0.3, -0.3].
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn ce_backward_single_row_handcrafted() {
|
||
let stream = make_test_stream();
|
||
let f = load_backward(&stream);
|
||
let softmax = vec![0.3_f32, 0.7];
|
||
let labels = vec![1_i32];
|
||
let valid = 1.0_f32;
|
||
let db2 = run_backward_db2_only(&stream, &f, &softmax, &labels, valid, 1, 4, 2);
|
||
assert!(
|
||
(db2[0] - 0.3).abs() < 1e-5,
|
||
"expected db2[0] = 0.3, got {}",
|
||
db2[0]
|
||
);
|
||
assert!(
|
||
(db2[1] - (-0.3)).abs() < 1e-5,
|
||
"expected db2[1] = -0.3, got {}",
|
||
db2[1]
|
||
);
|
||
}
|
||
|
||
// ─── Test 11: CE backward batch of 4 mixed ────────────────────────────────
|
||
|
||
/// 4 rows: 3 valid + 1 mask. Batch_valid = 3.
|
||
/// row 0: softmax = [0.2, 0.8], label = 1 → d_logits = [0.2, -0.2] / 3
|
||
/// row 1: softmax = [0.9, 0.1], label = 0 → d_logits = [-0.1, 0.1] / 3
|
||
/// row 2: softmax = [0.4, 0.6], label = 1 → d_logits = [0.4, -0.4] / 3
|
||
/// row 3: softmax = [0.5, 0.5], label = -1 → masked → zero across K.
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn ce_backward_batch_mixed() {
|
||
let stream = make_test_stream();
|
||
let f = load_backward(&stream);
|
||
let softmax = vec![
|
||
0.2_f32, 0.8,
|
||
0.9, 0.1,
|
||
0.4, 0.6,
|
||
0.5, 0.5,
|
||
];
|
||
let labels = vec![1_i32, 0, 1, -1];
|
||
let valid = 3.0_f32;
|
||
let db2 = run_backward_db2_only(&stream, &f, &softmax, &labels, valid, 4, 4, 2);
|
||
// Row 0
|
||
assert!((db2[0] - 0.2 / 3.0).abs() < 1e-5, "db2[0,0] mismatch: {}", db2[0]);
|
||
assert!((db2[1] - (-0.2) / 3.0).abs() < 1e-5, "db2[0,1] mismatch: {}", db2[1]);
|
||
// Row 1
|
||
assert!((db2[2] - (-0.1) / 3.0).abs() < 1e-5, "db2[1,0] mismatch: {}", db2[2]);
|
||
assert!((db2[3] - 0.1 / 3.0).abs() < 1e-5, "db2[1,1] mismatch: {}", db2[3]);
|
||
// Row 2
|
||
assert!((db2[4] - 0.4 / 3.0).abs() < 1e-5, "db2[2,0] mismatch: {}", db2[4]);
|
||
assert!((db2[5] - (-0.4) / 3.0).abs() < 1e-5, "db2[2,1] mismatch: {}", db2[5]);
|
||
// Row 3 (masked) → zero
|
||
assert!(db2[6].abs() < 1e-6, "db2[3,0] expected 0 (masked), got {}", db2[6]);
|
||
assert!(db2[7].abs() < 1e-6, "db2[3,1] expected 0 (masked), got {}", db2[7]);
|
||
}
|
||
|
||
// ─── Test 12: dir_acc argmax correctness on hand-crafted ─────────────────
|
||
|
||
/// 4 rows: argmax-vs-label cases:
|
||
/// row 0: softmax [0.1, 0.9], label 1 → correct
|
||
/// row 1: softmax [0.1, 0.9], label 1 → correct
|
||
/// row 2: softmax [0.1, 0.9], label 0 → wrong
|
||
/// row 3: softmax [0.5, 0.5], label -1 → masked
|
||
/// Valid bars = 3 (2 correct + 1 wrong). dir_acc = 2/3.
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn dir_acc_handcrafted_argmax() {
|
||
let stream = make_test_stream();
|
||
let f = load_dir_acc(&stream);
|
||
let softmax = vec![
|
||
0.1_f32, 0.9,
|
||
0.1, 0.9,
|
||
0.1, 0.9,
|
||
0.5, 0.5,
|
||
];
|
||
let labels = vec![1_i32, 1, 0, -1];
|
||
let out = run_dir_acc(&stream, &f, &softmax, &labels, 2);
|
||
assert!(
|
||
(out[0] - (2.0 / 3.0)).abs() < 1e-5,
|
||
"expected dir_acc = 2/3, got {}",
|
||
out[0]
|
||
);
|
||
assert!((out[3] - 1.0).abs() < 1e-6, "n_down expected 1, got {}", out[3]);
|
||
assert!((out[4] - 2.0).abs() < 1e-6, "n_up expected 2, got {}", out[4]);
|
||
assert!((out[5] - 1.0).abs() < 1e-6, "n_skip expected 1, got {}", out[5]);
|
||
}
|
||
|
||
// ─── Test 13: dir_acc all-skip (NaN-safe) ────────────────────────────────
|
||
|
||
/// All-mask batch → sentinel 0.5 in slots 0..3, raw counts 0/0/B in
|
||
/// slots 3..6. NO NaN anywhere.
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn dir_acc_all_skip_nan_safe() {
|
||
let stream = make_test_stream();
|
||
let f = load_dir_acc(&stream);
|
||
let softmax = vec![0.5_f32; 8];
|
||
let labels = vec![-1_i32; 4];
|
||
let out = run_dir_acc(&stream, &f, &softmax, &labels, 2);
|
||
for (i, v) in out.iter().enumerate() {
|
||
assert!(v.is_finite(), "slot {i} non-finite: {v}");
|
||
}
|
||
assert!(
|
||
(out[0] - 0.5).abs() < 1e-6,
|
||
"expected dir_acc sentinel 0.5, got {}",
|
||
out[0]
|
||
);
|
||
assert!((out[3]).abs() < 1e-6, "n_down expected 0, got {}", out[3]);
|
||
assert!((out[4]).abs() < 1e-6, "n_up expected 0, got {}", out[4]);
|
||
assert!((out[5] - 4.0).abs() < 1e-6, "n_skip expected 4, got {}", out[5]);
|
||
}
|
||
|
||
// ─── Test 14: aux_pred_to_isv_tanh bounded in [-1, +1] (3 fuzz) ──────────
|
||
|
||
/// Three randomly-shaped softmax tiles; each must produce mean diff
|
||
/// inside `[-1, +1]` (structural bound). Uses a simple PRNG so the
|
||
/// test is deterministic across runs.
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn isv_tanh_bounded_fuzz() {
|
||
let stream = make_test_stream();
|
||
let f = load_isv_tanh(&stream);
|
||
|
||
// Three deterministic fuzz inputs: skewed up, skewed down, mixed.
|
||
let cases: [Vec<f32>; 3] = [
|
||
// 8 rows skewed up: each row ≈ [0.2, 0.8].
|
||
vec![
|
||
0.2, 0.8, 0.15, 0.85, 0.25, 0.75, 0.3, 0.7,
|
||
0.18, 0.82, 0.22, 0.78, 0.21, 0.79, 0.27, 0.73,
|
||
],
|
||
// 8 rows skewed down: each row ≈ [0.8, 0.2].
|
||
vec![
|
||
0.8, 0.2, 0.85, 0.15, 0.75, 0.25, 0.7, 0.3,
|
||
0.82, 0.18, 0.78, 0.22, 0.79, 0.21, 0.73, 0.27,
|
||
],
|
||
// 4 mixed rows.
|
||
vec![0.1, 0.9, 0.9, 0.1, 0.4, 0.6, 0.6, 0.4],
|
||
];
|
||
|
||
for (i, softmax) in cases.iter().enumerate() {
|
||
let out = run_isv_tanh(&stream, &f, softmax, 2);
|
||
assert!(
|
||
out >= -1.0 && out <= 1.0,
|
||
"case {i}: out {out} outside [-1, +1] structural bound"
|
||
);
|
||
assert!(out.is_finite(), "case {i}: non-finite out {out}");
|
||
}
|
||
}
|
||
|
||
// ─── Test 15: aux_pred_to_isv_tanh mean correctness on hand-crafted ──────
|
||
|
||
/// 3 rows: [0.2, 0.8], [0.6, 0.4], [0.5, 0.5].
|
||
/// Diffs: +0.6, -0.2, 0.0
|
||
/// Mean: (0.6 - 0.2 + 0.0) / 3 = 0.4 / 3 ≈ 0.1333.
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn isv_tanh_mean_handcrafted() {
|
||
let stream = make_test_stream();
|
||
let f = load_isv_tanh(&stream);
|
||
let softmax = vec![0.2_f32, 0.8, 0.6, 0.4, 0.5, 0.5];
|
||
let out = run_isv_tanh(&stream, &f, &softmax, 2);
|
||
let expected = (0.6 - 0.2 + 0.0) / 3.0;
|
||
assert!(
|
||
(out - expected).abs() < 1e-5,
|
||
"expected mean diff = {expected}, got {out}"
|
||
);
|
||
}
|
||
|
||
// ─── Test 16: Fingerprint regression (CPU-only; no GPU required) ─────────
|
||
|
||
/// Inline FNV-1a 64-bit hash for the regression test. Mirrors the
|
||
/// `const fn fnv1a_64` in `gpu_dqn_trainer.rs` so the test computes
|
||
/// the same hash the production fingerprint uses.
|
||
const fn fnv1a_64(bytes: &[u8]) -> u64 {
|
||
const OFFSET: u64 = 0xcbf29ce484222325;
|
||
const PRIME: u64 = 0x00000100000001b3;
|
||
let mut h: u64 = OFFSET;
|
||
let mut i = 0;
|
||
while i < bytes.len() {
|
||
h ^= bytes[i] as u64;
|
||
h = h.wrapping_mul(PRIME);
|
||
i += 1;
|
||
}
|
||
h
|
||
}
|
||
|
||
/// Pre-B1.1a seed — the exact same seed string as the post-B1.0 layout
|
||
/// fingerprint EXCEPT the next-bar tensor names are
|
||
/// `PARAM_AUX_NB_W2` / `PARAM_AUX_NB_B2` (regression-mode names) rather
|
||
/// than the B1.1a `_K2` suffix variants. Hashing this constant gives
|
||
/// the pre-B1.1a fingerprint that the bump must differ from. Any
|
||
/// silent revert of the rename would make this test fail.
|
||
///
|
||
/// Layout: this is the verbatim seed string from `gpu_dqn_trainer.rs`'s
|
||
/// `layout_fingerprint_seed()` at HEAD `75e94858c` (B1.0). The only
|
||
/// section that changed in B1.1a is the `PARAM_AUX_NB_W2/B2 →
|
||
/// PARAM_AUX_NB_W2_K2/B2_K2` renames.
|
||
const PRE_B1_1A_SEED: &[u8] = b"SLOT_0_Q_DRIFT=0;\
|
||
SLOT_1_GRAD_NORM_EMA=1;\
|
||
SLOT_2_TD_ERR_EMA=2;\
|
||
SLOT_3_ENS_VAR_EMA=3;\
|
||
SLOT_4_ENS_VAR_VEL=4;\
|
||
SLOT_5_REWARD_EMA=5;\
|
||
SLOT_6_ATOM_UTIL_EMA=6;\
|
||
SLOT_7_LOSS_EMA=7;\
|
||
SLOT_8_ADX_EMA=8;\
|
||
SLOT_9_REGIME_DISAGREE=9;\
|
||
SLOT_10_REGIME_VEL_EMA=10;\
|
||
SLOT_11_REGIME_STABILITY=11;\
|
||
LEARNING_HEALTH=12;\
|
||
Q_MAG_MEAN_QUARTER=13;Q_MAG_MEAN_HALF=14;Q_MAG_MEAN_FULL=15;Q_ABS_REF=16;\
|
||
Q_DIR_MEAN_SHORT=17;Q_DIR_MEAN_HOLD=18;Q_DIR_MEAN_LONG=19;Q_DIR_MEAN_FLAT=20;Q_DIR_ABS_REF=21;\
|
||
SHARPE_EMA=22;\
|
||
V_CENTER_DIR=23;V_HALF_DIR=24;V_CENTER_MAG=25;V_HALF_MAG=26;\
|
||
V_CENTER_ORD=27;V_HALF_ORD=28;V_CENTER_URG=29;V_HALF_URG=30;\
|
||
GRAD_NORM_TARGET_DIR=31;GRAD_NORM_TARGET_MAG=32;GRAD_NORM_TARGET_ORD=33;GRAD_NORM_TARGET_URG=34;\
|
||
GRAD_SCALE_LIMIT=35;IQL_BRANCH_SCALE_FLOOR=36;\
|
||
EPOCH_IDX=39;TOTAL_EPOCHS=40;\
|
||
EPSILON_EFF=41;TAU_EFF=42;\
|
||
GAMMA_DIR_EFF=43;GAMMA_MAG_EFF=44;GAMMA_ORD_EFF=45;GAMMA_URG_EFF=46;\
|
||
KELLY_CAP_EFF=47;CQL_ALPHA=48;PLAN_THRESHOLD=49;\
|
||
Q_P05_DIR=50;Q_P05_MAG=51;Q_P05_ORD=52;Q_P05_URG=53;\
|
||
Q_P95_DIR=54;Q_P95_MAG=55;Q_P95_ORD=56;Q_P95_URG=57;\
|
||
TLOB_REGIME_FOCUS_EMA=60;\
|
||
REWARD_POPART_EMA=63;REWARD_CF_EMA=64;REWARD_TRAIL_EMA=65;\
|
||
REWARD_MICRO_EMA=66;REWARD_OPP_COST_EMA=67;REWARD_BONUS_EMA=68;\
|
||
TRADE_ATTEMPT_RATE_EMA=71;TRADE_TARGET_RATE=72;\
|
||
READINESS_EMA=75;\
|
||
STATE_KL_EMA=78;STATE_KL_AMP=79;\
|
||
SEED_STEPS_TARGET=82;SEED_STEPS_DONE=83;SEED_FRAC_EMA=84;\
|
||
VSN_MAG_EMA=87;VSN_DIR_EMA=88;MAMBA2_RETENTION_EMA=89;\
|
||
TARGET_DRIFT_MAG_EMA=92;TARGET_DRIFT_DIR_EMA=93;\
|
||
H_S2_RMS_EMA=96;\
|
||
IQN_Q_P05_EMA=99;IQN_Q_P25_EMA=100;IQN_Q_P75_EMA=101;IQN_Q_P95_EMA=102;\
|
||
VSN_MASK_GROUP_0_EMA=105;VSN_MASK_GROUP_1_EMA=106;VSN_MASK_GROUP_2_EMA=107;\
|
||
VSN_MASK_GROUP_3_EMA=108;VSN_MASK_GROUP_4_EMA=109;VSN_MASK_GROUP_5_EMA=110;\
|
||
AUX_NEXT_BAR_MSE_EMA=113;AUX_REGIME_CE_EMA=114;\
|
||
ISV_LAYOUT_FINGERPRINT_LO=115;ISV_LAYOUT_FINGERPRINT_HI=116;\
|
||
MOE_EXPERT_UTIL_EMA_BASE=118;MOE_EXPERT_UTIL_EMA_COUNT=8;\
|
||
MOE_GATE_ENTROPY_EMA=126;\
|
||
MOE_LAMBDA_EFF=128;\
|
||
Q_DRIFT_RATE=129;\
|
||
FOLD_WARMUP_FACTOR=130;\
|
||
TARGET_Q_BOUND=131;\
|
||
ATOM_POS_BOUND_BRANCH_0=132;ATOM_POS_BOUND_BRANCH_1=133;ATOM_POS_BOUND_BRANCH_2=134;ATOM_POS_BOUND_BRANCH_3=135;\
|
||
WEIGHT_BOUND_GROUP_0=136;WEIGHT_BOUND_GROUP_1=137;WEIGHT_BOUND_GROUP_2=138;WEIGHT_BOUND_GROUP_3=139;\
|
||
WEIGHT_BOUND_GROUP_4=140;WEIGHT_BOUND_GROUP_5=141;WEIGHT_BOUND_GROUP_6=142;WEIGHT_BOUND_GROUP_7=143;\
|
||
ADAM_M_BOUND_GROUP_0=144;ADAM_M_BOUND_GROUP_1=145;ADAM_M_BOUND_GROUP_2=146;ADAM_M_BOUND_GROUP_3=147;\
|
||
ADAM_M_BOUND_GROUP_4=148;ADAM_M_BOUND_GROUP_5=149;ADAM_M_BOUND_GROUP_6=150;ADAM_M_BOUND_GROUP_7=151;\
|
||
ADAM_V_BOUND_GROUP_0=152;ADAM_V_BOUND_GROUP_1=153;ADAM_V_BOUND_GROUP_2=154;ADAM_V_BOUND_GROUP_3=155;\
|
||
ADAM_V_BOUND_GROUP_4=156;ADAM_V_BOUND_GROUP_5=157;ADAM_V_BOUND_GROUP_6=158;ADAM_V_BOUND_GROUP_7=159;\
|
||
WD_RATE_GROUP_0=160;WD_RATE_GROUP_1=161;WD_RATE_GROUP_2=162;WD_RATE_GROUP_3=163;\
|
||
WD_RATE_GROUP_4=164;WD_RATE_GROUP_5=165;WD_RATE_GROUP_6=166;WD_RATE_GROUP_7=167;\
|
||
GRAD_CLIP_BOUND=168;H_S2_BOUND=169;L1_LAMBDA_TRUNK=170;\
|
||
BW_D_H_S2_BOUND=171;Q_DIR_GRAD_BOUND=172;\
|
||
SP5_BASE=174;ATOM_V_CENTER_BASE=174;ATOM_V_HALF_BASE=178;ATOM_HEADROOM_BASE=182;\
|
||
ATOM_CLIP_RATE_BASE=186;BUDGET_C51_BASE=190;BUDGET_IQN_BASE=194;BUDGET_CQL_BASE=198;\
|
||
BUDGET_ENS_BASE=202;FLATNESS_BASE=206;NOISY_SIGMA_BASE=210;SIGMA_FRACTION_BASE=214;\
|
||
BRANCH_ENTROPY_BASE=218;Q_VAR_PER_BRANCH_BASE=222;ADAM_BETA1_BASE=226;ADAM_BETA2_BASE=234;\
|
||
ADAM_EPS_BASE=242;IQN_TAU_BASE=250;TRAIL_DIST_PER_DIR_BASE=270;ATOM_NUM_ATOMS_BASE=274;\
|
||
KELLY_F_SMOOTH=280;CONVICTION_SMOOTH=281;TRADE_VAR_SMOOTH=282;\
|
||
KELLY_SAMPLE_COUNT=283;WIN_RATE_SMOOTH=284;LOSS_RATE_SMOOTH=285;\
|
||
PNL_TOTAL=286;PNL_MEAN=287;PNL_VAR=288;PNL_MAX_DD=289;\
|
||
HEALTH_SCORE=290;Q_GAP_NORM=291;Q_VAR_NORM=292;GRAD_NORM_NORM=293;\
|
||
TRAINING_SHARPE_EMA=294;MAX_DD_EMA=295;LOW_DD_RATIO=296;\
|
||
LB_DIFF_VAR_CQL_BASE=297;LB_SAMPLE_VAR_CQL_BASE=301;\
|
||
LB_DIFF_VAR_C51_BASE=305;LB_SAMPLE_VAR_C51_BASE=309;\
|
||
LB_CQL_ACTIVE_BASE=313;LB_C51_ACTIVE_BASE=317;\
|
||
TRAIN_ACTIVE_FRAC_INDEX=321;\
|
||
LB_MAX_BUDGET_CQL_BASE=322;LB_MAX_BUDGET_C51_BASE=326;\
|
||
KELLY_WARMUP_FLOOR=330;Q_VAR_MAG_EMA=331;\
|
||
INTENT_EVAL_DIVERGENCE=332;KELLY_SAMPLE_COUNT_TARGET=333;\
|
||
KELLY_DIVERGENCE_TARGET=334;KELLY_TEMPORAL_TARGET=335;\
|
||
EVAL_DIST_Q=336;EVAL_DIST_H=337;EVAL_DIST_F=338;\
|
||
EVAL_THOMPSON_TEMP=339;\
|
||
REWARD_POPART_WEIGHT=340;REWARD_CF_WEIGHT=341;REWARD_TRAIL_WEIGHT=342;\
|
||
REWARD_MICRO_WEIGHT=343;REWARD_OPP_COST_WEIGHT=344;REWARD_BONUS_WEIGHT=345;\
|
||
CURIOSITY_PRESSURE=346;SABOTEUR_INTENSITY_MULT=347;\
|
||
REWARD_WEIGHT_FLOOR=348;CURIOSITY_BOUND=349;\
|
||
VAL_SHARPE_DELTA_EMA=350;VAL_SHARPE_VAR_EMA=351;\
|
||
REWARD_COMPONENT_MAG_RATIO_BASE=352;\
|
||
SABOTEUR_ENGAGEMENT_RATE=358;PNL_REWARD_MAGNITUDE_EMA=359;\
|
||
POPART_COMPONENT_MAG_EMA=360;\
|
||
REWARD_COMPONENT_VAR_EMA_BASE=361;\
|
||
TARGET_DIR_ACC=372;AUX_DIR_ACC_SHORT_EMA=373;AUX_DIR_ACC_LONG_EMA=374;\
|
||
AUX_DIR_PREDICTION=375;DIR_SKILL_BONUS_ALPHA=376;DIR_SKILL_BONUS_BETA=377;\
|
||
LUCK_WIN_DISCOUNT=378;SKILL_BONUS_CAP_RATIO=379;\
|
||
HOLD_COST=380;HOLD_RATE_TARGET=381;HOLD_RATE_OBSERVED_EMA=382;\
|
||
ISV_TOTAL_DIM=383;\
|
||
PARAM_W_A_H_S1=0;PARAM_B_A_H_S1=1;PARAM_W_B_H_S1=2;PARAM_B_B_H_S1=3;\
|
||
PARAM_W_RESIDUAL_H_S1=4;PARAM_GAMMA_H_S1=5;PARAM_BETA_H_S1=6;\
|
||
PARAM_W_A_H_S2=7;PARAM_B_A_H_S2=8;PARAM_W_B_H_S2=9;PARAM_B_B_H_S2=10;\
|
||
PARAM_GAMMA_H_S2=11;PARAM_BETA_H_S2=12;\
|
||
PARAM_W_V1=13;PARAM_B_V1=14;PARAM_W_V2=15;PARAM_B_V2=16;\
|
||
PARAM_W_B0FC=17;PARAM_B_B0FC=18;PARAM_W_B0OUT=19;PARAM_B_B0OUT=20;\
|
||
PARAM_W_B1FC=21;PARAM_B_B1FC=22;PARAM_W_B1OUT=23;PARAM_B_B1OUT=24;\
|
||
PARAM_W_B2FC=25;PARAM_B_B2FC=26;PARAM_W_B2OUT=27;PARAM_B_B2OUT=28;\
|
||
PARAM_W_B3FC=29;PARAM_B_B3FC=30;PARAM_W_B3OUT=31;PARAM_B_B3OUT=32;\
|
||
PARAM_W_BN=33;PARAM_B_BN=34;\
|
||
PARAM_W_VSN1_0=35;PARAM_W_VSN2_0=36;\
|
||
PARAM_W_VSN1_1=37;PARAM_W_VSN2_1=38;\
|
||
PARAM_W_VSN1_2=39;PARAM_W_VSN2_2=40;\
|
||
PARAM_W_VSN1_3=41;PARAM_W_VSN2_3=42;\
|
||
PARAM_W_GATE_0=43;PARAM_B_GATE_0=44;\
|
||
PARAM_W_GATE_1=45;PARAM_B_GATE_1=46;\
|
||
PARAM_W_GATE_2=47;PARAM_B_GATE_2=48;\
|
||
PARAM_W_GATE_3=49;PARAM_B_GATE_3=50;\
|
||
PARAM_KAN_COEFF_0=51;PARAM_KAN_RESID_0=52;\
|
||
PARAM_KAN_COEFF_1=53;PARAM_KAN_RESID_1=54;\
|
||
PARAM_KAN_COEFF_2=55;PARAM_KAN_RESID_2=56;\
|
||
PARAM_KAN_COEFF_3=57;PARAM_KAN_RESID_3=58;\
|
||
PARAM_W_REGIME=59;PARAM_B_REGIME=60;\
|
||
PARAM_SPACING_RAW_0=61;PARAM_SPACING_RAW_1=62;\
|
||
PARAM_SPACING_RAW_2=63;PARAM_SPACING_RAW_3=64;\
|
||
PARAM_W_V1_5BAR=65;PARAM_B_V1_5BAR=66;\
|
||
PARAM_W_V2_5BAR=67;PARAM_B_V2_5BAR=68;\
|
||
PARAM_W_V1_20BAR=69;PARAM_B_V1_20BAR=70;\
|
||
PARAM_W_V2_20BAR=71;PARAM_B_V2_20BAR=72;\
|
||
PARAM_W_RISK_FC=73;PARAM_B_RISK_FC=74;\
|
||
PARAM_W_RISK_OUT=75;PARAM_B_RISK_OUT=76;\
|
||
PARAM_W_ISV_FC1=77;PARAM_B_ISV_FC1=78;\
|
||
PARAM_W_ISV_FC2=79;PARAM_B_ISV_FC2=80;\
|
||
PARAM_W_ISV_GATE=81;PARAM_B_ISV_GATE=82;\
|
||
PARAM_W_ISV_GAMMA=83;PARAM_B_ISV_GAMMA=84;\
|
||
PARAM_W_CONF_FC=85;PARAM_B_CONF_FC=86;\
|
||
PARAM_W_FEATURE_GATE=87;PARAM_B_FEATURE_GATE=88;\
|
||
PARAM_W_TEMPORAL_ROUTE=89;PARAM_B_TEMPORAL_ROUTE=90;\
|
||
PARAM_W_PLAN_FC=91;PARAM_B_PLAN_FC=92;\
|
||
PARAM_W_PLAN_OUT=93;PARAM_B_PLAN_OUT=94;\
|
||
PARAM_VSN_W1_G0=95;PARAM_VSN_B1_G0=96;PARAM_VSN_W2_G0=97;PARAM_VSN_B2_G0=98;\
|
||
PARAM_VSN_W1_G1=99;PARAM_VSN_B1_G1=100;PARAM_VSN_W2_G1=101;PARAM_VSN_B2_G1=102;\
|
||
PARAM_VSN_W1_G2=103;PARAM_VSN_B1_G2=104;PARAM_VSN_W2_G2=105;PARAM_VSN_B2_G2=106;\
|
||
PARAM_VSN_W1_G3=107;PARAM_VSN_B1_G3=108;PARAM_VSN_W2_G3=109;PARAM_VSN_B2_G3=110;\
|
||
PARAM_VSN_W1_G4=111;PARAM_VSN_B1_G4=112;PARAM_VSN_W2_G4=113;PARAM_VSN_B2_G4=114;\
|
||
PARAM_VSN_W1_G5=115;PARAM_VSN_B1_G5=116;PARAM_VSN_W2_G5=117;PARAM_VSN_B2_G5=118;\
|
||
PARAM_AUX_NB_W1=119;PARAM_AUX_NB_B1=120;PARAM_AUX_NB_W2=121;PARAM_AUX_NB_B2=122;\
|
||
PARAM_AUX_RG_W1=123;PARAM_AUX_RG_B1=124;PARAM_AUX_RG_W2=125;PARAM_AUX_RG_B2=126;\
|
||
PARAM_MOE_GATE_W1=127;PARAM_MOE_GATE_B1=128;PARAM_MOE_GATE_W2=129;PARAM_MOE_GATE_B2=130;\
|
||
PARAM_MOE_EXPERT_0_W1=131;PARAM_MOE_EXPERT_0_B1=132;PARAM_MOE_EXPERT_0_W2=133;PARAM_MOE_EXPERT_0_B2=134;\
|
||
PARAM_MOE_EXPERT_1_W1=135;PARAM_MOE_EXPERT_1_B1=136;PARAM_MOE_EXPERT_1_W2=137;PARAM_MOE_EXPERT_1_B2=138;\
|
||
PARAM_MOE_EXPERT_2_W1=139;PARAM_MOE_EXPERT_2_B1=140;PARAM_MOE_EXPERT_2_W2=141;PARAM_MOE_EXPERT_2_B2=142;\
|
||
PARAM_MOE_EXPERT_3_W1=143;PARAM_MOE_EXPERT_3_B1=144;PARAM_MOE_EXPERT_3_W2=145;PARAM_MOE_EXPERT_3_B2=146;\
|
||
PARAM_MOE_EXPERT_4_W1=147;PARAM_MOE_EXPERT_4_B1=148;PARAM_MOE_EXPERT_4_W2=149;PARAM_MOE_EXPERT_4_B2=150;\
|
||
PARAM_MOE_EXPERT_5_W1=151;PARAM_MOE_EXPERT_5_B1=152;PARAM_MOE_EXPERT_5_W2=153;PARAM_MOE_EXPERT_5_B2=154;\
|
||
PARAM_MOE_EXPERT_6_W1=155;PARAM_MOE_EXPERT_6_B1=156;PARAM_MOE_EXPERT_6_W2=157;PARAM_MOE_EXPERT_6_B2=158;\
|
||
PARAM_MOE_EXPERT_7_W1=159;PARAM_MOE_EXPERT_7_B1=160;PARAM_MOE_EXPERT_7_W2=161;PARAM_MOE_EXPERT_7_B2=162;\
|
||
PARAM_TOTAL_TENSORS=163";
|
||
|
||
#[test]
|
||
fn fingerprint_bumped_from_pre_b1_1a() {
|
||
use ml::cuda_pipeline::gpu_dqn_trainer::LAYOUT_FINGERPRINT_CURRENT;
|
||
let pre_b1_1a_fp = fnv1a_64(PRE_B1_1A_SEED);
|
||
assert_ne!(
|
||
LAYOUT_FINGERPRINT_CURRENT, pre_b1_1a_fp,
|
||
"B1.1a fingerprint must differ from pre-B1.1a value (the W2/B2 \
|
||
rename to _K2 must change the seed hash). \
|
||
current = {:#018x}, pre-B1.1a = {:#018x}",
|
||
LAYOUT_FINGERPRINT_CURRENT, pre_b1_1a_fp
|
||
);
|
||
}
|
||
|
||
// ─── Test 18: HEALTH_DIAG snapshot size unchanged at 149*4 ────────────────
|
||
|
||
/// B1.1a does NOT touch the HEALTH_DIAG snap-words layout — the aux
|
||
/// block remained at 3 words after B1.0 retired the `aux_label_scale`
|
||
/// entry (B1.0 dropped the size from 150 → 149). B1.1a's struct flips
|
||
/// (aux_nb_label_buf f32→i32, K=1→2 buffer growth, dir_acc_buf 3→6)
|
||
/// are all GPU-only: HEALTH_DIAG-snap remains 149 floats × 4 bytes =
|
||
/// 596 bytes. This test guards against accidental snap-layout
|
||
/// regressions during the cascade.
|
||
#[test]
|
||
fn health_diag_snap_size_stable_at_149_floats() {
|
||
use ml::cuda_pipeline::health_diag::HealthDiagSnapshot;
|
||
let n = std::mem::size_of::<HealthDiagSnapshot>();
|
||
assert_eq!(
|
||
n, 149 * 4,
|
||
"expected HealthDiagSnapshot to be 149*4 = 596 bytes (B1.0 stable, \
|
||
B1.1a unchanged), got {n}"
|
||
);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// SP13 Layer B Commit B1.1b: producer kernel correctness tests
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
//
|
||
// `aux_sign_label_kernel` is a pure per-thread map. Tests below construct
|
||
// synthetic price trajectories in a `[total_bars, 6]` (stride 6, col 2 =
|
||
// raw_close) mapped-pinned buffer + a per-experience `bar_indices` array,
|
||
// launch the kernel, and verify the resulting `[total] i32` labels match
|
||
// the expected -1/0/1 contract:
|
||
// -1 = skip (bar < 0 OR bar+lookahead >= total_bars)
|
||
// 0 = down/flat (p_fut <= p_now under strict greater-than tie-break)
|
||
// 1 = up (p_fut > p_now)
|
||
//
|
||
// Lookahead = 30 here matches the production `config.hindsight_lookahead`
|
||
// default; the kernel's lookahead arg lets future tests vary it.
|
||
|
||
fn load_aux_sign_label(stream: &Arc<CudaStream>) -> CudaFunction {
|
||
let module = stream
|
||
.context()
|
||
.load_cubin(SP13_AUX_SIGN_LABEL_CUBIN.to_vec())
|
||
.expect("load aux_sign_label cubin");
|
||
module
|
||
.load_function("aux_sign_label_kernel")
|
||
.expect("load aux_sign_label_kernel function")
|
||
}
|
||
|
||
/// Run the producer kernel and return the host-side labels vector.
|
||
/// `targets_close` is a slice of length `total_bars` containing the
|
||
/// raw_close column values; the kernel reads them at stride-6 offset 2,
|
||
/// so we materialize a `[total_bars * 6]` buffer with col 2 = the input.
|
||
fn run_producer(
|
||
targets_close: &[f32],
|
||
bar_indices: &[i32],
|
||
lookahead: i32,
|
||
) -> Vec<i32> {
|
||
let stream = make_test_stream();
|
||
let kernel = load_aux_sign_label(&stream);
|
||
|
||
let total_bars = targets_close.len();
|
||
let total = bar_indices.len();
|
||
|
||
// Build full [total_bars, 6] targets buffer with raw_close at col 2.
|
||
let mut targets_full = vec![0.0_f32; total_bars * 6];
|
||
for (i, &p) in targets_close.iter().enumerate() {
|
||
targets_full[i * 6 + 2] = p;
|
||
}
|
||
|
||
let targets_buf = unsafe { MappedF32Buffer::new(total_bars * 6) }.unwrap();
|
||
let bar_idx_buf = unsafe { MappedI32Buffer::new(total) }.unwrap();
|
||
let out_buf = unsafe { MappedI32Buffer::new(total) }.unwrap();
|
||
targets_buf.write_from_slice(&targets_full);
|
||
bar_idx_buf.write_from_slice(bar_indices);
|
||
|
||
let total_i32 = total as i32;
|
||
let total_bars_i32 = total_bars as i32;
|
||
let blocks = ((total as u32 + 255) / 256).max(1);
|
||
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&kernel)
|
||
.arg(&targets_buf.dev_ptr)
|
||
.arg(&bar_idx_buf.dev_ptr)
|
||
.arg(&out_buf.dev_ptr)
|
||
.arg(&total_i32)
|
||
.arg(&total_bars_i32)
|
||
.arg(&lookahead)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (blocks, 1, 1),
|
||
block_dim: (256, 1, 1),
|
||
shared_mem_bytes: 0,
|
||
})
|
||
.expect("aux_sign_label_kernel launch");
|
||
}
|
||
unsafe {
|
||
cudarc::driver::sys::cuStreamSynchronize(stream.cu_stream());
|
||
}
|
||
out_buf.read_all()
|
||
}
|
||
|
||
// ─── Test 1: monotone-up trajectory ────────────────────────────────────────
|
||
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn aux_sign_label_monotone_up_all_ones() {
|
||
// 60 bars, +0.1 per step. Every valid bar has p_fut > p_now ⇒ label = 1.
|
||
let total_bars = 60;
|
||
let lookahead = 30;
|
||
let targets: Vec<f32> = (0..total_bars).map(|i| 100.0 + i as f32 * 0.1).collect();
|
||
// 30 valid experiences (bar 0..29), all have lookahead window in-range.
|
||
let bar_indices: Vec<i32> = (0..30).collect();
|
||
let labels = run_producer(&targets, &bar_indices, lookahead);
|
||
for (i, &l) in labels.iter().enumerate() {
|
||
assert_eq!(l, 1, "monotone-up: bar {i} should be label 1, got {l}");
|
||
}
|
||
}
|
||
|
||
// ─── Test 2: monotone-down trajectory ──────────────────────────────────────
|
||
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn aux_sign_label_monotone_down_all_zeros() {
|
||
// 60 bars, -0.1 per step. Every valid bar has p_fut < p_now ⇒ label = 0.
|
||
let total_bars = 60;
|
||
let lookahead = 30;
|
||
let targets: Vec<f32> = (0..total_bars).map(|i| 100.0 - i as f32 * 0.1).collect();
|
||
let bar_indices: Vec<i32> = (0..30).collect();
|
||
let labels = run_producer(&targets, &bar_indices, lookahead);
|
||
for (i, &l) in labels.iter().enumerate() {
|
||
assert_eq!(l, 0, "monotone-down: bar {i} should be label 0, got {l}");
|
||
}
|
||
}
|
||
|
||
// ─── Test 3: flat trajectory (strict greater-than tie-break ⇒ 0) ───────────
|
||
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn aux_sign_label_flat_all_zeros_strict_gt() {
|
||
// 60 bars all at 100.0. p_fut == p_now under strict greater-than maps
|
||
// to label 0 (down/flat) — the K=2 head encodes "up" vs "not-up".
|
||
let total_bars = 60;
|
||
let lookahead = 30;
|
||
let targets = vec![100.0_f32; total_bars];
|
||
let bar_indices: Vec<i32> = (0..30).collect();
|
||
let labels = run_producer(&targets, &bar_indices, lookahead);
|
||
for (i, &l) in labels.iter().enumerate() {
|
||
assert_eq!(l, 0, "flat: bar {i} should be label 0 (strict-gt tie-break), got {l}");
|
||
}
|
||
}
|
||
|
||
// ─── Test 4: last-30 bars get skip sentinel ────────────────────────────────
|
||
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn aux_sign_label_last_30_bars_skip() {
|
||
// 60 bars; bar+30 must be < total_bars=60. So bar=30..59 fail the
|
||
// bound check (bar+30 ∈ [60..89] >= 60) and get -1.
|
||
let total_bars = 60;
|
||
let lookahead = 30;
|
||
let targets: Vec<f32> = (0..total_bars).map(|i| 100.0 + i as f32 * 0.1).collect();
|
||
let bar_indices: Vec<i32> = (0..total_bars as i32).collect();
|
||
let labels = run_producer(&targets, &bar_indices, lookahead);
|
||
for bar in 0..total_bars {
|
||
let expected = if bar + 30 >= total_bars { -1 } else { 1 };
|
||
assert_eq!(
|
||
labels[bar], expected,
|
||
"bar {bar}: expected {expected}, got {}", labels[bar]
|
||
);
|
||
}
|
||
// Sanity: exactly 30 -1s and 30 1s.
|
||
let n_skip = labels.iter().filter(|&&l| l == -1).count();
|
||
let n_up = labels.iter().filter(|&&l| l == 1).count();
|
||
assert_eq!(n_skip, 30, "expected 30 skip-labels, got {n_skip}");
|
||
assert_eq!(n_up, 30, "expected 30 up-labels, got {n_up}");
|
||
}
|
||
|
||
// ─── Test 5: boundary — bar=0 valid, bar=total_bars-1 skip ────────────────
|
||
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn aux_sign_label_boundary_first_valid_last_skip() {
|
||
let total_bars = 100;
|
||
let lookahead = 30;
|
||
let targets: Vec<f32> = (0..total_bars).map(|i| 100.0 + i as f32 * 0.05).collect();
|
||
let bar_indices: Vec<i32> = vec![0, (total_bars - 1) as i32];
|
||
let labels = run_producer(&targets, &bar_indices, lookahead);
|
||
assert_eq!(labels[0], 1, "bar=0 with monotone-up trajectory: expected label 1, got {}", labels[0]);
|
||
assert_eq!(
|
||
labels[1], -1,
|
||
"bar=total_bars-1=99: 99+30=129 >= 100, expected -1, got {}", labels[1]
|
||
);
|
||
}
|
||
|
||
// ─── Test 6: multi-episode — per-episode skip semantics ────────────────────
|
||
|
||
#[test]
|
||
#[ignore = "requires GPU"]
|
||
fn aux_sign_label_multi_episode_per_episode_skip() {
|
||
// 2 episodes × 50 bars at distinct trajectories. Total bars = 100
|
||
// (concatenated). Episode 0: bars 0..49 (monotone up). Episode 1:
|
||
// bars 50..99 (monotone down). All 100 experiences have bar_indices
|
||
// pointing at their respective bar; only the last 30 of episode 1
|
||
// (bars 70..99) hit the global total_bars boundary because
|
||
// total_bars=100 and bar+30 >= 100 ⇔ bar >= 70.
|
||
//
|
||
// NB: Skip semantics here are GLOBAL (against total_bars), not
|
||
// per-episode. The producer kernel doesn't know about episode
|
||
// boundaries — it only checks `bar + lookahead < total_bars`. The
|
||
// production code in `gpu_experience_collector.rs` enforces episode
|
||
// boundaries by virtue of `episode_starts[ep] + t` always being
|
||
// within the same episode's contiguous bar range. So the same global
|
||
// check applies.
|
||
let total_bars = 100;
|
||
let lookahead = 30;
|
||
let mut targets = vec![0.0_f32; total_bars];
|
||
for i in 0..50 {
|
||
targets[i] = 100.0 + i as f32 * 0.1; // ep 0 monotone up
|
||
targets[50 + i] = 200.0 - i as f32 * 0.1; // ep 1 monotone down (starts above)
|
||
}
|
||
let bar_indices: Vec<i32> = (0..total_bars as i32).collect();
|
||
let labels = run_producer(&targets, &bar_indices, lookahead);
|
||
|
||
// bars 0..49: ep 0 monotone up; bars 0..19 valid (label 1), bars 20..49
|
||
// straddle ep boundary into ep 1 (bar+30 ∈ [50..79], reads ep 1 trajectory).
|
||
// bar=20 reads p_fut=200 - (20+30-50)*0.1 = 200; p_now = 100 + 20*0.1 = 102.
|
||
// p_fut=200 > p_now=102 ⇒ label 1.
|
||
// Skip sentinel only fires for bar+30 >= 100 ⇒ bar >= 70.
|
||
// bars 50..69: ep 1 monotone down; p_fut < p_now ⇒ label 0.
|
||
// bars 70..99: skip ⇒ label -1.
|
||
for bar in 0..total_bars {
|
||
let expected = if bar + 30 >= total_bars {
|
||
-1
|
||
} else {
|
||
let p_now = targets[bar];
|
||
let p_fut = targets[bar + 30];
|
||
if p_fut > p_now { 1 } else { 0 }
|
||
};
|
||
assert_eq!(
|
||
labels[bar], expected,
|
||
"multi-episode bar {bar} (p_now={}, p_fut={}): expected {expected}, got {}",
|
||
targets[bar],
|
||
if bar + 30 < total_bars { targets[bar + 30] } else { f32::NAN },
|
||
labels[bar]
|
||
);
|
||
}
|
||
let n_skip = labels.iter().filter(|&&l| l == -1).count();
|
||
assert_eq!(n_skip, 30, "expected 30 skip-labels (bars 70..99), got {n_skip}");
|
||
}
|