Files
foxhunt/docs/sp18-wireup-audit.md
jgrusewski 5ee5a1b655 chore(sp18): refresh audit fingerprint after Phase 4 docstring update
Off-by-one count drift in the td_lambda_kernel launch-sites bucket
(19→20) from a single additional reference in sp18_td_lambda_q_next_oracle_tests.rs's
docstring (the docstring now mentions td_lambda_kernel one extra time
per the T_SKEL.3 atomic-refactor guard test). Pure documentation
update; no source-code change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:26:17 +02:00

538 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SP18 Combined-Scope Wire-Up Audit
> Audit captured during Phase 1 Task 1.1 of `docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md`.
>
> **Branch**: `feat/sp18-combined` @ `0b737ec30` (Phase 0 baseline, pre-Phase-1 atomic deletion).
>
> **Audit script**: `scripts/audit_sp18_consumers.sh`. Re-run after every commit that touches the SP13/SP16 chain (D-leg) or the TD(λ) `q_next` site (B-leg). Pre-commit hook (Task 1.2) enforces snapshot-vs-current diff via `--check`.
## Outcome of Phase 1 Task 1.1 (audit-first, halt-on-drift)
The pre-dispatch consumer-audit run surfaced **multiple consumers NOT in the plan's locked checklist** (spec §"Architecture Changes"). Per task input — *"If your audit surfaces ANY additional consumer, halt and report — do NOT proceed with deletion ad-hoc"* — Phase 1 Tasks 1.3-1.5 (atomic deletion) are **HALTED** pending plan / spec amendment.
### Plan's locked D-leg checklist (8 entries)
| # | Site | Action |
|---|------|--------|
| 1 | `experience_kernels.cu:3162-3166, 3637-3643, 3710-3716` (3 cost sites) | DELETE — replace with placeholder comment per Task 1.5 |
| 2 | `training_loop.rs:564` (launcher call) + `:631-667` (hold_cost_scale_diag emit) + `:4171-4226` (host controller block) | DELETE launcher + diag emit + controller; RETAIN slot-382 min_hold_temp_diag emit at `:567-617` |
| 3 | `crates/ml/src/cuda_pipeline/hold_cost_scale_update_kernel.cu` | DELETE entire file |
| 4 | `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs` slots [461..468) (`HOLD_COST_SCALE_INDEX` + `HCS_*` Welford) + their `MIN`/`MAX`/`SENTINEL` constants | DELETE constants + the layout-locked tests for them |
| 5 | `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` `launch_hold_cost_scale_update` method (line 15837) + `HOLD_COST_SCALE_UPDATE_CUBIN` static (line 2257) + struct field `hold_cost_scale_update_ops` (line 7853) + constructor wiring (lines 24433-24434, 25296) | DELETE all |
| 6 | `crates/ml/src/trainers/dqn/state_reset_registry.rs` 7 entries (`sp16_phase2_hold_cost_scale_adaptive` + `sp16_t3_hcs_*` × 6) | DELETE entries |
| 7 | `crates/ml/build.rs:881` cubin manifest entry `hold_cost_scale_update_kernel.cu` | DELETE |
| 8 | `crates/ml/src/cuda_pipeline/state_layout.cuh:214` (`ISV_HOLD_COST_SCALE_IDX`) + `:228-233` (`ISV_HCS_*` mirror) | DELETE |
### Additional consumers surfaced by audit (NOT in plan checklist)
The following consumers must be added to the atomic-deletion commit. They were missed by the plan's spec §"Architecture Changes" and would have left the deletion partial — per `feedback_no_partial_refactor.md` this would block compile or, worse, leave dangling references that compile but break test/integration paths.
| # | Site | Type | Resolution |
|---|------|------|------------|
| **A1** | `crates/ml/src/cuda_pipeline/gpu_aux_trunk.rs:1240-1323``HoldCostScaleUpdateOps` struct + `impl` (kernel-load + launcher) | **PRODUCER WRAPPER** (Rust-side). The plan said the launcher is in `gpu_dqn_trainer.rs` but actually only the trainer's *thin call site* `launch_hold_cost_scale_update` is there; the real `HoldCostScaleUpdateOps::new`/`launch` impl lives in `gpu_aux_trunk.rs`. | DELETE the struct + impl block (84 lines). DELETE the `HOLD_COST_SCALE_UPDATE_CUBIN` import at `gpu_aux_trunk.rs:56` (transitively unused after the struct is gone). |
| **A2** | `crates/ml/tests/sp14_oracle_tests.rs:2174-2920` — 11 GPU oracle tests + the doc-block at the head | **TESTS** (75 occurrences of HOLD_COST_SCALE / HCS_TARGET / hold_cost_scale_update across this file). Tests: `sp16_phase2_hold_cost_scale_climbs_with_overrun`, `sp16_phase2_hold_cost_scale_at_target_is_one`, `sp16_phase2_hold_cost_scale_under_target_is_one`, `sp16_phase2_hold_cost_scale_bounds_clamp`, `sp16_phase2_hold_cost_scale_pearl_a_bootstrap`, `sp16_phase3_alpha_high_during_signal_jump`, `sp16_phase3_alpha_low_in_steady_state`, `sp16_phase3_pearl_a_bootstrap_first_obs`, `sp16_phase3_alpha_naturally_bounded`, `sp16_phase3_no_hardcoded_alpha`, plus the helper `recover_alpha`/`compute_target_scale`/`run_synthetic_sequence` infrastructure (~750 lines). All exercise the now-deleted `hold_cost_scale_update` kernel via `include_bytes!(.../hold_cost_scale_update_kernel.cubin)` — without the kernel they fail to compile. | DELETE the 2 test modules (`sp16_phase2_hold_cost_scale_gpu` + `sp16_t3_hold_cost_scale_alpha_gpu`) and the standalone `sp16_phase3_no_hardcoded_alpha` test. ~750 lines from `sp14_oracle_tests.rs:2174-2920`. |
| **A3** | `crates/ml/src/trainers/dqn/trainer/training_loop.rs:8971-9043` — 7 dispatch arms in `reset_named_state` (`"sp16_phase2_hold_cost_scale_adaptive"` + `"sp16_t3_hcs_*"` × 6) | **DISPATCH ARMS**. Required atomically with the registry-entry deletion (Task 1.4 expanded scope) — the contract test `every_fold_and_soft_reset_entry_has_dispatch_arm` (state_reset_registry.rs:2120-2280) fails if the registry has an entry but no dispatch arm OR vice versa. | DELETE the 7 match arms (slot 461 + HCS slots 462-467) atomically with the registry entries. |
| **A4** | `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs:23200-23216` — constructor block writing `HOLD_COST_BASE` to `HOLD_COST_INDEX=380` + `HOLD_RATE_TARGET_DEFAULT` to slot 381 | **CONSTRUCTOR WRITE** of slot 380 (controller output anchor). Per DD7c, slot 380 is RETAINED as part of the observation chain — but it now has no producer. The constructor-write of `HOLD_COST_BASE` keeps it at 0.005 cold-start. After deletion of the host controller, slot 380 becomes a **read-only diagnostic slot** (constructor-init only, no per-step rewrites). | KEEP the constructor-write (DD7c retains slot 380). UPDATE the comment block at lines 23201-23210 to reflect the post-SP18-Phase-1 reality: "After SP18 Phase 1, slot 380 is no longer rewritten by a host-side controller (deleted per DD7=c). The constructor-init is now its only assignment — it serves as a read-only diagnostic anchor surfacing the static base cost. Phase 2 lands the structural Hold opportunity-cost reward at the 3 deleted cost-subtraction sites; that reward does NOT consume slot 380." |
| **A5** | `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs:617, 2245-2256` — doc-block prose referring to deleted machinery | **DOC PROSE**. References `launch_hold_cost_scale_update`, `gpu_aux_trunk::HoldCostScaleUpdateOps`, `isv[461]`, etc. | UPDATE comments to remove deleted-machinery references; replace with "(deleted SP18 Phase 1, DD7=c — see docs/sp18-wireup-audit.md)" pointer. No code change. |
| **A6** | `crates/ml/src/cuda_pipeline/sp13_isv_slots.rs:73-77``HOLD_COST_BASE`, `HOLD_COST_CONTROLLER_GAIN`, `HOLD_COST_FLOOR_RATIO`, `HOLD_COST_CEIL_RATIO` constants | **HOST-CONTROLLER CONSTANTS**. Used ONLY by the host controller block at `training_loop.rs:4213-4223` we're deleting. Per the SP13 v3 P0a comment block at `sp13_isv_slots.rs:50-72`, `HOLD_COST_BASE` is also used by the constructor-write at `gpu_dqn_trainer.rs:23215`. | KEEP `HOLD_COST_BASE` (consumed by constructor-init at A4 — slot 380 retained). DELETE `HOLD_COST_CONTROLLER_GAIN`, `HOLD_COST_FLOOR_RATIO`, `HOLD_COST_CEIL_RATIO` (sole consumer is the deleted controller block). |
| **A7** | `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs:5579-5585` — comment doc-ref to deleted controller | **COMMENT DOC-REF**. References `training_loop.rs:3604` (stale line — now `:4209`) and the priced-cost flow. | UPDATE prose to reflect post-Phase-1 reality. |
| **A8** | `crates/ml/src/cuda_pipeline/sp5_isv_slots.rs:325-327` — comment doc-ref describing the controller chain | **COMMENT DOC-REF**. Documents slot 380/381/382. Slots 381 + 382 retained per DD7c; slot 380 retained but no longer controller-written. | UPDATE prose to reflect post-Phase-1 reality. |
| **A9** | `crates/ml/src/cuda_pipeline/state_reset_registry.rs:1138-1271` — registry entries already marked RETIRED in PP.4 (commit c66e15f6d) for slots 461 + 462-467 | **REGISTRY ENTRIES**. Already marked RETIRED in Pre-Phase PP.4. Phase 1 promotes RETIRED-with-sentinel-0.0-write to FULL DELETION. | DELETE the 7 entries (slot 461 + HCS slots 462-467) from the registry; layout fingerprint must be updated atomically — the fingerprint seed string covers slot allocations [461..468) which become RESERVED-gap (SP14-C.1 pattern). Slots themselves stay allocated (ISV_TOTAL_DIM unchanged) to preserve checkpoint compatibility. |
| **A10** | `crates/ml/src/trainers/dqn/state_reset_registry.rs:2256-2308``lock_sp18_v2_pp4_retired_chain` test which asserts the 7 retired entries STILL EXIST | **CONTRACT TEST**. PP.4 added a lock test asserting the entries exist with sentinel 0.0. After full deletion, this test must be deleted (the entries no longer exist, so asserting their existence panics). | DELETE the `lock_sp18_v2_pp4_retired_chain` test (or rewrite to assert the slots are NOT registered). |
### Out-of-scope mentions (do NOT delete; verify only)
These show up in the audit grep but per DD7c are RETAINED for HEALTH_DIAG continuity OR are independent modules:
| Site | Reason RETAINED |
|------|----------------|
| `crates/ml/src/cuda_pipeline/hold_rate_observer_kernel.cu` | Producer of slot 382 (observation chain). DD7c retains entire observation chain. |
| `crates/ml/src/cuda_pipeline/min_hold_temperature_update_kernel.cu` | Independent SP16-P1 producer of slot 460 (different chain). Reads same slots 381+382 but writes different output. RETAINED. |
| `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs` MHT_* slots [468..474) | SP16-P1 (MIN_HOLD_TEMPERATURE) Welford accumulators — distinct chain. RETAINED. |
| `crates/ml/src/cuda_pipeline/sp13_isv_slots.rs:43` `HOLD_COST_INDEX = 380` | Slot constant for the retained observation chain. RETAINED. |
| `crates/ml/src/cuda_pipeline/state_layout.cuh:200-202` `ISV_HOLD_COST_IDX/ISV_HOLD_RATE_TARGET_IDX/ISV_HOLD_RATE_OBSERVED_EMA_IDX` (slots 380/381/382) | Mirror constants for retained observation chain. RETAINED. |
| `crates/ml/src/trainers/dqn/state_reset_registry.rs:939` `sp13_hold_rate_observed_ema` registry entry for slot 382 | FoldReset entry for retained observation chain. RETAINED. |
| `crates/ml/tests/sp13_phase0_oracle_tests.rs` references to `hold_rate_observer_kernel` | Tests for retained observer kernel. RETAINED. |
| `docs/superpowers/specs/2026-05-04-sp13-redefine-success-for-predictive-skill.md` and related plan docs | Historical spec/plan archaeology. NOT modified. |
## Path forward
Two options:
1. **EXPAND deletion scope to cover all 10 additional consumers (A1-A10) atomically in the same Phase 1 commit.** The plan's spec §"Architecture Changes" was incomplete; this audit makes the scope explicit. The atomic commit grows from ~8 sites to ~18 sites but stays within the same conceptual change ("delete SP13/SP16 cost-application chain"). Per `feedback_no_partial_refactor`.
2. **Halt and request human review.** The plan author may have intended these consumers to be discovered iteratively (the plan says "trust the script, not the plan author"). But the script's purpose is to *surface* missed consumers BEFORE the deletion, exactly so the deletion stays atomic. Halting honours that intent; expanding the scope ad-hoc honours the deletion's atomicity.
**Recommended**: Option 1, with this audit doc serving as the spec-amendment record. The 10 additional consumers all flow from the same chain — none introduce new architectural concerns. They just expand the byte-count of the atomic-deletion commit.
## Status
**Phase 1 Tasks 1.3-1.6 LANDED (2026-05-09)** — atomic deletion of all 18 sites
(8 plan + 10 audit-expanded A1-A10) committed in a single atomic commit per
`feedback_no_partial_refactor`. The branch was placed in INTERIM STATE: 3 reward
sites (`experience_kernels.cu` segment_complete + per-bar positioned-Hold +
per-bar flat-Hold) were placeholder-commented but contained no Hold cost.
**Phase 2 Tasks 2.1-2.5 LANDED (2026-05-09)** — atomic introduction of the
`compute_sp18_hold_opportunity_cost` device function + 3-site migration +
CPU/GPU oracle tests in a single commit per `feedback_no_partial_refactor`.
**INTERIM STATE LIFTED.** L40S dispatch remains FORBIDDEN pending Phase 3 cap
producer + Phase 4-5 B-leg, but the reward function is now structurally
complete (no placeholder sites, no orphaned consumers).
Per DD7c, observation chain preserved: slot 380 (HOLD_COST_INDEX, constructor-
init-only diagnostic at HOLD_COST_BASE=0.005), slot 381
(HOLD_RATE_TARGET_INDEX, constructor-init at 0.20), slot 382
(HOLD_RATE_OBSERVED_EMA_INDEX, FoldReset chain still wired). MHT_* slots
[468..474) preserved (MIN_HOLD_TEMPERATURE chain RETAINED).
Sites deleted (atomic commit):
- 3 reward subtraction sites in experience_kernels.cu (placeholder comments)
- hold_cost_scale_update_kernel.cu (file deleted)
- HoldCostScaleUpdateOps in gpu_aux_trunk.rs (struct + impl, ~120 lines)
- launch_hold_cost_scale_update + cubin static + struct field + constructor
+ field-init in gpu_dqn_trainer.rs (5 sites)
- HOLD_COST_SCALE_INDEX, HCS_* slots, sentinel/bounds, layout-locked tests in
sp14_isv_slots.rs
- HOLD_COST_CONTROLLER_GAIN/FLOOR/CEIL constants in sp13_isv_slots.rs
- ISV_HOLD_COST_SCALE_IDX + ISV_HCS_* mirror constants in state_layout.cuh
- build.rs cubin manifest entry
- 7 fold-reset registry entries (slot 461 + HCS_* 462..468)
- 7 dispatch arms in training_loop.rs
- Host controller block in training_loop.rs (lines ~4171-4226)
- launcher call site + hold_cost_scale_diag emit block in training_loop.rs
- 11 GPU oracle tests + helpers in sp14_oracle_tests.rs (~750 lines)
- lock_sp18_v2_pp4_retired_chain test (deleted, no inverse-contract
replacement — pointless ceremony per user call)
- Layout fingerprint seed updated (HOLD_COST_SCALE + HCS_* removed; range
[461..468) is RESERVED gap; ISV_TOTAL_DIM stays at 507 per checkpoint
compatibility — RESERVED-gap pattern from SP14-C.1)
**Validation: cargo check --workspace clean; cargo test -p ml --lib passes
(state_reset_registry::every_fold_and_soft_reset_entry_has_dispatch_arm,
sp16_t3_wiener_welford_slot_layout_locked, sp18_combined_slot_layout_locked
all OK).**
### Phase 2 sites introduced (atomic commit)
- `crates/ml/src/cuda_pipeline/trade_physics.cuh` — new device function
`compute_sp18_hold_opportunity_cost` after `compute_lump_sum_opp_cost`.
Single forward-only function; reuses `compute_asymmetric_capped_pnl` for
the bilateral cap. No new infrastructure beyond existing primitives.
- `crates/ml/src/cuda_pipeline/experience_kernels.cu` — 3 placeholder
comments replaced atomically:
- Site 1 (segment_complete branch ~3143): unconditional add into
`base_reward` BEFORE the SP12 asymmetric cap; preserves the SP12 cap
interaction and cap participation per the original SP13 P0a comment
contract.
- Site 2 (per-bar positioned-Hold ~3641): unconditional add into
`r_micro` (rc[3]) — same component the deleted subtraction wrote to
pre-SP18, preserving SP11 reward-subsystem controller signal
attribution. `vol_proxy` recomputed locally from
`features[bar_idx * market_dim + 9]` (the per-bar branches are
outside the segment_complete `vol_proxy` scope).
- Site 3 (per-bar flat-Hold ~3760): unconditional add into `r_opp_cost`
(rc[4]) — same component the deleted subtraction wrote to pre-SP18.
`vol_proxy` recomputed locally as in Site 2.
- `crates/ml/src/cuda_pipeline/sp18_hold_opp_test_kernel.cu` — new GPU
oracle test wrapper (single-thread single-block). Wraps the device fn
for the GPU oracle test; mapped-pinned f32 input/output per
`feedback_no_htod_htoh_only_mapped_pinned`.
- `crates/ml/build.rs` — added `sp18_hold_opp_test_kernel.cu` to the
cubin manifest.
- `crates/ml/tests/sp18_hold_reward_oracle_tests.rs` — added T1 (CPU
oracle, 6 cases covering positive/negative/saturation/dir-gate/
shaping_scale=0) + T2 (GPU oracle parity with CPU at ε=1e-5) per spec
§"Behavioral Test Predictions".
### Phase 2 design notes
- **`vol_proxy` scoping**: the per-bar Hold sites (Sites 2-3) are outside
the segment_complete `vol_proxy` scope (declared at
`experience_kernels.cu:3124`). Each per-bar site recomputes
`vol_proxy` locally from the same `features[bar_idx * market_dim + 9]`
ATR-norm input the segment_complete branch and the B.2/D.4c bonus
blocks consume. The `vol_proxy_pb1`/`vol_proxy_pb2` locals mirror the
segment_complete computation modulo the `× sqrt(hold_time)` term
(which is 1 for per-bar / hold_time=1 — matches the device fn's
contract).
- **ISV cap fallback**: each call site reads `ISV[483]` (POS_CAP) and
`ISV[484]` (NEG_CAP) with the same sentinel-or-out-of-bounds guard
used at Site 1: if the slot value is within ε of the sentinel OR
outside `[POS_CAP_MIN_BOUND, POS_CAP_MAX_BOUND]`, the call falls
through to the macro defaults (+5/-10). This is bit-identical to a
fixed +5/-10 cap during cold-start and Pre-Phase-3 (when the producer
kernel `hold_reward_cap_update_kernel` has not yet landed).
- **Reward-component slot routing**: Sites 1/2/3 write to slots 0/3/4
(popart/micro/opp_cost) per the SP15 Wave 2 reward-component layout.
Routing matches the deleted SP13/SP16 subtraction destinations
exactly, so the SP11 reward-subsystem controller's component-weight
estimator sees the same input distribution shape (modulo the
structural-counterfactual vs reactive-cost magnitude shift, which is
precisely the change the controller is meant to learn).
- **`shaping_scale` gating**: the device fn multiplies the capped output
by `shaping_scale`. Validation runs at `shaping_scale=0` → the device
fn returns 0.0 → no Hold opp cost in pure backtest, matching the SP12
lump-sum `compute_lump_sum_opp_cost` contract.
### Phase 3 sites introduced (atomic commit, 2026-05-09)
D-leg adaptive HOLD_REWARD_POS/NEG_CAP producer kernel + launcher +
per-epoch wiring + GPU oracle tests. Lifts the Phase 2 caps from
"sentinel-driven cold-start" (5.0/-10.0 fixed) to "p99(|step_ret|) over
Long/Short trade closes × 1.5 safety factor" with Wiener-optimal α blend
per `pearl_wiener_optimal_adaptive_alpha`. The Phase 2 consumer
(`compute_sp18_hold_opportunity_cost`) sees the producer-driven slots
[483]/[484] from epoch 1 onward; the sentinel branch in the consumer
remains as the cold-start fallback path (zero-trade-close epoch ⇒
producer early-returns ⇒ slots stay at sentinel ⇒ consumer falls
through to the +5/-10 macro defaults — bit-identical to pre-Phase-3
behavior).
Sites introduced (single atomic commit per `feedback_no_partial_refactor`):
- **Kernel**: `crates/ml/src/cuda_pipeline/hold_reward_cap_update_kernel.cu`
(NEW, ~310 lines). Single-block 256-thread producer; mirrors the SP14
P0-A `reward_cap_update_kernel` structural template with three
differences:
1. Filter: `(is_close && step_ret != 0)` — both winners AND losers
(the consumer needs the magnitude scale of *all* Long/Short
closes, not just winners). Position-side cap filters
`(is_close && step_ret > 0)`.
2. Welford-derived Wiener-α (slots [487..493)) replaces fixed α=0.01.
Floor at WELFORD_ALPHA_MIN=0.4 per
`pearl_wiener_alpha_floor_for_nonstationary` — the policy-realised
Long/Short close magnitude distribution is intrinsically
non-stationary as the policy adapts, sharpening tails.
3. Bounds [HOLD_REWARD_POS_CAP_MIN_BOUND=0.5,
HOLD_REWARD_POS_CAP_MAX_BOUND=50.0] (vs. position-side [1.0, 50.0]).
- **Cubin manifest**: `crates/ml/build.rs` — added kernel entry. Build-rs
cubin static `HOLD_REWARD_CAP_UPDATE_CUBIN` exposed in
`gpu_dqn_trainer.rs:2190`.
- **Producer wrapper**: `crates/ml/src/cuda_pipeline/gpu_aux_trunk.rs`
new `HoldRewardCapUpdateOps` struct (mirrors `RewardCapUpdateOps`)
with `new`/`launch` impl; pre-loads `CudaFunction` at construction per
`pearl_no_host_branches_in_captured_graph.md`.
- **Trainer wiring**: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`:
- Struct field `hold_reward_cap_update_ops` at line 7828.
- Constructor instantiation at line 24409.
- Field-init in struct literal at line 25293.
- Method `launch_hold_reward_cap_update` placed right after
`launch_reward_cap_update`.
- **Per-epoch wire-up**:
`crates/ml/src/trainers/dqn/trainer/training_loop.rs` line 511 —
`fused.trainer().launch_hold_reward_cap_update(step_ret_ptr,
trade_close_ptr, total_samples)` placed right after
`launch_reward_cap_update` so both cap producers share the same
source buffers (read-only; independent ISV slot pairs at
[452]/[453] and [483]/[484]).
- **HEALTH_DIAG**: `training_loop.rs` (post `td_error_pre` emit) — new
line `HEALTH_DIAG[N]: hold_reward_cap [pos={:.4} neg={:.4}
fire_rate={:.4}]`. Reads ISV[483]/[484]/[486] via
`read_isv_signal_at`; slot 486 (HOLD_OPP_COST_FIRE_RATE_EMA) is the
engagement-canary diag for Phase 8 to populate.
- **GPU oracle tests**:
`crates/ml/tests/sp18_hold_reward_oracle_tests.rs` — three new tests
in the `mod gpu` block:
- `hold_reward_cap_producer_drives_slots` — T5 from spec: 100
simulated Long/Short closes with jittered ~N(0, 0.01); verifies
ISV[483] is bilaterally clamped to MIN_BOUND=0.5 (small p99 ⇒
target_cap << bound), ISV[484] = -2 × ISV[483], filler outliers
(close=0) filtered out.
- `hold_reward_cap_pearl_a_first_observation_replace` — Pearl-A
bootstrap: 4 large closes with `step_ret = [+10, -20, +30, -40]`,
target_cap = max(p99, max) × 1.5 ≈ 76.5, clamped to MAX_BOUND=50,
Pearl-A REPLACE on sentinel writes 50.0 directly (NOT blended
with sentinel 5.0).
- `hold_reward_cap_no_closes_preserves_isv` — no closes
(trade_close=0 ∀i): every ISV slot (POS, NEG, all 6 Welford)
preserved bit-exactly. Verifies the cold-start fallback path.
- **Audit script**: `scripts/audit_sp18_consumers.sh --check` exits 0
(no fingerprint drift in tracked sections — SP18 D-leg/B-leg legacy
references unchanged; the new Phase 3 producer is in its own
symbol-namespace not tracked by the audit's snapshot, which is by
design — the audit tracks the DELETED SP13/SP16 legacy chain).
**Validation: cargo check --workspace clean (1m 12s); 3 GPU oracle
tests pass on local RTX 3050 Ti (1.93s wallclock).**
### Phase 3 design notes
- **Why Wiener-α (vs. fixed α=0.01 on the position-side cap)**: the
position-side cap drifts slowly because the underlying alpha
distribution is approximately stationary over training. The Hold-side
cap distribution is intrinsically NON-stationary as the policy adapts
(sizing tightens, trail shifts). Welford-derived α tracks that
non-stationarity; the 0.4 floor prevents the canonical "α decays to 0
once policy converges" trap (SP16 T3 regression at
train-multi-seed-b5gmp sha 641aa0dfd Fold 1: α=0.248 at HD4 → Hold%
52.6% at HD5 — see `pearl_wiener_alpha_floor_for_nonstationary`).
- **Why no separate Long/Short flag buffer**: `step_ret_per_sample[i]`
is non-zero ONLY on close events from positions that had non-zero
pre_sign — i.e. Long or Short positions. Hold/Flat positions don't
generate non-zero step_ret per `experience_kernels.cu:2855`.
Filtering by `(is_close && step_ret != 0)` is the cheapest correct
extraction of the Long/Short close distribution; no extra
dir_idx_per_sample buffer needed.
- **Cold-start fallback behavior**: when `total_count == 0` (no closes
this epoch — Hold-heavy phase), the kernel early-returns at the
guard in Phase 2 of its body. ALL slots (POS, NEG, all 6 Welford)
remain unchanged. The Phase 2 consumer
(`compute_sp18_hold_opportunity_cost`) sees the cold-start sentinels
(5.0/-10.0) and routes through its own `is_sentinel` branch to the
macro defaults +5/-10 — bit-identical to pre-Phase-3.
- **Coexistence with `reward_cap_update_kernel`**: both producers
read the same `step_ret_per_sample` + `trade_close_per_sample`
buffers (read-only; no shared state). They drive independent ISV
slot pairs and use different filters. Per-epoch boundary launches
are sequential (position-side first, Hold-side second) so the
two_kernel sequence is graph-capturable (single stream, no host
branches). The redundant block-tree-reduce on the same input
buffers is a micro-cost (≤ 2 × 64K samples × 4 floats per epoch =
cheap relative to per-step training cost).
## B-leg verification (Phase 1.1 sub-objective)
Per the plan's locked B-leg checklist:
- **`td_lambda_kernel` launch sites**: 1 site at `gpu_experience_collector.rs:4154` (per the plan). Verified.
- **`q_next` self-bootstrap**: line `~4143` confirmed (`let q_next = rewards`). Phase 5 will replace.
- **PER priority recomputation**: 5 sites in `crates/ml-dqn/src/gpu_replay_buffer.rs`, `crates/ml-dqn/src/replay_buffer_type.rs`, `crates/ml/src/trainers/dqn/config.rs`, `crates/ml/src/trainers/dqn/fused_training.rs`, `crates/ml/tests/gpu_per_integration_test.rs`. NONE store `q_next` — confirms B-DD4 (no PER migration needed).
- **C51 loss target-Q origin**: `c51_loss_kernel.cu` `block_bellman_project_f` already uses target-net Q at loss time — no migration needed.
- **`target_params_buf`**: 17 references in `gpu_dqn_trainer.rs`, Polyak-tracked, ready for B-leg consumption.
- **PopArt slot 63**: catalogued; B-DD11 reset is the only PopArt change in scope.
B-leg pre-conditions verified ✓ — no additional B-leg consumers surfaced (Phase 4-5 atomic-refactor scope is unchanged).
## Captured audit fingerprint (locked snapshot — pre-commit hook `--check` diffs against this)
The fingerprint below is a per-section, per-file hit count summary. It is short
enough to inline in markdown, sortable for human review, and stable enough to
diff cleanly. Catches both new-consumer drift (a file appears in a section
where it didn't before) and expanded-consumer drift (a file's hit count
changes). It does NOT catch line-by-line content drift within a single file —
that's checked by the per-section grep output diffed in PR review.
To regenerate:
```
scripts/audit_sp18_consumers.sh --fingerprint
# update this section between BEGIN/END markers, then:
git add docs/sp18-wireup-audit.md
# the pre-commit hook check_sp18_consumer_audit fails the next commit if
# scripts/audit_sp18_consumers.sh --check reports drift vs this snapshot.
```
After Phase 1's atomic deletion lands, this fingerprint will shrink dramatically
(slot 461 + HCS_* + hold_cost_scale_update sections drop to zero hits). That
re-capture is committed as part of Task 1.6 (close-out).
<!-- BEGIN audit_sp18_consumers.sh FINGERPRINT -->
```
=== D-leg: Slot 380 (HOLD_COST_INDEX) consumers ===
total_hits=39
crates/ml/build.rs 1
crates/ml/src/cuda_pipeline/state_layout.cuh 1
crates/ml/src/cuda_pipeline/hold_rate_observer_kernel.cu 1
crates/ml/src/cuda_pipeline/sp5_isv_slots.rs 1
crates/ml/src/cuda_pipeline/sp13_isv_slots.rs 1
crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs 2
crates/ml/src/trainers/dqn/trainer/training_loop.rs 3
crates/ml/src/trainers/dqn/state_reset_registry.rs 1
docs/sp18-wireup-audit.md 5
docs/dqn-wire-up-audit.md 10
docs/superpowers/specs/2026-05-04-sp13-redefine-success-for-predictive-skill.md 4
docs/superpowers/plans/2026-05-04-sp13-redefine-success-for-predictive-skill.md 5
docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md 4
=== D-leg: Slot 461 (HOLD_COST_SCALE_INDEX) consumers ===
total_hits=28
docs/sp18-wireup-audit.md 6
docs/dqn-wire-up-audit.md 9
docs/superpowers/plans/2026-05-08-sp16-hold-collapse-structural-fix.md 10
docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md 3
=== D-leg: Slots [462..468) (HCS_* Welford) consumers ===
total_hits=9
docs/dqn-wire-up-audit.md 8
docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md 1
=== D-leg: hold_cost_scale_update_kernel references ===
total_hits=58
docs/sp18-wireup-audit.md 11
docs/dqn-wire-up-audit.md 22
docs/superpowers/specs/2026-05-08-sp18-reward-shape-hold-attractor-design.md 4
docs/superpowers/plans/2026-05-08-sp16-hold-collapse-structural-fix.md 9
docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md 12
=== D-leg: hold_rate_observer_kernel (RETAINED — diag chain stays per DD7=c) ===
total_hits=160
crates/ml/tests/sp13_phase0_oracle_tests.rs 19
crates/ml/tests/sp14_oracle_tests.rs 20
crates/ml/build.rs 5
crates/ml/src/cuda_pipeline/min_hold_temperature_update_kernel.cu 6
crates/ml/src/cuda_pipeline/state_layout.cuh 1
crates/ml/src/cuda_pipeline/gpu_experience_collector.rs 12
crates/ml/src/cuda_pipeline/hold_rate_observer_kernel.cu 4
crates/ml/src/cuda_pipeline/sp5_isv_slots.rs 3
crates/ml/src/cuda_pipeline/sp13_isv_slots.rs 2
crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs 8
crates/ml/src/cuda_pipeline/gpu_aux_trunk.rs 5
crates/ml/src/cuda_pipeline/sp14_isv_slots.rs 5
crates/ml/src/trainers/dqn/trainer/training_loop.rs 13
crates/ml/src/trainers/dqn/state_reset_registry.rs 3
docs/sp18-wireup-audit.md 7
docs/isv-slots.md 1
docs/dqn-wire-up-audit.md 11
docs/superpowers/specs/2026-05-04-sp13-redefine-success-for-predictive-skill.md 6
docs/superpowers/specs/2026-05-08-sp18-reward-shape-hold-attractor-design.md 1
docs/superpowers/plans/2026-05-05-sp14-aux-q-wire-earned-gradient-flow.md 5
docs/superpowers/plans/2026-05-04-sp13-redefine-success-for-predictive-skill.md 20
docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md 3
=== D-leg: Per-bar Hold-cost subtraction sites (3 lines we change in Phase 1.5) ===
total_hits=
=== D-leg: state_layout.cuh mirror constants (HOLD_COST + HCS) ===
total_hits=1
crates/ml/src/cuda_pipeline/state_layout.cuh 1
=== D-leg: state_reset_registry slot 461 entries ===
total_hits=
=== D-leg: Cubin manifest references in build.rs ===
total_hits=2
crates/ml/build.rs 2
=== B-leg: td_lambda_kernel launch sites ===
total_hits=20
crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs 8
crates/ml/src/cuda_pipeline/gpu_experience_collector.rs 11
crates/ml/src/cuda_pipeline/nstep_kernel.cu 1
=== B-leg: q_next argument origin (the line ~4143 site) ===
total_hits=
=== B-leg: rewards_out consumers (downstream of TD(λ) output) ===
total_hits=
=== B-leg: PER priority computation sites (verify B-DD4: NO migration needed) ===
total_hits=18
crates/ml-dqn/src/gpu_replay_buffer.rs 7
crates/ml-dqn/src/replay_buffer_type.rs 3
crates/ml/tests/gpu_per_integration_test.rs 3
crates/ml/src/trainers/dqn/config.rs 4
crates/ml/src/trainers/dqn/fused_training.rs 1
=== B-leg: Replay buffer schema — q_next storage check (verify NO migration) ===
total_hits=
=== B-leg: c51_loss_kernel target-Q origin (verify already correct at loss time) ===
total_hits=
=== B-leg: target_params_buf consumers (verify Polyak-tracked, available at collection time) ===
total_hits=
=== B-leg: PopArt slot 63 references (verify B-DD11 reset is the only PopArt change) ===
total_hits=20
crates/ml/tests/sp18_hold_reward_oracle_tests.rs 17
crates/ml/tests/sp11_producer_unit_tests.rs 3
=== END ===
total_hits=
```
<!-- END audit_sp18_consumers.sh FINGERPRINT -->
## Phase 4 close-out — B-leg target-net forward additive infrastructure (2026-05-09)
Phase 4 lands the **additive dead-code** state for the B-leg target-Q
DDQN bootstrap that Phase 5 will swap into `td_lambda_kernel`'s `q_next`
argument at `gpu_experience_collector.rs:~4313` (post-Task-4.1 hoist).
**Scope landed in this commit:**
1. **Task 4.1**`next_states` build-site relocated. The
`build_next_states_f32` invocation now precedes the TD(λ) launch
block (no behavioral change — `next_states` had no observer between
its pre-Phase-4 build site and the bottom-of-fn batch assembly).
Pure ordering hoist; verified via the audit grep
`next_states\b` flagging 0 post-TD(λ) consumers.
2. **Task 4.2**`compute_q_next_target_bootstrap` skeleton method
added on `GpuExperienceCollector` with full plan-aligned signature:
```rust
pub fn compute_q_next_target_bootstrap(
&mut self,
next_states: &CudaSlice<f32>,
target_params_ptr: u64,
online_params_ptr: u64,
batch_size: usize,
) -> Result<CudaSlice<f32>, MLError>
```
Body is a hard-error early-return per `feedback_no_stubs` —
no production caller exists in Phase 4 (the caller is wired in
Phase 5 Task 5.1, replacing the self-bootstrap clone at line ~4313).
Tasks 4.3-4.5 fill in the body atomically before Phase 5 wires
the production call site. Returning Err instead of `todo!()`
satisfies Invariant 9 (no deferred-work markers); any accidental
pre-Phase-5 call site hard-errors with a clear pointer to the
open work.
**Phase 4 introspection tests** (no GPU required, all pass):
| Test | File | Purpose |
|---|---|---|
| `compute_q_next_target_bootstrap_method_exists` | `crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs` | Asserts method signature exists with documented parameters |
| `next_states_built_before_td_lambda` | same | Asserts Task 4.1 ordering invariant via offset-comparison |
| `td_lambda_still_consumes_self_bootstrap_q_next_in_phase4` | same | Atomic-refactor guard: Phase 4 STILL uses `q_next = rewards_out` clone — Phase 5 Task 5.1 swaps |
**Atomic-refactor invariant (HARD — `feedback_no_partial_refactor`):**
Phase 4 leaves `compute_q_next_target_bootstrap` as **dead code** (no
production caller, only introspection tests). Phase 5.1 makes it live
in lockstep with the `q_next` argument swap at the TD(λ) launch:
> **NO L40S DISPATCH between the Phase 4 close-out commit and the
> Phase 5 Task 5.1 commit** that replaces
> `let q_next = dtod_clone_f32_native(&self.stream, &self.rewards_out, ...)`
> with `let q_next = self.compute_q_next_target_bootstrap(&next_states, ...)`.
**Tasks 4.3-4.5 remaining (deferred to follow-up subagent dispatch):**
- **Task 4.3** — implement online-net forward on `next_states` for
DDQN per-branch argmax (mirror of `submit_forward_ops_ddqn` infra:
VSN+BN+trunk+branch-head chain via `cublas_forward.forward_online_raw`,
output: `[B, 4]` i32 per-branch argmax actions).
- **Task 4.4** — implement target-net forward via
`cublas_forward.forward_target_raw`, then `compute_expected_q` with
target's distributional outputs to produce per-action `E[Q]`; gather
per-branch argmax `Q_target[b, a*_online_branch]` and resolve final
scalar form (B-DD8.a: dir-branch only, OR sum across 4 branches).
GPU oracle tests T8 + T11 (`#[ignore = "requires GPU"]`).
- **Task 4.5** — replace `todo!()` body; close-out grep audit asserts
`todo!` absent in `compute_q_next_target_bootstrap`; Phase 4 ends
in additive-dead-code state with full body wired.
These tasks each require substantial new infrastructure on the
collector (target-net trunk forward chain duplicating
`gpu_dqn_trainer.rs` Pass 2's VSN+BN+OFI+trunk+branch-head sequence)
and are decomposed in the plan §"Phase 4" as 3 separate TDD cycles
with GPU oracle test gates between each. Atomic-refactor rule binds
the entire chain Task 4.5 → Task 5.1: NO L40S DISPATCH between.
**Sub-task verification status:**
```bash
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check -p ml
# → finished `dev` profile [unoptimized + debuginfo] target(s); clean
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 \
cargo test -p ml --test sp18_td_lambda_q_next_oracle_tests
# → 3 passed; 0 failed; 0 ignored
```
**Audit script post-Phase-4 fingerprint diff (vs Phase 1 close-out):**
- `td_lambda_kernel launch sites` count rose from 9 → 19 due to
the new `sp18_td_lambda_q_next_oracle_tests.rs` introspection test
file (7 hits via test docstrings + assertions + the build-site
ordering check); the source `gpu_experience_collector.rs` count
rose by 3 (additive comments referencing `td_lambda_kernel` per the
Phase-4 wiring narrative). Fingerprint regenerated above.
- `compute_q_next_target_bootstrap` introduced — 1 producer site
(the new method in `gpu_experience_collector.rs`) + 1 introspection
test referencing it. No production caller (Phase 4 = additive dead
code by design).