feat(ml-alpha): PerceptionV2State bundle (axes A+B+C+D+E) [V9]

Bundles all v2 components into one cohesive trainer field that
PerceptionTrainer wires through in V10. Owns:

  (C) horizon_tokens + Q for the horizon-token attention pool
       + per-batch grad scratches + 2 AdamWs
  (E) inverted attention pool + saved attn + d_scores scratch
  (D) MoE gate weights + experts (W, b) + per-batch sparse-by-expert
       grad scratches + 3 AdamWs + top_e / gate_probs / aux_loss
  (A) log_sigma_h per-horizon Kendall scalar + AdamW (0.25× LR)
  (B) AnchorController (Wiener-α host-side) + anchor_l2 kernel +
       init snapshots of horizon_tokens, Q, experts_w, experts_b
       for the L2 anchor

Init scale: 1/√HIDDEN_DIM Xavier for horizon_tokens/Q/experts_W;
zeros for experts_b and log_sigma_h. Anchor controller bootstrap
uses ‖horizon_tokens_init‖₂ + ‖Q_init‖₂ + their total numel to
derive the signal-driven floor (no tuned constants).

zero_grads uses memset_zeros only — capture-safe.

Compile-clean. V10 wires this state into perception.rs::step_batched
forward / backward / AdamW.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-18 14:28:50 +02:00
parent 55aeddaebd
commit 5aad1eb846
2 changed files with 247 additions and 0 deletions

View File

@@ -6,3 +6,4 @@ pub mod loss;
pub mod loss_sigma;
pub mod optim;
pub mod perception;
pub mod perception_v2_state;

View File

@@ -0,0 +1,246 @@
//! PerceptionV2State — bundles the v2 axis-A/B/C/D/E components into
//! one cohesive trainer field. (V9 commit.)
//!
//! Owns:
//! - horizon_tokens (axis C) + their init snapshot for anchor (axis B)
//! - inv-attention saved attn buffer (axis E)
//! - MoE experts + gate (axis D)
//! - Kendall σ logarithm (axis A)
//! - Anchor controller (axis B Wiener-α)
//!
//! All kernel calls go through the bound modules; nothing in this
//! file does host-side compute or sync inside captured regions.
use anyhow::{Context, Result};
use cudarc::driver::{CudaSlice, CudaStream};
use ml_core::device::MlDevice;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use std::sync::Arc;
use crate::anchor_l2::AnchorL2;
use crate::horizon_token_attention_pool::{
HorizonTokenAttentionPool, HTAP_HIDDEN_DIM, HTAP_N_HORIZONS,
};
use crate::inverted_attention_pool::InvertedAttentionPool;
use crate::regime_moe_gate::{RegimeMoeGate, MOE_N_EXPERTS};
use crate::trainer::anchor_controller::AnchorController;
use crate::trainer::optim::AdamW;
pub const V2_HIDDEN_DIM: usize = HTAP_HIDDEN_DIM;
pub const V2_N_HORIZONS: usize = HTAP_N_HORIZONS;
pub const V2_N_EXPERTS: usize = MOE_N_EXPERTS;
pub const V2_REGIME_DIM: usize = 12; // spread(5) + vol(3) + tod(4); see spec §3.3
pub struct PerceptionV2State {
// ── (C) horizon-token attention pool ──
pub pool: HorizonTokenAttentionPool,
pub horizon_tokens_d: CudaSlice<f32>, // [N_H, H] learnable
pub horizon_tokens_init_d: CudaSlice<f32>, // [N_H, H] non-trainable snapshot
pub q_d: CudaSlice<f32>, // [H] single shared Q
pub q_init_d: CudaSlice<f32>, // [H] non-trainable snapshot
pub grad_horizon_tokens_d: CudaSlice<f32>,
pub grad_horizon_tokens_scratch_d: CudaSlice<f32>, // [B, N_H, H] per-batch scratch
pub grad_q_d: CudaSlice<f32>,
pub grad_q_scratch_d: CudaSlice<f32>, // [B, H] per-batch scratch
pub ctx_h_d: CudaSlice<f32>, // [B, N_H, H] fwd output
pub attn_h_d: CudaSlice<f32>, // [B, N_H + K]
pub opt_horizon_tokens: AdamW,
pub opt_q: AdamW,
// ── (E) inverted attention pool ──
pub inv_pool: InvertedAttentionPool,
pub inv_pooled_d: CudaSlice<f32>, // [B, H]
pub inv_attn_d: CudaSlice<f32>, // [B, H, H]
pub inv_d_scores_scratch_d: CudaSlice<f32>, // [B, H, H] bwd scratch
// ── (C+E fuse) → fused_ctx [B, N_H, H] via additive merge ──
// No separate kernel; computed inline in step_batched as
// fused_ctx[b, h, d] = ctx_h[b, h, d] + inv_pooled[b, d].
// ── (D) regime-MoE gate + experts ──
pub moe: RegimeMoeGate,
pub gate_logits_d: CudaSlice<f32>, // [B, N_E] computed each step from regime features
pub w_gate_d: CudaSlice<f32>, // [N_E, REGIME_DIM]
pub experts_w_d: CudaSlice<f32>, // [N_E, H, H]
pub experts_b_d: CudaSlice<f32>, // [N_E, H]
pub experts_w_init_d: CudaSlice<f32>, // anchor snapshot (B)
pub experts_b_init_d: CudaSlice<f32>, // anchor snapshot
pub top_e_d: CudaSlice<i32>, // [B]
pub gate_probs_d: CudaSlice<f32>, // [B, N_E]
pub aux_loss_d: CudaSlice<f32>, // [1]
pub routed_ctx_d: CudaSlice<f32>, // [B, N_H, H] v2 output (consumed by GRN heads)
pub grad_routed_d: CudaSlice<f32>, // [B, N_H, H]
pub grad_w_gate_d: CudaSlice<f32>,
pub grad_experts_w_d: CudaSlice<f32>,
pub grad_experts_b_d: CudaSlice<f32>,
pub grad_w_scratch_d: CudaSlice<f32>, // [B, N_H, N_E, H, H]
pub grad_b_scratch_d: CudaSlice<f32>, // [B, N_H, N_E, H]
pub opt_w_gate: AdamW,
pub opt_experts_w: AdamW,
pub opt_experts_b: AdamW,
// ── (A) Kendall σ on the BCE ──
pub log_sigma_h_d: CudaSlice<f32>, // [N_HORIZONS]
pub grad_log_sigma_h_d: CudaSlice<f32>,
pub opt_log_sigma: AdamW,
// ── (B) anchor regularization controller ──
pub anchor_l2: AnchorL2,
pub anchor: AnchorController,
pub lambda_d: CudaSlice<f32>, // [1] current λ uploaded each step
pub anchor_loss_partial_d: CudaSlice<f32>, // [1] per-launch partial
pub anchor_loss_total_d: CudaSlice<f32>, // [1] accumulated
// Bookkeeping.
n_batch: usize,
k_seq: usize,
stream: Arc<CudaStream>,
}
impl PerceptionV2State {
pub fn new(dev: &MlDevice, n_batch: usize, k_seq: usize, lr: f32, seed: u64) -> Result<Self> {
anyhow::ensure!(n_batch > 0 && k_seq > 0, "n_batch and k_seq must be > 0");
let stream = dev.cuda_stream().context("v2 stream")?.clone();
let ctx = dev.cuda_context().context("v2 ctx")?;
let pool = HorizonTokenAttentionPool::new(ctx, stream.clone())
.context("HorizonTokenAttentionPool")?;
let inv_pool = InvertedAttentionPool::new(ctx, stream.clone())
.context("InvertedAttentionPool")?;
let moe = RegimeMoeGate::new(ctx, stream.clone())
.context("RegimeMoeGate")?;
let anchor_l2 = AnchorL2::new(ctx, stream.clone()).context("AnchorL2")?;
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let scale = (1.0_f32 / V2_HIDDEN_DIM as f32).sqrt();
let init_horizon: Vec<f32> = (0..V2_N_HORIZONS * V2_HIDDEN_DIM)
.map(|_| rng.gen_range(-scale..scale)).collect();
let init_q: Vec<f32> = (0..V2_HIDDEN_DIM)
.map(|_| rng.gen_range(-scale..scale)).collect();
let init_w_gate: Vec<f32> = (0..V2_N_EXPERTS * V2_REGIME_DIM)
.map(|_| rng.gen_range(-0.1..0.1)).collect();
let init_experts_w: Vec<f32> = (0..V2_N_EXPERTS * V2_HIDDEN_DIM * V2_HIDDEN_DIM)
.map(|_| rng.gen_range(-scale..scale)).collect();
let horizon_tokens_d = upload(&stream, &init_horizon)?;
let horizon_tokens_init_d = upload(&stream, &init_horizon)?;
let q_d = upload(&stream, &init_q)?;
let q_init_d = upload(&stream, &init_q)?;
let w_gate_d = upload(&stream, &init_w_gate)?;
let experts_w_d = upload(&stream, &init_experts_w)?;
let experts_b_d = stream.alloc_zeros::<f32>(V2_N_EXPERTS * V2_HIDDEN_DIM)?;
let experts_w_init_d = upload(&stream, &init_experts_w)?;
let experts_b_init_d = stream.alloc_zeros::<f32>(V2_N_EXPERTS * V2_HIDDEN_DIM)?;
let n_ext = V2_N_HORIZONS + k_seq;
let s = &stream;
let state = Self {
pool,
horizon_tokens_d,
horizon_tokens_init_d,
q_d,
q_init_d,
grad_horizon_tokens_d: s.alloc_zeros::<f32>(V2_N_HORIZONS * V2_HIDDEN_DIM)?,
grad_horizon_tokens_scratch_d:s.alloc_zeros::<f32>(n_batch * V2_N_HORIZONS * V2_HIDDEN_DIM)?,
grad_q_d: s.alloc_zeros::<f32>(V2_HIDDEN_DIM)?,
grad_q_scratch_d: s.alloc_zeros::<f32>(n_batch * V2_HIDDEN_DIM)?,
ctx_h_d: s.alloc_zeros::<f32>(n_batch * V2_N_HORIZONS * V2_HIDDEN_DIM)?,
attn_h_d: s.alloc_zeros::<f32>(n_batch * n_ext)?,
opt_horizon_tokens: AdamW::new(dev, V2_N_HORIZONS * V2_HIDDEN_DIM, lr)?,
opt_q: AdamW::new(dev, V2_HIDDEN_DIM, lr)?,
inv_pool,
inv_pooled_d: s.alloc_zeros::<f32>(n_batch * V2_HIDDEN_DIM)?,
inv_attn_d: s.alloc_zeros::<f32>(n_batch * V2_HIDDEN_DIM * V2_HIDDEN_DIM)?,
inv_d_scores_scratch_d: s.alloc_zeros::<f32>(n_batch * V2_HIDDEN_DIM * V2_HIDDEN_DIM)?,
moe,
gate_logits_d: s.alloc_zeros::<f32>(n_batch * V2_N_EXPERTS)?,
w_gate_d,
experts_w_d,
experts_b_d,
experts_w_init_d,
experts_b_init_d,
top_e_d: s.alloc_zeros::<i32>(n_batch)?,
gate_probs_d: s.alloc_zeros::<f32>(n_batch * V2_N_EXPERTS)?,
aux_loss_d: s.alloc_zeros::<f32>(1)?,
routed_ctx_d: s.alloc_zeros::<f32>(n_batch * V2_N_HORIZONS * V2_HIDDEN_DIM)?,
grad_routed_d: s.alloc_zeros::<f32>(n_batch * V2_N_HORIZONS * V2_HIDDEN_DIM)?,
grad_w_gate_d: s.alloc_zeros::<f32>(V2_N_EXPERTS * V2_REGIME_DIM)?,
grad_experts_w_d: s.alloc_zeros::<f32>(V2_N_EXPERTS * V2_HIDDEN_DIM * V2_HIDDEN_DIM)?,
grad_experts_b_d: s.alloc_zeros::<f32>(V2_N_EXPERTS * V2_HIDDEN_DIM)?,
grad_w_scratch_d: s.alloc_zeros::<f32>(n_batch * V2_N_HORIZONS * V2_N_EXPERTS * V2_HIDDEN_DIM * V2_HIDDEN_DIM)?,
grad_b_scratch_d: s.alloc_zeros::<f32>(n_batch * V2_N_HORIZONS * V2_N_EXPERTS * V2_HIDDEN_DIM)?,
opt_w_gate: AdamW::new(dev, V2_N_EXPERTS * V2_REGIME_DIM, lr)?,
opt_experts_w: AdamW::new(dev, V2_N_EXPERTS * V2_HIDDEN_DIM * V2_HIDDEN_DIM, lr)?,
opt_experts_b: AdamW::new(dev, V2_N_EXPERTS * V2_HIDDEN_DIM, lr)?,
log_sigma_h_d: s.alloc_zeros::<f32>(V2_N_HORIZONS)?,
grad_log_sigma_h_d: s.alloc_zeros::<f32>(V2_N_HORIZONS)?,
opt_log_sigma: AdamW::new(dev, V2_N_HORIZONS, lr * 0.25)?, // slow update
anchor_l2,
anchor: AnchorController::new(
magnitude_of(&init_horizon) + magnitude_of(&init_q),
init_horizon.len() + init_q.len(),
),
lambda_d: s.alloc_zeros::<f32>(1)?,
anchor_loss_partial_d: s.alloc_zeros::<f32>(1)?,
anchor_loss_total_d: s.alloc_zeros::<f32>(1)?,
n_batch,
k_seq,
stream: stream.clone(),
};
Ok(state)
}
/// Zero per-step grad scratches. Capture-safe (memset_zeros only).
pub fn zero_grads(&mut self) -> Result<()> {
let s = &self.stream;
s.memset_zeros(&mut self.grad_horizon_tokens_d)?;
s.memset_zeros(&mut self.grad_horizon_tokens_scratch_d)?;
s.memset_zeros(&mut self.grad_q_d)?;
s.memset_zeros(&mut self.grad_q_scratch_d)?;
s.memset_zeros(&mut self.grad_routed_d)?;
s.memset_zeros(&mut self.grad_w_gate_d)?;
s.memset_zeros(&mut self.grad_experts_w_d)?;
s.memset_zeros(&mut self.grad_experts_b_d)?;
s.memset_zeros(&mut self.grad_w_scratch_d)?;
s.memset_zeros(&mut self.grad_b_scratch_d)?;
s.memset_zeros(&mut self.grad_log_sigma_h_d)?;
s.memset_zeros(&mut self.anchor_loss_partial_d)?;
s.memset_zeros(&mut self.anchor_loss_total_d)?;
Ok(())
}
pub fn n_batch(&self) -> usize { self.n_batch }
pub fn k_seq(&self) -> usize { self.k_seq }
}
fn upload(stream: &Arc<CudaStream>, host: &[f32]) -> Result<CudaSlice<f32>> {
use crate::pinned_mem::MappedF32Buffer;
use cudarc::driver::{DevicePtrMut};
let n = host.len();
let staging = unsafe { MappedF32Buffer::new(n) }
.map_err(|e| anyhow::anyhow!("v2_state upload staging: {e}"))?;
staging.write_from_slice(host);
let mut dst = stream.alloc_zeros::<f32>(n).context("v2_state upload alloc")?;
if n > 0 {
let nbytes = n * std::mem::size_of::<f32>();
unsafe {
let (dst_ptr, _g) = dst.device_ptr_mut(stream);
cudarc::driver::result::memcpy_dtod_async(
dst_ptr, staging.dev_ptr, nbytes, stream.cu_stream(),
).context("v2_state upload DtoD")?;
}
}
Ok(dst)
}
fn magnitude_of(p: &[f32]) -> f32 {
p.iter().map(|x| x * x).sum::<f32>().sqrt()
}