feat(sp22-vnext): Phase A4 — aux_trade_outcome loss reduce kernel

K=3 sparse cross-entropy reduce over the trade-outcome softmax tile
produced by `aux_trade_outcome_forward` (Phase A3). Mirrors the K=2
sibling `aux_next_bar_loss_reduce` structurally: single-block shmem-tree
reduce, two parallel partial strips (loss_numer + valid_count) reduced
lockstep, fmaxf(p_tgt, 1e-30) numerical floor, fmaxf(valid, 1.0) all-
skip-batch guard, valid_count_out[1] save-for-backward.

Kept as SEPARATE kernel from the K=2 sibling:
- Diagnostic isolation (distinct HEALTH_DIAG slot, distinct cubin in
  profiles for clean per-loss-source attribution)
- Sparse-label semantic clarity (~95-99% mask=-1 vs ~50-100% valid for
  the K=2 next-bar head)
- Future per-class weighting headroom (Profit/Stop/Timeout 3:1-10:1
  imbalance will likely need class-weighted CE — surgical mod here
  without touching the K=2 head's contract)

Phase A4 (this commit) is dead code — no Rust launcher yet. Phase A5
lands backward; Phase B wires the full forward→loss→backward chain.

Discipline: feedback_no_atomicadd (single-block tree-reduce), feedback_
cpu_is_read_only (pure GPU), pearl_first_observation_bootstrap (sentinel
0 valid_count produces zero gradients gracefully on cold start).

Audit: docs/dqn-wire-up-audit.md Phase A4 section.
Cubin: aux_trade_outcome_loss_reduce_kernel.cubin (9.9 KB) compiles clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-13 23:41:59 +02:00
parent 07728f9efc
commit 3ddcfb8868
3 changed files with 188 additions and 0 deletions

View File

@@ -812,6 +812,15 @@ fn main() {
// `docs/plans/2026-05-14-sp22-h6-vNext-trade-outcome-aux.md` for // `docs/plans/2026-05-14-sp22-h6-vNext-trade-outcome-aux.md` for
// the full vNext architecture spec. // the full vNext architecture spec.
"aux_trade_outcome_forward_kernel.cu", "aux_trade_outcome_forward_kernel.cu",
// SP22 H6 vNext Phase A4 (2026-05-13): sparse cross-entropy reduce
// over the K=3 softmax tile from `aux_trade_outcome_forward`.
// Mirrors `aux_next_bar_loss_reduce`'s single-block shmem-tree
// pattern: mean over B_valid (skip label=-1 rows), fmaxf(p, 1e-30)
// numerical floor, saved valid_count for backward. Dead code until
// Phase A5 (backward) + Phase B (Rust launcher wireup) land. Kept
// separate from the K=2 sibling for diagnostic isolation, sparse-
// label semantic clarity, and future per-class weighting headroom.
"aux_trade_outcome_loss_reduce_kernel.cu",
// SP22 H6 (2026-05-12): per-env p_up extractor that copies // SP22 H6 (2026-05-12): per-env p_up extractor that copies
// `aux_softmax[env, 1]` → `prev_aux_dir_prob[env]` after each aux // `aux_softmax[env, 1]` → `prev_aux_dir_prob[env]` after each aux
// forward. The cache buffer is read by `experience_state_gather` // forward. The cache buffer is read by `experience_state_gather`

View File

@@ -0,0 +1,150 @@
// crates/ml/src/cuda_pipeline/aux_trade_outcome_loss_reduce_kernel.cu
//
// SP22 H6 vNext Phase A4 (2026-05-13) — sparse cross-entropy reduce over
// the K=3 trade-outcome softmax tile produced by
// `aux_trade_outcome_forward`.
//
// Mirrors `aux_next_bar_loss_reduce`'s single-block shmem-tree reduce
// pattern (mean over valid-row count, fmaxf(p_tgt, 1e-30) numerical floor,
// saved valid_count for backward). Kept as a SEPARATE kernel from the K=2
// sibling because:
//
// 1. Diagnostic isolation: the trade-outcome head's loss feeds a
// DIFFERENT HEALTH_DIAG slot (`aux_outcome_ce_ema`, allocated in
// Phase F) than the next-bar head's `aux_next_bar_mse_ema_idx`.
// Two distinct kernels = two distinct cubins = clean
// per-loss-source attribution in profiles and reward decomposition.
//
// 2. Sparse-label semantics: the trade-outcome producer emits a `-1`
// mask on EVERY bar without a trade-close event, which is the
// common case (~95-99% of bars). The next-bar head sees ~50-100%
// valid labels (only the trailing H bars near series-end get
// masked). The aggressive sparsity changes the gradient-scale
// arithmetic: an active-batch fraction of ~1-5% means each valid
// row's gradient gets divided by a much smaller B_valid → higher
// per-trade-close gradient magnitude. The kernel logic handles
// this correctly via `fmaxf(valid, 1.0f)` divisor (matches the
// backward kernel's `inv_B = 1 / B_valid` semantics) but the
// independent kernel lets Phase F's Adam-LR controller tune the
// trade-outcome head separately from the next-bar head if
// magnitude calibration becomes necessary.
//
// 3. Future per-class weighting: Profit/Stop/Timeout class imbalance
// can be ~3:1 to ~10:1 depending on the regime. Phase D+ may need
// class-weighted CE; an isolated kernel makes the modification
// surgical without touching the K=2 head's contract.
//
// Inputs / outputs (mirror the K=2 sibling contract)
// ──────────────────────────────────────────────────
// softmax [B, K=3] f32 row-major — saved by
// `aux_trade_outcome_forward`.
// labels [B] i32 in {-1, 0, 1, 2} — from
// `trade_outcome_label_kernel` at the
// same step (sparse: -1 = no trade
// close at this bar).
// B batch size (= N × L in the replay-batch use case;
// = n_envs in the rollout-per-step use
// case). K-runtime arg even though this
// kernel is K=3-semantic — keeps the
// launcher signature symmetric with
// the K=2 sibling for testing.
// loss_out [1] mean CE over the B_valid valid rows.
// valid_count_out [1] B_valid (saved for backward — matches the K=2
// sibling's contract).
//
// Discipline
// ──────────
// - No atomicAdd per `feedback_no_atomicadd.md` — single-block shmem-
// tree reduction.
// - GPU-only per `feedback_cpu_is_read_only.md`.
// - Numerical floor at 1e-30 prevents -log(0) propagation through the
// EMA when an untrained softmax emits near-zero probabilities.
//
// Block: AUX_BLOCK threads. Grid: (1, 1, 1).
// Shared memory: 2 * AUX_BLOCK floats (loss numerator + valid-count
// partials, lockstep-reduced).
#include <cuda_runtime.h>
#include <math_constants.h>
#ifndef AUX_BLOCK
#define AUX_BLOCK 256
#endif
extern "C" __global__ void aux_trade_outcome_loss_reduce(
/* [B, K=3] f32 row-major softmax tile from
* `aux_trade_outcome_forward`. */
const float* __restrict__ softmax,
/* [B] i32 labels in `{-1, 0=Profit, 1=Stop, 2=Timeout}`. Producer is
* `trade_outcome_label_kernel.cu` — emits -1 (mask) on bars with no
* trade-close event (the common case at ~95-99% of bars). */
const int* __restrict__ labels,
int B,
int K,
/* [1] mean CE over the B_valid rows whose label != -1. An all-skip
* batch produces 0 (no NaN; fmaxf(valid, 1) divisor). */
float* __restrict__ loss_out,
/* [1] B_valid saved for backward. Matches the K=2 sibling's contract
* — the backward kernel uses this as `inv_B = 1 / B_valid` for the
* d_logits scaling. An all-skip batch saves 0; backward then emits
* zero gradients across the board (no NaN). */
float* __restrict__ valid_count_out
) {
if (blockIdx.x != 0) return;
extern __shared__ float smem[];
float* sh_loss = smem;
float* sh_valid = smem + blockDim.x;
const int tid = threadIdx.x;
const int block = blockDim.x;
/* Per-thread strided accumulation: each thread folds its slice of
* `[0, B)` into private (loss_numer, valid_count) accumulators.
* Mask-encoded rows (label == -1) contribute 0 to BOTH accumulators —
* the common case for trade-outcome labels where most bars have no
* trade-close event. The valid-row branch reads `softmax[i, label]`
* directly (no max-shift needed; the forward kernel already
* materialised stable softmax probs). */
float local_loss = 0.0f;
float local_valid = 0.0f;
for (int i = tid; i < B; i += block) {
const int tgt = labels[i];
if (tgt == -1) continue;
/* Defensive bounds — out-of-range labels degrade to skip
* rather than read past the end of the [B, K] softmax row. The
* label producer (Phase A2 `trade_outcome_label_kernel`) emits
* only -1 / {0, 1, 2} so this branch is dead under correct
* producer behavior; kept for kernel robustness against
* producer bugs. */
if (tgt < 0 || tgt >= K) continue;
const float p_tgt = softmax[(size_t)i * K + tgt];
/* Numerical floor at 1e-30 — matches the K=2 sibling's clip.
* An untrained `aux_trade_outcome_forward` with extreme W2 can
* emit p ≈ 0 for some class; -log(0) = +inf would propagate
* to the EMA. log(1e-30) ≈ -69 is well outside the trained
* regime — the floor is robustness, not a substitute for
* training stability. */
const float p_clipped = fmaxf(p_tgt, 1e-30f);
local_loss += -logf(p_clipped);
local_valid += 1.0f;
}
sh_loss[tid] = local_loss;
sh_valid[tid] = local_valid;
__syncthreads();
/* Standard log2(BLOCK) tree reduction over both arrays in lockstep
* — identical pattern to the K=2 sibling so the proven correctness
* carries over directly. */
for (int s = block / 2; s > 0; s >>= 1) {
if (tid < s) {
sh_loss[tid] += sh_loss[tid + s];
sh_valid[tid] += sh_valid[tid + s];
}
__syncthreads();
}
if (tid == 0) {
const float valid = sh_valid[0];
loss_out[0] = sh_loss[0] / fmaxf(valid, 1.0f);
valid_count_out[0] = valid;
}
}

View File

@@ -17693,3 +17693,32 @@ Second foundation commit for the SP22 H6 vNext trade-outcome aux head. Lands two
- Phase F: validation smoke - Phase F: validation smoke
Cargo check clean. Registry tests clean. Ready for Phase A4. Cargo check clean. Registry tests clean. Ready for Phase A4.
#### Phase A4 — aux_trade_outcome loss reduce kernel (2026-05-13)
Third foundation commit for the SP22 H6 vNext trade-outcome aux head. Lands the sparse cross-entropy reduce.
**NEW kernel `aux_trade_outcome_loss_reduce_kernel.cu`** — K=3 mean CE over the trade-outcome softmax tile with `-1` label masking.
- Pattern: single-block shmem-tree reduce, two parallel partial strips (loss_numer + valid_count) reduced in lockstep. Identical structural shape to `aux_next_bar_loss_reduce` (K=2 sibling); same `fmaxf(p_tgt, 1e-30)` numerical floor, same `fmaxf(valid, 1.0)` all-skip-batch guard, same `valid_count_out[1]` save-for-backward contract.
- Sparse-label arithmetic: most bars (~95-99%) have `label=-1` → contribute 0 to both numerator and valid_count. The few valid-trade-close rows produce CE numerator divided by a small B_valid → higher per-trade-close gradient magnitude. The kernel correctly handles all-skip batches (loss=0, valid=0 → backward emits zero gradients).
- **Kept separate from the K=2 sibling** for three reasons:
1. Diagnostic isolation — distinct HEALTH_DIAG slot (`aux_outcome_ce_ema`, lands in Phase F), distinct cubin in profiles, clean attribution.
2. Sparse-label semantic clarity — the kernel comment documents the ~1-5% active-batch fraction expected at the trade-outcome head.
3. Future per-class weighting headroom — Profit/Stop/Timeout imbalance can be 3:1 to 10:1; an isolated kernel makes class-weighted CE a surgical modification without touching the K=2 head's contract.
- Cubin: `aux_trade_outcome_loss_reduce_kernel.cubin` (9.9 KB).
- Phase A4 (this commit) **dead code** — no Rust launcher yet. Phase A5 (backward) reads `valid_count_out` for `inv_B = 1/B_valid` scaling. Phase B wires the full forward→loss→backward chain.
**Discipline**:
- `feedback_no_atomicadd` — single-block shmem-tree reduction.
- `pearl_first_observation_bootstrap` — sentinel 0.0 for valid_count means cold-start (no trade-closes yet) produces zero gradients gracefully.
- `feedback_cpu_is_read_only` — all GPU-side.
**Remaining vNext work**:
- Phase A5: `aux_trade_outcome_backward_kernel.cu`
- Phase B: 262-dim input concat (`h_s2_aux` || `plan_params`)
- Phase C: 3-slot state assembly (state[121..124] = p_Profit, p_Stop, p_Timeout)
- Phase D: 12-weight W atom-shift (4 actions × 3 outcomes)
- Phase E: dW + Adam for W[4, 3]
- Phase F: validation smoke
Cargo check clean. Ready for Phase A5.