Lands the SP20 Component 1 reward math as a header-only `__device__
__forceinline__` function in a new `sp20_reward.cuh` header, plus the
GPU oracle test scaffold for the 4-quadrant truth table per spec §4.1.
* `sp20_reward.cuh` — `sp20_compute_event_reward(close_pnl,
label_at_open_sign, loss_cap)` function. Returns bounded scalar
`R_event ∈ [loss_cap, +1.0]`. 4-quadrant table:
+1, +1 → +1.0 (right reason, right outcome)
+1, -1 → +0.5 (wrong reason, right outcome)
-1, -1 → -0.5 (right reason, wrong outcome)
-1, +1 → loss_cap (wrong reason, wrong outcome — ISV-driven)
0, * → 0.0 (no information — close_pnl == 0)
Sentinel `label_at_open_sign == 0` (Task 2.0 contract) maps to
dir_match=false ⇒ wrong-reason quadrant. Documented as the safer
default in the header comment.
* `sp20_event_reward_test_kernel.cu` — standalone single-thread test
wrapper, mirrors the `sp12_reward_math_test_kernel.cu` /
`thompson_test_kernel.cu` pattern. Outputs to a mapped-pinned [1]
f32 with `__threadfence_system()` for PCIe-visible coherence.
* `tests/sp20_event_reward_test.rs` — 8 GPU oracle tests
(`#[ignore = "requires GPU"]`-gated):
1. quadrant_right_reason_right_outcome
2. quadrant_wrong_reason_right_outcome
3. quadrant_right_reason_wrong_outcome
4. quadrant_wrong_reason_wrong_outcome_loss_cap_at_minus_1
5. quadrant_wrong_reason_wrong_outcome_loss_cap_at_minus_2
6. zero_pnl_returns_zero
7. sentinel_label_winning_trade_lands_shoulder (Task 2.0 contract)
8. sentinel_label_losing_trade_lands_loss_cap (Task 2.0 contract)
* `experience_kernels.cu` — `#include "sp20_reward.cuh"` so Task 2.2's
trade-close site replacement has the function in scope.
* `build.rs` — `sp20_event_reward_test_kernel.cu` cubin entry.
No production callers in this commit. Task 2.2 atomically:
- replaces the SP12 v3 reward block at experience_kernels.cu:3216-3380
with the SP20 4-quadrant reward;
- adds the `is_close` consumer of `label_at_open_per_env[i]` (Task
2.0's per-env scratch);
- threads per-env alpha through the SP20 aggregation kernel,
making `alpha_ema` non-zero post-trade-close (replacing the
Phase 1.4 0.0 forward-reference placeholder).
Per `feedback_no_partial_refactor.md` the device function ships
BEFORE the production caller so Task 2.1's GPU oracle tests can
exercise the math in isolation; the partial refactor that breaks
the no-partial-refactor rule is "feature behind the function with
no caller", which this commit's tests resolve in scope.
Per `feedback_no_cpu_test_fallbacks.md` GPU oracle only — no CPU
reference impl. Per `feedback_no_htod_htoh_only_mapped_pinned.md`
all CPU↔GPU buffers are mapped-pinned (`MappedF32Buffer`).
Verification:
SQLX_OFFLINE=true cargo check -p ml --tests --features cuda # clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>