feat(sp18 v2 P4.T1+T2): B-leg next_states hoist + compute_q_next_target_bootstrap skeleton (additive dead code)

Phase 4 lands the additive infrastructure 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).

Tasks landed:
  - **Task 4.1**: build_next_states_f32 invocation hoisted to BEFORE
    the TD(λ) launch block. Pure ordering change — verified via the
    SP18 audit grep flagging 0 post-TD(λ) consumers of next_states.
  - **Task 4.2**: compute_q_next_target_bootstrap skeleton method on
    GpuExperienceCollector with full plan-aligned signature
    (next_states, target_params_ptr, online_params_ptr, batch_size →
    Result<CudaSlice<f32>, MLError>). Body is a hard-error early-return
    per feedback_no_stubs — no production caller in Phase 4 (the
    caller is wired by Phase 5 Task 5.1, replacing the self-bootstrap
    clone). Returning Err satisfies Invariant 9 (no deferred-work
    markers); any accidental pre-Phase-5 call hard-errors with a
    clear pointer to the open sub-tasks.

Tasks 4.3 (online forward + DDQN per-branch argmax), 4.4 (target
forward + compute_expected_q gather), 4.5 (replace error early-return
with full implementation + GPU oracle test gates) are deferred to a
follow-up subagent dispatch per their plan-defined per-task TDD cycle
scope. Each requires 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).

Tests:
  - crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs (NEW):
    3 introspection tests (no GPU required):
      * compute_q_next_target_bootstrap_method_exists
      * next_states_built_before_td_lambda (Task 4.1 ordering invariant)
      * td_lambda_still_consumes_self_bootstrap_q_next_in_phase4
        (Phase 4→Phase 5 atomic-refactor guard)

Atomic-refactor invariant (HARD — feedback_no_partial_refactor):
  NO L40S DISPATCH between this Phase 4 close-out commit and the
  Phase 5 Task 5.1 commit that replaces the q_next clone with
  self.compute_q_next_target_bootstrap(&next_states, ...).

Verification:
  SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace  → clean
  SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 \
    cargo test -p ml --test sp18_td_lambda_q_next_oracle_tests
                                                                → 3/3 pass
  bash scripts/audit_sp18_consumers.sh --check                  → exit 0

Files:
  crates/ml/src/cuda_pipeline/gpu_experience_collector.rs (Task 4.1+4.2)
  crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs (NEW; 3 tests)
  docs/sp18-wireup-audit.md (Phase 4 close-out section + fingerprint)
  docs/dqn-wire-up-audit.md (Invariant 7 entry)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-09 11:25:50 +02:00
parent 5ea5aa9b8e
commit 834eaec4bd
4 changed files with 449 additions and 9 deletions

View File

@@ -3937,6 +3937,101 @@ impl GpuExperienceCollector {
Ok(())
}
/// SP18 v2 Phase 4 (2026-05-08): compute `Q_target(s', argmax_a Q_online(s', a))`
/// per-sample using DDQN per-branch argmax. Uses target-net params to
/// evaluate the action selected by the online net on `next_states`.
///
/// Per spec design decisions (B-DD2 + B-DD3 + B-DD8.a):
/// - **B-DD2**: DDQN argmax — argmax_a from ONLINE network, max-Q from
/// TARGET network at that argmax. Reduces overestimation bias of
/// vanilla DQN's `max_a Q_target(s', a)` form.
/// - **B-DD3**: per-branch DDQN — 4 separate argmaxes for
/// dir/mag/ord/urg, mirroring SP17 dueling-head per-branch separation.
/// - **B-DD8.a (scalar)**: returns scalar Q-bootstrap per sample, NOT
/// per-atom distributional. The dir-branch's
/// `Q_target[s', argmax_a Q_online_dir(s', a)]` is the load-bearing
/// contribution; mag/ord/urg branches contribute via reward-shape
/// and PER priority, NOT via TD(λ) bootstrap directly. This avoids
/// a `td_lambda_kernel` signature change in Phase 5 (per-atom would
/// require new arg).
///
/// Returns `CudaSlice<f32>` of length `B` — one scalar Q-bootstrap value
/// per sample. Phase 5 (`q_next` replacement at line ~4252) consumes the
/// output via `td_lambda_kernel`'s `q_next` argument; Phase 4 ships this
/// method as **DEAD CODE** (no production caller) covered only by GPU
/// oracle tests T8 + T11. NO L40S DISPATCH between Phase 4 close and
/// Phase 5 (atomic refactor rule per `feedback_no_partial_refactor`).
///
/// # Arguments
/// * `next_states` - GPU buffer `[B, STATE_DIM]` f32 (post-VSN gating
/// contract; built by `build_next_states_f32` post-Task 4.1 hoist).
/// * `target_params_ptr` - device pointer to the trainer's
/// `target_params_buf` (Polyak EMA-tracked target network parameters).
/// See `GpuDqnTrainer::target_params_buf_device_ptr()`.
/// * `online_params_ptr` - device pointer to the trainer's `params_buf`
/// (online network master parameters; same buffer the collector reads
/// for action selection via `trainer_params_ptr`).
/// * `batch_size` - `B`, total experience count
/// (`n_episodes * timesteps * 2` including counterfactual).
///
/// # Implementation status (Phase 4 sub-tasks)
/// - **Task 4.1** done — `next_states` build-site relocated before TD(λ)
/// launch (caller-side, see comment at line ~4243).
/// - **Task 4.2** done — this skeleton + dispatch contract (current state).
/// - **Task 4.3** outstanding — online-net forward on `next_states` for
/// DDQN per-branch argmax. Mirror of `submit_forward_ops_ddqn`'s online
/// VSN+BN+trunk+branch-head chain via `cublas_forward.forward_online_raw`,
/// output: per-branch argmax actions `[B, 4]` (i32).
/// - **Task 4.4** outstanding — target-net forward on `next_states` 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 and SUM across 4 branches into
/// `[B]` scalar (or per B-DD8.a use dir-branch only — final form
/// resolved at Task 4.4 close).
/// - **Task 4.5** outstanding — replace this dead-method early-return
/// with full implementation; `cargo check --workspace` clean; GPU
/// oracle tests T8 + T11 pass.
///
/// # Why this returns an error in Phase 4 instead of being absent
///
/// Phase 4 lands the method as **dead code** — no production caller
/// exists yet (the caller is Phase 5 Task 5.1, which replaces the
/// self-bootstrap clone at line ~4313). Returning a hard error from
/// the body satisfies both:
///
/// 1. **`feedback_no_stubs`** — no deferred-work macro, no
/// return-zero stub; the method explicitly errors with a
/// pointer to the plan-decomposed sub-tasks (4.3-4.4) that
/// replace this body atomically before any caller is wired.
/// 2. **`feedback_no_partial_refactor`** — the atomic-refactor
/// rule binds Phase 4 → Phase 5: NO L40S DISPATCH between
/// Task 4.5 (replaces this body with the real DDQN bootstrap)
/// and Task 5.1 (wires the production caller at line ~4313).
/// Until Task 4.5 closes, any accidental call (e.g. introduced
/// by a mid-refactor mistake) hard-errors with a clear pointer
/// to the open sub-task — preventing silent self-bootstrap
/// regression.
///
/// Phase 5.1 close-out grep audit asserts the error string is absent
/// in this method body (replaced by the real implementation).
pub fn compute_q_next_target_bootstrap(
&mut self,
next_states: &cudarc::driver::CudaSlice<f32>,
target_params_ptr: u64,
online_params_ptr: u64,
batch_size: usize,
) -> Result<cudarc::driver::CudaSlice<f32>, MLError> {
let _ = (next_states, target_params_ptr, online_params_ptr, batch_size);
Err(MLError::ModelError(
"SP18 v2 Phase 4: compute_q_next_target_bootstrap body is dead-code \
pending Tasks 4.3-4.5 (online forward + target forward + DDQN gather). \
Plan reference: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md \
§'Phase 4'. Atomic-refactor rule: NO L40S DISPATCH between Phase 4 \
close (Task 4.5) and Phase 5 (Task 5.1)."
.to_string(),
))
}
/// Task 2.X prerequisite (policy-quality scoping): per-magnitude win rate
/// + realized step-return variance, computed host-side from the per-sample
/// buffers written by `experience_env_step`.
@@ -4240,6 +4335,21 @@ impl GpuExperienceCollector {
debug!(n_steps, gamma = config.gamma, "N-step return accumulation applied");
}
// SP18 v2 Phase 4 Task 4.1 (2026-05-08): `next_states` build-site
// relocation. The TD(λ) launch below now requires `next_states` for
// the Phase 5 `q_next` replacement (target-Q via DDQN argmax computed
// by `compute_q_next_target_bootstrap`). Phase 4 lands the build-site
// hoist as a no-op behavioral change — `next_states` is consumed
// downstream by `GpuExperienceBatch.next_states` (returned at the end
// of this fn) AND, in Phase 5, by the new bootstrap method. This
// hoist has no observer (the only post-TD(λ) reader was the batch
// assembly at the bottom of the fn — verified via the SP18 audit
// script `next_states\b` grep). Pure ordering change; tests assert
// build-site precedes TD(λ) launch via `include_str!` introspection.
let next_states = build_next_states_f32(
&self.stream, &states, n_episodes * 2, timesteps, sd, n_steps as usize,
)?;
// v8: TD(λ) — overwrites n-step results with geometrically-weighted multi-horizon returns
let td_lambda = config.td_lambda;
let max_trace = config.max_trace_length;
@@ -4248,7 +4358,16 @@ impl GpuExperienceCollector {
// the current state — TD(λ) independently accumulates from the same base signals)
let raw_rewards_td = dtod_clone_f32_native(&self.stream, &self.rewards_out, total, "raw_rewards_td")?;
let raw_dones_td = dtod_clone_f32_native(&self.stream, &self.done_out, total, "raw_dones_td")?;
// Self-bootstrap: use rewards as Q(s') approximation (improves as model learns)
// SP18 v2 Phase 4 (2026-05-08): self-bootstrap retained.
//
// The `q_next = rewards` self-bootstrap is the original td_lambda_kernel
// contract per its header docstring ("When not available, pass raw_rewards
// as q_next for a self-bootstrap approximation"). Phase 4 lands the
// additive infrastructure for a real `Q_target(s', argmax_a Q_online(s', a))`
// bootstrap (B-DD2 + B-DD3 + B-DD8) via `compute_q_next_target_bootstrap`
// — DEAD until Phase 5 swaps this `q_next` argument to consume the
// method's output. Per `feedback_no_partial_refactor`, NO L40S
// DISPATCH between Phase 4 and Phase 5 close.
let q_next = dtod_clone_f32_native(&self.stream, &self.rewards_out, total, "q_next_bootstrap")?;
let gamma_f32 = config.gamma;
@@ -4286,12 +4405,6 @@ impl GpuExperienceCollector {
// Per-bar percentage returns (~0.001) are naturally scaled.
// Normalizing would scramble the C51 atom distribution.
// Build next_states on GPU (episode-aware shift by n_steps)
// Must use total (includes counterfactual) so next_states covers all experiences.
let next_states = build_next_states_f32(
&self.stream, &states, n_episodes * 2, timesteps, sd, n_steps as usize,
)?;
// ── Episode ID fill (GPU-native, zero CPU) ────────────────────
// episode_ids[i] = i / L — enables HER Future and Final strategies.
// Direct integer arithmetic: no linear scan needed.

View File

@@ -0,0 +1,147 @@
//! SP18 v2 Phase 4 — B-leg `q_next` target-Q bootstrap oracle tests.
//!
//! Plan: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md
//!
//! Phase 4 lands additive infrastructure for the
//! `Q_target(s', argmax_a Q_online(s', a))` (DDQN) bootstrap that replaces
//! the `q_next = rewards` self-bootstrap at
//! `gpu_experience_collector.rs:~4252` in Phase 5. Phase 4 is dead-code
//! — `compute_q_next_target_bootstrap` has no production caller, only
//! the GPU oracle tests T8 + T11 (added in Tasks 4.4) once Tasks 4.3
//! and 4.4 fill in the body.
//!
//! At Phase 4 Task 4.2 close (this file's initial state), only the
//! source-introspection contract tests are present:
//!
//! - **T_SKEL.1**: Method signature exists in
//! `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs` with the
//! required parameters per the plan §"Phase 4 Task 4.2".
//! - **T_SKEL.2**: `next_states` build-site precedes the
//! `td_lambda_kernel` launch (Phase 4 Task 4.1 ordering invariant).
//! - **T_SKEL.3**: Phase-4-pre-Phase-5 atomic-refactor guard —
//! `td_lambda_kernel` STILL consumes the self-bootstrap `q_next`
//! clone (Phase 5 Task 5.1 swaps).
//!
//! Tasks 4.3-4.4 will add GPU oracle tests T8 + T11
//! (`#[ignore = "requires GPU"]`) once the body is filled in.
//!
//! Run on any host (introspection-only, no GPU needed for T_SKEL):
//!
//! SQLX_OFFLINE=true cargo test -p ml \
//! --test sp18_td_lambda_q_next_oracle_tests
//!
//! Per `feedback_no_partial_refactor`, NO L40S DISPATCH between the
//! Phase 4 close commit and the Phase 5.1 commit that swaps the `q_next`
//! argument at line ~4252 to consume `compute_q_next_target_bootstrap`'s
//! output.
const COLLECTOR_SRC: &str = include_str!(
"../src/cuda_pipeline/gpu_experience_collector.rs"
);
/// **T_SKEL.1** (Phase 4 Task 4.2 source-introspection contract).
///
/// Asserts the new method signature exists with the documented parameters
/// per the plan §"Phase 4 Task 4.2". Until Task 4.5 closes Phase 4, the
/// method body is a hard-error early-return per `feedback_no_stubs` —
/// no production caller exists yet (Phase 5 Task 5.1 wires it).
#[test]
fn compute_q_next_target_bootstrap_method_exists() {
// Method declaration with full signature line — match the exact
// declaration produced by Task 4.2.
assert!(
COLLECTOR_SRC.contains("pub fn compute_q_next_target_bootstrap("),
"Phase 4 Task 4.2: `compute_q_next_target_bootstrap` method must \
exist on `GpuExperienceCollector`"
);
// Required parameters from the plan §"Task 4.2".
let want_args = [
"next_states: &cudarc::driver::CudaSlice<f32>",
"target_params_ptr: u64",
"online_params_ptr: u64",
"batch_size: usize",
];
for arg in want_args {
assert!(
COLLECTOR_SRC.contains(arg),
"Phase 4 Task 4.2: missing required parameter signature `{arg}` \
on `compute_q_next_target_bootstrap`"
);
}
// Return type must be CudaSlice<f32> (B-DD8.a scalar form per the plan).
assert!(
COLLECTOR_SRC.contains(
"Result<cudarc::driver::CudaSlice<f32>, MLError>"
),
"Phase 4 Task 4.2: return type must be \
`Result<CudaSlice<f32>, MLError>` per B-DD8.a scalar bootstrap"
);
// Dead-method body must hard-error with a pointer to the close-out
// sub-tasks (Tasks 4.3-4.5). Returning Err satisfies Invariant 9
// (no deferred-work markers) AND `feedback_no_stubs` — any accidental
// Phase-5-pre-empting call site hard-errors with a clear pointer to
// the open work. This guard string is REPLACED in Task 4.5 commit
// alongside the body fill-in.
assert!(
COLLECTOR_SRC.contains(
"compute_q_next_target_bootstrap body is dead-code"
),
"Phase 4 Task 4.2: dead-method body must hard-error with the \
documented Phase-4-pending pointer per `feedback_no_partial_refactor`"
);
}
/// **T_SKEL.2** (Phase 4 Task 4.1 build-site relocation invariant).
///
/// Asserts `build_next_states_f32` is invoked BEFORE the `td_lambda_kernel`
/// launch site, so that Phase 5's `q_next` replacement can pass
/// `&next_states` to `compute_q_next_target_bootstrap` (which lives where
/// the current self-bootstrap clone is). Pure ordering invariant —
/// behaviorally a no-op since `next_states` had no observer between its
/// pre-Phase-4 build site and the bottom-of-fn batch assembly.
#[test]
fn next_states_built_before_td_lambda() {
let build_pos = COLLECTOR_SRC.find("let next_states = build_next_states_f32(")
.expect("`let next_states = build_next_states_f32(` must exist in collector");
let td_lambda_pos = COLLECTOR_SRC.find(".launch_builder(&self.td_lambda_kernel)")
.expect("`.launch_builder(&self.td_lambda_kernel)` must exist in collector");
assert!(
build_pos < td_lambda_pos,
"Phase 4 Task 4.1: `build_next_states_f32` (offset {build_pos}) must \
precede the `td_lambda_kernel` launch (offset {td_lambda_pos}) so that \
Phase 5 `compute_q_next_target_bootstrap(&next_states, ...)` can \
consume it. Audit script grep `next_states\\b` flagged 0 \
post-TD(λ) consumers — this hoist is a pure ordering change."
);
}
/// **T_SKEL.3** (Phase 4 Phase 5 atomic-refactor guard).
///
/// Asserts that the `td_lambda_kernel` launch STILL consumes the
/// self-bootstrap `q_next = rewards_out` clone. Phase 5 (Task 5.1) flips
/// this — at that point, this test will be deleted/inverted alongside the
/// `q_next` argument swap.
///
/// Until Phase 5, this test is a regression guard: if a future change
/// accidentally removes the self-bootstrap before `compute_q_next_target_bootstrap`
/// is wired, the test fails with a clear pointer to the atomic-refactor
/// invariant.
#[test]
fn td_lambda_still_consumes_self_bootstrap_q_next_in_phase4() {
// The self-bootstrap clone — Phase 5 Task 5.1 replaces this with
// `self.compute_q_next_target_bootstrap(&next_states, ...)`.
assert!(
COLLECTOR_SRC.contains(r#""q_next_bootstrap")"#),
"Phase 4 atomicity: self-bootstrap `q_next = rewards_out` clone \
must still be wired to `td_lambda_kernel` (literal label \
\"q_next_bootstrap\") until Phase 5 Task 5.1. Per \
`feedback_no_partial_refactor`, the swap is atomic in Phase 5 \
alongside the `compute_q_next_target_bootstrap` body fill-in \
(Tasks 4.3-4.5)."
);
}

View File

@@ -10289,3 +10289,81 @@ stream sync via `read_volatile`.
| `crates/ml/src/trainers/dqn/trainer/training_loop.rs` | +30 / -0 | Per-epoch HEALTH_DIAG `weight_drift` emit |
| `crates/ml/tests/sp18_weight_drift_test.rs` | NEW | GPU oracle test (4 cases, all `#[ignore = "requires GPU"]`) |
| `docs/dqn-wire-up-audit.md` | +N / -0 | This entry |
## 2026-05-09 — SP18 v2 Phase 4 Tasks 4.1+4.2: B-leg target-Q DDQN bootstrap skeleton (additive dead code)
Plan: `docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md`
§"Phase 4 — B-leg target-net forward pass during experience collection".
**Goal:** land additive infrastructure for the
`Q_target(s', argmax_a Q_online(s', a))` (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), replacing
the self-bootstrap `q_next = rewards_out` clone documented in
`project_c51_partial_centering_contract_mismatch`.
### Tasks landed in this commit
- **Task 4.1**`next_states` build-site relocation. The
`build_next_states_f32` invocation now precedes the TD(λ) launch
block. Pure ordering hoist (no behavioral change — `next_states`
had no observer between its pre-Phase-4 build site and the
bottom-of-fn batch assembly). Required for Phase 5 to pass
`&next_states` to `compute_q_next_target_bootstrap`.
- **Task 4.2**`compute_q_next_target_bootstrap` skeleton on
`GpuExperienceCollector` with full plan-aligned signature:
`(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 Phase 5 Task 5.1, which replaces 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.
### Tests
| Test | File | Purpose |
|------|------|---------|
| `compute_q_next_target_bootstrap_method_exists` | `crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs` | Phase 4 Task 4.2 source-introspection contract — asserts method signature with documented parameters |
| `next_states_built_before_td_lambda` | same | Phase 4 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`)
**NO L40S DISPATCH between this 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 deferred to follow-up subagent dispatch
The plan's Tasks 4.3 (online forward + DDQN per-branch argmax),
4.4 (target forward + `compute_expected_q` gather), 4.5 (`todo!()`
removal + GPU oracle tests T8 + T11) 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 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.
### Verification
```bash
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace # clean
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 \
cargo test -p ml --test sp18_td_lambda_q_next_oracle_tests # 3/3 pass
bash scripts/audit_sp18_consumers.sh --check # exit 0
```
### Files changed
| File | LOC delta | Purpose |
|------|-----------|---------|
| `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs` | +112 / -9 | Task 4.1 hoist + Task 4.2 skeleton method |
| `crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs` | NEW | 3 introspection tests (no GPU required) |
| `docs/sp18-wireup-audit.md` | +97 / +1 fingerprint | Phase 4 close-out section + audit fingerprint regenerate |
| `docs/dqn-wire-up-audit.md` | +N / -0 | This entry |

View File

@@ -405,8 +405,9 @@ re-capture is committed as part of Task 1.6 (close-out).
total_hits=2
crates/ml/build.rs 2
=== B-leg: td_lambda_kernel launch sites ===
total_hits=9
crates/ml/src/cuda_pipeline/gpu_experience_collector.rs 8
total_hits=19
crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs 7
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=
@@ -433,3 +434,104 @@ re-capture is committed as part of Task 1.6 (close-out).
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).