feat(sp14): B.10 — backward wire gradient gating by ALPHA_GRAD_SMOOTHED

Critical safety mechanism that completes the EGF pearl: scales the wire
column of `dL/dx_concat [B, SH2 + 1]` (the gradient flowing FROM the
direction Q-head's first FC SGEMM TO `aux_softmax_diff`) by
`ISV[ALPHA_GRAD_SMOOTHED_INDEX = 393]`, computed by B.4's
`alpha_grad_compute_kernel` and orchestrated per-step in B.11.

`dL/dW[wire_col]` (Q-head's own weight gradient for the appended column)
is NOT scaled — the dW SGEMM `dY^T × x_concat` and the dX SGEMM
`dY × W^T` are independent, so scaling `dx[:, SH2]` AFTER both have
completed leaves dW unaffected. Q-head learns to USE the wire freely;
only the gradient PROPAGATING BACK to aux is gated.

Pre-B.11 (no producer wired) `ISV[393]` holds sentinel `0.0` →
wire force-closed (gradient zeroed) — the conservative safety state.
Post-B.11, B.4 writes the live gate output ∈ [0, 1] each step.

Closes the latent K-mismatch B.8/B.9 left in backward
============================================================

B.8 grew `w_b0fc` to `[adv_h, SH2 + 1]` end-to-end (Adam m/v +
spectral-norm vector + smoke fixtures); B.9 closed the forward dispatch
K-mismatch. The backward dW/dX SGEMMs for `d == 0` still used `K = SH2`
against the new `LDA = SH2 + 1` weight tensor — silently dropping the
last column of dW and zeroing the wire-col gradient. B.10 closes that
gap atomically with the wire-col scale per `feedback_no_partial_refactor`:

  * `backward_branch_dw` for `d == 0` now uses `(dir_qaux_concat_ptr, SH2 + 1)`
    instead of `(save_h_s2, SH2)` — matching the forward consumer
    pattern from B.9.
  * `backward_branch_dx` for `d == 0` now writes to
    `d_dir_qaux_concat [B, SH2 + 1]` with `K = SH2 + 1` instead of
    `scratch_d_h_s2 [B, SH2]` with `K = SH2`. Mirrors the magnitude
    branch's wider-buffer pattern.

New artifacts
=============

  * `sp14_scale_wire_col_kernel.cu`: one thread per batch row, scales
    `dx_concat[b, SH2]` by `isv[393]` IN-PLACE. NaN-safe per the
    `dqn_scale_f32_kernel` precedent (explicit `α==0 ⇒ 0` branch).
    Pure per-thread map, no atomicAdd, no shared memory.
  * `sp14_d_dir_qaux_concat: CudaSlice<f32>` `[B, SH2 + 1]` trainer-
    struct field. Dx SGEMM destination; the wire-col scale acts on
    this buffer; the strided accumulator copies the first SH2 columns
    into `bw_d_h_s2` after the scale.
  * `launch_sp14_scale_wire_col` launcher reads `self.isv_signals_dev_ptr`
    and the new buffer's raw_ptr.
  * `backward_full` signature grows two trailing `u64` args
    (`dir_qaux_concat_ptr`, `d_dir_qaux_concat_ptr`); both
    `backward_full` call sites (CQL aux + main online) wired
    atomically per `feedback_no_partial_refactor`.

Post-call orchestration at trainer level
========================================

  1. `launch_sp14_dir_concat_qaux(save_h_s2)` rebuilds the ONLINE
     concat in `sp14_dir_qaux_concat_scratch` (the forward pass had
     overwritten it with the TARGET concat at line ~25817). Same
     one-step-lag semantic preserved — `aux_nb_softmax_buf` is
     unchanged between forward and backward.
  2. `cuMemsetD32Async` zero of `d_h_s2` — pre-B.10 the direction
     branch (d==0) wrote it with beta=0; post-B.10 the dir-Q dX
     lives in `d_dir_qaux_concat` and is gated + accumulated AFTER
     `backward_full` returns, so the value-FC dx accumulator inside
     `backward_full` (beta=1) needs an explicit zero baseline.
  3. `backward_full` runs: dir branch → `d_dir_qaux_concat`,
     mag/ord/urg branches → their concat dX buffers, value-FC →
     `d_h_s2` (beta=1, on top of zeroed buffer).
  4. `launch_sp14_scale_wire_col` gates col SH2 of `d_dir_qaux_concat`.
  5. `accumulate_d_h_s2_from_concat` (beta=1) copies first SH2 cols
     of `d_dir_qaux_concat` into `d_h_s2`. Wire col stays in
     `d_dir_qaux_concat[:, SH2]`, untouched by this accumulator (its
     destination range is [0, SH2)). Pre-B.11 the wire is already
     zeroed by the sentinel-α gate; the orchestrator that propagates
     the gated wire-col gradient back to the aux head's softmax CE
     backward chain lives in B.11.
  6. mag/ord/urg accumulators continue with beta=1 (comments updated).

Wire status
===========

  * Forward dispatch: unchanged (B.9-complete).
  * Backward dispatch: GATED on both call sites (CQL aux + main online).
  * dW unchanged: the `dW = dY^T × x_concat` SGEMM writes
    `grad_buf[goff_w_b0fc..]` BEFORE the scale-wire-col launches;
    the scale operates ONLY on `d_dir_qaux_concat` (the dx buffer)
    AFTER both dW and dX SGEMMs complete.
  * Target net unaffected: Polyak EMA-only, no backward.
  * CudaSlice wrapper path: passes `0u64` for both new args, falls
    back to the legacy K=SH2 path. Consistent with the forward
    wrapper's diagnostic-only residual.

Verified
========

  * `SQLX_OFFLINE=true cargo check -p ml` clean, 18 warnings (baseline)
  * `cargo test -p ml --test sp14_oracle_tests` 2 passed, 6 ignored (GPU)
  * Audit doc `docs/dqn-wire-up-audit.md` updated per Invariant 7.

After this commit, the EGF pearl is architecturally complete; the
orchestration of when/how the alpha_grad gates fire happens in B.11
(producer chain orchestrator).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-05 20:49:27 +02:00
parent ecf4757c0d
commit dc3f948ee9
5 changed files with 381 additions and 7 deletions

View File

@@ -811,6 +811,20 @@ fn main() {
// (enforced by graph capture orchestrator in B.10/B.11). Does NOT
// read or write ISV slots — purely data-movement.
"dir_concat_qaux_kernel.cu",
// SP14 Layer B Task B.10 (2026-05-05): Earned Gradient Flow
// backward gate. Scales the wire column of `dL/dx_concat
// [B, SH2+1]` (gradient flowing FROM the direction Q-head's
// first FC SGEMM TO `aux_softmax_diff`) by ISV[393] =
// ALPHA_GRAD_SMOOTHED, computed by `alpha_grad_compute_kernel`
// (B.4) and orchestrated per-step by B.11. Touches ONLY column
// SH2 of the [B, SH2+1] gradient buffer; dW (Q-head's own
// weight gradient computed by an independent SGEMM) stays
// unscaled per the pearl design — Q-head learns to USE the
// wire freely; only the back-flow to aux is gated. Pre-B.11
// (unwired producer) ISV[393] sentinels at 0.0 → wire force-
// closed for safety (gradient zeroed). Pure per-thread map:
// one thread per batch row's wire element. No atomicAdd.
"sp14_scale_wire_col_kernel.cu",
];
// ALL kernels get common header (BF16 types + wrappers)

View File

@@ -1451,6 +1451,11 @@ impl CublasBackwardSet {
d_ord_concat_ptr: u64,
urg_concat_ptr: u64,
d_urg_concat_ptr: u64,
// SP14 B.10 (2026-05-05): when non-zero, direction branch (d==0)
// dX writes to this `[B, SH2+1]` buffer with K = SH2+1 (matching
// the widened `w_b0fc [adv_h, SH2+1]` weight from B.8). When 0,
// falls back to legacy direct-write into `scratch_d_h_s2 [B, SH2]`.
d_dir_qaux_concat_ptr: u64,
b: usize,
) -> Result<(), MLError> {
// Read per-branch GLU scratch (produced by Steps 1-4 on branch stream,
@@ -1458,7 +1463,34 @@ impl CublasBackwardSet {
let d_glu_value_ptr = raw_f32_ptr(&self.branch_d_glu_value[d], stream);
let d_glu_gate_ptr = raw_f32_ptr(&self.branch_d_glu_gate[d], stream);
if d == 1 && mag_concat_ptr != 0 {
if d == 0 && d_dir_qaux_concat_ptr != 0 {
// SP14 B.10: direction Q-head with EGF wire — dX writes to
// d_dir_qaux_concat [B, SH2+1]. K = SH2+1 because `w_b0fc` is
// `[adv_h, SH2+1]` post-B.8 (last col = aux_softmax_diff weight).
// After this returns, the caller scales col SH2 by ISV[393] then
// accumulates first SH2 cols into scratch_d_h_s2 (with beta=0
// because d==0 is the first branch's contribution to d_h_s2).
self.launch_dx_only(
stream,
d_glu_value_ptr,
w_fc,
d_dir_qaux_concat_ptr,
self.adv_h,
self.shared_h2 + 1,
b,
0.0_f32,
)?;
self.launch_dx_only(
stream,
d_glu_gate_ptr,
w_gate,
d_dir_qaux_concat_ptr,
self.adv_h,
self.shared_h2 + 1,
b,
1.0_f32,
)?;
} else if d == 1 && mag_concat_ptr != 0 {
// Magnitude (direction-conditioned): dX writes to d_mag_concat [B, SH2+branch_0_size].
self.launch_dx_only(
stream,
@@ -1711,6 +1743,21 @@ impl CublasBackwardSet {
// 0 OR when the cached DRELU_BGRAD descriptor is unavailable, the
// backward path falls back to the standalone-kernel sequence.
value_fc_relu_mask_ptr: u64,
// SP14 Layer B Task B.10 (2026-05-05): direction-Q-head Earned
// Gradient Flow wire. When non-zero, the direction branch (`d == 0`)
// backward operates on the wider `[B, SH2 + 1]` concat (last col =
// `aux_softmax_diff`), matching the SP14 B.8/B.9 forward consumer
// pattern. dW reads `dir_qaux_concat_ptr` (saved forward concat,
// re-built by the caller via `launch_sp14_dir_concat_qaux` right
// before this `backward_full`), and dX writes to `d_dir_qaux_concat_ptr`
// with `K = SH2 + 1`. The caller then runs `sp14_scale_wire_col_kernel`
// to gate column SH2 by ISV[393] = ALPHA_GRAD_SMOOTHED, then
// accumulates the first SH2 columns into `scratch_d_h_s2` via
// `strided_accumulate` (mirroring the magnitude branch pattern).
// When 0 (e.g. CudaSlice wrapper path), `d == 0` falls back to the
// legacy direct-write path into `scratch_d_h_s2 [B, SH2]`.
dir_qaux_concat_ptr: u64, // [B, SH2+1] saved forward concat (for dW)
d_dir_qaux_concat_ptr: u64, // [B, SH2+1] dX output (caller scales col SH2 + accumulates first SH2 cols)
) -> Result<(), MLError> {
// Plan 4 Task 2c.3c.4: trunk backward swap.
//
@@ -1851,10 +1898,15 @@ impl CublasBackwardSet {
let n_d = branch_n[d];
let w_out = w_ptrs[w_bout_idx[d]];
// FC input for dW (branches 1,2,3 may use wider concat buffers).
// Magnitude (d==1) is direction-conditioned (SH2 + branch_0_size);
// order/urgency (d==2,3) are OFI-conditioned (SH2 + 3).
let (fc_input, fc_in_dim) = if d == 1 && mag_concat_ptr != 0 {
// FC input for dW (each branch may use a wider concat buffer).
// Direction (d==0): SP14 B.10 EGF wire — `[h_s2 ; aux_softmax_diff]`
// when `dir_qaux_concat_ptr != 0` (SH2 + 1 cols).
// Magnitude (d==1): direction-conditioned (SH2 + branch_0_size).
// Order/urgency (d==2,3): OFI-conditioned (SH2 + 3).
// Otherwise: raw `save_h_s2` (SH2 cols), the legacy path.
let (fc_input, fc_in_dim) = if d == 0 && dir_qaux_concat_ptr != 0 {
(dir_qaux_concat_ptr, self.shared_h2 + 1)
} else if d == 1 && mag_concat_ptr != 0 {
(mag_concat_ptr, self.shared_h2 + self.branch_0_size)
} else if d == 2 && ord_concat_ptr != 0 {
(ord_concat_ptr, self.shared_h2 + 3)
@@ -1921,6 +1973,9 @@ impl CublasBackwardSet {
d_ord_concat_ptr,
urg_concat_ptr,
d_urg_concat_ptr,
// SP14 B.10: route direction (d==0) dX to the wire-aware
// [B, SH2+1] buffer when the EGF wire is enabled.
d_dir_qaux_concat_ptr,
b,
)?;
}

View File

@@ -686,6 +686,15 @@ static SP14_GRAD_HACK_CUBIN: &[u8] =
static SP14_DIR_CONCAT_CUBIN: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/dir_concat_qaux_kernel.cubin"));
/// SP14 B.10 (2026-05-05): backward gradient gate kernel for the EGF wire.
/// Scales column SH2 of `d_dir_qaux_concat [B, SH2 + 1]` by
/// `ISV[ALPHA_GRAD_SMOOTHED_INDEX = 393]` IN-PLACE. dW (Q-head's own weight
/// gradient, computed by an independent SGEMM) is NOT touched — Q-head
/// learns to USE the wire freely; only the back-flow to aux is gated per
/// the EGF pearl design. Loaded from `sp14_scale_wire_col_kernel.cubin`.
static SP14_SCALE_WIRE_COL_CUBIN: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/sp14_scale_wire_col_kernel.cubin"));
/// SP11 Fix 39 (2026-05-04, Task A2): SimHash novelty signal — lookup +
/// update kernels sharing one cubin. Lookup reads
/// `1/sqrt(1+count)` ∈ [0, 1] for each (state, action) bucket; update
@@ -5119,6 +5128,29 @@ pub struct GpuDqnTrainer {
/// the prior direct `h_s2` input to the direction Q-head. B.7 allocates;
/// B.8 bumps the Q-head input-dim constant; B.9/B.11 launch and wire.
sp14_dir_qaux_concat_scratch: CudaSlice<f32>,
/// SP14 B.10 (2026-05-05): backward dX scratch for direction Q-head's
/// first FC. Shape `[B, SH2 + 1]`. Written by `backward_branch_dx`
/// (`d == 0` case) as the SGEMM `dY @ W^T` output, then column SH2 is
/// scaled by `ISV[ALPHA_GRAD_SMOOTHED_INDEX = 393]` via
/// `sp14_scale_wire_col_kernel`, and finally the first SH2 columns
/// accumulate into `bw_d_h_s2` via `strided_accumulate`. Mirrors the
/// `d_mag_concat_buf` precedent for the magnitude branch's wider input.
/// Pre-existing latent gap: B.8/B.9 grew `w_b0fc` to `[adv_h, SH2+1]`
/// in forward but the backward consumers (dW + dX) for `d == 0` still
/// used `K = SH2`, silently dropping the wire column gradient. B.10
/// closes that gap atomically AND adds the EGF gate; per
/// `feedback_no_partial_refactor.md`.
sp14_d_dir_qaux_concat: CudaSlice<f32>,
/// SP14 B.10 (2026-05-05): EGF backward gate kernel. Scales column SH2
/// of `d_dir_qaux_concat [B, SH2 + 1]` by `ISV[393] = ALPHA_GRAD_SMOOTHED`.
/// One thread per batch row. Loaded from
/// `sp14_scale_wire_col_kernel.cubin`. Launched immediately after
/// `backward_full` returns, before the first-SH2-cols accumulator copies
/// the (now-gated) gradient into `bw_d_h_s2`. The wire column itself is
/// dropped pre-B.11 (alpha_grad sentinel = 0 force-closes the gate); the
/// orchestrator that propagates the scaled wire-col gradient to the aux
/// head's softmax backward lives in B.11.
sp14_scale_wire_col_kernel: CudaFunction,
/// SP11 Fix 39 (2026-05-04, Task A2): reward-subsystem controller
/// kernel. Single-block, 10-thread producer reading 5 canary ISV slots
/// [350..360) and writing 10 floats to scratch[SCRATCH_SP11_CONTROLLER_BASE..+10).
@@ -7089,6 +7121,43 @@ impl GpuDqnTrainer {
/// kernel) reads it on the same stream BEFORE the direction Q-head SGEMM.
/// Sequential same-stream submission enforces the dep — no host barrier
/// needed and the order survives CUDA Graph capture.
/// SP14 Layer B Task B.10 (2026-05-05): EGF backward gate.
///
/// Scales column SH2 of `d_dir_qaux_concat [B, SH2 + 1]` (the gradient
/// flowing FROM the direction Q-head's first FC SGEMM TO
/// `aux_softmax_diff`) by `ISV[ALPHA_GRAD_SMOOTHED_INDEX = 393]`. dW
/// (Q-head's own weight gradient, computed by an independent SGEMM) is
/// NOT touched per the EGF pearl — the SGEMMs `dW = dY^T × x_concat`
/// and `dX = dY × W^T` are independent, so scaling `dx[:, SH2]` AFTER
/// both have completed leaves dW unaffected.
///
/// Pre-B.11 (no producer wired) ISV[393] holds sentinel `0.0` →
/// wire force-closed for safety (gradient zeroed). Post-B.11, B.4's
/// `alpha_grad_compute_kernel` writes the live gate output ∈ [0, 1].
/// MUST run AFTER `backward_branch_dx` writes `d_dir_qaux_concat` and
/// BEFORE `accumulate_d_h_s2_from_concat` reads the first SH2 columns.
pub(crate) fn launch_sp14_scale_wire_col(&self, d_dir_qaux_concat_ptr: u64) -> Result<(), MLError> {
let b_i32 = self.config.batch_size as i32;
let sh2_i32 = self.config.shared_h2 as i32;
let blocks = ((self.config.batch_size as u32 + 255) / 256).max(1);
let isv_ptr = self.isv_signals_dev_ptr;
unsafe {
self.stream
.launch_builder(&self.sp14_scale_wire_col_kernel)
.arg(&d_dir_qaux_concat_ptr)
.arg(&isv_ptr)
.arg(&b_i32)
.arg(&sh2_i32)
.launch(LaunchConfig {
grid_dim: (blocks, 1, 1),
block_dim: (256, 1, 1),
shared_mem_bytes: 0,
})
.map_err(|e| MLError::ModelError(format!("sp14_scale_wire_col: {e}")))?;
}
Ok(())
}
pub(crate) fn launch_sp14_dir_concat_qaux(&self, source_ptr: u64) -> Result<(), MLError> {
let b = self.config.batch_size;
let sh2 = self.config.shared_h2 as i32;
@@ -10386,6 +10455,32 @@ impl GpuDqnTrainer {
} else {
0u64
};
// SP14 B.10: rebuild the ONLINE direction-Q-head concat scratch
// [save_h_s2 | aux_softmax_diff] into `sp14_dir_qaux_concat_scratch`.
// The forward pass overwrote it with the TARGET concat (line ~25817 in
// `submit_forward_ops`), so the backward dW SGEMM would otherwise read
// tg_h_s2 instead of save_h_s2 — wrong gradient. `aux_nb_softmax_buf`
// is unchanged between forward and backward (CE loss writes a separate
// `d_aux_softmax` scratch), so re-running the same concat with
// `save_h_s2` yields the bit-identical online concat the forward
// SGEMM consumed. Same one-step-lag semantic preserved.
self.launch_sp14_dir_concat_qaux(self.ptrs.save_h_s2)?;
// SP14 B.10: pre-zero `scratch_d_h_s2` because the direction branch
// (d==0) no longer writes it directly with beta=0 — its dX now lands
// in `d_dir_qaux_concat` and is gated + accumulated AFTER backward_full
// returns. The value-FC dX accumulator inside backward_full uses
// beta=1 and assumed scratch_d_h_s2 was pre-written by d==0; with the
// direction branch redirected, we must establish the zero baseline
// explicitly. Raw cuMemsetD32Async — captured by CUDA Graph (cudarc
// memset_zeros is NOT graph-safe; matches the cql_grad_scratch
// pre-backward-full zero pattern below).
unsafe {
cudarc::driver::sys::cuMemsetD32Async(
scratch_d_h_s2, 0,
self.config.batch_size * self.config.shared_h2,
self.stream.cu_stream(),
);
}
self.cublas_backward.backward_full(
&self.stream,
d_v_ptr,
@@ -10409,8 +10504,33 @@ impl GpuDqnTrainer {
self.ptrs.bw_d_glu_value,
self.ptrs.bw_d_glu_gate,
value_fc_relu_mask_cql,
// SP14 B.10: direction-Q-head EGF wire — saved forward concat
// for dW + dX output buffer for col SH2 wire scaling.
self.sp14_dir_qaux_concat_scratch.raw_ptr(),
self.sp14_d_dir_qaux_concat.raw_ptr(),
).map_err(|e| MLError::ModelError(format!("CQL backward_full: {e}")))?;
// SP14 B.10: gate the wire column gradient by ISV[ALPHA_GRAD_SMOOTHED]
// before the first-SH2-cols accumulator copies it into scratch_d_h_s2.
// Pre-B.11 (no producer wired) the slot sentinel = 0.0 zeros the wire
// → effectively force-closed. Post-B.11, the gate value ∈ [0, 1] is
// applied per the EGF pearl design.
self.launch_sp14_scale_wire_col(self.sp14_d_dir_qaux_concat.raw_ptr())?;
// Accumulate first SH2 columns of d_dir_qaux_concat into scratch_d_h_s2
// with beta=1 (the value-FC dX inside backward_full already accumulated
// its contribution; we ADD on top, NOT overwrite). Wire column (col
// SH2) is NOT propagated downstream from here — the orchestrator that
// routes the gated wire-col gradient back to the aux head's softmax CE
// backward chain lives in B.11. Pre-B.11 the wire is zeroed by the
// sentinel-α gate above anyway, so the unrouted column is moot.
self.accumulate_d_h_s2_from_concat(
self.sp14_d_dir_qaux_concat.raw_ptr(),
scratch_d_h_s2,
self.config.batch_size,
self.config.shared_h2 + 1,
1.0,
)?;
// CQL: accumulate magnitude branch dX into scratch_d_h_s2
if self.ptrs.mag_concat_buf != 0 {
self.accumulate_d_h_s2_from_concat(
@@ -10418,7 +10538,7 @@ impl GpuDqnTrainer {
scratch_d_h_s2,
self.config.batch_size,
self.config.shared_h2 + self.config.branch_0_size, // mag: direction-conditioned
1.0, // beta=1: d==0 already wrote to scratch_d_h_s2
1.0, // beta=1: SP14 B.10 zeroed scratch_d_h_s2 + value-FC + dir-Q already accumulated
)?;
}
if self.ptrs.ord_concat_buf != 0 {
@@ -17038,6 +17158,26 @@ impl GpuDqnTrainer {
.alloc_zeros::<f32>(b * (config.shared_h2 + 1))
.map_err(|e| MLError::ModelError(format!("sp14 dir_qaux scratch: {e}")))?;
// SP14 B.10: backward dX scratch for the direction Q-head's first
// FC. Shape `[B, SH2 + 1]`. Receives the SGEMM `dY @ W^T` output
// (replacing the prior direct write into `bw_d_h_s2 [B, SH2]` for
// d == 0); column SH2 then gets gated by ISV[393] and the first
// SH2 columns accumulate into `bw_d_h_s2`. Symmetric with the
// existing `d_mag_concat_buf` (magnitude branch's `[B, SH2 + b0]`
// backward scratch). Allocated zero-initialised so the first
// backward replay sees a clean buffer; the dX SGEMM uses beta=0
// anyway so the prior contents are overwritten each step.
let sp14_d_dir_qaux_concat = stream
.alloc_zeros::<f32>(b * (config.shared_h2 + 1))
.map_err(|e| MLError::ModelError(format!("sp14 d_dir_qaux scratch: {e}")))?;
let sp14_scale_wire_col_module = stream.context()
.load_cubin(SP14_SCALE_WIRE_COL_CUBIN.to_vec())
.map_err(|e| MLError::ModelError(format!("sp14 scale_wire_col cubin: {e}")))?;
let sp14_scale_wire_col_kernel = sp14_scale_wire_col_module
.load_function("sp14_scale_wire_col_kernel")
.map_err(|e| MLError::ModelError(format!("sp14_scale_wire_col_kernel: {e}")))?;
// SP11 Fix 39 B1b fix-up (2026-05-04): allocate trainer-side
// popart-component-per-sample mapped-pinned placeholder buffer.
// The canonical per-bar buffer is owned by the experience
@@ -20539,6 +20679,12 @@ impl GpuDqnTrainer {
sp14_gradient_hack_detect_kernel,
sp14_dir_concat_qaux_kernel,
sp14_dir_qaux_concat_scratch,
// SP14 EGF backward gate (B.10). Scratch + kernel land
// atomically with the dW/dX SGEMM K-dim migration in
// `backward_branch_dw`/`backward_branch_dx` per
// `feedback_no_partial_refactor.md`.
sp14_d_dir_qaux_concat,
sp14_scale_wire_col_kernel,
// SP11 Fix 39 (Task A2): controller + SimHash novelty kernels
// and their backing buffers. Projection matrix was populated
// on-device above by `launch_novelty_simhash_proj_init`; hash
@@ -26832,6 +26978,32 @@ impl GpuDqnTrainer {
self.launch_clamp_finite_f32(d_value_logits_ptr, n_val_clamp, max_abs_q)?;
self.launch_clamp_finite_f32(d_adv_logits_ptr, n_adv_clamp, max_abs_q)?;
// SP14 B.10: rebuild the ONLINE direction-Q-head concat scratch
// [save_h_s2 | aux_softmax_diff] into `sp14_dir_qaux_concat_scratch`.
// The forward pass overwrote it with the TARGET concat (line ~25817 in
// `submit_forward_ops`), so the backward dW SGEMM would otherwise read
// tg_h_s2 instead of save_h_s2 — wrong gradient. `aux_nb_softmax_buf`
// is unchanged between forward and backward (CE loss writes a separate
// `d_aux_softmax` scratch), so re-running the same concat with
// `save_h_s2` yields the bit-identical online concat the forward
// SGEMM consumed. Same one-step-lag semantic preserved.
self.launch_sp14_dir_concat_qaux(self.ptrs.save_h_s2)?;
// SP14 B.10: pre-zero `d_h_s2` because the direction branch (d==0) no
// longer writes it directly with beta=0 — its dX now lands in
// `d_dir_qaux_concat` and is gated + accumulated AFTER backward_full
// returns. The value-FC dX accumulator inside backward_full uses
// beta=1 and assumed d_h_s2 was pre-written by d==0; with the
// direction branch redirected, we must establish the zero baseline
// explicitly. Raw cuMemsetD32Async — captured by CUDA Graph (cudarc
// memset_zeros is NOT graph-safe; matches the cql_grad_scratch
// pre-backward-full zero pattern in the CQL path).
unsafe {
cudarc::driver::sys::cuMemsetD32Async(
d_h_s2_ptr, 0,
self.config.batch_size * self.config.shared_h2,
self.stream.cu_stream(),
);
}
bw.backward_full(
&self.stream,
d_value_logits_ptr,
@@ -26859,6 +27031,34 @@ impl GpuDqnTrainer {
self.ptrs.bw_d_glu_value,
self.ptrs.bw_d_glu_gate,
value_fc_relu_mask,
// SP14 B.10: direction-Q-head EGF wire — saved forward concat
// for dW + dX output buffer for col SH2 wire scaling.
self.sp14_dir_qaux_concat_scratch.raw_ptr(),
self.sp14_d_dir_qaux_concat.raw_ptr(),
)?;
// SP14 B.10: gate the wire column gradient by ISV[ALPHA_GRAD_SMOOTHED]
// before the first-SH2-cols accumulator copies it into d_h_s2.
// Pre-B.11 (no producer wired) the slot sentinel = 0.0 zeros the wire
// → effectively force-closed. Post-B.11, the gate value ∈ [0, 1] is
// applied per the EGF pearl design. Critical: this scales `dL/dx` ONLY,
// NOT `dL/dW` — Q-head's first FC weight gradient (computed by an
// independent `dY^T × x_concat` SGEMM into `grad_base[goff_w_b0fc..]`)
// is unscaled, so the Q-head learns to USE the wire freely.
self.launch_sp14_scale_wire_col(self.sp14_d_dir_qaux_concat.raw_ptr())?;
// Accumulate first SH2 columns of d_dir_qaux_concat into d_h_s2 with
// beta=1 (the value-FC dX inside backward_full already accumulated its
// contribution; we ADD on top, NOT overwrite). Wire column (col SH2)
// is NOT propagated downstream from here — the orchestrator that
// routes the gated wire-col gradient back to the aux head's softmax CE
// backward chain lives in B.11. Pre-B.11 the wire is zeroed by the
// sentinel-α gate above anyway, so the unrouted column is moot.
self.accumulate_d_h_s2_from_concat(
self.sp14_d_dir_qaux_concat.raw_ptr(),
d_h_s2_ptr,
self.config.batch_size,
self.config.shared_h2 + 1,
1.0,
)?;
// Accumulate magnitude branch dX (first SH2 columns) into d_h_s2
@@ -26868,7 +27068,7 @@ impl GpuDqnTrainer {
d_h_s2_ptr,
self.config.batch_size,
self.config.shared_h2 + self.config.branch_0_size, // mag: direction-conditioned
1.0, // beta=1: d==0 already wrote to d_h_s2 in backward_full
1.0, // beta=1: SP14 B.10 zeroed d_h_s2 + value-FC + dir-Q already accumulated
)?;
}
// Accumulate order branch dX (first SH2 columns) into d_h_s2

View File

@@ -0,0 +1,64 @@
// crates/ml/src/cuda_pipeline/sp14_scale_wire_col_kernel.cu
//
// SP14 Layer B Task B.10 (2026-05-05): Earned Gradient Flow backward gate.
// Scales the wire column of `dL/dx_concat [B, SH2 + 1]` (the gradient
// flowing back FROM the direction Q-head's first FC SGEMM TO
// `aux_softmax_diff`) by `α_grad_smoothed` (read from ISV[393]).
//
// ── What this gates ─────────────────────────────────────────────────────
// dL/dx_concat shape is [B, SH2 + 1]. Columns [0..SH2) propagate back to
// `h_s2` via the existing trunk-encoder backward chain. Column SH2 is the
// gradient w.r.t. the appended `aux_softmax_diff` value, which downstream
// (B.11) propagates to the aux head's softmax → CE backward chain. THIS
// kernel multiplies ONLY column SH2 by `ALPHA_GRAD_SMOOTHED = ISV[393]`.
//
// Per the EGF pearl design (spec §B.4):
// - α_grad ≈ 0 → wire effectively closed (Q-loss can't pull aux head)
// - α_grad ≈ 1 → full co-training enabled
//
// Critical: dW (the Q-head's own weight gradient) is NOT touched. Q-head
// learns to USE the wire freely from Q-loss; only the gradient PROPAGATING
// BACK to aux head's representation is gated. The dW and dX SGEMMs are
// independent (`dW = dY^T × x_concat`; `dX = dY × W^T`), so scaling dx[:, SH2]
// AFTER both SGEMMs have completed leaves dW unaffected.
//
// ── ISV slot index (must match `sp14_isv_slots.rs`) ─────────────────────
// ALPHA_GRAD_SMOOTHED_INDEX = 393 (sentinel 0.0 pre-B.11 producer wires;
// when sentinel-active the wire is force-closed, the conservative state).
//
// ── Pure per-thread map ─────────────────────────────────────────────────
// One thread handles one batch row's wire-column element. No reductions,
// no shared memory, no atomicAdd (per `feedback_no_atomicadd.md`). Read of
// `isv[ALPHA_GRAD_SMOOTHED_INDEX]` is identical across all threads — safe
// concurrent read.
//
// Grid: ceil(B / 256), Block: 256.
extern "C" __global__
void sp14_scale_wire_col_kernel(
/* dL/dx_concat [B, SH2 + 1] — IN-PLACE scaled at column SH2 only */
float* __restrict__ dx_concat,
/* Global ISV bus. Reads ISV[ALPHA_GRAD_SMOOTHED_INDEX]; writes nothing. */
const float* __restrict__ isv,
int B,
int SH2)
{
/* ISV slot index — must match `sp14_isv_slots.rs::ALPHA_GRAD_SMOOTHED_INDEX`.
* The layout fingerprint regression test catches drift. */
const int ALPHA_GRAD_SMOOTHED = 393;
const int b = blockIdx.x * blockDim.x + threadIdx.x;
if (b >= B) return;
/* Wire column lives at offset SH2 in each row of stride (SH2+1). */
const int wire_idx = b * (SH2 + 1) + SH2;
/* Pre-B.11 producer is unwired → ISV[393] holds sentinel 0.0 →
* wire force-closed (gradient zeroed). Post-B.11, the value is the
* EGF gate output ∈ [0, 1].
*
* Match `dqn_scale_f32_kernel`'s NaN-safe contract: 0×NaN=NaN under
* IEEE 754; explicit branch yields a clean 0 when α=0. */
const float alpha = isv[ALPHA_GRAD_SMOOTHED];
dx_concat[wire_idx] = (alpha == 0.0f) ? 0.0f : (dx_concat[wire_idx] * alpha);
}

View File

@@ -6601,3 +6601,44 @@ Per `pearl_canary_input_freshness_launch_order`: the producer (aux_heads_forward
- **Diagnostic-path residual**: causal intervention, DDQN argmax, and the experience-collector / value-decoder forwards intentionally pass `0u64` for `dir_qaux_concat_ptr` and fall back to `K = SH2`. Their direction Q outputs feed either (a) only-value-logit consumers (causal) or (b) downstream argmax-only consumers with one-step-bias acknowledged by the spec (DDQN). The cuBLAS heuristic for `K = SH2, LDA = SH2` against the underlying `[adv_h, SH2 + 1]` weight tensor reads the first `adv_h * SH2` floats with stride SH2 — within bounds (no OOB), produces stable-but-incorrect outputs for the residual paths. `feedback_no_partial_refactor` is honoured for the train-time path (online forward, target forward, replay); diagnostic-path residuals are documented and bounded.
- **Reverse dependencies**: `forward_value_head_for_ensemble` and `forward_value_head` use only the value head (V_h ← h_s2; no branch heads), so unaffected by the direction Q-head wire. The `submit_dqn_step_loop_cublas` path is the same `forward_online_raw` API — no separate dispatch site.
## SP14 Layer B — Commit B.10: backward wire — gradient scaling at the wire column (2026-05-05)
**Why this commit.** B.10 is the critical safety mechanism that completes the EGF pearl. The forward wire (B.6/B.9) feeds `aux_softmax_diff` into the direction Q-head's first FC SGEMM via `[h_s2 ; aux_softmax_diff] [B, SH2 + 1]`. Without B.10, the Q-loss backward computes `dL/dx_concat[:, SH2]` (the gradient flowing back to `aux_softmax_diff`) and propagates it ungated all the way to the aux head's softmax — co-training aux on Q-loss without protection. B.10 scales `dL/dx[wire_col]` by `ISV[ALPHA_GRAD_SMOOTHED_INDEX = 393]` (computed by B.4's `alpha_grad_compute_kernel`, orchestrated per-step in B.11). Crucially, `dL/dW[wire_col]` (the Q-head's own weight gradient for the appended column) is NOT scaled — Q-head learns to USE the wire freely; only the gradient PROPAGATING BACK to aux is gated. The two SGEMMs `dW = dY^T × x_concat` and `dX = dY × W^T` are independent, so scaling `dx[:, SH2]` AFTER both have completed leaves dW unaffected.
### Pre-existing latent gap closed atomically with the wire-col scale
B.8/B.9 grew `w_b0fc` to `[adv_h, SH2 + 1]` end-to-end across forward dispatch + Adam m/v + spectral-norm power-iteration vector + smoke fixtures, but the **backward dW/dX SGEMMs for `d == 0` still used `K = SH2`** against the new `LDA = SH2 + 1` weight tensor — the same K-mismatch B.9 closed in forward, surviving in backward. B.10 closes that gap atomically with the wire-col scale per `feedback_no_partial_refactor.md`:
| Backward path | Pre-B.10 | Post-B.10 |
|---|---|---|
| `backward_branch_dw` for `d == 0` | `(save_h_s2, SH2)` → silent dW under-write of last column | `(dir_qaux_concat_ptr, SH2 + 1)` → full dW including last column |
| `backward_branch_dx` for `d == 0` | `(scratch_d_h_s2, SH2, beta=0)` → wire-col gradient dropped | `(d_dir_qaux_concat_ptr, SH2 + 1, beta=0)` → wire-col gradient preserved for B.10 scale |
| Wire-col gating | none | `sp14_scale_wire_col_kernel` reads `ISV[393]` and multiplies `d_dir_qaux_concat[:, SH2]` |
| Trunk `d_h_s2` accumulation | implicit (d==0 wrote `scratch_d_h_s2` with beta=0) | explicit `cuMemsetD32Async` zero pre-`backward_full`; `accumulate_d_h_s2_from_concat` adds first SH2 cols with beta=1 after wire-col scale |
### File-change summary
| Change | File |
|--------|------|
| New `sp14_scale_wire_col_kernel.cu` (one thread per batch row, scales col SH2 by `ISV[393]`); `extern "C"` symbol; structurally bounded NaN-safe per `dqn_scale_f32_kernel` precedent | `crates/ml/src/cuda_pipeline/sp14_scale_wire_col_kernel.cu` |
| New cubin entry in `kernels_with_common` | `crates/ml/build.rs` |
| New `static SP14_SCALE_WIRE_COL_CUBIN`; new struct fields `sp14_d_dir_qaux_concat: CudaSlice<f32>` (`[B, SH2 + 1]`) + `sp14_scale_wire_col_kernel: CudaFunction`; allocation + cubin load + struct init; new `launch_sp14_scale_wire_col` launcher; both `backward_full` call sites (CQL @ ~10484 + main @ ~26980) get `launch_sp14_dir_concat_qaux(save_h_s2)` re-build (the forward path overwrites the buffer with target concat at line ~25817), `cuMemsetD32Async` zero of d_h_s2 before the call, the new `(dir_qaux_concat_ptr, d_dir_qaux_concat_ptr)` trailing args, post-call `launch_sp14_scale_wire_col` + `accumulate_d_h_s2_from_concat(beta=1)` | `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` |
| `backward_full` signature grows `dir_qaux_concat_ptr` + `d_dir_qaux_concat_ptr` u64 args; `backward_branch_dw` adds `d == 0 && dir_qaux_concat_ptr != 0` arm using `(SH2 + 1)` fc_in_dim; `backward_branch_dx` adds `d == 0 && d_dir_qaux_concat_ptr != 0` arm writing `[B, SH2 + 1]` with `K = SH2 + 1` (mirroring magnitude branch's wider-buffer pattern) | `crates/ml/src/cuda_pipeline/batched_backward.rs` |
### Verification
- `SQLX_OFFLINE=true cargo check -p ml` — clean, 18 warnings (pre-existing baseline, no new warnings)
- `cargo test -p ml --test sp14_oracle_tests` — 2 passed, 6 ignored (GPU)
### Wire status
- **Forward dispatch**: unchanged (B.9-complete).
- **Backward dispatch**: GATED. Both `backward_full` call sites (main + CQL aux) use the EGF wire path: dW via `dir_qaux_concat_ptr` with `K = SH2 + 1`, dX into `d_dir_qaux_concat [B, SH2 + 1]` with `K = SH2 + 1`. The K-mismatch B.8/B.9 left in backward is now closed in lockstep with the wire-col scale.
- **Wire-col gating**: ACTIVE. `launch_sp14_scale_wire_col` reads `ISV[ALPHA_GRAD_SMOOTHED_INDEX = 393]` and multiplies column SH2 of `d_dir_qaux_concat` IN-PLACE. Pre-B.11 (no producer wired) the slot sentinel = 0.0 → wire force-closed (gradient zeroed) — the conservative safety state. Post-B.11, B.4's `alpha_grad_compute_kernel` writes the live gate output ∈ [0, 1] each step.
- **dW unchanged**: `dL/dW = dL/d_branch_h^T × x_concat` writes to `grad_buf[goff_w_b0fc..goff_w_b0fc + adv_h * (SH2 + 1) * 4]` via `launch_dw_only_ws` BEFORE the scale-wire-col launches; the scale operates ONLY on `d_dir_qaux_concat` (the dx buffer) AFTER both dW and dX SGEMMs complete. Q-head learns to use the wire freely.
- **First-SH2-cols accumulator**: `accumulate_d_h_s2_from_concat(d_dir_qaux_concat → d_h_s2, src_stride=SH2+1, beta=1.0)` runs AFTER `launch_sp14_scale_wire_col` so the scaled wire-col stays in `d_dir_qaux_concat[:, SH2]` (untouched by the accumulator's destination range `[0, SH2)`). The wire column gradient is NOT propagated downstream from here pre-B.11; the orchestrator that routes the gated wire-col gradient back to the aux head's softmax CE backward chain lives in B.11. Pre-B.11 the wire is zeroed by the sentinel-α gate anyway, so the unrouted column is moot.
- **One-step-lag preservation**: backward consumes the same `aux_nb_softmax_buf` snapshot the forward consumed — `aux_heads_forward` writes the buffer once per step BEFORE the next-step's online forward runs `launch_sp14_dir_concat_qaux`, and the CE loss kernel writes a separate `d_aux_softmax` scratch (does not overwrite `aux_nb_softmax_buf`). Re-running the concat with `save_h_s2` at backward start yields the bit-identical online concat the forward SGEMM consumed.
- **Target forward unaffected**: target net is Polyak-EMA-updated only (no backward), so the wire-col scale + K-dim migration apply only to the online backward path.
- **Reverse dependencies**: `apply_iqn_trunk_gradient` and aux-paths use the `bw_d_h_s2` that this path writes — the new accumulator pipeline (memset → backward_full → wire-col scale → strided accumulate from d_dir_qaux_concat → mag/ord/urg accumulators → value-FC inside backward_full) leaves `bw_d_h_s2` with the same algebraic value as pre-B.10 *except* for the gated wire-col contribution from the direction-Q's first FC. Pre-B.11 (α=0) the gated contribution is zero → bit-identical to pre-B.10.
- **CudaSlice wrapper path**: passes `0u64` for both `dir_qaux_concat_ptr` and `d_dir_qaux_concat_ptr`, falling back to the legacy K=SH2 path. This is consistent with the forward CudaSlice wrapper (`dir_qaux_concat_ptr: 0`); the wrapper-based callers (causal intervention, DDQN argmax) are diagnostic-only paths whose direction-Q outputs are downstream-bounded per the B.9 residual-path analysis.