feat(sp22-vnext): Phase B1 — trade-outcome head weight tensors + Xavier init

Adds the 4 weight tensors (W1, b1, W2, b2) for the SP22 H6 vNext
trade-outcome aux head into the trainer's flat params_buf at indices
[163..167). Adam machinery (m/v moment buffers, SAXPY iteration over
0..NUM_WEIGHT_TENSORS) picks up the new tensors uniformly — no
per-tensor wiring needed.

Changes:
- NUM_WEIGHT_TENSORS bumped 163 → 167. Most of the 54 references are
  &[u64; NUM_WEIGHT_TENSORS] array-size generics that resize
  uniformly with the constant.
- compute_param_sizes() adds 4 new size entries:
    [163] aux_to_w1 [H=128, SH2=256]  = 32,768 floats
    [164] aux_to_b1 [H=128]            = 128 floats
    [165] aux_to_w2 [K=3, H=128]       = 384 floats
    [166] aux_to_b2 [K=3]               = 3 floats
  Total: 33,283 floats = ~133 KB params, ~266 KB Adam state.
- compute_param_sizes() debug_assert updated 163 → 167.
- Xavier fan_dims added: (H, SH2) for W1, (0, 0) for biases (zero-init),
  (K=3, H) for W2. Cold-start: logits ≈ 0 → softmax ≈ uniform 1/3 → no
  Profit/Stop/Timeout preference per pearl_first_observation_bootstrap.

SH2 stays at 256 in this commit (mirrors K=2 head exactly). The spec's
Phase B input concat (256 → 262 with plan_params 6-dim) will re-shape
slot [163] to 128 × 262 = 33,536 floats later — small touch-up vs the
full B1 commit.

Verification:
- cargo check -p ml clean (21 warnings, none new).
- 14 pre-existing test failures stashed-verified unrelated (OFI features
  missing, ISV slot count drift — independent of NUM_WEIGHT_TENSORS).

Phase B2 next: saved-tensor + per-sample partial buffers (hidden_post,
logits, softmax, valid_count, dW*_partial, dh_s2_aux_out).

Audit: docs/dqn-wire-up-audit.md Phase B1 section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-14 00:33:38 +02:00
parent 108a426c38
commit 205e46c171
2 changed files with 96 additions and 3 deletions

View File

@@ -4484,7 +4484,22 @@ impl Default for CausalInterventionConfig {
/// → indices [127..163). Gate: gate_w1[SD,64], gate_b1[64], gate_w2[64,8],
/// gate_b2[8]. Per expert: w1[SH2,64], b1[64], w2[64,SH2], b2[SH2].
/// Allocated but not yet wired into forward/backward — Phase 3 wire-up.
pub(crate) const NUM_WEIGHT_TENSORS: usize = 163;
/// SP22 H6 vNext Phase B1 (2026-05-14): bumped 163 → 167 to host the
/// trade-outcome aux head's 4 weight tensors at indices [163..167):
/// [163] aux_to_w1 [H=128, SH2=256] Xavier
/// [164] aux_to_b1 [H=128] zero
/// [165] aux_to_w2 [K=3, H=128] Xavier
/// [166] aux_to_b2 [K=3] zero
///
/// Adam m/v moment buffers and the SAXPY iteration pick up the new
/// tensors uniformly via `for i in 0..NUM_WEIGHT_TENSORS` loops — no
/// per-tensor Adam wiring needed.
///
/// Sized with Phase A3 SH2=256 (mirrors the K=2 next-bar head). The
/// spec's Phase B input concat (256 → 262 with `plan_params`) will
/// land later as a small change to `compute_param_sizes()` slot [163]
/// shape + the kernel-launch SH2 arg.
pub(crate) const NUM_WEIGHT_TENSORS: usize = 167;
/// Compute the size (element count) of each weight tensor.
///
@@ -4735,8 +4750,38 @@ pub(crate) fn compute_param_sizes(cfg: &GpuDqnTrainConfig) -> [usize; NUM_WEIGHT
"compute_param_sizes: MoE expert fill ended at {} but expected 163",
eidx);
}
debug_assert!(NUM_WEIGHT_TENSORS == 163,
"compute_param_sizes: NUM_WEIGHT_TENSORS expected 163 after MoE extension, got {}",
// ── SP22 H6 vNext Phase B1 (2026-05-14): trade-outcome aux head ──
// Architecture: `Linear(SH2 → AUX_HIDDEN_DIM) → ELU → Linear(H → K=3)`.
// Same shape as `aux_nb_*` (K=2 sibling) but K_OUT = AUX_OUTCOME_K = 3.
//
// Indices: aux_to_w1=163, aux_to_b1=164, aux_to_w2=165, aux_to_b2=166.
//
// Per-tensor sizes (sh2 = cfg.shared_h2 = 256, h_dim = 128, k_to = 3):
// [163] aux_to_w1 [H, SH2] → 128 × 256 = 32,768 floats
// [164] aux_to_b1 [H] → 128 floats
// [165] aux_to_w2 [K=3, H] → 3 × 128 = 384 floats
// [166] aux_to_b2 [K=3] → 3 floats
//
// Total: 33,283 floats = ~133 KB. Adam m/v moments double this to
// ~266 KB additional VRAM — negligible vs the existing aux trunk
// tensor budget.
//
// SH2=256 mirrors the K=2 head exactly at this commit. The spec's
// Phase B (input concat with plan_params 6-dim → SH2=262) will
// re-shape slot [163] later: 128 × 262 = 33,536 floats (+128 from
// the 6 extra input columns × 128 hidden lanes).
{
let h_dim = crate::cuda_pipeline::gpu_aux_heads::AUX_HIDDEN_DIM;
let k_to = crate::cuda_pipeline::gpu_aux_heads::AUX_OUTCOME_K;
sizes[163] = h_dim * sh2; // aux_to_w1 [H, SH2]
sizes[164] = h_dim; // aux_to_b1 [H]
sizes[165] = k_to * h_dim; // aux_to_w2 [K=3, H]
sizes[166] = k_to; // aux_to_b2 [K=3]
}
debug_assert!(NUM_WEIGHT_TENSORS == 167,
"compute_param_sizes: NUM_WEIGHT_TENSORS expected 167 after vNext extension, got {}",
NUM_WEIGHT_TENSORS);
sizes
@@ -33341,6 +33386,25 @@ impl GpuDqnTrainer {
fan_dims[126] = (0, 0); // aux_rg_b2 (zero)
}
// ── SP22 H6 vNext Phase B1 (2026-05-14): trade-outcome head fan dims ──
// Same shape pattern as aux_nb_* (K=2 sibling) — Xavier on W
// matrices, zero on biases. Cold-start: logits ≈ 0 → softmax ≈
// uniform 1/3 → no class preference for {Profit, Stop, Timeout}.
// The label producer's sparse-label semantics (most bars masked)
// mean the head trains only on actual trade-close events, so
// cold-start neutrality is the correct starting point per
// pearl_first_observation_bootstrap.
//
// Indices: aux_to_w1=163, aux_to_b1=164, aux_to_w2=165, aux_to_b2=166.
{
let h_dim = crate::cuda_pipeline::gpu_aux_heads::AUX_HIDDEN_DIM;
let k_to = crate::cuda_pipeline::gpu_aux_heads::AUX_OUTCOME_K;
fan_dims[163] = (h_dim, cfg.shared_h2); // aux_to_w1 [H, SH2] (Xavier)
fan_dims[164] = (0, 0); // aux_to_b1 (zero)
fan_dims[165] = (k_to, h_dim); // aux_to_w2 [K=3, H] (Xavier)
fan_dims[166] = (0, 0); // aux_to_b2 (zero)
}
// ── Phase 1 Task 1.6: MoE fan dims (36 tensors at [127..163)) ─────────
// Gate: zero-init (fan_out=0,fan_in=0) so g(s) = uniform 1/K at cold start.
// Experts: Xavier init on w1/w2 for initial differentiation; zero on biases.