feat(sp20): Phase 2 Task 2.1 — sp20_compute_event_reward + 8 GPU oracle tests
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>
This commit is contained in:
@@ -2,6 +2,128 @@
|
||||
|
||||
**Status:** Populated during Plan 1 Task 6 (A.5 orphan audit). Updated on every commit per Invariant 7.
|
||||
|
||||
## 2026-05-09 — SP20 Phase 2 Task 2.1: sp20_compute_event_reward device function + GPU oracle tests (additive)
|
||||
|
||||
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.
|
||||
The function has no production callers in this commit — Task 2.2's
|
||||
trade-close site replacement wires it in atomically with the SP12 v3
|
||||
reward block deletion.
|
||||
|
||||
### Why a dedicated header (`sp20_reward.cuh`) instead of inlining in `experience_kernels.cu`
|
||||
|
||||
The plan called for adding `sp20_compute_event_reward` "in
|
||||
`experience_kernels.cu`". The user-decision review of Phase 2's six
|
||||
gaps revised this to "place it near the other SP20 helpers in that
|
||||
file" — i.e., the spirit was "don't put it in `trade_physics.cuh` (which
|
||||
is for trade physics)", not "literally inside the .cu file".
|
||||
|
||||
The test wrapper (`sp20_event_reward_test_kernel.cu`) needs to call
|
||||
the SAME math the production caller does — per
|
||||
`feedback_no_cpu_test_fallbacks` GPU oracle tests share the production
|
||||
function bit-for-bit. Three options:
|
||||
|
||||
- **Inline in `experience_kernels.cu` only** — test kernel cannot
|
||||
reach the function (it's in a `.cu` file, not a header). Breaks
|
||||
`feedback_no_quickfixes` (would force code duplication in the test
|
||||
kernel).
|
||||
- **Inline in `trade_physics.cuh`** — bloats trade_physics with SP20
|
||||
reward logic (rejected by user).
|
||||
- **New `sp20_reward.cuh` header** — clean SP20 namespace, included
|
||||
by both `experience_kernels.cu` (Task 2.2) and the test wrapper
|
||||
(this commit). **Chosen.**
|
||||
|
||||
Mirrors the `compute_asymmetric_capped_pnl` / `compute_min_hold_penalty`
|
||||
pattern in `trade_physics.cuh` for shared SP12 reward helpers.
|
||||
|
||||
### 4-quadrant truth table (spec §4.1)
|
||||
|
||||
| `pnl_sign` | `dir_match` | `R_event` | Constant |
|
||||
|------------|--------------|-------------------|--------------------------------|
|
||||
| `+1` | `true` | `+1.0` | `SP20_WIN_CAP` |
|
||||
| `+1` | `false` | `+0.5` | `SP20_SHOULDER_RIGHT_OUTCOME` |
|
||||
| `-1` | `true` | `-0.5` | `SP20_SHOULDER_WRONG_OUTCOME` |
|
||||
| `-1` | `false` | `loss_cap` | ISV[LOSS_CAP_INDEX] (caller) |
|
||||
| `0` | `*` | `0.0` | early-return |
|
||||
|
||||
`label_at_open_sign == 0` (sentinel: aux skip-window or
|
||||
no-position-open at trade-open per Task 2.0) maps to `dir_match =
|
||||
false` because the comparison `pnl_sign == 0` is never true once
|
||||
`pnl_sign` is filtered to `±1`. This routes sentinel inputs to the
|
||||
wrong-reason quadrant — `+0.5` shoulder on winning trades, `loss_cap`
|
||||
on losing trades. Safer default than over-rewarding a no-signal close
|
||||
(documented in `sp20_reward.cuh` header comment).
|
||||
|
||||
### Tests
|
||||
|
||||
`crates/ml/tests/sp20_event_reward_test.rs` — 8 GPU oracle tests
|
||||
(all `#[ignore = "requires GPU"]`):
|
||||
|
||||
1. `quadrant_right_reason_right_outcome` — pnl=+, label=+ → +1.0
|
||||
2. `quadrant_wrong_reason_right_outcome` — pnl=+, label=− → +0.5
|
||||
3. `quadrant_right_reason_wrong_outcome` — pnl=−, label=− → −0.5
|
||||
4. `quadrant_wrong_reason_wrong_outcome_loss_cap_at_minus_1` — cold-start cap
|
||||
5. `quadrant_wrong_reason_wrong_outcome_loss_cap_at_minus_2` — mature cap
|
||||
6. `zero_pnl_returns_zero` — early-return path
|
||||
7. `sentinel_label_winning_trade_lands_shoulder` — Task 2.0 contract
|
||||
8. `sentinel_label_losing_trade_lands_loss_cap` — Task 2.0 contract
|
||||
|
||||
Tests 7-8 lock the Task 2.0 sentinel-label contract: when the
|
||||
FoldReset-zeroed `label_at_open_per_env` slot reaches the reward
|
||||
function (no `entering_trade` fired this fold OR the aux landed in
|
||||
skip-window), the wrong-reason quadrant fires. This pins the Task 2.0
|
||||
"safer default" semantic against future regressions that might add a
|
||||
"sentinel = pass-through" early-return.
|
||||
|
||||
### Files modified
|
||||
|
||||
| File | Status | Purpose |
|
||||
|------|--------|---------|
|
||||
| `crates/ml/src/cuda_pipeline/sp20_reward.cuh` | NEW | Header with `sp20_compute_event_reward` |
|
||||
| `crates/ml/src/cuda_pipeline/sp20_event_reward_test_kernel.cu` | NEW | Standalone GPU oracle test wrapper |
|
||||
| `crates/ml/src/cuda_pipeline/experience_kernels.cu` | +1 include | Pre-includes for Task 2.2 caller |
|
||||
| `crates/ml/build.rs` | +1 cubin entry | Compile entry for nvcc |
|
||||
| `crates/ml/tests/sp20_event_reward_test.rs` | NEW | 8 GPU oracle tests |
|
||||
| `docs/dqn-wire-up-audit.md` | This entry | Audit log |
|
||||
|
||||
### Verification
|
||||
|
||||
```
|
||||
SQLX_OFFLINE=true cargo check -p ml --tests --features cuda # clean
|
||||
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml \
|
||||
--test sp20_event_reward_test --features cuda -- --ignored --nocapture
|
||||
```
|
||||
|
||||
### Phase 2 Task 2.1 → Task 2.2 forward references
|
||||
|
||||
Task 2.2 atomically:
|
||||
- replaces the SP12 v3 inlined reward block at
|
||||
`experience_kernels.cu:3216-3380` (segment_complete branch only)
|
||||
with the SP20 4-quadrant reward via `sp20_compute_event_reward`;
|
||||
- adds the `is_close` consumer of `label_at_open_per_env[i]` (Task
|
||||
2.0's per-env scratch);
|
||||
- adds per-env `alpha_per_env[N]` plumbing through the SP20
|
||||
aggregation kernel signature, so `alpha_ema` (ISV[ALPHA_EMA_INDEX])
|
||||
becomes non-zero post-trade-close (replacing the Phase 1.4 `0.0`
|
||||
forward-reference placeholder);
|
||||
- updates the 2 placeholder pin tests (`sp20_aggregate_inputs_test::
|
||||
alpha_and_per_bar_hold_reward_are_phase_2_and_3_2_placeholders`
|
||||
and `sp20_phase1_4_wireup_test::
|
||||
alpha_and_hold_reward_emas_stay_at_zero_phase_2_3_2_placeholders`)
|
||||
to assert the new contract: `alpha_ema` becomes non-zero after at
|
||||
least one trade close; `hold_reward_ema` stays at sentinel until
|
||||
Phase 3.2.
|
||||
|
||||
Per-bar SP18 D-leg call sites at `experience_kernels.cu:3722` and
|
||||
`:3833` are RETAINED pending Phase 3.2 Component 2 replacement (per
|
||||
`feedback_no_stubs.md`: working signal stays until replacement lands).
|
||||
|
||||
`compute_sp18_hold_opportunity_cost`, `compute_asymmetric_capped_pnl`,
|
||||
and `compute_min_hold_penalty` device functions in `trade_physics.cuh`
|
||||
are RETAINED — they remain called by per-bar SP18 sites and by the
|
||||
`sp12_reward_math_test_kernel.cu` test wrapper.
|
||||
|
||||
## 2026-05-09 — SP20 Phase 2 Task 2.0: per-env trade-open label-sign infrastructure (additive)
|
||||
|
||||
Lands the producer-side infrastructure for Phase 2's 4-quadrant reward
|
||||
|
||||
Reference in New Issue
Block a user