diff --git a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs index 4cf7c4fff..fab3ca44e 100644 --- a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs +++ b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs @@ -141,6 +141,30 @@ static H_S2_RMS_EMA_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/h_s /// Diagnostic only — producer-only in this commit. static IQN_QUANTILE_EMA_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/iqn_quantile_ema_kernel.cubin")); +/// Plan 4 Task 1B-i / 1B-ii: VSN feature-selection kernels (forward + backward). +/// Cubin loaded here as a `pub(crate) static` so the `include_bytes!()` happens +/// at compile time; the runtime `load_cubin(...)` + `load_function(...)` calls +/// are deferred to 1B-iii's forward orchestrator wire-in. Marked `dead_code` +/// here because the static is declared in advance of its consumer to keep +/// 1B-iii's diff scoped to the orchestrator + Linear_1/Linear_2 cuBLAS plumbing. +#[allow(dead_code)] +pub(crate) static VSN_FEATURE_SELECTION_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/vsn_feature_selection_kernel.cubin")); + +/// Plan 4 Task 1B-i / 1B-ii: VSN per-group mask EMA producer. +/// Same `pub(crate) static` + deferred-load pattern as `VSN_FEATURE_SELECTION_CUBIN`. +/// Launch site lands in 1B-iii alongside the forward orchestrator so the EMA +/// updates ISV[VSN_MASK_GROUP_0_EMA_INDEX..VSN_MASK_GROUP_5_EMA_INDEX) only +/// after the producer kernel is actually invoked. +#[allow(dead_code)] +pub(crate) static VSN_MASK_EMA_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/vsn_mask_ema_kernel.cubin")); + +/// Plan 4 Task 1B-ii: per-group VSN MLP hidden dim. `Linear_1[g]` is +/// `[VSN_HIDDEN_DIM, group_dim_g]`, `Linear_2[g]` is `[1, VSN_HIDDEN_DIM]`. +/// 16 chosen as a tractable expansion that fits comfortably against the +/// smallest group (`plan_isv` = 7 features); per-group params total +/// `VSN_HIDDEN_DIM*group_dim_g + VSN_HIDDEN_DIM + VSN_HIDDEN_DIM + 1`. +pub const VSN_HIDDEN_DIM: usize = 16; + /// Mamba2 temporal scan configuration. const MAMBA2_HISTORY_K: usize = 8; // Rolling history length const MAMBA2_STATE_DIM: usize = 16; // SSM state dimension @@ -318,11 +342,21 @@ const ISV_NETWORK_DIM: usize = 23; /// Note: the median (τ=0.50) lives in the existing C51 / IQL greedy-Q /// diagnostic — not duplicated here. Only the 4 off-median quantiles /// are new. -/// [103] ISV_LAYOUT_FINGERPRINT_LO_INDEX — low 32 bits of layout fingerprint (shifted from 97). -/// [104] ISV_LAYOUT_FINGERPRINT_HI_INDEX — high 32 bits of layout fingerprint (shifted from 98). +/// [105] VSN_MASK_GROUP_0_EMA_INDEX — Plan 4 Task 1B-ii — `market` feature +/// group importance mask EMA. Producer: `vsn_mask_ema_update` GPU kernel +/// (single-block shmem reduction over per-sample mask buffer; α=0.05). +/// Launch site wired in 1B-iii. Cold-start `1.0/SL_NUM_FEATURE_GROUPS` +/// = 1/6 (uniform prior — no group preference at init). FoldReset → 1/6. +/// [106] VSN_MASK_GROUP_1_EMA_INDEX — `ofi` group, same producer/contract. +/// [107] VSN_MASK_GROUP_2_EMA_INDEX — `tlob` group, same producer/contract. +/// [108] VSN_MASK_GROUP_3_EMA_INDEX — `mtf` group, same producer/contract. +/// [109] VSN_MASK_GROUP_4_EMA_INDEX — `portfolio` group, same producer/contract. +/// [110] VSN_MASK_GROUP_5_EMA_INDEX — `plan_isv` group, same producer/contract. +/// [111] ISV_LAYOUT_FINGERPRINT_LO_INDEX — low 32 bits of layout fingerprint (shifted from 103). +/// [112] ISV_LAYOUT_FINGERPRINT_HI_INDEX — high 32 bits of layout fingerprint (shifted from 104). /// Written by the constructor; checked at checkpoint load. Fail-fast only — no migration /// path exists. See spec §4.A.2 and `LAYOUT_FINGERPRINT_CURRENT` for structural-hash rationale. -const ISV_TOTAL_DIM: usize = 105; +const ISV_TOTAL_DIM: usize = 113; /// Legacy alias preserved for call sites that haven't been audited for the /// network-vs-total split. New code should pick `ISV_NETWORK_DIM` (for weight /// tensor sizing) or `ISV_TOTAL_DIM` (for the broadcast bus buffer). @@ -611,7 +645,35 @@ pub const IQN_Q_P25_EMA_INDEX: usize = 100; pub const IQN_Q_P75_EMA_INDEX: usize = 101; pub const IQN_Q_P95_EMA_INDEX: usize = 102; -/// ISV slot [103] — low 32 bits of the u64 layout fingerprint (stored as raw f32 bits). +/// ISV slots [105..111) — Plan 4 Task 1B-ii: per-group VSN mask EMAs. +/// +/// One slot per `FEATURE_GROUP_RANGES` entry (market / ofi / tlob / mtf / +/// portfolio / plan_isv). Producer: `vsn_mask_ema_update` GPU kernel +/// (single-block, 256 threads, shmem-reduction, no atomicAdd) — landed in +/// 1B-i, launch site wired in 1B-iii alongside the forward orchestrator that +/// produces the per-sample softmax mask. EMA α matches the rest of the +/// Plan 4 producer family (cold-path-cadence, one launch per training step +/// via `training_loop.rs`). +/// +/// Cold-start: `1.0 / SL_NUM_FEATURE_GROUPS = 1/6` (uniform prior — no +/// group preference at init). The producer kernel overwrites these slots on +/// every fire, so the cold-start value matters only for consumers that read +/// the EMA before the first kernel launch (currently none — this commit is +/// producer/consumer-orchestrator-free; 1B-iii adds the launch). +/// FoldReset → `1/6`. +/// +/// Producer-only after 1B-iii — diagnostic surface for HEALTH_DIAG / +/// per-group attention monitoring. No consumer kernel reads slots +/// [105..111) in this commit nor in 1B-iii (consumer attaches in a later +/// `attention_monitor` upgrade). +pub const VSN_MASK_GROUP_0_EMA_INDEX: usize = 105; +pub const VSN_MASK_GROUP_1_EMA_INDEX: usize = 106; +pub const VSN_MASK_GROUP_2_EMA_INDEX: usize = 107; +pub const VSN_MASK_GROUP_3_EMA_INDEX: usize = 108; +pub const VSN_MASK_GROUP_4_EMA_INDEX: usize = 109; +pub const VSN_MASK_GROUP_5_EMA_INDEX: usize = 110; + +/// ISV slot [111] — low 32 bits of the u64 layout fingerprint (stored as raw f32 bits). /// /// Design note: this is NOT a version number. There is no ordered version space. /// The fingerprint is a structural hash that automatically changes whenever any @@ -626,15 +688,16 @@ pub const IQN_Q_P95_EMA_INDEX: usize = 102; /// Plan 3 Task 4 B.4, 76→80 in Plan 3 Task 7 C.3, 80→85 in Plan 3 Task 8 B.3, /// 85→90 in Plan 4 Task 5 Mode A, 90→94 in Plan 4 follow-up (target-drift /// EMA replacing legacy CPU-DtoH path), 94→97 in Plan 4 Task 2c.3c.5 -/// (H_S2_RMS_EMA producer slot), then 97→103 in Plan 4 Task 3 E.3 (IQN -/// fixed-τ multi-quantile head, +4 diagnostic slots). -pub const ISV_LAYOUT_FINGERPRINT_LO_INDEX: usize = 103; -/// ISV slot [104] — high 32 bits of the u64 layout fingerprint (stored as raw f32 bits). +/// (H_S2_RMS_EMA producer slot), 97→103 in Plan 4 Task 3 E.3 (IQN +/// fixed-τ multi-quantile head, +4 diagnostic slots), then 103→111 in +/// Plan 4 Task 1B-ii (VSN per-group mask EMA, +6 slots). +pub const ISV_LAYOUT_FINGERPRINT_LO_INDEX: usize = 111; +/// ISV slot [112] — high 32 bits of the u64 layout fingerprint (stored as raw f32 bits). /// Shifted 62→70 in Plan 3 Task 1, 70→74 in Plan 3 Task 3, 74→77 in Plan 3 Task 4 B.4, /// 77→81 in Plan 3 Task 7 C.3, 81→86 in Plan 3 Task 8 B.3, 86→91 in Plan 4 Task 5, -/// 91→95 in Plan 4 follow-up, 95→98 in Plan 4 Task 2c.3c.5, then 98→104 in Plan 4 -/// Task 3 E.3. -pub const ISV_LAYOUT_FINGERPRINT_HI_INDEX: usize = 104; +/// 91→95 in Plan 4 follow-up, 95→98 in Plan 4 Task 2c.3c.5, 98→104 in Plan 4 +/// Task 3 E.3, then 104→112 in Plan 4 Task 1B-ii. +pub const ISV_LAYOUT_FINGERPRINT_HI_INDEX: usize = 112; /// Canonical alias for the fingerprint slot (the low half). pub const ISV_LAYOUT_FINGERPRINT_INDEX: usize = ISV_LAYOUT_FINGERPRINT_LO_INDEX; @@ -714,8 +777,10 @@ const fn layout_fingerprint_seed() -> &'static [u8] { TARGET_DRIFT_MAG_EMA=92;TARGET_DRIFT_DIR_EMA=93;\ H_S2_RMS_EMA=96;\ IQN_Q_P05_EMA=99;IQN_Q_P25_EMA=100;IQN_Q_P75_EMA=101;IQN_Q_P95_EMA=102;\ - ISV_LAYOUT_FINGERPRINT_LO=103;ISV_LAYOUT_FINGERPRINT_HI=104;\ - ISV_TOTAL_DIM=105;\ + VSN_MASK_GROUP_0_EMA=105;VSN_MASK_GROUP_1_EMA=106;VSN_MASK_GROUP_2_EMA=107;\ + VSN_MASK_GROUP_3_EMA=108;VSN_MASK_GROUP_4_EMA=109;VSN_MASK_GROUP_5_EMA=110;\ + ISV_LAYOUT_FINGERPRINT_LO=111;ISV_LAYOUT_FINGERPRINT_HI=112;\ + ISV_TOTAL_DIM=113;\ PARAM_W_A_H_S1=0;PARAM_B_A_H_S1=1;PARAM_W_B_H_S1=2;PARAM_B_B_H_S1=3;\ PARAM_W_RESIDUAL_H_S1=4;PARAM_GAMMA_H_S1=5;PARAM_BETA_H_S1=6;\ PARAM_W_A_H_S2=7;PARAM_B_A_H_S2=8;PARAM_W_B_H_S2=9;PARAM_B_B_H_S2=10;\ @@ -756,7 +821,13 @@ const fn layout_fingerprint_seed() -> &'static [u8] { PARAM_W_TEMPORAL_ROUTE=89;PARAM_B_TEMPORAL_ROUTE=90;\ PARAM_W_PLAN_FC=91;PARAM_B_PLAN_FC=92;\ PARAM_W_PLAN_OUT=93;PARAM_B_PLAN_OUT=94;\ - PARAM_TOTAL_TENSORS=95" + PARAM_VSN_W1_G0=95;PARAM_VSN_B1_G0=96;PARAM_VSN_W2_G0=97;PARAM_VSN_B2_G0=98;\ + PARAM_VSN_W1_G1=99;PARAM_VSN_B1_G1=100;PARAM_VSN_W2_G1=101;PARAM_VSN_B2_G1=102;\ + PARAM_VSN_W1_G2=103;PARAM_VSN_B1_G2=104;PARAM_VSN_W2_G2=105;PARAM_VSN_B2_G2=106;\ + PARAM_VSN_W1_G3=107;PARAM_VSN_B1_G3=108;PARAM_VSN_W2_G3=109;PARAM_VSN_B2_G3=110;\ + PARAM_VSN_W1_G4=111;PARAM_VSN_B1_G4=112;PARAM_VSN_W2_G4=113;PARAM_VSN_B2_G4=114;\ + PARAM_VSN_W1_G5=115;PARAM_VSN_B1_G5=116;PARAM_VSN_W2_G5=117;PARAM_VSN_B2_G5=118;\ + PARAM_TOTAL_TENSORS=119" } /// Compile-time layout fingerprint. Burned into the binary at build time. @@ -1150,8 +1221,13 @@ impl Default for CausalInterventionConfig { /// + 8 multi-horizon value heads (5-bar and 20-bar) /// + 4 learned risk management (5th branch) = 68 /// + 10 ISV encoder/conditioning + 2 feature gate + 2 temporal routing = 82 -/// + 4 trade plan head = 86. -pub(crate) const NUM_WEIGHT_TENSORS: usize = 95; +/// + 4 trade plan head = 86 → indices [0..95). +/// + 24 Plan 4 Task 1B-ii VSN per-group MLP params (4 tensors × 6 groups — +/// w1_g/b1_g/w2_g/b2_g per `FEATURE_GROUP_RANGES` entry, hidden dim +/// `VSN_HIDDEN_DIM = 16`) → indices [95..119). Producer-only params in +/// this commit; the `vsn_softmax_and_gate_forward` consumer kernel and +/// the cuBLAS `Linear_1`/`Linear_2` wire-in land in 1B-iii. +pub(crate) const NUM_WEIGHT_TENSORS: usize = 119; /// Compute the size (element count) of each weight tensor. /// @@ -1183,7 +1259,8 @@ pub(crate) fn compute_param_sizes(cfg: &GpuDqnTrainConfig) -> [usize; NUM_WEIGHT let bn_dim = cfg.bottleneck_dim; let market_dim = cfg.market_dim; - [ + let mut sizes = [0usize; NUM_WEIGHT_TENSORS]; + let core: [usize; 95] = [ // ── h_s1 GRN block (Plan 4 Task 2c.3a) ─────────────────────── // GRN(x) = LayerNorm(Linear_residual(x) + GLU(Linear_b(ELU(Linear_a(x))))). // h_s1 maps SD → SH1 (dims differ → Linear_residual is a real GEMM). @@ -1296,7 +1373,36 @@ pub(crate) fn compute_param_sizes(cfg: &GpuDqnTrainConfig) -> [usize; NUM_WEIGHT cfg.adv_h, // [92] b_plan_fc [AH] 6 * cfg.adv_h, // [93] w_plan_out [6, AH] 6, // [94] b_plan_out [6] - ] + ]; + sizes[..95].copy_from_slice(&core); + + // ── Plan 4 Task 1B-ii: VSN per-group MLP params (24 tensors) ─── + // 4 tensors per group × 6 groups (`FEATURE_GROUP_RANGES`). + // Per-group total = VSN_HIDDEN_DIM * group_dim_g + VSN_HIDDEN_DIM (b1) + // + VSN_HIDDEN_DIM (w2 flat) + 1 (b2) + // = 16 * group_dim_g + 33. + // Sum over groups (market=42, ofi=32, tlob=16, mtf=16, portfolio=8, + // plan_isv=7) = 16*121 + 6*33 = 1936 + 198 = 2134 floats. + // No production callers in this commit — params consumed by 1B-iii + // (forward orchestrator + Linear_1/Linear_2 cuBLAS wire-in) and + // 1B-iv (backward orchestrator). + let group_ranges = ml_core::state_layout::FEATURE_GROUP_RANGES; + let mut idx = 95; + let mut g = 0; + while g < ml_core::state_layout::SL_NUM_FEATURE_GROUPS { + let group_dim = group_ranges[g].1 - group_ranges[g].0; + sizes[idx] = VSN_HIDDEN_DIM * group_dim; // vsn_w1_g{g} [VSN_HIDDEN_DIM, group_dim_g] + sizes[idx + 1] = VSN_HIDDEN_DIM; // vsn_b1_g{g} [VSN_HIDDEN_DIM] + sizes[idx + 2] = VSN_HIDDEN_DIM; // vsn_w2_g{g} [1, VSN_HIDDEN_DIM] (flat-stored) + sizes[idx + 3] = 1; // vsn_b2_g{g} [1] + idx += 4; + g += 1; + } + debug_assert!(idx == NUM_WEIGHT_TENSORS, + "compute_param_sizes: VSN tail fill ended at {} but NUM_WEIGHT_TENSORS={}", + idx, NUM_WEIGHT_TENSORS); + + sizes } /// Align element count to 4-element boundary (16 bytes for f32). @@ -8985,6 +9091,21 @@ impl GpuDqnTrainer { *sig_ptr.add(IQN_Q_P25_EMA_INDEX) = 0.0_f32; *sig_ptr.add(IQN_Q_P75_EMA_INDEX) = 0.0_f32; *sig_ptr.add(IQN_Q_P95_EMA_INDEX) = 0.0_f32; + /* Plan 4 Task 1B-ii: VSN per-group mask-EMA cold-start. + * 1.0 / SL_NUM_FEATURE_GROUPS = 1/6 — uniform prior (no + * group preference at init). The producer kernel + * `vsn_mask_ema_update` (1B-i, launch site wired in + * 1B-iii) overwrites these slots on every fire; until + * 1B-iii lands, they stay at the uniform cold-start. + * FoldReset reapplies the same 1/6 at fold boundary so + * each fold begins with no inherited group bias. */ + let uniform_mask = 1.0_f32 / ml_core::state_layout::SL_NUM_FEATURE_GROUPS as f32; + *sig_ptr.add(VSN_MASK_GROUP_0_EMA_INDEX) = uniform_mask; + *sig_ptr.add(VSN_MASK_GROUP_1_EMA_INDEX) = uniform_mask; + *sig_ptr.add(VSN_MASK_GROUP_2_EMA_INDEX) = uniform_mask; + *sig_ptr.add(VSN_MASK_GROUP_3_EMA_INDEX) = uniform_mask; + *sig_ptr.add(VSN_MASK_GROUP_4_EMA_INDEX) = uniform_mask; + *sig_ptr.add(VSN_MASK_GROUP_5_EMA_INDEX) = uniform_mask; // Plan 2 C.1 Q-quantile bootstrap (2026-04-24): // q_p05 = v_min, q_p95 = v_max — matches the cold-start atom range // so the first update_eval_v_range call reads meaningful bounds. @@ -15565,7 +15686,8 @@ impl GpuDqnTrainer { // gamma, beta — 7 tensors; h_s2 GRN: Linear_a, Linear_b, gamma, beta — // 6 tensors since SH1==SH2 makes Linear_residual identity). Every // index ≥ 4 in the OLD layout shifts +9 in the NEW layout. - let fan_dims: [(usize, usize); NUM_WEIGHT_TENSORS] = [ + let mut fan_dims: [(usize, usize); NUM_WEIGHT_TENSORS] = [(0, 0); NUM_WEIGHT_TENSORS]; + let core_fan: [(usize, usize); 95] = [ // ── h_s1 GRN ── (cfg.shared_h1, s1_input_dim), // [0] w_a_h_s1(Kaiming/Xavier) (0, 0), // [1] b_a_h_s1 (zero) @@ -15669,6 +15791,27 @@ impl GpuDqnTrainer { (6, cfg.adv_h), // [93] w_plan_out (Xavier) (0, 0), // [94] b_plan_out (zero) ]; + fan_dims[..95].copy_from_slice(&core_fan); + + // ── Plan 4 Task 1B-ii: VSN per-group MLP fan dims (24 tensors) ─ + // Xavier on weights (`w1_g`, `w2_g`); zero on biases (`b1_g`, `b2_g`). + // Initial logits ≈ 0 → softmax mask ≈ uniform 1/SL_NUM_FEATURE_GROUPS + // per group at every sample (cold-start neutral, matches the ISV + // mask-EMA cold-start of 1/6). + { + let group_ranges = ml_core::state_layout::FEATURE_GROUP_RANGES; + let mut g = 0; + let mut idx = 95; + while g < ml_core::state_layout::SL_NUM_FEATURE_GROUPS { + let group_dim = group_ranges[g].1 - group_ranges[g].0; + fan_dims[idx] = (VSN_HIDDEN_DIM, group_dim); // w1_g [VSN_HIDDEN_DIM, group_dim_g] (Xavier) + fan_dims[idx + 1] = (0, 0); // b1_g (zero) + fan_dims[idx + 2] = (1, VSN_HIDDEN_DIM); // w2_g [1, VSN_HIDDEN_DIM] (Xavier) + fan_dims[idx + 3] = (0, 0); // b2_g (zero) + idx += 4; + g += 1; + } + } // Build flat host buffer: Xavier init for weights, zeros for biases + padding. let cutlass_tile_pad = 32 * cfg.adv_h.max(cfg.value_h); diff --git a/crates/ml/src/trainers/dqn/state_reset_registry.rs b/crates/ml/src/trainers/dqn/state_reset_registry.rs index 6167d926e..ea129a065 100644 --- a/crates/ml/src/trainers/dqn/state_reset_registry.rs +++ b/crates/ml/src/trainers/dqn/state_reset_registry.rs @@ -300,6 +300,37 @@ impl StateResetRegistry { category: ResetCategory::FoldReset, description: "ISV[IQN_Q_P95_EMA_INDEX=102] — mean |Q| at IQN τ=0.95 EMA; GPU iqn_quantile_ema_update kernel fills (Plan 4 Task 3 E.3). Cold-start 0.0 reapplied at fold boundary. Diagnostic only", }, + // ───── Plan 4 Task 1B-ii: VSN per-group mask EMAs ────────── + RegistryEntry { + name: "isv_vsn_mask_g0_ema", + category: ResetCategory::FoldReset, + description: "ISV[VSN_MASK_GROUP_0_EMA_INDEX=105] — `market` group importance mask EMA (α=0.05); GPU vsn_mask_ema_update kernel fills (Plan 4 Task 1B-i kernel; launch site wired in 1B-iii). Cold-start 1.0/SL_NUM_FEATURE_GROUPS=1/6 (uniform prior — no group preference) reapplied at fold boundary", + }, + RegistryEntry { + name: "isv_vsn_mask_g1_ema", + category: ResetCategory::FoldReset, + description: "ISV[VSN_MASK_GROUP_1_EMA_INDEX=106] — `ofi` group importance mask EMA; same producer/contract as g0; cold-start 1/6", + }, + RegistryEntry { + name: "isv_vsn_mask_g2_ema", + category: ResetCategory::FoldReset, + description: "ISV[VSN_MASK_GROUP_2_EMA_INDEX=107] — `tlob` group importance mask EMA; same producer/contract as g0; cold-start 1/6", + }, + RegistryEntry { + name: "isv_vsn_mask_g3_ema", + category: ResetCategory::FoldReset, + description: "ISV[VSN_MASK_GROUP_3_EMA_INDEX=108] — `mtf` group importance mask EMA; same producer/contract as g0; cold-start 1/6", + }, + RegistryEntry { + name: "isv_vsn_mask_g4_ema", + category: ResetCategory::FoldReset, + description: "ISV[VSN_MASK_GROUP_4_EMA_INDEX=109] — `portfolio` group importance mask EMA; same producer/contract as g0; cold-start 1/6", + }, + RegistryEntry { + name: "isv_vsn_mask_g5_ema", + category: ResetCategory::FoldReset, + description: "ISV[VSN_MASK_GROUP_5_EMA_INDEX=110] — `plan_isv` group importance mask EMA; same producer/contract as g0; cold-start 1/6", + }, ]; Self { entries } } diff --git a/crates/ml/src/trainers/dqn/trainer/training_loop.rs b/crates/ml/src/trainers/dqn/trainer/training_loop.rs index dd380913d..8aace35e8 100644 --- a/crates/ml/src/trainers/dqn/trainer/training_loop.rs +++ b/crates/ml/src/trainers/dqn/trainer/training_loop.rs @@ -4531,6 +4531,38 @@ impl DQNTrainer { fused.trainer().write_isv_signal_at(idx, 0.0); } } + // Plan 4 Task 1B-ii: VSN per-group mask-EMA producer slots + // (one per `FEATURE_GROUP_RANGES` entry). Reset at fold + // boundary to the uniform prior `1.0/SL_NUM_FEATURE_GROUPS` + // so each fold begins with no inherited group bias and the + // first `vsn_mask_ema_update` fire on the new fold EMAs the + // measured per-group mask toward 1/6 rather than carrying + // forward a stale fold-N estimate. Diagnostic only — no + // consumer kernel reads slots [105..111) in this commit nor + // in 1B-iii (the consumer attaches in a later + // `attention_monitor` upgrade). + "isv_vsn_mask_g0_ema" | "isv_vsn_mask_g1_ema" | "isv_vsn_mask_g2_ema" + | "isv_vsn_mask_g3_ema" | "isv_vsn_mask_g4_ema" | "isv_vsn_mask_g5_ema" => { + if let Some(ref fused) = self.fused_ctx { + let idx = match name { + "isv_vsn_mask_g0_ema" + => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_0_EMA_INDEX, + "isv_vsn_mask_g1_ema" + => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_1_EMA_INDEX, + "isv_vsn_mask_g2_ema" + => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_2_EMA_INDEX, + "isv_vsn_mask_g3_ema" + => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_3_EMA_INDEX, + "isv_vsn_mask_g4_ema" + => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_4_EMA_INDEX, + "isv_vsn_mask_g5_ema" + => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_5_EMA_INDEX, + _ => unreachable!(), + }; + let uniform = 1.0_f32 / ml_core::state_layout::SL_NUM_FEATURE_GROUPS as f32; + fused.trainer().write_isv_signal_at(idx, uniform); + } + } "isv_gamma_dir_eff" | "isv_gamma_mag_eff" | "isv_gamma_ord_eff" | "isv_gamma_urg_eff" => { // D.2 per-branch gamma slots. Reset to 0.0 at fold boundary; // per_branch_gamma_update GPU kernel re-populates on next epoch-boundary launch. diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index 1935381f9..7b502a553 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -291,6 +291,8 @@ Plan 4 Task 2c.3c.2 (2026-04-24): backward infrastructure additions — two help Plan 4 Task 2c.3c.3 (2026-04-24): GRN trunk backward scratch buffer allocation on `GpuDqnTrainer`. 8 new `CudaSlice` device buffers added (4 per GRN block × 2 blocks for h_s1 + h_s2): `bw_grn_h_s2_d_pre_ln` [B, SH2], `bw_grn_h_s2_d_linear_b_out` [B, 2*SH2], `bw_grn_h_s2_d_elu_out` [B, SH2], `bw_grn_h_s2_d_linear_a_out` [B, SH2], plus the four h_s1 mirrors at [B, SH1] (and 2*SH1 for the linear_b_out scratch — GLU's pre-activation has 2× hidden width because value-path + gate_pre are concatenated). The `d_pre_LN` buffer aliases both `d_glu_out` and `d_residual` (pure pass-through under `GrnBlock::backward_raw_phase1`'s pointer aliasing convention from 2c.3c.1); `d_linear_b_out` carries the phase1 → caller's Linear_b backward GEMM gradient; `d_elu_out` carries Linear_b backward's output into phase2; `d_linear_a_out` carries phase2's output to the caller's Linear_a backward GEMM. Allocated alongside the existing `alloc_backward_scratch` call in the trainer constructor (mirrors that helper's `stream.alloc_zeros::(size).map_err(...)?` pattern). Total footprint at SH1=SH2=256, B=512: 8 × 5 × 256 × 4B = 5.0 MB per fold (negligible against existing ~32 MB per-branch cuBLAS workspaces). Each field is `#[allow(dead_code)]` until 2c.3c.4's `apply_grn_trunk_backward_raw` helper consumes them — the comment `wired by Task 2c.3c.4` annotates each suppression. **Additive only — ZERO production callers in this commit**; the three backward panic gates (`backward_full`, `apply_iqn_trunk_gradient`, `apply_ensemble_diversity_backward`) remain in place until 2c.3c.4. Adam state for the new GRN tensors is auto-allocated since `m_buf`/`v_buf` are sized to `total_params + cutlass_tile_pad` which already includes the 13 GRN tensors after 2c.3a. cargo check clean at 11 warnings (baseline preserved); cargo build compiles 80 cubins (unchanged — no new kernel). +57 LOC. No new module / kernel / ISV slot / Orphan row. +Plan 4 Task 1B-ii (2026-04-25): VSN params + ISV slots landing — additive, no callers (param tensors written but unread; ISV slots cold-started but unread). 24 new param tensors appended at indices `[95..119)` covering the per-group VSN MLP (`Linear_1_g`/`b1_g`/`Linear_2_g`/`b2_g`) for each `FEATURE_GROUP_RANGES` entry — one quad per group, `VSN_HIDDEN_DIM = 16` (the new compile-time constant alongside `H_S2_RMS_EMA_INDEX` at the head of `gpu_dqn_trainer.rs`). Per-group element count `16*group_dim_g + 16 + 16 + 1`; sum `16*(42+32+16+16+8+7) + 6*33 = 1936 + 198 = 2134` floats (≈8.5 KB on params, mirrored on `target_params_buf` and Adam `m_buf`/`v_buf` — all four buffers grow automatically since their allocations use `compute_total_params(cfg) + cutlass_tile_pad`). `NUM_WEIGHT_TENSORS` 95 → 119. `compute_param_sizes()` rewritten to build a `[usize; NUM_WEIGHT_TENSORS]` from the existing 95-entry array literal `core` plus a runtime `for g in 0..SL_NUM_FEATURE_GROUPS` loop reading `FEATURE_GROUP_RANGES` from `ml_core::state_layout` (per Task 1A's prerequisite); `xavier_init_params_buf()` follows the same `core_fan` + per-group loop pattern with `(VSN_HIDDEN_DIM, group_dim)` for `w1_g`, `(1, VSN_HIDDEN_DIM)` for `w2_g`, and `(0,0)` (zero) for both biases — initial logits `≈ 0` therefore yield `softmax ≈ 1/SL_NUM_FEATURE_GROUPS` per group at every sample (cold-start neutral). 6 new ISV slots tail-appended for the per-group importance-mask EMA: `VSN_MASK_GROUP_0_EMA_INDEX=105` (market) through `VSN_MASK_GROUP_5_EMA_INDEX=110` (plan_isv); fingerprint pair shifted 103/104 → 111/112; `ISV_TOTAL_DIM` 105 → 113; `layout_fingerprint_seed()` extended with `VSN_MASK_GROUP_{0..5}_EMA=105..110;ISV_LAYOUT_FINGERPRINT_LO=111;ISV_LAYOUT_FINGERPRINT_HI=112;ISV_TOTAL_DIM=113;` plus the 24 new `PARAM_VSN_W1_G{g}=…;PARAM_VSN_B1_G{g}=…;PARAM_VSN_W2_G{g}=…;PARAM_VSN_B2_G{g}=…;` entries terminating at `PARAM_TOTAL_TENSORS=119` — new `LAYOUT_FINGERPRINT_CURRENT = 0x1b28321bb816f246` (was `0x5789155b683ab59c`). Constructor cold-start writes `1.0/SL_NUM_FEATURE_GROUPS = 1/6` (uniform prior — no group preference at init) into all six new ISV slots, mirroring the `H_S2_RMS_EMA = 1.0` cold-start convention from 2c.3c.5. `StateResetRegistry` extended with 6 new `FoldReset` entries (`isv_vsn_mask_g{0..5}_ema`); `training_loop.rs::reset_named_state` adds a single match arm covering all six names that recomputes the same `1.0 / SL_NUM_FEATURE_GROUPS` uniform value — no hardcoded `1.0/6.0` per `feedback_isv_for_adaptive_bounds.md` (the constant comes from `ml_core::state_layout::SL_NUM_FEATURE_GROUPS` so a future group-count change propagates automatically). Two new `pub(crate) static` cubin refs `VSN_FEATURE_SELECTION_CUBIN` and `VSN_MASK_EMA_CUBIN` added alongside the existing Plan 4 cubin block; `#[allow(dead_code)]` annotated because the runtime `load_cubin(...)` + `load_function(...)` calls are deferred to 1B-iii's forward orchestrator wire-in (the static refs themselves are compile-time `include_bytes!()` only — adding them in this commit means 1B-iii has zero new cubin-side file-touches). Mirrors the 2c.3a pattern (param tensor reshuffle without runtime callers — that landed cleanly because the runtime sites were panic-gated; here, the runtime sites simply don't exist yet so no gating needed). **Checkpoint break by intent** — fingerprint shift invalidates pre-1B-ii checkpoints at constructor load (fail-fast, no migration path per `feedback_no_legacy_aliases.md`). **No production callers in this commit** — params are written but unread (no consumer reads `param_buf[95..119)`), ISV slots are cold-started but unread (no consumer reads `isv_signals[105..111)`), and neither cubin is loaded yet. The forward orchestrator + cuBLAS `Linear_1`/`Linear_2` wire-in lands in 1B-iii; the backward orchestrator + autograd integration lands in 1B-iv. cargo check clean at 11 warnings (workspace baseline preserved); cargo build compiles 62/62 cubins clean (unchanged — no new `.cu` files, only new `pub(crate) static` refs to the 1B-i cubins). Smoke deferred — behaviour byte-identical to the f3e3ac347 / `0x5789155b683ab59c` post-bottleneck-fix baseline (700.43s, 3 folds × 5 epochs, per-fold best train Sharpe 6.53 / 80.11 / 66.72, geom-mean 39.27) since no consumer reads the new params or ISV slots; smoke re-validation with real numbers lands at 1B-iii where the forward orchestrator wire-in actually validates something. 2 new `dead_code`-annotated cubin refs (will retire `dead_code` at 1B-iii); 0 new Orphan rows (the kernels themselves were registered at 1B-i and remain Orphan-by-design until 1B-iii). 0 panic gates added/removed. + Plan 4 Task 1B-i (2026-04-25): VSN kernel module landing — additive, no callers. Two new CUDA kernel files registered in `build.rs::kernels_with_common` (kernel count 60 → 62, all compile clean under nvcc sm_80). Forward kernel `vsn_feature_selection_kernel.cu` exposes `vsn_softmax_and_gate_forward` — single-block-per-sample (256 threads × B blocks), reads `logits [B, num_groups]` + `state_in [B, state_dim_padded]`, computes a numerically-stable softmax over the 6 feature groups in shared memory (`max_l`+`exp_g/sum_e` serial pass on thread 0, broadcast via shmem since `num_groups=6` makes a tree reduce wasteful), saves the resulting mask `mask [B, num_groups]` for backward, and per-feature gate-multiplies the row (`gated_state[..., i] = state_in[..., i] * mask[group_of(i)]`); indices outside any group's `[group_begins[g], group_ends[g])` (the 7-element padding tail past `SL_PADDING_START`) are passed through unchanged so the byte image of the padding bytes matches the raw input regardless of downstream consumer behaviour. Backward kernel `vsn_softmax_and_gate_backward` implements the FULL softmax-over-groups Jacobian — `d_dot[g] = sum_{i in group_g} d_gated[i] * state[i]` via per-block shmem tree reduction (one pass per group, scratch reused across passes, no atomicAdd per `feedback_no_atomicadd.md`), then `d_logit[g] = mask[g] * (d_dot[g] - sum_h(mask[h] * d_dot[h]))` for the softmax derivative, plus per-feature `d_state[..., i] = d_gated[..., i] * mask[group_of(i)]` with the same passthrough convention as forward. Producer-only ISV kernel `vsn_mask_ema_kernel.cu` mirrors `h_s2_rms_ema_kernel.cu`'s shmem-reduce pattern exactly: 256 threads × 1 block, 256 floats of scratchpad reused across `num_groups` separate batch-mean reductions, EMA-updates `num_groups` adjacent ISV slots in `[isv_first_index, isv_first_index + num_groups)` with the caller-supplied `ema_alpha` (per-call parameter, not hardcoded — matches the `h_s2_rms_ema` / `reward_component_ema` plumbing convention from `training_loop.rs`). Layout convention for both kernels: row-major `[B, state_dim_padded]` for state, row-major `[B, num_groups]` for the mask (matches `state_layout.cuh` + sample-major convention from `dt_layernorm_kernel`). All reductions GPU-side per `pearl_cold_path_no_exception_to_gpu_drives.md`. Per-group `Linear_1_g` / `ReLU` / `Linear_2_g` MLPs (the importance-score producers feeding `logits`) remain caller-side cuBLAS GEMMs + the existing ReLU primitive — no new kernels for them, mirroring the `gpu_grn.rs` contract pattern. The kernels rely on `state_layout.cuh`'s Task 1A constants (`SL_NUM_FEATURE_GROUPS=6`, `SL_*_GROUP_BEGIN/END`) for compile-time bounds (the `vsn_group_of` helper unrolls a `#pragma unroll` loop bounded by `SL_NUM_FEATURE_GROUPS`); runtime `num_groups` / `group_begins[]` / `group_ends[]` are still passed in as scalar / pointer args for graph-capture symmetry with the host launchers (mirroring `gpu_grn.rs`'s style of taking dimensions as arguments). Module is **additive — ZERO production callers in this commit**; classified Orphan-by-design. Tasks 1B-iii (forward orchestrator + cuBLAS Linear_1/Linear_2 wire-in + per-group ReLU dispatch) and 1B-iv (backward orchestrator + autograd integration) wire it into the pre-trunk forward + backward path; Task 1B-ii allocates the param tensors and the 6 new ISV mask-EMA slots that `vsn_mask_ema_update`'s `isv_first_index` will point at. No checkpoint break, no fingerprint change, no new param tensors, no new ISV slots in this commit. Kernel LOC: vsn_feature_selection_kernel.cu = 262 lines, vsn_mask_ema_kernel.cu = 78 lines (340 lines total). Cubin sizes (sm_80, -O3): vsn_feature_selection_kernel.cubin = 31,392 B, vsn_mask_ema_kernel.cubin = 6,688 B. cargo check clean at 11 warnings (workspace baseline preserved); cargo build compiles 62/62 cubins clean (was 60 — `vsn_feature_selection_kernel.cubin` and `vsn_mask_ema_kernel.cubin` added). Smoke deferred — no behaviour change is possible since neither kernel is loaded by any Rust code in this commit; `multi_fold_convergence` byte-identical to the post-bottleneck-fix baseline (700.43s, 3 folds × 5 epochs, per-fold best train Sharpe 6.53 / 80.11 / 66.72, geom-mean 39.27). Smoke saved for 1B-iii where the forward orchestrator wire-in actually validates something. 2 new Orphan-by-design rows (will retire at 1B-iii when the forward kernel acquires a production caller via the encoder pre-pass). Bottleneck Linear runtime-indices + missing target/DDQN bn paths (2026-04-25, 2c.3a follow-up): three related bugs from the GRN trunk reshuffle that the runtime sites for the bottleneck Linear had silently inherited. (1) `launch_cublas_forward`'s online bottleneck GEMM and `launch_cublas_backward_to`'s `goff_w_bn` / `goff_b_bn` used stale legacy indices `on_w_ptrs[24]` / `padded_byte_offset(*, 24)` and `[25]`. After 2c.3a inserted 13 GRN tensors at indices [0..13), every legacy index ≥ 4 was supposed to migrate +9; these four bottleneck sites were missed. The post-2c.3a tensor at index 24 is `b_b1out` (153 floats = 3 × 51) and index 25 is `w_b2fc` (4288 floats = 64 × 67). The bottleneck Linear was therefore reading 153-float `b_b1out` as if it were the 672-float `w_bn` weight matrix and clipping after 153 floats, while the bias-add was treating the start of `w_b2fc` as a 16-float bias. The corresponding backward writes scribbled into the same wrong slots, so the gradient flow was self-consistent but acting on completely wrong tensors. The actual `w_bn` / `b_bn` tensors at the documented indices 33 / 34 sat untouched as zeros throughout training. Smoke tests passed despite this for ~10 commits because the GRN trunk's `Linear_residual` projection (which runs in parallel with the bottleneck path and reads raw states) provided a clean compensating pathway — the network was effectively training a residual-only encoder while the bottleneck slot accumulated gradient-corruption noise. Fix: replace 4 stale index references — forward `on_w_ptrs[24]` → `[33]`, forward `on_w_ptrs[25]` → `[34]`, backward `padded_byte_offset(.., 24)` → `(.., 33)`, backward `(.., 25)` → `(.., 34)`, with comments documenting the +9 migration that 2c.3a missed. (2) Target net's `forward_target_raw` passed raw `next_states_buf` directly to the encoder; the encoder expects `[B, s1_input_dim=102]` features (post-bottleneck shape), so it was reading the first 102 columns of `next_states_buf` (`[market | ofi | tlob | mtf]`) instead of `[bn_market_proj | portfolio]`. Fix: build a `tg_bn_concat_buf` mirroring the online inline-build but on `next_states_buf` with target weights `tg_w_ptrs[33]` / `tg_w_ptrs[34]`, and pass it as `tg_s1_input_ptr` to `forward_target_raw`. New buffer fields `tg_bn_hidden_buf` (`[B, bn_dim]`) + `tg_bn_concat_buf` (`[B, concat_dim]`) allocated alongside their online siblings. (3) DDQN argmax-pass `submit_forward_ops_ddqn` had the same shape mismatch and was running the online network on raw `next_states_buf`. Fix: build `on_next_bn_concat_buf` using **online** weights `on_w_ptrs[33]` / `[34]` (DDQN argmax uses online net on next_states, distinct from target's tg-on-next path); two new buffer fields `on_next_bn_hidden_buf` / `on_next_bn_concat_buf` allocated. The shared `bn_tanh_concat_kernel` is reused at all three call sites (online-on-states, target-on-next_states, ddqn-online-on-next_states); kernel itself unchanged. No fingerprint change (no ISV slot or param tensor added — pure runtime-index correction + new device buffers). Smoke (`cargo test … multi_fold_convergence --ignored --release`, 700.43s, 3 folds × 5 epochs): all 3 `dqn_fold{N}_best.safetensors` checkpoints written; per-fold best train Sharpe 6.53 / 80.11 / 66.72 at epochs 2 / 4 / 4; geom-mean 39.27 — the strongest smoke result of the Plan 4 chain to date (Task 3 was 20.03, 2c.3c.6 was 18.80). The Sharpe lift is consistent with the bottleneck Linear now seeing its actual weights and the target / DDQN nets seeing input-shape-consistent features for TD-target / argmax-action computations. cargo check clean at 11 warnings (baseline preserved); cargo build compiles all kernel cubins clean (no kernel changes). +166 / -9 LOC, all in `gpu_dqn_trainer.rs`. 0 panic gates added/removed. 0 stubs. No new module / kernel / ISV slot / Orphan row.