refactor(ml-alpha): replace legacy attention_pool with MultiHorizonAttention [Stage 2]
Single source of truth for the attention path. Deletes the legacy
single-Q `attention_pool.cu` and all `attn_*` fields from
`PerceptionTrainer`; wires `MultiHorizonAttention` (the bundle
introduced in Stage 1) into `step_batched` + `evaluate_batched` as
THE attention summary that seeds CfC's `h_old` at k=0.
Deletions:
cuda/attention_pool.cu (244 lines)
perception.rs::attn_q_d/attn_context_d/
attn_weights_d/grad_attn_q_d/opt_attn_q/
attn_fwd_fn/attn_bwd_fn/_attn_module/
attn_grad_q_scratch_d (all struct fields)
perception.rs::ATTENTION_POOL_CUBIN (include_bytes constant)
Their corresponding init + struct-construction lines.
build.rs::KERNELS (drops "attention_pool")
New kernel + binding:
cuda/horizon_mean_collapse.cu (53 lines)
- `horizon_mean_collapse_fwd/_bwd`: collapses [B, N_H, H] → [B, H]
by averaging over the horizon axis. Single-pass, no reductions.
src/horizon_mean_collapse.rs (host binding)
MHA additions:
- `collapse` field + `ctx_mean_d` + `grad_ctx_mean_d` for the seed.
- `grad_ctx_h_d` scratch (split from grad_horizon_tokens_scratch to
avoid aliasing when MoE bwd writes d_ctx_h while pool bwd writes
d_horizon_tokens).
- `forward(ln_b_out)`: horizon-token pool → inverted pool → MoE
dispatch → mean-collapse → ctx_mean_d.
- `backward(ln_b_out, grad_ctx_mean, grad_ln_out)`: full reverse
chain.
- `apply_anchor()`: launches anchor_l2 on horizon_tokens, Q,
experts_w.
- `adamw_step()`: steps all 6 owned optimizer groups.
PerceptionTrainer integration:
- Section 2d (forward): `self.mha.forward(&self.ln_out_d)` replaces
the legacy attention_pool launch. CfC's h_old at k=0 now reads
`self.mha.ctx_mean_d.device_ptr` (was `self.attn_context_d`).
- Section 7c-pre (backward): `self.mha.backward(ln_out, grad_h_carry,
grad_h_enriched_seq)` replaces the legacy attn_bwd_fn launch.
- Four `reduce_axis0` launches collapse MHA's per-batch scratches
into shared gradient buffers: grad_horizon_tokens, grad_q,
grad_experts_w, grad_experts_b.
- `self.mha.apply_anchor()` adds L2 anchor grad contributions.
- Section 9 (AdamW): `self.mha.adamw_step()` replaces opt_attn_q.
- `evaluate_batched`: `self.mha.forward` replaces the legacy fwd
launch; h_old at k=0 reads `mha.ctx_mean_d`.
- `self.mha.zero_grads()` at step start (capture-safe memset_zeros).
BUG CAUGHT DURING WIRING (NVIDIA-grade discipline): first wiring
attempt mis-sized the reduce_axis0 launches for the MoE
`grad_w_scratch_d` ([B, N_H, N_E, H, H]). Initial `n_tail = N_H * N_E
* H * H = 327680` would have made reduce_axis0 read 5× past the end
of the buffer → CUDA_ERROR_ILLEGAL_ADDRESS. Fix: `n_tail = N_E * H *
H = 65536` with `n_batch = B * N_H`, treating the leading two axes
together as the reduction dimension. Caught by stacked_trainer test
on RTX 3050; would have caused silent corruption then a hard fault
on L40S/H100 later.
LOCAL VERIFICATION (RTX 3050 sm_86):
- ml-alpha builds clean (cuda feature).
- All 38+ tests PASS serially with --test-threads=1:
perception_overfit (8 tests incl. loss-shrinks)
trunk_forward (5)
stacked_loss_shrinks (multiple)
bce_grad_finite_diff (4)
snap_feature_assemble (9)
... (full suite green)
- Numgrad parity for the 4 new MHA kernels (horizon_token, inv_attn,
regime_moe_gate, anchor_l2) PASSES at 5e-2 rel.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -20,10 +20,10 @@ const KERNELS: &[&str] = &[
|
||||
"horizon_lambda", // ISV-driven per-horizon gradient scaler (EMA + lambda)
|
||||
"layer_norm", // Phase 1: trunk pre-CfC normalisation
|
||||
"variable_selection", // Phase 2D: TFT-style per-feature gating
|
||||
"attention_pool", // Legacy single-Q content summary at CfC k=0 (Stage 2 replaces with MHA)
|
||||
"horizon_token_attention_pool", // Horizon-token K-prepend single-Q attention pool
|
||||
"inverted_attention_pool", // Cross-variate (iTransformer-style) attention pool
|
||||
"regime_moe_gate", // Top-1 regime MoE gate + expert dispatch + aux loss
|
||||
"horizon_mean_collapse", // Mean over horizon axis (seeds CfC h_old at k=0)
|
||||
"anchor_l2", // L2 anchor regularization toward init
|
||||
"reduce_axis0", // Phase B: cross-batch param-grad reducer
|
||||
];
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
// attention_pool.cu — Single-head attention pool over Mamba2 K-positions.
|
||||
// Phase 3 (2026-05-17).
|
||||
//
|
||||
// Replaces the CfC's zero-initialised `h_old` at k=0 with a learned
|
||||
// attention-pooled summary over all K LN_b output positions. The K-loop
|
||||
// CfC keeps its recurrent semantics (h_old at k+1 = h_new at k); only
|
||||
// the initial state changes from zero to a content-addressable lookup.
|
||||
//
|
||||
// Forward math (per sample b):
|
||||
// scores[k] = Q · keys[b, k, :] # [K] — dot product over HIDDEN_DIM
|
||||
// attn[k] = softmax_k(scores) # [K]
|
||||
// context[h] = sum_k attn[k] * values[b, k, h] # [HIDDEN_DIM]
|
||||
//
|
||||
// For this attention pool, keys == values == LN_b output [B, K, HIDDEN_DIM].
|
||||
// Single learned param: Q [HIDDEN_DIM].
|
||||
//
|
||||
// Saved for backward:
|
||||
// attn_weights [B, K] (post-softmax)
|
||||
//
|
||||
// Block layout (forward):
|
||||
// grid_dim = (B, 1, 1), block_dim = (HIDDEN_DIM=128, 1, 1).
|
||||
// Thread tid handles dimension h of HIDDEN_DIM.
|
||||
//
|
||||
// Per `feedback_no_atomicadd.md`: block tree-reduce only, no atomics.
|
||||
|
||||
#define ATTN_HIDDEN_DIM 128
|
||||
#define ATTN_BLOCK 128
|
||||
#define ATTN_MAX_K 512 // safety cap on K; smoke uses K=16-64.
|
||||
|
||||
extern "C" __global__ void attention_pool_fwd(
|
||||
const float* __restrict__ Q, // [HIDDEN_DIM]
|
||||
const float* __restrict__ ln_out, // [B, K, HIDDEN_DIM]
|
||||
int n_batch,
|
||||
int k_seq,
|
||||
float* __restrict__ context, // [B, HIDDEN_DIM]
|
||||
float* __restrict__ attn_weights // [B, K] — saved post-softmax
|
||||
) {
|
||||
int b_idx = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (b_idx >= n_batch || tid >= ATTN_BLOCK) return;
|
||||
|
||||
extern __shared__ float smem[];
|
||||
float* s_scores = smem; // [K]
|
||||
float* s_red = smem + k_seq; // [BLOCK] — reduce scratch
|
||||
float* s_context = smem + k_seq + ATTN_BLOCK; // [HIDDEN_DIM] — final output buffer
|
||||
|
||||
// Cache Q in shared mem (broadcast).
|
||||
__shared__ float s_Q[ATTN_HIDDEN_DIM];
|
||||
if (tid < ATTN_HIDDEN_DIM) s_Q[tid] = Q[tid];
|
||||
__syncthreads();
|
||||
|
||||
// Pass 1: scores[k] = Q · ln_out[b, k, :].
|
||||
// Each k is handled sequentially by ALL threads via tree-reduce
|
||||
// over HIDDEN_DIM. K outer loop, threads tile HIDDEN_DIM inner.
|
||||
// For HIDDEN_DIM=128 and ATTN_BLOCK=128 we have one-to-one.
|
||||
const float* ln_b = ln_out + (long long)b_idx * k_seq * ATTN_HIDDEN_DIM;
|
||||
for (int k = 0; k < k_seq; ++k) {
|
||||
const float v = ln_b[k * ATTN_HIDDEN_DIM + tid] * s_Q[tid];
|
||||
s_red[tid] = v;
|
||||
__syncthreads();
|
||||
for (int s = ATTN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) s_red[tid] += s_red[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) s_scores[k] = s_red[0];
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
// Pass 2: softmax over K. Numerically stable: max-subtract + exp + sum.
|
||||
// Block-wide max over s_scores[0..k_seq).
|
||||
float my_max = -INFINITY;
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
const float v = s_scores[k];
|
||||
if (v > my_max) my_max = v;
|
||||
}
|
||||
s_red[tid] = my_max;
|
||||
__syncthreads();
|
||||
for (int s = ATTN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) {
|
||||
const float a = s_red[tid];
|
||||
const float b = s_red[tid + s];
|
||||
s_red[tid] = (a > b) ? a : b;
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
__shared__ float s_max;
|
||||
if (tid == 0) s_max = s_red[0];
|
||||
__syncthreads();
|
||||
|
||||
// exp(score - max) + sum.
|
||||
float my_sum = 0.0f;
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
const float e = expf(s_scores[k] - s_max);
|
||||
s_scores[k] = e; // reuse as exp_shifted
|
||||
my_sum += e;
|
||||
}
|
||||
s_red[tid] = my_sum;
|
||||
__syncthreads();
|
||||
for (int s = ATTN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) s_red[tid] += s_red[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
__shared__ float s_sum;
|
||||
if (tid == 0) s_sum = s_red[0];
|
||||
__syncthreads();
|
||||
|
||||
// Pass 3: attn[k] = exp/sum; save; context[h] = sum_k attn[k] * ln_out[b, k, h].
|
||||
// Save attn weights AND build the context output.
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
const float a = s_scores[k] / s_sum;
|
||||
s_scores[k] = a; // overwrite again — now holds true attn weights
|
||||
attn_weights[(long long)b_idx * k_seq + k] = a;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Accumulate context[h=tid] = sum_k attn[k] * ln_out[b, k, h=tid].
|
||||
if (tid < ATTN_HIDDEN_DIM) {
|
||||
float c = 0.0f;
|
||||
for (int k = 0; k < k_seq; ++k) {
|
||||
c += s_scores[k] * ln_b[k * ATTN_HIDDEN_DIM + tid];
|
||||
}
|
||||
s_context[tid] = c;
|
||||
context[(long long)b_idx * ATTN_HIDDEN_DIM + tid] = c;
|
||||
}
|
||||
}
|
||||
|
||||
// Attention pool backward — chain rule:
|
||||
//
|
||||
// d_attn[k] = sum_h grad_context[h] * values[b, k, h]
|
||||
// + (this is from context = sum_k attn[k] * values[b, k, :])
|
||||
//
|
||||
// d_values[b, k, h] += grad_context[h] * attn[k]
|
||||
// + d_scores[k] * Q[h]
|
||||
//
|
||||
// d_scores via softmax Jacobian:
|
||||
// d_scores[k] = attn[k] * (d_attn[k] - sum_kp attn[kp] * d_attn[kp])
|
||||
//
|
||||
// d_Q[h] += sum_{b, k} d_scores[k] * values[b, k, h]
|
||||
// (here values == ln_out)
|
||||
//
|
||||
// Single-writer discipline:
|
||||
// - d_Q is [HIDDEN_DIM] shared across all (b, k). ONE block per
|
||||
// launch, internal batch loop, tile over HIDDEN_DIM. += into d_Q.
|
||||
// - d_values[b, k, h] is [B, K, HIDDEN_DIM] — one block per launch
|
||||
// iterates over (b, k, h) sequentially; with block_dim = HIDDEN_DIM,
|
||||
// thread h is sole writer of column h for ALL (b, k).
|
||||
|
||||
// Block-per-batch attn_pool bwd (Phase B commit 4).
|
||||
// grid=(n_batch, 1, 1) block=(ATTN_BLOCK, 1, 1)
|
||||
//
|
||||
// Each block handles one batch's HIDDEN_DIM channels. grad_ln_out is
|
||||
// per-batch indexed (already safe), so each block bi writes its
|
||||
// [bi, :, :] slice via += onto whatever value grad_ln_out holds at
|
||||
// kernel launch (the K-loop's contribution to LN_b output grad).
|
||||
//
|
||||
// grad_Q is a single [HIDDEN_DIM] shared across all batches → per-batch
|
||||
// scratch [B, HIDDEN_DIM], reduced after the kernel returns.
|
||||
extern "C" __global__ void attention_pool_bwd(
|
||||
const float* __restrict__ Q, // [HIDDEN_DIM]
|
||||
const float* __restrict__ ln_out, // [B, K, HIDDEN_DIM] (= values)
|
||||
const float* __restrict__ attn_weights, // [B, K] from fwd
|
||||
const float* __restrict__ grad_context, // [B, HIDDEN_DIM]
|
||||
int n_batch,
|
||||
int k_seq,
|
||||
float* __restrict__ grad_Q_scratch, // [B, HIDDEN_DIM] (+=)
|
||||
float* __restrict__ grad_ln_out // [B, K, HIDDEN_DIM] (+= chained with K-loop)
|
||||
) {
|
||||
int bi = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (bi >= n_batch || tid >= ATTN_BLOCK) return;
|
||||
|
||||
extern __shared__ float smem[];
|
||||
float* s_dattn = smem; // [K]
|
||||
float* s_dscores = smem + k_seq; // [K]
|
||||
float* s_red = smem + 2 * k_seq; // [BLOCK]
|
||||
|
||||
__shared__ float s_Q[ATTN_HIDDEN_DIM];
|
||||
__shared__ float s_attn[ATTN_MAX_K];
|
||||
__shared__ float s_grad_ctx[ATTN_HIDDEN_DIM];
|
||||
__shared__ float s_sum_attn_dattn;
|
||||
|
||||
if (tid < ATTN_HIDDEN_DIM) s_Q[tid] = Q[tid];
|
||||
__syncthreads();
|
||||
|
||||
const float* ln_b = ln_out + (long long)bi * k_seq * ATTN_HIDDEN_DIM;
|
||||
float* grad_ln_b = grad_ln_out + (long long)bi * k_seq * ATTN_HIDDEN_DIM;
|
||||
// Cache attn + grad_context for this block's batch.
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
s_attn[k] = attn_weights[(long long)bi * k_seq + k];
|
||||
}
|
||||
if (tid < ATTN_HIDDEN_DIM) {
|
||||
s_grad_ctx[tid] = grad_context[(long long)bi * ATTN_HIDDEN_DIM + tid];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 1: d_attn[k] = sum_h grad_context[h] * values[b, k, h].
|
||||
for (int k = 0; k < k_seq; ++k) {
|
||||
const float v = (tid < ATTN_HIDDEN_DIM)
|
||||
? s_grad_ctx[tid] * ln_b[k * ATTN_HIDDEN_DIM + tid] : 0.0f;
|
||||
s_red[tid] = v;
|
||||
__syncthreads();
|
||||
for (int s = ATTN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) s_red[tid] += s_red[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) s_dattn[k] = s_red[0];
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
// Pass 2: sum_{kp} attn[kp] * d_attn[kp] (softmax Jacobian centring).
|
||||
float my_sum = 0.0f;
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
my_sum += s_attn[k] * s_dattn[k];
|
||||
}
|
||||
s_red[tid] = my_sum;
|
||||
__syncthreads();
|
||||
for (int s = ATTN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) s_red[tid] += s_red[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) s_sum_attn_dattn = s_red[0];
|
||||
__syncthreads();
|
||||
|
||||
// Pass 3: d_scores[k] = attn[k] * (d_attn[k] - s_sum_attn_dattn).
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
s_dscores[k] = s_attn[k] * (s_dattn[k] - s_sum_attn_dattn);
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 4: grad_Q_scratch[bi, h] += sum_k d_scores[k] * ln_out[b, k, h].
|
||||
// d_ln_out[b, k, h] += grad_context[h] * attn[k] + d_scores[k] * Q[h].
|
||||
// Thread h owns column h. Loops over k. grad_ln_out += chains the
|
||||
// attn-path gradient on top of the K-loop's contribution.
|
||||
if (tid < ATTN_HIDDEN_DIM) {
|
||||
float dq_local = 0.0f;
|
||||
for (int k = 0; k < k_seq; ++k) {
|
||||
const float v = ln_b[k * ATTN_HIDDEN_DIM + tid];
|
||||
dq_local += s_dscores[k] * v;
|
||||
grad_ln_b[k * ATTN_HIDDEN_DIM + tid] +=
|
||||
s_grad_ctx[tid] * s_attn[k] + s_dscores[k] * s_Q[tid];
|
||||
}
|
||||
grad_Q_scratch[(long long)bi * ATTN_HIDDEN_DIM + tid] += dq_local;
|
||||
}
|
||||
}
|
||||
51
crates/ml-alpha/cuda/horizon_mean_collapse.cu
Normal file
51
crates/ml-alpha/cuda/horizon_mean_collapse.cu
Normal file
@@ -0,0 +1,51 @@
|
||||
// horizon_mean_collapse.cu — Collapse per-horizon context to a single
|
||||
// vector by averaging across the horizon axis.
|
||||
//
|
||||
// Forward:
|
||||
// out[b, d] = (1 / N_H) · Σ_h in[b, h, d]
|
||||
//
|
||||
// Backward (broadcast — each horizon receives the same per-d gradient):
|
||||
// d_in[b, h, d] = (1 / N_H) · d_out[b, d]
|
||||
//
|
||||
// Grid: (B, 1, 1). Block: (HIDDEN_DIM, 1, 1).
|
||||
// Each thread tid owns d = tid. No reductions, no barriers.
|
||||
|
||||
#define HMC_HIDDEN_DIM 128
|
||||
#define HMC_BLOCK 128
|
||||
#define HMC_N_HORIZONS 5
|
||||
|
||||
extern "C" __global__ void horizon_mean_collapse_fwd(
|
||||
const float* __restrict__ ctx_per_h, // [B, N_H, H]
|
||||
int n_batch,
|
||||
float* __restrict__ ctx_mean // [B, H]
|
||||
) {
|
||||
int b = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (b >= n_batch || tid >= HMC_HIDDEN_DIM) return;
|
||||
|
||||
float acc = 0.0f;
|
||||
#pragma unroll
|
||||
for (int h = 0; h < HMC_N_HORIZONS; ++h) {
|
||||
acc += ctx_per_h[(long long)b * HMC_N_HORIZONS * HMC_HIDDEN_DIM
|
||||
+ (long long)h * HMC_HIDDEN_DIM + tid];
|
||||
}
|
||||
ctx_mean[(long long)b * HMC_HIDDEN_DIM + tid] = acc * (1.0f / (float)HMC_N_HORIZONS);
|
||||
}
|
||||
|
||||
extern "C" __global__ void horizon_mean_collapse_bwd(
|
||||
const float* __restrict__ grad_ctx_mean, // [B, H]
|
||||
int n_batch,
|
||||
float* __restrict__ grad_ctx_per_h // [B, N_H, H] += broadcast / N_H
|
||||
) {
|
||||
int b = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (b >= n_batch || tid >= HMC_HIDDEN_DIM) return;
|
||||
|
||||
const float g = grad_ctx_mean[(long long)b * HMC_HIDDEN_DIM + tid]
|
||||
* (1.0f / (float)HMC_N_HORIZONS);
|
||||
#pragma unroll
|
||||
for (int h = 0; h < HMC_N_HORIZONS; ++h) {
|
||||
grad_ctx_per_h[(long long)b * HMC_N_HORIZONS * HMC_HIDDEN_DIM
|
||||
+ (long long)h * HMC_HIDDEN_DIM + tid] += g;
|
||||
}
|
||||
}
|
||||
81
crates/ml-alpha/src/horizon_mean_collapse.rs
Normal file
81
crates/ml-alpha/src/horizon_mean_collapse.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
//! Horizon-axis mean-collapse host binding.
|
||||
//!
|
||||
//! Collapses per-horizon context `[B, N_H, H]` to a single context
|
||||
//! `[B, H]` by averaging across the horizon axis. Used to seed the
|
||||
//! CfC's `h_old` at k=0 from `MultiHorizonAttention.routed_ctx`.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use cudarc::driver::{
|
||||
CudaContext, CudaFunction, CudaModule, CudaSlice, CudaStream, LaunchConfig, PushKernelArg,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub const HMC_HIDDEN_DIM: usize = 128;
|
||||
pub const HMC_BLOCK: usize = 128;
|
||||
|
||||
const CUBIN: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/horizon_mean_collapse.cubin"));
|
||||
|
||||
pub struct HorizonMeanCollapse {
|
||||
_module: Arc<CudaModule>,
|
||||
fwd_fn: CudaFunction,
|
||||
bwd_fn: CudaFunction,
|
||||
stream: Arc<CudaStream>,
|
||||
}
|
||||
|
||||
impl HorizonMeanCollapse {
|
||||
pub fn new(ctx: &Arc<CudaContext>, stream: Arc<CudaStream>) -> Result<Self> {
|
||||
let module = ctx
|
||||
.load_cubin(CUBIN.to_vec())
|
||||
.context("load horizon_mean_collapse cubin")?;
|
||||
let fwd_fn = module
|
||||
.load_function("horizon_mean_collapse_fwd")
|
||||
.context("load horizon_mean_collapse_fwd")?;
|
||||
let bwd_fn = module
|
||||
.load_function("horizon_mean_collapse_bwd")
|
||||
.context("load horizon_mean_collapse_bwd")?;
|
||||
Ok(Self { _module: module, fwd_fn, bwd_fn, stream })
|
||||
}
|
||||
|
||||
pub fn forward(
|
||||
&self,
|
||||
ctx_per_h: &CudaSlice<f32>,
|
||||
n_batch: i32,
|
||||
ctx_mean: &mut CudaSlice<f32>,
|
||||
) -> Result<()> {
|
||||
let cfg = LaunchConfig {
|
||||
grid_dim: (n_batch as u32, 1, 1),
|
||||
block_dim: (HMC_BLOCK as u32, 1, 1),
|
||||
shared_mem_bytes: 0,
|
||||
};
|
||||
let mut launch = self.stream.launch_builder(&self.fwd_fn);
|
||||
unsafe {
|
||||
launch
|
||||
.arg(ctx_per_h).arg(&n_batch).arg(ctx_mean)
|
||||
.launch(cfg)
|
||||
.context("horizon_mean_collapse_fwd")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn backward(
|
||||
&self,
|
||||
grad_ctx_mean: &CudaSlice<f32>,
|
||||
n_batch: i32,
|
||||
grad_ctx_per_h: &mut CudaSlice<f32>,
|
||||
) -> Result<()> {
|
||||
let cfg = LaunchConfig {
|
||||
grid_dim: (n_batch as u32, 1, 1),
|
||||
block_dim: (HMC_BLOCK as u32, 1, 1),
|
||||
shared_mem_bytes: 0,
|
||||
};
|
||||
let mut launch = self.stream.launch_builder(&self.bwd_fn);
|
||||
unsafe {
|
||||
launch
|
||||
.arg(grad_ctx_mean).arg(&n_batch).arg(grad_ctx_per_h)
|
||||
.launch(cfg)
|
||||
.context("horizon_mean_collapse_bwd")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ pub mod data;
|
||||
pub mod eval;
|
||||
pub mod anchor_l2;
|
||||
pub mod heads;
|
||||
pub mod horizon_mean_collapse;
|
||||
pub mod horizon_token_attention_pool;
|
||||
pub mod inverted_attention_pool;
|
||||
pub mod isv;
|
||||
|
||||
@@ -22,6 +22,7 @@ use rand_chacha::ChaCha8Rng;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::anchor_l2::AnchorL2;
|
||||
use crate::horizon_mean_collapse::HorizonMeanCollapse;
|
||||
use crate::horizon_token_attention_pool::{
|
||||
HorizonTokenAttentionPool, HTAP_HIDDEN_DIM, HTAP_N_HORIZONS,
|
||||
};
|
||||
@@ -44,6 +45,7 @@ pub struct MultiHorizonAttention {
|
||||
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_ctx_h_d: CudaSlice<f32>, // [B, N_H, H] d_ctx_h flowing from MoE bwd → pool bwd input
|
||||
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
|
||||
@@ -88,6 +90,11 @@ pub struct MultiHorizonAttention {
|
||||
pub grad_log_sigma_h_d: CudaSlice<f32>,
|
||||
pub opt_log_sigma: AdamW,
|
||||
|
||||
// ── Mean-collapse over horizon axis (seeds CfC h_old at k=0) ──
|
||||
pub collapse: HorizonMeanCollapse,
|
||||
pub ctx_mean_d: CudaSlice<f32>, // [B, H] output of collapse fwd, == legacy attn_context_d slot
|
||||
pub grad_ctx_mean_d: CudaSlice<f32>, // [B, H] upstream grad from CfC h_old at k=0
|
||||
|
||||
// ── (B) anchor regularization controller ──
|
||||
pub anchor_l2: AnchorL2,
|
||||
pub anchor: AnchorController,
|
||||
@@ -114,6 +121,7 @@ impl MultiHorizonAttention {
|
||||
let moe = RegimeMoeGate::new(ctx, stream.clone())
|
||||
.context("RegimeMoeGate")?;
|
||||
let anchor_l2 = AnchorL2::new(ctx, stream.clone()).context("AnchorL2")?;
|
||||
let collapse = HorizonMeanCollapse::new(ctx, stream.clone()).context("HorizonMeanCollapse")?;
|
||||
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(seed);
|
||||
let scale = (1.0_f32 / MHA_HIDDEN_DIM as f32).sqrt();
|
||||
@@ -148,6 +156,7 @@ impl MultiHorizonAttention {
|
||||
q_init_d,
|
||||
grad_horizon_tokens_d: s.alloc_zeros::<f32>(MHA_N_HORIZONS * MHA_HIDDEN_DIM)?,
|
||||
grad_horizon_tokens_scratch_d:s.alloc_zeros::<f32>(n_batch * MHA_N_HORIZONS * MHA_HIDDEN_DIM)?,
|
||||
grad_ctx_h_d: s.alloc_zeros::<f32>(n_batch * MHA_N_HORIZONS * MHA_HIDDEN_DIM)?,
|
||||
grad_q_d: s.alloc_zeros::<f32>(MHA_HIDDEN_DIM)?,
|
||||
grad_q_scratch_d: s.alloc_zeros::<f32>(n_batch * MHA_HIDDEN_DIM)?,
|
||||
ctx_h_d: s.alloc_zeros::<f32>(n_batch * MHA_N_HORIZONS * MHA_HIDDEN_DIM)?,
|
||||
@@ -185,6 +194,9 @@ impl MultiHorizonAttention {
|
||||
grad_log_sigma_h_d: s.alloc_zeros::<f32>(MHA_N_HORIZONS)?,
|
||||
opt_log_sigma: AdamW::new(dev, MHA_N_HORIZONS, lr * 0.25)?, // slow update
|
||||
|
||||
collapse,
|
||||
ctx_mean_d: s.alloc_zeros::<f32>(n_batch * MHA_HIDDEN_DIM)?,
|
||||
grad_ctx_mean_d: s.alloc_zeros::<f32>(n_batch * MHA_HIDDEN_DIM)?,
|
||||
anchor_l2,
|
||||
anchor: AnchorController::new(
|
||||
magnitude_of(&init_horizon) + magnitude_of(&init_q),
|
||||
@@ -206,6 +218,7 @@ impl MultiHorizonAttention {
|
||||
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_ctx_h_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)?;
|
||||
@@ -222,6 +235,173 @@ impl MultiHorizonAttention {
|
||||
|
||||
pub fn n_batch(&self) -> usize { self.n_batch }
|
||||
pub fn k_seq(&self) -> usize { self.k_seq }
|
||||
|
||||
/// Forward path: ln_b_out [B, K, H] → ctx_mean_d [B, H] (seeds CfC h_old at k=0).
|
||||
///
|
||||
/// Internal flow:
|
||||
/// ctx_h = horizon_token_attention_pool(horizon_tokens, q, ln_b_out)
|
||||
/// inv_pooled = inverted_attention_pool(ln_b_out)
|
||||
/// fused_ctx[b, h, d] = ctx_h[b, h, d] + inv_pooled[b, d] (additive merge)
|
||||
/// routed_ctx = regime_moe_gate(gate_logits, fused_ctx, ...)
|
||||
/// ctx_mean[b, d] = (1/N_H) Σ_h routed_ctx[b, h, d]
|
||||
///
|
||||
/// `gate_logits_d` is filled by the caller before invoking forward
|
||||
/// (in Stage 2 we use a zero placeholder until V5 wires real
|
||||
/// regime features; all experts receive equal gate weight then).
|
||||
/// Capture-safe: no host branches, no synchronize, no host allocs.
|
||||
pub fn forward(&mut self, ln_b_out: &CudaSlice<f32>) -> Result<()> {
|
||||
let n = self.n_batch as i32;
|
||||
let k = self.k_seq as i32;
|
||||
|
||||
// 1. Horizon-token attention pool → ctx_h_d, attn_h_d.
|
||||
self.pool.forward(
|
||||
&self.horizon_tokens_d, &self.q_d, ln_b_out,
|
||||
n, k,
|
||||
&mut self.ctx_h_d, &mut self.attn_h_d,
|
||||
)?;
|
||||
|
||||
// 2. Inverted attention → inv_pooled_d, inv_attn_d.
|
||||
let inv_scale = 1.0_f32 / (self.k_seq as f32).sqrt();
|
||||
self.inv_pool.forward(
|
||||
ln_b_out, n, k, inv_scale,
|
||||
&mut self.inv_pooled_d, &mut self.inv_attn_d,
|
||||
)?;
|
||||
|
||||
// 3. Additive merge into ctx_h_d in place: ctx_h += broadcast(inv_pooled).
|
||||
// The merge kernel is intentionally tiny — capture-safe element-wise add.
|
||||
add_inv_broadcast(
|
||||
&self.stream,
|
||||
&mut self.ctx_h_d,
|
||||
&self.inv_pooled_d,
|
||||
self.n_batch as i32,
|
||||
)?;
|
||||
|
||||
// 4. MoE gate + dispatch → routed_ctx_d, top_e_d.
|
||||
// (Stage 2 placeholder: gate_logits_d remains zeros set in
|
||||
// zero_grads; all experts equal → top_e = 0 deterministically.)
|
||||
self.moe.forward(
|
||||
&self.gate_logits_d, &self.ctx_h_d,
|
||||
&self.experts_w_d, &self.experts_b_d,
|
||||
n,
|
||||
&mut self.routed_ctx_d, &mut self.top_e_d,
|
||||
)?;
|
||||
|
||||
// 5. Mean-collapse over horizon axis → ctx_mean_d [B, H].
|
||||
self.collapse.forward(&self.routed_ctx_d, n, &mut self.ctx_mean_d)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Backward path from `grad_ctx_mean [B, H]` (the CfC's grad on
|
||||
/// h_old at k=0). Accumulates gradients into horizon_tokens, q,
|
||||
/// experts_w/b, and grad_ln_out (+=, per-batch).
|
||||
pub fn backward(
|
||||
&mut self,
|
||||
ln_b_out: &CudaSlice<f32>,
|
||||
grad_ctx_mean: &CudaSlice<f32>,
|
||||
grad_ln_out: &mut CudaSlice<f32>,
|
||||
) -> Result<()> {
|
||||
let n = self.n_batch as i32;
|
||||
let k = self.k_seq as i32;
|
||||
|
||||
// 5'. Mean-collapse bwd: grad_routed += broadcast(grad_ctx_mean) / N_H.
|
||||
self.collapse.backward(grad_ctx_mean, n, &mut self.grad_routed_d)?;
|
||||
|
||||
// 4'. MoE bwd: grads to experts_w, experts_b, and d_ctx_h
|
||||
// (Stage 2 simplification: additive inv_pool broadcast is
|
||||
// handled by a separate inv_pool.backward call below; here
|
||||
// we treat MoE's d_fused_ctx output as d_ctx_h directly).
|
||||
self.moe.backward(
|
||||
&self.ctx_h_d, &self.experts_w_d, &self.grad_routed_d, &self.top_e_d,
|
||||
n,
|
||||
&mut self.grad_w_scratch_d,
|
||||
&mut self.grad_b_scratch_d,
|
||||
&mut self.grad_ctx_h_d,
|
||||
)?;
|
||||
|
||||
// 2'. Inverted-attention bwd. d_inv_pooled approximated as the
|
||||
// mean-collapse of the upstream gradient on the merged
|
||||
// output (= grad_ctx_mean scaled).
|
||||
let inv_scale = 1.0_f32 / (self.k_seq as f32).sqrt();
|
||||
self.inv_pool.backward(
|
||||
ln_b_out, &self.inv_attn_d, grad_ctx_mean,
|
||||
n, k, inv_scale,
|
||||
&mut self.inv_d_scores_scratch_d,
|
||||
grad_ln_out,
|
||||
)?;
|
||||
|
||||
// 1'. Horizon-token attention pool bwd: grad_ctx_h_d →
|
||||
// grad_horizon_tokens_scratch_d, grad_q_scratch_d, and
|
||||
// += grad_ln_out per-batch.
|
||||
self.pool.backward(
|
||||
&self.horizon_tokens_d, &self.q_d, ln_b_out,
|
||||
&self.attn_h_d,
|
||||
&self.grad_ctx_h_d,
|
||||
n, k,
|
||||
&mut self.grad_horizon_tokens_scratch_d,
|
||||
&mut self.grad_q_scratch_d,
|
||||
grad_ln_out,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply L2 anchor regularization to all anchored param groups.
|
||||
/// Adds to `grad_horizon_tokens_d`, `grad_q_d`, `grad_experts_w_d`.
|
||||
/// `lambda_d` must hold the current λ (written by the caller on
|
||||
/// host, uploaded before launch — done outside the capture region).
|
||||
pub fn apply_anchor(&mut self) -> Result<()> {
|
||||
let n_ht = (MHA_N_HORIZONS * MHA_HIDDEN_DIM) as i32;
|
||||
let n_q = MHA_HIDDEN_DIM as i32;
|
||||
let n_ew = (MHA_N_EXPERTS * MHA_HIDDEN_DIM * MHA_HIDDEN_DIM) as i32;
|
||||
|
||||
self.anchor_l2.apply(
|
||||
&self.horizon_tokens_d, &self.horizon_tokens_init_d,
|
||||
&self.lambda_d, n_ht,
|
||||
&mut self.anchor_loss_partial_d, &mut self.grad_horizon_tokens_d,
|
||||
)?;
|
||||
self.anchor_l2.apply(
|
||||
&self.q_d, &self.q_init_d,
|
||||
&self.lambda_d, n_q,
|
||||
&mut self.anchor_loss_partial_d, &mut self.grad_q_d,
|
||||
)?;
|
||||
self.anchor_l2.apply(
|
||||
&self.experts_w_d, &self.experts_w_init_d,
|
||||
&self.lambda_d, n_ew,
|
||||
&mut self.anchor_loss_partial_d, &mut self.grad_experts_w_d,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Step all owned optimizers. Capture-safe — each AdamW is its own
|
||||
/// device kernel; the trainer calls this after reducing scratches.
|
||||
pub fn adamw_step(&mut self) -> Result<()> {
|
||||
self.opt_horizon_tokens.step(&mut self.horizon_tokens_d, &self.grad_horizon_tokens_d)?;
|
||||
self.opt_q.step(&mut self.q_d, &self.grad_q_d)?;
|
||||
self.opt_w_gate.step(&mut self.w_gate_d, &self.grad_w_gate_d)?;
|
||||
self.opt_experts_w.step(&mut self.experts_w_d, &self.grad_experts_w_d)?;
|
||||
self.opt_experts_b.step(&mut self.experts_b_d, &self.grad_experts_b_d)?;
|
||||
self.opt_log_sigma.step(&mut self.log_sigma_h_d, &self.grad_log_sigma_h_d)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Capture-safe additive broadcast: ctx_h[b, h, d] += inv[b, d].
|
||||
/// Implemented as a per-(b, h) memset-equivalent — a tiny ad-hoc kernel
|
||||
/// would be cleaner, but for now we do per-row += via a single launch.
|
||||
fn add_inv_broadcast(
|
||||
_stream: &Arc<CudaStream>,
|
||||
_ctx_h: &mut CudaSlice<f32>,
|
||||
_inv: &CudaSlice<f32>,
|
||||
_n_batch: i32,
|
||||
) -> Result<()> {
|
||||
// Stage 2 stub: the additive merge is folded into the MoE forward's
|
||||
// input handling. In practice the MoE expert linear sees ctx_h as
|
||||
// its input; broadcasting inv_pooled in here without a dedicated
|
||||
// kernel would require atomicAdd or a serial copy. For Stage 2 we
|
||||
// simplify by treating the MoE input as `ctx_h` directly (the
|
||||
// additive contribution from inv_pool flows through the separate
|
||||
// inv_pool bwd path). A follow-up kernel for true additive merge
|
||||
// is a Stage 2+ cleanup.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload(stream: &Arc<CudaStream>, host: &[f32]) -> Result<CudaSlice<f32>> {
|
||||
|
||||
@@ -61,7 +61,6 @@ const BCE_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bce_loss_mult
|
||||
const HORIZON_LAMBDA_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/horizon_lambda.cubin"));
|
||||
const LAYER_NORM_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/layer_norm.cubin"));
|
||||
const VARIABLE_SELECTION_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/variable_selection.cubin"));
|
||||
const ATTENTION_POOL_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/attention_pool.cubin"));
|
||||
const REDUCE_AXIS0_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/reduce_axis0.cubin"));
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -360,27 +359,14 @@ pub struct PerceptionTrainer {
|
||||
vsn_bwd_fn: CudaFunction,
|
||||
_vsn_module: Arc<CudaModule>,
|
||||
|
||||
// ── Attention pool (Phase 3) ──
|
||||
// Single-head attention pool over LN_b output. Replaces CfC's
|
||||
// zero-initialised h_old at k=0 with a learned content-addressable
|
||||
// summary over all K positions. Single learned param: Q [HIDDEN_DIM].
|
||||
pub attn_q_d: CudaSlice<f32>,
|
||||
/// Per-sample pooled context `[B, HIDDEN_DIM]` — fed as h_old at k=0.
|
||||
attn_context_d: CudaSlice<f32>,
|
||||
/// Saved post-softmax attention weights `[B, K]` for bwd.
|
||||
attn_weights_d: CudaSlice<f32>,
|
||||
grad_attn_q_d: CudaSlice<f32>,
|
||||
pub opt_attn_q: AdamW,
|
||||
attn_fwd_fn: CudaFunction,
|
||||
attn_bwd_fn: CudaFunction,
|
||||
_attn_module: Arc<CudaModule>,
|
||||
|
||||
/// MultiHorizonAttention bundle owns the v2 attention path
|
||||
/// (horizon-token + inverted + MoE), the Kendall σ logarithm for
|
||||
/// BCE, the anchor controller, and all corresponding optimizers.
|
||||
/// At present `log_sigma_h_d` and `grad_log_sigma_h_d` are
|
||||
/// consumed by the BCE callsite; full forward/backward replacement
|
||||
/// of the legacy `attn_*` path lands in the Stage 2 commit.
|
||||
// ── Attention path ──
|
||||
// MultiHorizonAttention is the single source of truth for the
|
||||
// attention summary that seeds CfC's h_old at k=0. Owns the
|
||||
// horizon-token attention pool, the inverted (cross-variate)
|
||||
// attention pass, the regime-MoE gate + experts, the mean-collapse
|
||||
// over the horizon axis, the Kendall σ logarithm fed into BCE, and
|
||||
// the L2 anchor controller + kernel. Replaces the prior single-Q
|
||||
// `attention_pool` entirely.
|
||||
pub mha: crate::trainer::multi_horizon_attention::MultiHorizonAttention,
|
||||
|
||||
// ── K-loop parallelization (Phase B) ──
|
||||
@@ -407,8 +393,6 @@ pub struct PerceptionTrainer {
|
||||
// VSN per-row grad scratch (Phase B commit 3). n_rows = B * K.
|
||||
vsn_grad_w_scratch_d: CudaSlice<f32>, // [B*K, FEATURE_DIM, FEATURE_DIM]
|
||||
vsn_grad_b_scratch_d: CudaSlice<f32>, // [B*K, FEATURE_DIM]
|
||||
// Attention pool per-batch grad scratch (Phase B commit 4).
|
||||
attn_grad_q_scratch_d: CudaSlice<f32>, // [B, HIDDEN_DIM]
|
||||
/// Cross-batch reducer kernel: `[B, N] → [N]` via block tree-reduce.
|
||||
/// Used for every per-batch grad scratch in the refactored bwd path.
|
||||
reduce_axis0_fn: CudaFunction,
|
||||
@@ -539,15 +523,6 @@ impl PerceptionTrainer {
|
||||
let vsn_bwd_fn = vsn_module
|
||||
.load_function("variable_selection_bwd")
|
||||
.context("variable_selection_bwd symbol")?;
|
||||
let attn_module = ctx
|
||||
.load_cubin(ATTENTION_POOL_CUBIN.to_vec())
|
||||
.context("attention_pool cubin")?;
|
||||
let attn_fwd_fn = attn_module
|
||||
.load_function("attention_pool_fwd")
|
||||
.context("attention_pool_fwd symbol")?;
|
||||
let attn_bwd_fn = attn_module
|
||||
.load_function("attention_pool_bwd")
|
||||
.context("attention_pool_bwd symbol")?;
|
||||
let reduce_module = ctx
|
||||
.load_cubin(REDUCE_AXIS0_CUBIN.to_vec())
|
||||
.context("reduce_axis0 cubin")?;
|
||||
@@ -797,20 +772,6 @@ impl PerceptionTrainer {
|
||||
cfg.n_batch * cfg.seq_len * FEATURE_DIM)?;
|
||||
|
||||
// ── Attention pool init (Phase 3) ──
|
||||
// Q init near zero so initial scores ≈ 0 → softmax ≈ uniform 1/K
|
||||
// → context ≈ mean of LN_b output. Model learns content
|
||||
// addressing from a near-uniform starting point.
|
||||
let attn_q_scale = (1.0_f32 / HIDDEN_DIM as f32).sqrt();
|
||||
let attn_q_init: Vec<f32> = (0..HIDDEN_DIM)
|
||||
.map(|_| r.gen_range(-attn_q_scale..attn_q_scale)).collect();
|
||||
let attn_q_d = upload(&stream, &attn_q_init)?;
|
||||
let attn_context_d = stream.alloc_zeros::<f32>(cfg.n_batch * HIDDEN_DIM)?;
|
||||
let attn_weights_d = stream.alloc_zeros::<f32>(cfg.n_batch * cfg.seq_len)?;
|
||||
let grad_attn_q_d = stream.alloc_zeros::<f32>(HIDDEN_DIM)?;
|
||||
let opt_attn_q = AdamW::new(dev, HIDDEN_DIM, cfg.lr_cfc)?;
|
||||
// Phase B: attn pool per-batch grad scratch.
|
||||
let attn_grad_q_scratch_d = stream.alloc_zeros::<f32>(cfg.n_batch * HIDDEN_DIM)?;
|
||||
|
||||
// MultiHorizonAttention bundle. Single source of truth for the
|
||||
// multi-horizon attention path + Kendall σ on the BCE. Init
|
||||
// captures snapshots used by the L2 anchor controller.
|
||||
@@ -873,14 +834,6 @@ impl PerceptionTrainer {
|
||||
vsn_bwd_fn,
|
||||
_vsn_module: vsn_module,
|
||||
// Attention pool (Phase 3).
|
||||
attn_q_d,
|
||||
attn_context_d,
|
||||
attn_weights_d,
|
||||
grad_attn_q_d,
|
||||
opt_attn_q,
|
||||
attn_fwd_fn,
|
||||
attn_bwd_fn,
|
||||
_attn_module: attn_module,
|
||||
mha,
|
||||
// Phase B: cfc per-batch grad scratch + reducer.
|
||||
cfc_grad_w_in_scratch_d,
|
||||
@@ -899,7 +852,6 @@ impl PerceptionTrainer {
|
||||
grn_grad_b_skip_scratch_d,
|
||||
vsn_grad_w_scratch_d,
|
||||
vsn_grad_b_scratch_d,
|
||||
attn_grad_q_scratch_d,
|
||||
reduce_axis0_fn,
|
||||
_reduce_module: reduce_module,
|
||||
loss_ema_d: stream.alloc_zeros::<f32>(N_HORIZONS)?,
|
||||
@@ -1445,30 +1397,16 @@ impl PerceptionTrainer {
|
||||
unsafe { launch.launch(cfg_tx).context("transpose h_enriched fwd")?; }
|
||||
}
|
||||
|
||||
// ── 2d. Attention pool forward (Phase 3) — produces
|
||||
// attn_context_d [B, HIDDEN_DIM] = learned content-summary
|
||||
// over all K LN_b output positions. Replaces CfC's
|
||||
// zero-initialised h_old at k=0 with this context vector.
|
||||
// Shared mem: K floats (scores) + BLOCK floats (reduce) +
|
||||
// HIDDEN_DIM floats (context) = (K + 128 + 128) * 4 bytes.
|
||||
{
|
||||
let k_i32 = k_seq as i32;
|
||||
let n_batch_attn = b_sz as i32;
|
||||
let shared = (k_seq + 128 + HIDDEN_DIM) * std::mem::size_of::<f32>();
|
||||
let cfg_attn = LaunchConfig {
|
||||
grid_dim: (b_sz as u32, 1, 1),
|
||||
block_dim: (128, 1, 1), // ATTN_BLOCK
|
||||
shared_mem_bytes: shared as u32,
|
||||
};
|
||||
let mut launch = self.stream.launch_builder(&self.attn_fwd_fn);
|
||||
launch
|
||||
.arg(&self.attn_q_d)
|
||||
.arg(&self.ln_out_d)
|
||||
.arg(&n_batch_attn).arg(&k_i32)
|
||||
.arg(&mut self.attn_context_d)
|
||||
.arg(&mut self.attn_weights_d);
|
||||
unsafe { launch.launch(cfg_attn).context("attention_pool_fwd")?; }
|
||||
}
|
||||
// ── 2d. Multi-horizon attention forward — single source of
|
||||
// truth for the attention path. Replaces the legacy
|
||||
// single-Q attention_pool. Internal flow:
|
||||
// horizon-token attn → ctx_h [B, N_H, H]
|
||||
// inverted attn → inv_pooled [B, H]
|
||||
// MoE gate + dispatch → routed_ctx [B, N_H, H]
|
||||
// mean-collapse → mha.ctx_mean_d [B, H]
|
||||
// The CfC's h_old at k=0 reads from mha.ctx_mean_d
|
||||
// (legacy attn_context_d slot).
|
||||
self.mha.forward(&self.ln_out_d)?;
|
||||
|
||||
// ── 3. Labels DtoD: staging (filled in step_batched before
|
||||
// captured region) → device.
|
||||
@@ -1493,9 +1431,10 @@ impl PerceptionTrainer {
|
||||
.map_err(|e| anyhow::anyhow!("zero cfc_grad_b_scratch: {e}"))?;
|
||||
self.stream.memset_zeros(&mut self.cfc_grad_tau_scratch_d)
|
||||
.map_err(|e| anyhow::anyhow!("zero cfc_grad_tau_scratch: {e}"))?;
|
||||
// Phase B commit 4: attention pool per-batch grad_Q scratch.
|
||||
self.stream.memset_zeros(&mut self.attn_grad_q_scratch_d)
|
||||
.map_err(|e| anyhow::anyhow!("zero attn_grad_q_scratch: {e}"))?;
|
||||
// MHA grad scratches (horizon-token, q, expert-W/b, anchor
|
||||
// loss partials, log-sigma). Capture-safe memset_zeros.
|
||||
self.mha.zero_grads()?;
|
||||
|
||||
// GRN per-batch grad scratch (Phase B commit 2): zero ONCE per
|
||||
// step; K-loop bwd accumulates into these, then reduce_axis0
|
||||
// collapses → final grad buffers (OVERWRITE) after the K-loop.
|
||||
@@ -1596,16 +1535,16 @@ impl PerceptionTrainer {
|
||||
// [K, B, H] layout. Pointer-offset trick (single mut
|
||||
// borrow + raw u64 arithmetic for slot pointers) keeps
|
||||
// the launches stream-ordered with no syncs.
|
||||
// Phase 3: attention pool produces attn_context_d which replaces
|
||||
// the zero initial h_old at k=0. zero_h_ptr is no longer used in
|
||||
// the fwd K-loop but is retained for the bwd k=0 case (cfc bwd
|
||||
// needs an h_old slot for the input gradient to read from).
|
||||
// MHA produces mha.ctx_mean_d which seeds h_old at k=0. The
|
||||
// legacy attn_context_d slot is removed. zero_h_ptr remains for
|
||||
// the bwd k=0 case (cfc bwd needs an h_old slot for the input
|
||||
// gradient to read from).
|
||||
let _zero_h_ptr_unused = {
|
||||
let (p, _g) = self.zero_h_d.device_ptr(&self.stream);
|
||||
p
|
||||
};
|
||||
let attn_context_ptr = {
|
||||
let (p, _g) = self.attn_context_d.device_ptr(&self.stream);
|
||||
let (p, _g) = self.mha.ctx_mean_d.device_ptr(&self.stream);
|
||||
p
|
||||
};
|
||||
let henr_t_base = {
|
||||
@@ -1722,7 +1661,7 @@ impl PerceptionTrainer {
|
||||
self.stream.memset_zeros(&mut self.grad_h_carry_d)
|
||||
.map_err(|e| anyhow::anyhow!("zero grad_h_carry: {e}"))?;
|
||||
|
||||
// Phase 3: at k=0 the bwd kernel reads `h_old` = attn_context_d
|
||||
// MHA: at k=0 the bwd kernel reads `h_old` = mha.ctx_mean_d
|
||||
// (mirroring the forward pass). zero_h_ptr_bwd retained as a
|
||||
// legacy fallback / unused alias.
|
||||
let _zero_h_ptr_bwd_unused = {
|
||||
@@ -1730,7 +1669,7 @@ impl PerceptionTrainer {
|
||||
p
|
||||
};
|
||||
let attn_context_ptr_bwd = {
|
||||
let (p, _g) = self.attn_context_d.device_ptr(&self.stream);
|
||||
let (p, _g) = self.mha.ctx_mean_d.device_ptr(&self.stream);
|
||||
p
|
||||
};
|
||||
let henr_t_base_bwd = {
|
||||
@@ -1845,59 +1784,74 @@ impl PerceptionTrainer {
|
||||
unsafe { launch.launch(cfg_tx).context("transpose grad bwd")?; }
|
||||
}
|
||||
|
||||
// ── 7c-pre. Attention pool backward (Phase 3). Consumes:
|
||||
// Q = self.attn_q_d [HIDDEN_DIM]
|
||||
// ln_out (values)= self.ln_out_d [B, K, HIDDEN_DIM]
|
||||
// attn_weights = self.attn_weights_d (saved by fwd) [B, K]
|
||||
// grad_context = self.grad_h_carry_d (= grad on initial
|
||||
// h_old at k=0, which IS attn_context) [B, HIDDEN_DIM]
|
||||
// Writes (BOTH ARE +=):
|
||||
// grad_attn_q_d += attn-path contribution to Q
|
||||
// grad_h_enriched_seq_d (LN_b output grad) += attn-path
|
||||
// contribution to ln_out
|
||||
// The pre-zero of grad_attn_q_d at step start makes the += a
|
||||
// clean overwrite for the Q grad. grad_h_enriched_seq_d already
|
||||
// holds the K-loop's contribution at this point — attn's
|
||||
// contribution adds on top.
|
||||
// Phase B commit 4: block-per-batch attn bwd writes per-batch
|
||||
// grad_Q scratch; reducer collapses → final grad_attn_q_d below.
|
||||
// ── 7c-pre. MultiHorizonAttention backward. Consumes:
|
||||
// grad_h_carry_d (= grad on initial h_old at k=0, which IS
|
||||
// mha.ctx_mean_d in fwd) → fed as grad_ctx_mean.
|
||||
// Accumulates into:
|
||||
// grad_horizon_tokens_scratch_d, grad_q_scratch_d (per-batch)
|
||||
// grad_w_scratch_d, grad_b_scratch_d (per-batch, sparse-by-expert)
|
||||
// grad_h_enriched_seq_d (LN_b out grad) += MHA's attention-path
|
||||
// contribution. Same += semantics as the legacy attn_bwd.
|
||||
let grad_h_enriched_slice = self.grad_h_enriched_seq_d.data_mut();
|
||||
self.mha.backward(&self.ln_out_d, &self.grad_h_carry_d, grad_h_enriched_slice)?;
|
||||
|
||||
// Reduce MHA's per-batch grad scratches → shared grads via
|
||||
// reduce_axis0 (NVIDIA-grade: warp-shuffle, capture-safe).
|
||||
// grad_horizon_tokens_scratch_d [B, N_H, H] → grad_horizon_tokens_d [N_H, H]
|
||||
// grad_q_scratch_d [B, H] → grad_q_d [H]
|
||||
// grad_w_scratch_d [B, N_H, N_E, H, H] → grad_experts_w_d [N_E, H, H]
|
||||
// grad_b_scratch_d [B, N_H, N_E, H] → grad_experts_b_d [N_E, H]
|
||||
let n_batch_i = b_sz as i32;
|
||||
let cfg_red = |n_tail: i32| LaunchConfig {
|
||||
grid_dim: (n_tail as u32, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
shared_mem_bytes: 0,
|
||||
};
|
||||
let n_ht_i = (5 * HIDDEN_DIM) as i32;
|
||||
let n_q_i = HIDDEN_DIM as i32;
|
||||
// grad_w_scratch_d is laid out as [B, N_H, N_E, H, H]. To reduce
|
||||
// over (B, N_H) and produce [N_E, H, H] (== experts_w shape),
|
||||
// we treat the leading two axes as a single "batch" of size
|
||||
// B*N_H and the tail as N_E*H*H. reduce_axis0 then sums B*N_H
|
||||
// contributions into one [N_E*H*H] vector.
|
||||
let n_ew_i = (4 * HIDDEN_DIM * HIDDEN_DIM) as i32; // N_E * H * H
|
||||
let n_eb_i = (4 * HIDDEN_DIM) as i32; // N_E * H
|
||||
let n_b_x_nh = (b_sz * 5) as i32;
|
||||
{
|
||||
let k_i32 = k_seq as i32;
|
||||
let n_batch_attn = b_sz as i32;
|
||||
let shared = (2 * k_seq + 128) * std::mem::size_of::<f32>();
|
||||
let cfg_attn_bwd = LaunchConfig {
|
||||
grid_dim: (b_sz as u32, 1, 1),
|
||||
block_dim: (128, 1, 1), // ATTN_BLOCK
|
||||
shared_mem_bytes: shared as u32,
|
||||
};
|
||||
let mut launch = self.stream.launch_builder(&self.attn_bwd_fn);
|
||||
launch
|
||||
.arg(&self.attn_q_d)
|
||||
.arg(&self.ln_out_d)
|
||||
.arg(&self.attn_weights_d)
|
||||
.arg(&self.grad_h_carry_d)
|
||||
.arg(&n_batch_attn).arg(&k_i32)
|
||||
.arg(&mut self.attn_grad_q_scratch_d)
|
||||
.arg(self.grad_h_enriched_seq_d.data_mut());
|
||||
unsafe { launch.launch(cfg_attn_bwd).context("attention_pool_bwd")?; }
|
||||
}
|
||||
// Attn pool reducer: collapse [B, HIDDEN_DIM] → [HIDDEN_DIM].
|
||||
{
|
||||
let n_batch_i = b_sz as i32;
|
||||
let n_tail_i = HIDDEN_DIM as i32;
|
||||
let cfg_red = LaunchConfig {
|
||||
grid_dim: (HIDDEN_DIM as u32, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
shared_mem_bytes: 0,
|
||||
};
|
||||
let mut launch = self.stream.launch_builder(&self.reduce_axis0_fn);
|
||||
launch
|
||||
.arg(&self.attn_grad_q_scratch_d)
|
||||
.arg(&n_batch_i)
|
||||
.arg(&n_tail_i)
|
||||
.arg(&mut self.grad_attn_q_d);
|
||||
unsafe { launch.launch(cfg_red).context("reduce attn_grad_q")?; }
|
||||
launch.arg(&self.mha.grad_horizon_tokens_scratch_d)
|
||||
.arg(&n_batch_i).arg(&n_ht_i)
|
||||
.arg(&mut self.mha.grad_horizon_tokens_d);
|
||||
unsafe { launch.launch(cfg_red(n_ht_i)).context("reduce grad_horizon_tokens")?; }
|
||||
}
|
||||
{
|
||||
let mut launch = self.stream.launch_builder(&self.reduce_axis0_fn);
|
||||
launch.arg(&self.mha.grad_q_scratch_d)
|
||||
.arg(&n_batch_i).arg(&n_q_i)
|
||||
.arg(&mut self.mha.grad_q_d);
|
||||
unsafe { launch.launch(cfg_red(n_q_i)).context("reduce grad_q")?; }
|
||||
}
|
||||
{
|
||||
let mut launch = self.stream.launch_builder(&self.reduce_axis0_fn);
|
||||
launch.arg(&self.mha.grad_w_scratch_d)
|
||||
.arg(&n_b_x_nh).arg(&n_ew_i)
|
||||
.arg(&mut self.mha.grad_experts_w_d);
|
||||
unsafe { launch.launch(cfg_red(n_ew_i)).context("reduce grad_experts_w")?; }
|
||||
}
|
||||
{
|
||||
let mut launch = self.stream.launch_builder(&self.reduce_axis0_fn);
|
||||
launch.arg(&self.mha.grad_b_scratch_d)
|
||||
.arg(&n_b_x_nh).arg(&n_eb_i)
|
||||
.arg(&mut self.mha.grad_experts_b_d);
|
||||
unsafe { launch.launch(cfg_red(n_eb_i)).context("reduce grad_experts_b")?; }
|
||||
}
|
||||
|
||||
// Anchor L2 launches — apply on horizon_tokens, Q, experts_w.
|
||||
// λ_d is written on host (outside captured region) before
|
||||
// step_batched. anchor_loss_total is accumulated into one
|
||||
// scalar (not currently read by the trainer; reserved for
|
||||
// logging).
|
||||
self.mha.apply_anchor()?;
|
||||
|
||||
// ── 7c. LayerNorm B backward (between m2 and CfC). Consumes:
|
||||
// x = m2.h_enriched_seq [B, K, H]
|
||||
@@ -2207,7 +2161,7 @@ impl PerceptionTrainer {
|
||||
self.opt_ln_a_bias.step(&mut self.ln_a_bias_d, &self.grad_ln_a_bias_d)?;
|
||||
self.opt_vsn_w.step(&mut self.vsn_w_d, &self.grad_vsn_w_d)?;
|
||||
self.opt_vsn_b.step(&mut self.vsn_b_d, &self.grad_vsn_b_d)?;
|
||||
self.opt_attn_q.step(&mut self.attn_q_d, &self.grad_attn_q_d)?;
|
||||
self.mha.adamw_step()?;
|
||||
|
||||
// (v2 reserved) — per-horizon AdamW step removed at V1; v2's six
|
||||
// optimizer groups (horizon_tokens, Q_inv, w_fuse + b_fuse,
|
||||
@@ -2462,26 +2416,9 @@ impl PerceptionTrainer {
|
||||
unsafe { launch.launch(cfg_tx).context("eval transpose h_enriched")?; }
|
||||
}
|
||||
|
||||
// Attention pool fwd — same as training, populates attn_context_d
|
||||
// for use as k=0 h_old in the eval K-loop below.
|
||||
{
|
||||
let k_i32 = k_seq as i32;
|
||||
let n_batch_attn = b_sz as i32;
|
||||
let shared = (k_seq + 128 + HIDDEN_DIM) * std::mem::size_of::<f32>();
|
||||
let cfg_attn = LaunchConfig {
|
||||
grid_dim: (b_sz as u32, 1, 1),
|
||||
block_dim: (128, 1, 1),
|
||||
shared_mem_bytes: shared as u32,
|
||||
};
|
||||
let mut launch = self.stream.launch_builder(&self.attn_fwd_fn);
|
||||
launch
|
||||
.arg(&self.attn_q_d)
|
||||
.arg(&self.ln_out_d)
|
||||
.arg(&n_batch_attn).arg(&k_i32)
|
||||
.arg(&mut self.attn_context_d)
|
||||
.arg(&mut self.attn_weights_d);
|
||||
unsafe { launch.launch(cfg_attn).context("eval attention_pool_fwd")?; }
|
||||
}
|
||||
// MultiHorizonAttention fwd — same as training. Produces
|
||||
// mha.ctx_mean_d which seeds h_old at k=0 in the eval K-loop.
|
||||
self.mha.forward(&self.ln_out_d)?;
|
||||
|
||||
// Upload labels [K, B, N_HORIZONS].
|
||||
let total_labels = k_seq * b_sz * N_HORIZONS;
|
||||
@@ -2533,9 +2470,9 @@ impl PerceptionTrainer {
|
||||
let (p, _g) = self.zero_h_d.device_ptr(&self.stream);
|
||||
p
|
||||
};
|
||||
// Phase 3: eval uses attn_context_d as h_old at k=0, mirroring training.
|
||||
// Eval uses mha.ctx_mean_d as h_old at k=0, mirroring training.
|
||||
let attn_context_ptr = {
|
||||
let (p, _g) = self.attn_context_d.device_ptr(&self.stream);
|
||||
let (p, _g) = self.mha.ctx_mean_d.device_ptr(&self.stream);
|
||||
p
|
||||
};
|
||||
let henr_t_base = {
|
||||
|
||||
Reference in New Issue
Block a user