Per train-multi-seed-hjzss validation: SP16 T1+T2 chain was structurally
landed but BEHAVIORALLY INERT in 5-epoch smoke (bit-identical to pfh9n
baseline through epoch 3). Root cause: hardcoded `alpha = 0.05f` in both
producer kernels violates feedback_isv_for_adaptive_bounds AND prevents
convergence in short runs (~60 epochs needed from cold start).
Fix per pearl_wiener_optimal_adaptive_alpha:
α = diff_var / (diff_var + sample_var + ε)
Where sample_var = running variance of target signal (Welford accumulator)
and diff_var = running variance of consecutive one-step differences.
Cold-start: target jumps 1.0 → 6.4 → 7.0 → high diff_var → α ≈ 0.6+
→ near-bootstrap responsiveness in epochs 1-3
Steady-state: signal stabilizes → diff_var drops → α decays naturally
→ smoothing emerges without hardcoded constant
Adds 12 new ISV slots (6 per producer):
- HCS_TARGET_MEAN/M2, HCS_DIFF_MEAN/M2, HCS_PREV_TARGET, HCS_SAMPLE_COUNT
- MHT_TARGET_MEAN/M2, MHT_DIFF_MEAN/M2, MHT_PREV_TARGET, MHT_SAMPLE_COUNT
ISV_TOTAL_DIM 462 → 474.
Both kernels migrated atomically. Pearl-A bootstrap preserved (sentinel
on prev_blended triggers REPLACE; cold-start α=1.0 when N<3 samples).
Defensive bounds [WELFORD_ALPHA_MIN=0.01, WELFORD_ALPHA_MAX=0.95] on the
Wiener-derived α to guard against denormal/underflow corner cases.
HEALTH_DIAG[N] emit extended with `alpha=...` and `sample_count=...` for
direct trajectory observation in validation smoke.
Behavioral tests verify:
- α high during signal jumps (>0.3 at epoch 3 post-cold-start)
- α low in steady state (mean tail α<0.4 under converging signal)
- Pearl-A bootstrap fires on first observation (Welford state advances
regardless of REPLACE branch)
- α stays within [WELFORD_ALPHA_MIN, WELFORD_ALPHA_MAX] over 50 epochs
(post-cold-start; cold-start α=1.0 by design)
- No 0.05f hardcoded literal remains in blend math (regression-locked
via host-only string scan)
5 GPU + host tests pass: sp16_phase3_alpha_high_during_signal_jump,
alpha_low_in_steady_state, pearl_a_bootstrap_first_obs,
alpha_naturally_bounded, no_hardcoded_alpha. sp14 + sp15 oracle suites
unchanged (34 GPU tests + 4 host tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1324 lines
56 KiB
Rust
1324 lines
56 KiB
Rust
#![allow(unsafe_code)] // Required for CUDA kernel launches.
|
||
// Allow non-snake-case for `dW{1,2,3}_ptr` / `dW_reduce_kernel` /
|
||
// `launch_dW_reduce` — the math notation `dW` is the universally
|
||
// recognised symbol for weight gradient (matches the kernel parameter
|
||
// names in `aux_trunk_backward_kernel.cu`). Snake-casing them
|
||
// (`d_w1_ptr`, `d_w_reduce_kernel`) loses the math correspondence and
|
||
// hurts readability when paired with the kernel source.
|
||
#![allow(non_snake_case)]
|
||
|
||
//! `gpu_aux_trunk` — SP14 Layer C Phase C.3 (2026-05-08).
|
||
//!
|
||
//! Rust wrapper for the auxiliary trunk forward kernel
|
||
//! (`aux_trunk_forward_kernel.cu`). Owns the pre-loaded `CudaFunction`
|
||
//! handle so launches in the captured graph never touch host-side cubin
|
||
//! load paths (per `pearl_no_host_branches_in_captured_graph.md`).
|
||
//!
|
||
//! # Architecture
|
||
//!
|
||
//! 3-layer Linear→ELU→Linear→ELU→Linear MLP parallel to Q's GRN trunk.
|
||
//! Both trunks read the shared encoder output (`x_in [B, ENCODER_OUT_DIM]`);
|
||
//! Q trunk produces `h_s2`, aux trunk produces `h_s2_aux`. Aux backward
|
||
//! (Phase C.4) terminates at the encoder boundary — Q-loss is the sole
|
||
//! shaping force on the encoder per the locked design decision in
|
||
//! `2026-05-07-sp14-layer-c-separate-aux-trunk.md`.
|
||
//!
|
||
//! Saved-for-backward outputs (consumed by aux trunk backward in C.4):
|
||
//!
|
||
//! - `h_aux1 [B, H1]` — Layer-1 post-ELU activation
|
||
//! - `h_aux2 [B, H2]` — Layer-2 post-ELU activation
|
||
//! - `h_s2_aux [B, AUX_HIDDEN_DIM]` — final linear output (no activation)
|
||
//!
|
||
//! Production topology (verified C.2):
|
||
//! - `ENCODER_OUT_DIM = config.shared_h1 = 256`
|
||
//! - `H1 = AUX_TRUNK_H1 = 256`
|
||
//! - `H2 = AUX_TRUNK_H2 = 128`
|
||
//! - `AUX_HIDDEN_DIM = config.shared_h2 = 256` (matches existing aux head input dim)
|
||
//!
|
||
//! # Pearls applied
|
||
//!
|
||
//! - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||
//! pre-loaded once at construction; no `load_cubin` / `load_function`
|
||
//! on the launch path.
|
||
//! - `feedback_no_atomicadd.md` — forward kernel writes only per-(b, j)
|
||
//! outputs; no contention across blocks.
|
||
//! - `feedback_no_stubs.md` — full launcher, no placeholder body.
|
||
|
||
use std::sync::Arc;
|
||
|
||
use cudarc::driver::{CudaFunction, CudaStream, LaunchConfig, PushKernelArg};
|
||
|
||
use crate::MLError;
|
||
|
||
use super::gpu_dqn_trainer::{
|
||
AUX_HORIZON_UPDATE_CUBIN, AUX_TRUNK_BACKWARD_CUBIN, AUX_TRUNK_FORWARD_CUBIN,
|
||
AVG_WIN_HOLD_TIME_UPDATE_CUBIN, DD_SATURATION_FLOOR_UPDATE_CUBIN,
|
||
HOLD_COST_SCALE_UPDATE_CUBIN, H_S2_AUX_RMS_EMA_CUBIN,
|
||
KELLY_BAYESIAN_PRIORS_UPDATE_CUBIN, MIN_HOLD_TEMPERATURE_UPDATE_CUBIN,
|
||
REWARD_CAP_UPDATE_CUBIN,
|
||
};
|
||
|
||
/// Hidden width of the aux trunk's first internal layer (Linear_1 → ELU
|
||
/// output). Mirrors the encoder output dimension so Layer-1 acts as a
|
||
/// representation-mixing identity-shaped projection.
|
||
pub(crate) const AUX_TRUNK_H1: usize = 256;
|
||
|
||
/// Hidden width of the aux trunk's second internal layer (Linear_2 → ELU
|
||
/// output). Smaller than H1 for a mild bottleneck — encourages the aux
|
||
/// trunk to compress its representation before lifting back up to
|
||
/// `AUX_HIDDEN_DIM` in Layer 3.
|
||
pub(crate) const AUX_TRUNK_H2: usize = 128;
|
||
|
||
/// Block dim used by the per-sample forward kernel. Mirrors
|
||
/// `aux_heads_kernel.cu`'s `AUX_BLOCK = 256`.
|
||
const AUX_TRUNK_BLOCK: u32 = 256;
|
||
|
||
/// Forward orchestrator — owns the single `CudaFunction` for
|
||
/// `aux_trunk_forward`. Backward kernel + ops land in Phase C.4.
|
||
///
|
||
/// Dropping this struct releases the underlying `CudaFunction` handle
|
||
/// via cudarc's RAII; no explicit teardown required.
|
||
#[allow(missing_debug_implementations)]
|
||
pub(crate) struct AuxTrunkForwardOps {
|
||
/// Pre-loaded forward kernel handle (loaded once at construction;
|
||
/// never re-loaded on the launch path).
|
||
forward_kernel: CudaFunction,
|
||
}
|
||
|
||
impl AuxTrunkForwardOps {
|
||
/// Load the forward kernel handle from the precompiled cubin.
|
||
/// Mirrors `GrnBlock::new`'s loader pattern.
|
||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||
let context = stream.context();
|
||
let module = context
|
||
.load_cubin(AUX_TRUNK_FORWARD_CUBIN.to_vec())
|
||
.map_err(|e| MLError::ModelError(format!("aux_trunk_forward cubin load: {e}")))?;
|
||
let forward_kernel = module
|
||
.load_function("aux_trunk_forward")
|
||
.map_err(|e| MLError::ModelError(format!("aux_trunk_forward load: {e}")))?;
|
||
Ok(Self { forward_kernel })
|
||
}
|
||
|
||
/// Launch `aux_trunk_forward`: 3-layer Linear→ELU→Linear→ELU→Linear MLP.
|
||
///
|
||
/// Caller-owned buffers (raw `u64` device pointers for graph-capture
|
||
/// safety; mirrors `gpu_grn.rs::forward_raw` and `gpu_aux_heads.rs`):
|
||
/// * `x_in_ptr` — `[B, ENCODER_OUT_DIM]` row-major (encoder output).
|
||
/// * `w1_ptr`/`b1_ptr` — `[ENCODER_OUT_DIM, H1]` / `[H1]`.
|
||
/// * `w2_ptr`/`b2_ptr` — `[H1, H2]` / `[H2]`.
|
||
/// * `w3_ptr`/`b3_ptr` — `[H2, AUX_HIDDEN_DIM]` / `[AUX_HIDDEN_DIM]`.
|
||
/// * `h_aux1_out_ptr` — `[B, H1]` SAVED post-ELU Layer-1 (consumed by C.4 backward).
|
||
/// * `h_aux2_out_ptr` — `[B, H2]` SAVED post-ELU Layer-2 (consumed by C.4 backward).
|
||
/// * `h_s2_aux_out_ptr` — `[B, AUX_HIDDEN_DIM]` final aux trunk output.
|
||
///
|
||
/// `h1` / `h2` / `aux_hidden_dim` are passed as runtime args for
|
||
/// kernel-signature stability (a future bump won't require an ABI
|
||
/// change). `encoder_out_dim` is the encoder's actual output width
|
||
/// (typically `config.shared_h1 = 256`).
|
||
///
|
||
/// Block: `AUX_TRUNK_BLOCK = 256` threads.
|
||
/// Grid: `B` blocks (one block per batch row).
|
||
/// Shared mem: `(H1 + H2) * sizeof(f32)` bytes — Layer-1 cache for
|
||
/// Layer-2 matmul + Layer-2 cache for Layer-3 matmul.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn launch(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
x_in_ptr: u64,
|
||
w1_ptr: u64,
|
||
b1_ptr: u64,
|
||
w2_ptr: u64,
|
||
b2_ptr: u64,
|
||
w3_ptr: u64,
|
||
b3_ptr: u64,
|
||
h_aux1_out_ptr: u64,
|
||
h_aux2_out_ptr: u64,
|
||
h_s2_aux_out_ptr: u64,
|
||
b: usize,
|
||
encoder_out_dim: usize,
|
||
h1: usize,
|
||
h2: usize,
|
||
aux_hidden_dim: usize,
|
||
) -> Result<(), MLError> {
|
||
let b_i32 = b as i32;
|
||
let enc_i32 = encoder_out_dim as i32;
|
||
let h1_i32 = h1 as i32;
|
||
let h2_i32 = h2 as i32;
|
||
let aux_i32 = aux_hidden_dim as i32;
|
||
// Shared memory: H1 + H2 floats — Layer-1 cache + Layer-2 cache.
|
||
let smem_bytes = (h1 as u32 + h2 as u32) * std::mem::size_of::<f32>() as u32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.forward_kernel)
|
||
.arg(&x_in_ptr)
|
||
.arg(&w1_ptr)
|
||
.arg(&b1_ptr)
|
||
.arg(&w2_ptr)
|
||
.arg(&b2_ptr)
|
||
.arg(&w3_ptr)
|
||
.arg(&b3_ptr)
|
||
.arg(&h_aux1_out_ptr)
|
||
.arg(&h_aux2_out_ptr)
|
||
.arg(&h_s2_aux_out_ptr)
|
||
.arg(&b_i32)
|
||
.arg(&enc_i32)
|
||
.arg(&h1_i32)
|
||
.arg(&h2_i32)
|
||
.arg(&aux_i32)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (b as u32, 1, 1),
|
||
block_dim: (AUX_TRUNK_BLOCK, 1, 1),
|
||
shared_mem_bytes: smem_bytes,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("aux_trunk_forward: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Block dim for the backward kernels — power-of-two so the shmem-tree
|
||
/// reduce in `aux_trunk_bwd_dW_reduce` / `aux_trunk_bwd_db_reduce` halves
|
||
/// cleanly without odd-stride bookkeeping. Mirrors `AUX_TRUNK_BWD_BLOCK`
|
||
/// in `aux_trunk_backward_kernel.cu`.
|
||
const AUX_TRUNK_BWD_BLOCK: u32 = 256;
|
||
|
||
/// Backward orchestrator — owns three pre-loaded `CudaFunction` handles
|
||
/// for `aux_trunk_bwd_dh_pre`, `aux_trunk_bwd_dW_reduce`, and
|
||
/// `aux_trunk_bwd_db_reduce`. A single `launch()` call orchestrates the
|
||
/// full backward pass over all three trunk layers without allocating
|
||
/// per-sample partial buffers (per-element block-tree-reduce instead).
|
||
///
|
||
/// CRITICAL: this op set does NOT compute or write `dx_in`. The encoder
|
||
/// boundary is the stop-gradient per the locked design decision in
|
||
/// `2026-05-07-sp14-layer-c-separate-aux-trunk.md` §C.4. The structural
|
||
/// enforcement is in the kernel signatures — none of the three kernels
|
||
/// accept a `dx_in_out` pointer, so the kernel set literally cannot
|
||
/// touch encoder gradient memory. This is verified by the
|
||
/// `aux_trunk_backward_does_not_write_dx` oracle test which inspects the
|
||
/// kernel source for any `dx_in` write pattern.
|
||
///
|
||
/// # Pearls applied
|
||
///
|
||
/// - `pearl_no_host_branches_in_captured_graph.md` — all three
|
||
/// `CudaFunction`s are pre-loaded at construction. The backward
|
||
/// `launch()` issues a fixed sequence of seven kernel launches with
|
||
/// no host-side branching.
|
||
/// - `feedback_no_atomicadd.md` — every dW/db output cell is owned by
|
||
/// exactly one block; block-tree-reduce over the batch dim within
|
||
/// shared memory.
|
||
/// - `feedback_no_stubs.md` — full backward, no zero-fill placeholders,
|
||
/// no skipped layers.
|
||
#[allow(missing_debug_implementations)]
|
||
pub(crate) struct AuxTrunkBackwardOps {
|
||
/// Pre-pass: per-sample kernel that emits scratch buffers
|
||
/// `dh_aux1_pre [B, H1]` and `dh_aux2_pre [B, H2]`.
|
||
dh_pre_kernel: CudaFunction,
|
||
/// Generic outer-product reduce for any dW matrix:
|
||
/// `dW[k, j] = sum_b A[b, k] * B[b, j]`. Launched 3× per backward.
|
||
dW_reduce_kernel: CudaFunction,
|
||
/// Generic batch reduce for any db vector:
|
||
/// `db[j] = sum_b B[b, j]`. Launched 3× per backward.
|
||
db_reduce_kernel: CudaFunction,
|
||
}
|
||
|
||
impl AuxTrunkBackwardOps {
|
||
/// Load the three backward kernel handles from the precompiled cubin.
|
||
/// Mirrors `AuxTrunkForwardOps::new`'s loader pattern. All three
|
||
/// kernels live in a single cubin so one `load_cubin` call is
|
||
/// sufficient.
|
||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||
let context = stream.context();
|
||
let module = context
|
||
.load_cubin(AUX_TRUNK_BACKWARD_CUBIN.to_vec())
|
||
.map_err(|e| MLError::ModelError(format!("aux_trunk_backward cubin load: {e}")))?;
|
||
let dh_pre_kernel = module
|
||
.load_function("aux_trunk_bwd_dh_pre")
|
||
.map_err(|e| MLError::ModelError(format!("aux_trunk_bwd_dh_pre load: {e}")))?;
|
||
let dW_reduce_kernel = module
|
||
.load_function("aux_trunk_bwd_dW_reduce")
|
||
.map_err(|e| MLError::ModelError(format!("aux_trunk_bwd_dW_reduce load: {e}")))?;
|
||
let db_reduce_kernel = module
|
||
.load_function("aux_trunk_bwd_db_reduce")
|
||
.map_err(|e| MLError::ModelError(format!("aux_trunk_bwd_db_reduce load: {e}")))?;
|
||
Ok(Self {
|
||
dh_pre_kernel,
|
||
dW_reduce_kernel,
|
||
db_reduce_kernel,
|
||
})
|
||
}
|
||
|
||
/// Launch the full backward pass for the aux trunk.
|
||
///
|
||
/// Orchestrates seven kernel launches in fixed sequence (no host-
|
||
/// side branching, capture-friendly):
|
||
/// 1. `aux_trunk_bwd_dh_pre` — writes `dh_aux2_pre`, `dh_aux1_pre`.
|
||
/// 2. `aux_trunk_bwd_dW_reduce` — writes `dW3 = h_aux2.T @ d_logits`.
|
||
/// 3. `aux_trunk_bwd_db_reduce` — writes `db3 = sum_b d_logits[b, :]`.
|
||
/// 4. `aux_trunk_bwd_dW_reduce` — writes `dW2 = h_aux1.T @ dh_aux2_pre`.
|
||
/// 5. `aux_trunk_bwd_db_reduce` — writes `db2 = sum_b dh_aux2_pre[b, :]`.
|
||
/// 6. `aux_trunk_bwd_dW_reduce` — writes `dW1 = x_in.T @ dh_aux1_pre`.
|
||
/// 7. `aux_trunk_bwd_db_reduce` — writes `db1 = sum_b dh_aux1_pre[b, :]`.
|
||
///
|
||
/// Caller-owned buffers (raw `u64` device pointers; mirrors
|
||
/// `AuxTrunkForwardOps::launch` and `gpu_aux_heads.rs`):
|
||
/// - `dh_s2_aux_in_ptr` `[B, AUX_HIDDEN_DIM]` upstream gradient.
|
||
/// - `x_in_ptr` `[B, ENCODER_OUT_DIM]` saved-fwd input.
|
||
/// - `h_aux1_ptr` `[B, H1]` saved-fwd post-ELU.
|
||
/// - `h_aux2_ptr` `[B, H2]` saved-fwd post-ELU.
|
||
/// - `w2_ptr` / `w3_ptr` weight matrices for backward matmuls.
|
||
/// - `dh_aux1_pre_scratch` `[B, H1]` caller-allocated scratch.
|
||
/// - `dh_aux2_pre_scratch` `[B, H2]` caller-allocated scratch.
|
||
/// - `dW1_ptr` / `db1_ptr` `[ENCODER_OUT_DIM, H1]` / `[H1]`.
|
||
/// - `dW2_ptr` / `db2_ptr` `[H1, H2]` / `[H2]`.
|
||
/// - `dW3_ptr` / `db3_ptr` `[H2, AUX_HIDDEN_DIM]` / `[AUX_HIDDEN_DIM]`.
|
||
///
|
||
/// Output gradient buffers are OVERWRITTEN (not accumulated). If
|
||
/// caller wants accumulation across multiple backward calls, they
|
||
/// must SAXPY into a separate accumulator after this call. (Aux
|
||
/// trunk's gradient is computed once per training step in C.5; no
|
||
/// accumulation needed.)
|
||
///
|
||
/// NO `dx_in_out` parameter — that is the structural stop-gradient
|
||
/// at the encoder boundary. Encoder gradient stays Q-shaped.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn launch(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
dh_s2_aux_in_ptr: u64,
|
||
x_in_ptr: u64,
|
||
h_aux1_ptr: u64,
|
||
h_aux2_ptr: u64,
|
||
w2_ptr: u64,
|
||
w3_ptr: u64,
|
||
dh_aux1_pre_scratch_ptr: u64,
|
||
dh_aux2_pre_scratch_ptr: u64,
|
||
dW1_ptr: u64,
|
||
db1_ptr: u64,
|
||
dW2_ptr: u64,
|
||
db2_ptr: u64,
|
||
dW3_ptr: u64,
|
||
db3_ptr: u64,
|
||
b: usize,
|
||
encoder_out_dim: usize,
|
||
h1: usize,
|
||
h2: usize,
|
||
aux_hidden_dim: usize,
|
||
) -> Result<(), MLError> {
|
||
let b_i32 = b as i32;
|
||
// `encoder_out_dim` is consumed by `launch_dW_reduce` for dW1
|
||
// (Krows = ENCODER_OUT_DIM); the dh_pre kernel doesn't need it as
|
||
// a runtime arg because dh_pre only computes hidden-layer
|
||
// gradients. `enc_i32` would be redundant here.
|
||
let h1_i32 = h1 as i32;
|
||
let h2_i32 = h2 as i32;
|
||
let aux_i32 = aux_hidden_dim as i32;
|
||
let block_smem = AUX_TRUNK_BWD_BLOCK * std::mem::size_of::<f32>() as u32;
|
||
|
||
// ── 1. Pre-pass: dh_aux2_pre + dh_aux1_pre ────────────────────
|
||
// Block: AUX_TRUNK_BWD_BLOCK threads. Grid: (B, 1, 1).
|
||
// Shared memory: H2 floats (Layer-2 post-multiply cache).
|
||
let dh_pre_smem = h2 as u32 * std::mem::size_of::<f32>() as u32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.dh_pre_kernel)
|
||
.arg(&dh_s2_aux_in_ptr)
|
||
.arg(&w3_ptr)
|
||
.arg(&w2_ptr)
|
||
.arg(&h_aux1_ptr)
|
||
.arg(&h_aux2_ptr)
|
||
.arg(&dh_aux2_pre_scratch_ptr)
|
||
.arg(&dh_aux1_pre_scratch_ptr)
|
||
.arg(&b_i32)
|
||
.arg(&h1_i32)
|
||
.arg(&h2_i32)
|
||
.arg(&aux_i32)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (b as u32, 1, 1),
|
||
block_dim: (AUX_TRUNK_BWD_BLOCK, 1, 1),
|
||
shared_mem_bytes: dh_pre_smem,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("aux_trunk_bwd_dh_pre: {e}")))?;
|
||
}
|
||
|
||
// ── 2. dW3 = h_aux2.T @ d_logits [H2, AUX_HIDDEN_DIM] ────────
|
||
self.launch_dW_reduce(
|
||
stream,
|
||
h_aux2_ptr,
|
||
dh_s2_aux_in_ptr,
|
||
dW3_ptr,
|
||
b,
|
||
h2,
|
||
aux_hidden_dim,
|
||
block_smem,
|
||
)?;
|
||
|
||
// ── 3. db3 = sum_b d_logits[b, :] [AUX_HIDDEN_DIM] ───────────
|
||
self.launch_db_reduce(stream, dh_s2_aux_in_ptr, db3_ptr, b, aux_hidden_dim, block_smem)?;
|
||
|
||
// ── 4. dW2 = h_aux1.T @ dh_aux2_pre [H1, H2] ─────────────────
|
||
self.launch_dW_reduce(
|
||
stream,
|
||
h_aux1_ptr,
|
||
dh_aux2_pre_scratch_ptr,
|
||
dW2_ptr,
|
||
b,
|
||
h1,
|
||
h2,
|
||
block_smem,
|
||
)?;
|
||
|
||
// ── 5. db2 = sum_b dh_aux2_pre[b, :] [H2] ────────────────────
|
||
self.launch_db_reduce(stream, dh_aux2_pre_scratch_ptr, db2_ptr, b, h2, block_smem)?;
|
||
|
||
// ── 6. dW1 = x_in.T @ dh_aux1_pre [ENCODER_OUT_DIM, H1] ──────
|
||
self.launch_dW_reduce(
|
||
stream,
|
||
x_in_ptr,
|
||
dh_aux1_pre_scratch_ptr,
|
||
dW1_ptr,
|
||
b,
|
||
encoder_out_dim,
|
||
h1,
|
||
block_smem,
|
||
)?;
|
||
|
||
// ── 7. db1 = sum_b dh_aux1_pre[b, :] [H1] ────────────────────
|
||
self.launch_db_reduce(stream, dh_aux1_pre_scratch_ptr, db1_ptr, b, h1, block_smem)?;
|
||
|
||
// STOP — no Layer-1 dx kernel. Encoder boundary is the stop-grad.
|
||
Ok(())
|
||
}
|
||
|
||
/// Helper: launch `aux_trunk_bwd_dW_reduce` for one weight matrix.
|
||
/// Grid: `(Krows * Jcols, 1, 1)` — one block per output element.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn launch_dW_reduce(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
a_ptr: u64,
|
||
b_grad_ptr: u64,
|
||
dw_out_ptr: u64,
|
||
b: usize,
|
||
krows: usize,
|
||
jcols: usize,
|
||
smem_bytes: u32,
|
||
) -> Result<(), MLError> {
|
||
let b_i32 = b as i32;
|
||
let k_i32 = krows as i32;
|
||
let j_i32 = jcols as i32;
|
||
let grid = (krows * jcols) as u32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.dW_reduce_kernel)
|
||
.arg(&a_ptr)
|
||
.arg(&b_grad_ptr)
|
||
.arg(&dw_out_ptr)
|
||
.arg(&b_i32)
|
||
.arg(&k_i32)
|
||
.arg(&j_i32)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (grid, 1, 1),
|
||
block_dim: (AUX_TRUNK_BWD_BLOCK, 1, 1),
|
||
shared_mem_bytes: smem_bytes,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("aux_trunk_bwd_dW_reduce: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Helper: launch `aux_trunk_bwd_db_reduce` for one bias vector.
|
||
/// Grid: `(Jcols, 1, 1)` — one block per output element.
|
||
fn launch_db_reduce(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
b_grad_ptr: u64,
|
||
db_out_ptr: u64,
|
||
b: usize,
|
||
jcols: usize,
|
||
smem_bytes: u32,
|
||
) -> Result<(), MLError> {
|
||
let b_i32 = b as i32;
|
||
let j_i32 = jcols as i32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.db_reduce_kernel)
|
||
.arg(&b_grad_ptr)
|
||
.arg(&db_out_ptr)
|
||
.arg(&b_i32)
|
||
.arg(&j_i32)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (jcols as u32, 1, 1),
|
||
block_dim: (AUX_TRUNK_BWD_BLOCK, 1, 1),
|
||
shared_mem_bytes: smem_bytes,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("aux_trunk_bwd_db_reduce: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// SP14 Layer C Phase C.4b (2026-05-08): adaptive aux prediction horizon
|
||
/// producer.
|
||
///
|
||
/// Drives `ISV[AUX_PRED_HORIZON_BARS_INDEX=450]` from
|
||
/// `ISV[AVG_WIN_HOLD_TIME_BARS_INDEX=451]` via Pearl-A first-observation
|
||
/// bootstrap + slow Wiener-α EMA. Single-thread, single-block kernel —
|
||
/// horizon is slow-moving so per-epoch boundary launch is appropriate
|
||
/// (per-step would be wasteful and would track sample noise).
|
||
///
|
||
/// # Pearls applied
|
||
/// - `pearl_first_observation_bootstrap.md` — sentinel = 60.0; first
|
||
/// valid observation REPLACES (no blend) so the cold-start sentinel
|
||
/// never contaminates the EMA.
|
||
/// - `pearl_wiener_optimal_adaptive_alpha.md` — fixed α=0.01 fallback
|
||
/// when no target-variance EMA exists.
|
||
/// - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||
/// pre-loaded once at construction.
|
||
/// - `feedback_isv_for_adaptive_bounds.md` — h_min=1, h_max=240 are
|
||
/// fundamental floors/ceilings, not tuning parameters.
|
||
#[allow(missing_debug_implementations)]
|
||
pub(crate) struct AuxHorizonUpdateOps {
|
||
update_kernel: CudaFunction,
|
||
}
|
||
|
||
impl AuxHorizonUpdateOps {
|
||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||
let context = stream.context();
|
||
let module = context
|
||
.load_cubin(AUX_HORIZON_UPDATE_CUBIN.to_vec())
|
||
.map_err(|e| MLError::ModelError(format!("aux_horizon_update cubin load: {e}")))?;
|
||
let update_kernel = module
|
||
.load_function("aux_horizon_update")
|
||
.map_err(|e| MLError::ModelError(format!("aux_horizon_update load: {e}")))?;
|
||
Ok(Self { update_kernel })
|
||
}
|
||
|
||
/// Launch the horizon producer.
|
||
///
|
||
/// Args:
|
||
/// - `isv_ptr`: ISV[ISV_TOTAL_DIM] device pointer (mapped-pinned).
|
||
/// - `isv_h_idx`: AUX_PRED_HORIZON_BARS_INDEX (450).
|
||
/// - `isv_h_target_idx`: AVG_WIN_HOLD_TIME_BARS_INDEX (451).
|
||
/// - `isv_h_target_var_idx`: variance EMA slot for target, or `-1`
|
||
/// if none (uses fixed α=0.01).
|
||
/// - `h_min`, `h_max`: fundamental bounds (1.0, 240.0).
|
||
/// - `sentinel_h`: SENTINEL_AUX_PRED_HORIZON_BARS (60.0).
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn launch(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
isv_ptr: u64,
|
||
isv_h_idx: i32,
|
||
isv_h_target_idx: i32,
|
||
isv_h_target_var_idx: i32,
|
||
h_min: f32,
|
||
h_max: f32,
|
||
sentinel_h: f32,
|
||
) -> Result<(), MLError> {
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.update_kernel)
|
||
.arg(&isv_ptr)
|
||
.arg(&isv_h_idx)
|
||
.arg(&isv_h_target_idx)
|
||
.arg(&isv_h_target_var_idx)
|
||
.arg(&h_min)
|
||
.arg(&h_max)
|
||
.arg(&sentinel_h)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (1, 1, 1),
|
||
shared_mem_bytes: 0,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("aux_horizon_update: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// SP14 Layer C Phase C.4b (2026-05-08): avg winning hold time EMA producer.
|
||
///
|
||
/// Sweeps the per-epoch `hold_at_exit_per_sample` +
|
||
/// `trade_profitable_per_sample` buffers (populated by
|
||
/// `unified_env_step_core` in `experience_kernels.cu`) and writes the
|
||
/// EMA-blended mean to `ISV[AVG_WIN_HOLD_TIME_BARS_INDEX=451]` for
|
||
/// downstream consumption by `AuxHorizonUpdateOps`.
|
||
///
|
||
/// Single-block kernel; block-tree-reduce over the per-sample buffer.
|
||
/// Pearl-A first-observation bootstrap; α=0.05 EMA thereafter.
|
||
///
|
||
/// # Pearls applied
|
||
/// - `feedback_no_atomicadd.md` — single block, block-tree-reduce in shmem.
|
||
/// - `pearl_first_observation_bootstrap.md` — sentinel = 0.0 → REPLACE.
|
||
/// - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||
/// pre-loaded at construction; on-device `count == 0` guard.
|
||
#[allow(missing_debug_implementations)]
|
||
pub(crate) struct AvgWinHoldTimeUpdateOps {
|
||
update_kernel: CudaFunction,
|
||
}
|
||
|
||
impl AvgWinHoldTimeUpdateOps {
|
||
/// Block dim used by the producer kernel — must match `BLK_DIM` in
|
||
/// `avg_win_hold_time_update_kernel.cu`.
|
||
const BLK_DIM: u32 = 256;
|
||
/// Steady-state EMA blend rate. Slow enough to filter
|
||
/// sample-distribution noise, fast enough to track policy evolution
|
||
/// across epochs.
|
||
pub(crate) const ALPHA: f32 = 0.05;
|
||
|
||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||
let context = stream.context();
|
||
let module = context
|
||
.load_cubin(AVG_WIN_HOLD_TIME_UPDATE_CUBIN.to_vec())
|
||
.map_err(|e| MLError::ModelError(format!("avg_win_hold_time_update cubin load: {e}")))?;
|
||
let update_kernel = module
|
||
.load_function("avg_win_hold_time_update")
|
||
.map_err(|e| MLError::ModelError(format!("avg_win_hold_time_update load: {e}")))?;
|
||
Ok(Self { update_kernel })
|
||
}
|
||
|
||
/// Launch the producer.
|
||
///
|
||
/// Args:
|
||
/// - `hold_at_exit_ptr`: f32 device ptr `[total_samples]`.
|
||
/// - `trade_profitable_ptr`: i32 device ptr `[total_samples]`.
|
||
/// - `total_samples`: N*L (B*T total samples this epoch).
|
||
/// - `isv_ptr`: ISV[ISV_TOTAL_DIM] device pointer.
|
||
/// - `isv_target_idx`: AVG_WIN_HOLD_TIME_BARS_INDEX (451).
|
||
/// - `sentinel`: SENTINEL_AVG_WIN_HOLD_TIME_BARS (0.0).
|
||
/// - `alpha`: EMA blend rate (use `Self::ALPHA = 0.05`).
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn launch(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
hold_at_exit_ptr: u64,
|
||
trade_profitable_ptr: u64,
|
||
total_samples: i32,
|
||
isv_ptr: u64,
|
||
isv_target_idx: i32,
|
||
sentinel: f32,
|
||
alpha: f32,
|
||
) -> Result<(), MLError> {
|
||
let smem_bytes = 2 * Self::BLK_DIM * std::mem::size_of::<f32>() as u32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.update_kernel)
|
||
.arg(&hold_at_exit_ptr)
|
||
.arg(&trade_profitable_ptr)
|
||
.arg(&total_samples)
|
||
.arg(&isv_ptr)
|
||
.arg(&isv_target_idx)
|
||
.arg(&sentinel)
|
||
.arg(&alpha)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (Self::BLK_DIM, 1, 1),
|
||
shared_mem_bytes: smem_bytes,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("avg_win_hold_time_update: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Class A P0-A (2026-05-08): adaptive REWARD_POS/NEG_CAP producer.
|
||
///
|
||
/// Sweeps the per-epoch `step_ret_per_sample` + `trade_close_per_sample`
|
||
/// buffers (populated by `unified_env_step_core` in `experience_kernels.cu`),
|
||
/// computes a Welford `mean + Z_99 × sigma` p99 estimator over winning
|
||
/// realized returns plus a `max(winning_returns)` conservative takeover,
|
||
/// applies a 1.5× safety factor, clamps to dimensional bounds [1, 50],
|
||
/// and writes both `ISV[REWARD_POS_CAP_ADAPTIVE_INDEX=452]` (POS cap)
|
||
/// and `ISV[REWARD_NEG_CAP_ADAPTIVE_INDEX=453]` (NEG = −2 × POS,
|
||
/// preserving Kahneman/Tversky 2:1 loss-aversion asymmetry per
|
||
/// `pearl_audit_unboundedness_for_implicit_asymmetry` — moved from
|
||
/// hardcoded scalar to producer-time multiplier; SINGLE source of truth).
|
||
///
|
||
/// Single-block kernel; block-tree-reduce over the per-sample buffer
|
||
/// (no atomicAdd per `feedback_no_atomicadd.md`). Pearl-A
|
||
/// first-observation bootstrap (sentinel 5.0/-10.0 matches pre-fix
|
||
/// hardcoded constants for bit-identical cold-start); α=0.01 slow EMA
|
||
/// thereafter.
|
||
///
|
||
/// Per-epoch boundary launch — reward distribution is the foundation
|
||
/// of training and shouldn't move fast; per-step would track sample noise.
|
||
///
|
||
/// # Pearls applied
|
||
/// - `feedback_no_atomicadd.md` — single block, block-tree-reduce in shmem.
|
||
/// - `pearl_first_observation_bootstrap.md` — sentinel = 5.0 → REPLACE.
|
||
/// - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||
/// pre-loaded at construction; on-device guards.
|
||
/// - `pearl_symmetric_clamp_audit.md` — bilateral `fmaxf(lo, fminf(x, hi))`
|
||
/// on POS, NEG derived clamp.
|
||
/// - `feedback_isv_for_adaptive_bounds.md` — POS bounds [1, 50] are
|
||
/// Category-1 dimensional safety, NOT tuning.
|
||
#[allow(missing_debug_implementations)]
|
||
pub(crate) struct RewardCapUpdateOps {
|
||
update_kernel: CudaFunction,
|
||
}
|
||
|
||
impl RewardCapUpdateOps {
|
||
/// Block dim used by the producer kernel — must match `BLK_DIM` in
|
||
/// `reward_cap_update_kernel.cu`.
|
||
const BLK_DIM: u32 = 256;
|
||
|
||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||
let context = stream.context();
|
||
let module = context
|
||
.load_cubin(REWARD_CAP_UPDATE_CUBIN.to_vec())
|
||
.map_err(|e| MLError::ModelError(format!("reward_cap_update cubin load: {e}")))?;
|
||
let update_kernel = module
|
||
.load_function("reward_cap_update")
|
||
.map_err(|e| MLError::ModelError(format!("reward_cap_update load: {e}")))?;
|
||
Ok(Self { update_kernel })
|
||
}
|
||
|
||
/// Launch the adaptive reward-cap producer.
|
||
///
|
||
/// Args:
|
||
/// - `step_ret_ptr`: f32 device ptr `[total_samples]` —
|
||
/// `step_ret_core` raw signed per-step return.
|
||
/// - `trade_close_ptr`: i32 device ptr `[total_samples]` — 1 iff
|
||
/// exiting_trade || reversing_trade.
|
||
/// - `total_samples`: N*L (B*T total samples this epoch).
|
||
/// - `isv_ptr`: ISV[ISV_TOTAL_DIM] device pointer.
|
||
/// - `pos_idx`: REWARD_POS_CAP_ADAPTIVE_INDEX (452).
|
||
/// - `neg_idx`: REWARD_NEG_CAP_ADAPTIVE_INDEX (453).
|
||
/// - `sentinel_pos`: SENTINEL_REWARD_POS_CAP (5.0).
|
||
/// - `sentinel_neg`: SENTINEL_REWARD_NEG_CAP (-10.0).
|
||
/// - `safety_factor`: REWARD_CAP_SAFETY_FACTOR (1.5).
|
||
/// - `neg_to_pos_ratio`: REWARD_NEG_TO_POS_RATIO (2.0).
|
||
/// - `pos_min`, `pos_max`: REWARD_POS_CAP_MIN/MAX (1.0, 50.0).
|
||
/// - `alpha`: REWARD_CAP_EMA_ALPHA (0.01).
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn launch(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
step_ret_ptr: u64,
|
||
trade_close_ptr: u64,
|
||
total_samples: i32,
|
||
isv_ptr: u64,
|
||
pos_idx: i32,
|
||
neg_idx: i32,
|
||
sentinel_pos: f32,
|
||
sentinel_neg: f32,
|
||
safety_factor: f32,
|
||
neg_to_pos_ratio: f32,
|
||
pos_min: f32,
|
||
pos_max: f32,
|
||
alpha: f32,
|
||
) -> Result<(), MLError> {
|
||
// 4 arrays × BLK_DIM × sizeof(f32) — see the kernel comment block.
|
||
let smem_bytes = 4 * Self::BLK_DIM * std::mem::size_of::<f32>() as u32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.update_kernel)
|
||
.arg(&step_ret_ptr)
|
||
.arg(&trade_close_ptr)
|
||
.arg(&total_samples)
|
||
.arg(&isv_ptr)
|
||
.arg(&pos_idx)
|
||
.arg(&neg_idx)
|
||
.arg(&sentinel_pos)
|
||
.arg(&sentinel_neg)
|
||
.arg(&safety_factor)
|
||
.arg(&neg_to_pos_ratio)
|
||
.arg(&pos_min)
|
||
.arg(&pos_max)
|
||
.arg(&alpha)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (Self::BLK_DIM, 1, 1),
|
||
shared_mem_bytes: smem_bytes,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("reward_cap_update: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Class A P1-Producer (2026-05-08): adaptive Bayesian Kelly priors producer.
|
||
///
|
||
/// Replaces the previously-hardcoded `prior_wins=2.0f, prior_losses=2.0f,
|
||
/// prior_sum_wins=0.01f, prior_sum_losses=0.01f` constants used in
|
||
/// `kelly_cap_update_kernel.cu` and `trade_physics.cuh::kelly_position_cap`
|
||
/// with ISV-driven slow-EMA values fed by the
|
||
/// `kelly_bayesian_priors_update_kernel` producer at per-fold-end (per-epoch
|
||
/// boundary in the current scheduling). Aggregates the realized
|
||
/// PS_KELLY_{WIN_COUNT, LOSS_COUNT, SUM_WINS, SUM_LOSSES} fields across all
|
||
/// envs from the same `portfolio_state[n_envs, PS_STRIDE]` buffer that
|
||
/// `kelly_cap_update_kernel` reads from at the same boundary, then
|
||
/// slow-EMA-blends into ISV[KELLY_PRIOR_*_INDEX] = ISV[454..458).
|
||
///
|
||
/// Single-block 256-thread kernel; shmem block-tree-reduce
|
||
/// (no atomicAdd per `feedback_no_atomicadd.md`). `CudaFunction` pre-loaded
|
||
/// at construction per `pearl_no_host_branches_in_captured_graph.md`.
|
||
///
|
||
/// Pearls applied:
|
||
/// - `feedback_no_atomicadd.md` — block-tree-reduce in shmem, single
|
||
/// global write per slot from thread 0.
|
||
/// - `pearl_first_observation_bootstrap.md` — sentinel match (within 1e-6
|
||
/// of pre-P1-Producer hardcoded value) → REPLACE; otherwise EMA blend.
|
||
/// - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||
/// pre-loaded at construction; on-device guard for "no realized trades".
|
||
/// - `feedback_no_stubs.md` — full body, no placeholder.
|
||
/// - `feedback_isv_for_adaptive_bounds.md` — count bounds [0.5, 100],
|
||
/// sum bounds [0.001, 1.0] are dimensional safety floors, NOT tuning.
|
||
/// - `pearl_symmetric_clamp_audit.md` — bilateral
|
||
/// `fmaxf(lo, fminf(x, hi))` clamp on each slot before writing.
|
||
/// - `pearl_controller_anchors_isv_driven.md` — every controller anchor
|
||
/// (priors are anchors for the Kelly cap formula) is ISV-driven.
|
||
#[allow(missing_debug_implementations)]
|
||
pub(crate) struct KellyBayesianPriorsUpdateOps {
|
||
update_kernel: CudaFunction,
|
||
}
|
||
|
||
impl KellyBayesianPriorsUpdateOps {
|
||
/// Block dim used by the producer kernel — must match `BLK_DIM` in
|
||
/// `kelly_bayesian_priors_update_kernel.cu`.
|
||
const BLK_DIM: u32 = 256;
|
||
|
||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||
let context = stream.context();
|
||
let module = context
|
||
.load_cubin(KELLY_BAYESIAN_PRIORS_UPDATE_CUBIN.to_vec())
|
||
.map_err(|e| MLError::ModelError(format!("kelly_bayesian_priors_update cubin load: {e}")))?;
|
||
let update_kernel = module
|
||
.load_function("kelly_bayesian_priors_update")
|
||
.map_err(|e| MLError::ModelError(format!("kelly_bayesian_priors_update load: {e}")))?;
|
||
Ok(Self { update_kernel })
|
||
}
|
||
|
||
/// Launch the adaptive Bayesian-priors producer.
|
||
///
|
||
/// Args:
|
||
/// - `portfolio_state_ptr`: f32 device ptr `[n_envs * ps_stride]` —
|
||
/// the same buffer `kelly_cap_update_kernel` reads from. We use
|
||
/// the four PS_KELLY_* fields per env.
|
||
/// - `n_envs`: number of envs (alloc_episodes from the collector).
|
||
/// - `ps_stride`: PS_STRIDE from state_layout.cuh (43 post-Plan-3-D.4c).
|
||
/// - `isv_ptr`: ISV[ISV_TOTAL_DIM] device pointer (mapped-pinned).
|
||
/// - `wins_idx`/`losses_idx`/`sum_wins_idx`/`sum_losses_idx`:
|
||
/// KELLY_PRIOR_*_INDEX (454..458).
|
||
/// - `sentinel_*`: SENTINEL_KELLY_PRIOR_* (2.0/2.0/0.01/0.01).
|
||
/// - `count_min`/`count_max`: KELLY_PRIOR_COUNT_MIN/MAX (0.5, 100).
|
||
/// - `sum_min`/`sum_max`: KELLY_PRIOR_SUM_MIN/MAX (0.001, 1.0).
|
||
/// - `alpha`: KELLY_PRIOR_EMA_ALPHA (0.005, slow per-fold cadence).
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn launch(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
portfolio_state_ptr: u64,
|
||
n_envs: i32,
|
||
ps_stride: i32,
|
||
isv_ptr: u64,
|
||
wins_idx: i32,
|
||
losses_idx: i32,
|
||
sum_wins_idx: i32,
|
||
sum_losses_idx: i32,
|
||
sentinel_wins: f32,
|
||
sentinel_losses: f32,
|
||
sentinel_sum_wins: f32,
|
||
sentinel_sum_losses: f32,
|
||
count_min: f32,
|
||
count_max: f32,
|
||
sum_min: f32,
|
||
sum_max: f32,
|
||
alpha: f32,
|
||
) -> Result<(), MLError> {
|
||
// 4 arrays × BLK_DIM × sizeof(f32) — see the kernel comment block.
|
||
let smem_bytes = 4 * Self::BLK_DIM * std::mem::size_of::<f32>() as u32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.update_kernel)
|
||
.arg(&portfolio_state_ptr)
|
||
.arg(&n_envs)
|
||
.arg(&ps_stride)
|
||
.arg(&isv_ptr)
|
||
.arg(&wins_idx)
|
||
.arg(&losses_idx)
|
||
.arg(&sum_wins_idx)
|
||
.arg(&sum_losses_idx)
|
||
.arg(&sentinel_wins)
|
||
.arg(&sentinel_losses)
|
||
.arg(&sentinel_sum_wins)
|
||
.arg(&sentinel_sum_losses)
|
||
.arg(&count_min)
|
||
.arg(&count_max)
|
||
.arg(&sum_min)
|
||
.arg(&sum_max)
|
||
.arg(&alpha)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (Self::BLK_DIM, 1, 1),
|
||
shared_mem_bytes: smem_bytes,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("kelly_bayesian_priors_update: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Class A audit-fix Batch 4-A (2026-05-08): adaptive DD saturation floor producer.
|
||
///
|
||
/// Sweeps the per-env `dd_state_per_env[n_envs * 6]` tile (populated by
|
||
/// `dd_state_kernel` at the same boundary), aggregates DD_MAX (offset 1
|
||
/// within each env's tile) across envs, computes a Welford `mean +
|
||
/// Z_75 × sigma` p75 estimator with a `max(p75, mean)` robustness guard,
|
||
/// applies a 1.5× safety factor, clamps to dimensional bounds [0.10, 0.50],
|
||
/// and writes `ISV[DD_SATURATION_FLOOR_ADAPTIVE_INDEX=458]`. Replaces the
|
||
/// hardcoded `0.25f` saturation floor in `trade_physics.cuh::apply_margin_cap`
|
||
/// (the upper end of the linear position-size scaling ramp).
|
||
///
|
||
/// Single-block kernel; block-tree-reduce over the per-env tile (no
|
||
/// atomicAdd per `feedback_no_atomicadd.md`). Pearl-A first-observation
|
||
/// bootstrap (sentinel 0.25 matches pre-fix hardcoded constant for
|
||
/// bit-identical cold-start); α=0.01 slow EMA thereafter (DD distribution
|
||
/// is a slow-moving fold-volatility property — same cadence as the P0-A
|
||
/// REWARD_POS_CAP producer).
|
||
///
|
||
/// Per-epoch boundary launch — DD distribution shouldn't move fast.
|
||
///
|
||
/// # Pearls applied
|
||
/// - `feedback_no_atomicadd.md` — single block, block-tree-reduce in shmem.
|
||
/// - `pearl_first_observation_bootstrap.md` — sentinel 0.25 → REPLACE.
|
||
/// - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||
/// pre-loaded at construction; on-device guards.
|
||
/// - `pearl_symmetric_clamp_audit.md` — bilateral `fmaxf(lo, fminf(x, hi))`.
|
||
/// - `feedback_isv_for_adaptive_bounds.md` — bounds [0.10, 0.50] are
|
||
/// Category-1 dimensional safety, NOT tuning.
|
||
#[allow(missing_debug_implementations)]
|
||
pub(crate) struct DdSaturationFloorUpdateOps {
|
||
update_kernel: CudaFunction,
|
||
}
|
||
|
||
impl DdSaturationFloorUpdateOps {
|
||
/// Block dim used by the producer kernel — must match `BLK_DIM` in
|
||
/// `dd_saturation_floor_update_kernel.cu`.
|
||
const BLK_DIM: u32 = 256;
|
||
|
||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||
let context = stream.context();
|
||
let module = context
|
||
.load_cubin(DD_SATURATION_FLOOR_UPDATE_CUBIN.to_vec())
|
||
.map_err(|e| MLError::ModelError(format!("dd_saturation_floor_update cubin load: {e}")))?;
|
||
let update_kernel = module
|
||
.load_function("dd_saturation_floor_update")
|
||
.map_err(|e| MLError::ModelError(format!("dd_saturation_floor_update load: {e}")))?;
|
||
Ok(Self { update_kernel })
|
||
}
|
||
|
||
/// Launch the adaptive DD saturation floor producer.
|
||
///
|
||
/// Args:
|
||
/// - `dd_state_per_env_ptr`: f32 device ptr `[n_envs * dd_state_stride]`
|
||
/// — the same buffer `compute_sp15_final_reward_kernel` reads from.
|
||
/// We use the per-env DD_MAX field at offset `dd_max_off`.
|
||
/// - `n_envs`: number of envs (alloc_episodes from the collector).
|
||
/// - `dd_state_stride`: 6 (mirrors the per-env tile layout in
|
||
/// `dd_state_kernel`).
|
||
/// - `dd_max_off`: 1 (DD_MAX offset within each env's tile).
|
||
/// - `isv_ptr`: ISV[ISV_TOTAL_DIM] device pointer (mapped-pinned).
|
||
/// - `floor_idx`: DD_SATURATION_FLOOR_ADAPTIVE_INDEX (458).
|
||
/// - `sentinel_floor`: SENTINEL_DD_SATURATION_FLOOR (0.25).
|
||
/// - `safety_factor`: DD_SATURATION_FLOOR_SAFETY_FACTOR (1.5).
|
||
/// - `floor_min`/`floor_max`: DD_SATURATION_FLOOR_MIN/MAX (0.10, 0.50).
|
||
/// - `alpha`: DD_SATURATION_FLOOR_EMA_ALPHA (0.01, slow per-epoch).
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn launch(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
dd_state_per_env_ptr: u64,
|
||
n_envs: i32,
|
||
dd_state_stride: i32,
|
||
dd_max_off: i32,
|
||
isv_ptr: u64,
|
||
floor_idx: i32,
|
||
sentinel_floor: f32,
|
||
safety_factor: f32,
|
||
floor_min: f32,
|
||
floor_max: f32,
|
||
alpha: f32,
|
||
) -> Result<(), MLError> {
|
||
// 4 arrays × BLK_DIM × sizeof(f32) — see the kernel comment block.
|
||
let smem_bytes = 4 * Self::BLK_DIM * std::mem::size_of::<f32>() as u32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.update_kernel)
|
||
.arg(&dd_state_per_env_ptr)
|
||
.arg(&n_envs)
|
||
.arg(&dd_state_stride)
|
||
.arg(&dd_max_off)
|
||
.arg(&isv_ptr)
|
||
.arg(&floor_idx)
|
||
.arg(&sentinel_floor)
|
||
.arg(&safety_factor)
|
||
.arg(&floor_min)
|
||
.arg(&floor_max)
|
||
.arg(&alpha)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (Self::BLK_DIM, 1, 1),
|
||
shared_mem_bytes: smem_bytes,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("dd_saturation_floor_update: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// SP16 Phase 1 (revised, 2026-05-08): adaptive MIN_HOLD_TEMPERATURE
|
||
/// producer driven by hold-rate overrun (signal swap from slot 373).
|
||
///
|
||
/// Single-thread cold-path kernel (per-epoch boundary cadence) that
|
||
/// reads two SP13 hold-pricing chain slots from ISV:
|
||
///
|
||
/// - `ISV[HOLD_RATE_OBSERVED_EMA_INDEX=382]` — per-fold EMA of the
|
||
/// per-step Hold-pick rate, populated by `apply_fixed_alpha_ema_kernel`
|
||
/// chained off `hold_rate_observer_kernel`.
|
||
/// - `ISV[HOLD_RATE_TARGET_INDEX=381]` — static target (default 0.20).
|
||
///
|
||
/// And maps the overrun into a [5, 50] temperature:
|
||
///
|
||
/// overrun = max(0, observed − target)
|
||
/// overrun_norm = clamp(overrun / max(target, 0.01), 0, 1)
|
||
/// temp = TEMP_MIN + (TEMP_MAX − TEMP_MIN) × overrun_norm
|
||
///
|
||
/// then slow-EMA-blends into
|
||
/// `ISV[MIN_HOLD_TEMPERATURE_ADAPTIVE_INDEX=460]` with α=0.05.
|
||
///
|
||
/// Per `compute_min_hold_penalty` at trade_physics.cuh:575
|
||
/// (`soft_factor = deficit / (deficit + T)`), HIGH T = forgiving
|
||
/// (saturates → 0, no exit penalty), LOW T = sharp (saturates → 1,
|
||
/// max exit penalty). The hold-rate-overrun mapping therefore gives
|
||
/// the over-holding scenario its designed behaviour:
|
||
/// - observed = 2× target → overrun_norm = 1.0 → temp = 50
|
||
/// (permissive — exit ramp out of over-holding)
|
||
/// - observed = target → overrun_norm = 0 → temp = 5
|
||
/// (strict — force commitment at the hold-rate target)
|
||
/// - observed < target → overrun_norm = 0 → temp = 5
|
||
/// (under-holding — strict, no relaxation)
|
||
///
|
||
/// Signal swap rationale (post-mortem of train-multi-seed-pfh9n,
|
||
/// 2026-05-08): the original mapping read
|
||
/// `ISV[AUX_DIR_ACC_SHORT_EMA_INDEX=373]` but slot 373 stayed at
|
||
/// sentinel 0.5 in Fold 1 — the per-step aux producer either didn't
|
||
/// push it off sentinel quickly enough or the binary classifier
|
||
/// converged within ε of 0.5 in noisy environments. The kernel's
|
||
/// early-return-on-sentinel guard then fired on every launch and slot
|
||
/// 460 stayed pinned at 50 for the entire fold. Hold rates in
|
||
/// contrast are measured per-epoch from realised actions and never at
|
||
/// a "no-data" sentinel after epoch 1, so the fold-1 chain breakage
|
||
/// goes away.
|
||
///
|
||
/// Cold-start sentinel guard preserved: when `observed_hold_rate` is
|
||
/// at the SP13 fold-reset sentinel 0.0 (no per-step Hold observations
|
||
/// yet in the new fold), the kernel keeps slot 460 unchanged — the
|
||
/// consumer falls back to MIN_HOLD_TEMPERATURE_FALLBACK=50.0 via the
|
||
/// same sentinel-detect path used pre-swap.
|
||
///
|
||
/// # Pearls applied
|
||
/// - `feedback_no_atomicadd.md` — single-thread kernel; no reductions.
|
||
/// - `pearl_first_observation_bootstrap.md` — sentinel 50.0 → REPLACE.
|
||
/// - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||
/// pre-loaded at construction; on-device guards.
|
||
/// - `pearl_symmetric_clamp_audit.md` — bilateral
|
||
/// `fmaxf(lo, fminf(x, hi))` clamp on target_temp + post-blend.
|
||
/// - `feedback_isv_for_adaptive_bounds.md` — bounds [5, 50] are
|
||
/// Category-1 dimensional safety floors (matches the original
|
||
/// schedule range MIN_HOLD_TEMPERATURE_END=5 ↔
|
||
/// MIN_HOLD_TEMPERATURE_START=50), NOT tuning.
|
||
/// - `feedback_no_partial_refactor.md` — kernel + launcher + call site
|
||
/// + tests + audit doc updated atomically in the same commit.
|
||
#[allow(missing_debug_implementations)]
|
||
pub(crate) struct MinHoldTemperatureUpdateOps {
|
||
update_kernel: CudaFunction,
|
||
}
|
||
|
||
impl MinHoldTemperatureUpdateOps {
|
||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||
let context = stream.context();
|
||
let module = context
|
||
.load_cubin(MIN_HOLD_TEMPERATURE_UPDATE_CUBIN.to_vec())
|
||
.map_err(|e| MLError::ModelError(format!("min_hold_temperature_update cubin load: {e}")))?;
|
||
let update_kernel = module
|
||
.load_function("min_hold_temperature_update")
|
||
.map_err(|e| MLError::ModelError(format!("min_hold_temperature_update load: {e}")))?;
|
||
Ok(Self { update_kernel })
|
||
}
|
||
|
||
/// Launch the adaptive MIN_HOLD_TEMPERATURE producer (SP16 Phase 1
|
||
/// revised — hold-rate-overrun driven; SP16 T3 — Wiener-optimal α).
|
||
///
|
||
/// Args:
|
||
/// - `isv_ptr`: ISV[ISV_TOTAL_DIM] device pointer (mapped-pinned).
|
||
/// - `temp_idx`: MIN_HOLD_TEMPERATURE_ADAPTIVE_INDEX (460).
|
||
/// - `hold_rate_observed_idx`: HOLD_RATE_OBSERVED_EMA_INDEX (382).
|
||
/// - `hold_rate_target_idx`: HOLD_RATE_TARGET_INDEX (381).
|
||
/// - Welford accumulator slot indices (T3): MHT_TARGET_MEAN_INDEX
|
||
/// (468), MHT_TARGET_M2_INDEX (469), MHT_DIFF_MEAN_INDEX (470),
|
||
/// MHT_DIFF_M2_INDEX (471), MHT_PREV_TARGET_INDEX (472),
|
||
/// MHT_SAMPLE_COUNT_INDEX (473).
|
||
/// - `sentinel_temp`: SENTINEL_MIN_HOLD_TEMPERATURE (50.0).
|
||
/// - `sentinel_observed_hold_rate`: SENTINEL_HOLD_RATE_OBSERVED
|
||
/// (0.0; matches the SP13 fold-reset registry sentinel for
|
||
/// `sp13_hold_rate_observed_ema`).
|
||
/// - `temp_min`/`temp_max`: MIN_HOLD_TEMPERATURE_MIN/MAX (5.0, 50.0).
|
||
///
|
||
/// SP16 T3: the `alpha` argument is gone — α is computed inside the
|
||
/// kernel from the Welford accumulators (Wiener-optimal: α = diff_var
|
||
/// / (diff_var + sample_var + ε)).
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn launch(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
isv_ptr: u64,
|
||
temp_idx: i32,
|
||
hold_rate_observed_idx: i32,
|
||
hold_rate_target_idx: i32,
|
||
target_mean_idx: i32,
|
||
target_m2_idx: i32,
|
||
diff_mean_idx: i32,
|
||
diff_m2_idx: i32,
|
||
prev_target_idx: i32,
|
||
sample_count_idx: i32,
|
||
sentinel_temp: f32,
|
||
sentinel_observed_hold_rate: f32,
|
||
temp_min: f32,
|
||
temp_max: f32,
|
||
) -> Result<(), MLError> {
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.update_kernel)
|
||
.arg(&isv_ptr)
|
||
.arg(&temp_idx)
|
||
.arg(&hold_rate_observed_idx)
|
||
.arg(&hold_rate_target_idx)
|
||
.arg(&target_mean_idx)
|
||
.arg(&target_m2_idx)
|
||
.arg(&diff_mean_idx)
|
||
.arg(&diff_m2_idx)
|
||
.arg(&prev_target_idx)
|
||
.arg(&sample_count_idx)
|
||
.arg(&sentinel_temp)
|
||
.arg(&sentinel_observed_hold_rate)
|
||
.arg(&temp_min)
|
||
.arg(&temp_max)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (1, 1, 1),
|
||
shared_mem_bytes: 0,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("min_hold_temperature_update: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// SP14 Layer C Phase C.6 (2026-05-08): h_s2_aux RMS EMA producer.
|
||
///
|
||
/// Computes `RMS(h_s2_aux) = sqrt(mean(h_s2_aux²))` over the
|
||
/// `[B, SH2]` aux trunk output and EMA-blends the result into
|
||
/// `ISV[H_S2_AUX_RMS_EMA_INDEX=449]`.
|
||
///
|
||
/// ISV slot 449 is in the SP14 Layer C block (outside the SP4/SP5 wiener
|
||
/// buffer linear span), so Pearl-A bootstrap logic is embedded in the
|
||
/// kernel body rather than delegated to `apply_pearls_ad_kernel`. Fixed
|
||
/// α=0.05 (per-step cadence blend rate; no Wiener-optimal adaptive α
|
||
/// available without a wiener_state_buf triple for this slot).
|
||
///
|
||
/// Single-block 256-thread kernel; shmem block-tree-reduce (no atomicAdd
|
||
/// per `feedback_no_atomicadd.md`). `CudaFunction` pre-loaded at
|
||
/// construction per `pearl_no_host_branches_in_captured_graph.md`.
|
||
///
|
||
/// # Pearls applied
|
||
/// - `feedback_no_atomicadd.md` — block-tree-reduce in shmem, one global
|
||
/// write from thread 0.
|
||
/// - `pearl_first_observation_bootstrap.md` — sentinel 0.0 → REPLACE.
|
||
/// - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||
/// pre-loaded at construction; no per-launch cubin load.
|
||
/// - `feedback_no_stubs.md` — full body, no placeholder.
|
||
#[allow(missing_debug_implementations)]
|
||
pub(crate) struct HS2AuxRmsEmaOps {
|
||
update_kernel: CudaFunction,
|
||
}
|
||
|
||
impl HS2AuxRmsEmaOps {
|
||
/// Block dim — must match the kernel's `BLK_DIM` of 256.
|
||
const BLK_DIM: u32 = 256;
|
||
/// Per-step EMA blend rate. Matches the aux trunk forward launch
|
||
/// cadence — one observation per collector step.
|
||
pub(crate) const ALPHA: f32 = 0.05;
|
||
|
||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||
let context = stream.context();
|
||
let module = context
|
||
.load_cubin(H_S2_AUX_RMS_EMA_CUBIN.to_vec())
|
||
.map_err(|e| MLError::ModelError(format!("h_s2_aux_rms_ema cubin load: {e}")))?;
|
||
let update_kernel = module
|
||
.load_function("h_s2_aux_rms_ema_update")
|
||
.map_err(|e| MLError::ModelError(format!("h_s2_aux_rms_ema_update load: {e}")))?;
|
||
Ok(Self { update_kernel })
|
||
}
|
||
|
||
/// Launch the RMS EMA producer.
|
||
///
|
||
/// Args:
|
||
/// - `h_s2_aux_ptr`: aux trunk output `[B, SH2]` device pointer.
|
||
/// - `b`: batch size (B).
|
||
/// - `sh2`: aux trunk output width (SH2 = AUX_HIDDEN_DIM = 256).
|
||
/// - `isv_ptr`: ISV[ISV_TOTAL_DIM] device pointer (mapped-pinned).
|
||
/// - `isv_target_idx`: H_S2_AUX_RMS_EMA_INDEX (449).
|
||
/// - `alpha`: EMA blend rate (use `Self::ALPHA = 0.05`).
|
||
pub(crate) fn launch(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
h_s2_aux_ptr: u64,
|
||
b: i32,
|
||
sh2: i32,
|
||
isv_ptr: u64,
|
||
isv_target_idx: i32,
|
||
alpha: f32,
|
||
) -> Result<(), MLError> {
|
||
let smem_bytes = Self::BLK_DIM * std::mem::size_of::<f32>() as u32;
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.update_kernel)
|
||
.arg(&h_s2_aux_ptr)
|
||
.arg(&b)
|
||
.arg(&sh2)
|
||
.arg(&isv_ptr)
|
||
.arg(&isv_target_idx)
|
||
.arg(&alpha)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (Self::BLK_DIM, 1, 1),
|
||
shared_mem_bytes: smem_bytes,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("h_s2_aux_rms_ema_update: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// SP16 Phase 2 (2026-05-08): adaptive Hold cost scale producer ops.
|
||
///
|
||
/// Mirrors `MinHoldTemperatureUpdateOps` (same input chain — hold-rate
|
||
/// overrun — same per-epoch cadence — same Pearl-A bootstrap pattern).
|
||
/// The kernel is a single-block single-thread cold-path producer that
|
||
/// reads `ISV[HOLD_RATE_OBSERVED_EMA_INDEX=382]` and
|
||
/// `ISV[HOLD_RATE_TARGET_INDEX=381]`, maps overrun to a [1, 25]
|
||
/// multiplier, and Pearl-A-bootstrap + α=0.05 EMA blends into
|
||
/// `ISV[HOLD_COST_SCALE_INDEX=461]`.
|
||
///
|
||
/// # Why this struct exists separately from MinHoldTemperatureUpdateOps
|
||
///
|
||
/// Both producers read the SAME inputs (slots 382 + 381) and run at the
|
||
/// SAME cadence (per-epoch boundary), but write to DIFFERENT outputs
|
||
/// (slot 460 — temperature in [5, 50] vs slot 461 — scale in [1, 25]).
|
||
/// Combining them into a single fused kernel was rejected because:
|
||
/// 1. The two outputs are downstream of two semantically-distinct
|
||
/// consumer paths (min-hold soft saturation vs Hold cost
|
||
/// magnitude); fusing them couples kernels that should be
|
||
/// independently auditable.
|
||
/// 2. Per-epoch launch overhead is negligible (single-thread cold
|
||
/// kernel), so the fusion buys no measurable perf.
|
||
/// 3. Mirrors the SP14-P0-A and SP14-P1-Producer split — ONE producer
|
||
/// kernel per ISV slot, ONE wiring point per consumer.
|
||
///
|
||
/// # Pearls applied
|
||
/// - `feedback_no_atomicadd.md` — single-thread kernel; no reductions.
|
||
/// - `pearl_first_observation_bootstrap.md` — sentinel 0.0 → REPLACE.
|
||
/// - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||
/// pre-loaded at construction; on-device guards.
|
||
/// - `pearl_symmetric_clamp_audit.md` — bilateral
|
||
/// `fmaxf(lo, fminf(x, hi))` clamp on target_scale + post-blend.
|
||
/// - `feedback_isv_for_adaptive_bounds.md` — bounds [1, 25] are
|
||
/// Category-1 dimensional safety floors (NOT tuning).
|
||
/// - `feedback_no_partial_refactor.md` — kernel + launcher + 3 consumer
|
||
/// sites + tests + audit doc updated atomically in the same commit.
|
||
#[allow(missing_debug_implementations)]
|
||
pub(crate) struct HoldCostScaleUpdateOps {
|
||
update_kernel: CudaFunction,
|
||
}
|
||
|
||
impl HoldCostScaleUpdateOps {
|
||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||
let context = stream.context();
|
||
let module = context
|
||
.load_cubin(HOLD_COST_SCALE_UPDATE_CUBIN.to_vec())
|
||
.map_err(|e| MLError::ModelError(format!("hold_cost_scale_update cubin load: {e}")))?;
|
||
let update_kernel = module
|
||
.load_function("hold_cost_scale_update")
|
||
.map_err(|e| MLError::ModelError(format!("hold_cost_scale_update load: {e}")))?;
|
||
Ok(Self { update_kernel })
|
||
}
|
||
|
||
/// Launch the adaptive Hold cost scale producer (SP16 Phase 2 +
|
||
/// T3 Wiener-optimal α).
|
||
///
|
||
/// Args:
|
||
/// - `isv_ptr`: ISV[ISV_TOTAL_DIM] device pointer (mapped-pinned).
|
||
/// - `scale_idx`: HOLD_COST_SCALE_INDEX (461).
|
||
/// - `hold_rate_observed_idx`: HOLD_RATE_OBSERVED_EMA_INDEX (382).
|
||
/// - `hold_rate_target_idx`: HOLD_RATE_TARGET_INDEX (381).
|
||
/// - Welford accumulator slot indices (T3): HCS_TARGET_MEAN_INDEX
|
||
/// (462), HCS_TARGET_M2_INDEX (463), HCS_DIFF_MEAN_INDEX (464),
|
||
/// HCS_DIFF_M2_INDEX (465), HCS_PREV_TARGET_INDEX (466),
|
||
/// HCS_SAMPLE_COUNT_INDEX (467).
|
||
/// - `sentinel_scale`: SENTINEL_HOLD_COST_SCALE (0.0).
|
||
/// - `sentinel_observed_hold_rate`: SENTINEL_HOLD_RATE_OBSERVED
|
||
/// (0.0; matches the SP13 fold-reset registry sentinel — shared
|
||
/// by design with SP16-P1).
|
||
/// - `scale_min`/`scale_max`: HOLD_COST_SCALE_MIN/MAX (1.0, 25.0).
|
||
///
|
||
/// SP16 T3: the `alpha` argument is gone — α is computed inside the
|
||
/// kernel from the Welford accumulators (Wiener-optimal: α = diff_var
|
||
/// / (diff_var + sample_var + ε)).
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn launch(
|
||
&self,
|
||
stream: &Arc<CudaStream>,
|
||
isv_ptr: u64,
|
||
scale_idx: i32,
|
||
hold_rate_observed_idx: i32,
|
||
hold_rate_target_idx: i32,
|
||
target_mean_idx: i32,
|
||
target_m2_idx: i32,
|
||
diff_mean_idx: i32,
|
||
diff_m2_idx: i32,
|
||
prev_target_idx: i32,
|
||
sample_count_idx: i32,
|
||
sentinel_scale: f32,
|
||
sentinel_observed_hold_rate: f32,
|
||
scale_min: f32,
|
||
scale_max: f32,
|
||
) -> Result<(), MLError> {
|
||
unsafe {
|
||
stream
|
||
.launch_builder(&self.update_kernel)
|
||
.arg(&isv_ptr)
|
||
.arg(&scale_idx)
|
||
.arg(&hold_rate_observed_idx)
|
||
.arg(&hold_rate_target_idx)
|
||
.arg(&target_mean_idx)
|
||
.arg(&target_m2_idx)
|
||
.arg(&diff_mean_idx)
|
||
.arg(&diff_m2_idx)
|
||
.arg(&prev_target_idx)
|
||
.arg(&sample_count_idx)
|
||
.arg(&sentinel_scale)
|
||
.arg(&sentinel_observed_hold_rate)
|
||
.arg(&scale_min)
|
||
.arg(&scale_max)
|
||
.launch(LaunchConfig {
|
||
grid_dim: (1, 1, 1),
|
||
block_dim: (1, 1, 1),
|
||
shared_mem_bytes: 0,
|
||
})
|
||
.map_err(|e| MLError::ModelError(format!("hold_cost_scale_update: {e}")))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|