feat(sp20): Phase 1.1 sp20_stats_compute kernel

Component 5 / Kernel 3 of the SP20 fused-producer chain. Single-block
BLOCK=256 kernel reads `aux_logits [B, 3]` (the SP14-C aux head's
3-class direction logits) and emits `[aux_conf_p50, aux_conf_std]`
into a `MappedF32Buffer<2>`, where the per-row signal is
`aux_conf[i] = max_c softmax(logits[i, *])[c] - 1/3`.

p50 uses the inlined `sp4_histogram_p99` pattern (per-warp tile
binning + cumulative-from-bottom, no atomicAdd per
`feedback_no_atomicadd`); std uses two block tree-reductions sharing
one shmem tile sequentially. One fused kernel streams `aux_logits`
once for both stats per `pearl_fused_per_group_statistics_oracle`.

Phase 1.4 wires the production launch site atomically with the rest
of the SP20 reward chain per `feedback_no_partial_refactor`. This
commit lands kernel + Rust launcher + GPU oracle tests + build entry
+ audit-doc entry together so the kernel is independently verifiable
on RTX 3050 Ti (sm_86) and L40S (sm_89) before the EMA + controller
producers (Phase 1.2 + 1.3) reference its outputs.

Tests verify:
  - uniform logits → aux_conf = 0 → [p50, std] = [0, 0]
  - varied confidence (logit ramp 0 → 3) → matches CPU oracle
  - heterogeneous half-hot half-uniform → matches CPU oracle
  - empty batch → degenerate-guard writes [0, 0]

All 4 GPU oracle tests + 4 launcher unit tests pass on RTX 3050 Ti.
Test data uses per-row variance to avoid the
`pearl_sp4_histogram_warp_tile_undercount` lockstep-uniform trap
(concentrated values within one bin_width race the per-warp
non-atomic increments).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-09 18:43:11 +02:00
parent ef5e745a13
commit de922c6a4a
6 changed files with 968 additions and 0 deletions

View File

@@ -10751,3 +10751,159 @@ bash scripts/audit_sp18_consumers.sh --check # exit
round-trip equality. Direction sometimes flips at low Q values,
unrelated to ISV slot layout. Verified flaky pre-Commit B by
stashing the SP19 changes and re-running 3× — fails 2/3 runs.
## 2026-05-09 — SP20 Phase 1.1: sp20_stats_compute kernel (additive)
### Component
- New CUDA kernel `crates/ml/src/cuda_pipeline/sp20_stats_compute_kernel.cu`
- New Rust launcher `crates/ml/src/cuda_pipeline/sp20_stats_compute.rs`
(re-exported from `cuda_pipeline::sp20_stats_compute`)
- New GPU oracle test `crates/ml/tests/sp20_stats_compute_test.rs`
- Cubin manifest entry in `crates/ml/build.rs`
### Purpose
Component 5 / Kernel 3 of the SP20 fused-producer chain (Phase 1.4
will land Kernels 1+2 — `sp20_emas_compute` + `sp20_controllers_
compute` — and the production launch site atomically with the
training-loop wire-up per `feedback_no_partial_refactor`).
Reads `aux_logits [B, 3]` (the SP14-C aux head's 3-class direction
logits) and emits `[aux_conf_p50, aux_conf_std]` into a
`MappedF32Buffer<2>`, where the per-row signal is
```
aux_conf[i] = max_c softmax(logits[i, *])[c] - 1/3
```
`aux_conf` measures peak class confidence above the K=3 uniform
baseline (0 ⇔ uniform, 2/3 ⇔ fully concentrated). The downstream
EMA producer (Phase 1.2) Wiener-blends p50 + std into ISV slots
within [510..520) reserved by SP20 (`f5eed1fa7`).
### Algorithm
Single-block, 256-thread kernel; one fused stream of `aux_logits`
for both stats per `pearl_fused_per_group_statistics_oracle`.
- **Pass A**: per-row numerically-stable softmax → max → minus 1/3,
write `aux_conf[i]` into per-row scratch in dynamic shmem.
- **Pass B1**: block tree-reduce `sum(aux_conf)`. Reuses
`s_bins[0..256]` (the histogram tile) as a float-via-int reduce
tile, BLOCK=256 ≤ 256 bins so the alias fits exactly.
- **Pass B2**: block tree-reduce `sum(aux_conf²)` using the same
shmem tile sequentially.
- **Pass C.1**: max-reduce `|aux_conf|` to seed `step_max` for the
histogram (mirrors `sp4_histogram_p99` pass 1 verbatim).
- **Pass C.2**: per-warp tile binning into 256 linear bins (no
atomicAdd per `feedback_no_atomicadd`; mirrors
`sp4_histogram_p99` pass 2).
- **Pass C.3**: cumulative-from-BOTTOM scan (`cumul ≥ ⌈B/2⌉`) →
p50 = bin upper-edge. Inverts the direction of
`sp4_histogram_p99` pass 3 (which scans top-down for the 99th
percentile); the scan logic itself is otherwise identical.
Thread 0 writes `out[0] = p50`, `out[1] = sqrt(var)` with
`__threadfence_system()` for mapped-pinned coherence.
### Wiring contract
Phase 1.1 wires kernel + launcher + tests + build entry **only**.
There is NO production caller in this commit — the kernel is dead
code awaiting Phase 1.4's atomic wire-up of:
1. The training-loop call site (post-aux-head-forward, pre-EMA
producer) supplying the `aux_logits` device pointer.
2. A trainer-owned `MappedF32Buffer<2>` for the `[p50, std]` output.
3. Stream ordering with the aux-head forward (same-stream is
sufficient; CUDA stream-ordering invariant guarantees the
`aux_logits` write is visible before this kernel reads it).
The `cuda_pipeline::sp20_stats_compute` module exports
`launch_sp20_stats_compute` + the contract constants (`AUX_K_CLASSES`,
`SP20_STATS_BLOCK`, `SP20_STATS_HIST_BINS`, `dynamic_shmem_bytes`)
the wire-up will consume.
### Pearls + invariants honoured
- `feedback_no_atomicadd` — block tree-reduce + per-warp tile
binning; no atomicAdd anywhere.
- `feedback_no_cpu_compute_strict` — every operation GPU-side.
- `feedback_no_htod_htoh_only_mapped_pinned` — output is a
`MappedF32Buffer<2>`; kernel emits `__threadfence_system()`.
- `feedback_no_partial_refactor` — kernel + launcher + tests +
build entry land in one commit; production wire-up lands
atomically in Phase 1.4 with the EMA + controller producers.
- `pearl_fused_per_group_statistics_oracle` — one kernel
computing both p50 and std from a single stream of `aux_logits`.
- `pearl_no_host_branches_in_captured_graph` — single-block
kernel, captureable.
- `pearl_first_observation_bootstrap` — degenerate-distribution
branch (step_max == 0) writes `[0, 0]` (sentinel) so the
downstream EMA producer can apply Pearl-A bootstrap on the
next non-degenerate observation.
- `pearl_sp4_histogram_warp_tile_undercount` (test data) — test
vectors use varied per-row confidences (logit ramps, jittered
half-hot/half-uniform) to avoid the lockstep-uniform trap that
causes warp-tile race undercounts in the non-atomic per-warp
histogram.
### Tests
`crates/ml/tests/sp20_stats_compute_test.rs` — GPU oracle tests
(all `#[ignore = "requires GPU"]`):
1. `uniform_logits_emit_zero_stats``aux_logits = 0`
softmax = [1/3, 1/3, 1/3] ⇒ aux_conf = 0 ⇒ p50 = std = 0.
Exercises the kernel's degenerate-distribution branch.
2. `varied_confidence_logits_match_cpu_oracle` — class-0 logit
ramps 0 → 3, others 0; CPU oracle matches GPU within
bin_width tolerance for p50 (≈ 0.002) and 5e-3 for std.
3. `heterogeneous_logits_match_cpu_oracle` — half jittered
hot, half jittered uniform; CPU oracle matches GPU within
0.02 for p50 and 5e-3 for std.
4. `empty_batch_writes_zero_stats``B = 0` exercises the
pre-warmup degenerate guard; kernel writes `[0, 0]` and
returns without entering the histogram path.
Plus 4 unit tests in the launcher module (`AUX_K_CLASSES = 3`,
block-size discipline, shmem budget under L40S 48 KiB, zero-batch
shmem accounting).
### Files modified
| File | Status | Purpose |
|------|--------|---------|
| `crates/ml/src/cuda_pipeline/sp20_stats_compute_kernel.cu` | NEW | Single-block stats producer kernel |
| `crates/ml/src/cuda_pipeline/sp20_stats_compute.rs` | NEW | Rust launcher + contract constants |
| `crates/ml/tests/sp20_stats_compute_test.rs` | NEW | 4 GPU oracle tests |
| `crates/ml/src/cuda_pipeline/mod.rs` | +pub mod | Module declaration |
| `crates/ml/build.rs` | +cubin entry | Compile entry for nvcc |
| `docs/dqn-wire-up-audit.md` | This entry | Audit log |
### Verification
```
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check -p ml --features cuda
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml \
--test sp20_stats_compute_test --features cuda -- --ignored --nocapture
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml \
--features cuda --lib sp20_stats_compute
```
All 4 GPU oracle tests + 4 unit tests pass on RTX 3050 Ti (sm_86).
L40S verification deferred until Phase 1.4 lands the production
caller.
### Phase 1.1 → Phase 1.4 forward references
- Phase 1.2: `sp20_emas_compute_kernel.cu` consumes
`[aux_conf_p50, aux_conf_std]` (this kernel's output) +
per-trade `R_event` / WR-window observations to advance the
Wiener EMAs in ISV slots [510..520).
- Phase 1.3: `sp20_controllers_compute_kernel.cu` derives
`LOSS_CAP / TARGET_HOLD_PCT / N_STEP / AUX_CONF_THRESHOLD /
AUX_GATE_TEMP / HOLD_COST_SCALE` from the EMAs.
- Phase 1.4: training-loop wire-up calls all 3 producers in
sequence on the same stream as the aux-head forward.