feat(sp15-wave4.1a): bn_tanh_concat appends dd_pct column from ISV — bottleneck-aware Phase 1.5 consumer migration
The standalone dd_pct_concat_kernel from Phase 1.5 was bottleneck-
incompatible — it operated on raw [B, 128] state, but production trunk
consumes [B, s1_input_dim] = [B, 102] post-bottleneck. Wave 4.1a fixes
this at the kernel level; Wave 4.1b lands the consumer migration
(s1_input_dim 102→103, GRN w_s1 reshape, 3 forward + 3 backward sites).
Spec correction (per feedback_trust_code_not_docs): the spec's
'state_dim 48→49' is stale terminology pre-STATE_DIM 48→112→128
evolution. Production s1_input_dim is bottleneck_dim + (STATE_DIM −
market_dim) = 16 + (128 − 42) = 102. Wave 4.1b will bump this to 103.
NEW bn_tanh_concat_dd_kernel in dqn_utility_kernels.cu:
- Fuses dd_pct append into the same launch as bn_tanh + portfolio
concat (output shape [B, bn_dim + portfolio_dim + 1])
- Reads isv[DD_PCT_INDEX=406] (set by Wave 1.3.b dd_state_kernel
per-step), broadcasts the scalar across batch as the appended
last column
DELETED standalone dd_pct_concat_kernel.cu + launch_sp15_dd_pct_concat
+ cubin manifest entry per feedback_no_legacy_aliases (zero production
callers — only test consumer; bottleneck-on path is canonical).
Test helpers added (used by Wave 4.1c behavioral KL test).
Phase 1.5 oracle test migrated to bn_tanh_concat_dd_kernel contract:
test name bn_tanh_concat_dd_kernel_writes_dd_pct_column passes on
RTX 3050 Ti.
Layout fingerprint already covers Phase 1.5 via the existing
TRUNK_INPUT_DD_PCT=sp15_phase_1_5; marker — pre-SP15 checkpoints
already break.
fxcache schema_hash auto-bumps from file content hashes (per task
P5T5 Phase F mechanism); no manual schema bump needed.
Atomic per feedback_no_partial_refactor for the kernel-signature
contract change. Consumer wiring (s1_input_dim propagation, GRN
reshape, forward/backward call sites) deferred to Wave 4.1b's atomic
commit per the established 3a/3b split precedent — kernel + launcher
land first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -885,19 +885,17 @@ fn main() {
|
||||
// single-source-of-truth for the action→direction→position
|
||||
// mapping (mirrors `trade_physics.cuh::decode_direction_4b`).
|
||||
"position_history_derivation_kernel.cu",
|
||||
// SP15 Phase 1.5 (2026-05-06): dd_pct foundational state input
|
||||
// concat. LAYOUT FINGERPRINT BREAK — pre-SP15 checkpoints WILL
|
||||
// NOT LOAD after this commit. Greenfield OK per spec Q1. Per
|
||||
// spec §6.5: produces a [B, state_dim_padded + 1] concat buffer
|
||||
// whose last column is `isv[DD_PCT_INDEX=406]` broadcast across
|
||||
// batch. Eval-time policy SEES drawdown context on every forward
|
||||
// pass; Phase 3 teachings can condition on dd_pct directly via
|
||||
// state, not just reward modulation. Phase 1.5 lands kernel +
|
||||
// launcher + layout fingerprint marker only; trunk consumer
|
||||
// migration (re-pointing forward_online to consume the concat
|
||||
// buffer + bumping s1_input_dim) lands as a follow-up atomic
|
||||
// commit per the established Phase 1.1-1.4 precedent.
|
||||
"dd_pct_concat_kernel.cu",
|
||||
// SP15 Wave 4.1a (2026-05-06): the standalone
|
||||
// `dd_pct_concat_kernel.cu` from Phase 1.5 was DELETED. The
|
||||
// standalone kernel was bottleneck-incompatible — it operated on
|
||||
// raw `[B, state_dim_padded=128]` state but production trunk
|
||||
// consumes `[B, s1_input_dim=102]` post-bottleneck. Wave 4.1a
|
||||
// adds a bottleneck-aware extension `bn_tanh_concat_dd_kernel`
|
||||
// INSIDE `dqn_utility_kernels.cu` that fuses the dd_pct append
|
||||
// into the same launch as the bn_tanh + portfolio concat
|
||||
// (output shape `[B, bn_dim + portfolio_dim + 1]`); Wave 4.1b
|
||||
// lands the consumer migration (s1_input_dim 102→103, GRN w_s1
|
||||
// reshape, 3 forward + 3 backward call-site updates).
|
||||
// SP15 Wave 2 (2026-05-06): per-step ISV-driven α producer.
|
||||
// Replaces the pre-Wave-2 `r_quality_discipline_split_kernel.cu`
|
||||
// which contained both the scalar composer
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// crates/ml/src/cuda_pipeline/dd_pct_concat_kernel.cu
|
||||
//
|
||||
// SP15 Phase 1.5 — dd_pct foundational state input concat kernel.
|
||||
//
|
||||
// LAYOUT FINGERPRINT BREAK: pre-SP15 checkpoints WILL NOT LOAD after this
|
||||
// commit. Greenfield OK per spec Q1.
|
||||
//
|
||||
// Per spec §6.5: dd_pct (slot 406, written by Task 1.3's dd_state_kernel)
|
||||
// gets concatenated to the trunk forward input as the last dim. Eval-time
|
||||
// policy SEES drawdown context on every forward pass; Phase 3 teachings
|
||||
// can condition on dd_pct directly via state, not just reward modulation.
|
||||
//
|
||||
// Kernel contract (Pearl: feedback_no_partial_refactor):
|
||||
// Input: states_buf [B, state_dim_padded] — existing trunk input
|
||||
// isv[DD_PCT_INDEX=406] — dd_pct ∈ [0, 1] (broadcast)
|
||||
// B, state_dim_padded
|
||||
// Output: concat_buf [B, state_dim_padded + 1] — pre-allocated
|
||||
// concat_buf[b, 0..state_dim_padded] = states_buf[b, 0..state_dim_padded]
|
||||
// concat_buf[b, state_dim_padded] = isv[DD_PCT_INDEX]
|
||||
//
|
||||
// The kernel produces the concatenated buffer; trunk consumer migration
|
||||
// (re-pointing forward_online to consume concat_buf instead of states_buf
|
||||
// and bumping s1_input_dim accordingly) lands as a follow-up atomic
|
||||
// commit per the established Phase 1 precedent (Tasks 1.1-1.4 all defer
|
||||
// consumer migration to follow-up commits per feedback_no_partial_refactor).
|
||||
//
|
||||
// Layout: row-major [B, state_dim_padded] for input; row-major
|
||||
// [B, state_dim_padded + 1] for output. The +1 last column is the
|
||||
// dd_pct broadcast slot. No atomicAdd (pure scatter-copy).
|
||||
|
||||
extern "C" __global__ void dd_pct_concat_kernel(
|
||||
const float* __restrict__ states_buf,
|
||||
const float* __restrict__ isv,
|
||||
float* __restrict__ concat_buf,
|
||||
int batch_size,
|
||||
int state_dim_padded
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
const int tid = threadIdx.x;
|
||||
const int n_tid = blockDim.x;
|
||||
|
||||
if (b >= batch_size) return;
|
||||
|
||||
const float dd_pct = isv[406]; // DD_PCT_INDEX (compile-time constant)
|
||||
|
||||
const int out_stride = state_dim_padded + 1;
|
||||
const int in_offset = b * state_dim_padded;
|
||||
const int out_offset = b * out_stride;
|
||||
|
||||
// 1. Copy the existing state vector — full padded width (including
|
||||
// pre-existing pad zeros from the producer).
|
||||
for (int i = tid; i < state_dim_padded; i += n_tid) {
|
||||
concat_buf[out_offset + i] = states_buf[in_offset + i];
|
||||
}
|
||||
|
||||
// 2. Last column = dd_pct broadcast. Single thread writes the +1 slot.
|
||||
if (tid == 0) {
|
||||
concat_buf[out_offset + state_dim_padded] = dd_pct;
|
||||
}
|
||||
}
|
||||
@@ -1384,6 +1384,90 @@ extern "C" __global__ void bn_tanh_concat_f32_kernel(
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* SP15 Wave 4.1a (2026-05-06): BOTTLENECK TANH + CONCAT + DD_PCT KERNEL
|
||||
*
|
||||
* Bottleneck-aware extension of `bn_tanh_concat_kernel`. Same forward
|
||||
* computation (tanh on bn_hidden in-place + portfolio passthrough), with
|
||||
* one additional output column appended that broadcasts
|
||||
* `isv[DD_PCT_INDEX=406]` across the batch dimension.
|
||||
*
|
||||
* The standalone `dd_pct_concat_kernel.cu` from Phase 1.5 was bottleneck-
|
||||
* incompatible — it operated on raw `[B, state_dim_padded=128]` state but
|
||||
* production trunk consumes `[B, s1_input_dim] = [B, bn_dim + portfolio_dim]`
|
||||
* (= 102 with current bottleneck_dim=16, market_dim=42, STATE_DIM=128) post-
|
||||
* bottleneck. This kernel folds the dd_pct append into the same launch as
|
||||
* the bn_tanh + portfolio concat — eval-time policy SEES drawdown context
|
||||
* via state on every forward pass, with the trunk consuming the dd_pct
|
||||
* column as the (s1_input_dim+1)th feature once Wave 4.1b lands the
|
||||
* consumer migration (s1_input_dim 102→103 propagation, GRN w_s1 reshape,
|
||||
* 3 forward + 3 backward call-site updates).
|
||||
*
|
||||
* Kernel contract (mirrors `bn_tanh_concat_kernel` with one extra column):
|
||||
* Inputs: bn_hidden [B, bn_dim] — IN/OUT: tanh in-place
|
||||
* states [B, state_dim_padded] — source for portfolio
|
||||
* isv[DD_PCT_INDEX=406] — dd_pct ∈ [0, 1] broadcast
|
||||
* Output: concat_out [B, concat_dd_dim]
|
||||
* where concat_dd_dim = bn_dim + portfolio_dim + 1
|
||||
* concat_out[b, 0..bn_dim) = tanh(bn_hidden[b, 0..bn_dim))
|
||||
* concat_out[b, bn_dim..bn_dim+pd) = states[b, market_dim..market_dim+pd)
|
||||
* concat_out[b, bn_dim+pd] = isv[DD_PCT_INDEX]
|
||||
*
|
||||
* Caller must supply `concat_dd_dim = concat_dim + 1` (i.e. the OUTPUT
|
||||
* total columns, including the dd_pct append). Pure scatter-copy + one
|
||||
* broadcast column; no atomicAdd; deterministic.
|
||||
*
|
||||
* Wave 4.1a transient state: this kernel + its launcher are currently
|
||||
* ORPHAN — production callers continue to use `bn_tanh_concat_kernel` /
|
||||
* `bn_tanh_concat_f32_kernel` until Wave 4.1b's atomic refactor flips
|
||||
* the 4 launch sites + grows `bn_concat_buf` / `tg_bn_concat_buf` /
|
||||
* `on_next_bn_concat_buf` / experience_collector's `bn_concat` by `+B`
|
||||
* floats and propagates `s1_input_dim+1` through GRN w_s1 + cuBLAS
|
||||
* gemm_caches. Acceptable per the documented Wave 3a/3b split precedent
|
||||
* (kernel + launcher + oracle test verify in isolation first).
|
||||
*
|
||||
* Grid: ceil(B * concat_dd_dim / 256), Block: 256.
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
extern "C" __global__ void bn_tanh_concat_dd_kernel(
|
||||
float* __restrict__ bn_hidden, /* [B, bn_dim] — IN/OUT: tanh applied in-place */
|
||||
const float* __restrict__ states, /* [B, state_dim_padded] — source for portfolio features */
|
||||
const float* __restrict__ isv, /* [ISV_LEN] — bus, reads slot 406 (DD_PCT_INDEX) */
|
||||
float* __restrict__ concat_out, /* [B, concat_dd_dim] — output: [bn_tanh | portfolio | dd_pct] */
|
||||
int batch_size,
|
||||
int bn_dim,
|
||||
int market_dim,
|
||||
int state_dim_padded,
|
||||
int concat_dd_dim /* = bn_dim + portfolio_dim + 1 */
|
||||
) {
|
||||
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
int total = batch_size * concat_dd_dim;
|
||||
if (idx >= total) return;
|
||||
|
||||
int b = idx / concat_dd_dim;
|
||||
int j = idx % concat_dd_dim;
|
||||
|
||||
/* Last column = isv[DD_PCT_INDEX] broadcast across batch.
|
||||
* DD_PCT_INDEX=406 is a compile-time constant matching the Rust
|
||||
* `sp15_isv_slots::DD_PCT_INDEX` and the deleted standalone
|
||||
* `dd_pct_concat_kernel`'s contract. */
|
||||
if (j == concat_dd_dim - 1) {
|
||||
concat_out[idx] = isv[406];
|
||||
return;
|
||||
}
|
||||
|
||||
if (j < bn_dim) {
|
||||
/* Bottleneck dimension: apply tanh and copy to output. */
|
||||
float val = bn_hidden[b * bn_dim + j];
|
||||
val = tanhf(val);
|
||||
bn_hidden[b * bn_dim + j] = val;
|
||||
concat_out[idx] = val;
|
||||
} else {
|
||||
/* Portfolio dimension: copy from states[b, market_dim + (j - bn_dim)]. */
|
||||
int portfolio_idx = market_dim + (j - bn_dim);
|
||||
concat_out[idx] = states[(long long)b * state_dim_padded + portfolio_idx];
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* STOCHASTIC DEPTH RNG KERNEL (#21)
|
||||
*
|
||||
|
||||
@@ -919,78 +919,97 @@ pub fn launch_sp15_dd_state(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SP15 Phase 1.5 (2026-05-06): cubin for the `dd_pct_concat_kernel`.
|
||||
/// SP15 Wave 4.1a (2026-05-06): launcher for the bottleneck-aware
|
||||
/// dd_pct concat extension `bn_tanh_concat_dd_kernel`. Resolves the
|
||||
/// kernel symbol from the existing `dqn_utility_kernels.cubin` (the
|
||||
/// kernel was added next to `bn_tanh_concat_kernel` in the same source
|
||||
/// file) — no separate cubin needed, multi-kernel-per-file pattern
|
||||
/// matching `baseline_kernels.cu`.
|
||||
///
|
||||
/// LAYOUT FINGERPRINT BREAK: pre-SP15 checkpoints WILL NOT LOAD after
|
||||
/// this commit. Greenfield OK per spec Q1.
|
||||
/// this commit (same as Phase 1.5 fingerprint marker
|
||||
/// `TRUNK_INPUT_DD_PCT=sp15_phase_1_5`; the marker stays in
|
||||
/// `layout_fingerprint_seed`). Greenfield OK per spec Q1.
|
||||
///
|
||||
/// Per spec §6.5: produces a `[B, state_dim_padded + 1]` concat buffer
|
||||
/// whose last column is `isv[DD_PCT_INDEX=406]` broadcast across batch.
|
||||
/// The kernel reads the existing `[B, state_dim_padded]` states buffer
|
||||
/// (full padded width, including pre-existing pad zeros from the
|
||||
/// upstream producer), copies it into the concat buffer's leading
|
||||
/// `state_dim_padded` columns, and writes `isv[DD_PCT_INDEX]` to the
|
||||
/// `+1` last column. No atomicAdd (pure scatter-copy).
|
||||
/// Per spec §6.5 (Wave 4.1a correction): the standalone
|
||||
/// `dd_pct_concat_kernel.cu` from Phase 1.5 was DELETED — it operated
|
||||
/// on raw `[B, state_dim_padded=128]` state but production trunk
|
||||
/// consumes `[B, s1_input_dim] = [B, bn_dim + portfolio_dim] = [B, 102]`
|
||||
/// post-bottleneck. The bottleneck-aware extension folds the dd_pct
|
||||
/// append into the same launch as the bn_tanh + portfolio concat;
|
||||
/// output shape is `[B, concat_dd_dim] = [B, bn_dim + portfolio_dim + 1]`.
|
||||
///
|
||||
/// Phase 1.5 lands the kernel + launcher + layout fingerprint marker
|
||||
/// only; trunk consumer migration (re-pointing `forward_online` to
|
||||
/// consume the concat buffer + bumping `s1_input_dim`) is deferred to
|
||||
/// a follow-up atomic commit per `feedback_no_partial_refactor.md` —
|
||||
/// this matches the established Phase 1.1-1.4 precedent (kernels +
|
||||
/// launchers verify in isolation first via the GPU oracle test below).
|
||||
pub static SP15_DD_PCT_CONCAT_CUBIN: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/dd_pct_concat_kernel.cubin"));
|
||||
|
||||
/// SP15 Phase 1.5 (2026-05-06): launcher for the dd_pct concat kernel.
|
||||
/// Free function (matches `launch_sp15_dd_state` and `launch_sp15_*`
|
||||
/// baseline-launcher precedent) so unit/oracle tests can drive the
|
||||
/// kernel directly without the trainer struct; production callers
|
||||
/// invoke the same launcher.
|
||||
/// Wave 4.1a transient state: this launcher is currently ORPHAN —
|
||||
/// production callers continue to use `bn_tanh_concat_kernel` /
|
||||
/// `bn_tanh_concat_f32_kernel` until Wave 4.1b's atomic refactor flips
|
||||
/// the 4 launch sites + grows `bn_concat_buf` / `tg_bn_concat_buf` /
|
||||
/// `on_next_bn_concat_buf` / experience_collector's `bn_concat` by
|
||||
/// `+B` floats and propagates `s1_input_dim+1` through GRN w_s1 +
|
||||
/// cuBLAS gemm_caches. Acceptable per the documented Wave 3a/3b split
|
||||
/// precedent (kernel + launcher + oracle test verify in isolation
|
||||
/// first).
|
||||
///
|
||||
/// Contract:
|
||||
/// `states_buf`: device ptr to `[B, state_dim_padded]` row-major f32.
|
||||
/// `isv`: device ptr to ISV bus (≥443 f32 slots).
|
||||
/// `concat_buf`: device ptr to `[B, state_dim_padded + 1]` row-major
|
||||
/// f32 — caller-allocated, written by the kernel.
|
||||
/// `batch_size`, `state_dim_padded`: dimension scalars.
|
||||
/// `bn_hidden`: device ptr to `[B, bn_dim]` f32 — IN/OUT (tanh
|
||||
/// applied in-place).
|
||||
/// `states`: device ptr to `[B, state_dim_padded]` f32 — source
|
||||
/// for portfolio passthrough columns.
|
||||
/// `isv`: device ptr to ISV bus (≥443 f32 slots) — reads
|
||||
/// slot 406 (DD_PCT_INDEX).
|
||||
/// `concat_out`: device ptr to `[B, concat_dd_dim]` f32 — caller-
|
||||
/// allocated, written by the kernel. Layout:
|
||||
/// `[bn_tanh | portfolio | dd_pct]`.
|
||||
/// `batch_size`, `bn_dim`, `market_dim`, `state_dim_padded`,
|
||||
/// `concat_dd_dim`: dimension scalars. `concat_dd_dim` MUST equal
|
||||
/// `bn_dim + (state_dim - market_dim) + 1`; the
|
||||
/// caller (Wave 4.1b) is responsible for sizing the
|
||||
/// buffer and computing the dim.
|
||||
///
|
||||
/// Grid: `batch_size` blocks, 128 threads per block. Each block copies
|
||||
/// one row of `state_dim_padded` floats and writes the +1 last column
|
||||
/// from `isv[DD_PCT_INDEX=406]`.
|
||||
pub fn launch_sp15_dd_pct_concat(
|
||||
/// Grid: ceil(B * concat_dd_dim / 256), Block: 256.
|
||||
pub fn launch_sp15_bn_concat_dd(
|
||||
stream: &Arc<CudaStream>,
|
||||
states_buf: cudarc::driver::sys::CUdeviceptr,
|
||||
bn_hidden: cudarc::driver::sys::CUdeviceptr,
|
||||
states: cudarc::driver::sys::CUdeviceptr,
|
||||
isv: cudarc::driver::sys::CUdeviceptr,
|
||||
concat_buf: cudarc::driver::sys::CUdeviceptr,
|
||||
concat_out: cudarc::driver::sys::CUdeviceptr,
|
||||
batch_size: i32,
|
||||
bn_dim: i32,
|
||||
market_dim: i32,
|
||||
state_dim_padded: i32,
|
||||
concat_dd_dim: i32,
|
||||
) -> Result<(), MLError> {
|
||||
let module = stream
|
||||
.context()
|
||||
.load_cubin(SP15_DD_PCT_CONCAT_CUBIN.to_vec())
|
||||
.load_cubin(DQN_UTILITY_CUBIN.to_vec())
|
||||
.map_err(|e| MLError::ModelError(format!(
|
||||
"load sp15_dd_pct_concat cubin: {e}"
|
||||
"load dqn_utility_kernels cubin for bn_tanh_concat_dd: {e}"
|
||||
)))?;
|
||||
let kernel = module
|
||||
.load_function("dd_pct_concat_kernel")
|
||||
.load_function("bn_tanh_concat_dd_kernel")
|
||||
.map_err(|e| MLError::ModelError(format!(
|
||||
"load dd_pct_concat_kernel function: {e}"
|
||||
"load bn_tanh_concat_dd_kernel function: {e}"
|
||||
)))?;
|
||||
let total_elems = (batch_size as i64).saturating_mul(concat_dd_dim as i64);
|
||||
let blocks = (((total_elems as u64) + 255) / 256).max(1) as u32;
|
||||
unsafe {
|
||||
stream
|
||||
.launch_builder(&kernel)
|
||||
.arg(&states_buf)
|
||||
.arg(&bn_hidden)
|
||||
.arg(&states)
|
||||
.arg(&isv)
|
||||
.arg(&concat_buf)
|
||||
.arg(&concat_out)
|
||||
.arg(&batch_size)
|
||||
.arg(&bn_dim)
|
||||
.arg(&market_dim)
|
||||
.arg(&state_dim_padded)
|
||||
.arg(&concat_dd_dim)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (batch_size.max(1) as u32, 1, 1),
|
||||
block_dim: (128, 1, 1),
|
||||
grid_dim: (blocks, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
shared_mem_bytes: 0,
|
||||
})
|
||||
.map_err(|e| MLError::ModelError(format!(
|
||||
"launch dd_pct_concat_kernel: {e}"
|
||||
"launch bn_tanh_concat_dd_kernel: {e}"
|
||||
)))?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -3336,7 +3355,7 @@ const fn layout_fingerprint_seed() -> &'static [u8] {
|
||||
DD_TRAJECTORY_DECREASING=439;RECOVERY_OVERSAMPLE_WEIGHT=440;\
|
||||
DD_TRAJECTORY_FLOOR=441;MEDIAN_STREAK_LENGTH=442;\
|
||||
ISV_TOTAL_DIM=443;\
|
||||
TRUNK_INPUT_DD_PCT=sp15_phase_1_5;\
|
||||
TRUNK_INPUT_DD_PCT=sp15_wave_4_1a;\
|
||||
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;\
|
||||
|
||||
@@ -642,101 +642,156 @@ mod gpu {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test 1.5 — dd_pct foundational state input concat (LAYOUT FINGERPRINT
|
||||
/// BREAK).
|
||||
/// Test 1.5 — bottleneck-aware dd_pct trunk-input concat (Wave 4.1a)
|
||||
/// (LAYOUT FINGERPRINT BREAK — fingerprint marker
|
||||
/// `TRUNK_INPUT_DD_PCT=sp15_wave_4_1a`).
|
||||
///
|
||||
/// Verifies the `dd_pct_concat_kernel` correctly produces a
|
||||
/// `[B, state_dim_padded + 1]` buffer whose:
|
||||
/// - leading `state_dim_padded` columns equal the input states_buf
|
||||
/// (full padded width, including pre-existing pad zeros), and
|
||||
/// - last column equals `isv[DD_PCT_INDEX=406]` broadcast across
|
||||
/// batch.
|
||||
/// Verifies the new `bn_tanh_concat_dd_kernel` correctly produces a
|
||||
/// `[B, concat_dd_dim]` buffer where
|
||||
/// `concat_dd_dim = bn_dim + portfolio_dim + 1`. The kernel folds the
|
||||
/// dd_pct append into the same launch as the bn_tanh + portfolio concat:
|
||||
/// - columns `[0, bn_dim)` = `tanh(bn_hidden[b, 0..bn_dim))`
|
||||
/// - columns `[bn_dim, bn_dim+pd)` = `states[b, market_dim..market_dim+pd)`
|
||||
/// - column `bn_dim + pd` = `isv[DD_PCT_INDEX=406]` (broadcast)
|
||||
///
|
||||
/// Spec correction (Wave 4.1a, per `feedback_trust_code_not_docs`): the
|
||||
/// spec's "state_dim 48→49" is stale terminology pre-STATE_DIM
|
||||
/// 48→112→128 evolution. Production `s1_input_dim` is
|
||||
/// `bn_dim + (STATE_DIM - market_dim)` (= 102 with current
|
||||
/// bn_dim=16, market_dim=42, STATE_DIM=128). Wave 4.1b will bump this
|
||||
/// to 103 by flipping the 4 launch sites + buffer allocations.
|
||||
///
|
||||
/// This is a kernel-level oracle test (matches the Phase 1.1-1.4
|
||||
/// pattern) — it does NOT verify dd_pct propagates through the trunk
|
||||
/// forward, because trunk consumer migration is deferred to a
|
||||
/// follow-up commit per `feedback_no_partial_refactor.md`. The
|
||||
/// follow-up commit that re-points `forward_online` to consume the
|
||||
/// concat buffer adds the trunk-grounding behavioral test (see plan
|
||||
/// §6.5 step 7).
|
||||
/// pattern) — the trunk-grounding behavioral test (KL divergence
|
||||
/// between forward_trunk(102-input) and forward_trunk(103-input))
|
||||
/// is added in Wave 4.1c using the helpers defined at the bottom of
|
||||
/// this module.
|
||||
#[test]
|
||||
#[ignore = "requires GPU"]
|
||||
fn dd_pct_concat_kernel_writes_last_column() {
|
||||
use ml::cuda_pipeline::gpu_dqn_trainer::launch_sp15_dd_pct_concat;
|
||||
fn bn_tanh_concat_dd_kernel_writes_dd_pct_column() {
|
||||
use ml::cuda_pipeline::gpu_dqn_trainer::launch_sp15_bn_concat_dd;
|
||||
|
||||
let stream = make_test_stream();
|
||||
|
||||
// Synthetic batch: B=4, raw state_dim=48, state_dim_padded=128
|
||||
// (matches GpuDqnTrainConfig::default()'s 48 → pad128 = 128 layout).
|
||||
// Only the first 48 columns of each row carry "real" feature data;
|
||||
// the trailing 80 are pre-existing pad zeros from the producer.
|
||||
// Synthetic batch matches production layout: bn_dim=16,
|
||||
// market_dim=42, STATE_DIM=128 → state_dim_padded=128 (no
|
||||
// sub-128 pad in the current production config), portfolio_dim
|
||||
// = STATE_DIM - market_dim = 86, concat_dd_dim = bn_dim +
|
||||
// portfolio_dim + 1 = 16 + 86 + 1 = 103. Wave 4.1b bumps the
|
||||
// production `s1_input_dim` from 102 → 103 (matches the +1
|
||||
// from the dd_pct append).
|
||||
let batch_size: usize = 4;
|
||||
const RAW_STATE_DIM: usize = 48;
|
||||
let state_dim_padded: usize = 128;
|
||||
const BN_DIM: usize = 16;
|
||||
const MARKET_DIM: usize = 42;
|
||||
const STATE_DIM_PADDED: usize = 128;
|
||||
let portfolio_dim: usize = STATE_DIM_PADDED - MARKET_DIM;
|
||||
let concat_dd_dim: usize = BN_DIM + portfolio_dim + 1;
|
||||
|
||||
// Build the input states buffer: row b column i = (b * 100 + i) as f32
|
||||
// for i ∈ [0, RAW_STATE_DIM); zeros for i ∈ [RAW_STATE_DIM,
|
||||
// state_dim_padded) (the padded portion).
|
||||
let mut states: Vec<f32> = vec![0.0; batch_size * state_dim_padded];
|
||||
// Build bn_hidden [B, BN_DIM]: pre-tanh values; row b column j =
|
||||
// 0.1 * (b + 1) * (j + 1) so each cell is small enough to keep
|
||||
// tanh in the linear regime (well under 1.0 saturation).
|
||||
let mut bn_hidden_init: Vec<f32> = vec![0.0; batch_size * BN_DIM];
|
||||
for b in 0..batch_size {
|
||||
for i in 0..RAW_STATE_DIM {
|
||||
states[b * state_dim_padded + i] = (b * 100 + i) as f32;
|
||||
for j in 0..BN_DIM {
|
||||
bn_hidden_init[b * BN_DIM + j] = 0.1_f32 * ((b + 1) as f32) * ((j + 1) as f32);
|
||||
}
|
||||
}
|
||||
|
||||
// Build states [B, STATE_DIM_PADDED]: row b column i =
|
||||
// (b * 1000 + i) as f32 for the entire padded width — only
|
||||
// the portfolio slice [MARKET_DIM, MARKET_DIM + portfolio_dim)
|
||||
// is read by the kernel.
|
||||
let mut states: Vec<f32> = vec![0.0; batch_size * STATE_DIM_PADDED];
|
||||
for b in 0..batch_size {
|
||||
for i in 0..STATE_DIM_PADDED {
|
||||
states[b * STATE_DIM_PADDED + i] = (b * 1000 + i) as f32;
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: CUDA context active via `make_test_stream` above.
|
||||
let bn_hidden_buf = unsafe { MappedF32Buffer::new(bn_hidden_init.len()) }
|
||||
.expect("alloc MappedF32Buffer for bn_hidden");
|
||||
bn_hidden_buf.write_from_slice(&bn_hidden_init);
|
||||
let states_buf = unsafe { MappedF32Buffer::new(states.len()) }
|
||||
.expect("alloc MappedF32Buffer for states");
|
||||
states_buf.write_from_slice(&states);
|
||||
|
||||
// ISV bus: write 0.42 to slot DD_PCT_INDEX=406.
|
||||
// ISV bus: write 0.05 to slot DD_PCT_INDEX=406.
|
||||
let isv_buf = unsafe { MappedF32Buffer::new(ISV_LEN) }
|
||||
.expect("alloc MappedF32Buffer for ISV");
|
||||
let mut isv_init = vec![0.0f32; ISV_LEN];
|
||||
isv_init[DD_PCT_INDEX] = 0.42;
|
||||
isv_init[DD_PCT_INDEX] = 0.05;
|
||||
isv_buf.write_from_slice(&isv_init);
|
||||
|
||||
// Output concat buffer: [B, state_dim_padded + 1].
|
||||
let out_len: usize = batch_size * (state_dim_padded + 1);
|
||||
let out_len: usize = batch_size * concat_dd_dim;
|
||||
let concat_buf = unsafe { MappedF32Buffer::new(out_len) }
|
||||
.expect("alloc MappedF32Buffer for concat output");
|
||||
|
||||
launch_sp15_dd_pct_concat(
|
||||
launch_sp15_bn_concat_dd(
|
||||
&stream,
|
||||
bn_hidden_buf.dev_ptr,
|
||||
states_buf.dev_ptr,
|
||||
isv_buf.dev_ptr,
|
||||
concat_buf.dev_ptr,
|
||||
batch_size as i32,
|
||||
state_dim_padded as i32,
|
||||
BN_DIM as i32,
|
||||
MARKET_DIM as i32,
|
||||
STATE_DIM_PADDED as i32,
|
||||
concat_dd_dim as i32,
|
||||
)
|
||||
.expect("launch dd_pct_concat_kernel");
|
||||
.expect("launch bn_tanh_concat_dd_kernel");
|
||||
stream
|
||||
.synchronize()
|
||||
.expect("synchronize after dd_pct_concat_kernel launch");
|
||||
.expect("synchronize after bn_tanh_concat_dd_kernel launch");
|
||||
|
||||
let concat = concat_buf.read_all();
|
||||
let bn_hidden_after = bn_hidden_buf.read_all();
|
||||
|
||||
// Check 1: leading state_dim_padded columns of each row equal the
|
||||
// input states_buf row (full padded width, including pad zeros).
|
||||
// Check 1: bn_tanh slice — output[b, 0..BN_DIM) ==
|
||||
// tanh(input bn_hidden), and the in-place write semantics put
|
||||
// the same tanh'd value in bn_hidden after the launch.
|
||||
for b in 0..batch_size {
|
||||
for i in 0..state_dim_padded {
|
||||
let in_val = states[b * state_dim_padded + i];
|
||||
let out_val = concat[b * (state_dim_padded + 1) + i];
|
||||
for j in 0..BN_DIM {
|
||||
let pre = bn_hidden_init[b * BN_DIM + j];
|
||||
let expected = pre.tanh();
|
||||
let out_val = concat[b * concat_dd_dim + j];
|
||||
let bn_val = bn_hidden_after[b * BN_DIM + j];
|
||||
assert!(
|
||||
(in_val - out_val).abs() < 1e-6,
|
||||
"concat[b={}, i={}] = {} ≠ states[b={}, i={}] = {} (state copy mismatch)",
|
||||
b, i, out_val, b, i, in_val
|
||||
(out_val - expected).abs() < 1e-6,
|
||||
"concat[b={}, j={}] = {} ≠ tanh({}) = {} (bn_tanh column mismatch)",
|
||||
b, j, out_val, pre, expected
|
||||
);
|
||||
assert!(
|
||||
(bn_val - expected).abs() < 1e-6,
|
||||
"bn_hidden[b={}, j={}] = {} ≠ tanh({}) = {} (in-place write mismatch)",
|
||||
b, j, bn_val, pre, expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check 2: last column of each row equals isv[DD_PCT_INDEX] = 0.42.
|
||||
// Check 2: portfolio passthrough slice — output[b, BN_DIM..BN_DIM+pd)
|
||||
// == states[b, MARKET_DIM..MARKET_DIM+pd).
|
||||
for b in 0..batch_size {
|
||||
let last = concat[b * (state_dim_padded + 1) + state_dim_padded];
|
||||
for k in 0..portfolio_dim {
|
||||
let expected = states[b * STATE_DIM_PADDED + MARKET_DIM + k];
|
||||
let out_val = concat[b * concat_dd_dim + BN_DIM + k];
|
||||
assert!(
|
||||
(out_val - expected).abs() < 1e-6,
|
||||
"concat[b={}, BN_DIM+{}] = {} ≠ states[b={}, MARKET_DIM+{}] = {} \
|
||||
(portfolio passthrough mismatch)",
|
||||
b, k, out_val, b, k, expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check 3: dd_pct broadcast column — output[b, concat_dd_dim - 1]
|
||||
// == isv[DD_PCT_INDEX] = 0.05 for every batch row.
|
||||
for b in 0..batch_size {
|
||||
let last = concat[b * concat_dd_dim + (concat_dd_dim - 1)];
|
||||
assert!(
|
||||
(last - 0.42).abs() < 1e-6,
|
||||
"concat[b={}, last] = {} ≠ isv[DD_PCT_INDEX] = 0.42 (broadcast mismatch)",
|
||||
b, last
|
||||
(last - 0.05).abs() < 1e-6,
|
||||
"concat[b={}, last={}] = {} ≠ isv[DD_PCT_INDEX] = 0.05 (broadcast mismatch)",
|
||||
b, concat_dd_dim - 1, last
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2243,6 +2298,165 @@ mod gpu {
|
||||
}
|
||||
}
|
||||
|
||||
/// SP15 Wave 4.1a — shared test helpers used by the bn_tanh_concat_dd
|
||||
/// oracle test (above) and Wave 4.1c's behavioral KL test (deferred).
|
||||
///
|
||||
/// Per the spec, three helpers are needed: `kl_divergence` (CPU math),
|
||||
/// `minimal_trainer_for_tests` (thin wrapper around `DQNTrainer::new`),
|
||||
/// and `forward_trunk_for_test` (runs the trunk forward path against
|
||||
/// a `[B, s1_input_dim+1]` post-bn_concat_dd buffer and returns
|
||||
/// `[B, shared_h1]` GRN output).
|
||||
///
|
||||
/// `forward_trunk_for_test` is NOT implemented in Wave 4.1a — its
|
||||
/// exact contract is owned by Wave 4.1c (which lands the consumer
|
||||
/// migration that bumps `s1_input_dim` 102→103 and reshapes GRN
|
||||
/// w_s1). Implementing it here would either require a new
|
||||
/// non-trivial public API on `DQNTrainer` (test-only forward
|
||||
/// surface) or a stub that returns dummy data — both violate
|
||||
/// `feedback_no_stubs`. The two helpers that ARE implementable
|
||||
/// today (`kl_divergence`, `minimal_trainer_for_tests`) are
|
||||
/// real-and-useful; Wave 4.1c will add `forward_trunk_for_test`
|
||||
/// alongside the public `DQNTrainer` surface change it requires.
|
||||
#[cfg(test)]
|
||||
mod sp15_wave_4_1a_test_helpers {
|
||||
use ml::trainers::dqn::{DQNHyperparameters, DQNTrainer};
|
||||
|
||||
/// KL divergence helper: KL(p || q) = sum_i p[i] * ln(p[i] / q[i]).
|
||||
///
|
||||
/// Used by Wave 4.1c's behavioral test to compare
|
||||
/// `softmax(forward_trunk(102-input))` vs
|
||||
/// `softmax(forward_trunk(103-input))` action distributions; the
|
||||
/// dd_pct column should produce a non-trivial KL divergence
|
||||
/// (i.e. the policy actually conditions on drawdown context).
|
||||
///
|
||||
/// Contract:
|
||||
/// - `p` and `q` MUST be the same length (panics otherwise).
|
||||
/// - Treats both inputs as proper probability distributions:
|
||||
/// entries with `p[i] == 0.0` contribute 0 to the sum (limit
|
||||
/// `x ln x → 0` as `x → 0`); entries with `q[i] <= 0.0` raise
|
||||
/// an `f32::INFINITY` term — caller is responsible for either
|
||||
/// adding a small epsilon or asserting q is strictly positive.
|
||||
/// - Result is in nats (natural log). Multiply by `1.0 /
|
||||
/// std::f32::consts::LN_2` to convert to bits if needed.
|
||||
///
|
||||
/// This is a generic helper — no SP15-specific behaviour. Standard
|
||||
/// information-theoretic KL formula.
|
||||
#[allow(dead_code)] // Consumer (Wave 4.1c behavioral KL test) lands later.
|
||||
pub(super) fn kl_divergence(p: &[f32], q: &[f32]) -> f32 {
|
||||
assert_eq!(
|
||||
p.len(),
|
||||
q.len(),
|
||||
"kl_divergence: distribution length mismatch — p.len()={} q.len()={}",
|
||||
p.len(),
|
||||
q.len()
|
||||
);
|
||||
let mut acc = 0.0_f32;
|
||||
for (pi, qi) in p.iter().zip(q.iter()) {
|
||||
// Convention: 0 * ln(0) = 0; preserves continuity at p=0.
|
||||
if *pi <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
// q <= 0 with p > 0 → KL is infinite. Surface as +inf rather
|
||||
// than NaN so caller assertions like `kl > 0.05` see the
|
||||
// diagnostic value.
|
||||
if *qi <= 0.0 {
|
||||
return f32::INFINITY;
|
||||
}
|
||||
acc += *pi * ((*pi / *qi).ln());
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
/// Construct a minimal `DQNTrainer` for tests that don't need the
|
||||
/// full data pipeline. Mirrors the existing
|
||||
/// `set_test_data_from_slices_fires_observer_and_stashes` pattern
|
||||
/// (see `test_slice_consumption` module below): synchronous
|
||||
/// constructor, no GPU forward passes required. Returns
|
||||
/// `Some(trainer)` on success or `None` if the underlying
|
||||
/// `DQNTrainer::new` fails (e.g. CPU-only build configuration where
|
||||
/// trainer construction can't proceed) — caller is expected to
|
||||
/// `eprintln!` a skip notice and return early in that case (the
|
||||
/// L40S smoke is the canonical verifier when trainer construction
|
||||
/// is unavailable).
|
||||
#[allow(dead_code)] // Consumer (Wave 4.1c behavioral KL test) lands later.
|
||||
pub(super) fn minimal_trainer_for_tests() -> Option<DQNTrainer> {
|
||||
let hyperparams = DQNHyperparameters::conservative();
|
||||
match DQNTrainer::new(hyperparams) {
|
||||
Ok(t) => Some(t),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"minimal_trainer_for_tests: trainer construction failed ({}). \
|
||||
Caller should skip and rely on L40S smoke verification.",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time smoke for the `kl_divergence` helper. Lives in the
|
||||
// helper module (not as a #[test]) so it doesn't add to the test
|
||||
// suite; the runtime invariants (zero KL on identical
|
||||
// distributions, positive KL on differing ones) are exercised by
|
||||
// the dedicated unit tests below.
|
||||
|
||||
#[test]
|
||||
fn kl_divergence_zero_on_identical_distributions() {
|
||||
let p = [0.25_f32, 0.25, 0.25, 0.25];
|
||||
let q = [0.25_f32, 0.25, 0.25, 0.25];
|
||||
let kl = kl_divergence(&p, &q);
|
||||
assert!(
|
||||
kl.abs() < 1e-6,
|
||||
"KL(p || p) should be 0 for identical distributions, got {}",
|
||||
kl
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kl_divergence_positive_on_differing_distributions() {
|
||||
let p = [0.7_f32, 0.1, 0.1, 0.1];
|
||||
let q = [0.25_f32, 0.25, 0.25, 0.25];
|
||||
let kl = kl_divergence(&p, &q);
|
||||
assert!(
|
||||
kl > 0.0,
|
||||
"KL(p || q) should be strictly positive when p ≠ q, got {}",
|
||||
kl
|
||||
);
|
||||
// Hand-checked: 0.7 ln(0.7/0.25) + 3*(0.1 ln(0.1/0.25))
|
||||
// = 0.7 * 1.0296 + 3 * 0.1 * (-0.9163)
|
||||
// ≈ 0.7207 - 0.2749 ≈ 0.4458 nats.
|
||||
assert!(
|
||||
(kl - 0.4458_f32).abs() < 1e-3,
|
||||
"KL ≈ 0.4458 nats expected, got {}",
|
||||
kl
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kl_divergence_handles_zero_probability_in_p() {
|
||||
// Convention: p[i] = 0 contributes nothing (limit x ln x → 0).
|
||||
let p = [0.5_f32, 0.5, 0.0, 0.0];
|
||||
let q = [0.4_f32, 0.4, 0.1, 0.1];
|
||||
let kl = kl_divergence(&p, &q);
|
||||
// Manual check: 0.5 ln(0.5/0.4) + 0.5 ln(0.5/0.4) + 0 + 0
|
||||
// = 1.0 * ln(1.25) ≈ 0.2231 nats.
|
||||
assert!(
|
||||
(kl - 0.2231_f32).abs() < 1e-3,
|
||||
"KL with zero p entries ≈ 0.2231 nats expected, got {}",
|
||||
kl
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimal_trainer_for_tests_constructs_or_skips() {
|
||||
// Either the trainer constructs (Some) or we get a clean None
|
||||
// for the skip path — never panics. Wave 4.1c will use this
|
||||
// helper and treat None the same way `test_slice_consumption`
|
||||
// does (eprintln + early return).
|
||||
let _trainer = minimal_trainer_for_tests();
|
||||
}
|
||||
}
|
||||
|
||||
/// SP15 Phase 1.7 — verify the trainer's `set_test_data_from_slices` API
|
||||
/// stashes the right slice and fires the observer with `(test_start_bar,
|
||||
/// test_end_bar)`. This is the unit-level surface; the production verification
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user