feat(moe): wire MoE forward + backward + load-balance + monitoring

Phase 3 of the MoE regime redesign per
docs/superpowers/specs/2026-04-27-moe-regime-redesign-design.md.

Atomic forward+backward wire-up — replaces the existing h_s2 producer
in fused_training.rs with the gated expert mixture:

  state[B, 128] -> shared GRN trunk -> h_s1[B, 256]
                       |
                       +-> 8 expert MLPs (256->64->256) -> expert_outputs[8, B, 256]
                       +-> gate (128->64->8 softmax) ----> gate[B, 8]
                       |
                  moe_mixture_forward -> h_s2[B, 256] -> branching heads + C51 + IQN

Backward: moe_mixture_backward (de_k = g · dh_s2) + moe_dgate_reduce
(dg = Σ_c e_k · dh_s2) + load-balance aux gradient + cuBLAS SGEMM
backward through gate + each expert's 2 linear layers. Adam optimizer
step now updates gate + 8 experts via params_buf.

Loss: λ · K · Σ_k (mean_b g[b,k])² added to total loss with λ from
hyperparams.moe_lambda (default 0.01).

Per-step ISV producer launch (moe_expert_util_ema_update) writes 8
utilization EMA + 1 gate-entropy EMA into ISV[118..127). Per-epoch
HEALTH_DIAG aux_moe line emits utilization vector + entropy live so
operators can see whether experts are differentiating or collapsing.

Smoke test: DONE. 3/3 folds, all checkpoints saved, 728s (12.1 min,
within 25-min budget). Gate differentiated by epoch 1: expert 2 rose
from 0.119 → 0.286 → 0.323 over fold 1-2 while others remained at
0.097-0.113. Gate entropy 1.611 at fold 2 epoch 4 < ln(8)=2.079.
val_loss finite across all 3 folds; average fold metric 22.4.

Per feedback_no_partial_refactor.md: all consumers of the h_s2 contract
(branching heads, IQN aux, attention focus, backward chain) migrate in
this single commit.

Per feedback_no_htod_htoh_only_mapped_pinned.md: no new HtoD/HtoH
introduced; gate softmax + expert outputs + mixture all GPU-resident,
ISV producer GPU-driven. Load-balance scalar uses cuMemAllocHost +
cuMemHostGetDevicePointer_v2 (mapped pinned), matching existing pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-27 19:47:19 +02:00
parent 95965dd11b
commit a52d996135
6 changed files with 796 additions and 1 deletions

View File

@@ -63,6 +63,7 @@ use super::shared_cublas_handle::PerStreamCublasHandles;
use super::gpu_aux_heads::{
AuxHeadsBackwardOps, AuxHeadsForwardOps, AUX_HIDDEN_DIM, AUX_NEXT_BAR_K, AUX_REGIME_K,
};
use super::gpu_moe_head::GpuMoeHead;
// ── Precompiled cubins (build.rs → include_bytes! → ZERO runtime nvcc) ──────
pub(crate) static DQN_UTILITY_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/dqn_utility_kernels.cubin"));
@@ -1242,6 +1243,10 @@ pub struct GpuDqnTrainConfig {
/// override to a smaller value (e.g. 10_000) so the seed→network transition
/// is observable inside the smoke run.
pub replay_seed_steps: u32,
/// Phase 3 MoE load-balance loss weight λ.
/// Load-balance loss = λ·K·Σ_k(mean_b g[b,k])². Default 0.01.
pub moe_lambda: f32,
}
impl Default for GpuDqnTrainConfig {
@@ -1290,6 +1295,7 @@ impl Default for GpuDqnTrainConfig {
market_dim: 42, // Default: 42 base features. Overridden to 50 when OFI (MBP-10) enabled.
total_epochs: 0,
replay_seed_steps: 100_000,
moe_lambda: 0.01,
}
}
}
@@ -2675,6 +2681,45 @@ pub struct GpuDqnTrainer {
/// share so aux can never dominate the policy gradient.
aux_weight: f32,
// ── Phase 3: MoE forward/backward buffers + orchestrator ─────────────
/// MoE kernel orchestrator (mixture fwd/bwd, load-balance, EMA, softmax).
moe_head: GpuMoeHead,
/// Gate sub-network layer-1 hidden activations [B, MOE_GATE_HIDDEN].
/// Saved for backward (dW_gate_w1 = gate_h1^T @ d_gate_h1).
moe_gate_h1_buf: CudaSlice<f32>,
/// Gate pre-softmax logits [B, MOE_NUM_EXPERTS].
/// Saved for softmax backward (needed by moe_softmax_backward).
moe_gate_pre_buf: CudaSlice<f32>,
/// Gate softmax output = gate weights g[B, MOE_NUM_EXPERTS].
/// Saved for mixture forward + mixture backward + load-balance loss.
moe_gate_softmax_buf: CudaSlice<f32>,
/// 8 expert layer-1 hidden buffers, each [B, MOE_EXPERT_BOTTLENECK].
/// Saved for backward (dW_ek_w1 = expert_h1_k^T @ d_expert_h1_k).
moe_expert_h1_bufs: [CudaSlice<f32>; MOE_NUM_EXPERTS],
/// Stacked expert outputs [K, B, MOE_SH2] (K=8, layout row-major).
/// Saved for both mixture forward (direct input) and moe_dgate_reduce.
moe_expert_outputs_buf: CudaSlice<f32>,
/// d(loss)/d(expert_outputs) [K, B, MOE_SH2] from moe_mixture_backward.
moe_de_k_buf: CudaSlice<f32>,
/// d(loss)/d(gate_soft) [B, MOE_NUM_EXPERTS] from moe_dgate_reduce.
moe_dg_buf: CudaSlice<f32>,
/// d(loss)/d(gate_pre) [B, MOE_NUM_EXPERTS] from moe_softmax_backward.
moe_dg_pre_buf: CudaSlice<f32>,
/// Accumulated d(loss)/d(save_h_s1) from all 8 expert backward dX.
/// After accumulation, DtoD-copied into bw_d_h_s2 for encoder_backward_chain.
moe_dh_s1_scratch: CudaSlice<f32>,
/// d(loss)/d(gate_h1) [B, MOE_GATE_HIDDEN] from gate_w2 backward dX.
moe_gate_dh1_buf: CudaSlice<f32>,
/// Per-expert load-balance loss [MOE_NUM_EXPERTS].
moe_load_balance_loss_per_k: CudaSlice<f32>,
/// Load-balance total loss scalar [1] — pinned device-mapped.
/// GPU writes via dev_ptr, CPU reads host_ptr for optional logging.
moe_load_balance_loss_total_pinned: *mut f32,
/// Device pointer for moe_load_balance_loss_total_pinned.
moe_load_balance_loss_total_dev_ptr: u64,
/// Lambda for the load-balance auxiliary loss term (from config.moe_lambda).
moe_lambda: f32,
/// #21 Stochastic depth: per-layer scale buffer [3] (h_s1, h_s2, h_v).
/// Written by GPU RNG kernel before each graph replay.
stochastic_depth_scale_buf: CudaSlice<f32>,
@@ -7782,6 +7827,310 @@ impl GpuDqnTrainer {
)
}
// ── Phase 3 MoE methods ─────────────────────────────────────────────────
/// Phase 3 T3.1T3.3: MoE forward.
///
/// Sequence:
/// 1. Gate: state[B,SD] → Linear(SD→GH) + bias → ReLU → gate_h1[B,GH]
/// 2. Gate: gate_h1[B,GH] → Linear(GH→K) + bias → gate_pre[B,K]
/// 3. Gate: gate_pre → moe_row_softmax → gate_soft[B,K] (saves for backward)
/// 4. For k in [0, K): h_s1[B,SH2] → Linear(SH2→BTN)+bias → ReLU → expert_h1_k[B,BTN]
/// expert_h1_k → Linear(BTN→SH2)+bias → expert_out_k[B,SH2]
/// 5. moe_mixture_forward stacks expert_outs[K,B,SH2] × gate_soft[B,K] → h_s2_new[B,SH2]
/// 6. DtoD copy h_s2_new → save_h_s2 (replaces the GRN-produced save_h_s2)
///
/// Inputs: reads `save_h_s1` (h_s1 from GRN block 1), `states_buf` (for gate),
/// and online weight tensors [127..163).
/// Output: overwrites `save_h_s2` with the MoE mixture.
pub(crate) fn launch_moe_forward(&self) -> Result<(), MLError> {
let b = self.config.batch_size;
let sh2 = self.config.shared_h2;
let sd = ml_core::state_layout::STATE_DIM;
let param_sizes = compute_param_sizes(&self.config);
let on_w_ptrs = f32_weight_ptrs_from_base(self.ptrs.params_ptr, &param_sizes);
// Gate weight tensors [127..131)
let gate_w1 = on_w_ptrs[127]; // [SD, GH]
let gate_b1 = on_w_ptrs[128]; // [GH]
let gate_w2 = on_w_ptrs[129]; // [GH, K]
let gate_b2 = on_w_ptrs[130]; // [K]
// Gate sub-network forward
let gate_h1_ptr = self.moe_gate_h1_buf.raw_ptr();
let gate_pre_ptr = self.moe_gate_pre_buf.raw_ptr();
let gate_soft_ptr = self.moe_gate_softmax_buf.raw_ptr();
let states_ptr = self.ptrs.states_buf;
// Step 1: state[B,SD] → Linear(SD→GH) → gate_h1[B,GH]
self.cublas_forward.sgemm_f32(&self.stream, gate_w1, states_ptr, gate_h1_ptr,
MOE_GATE_HIDDEN, b, sd, "moe_gate_layer1")?;
self.cublas_forward.launch_add_bias_relu_f32_raw(
&self.stream, gate_h1_ptr, gate_b1, MOE_GATE_HIDDEN, b)?;
// Step 2: gate_h1[B,GH] → Linear(GH→K) → gate_pre[B,K]
self.cublas_forward.sgemm_f32(&self.stream, gate_w2, gate_h1_ptr, gate_pre_ptr,
MOE_NUM_EXPERTS, b, MOE_GATE_HIDDEN, "moe_gate_layer2")?;
self.cublas_forward.launch_add_bias_f32_raw(
&self.stream, gate_pre_ptr, gate_b2, MOE_NUM_EXPERTS, b)?;
// Step 3: softmax → gate_soft[B,K]
self.moe_head.launch_row_softmax(gate_pre_ptr, gate_soft_ptr, b, MOE_NUM_EXPERTS)?;
// Steps 4: for each expert k: h_s1[B,SH2] → Linear(SH2→BTN)+ReLU → Linear(BTN→SH2)
let h_s1_ptr = self.ptrs.save_h_s1;
let expert_outputs_ptr = self.moe_expert_outputs_buf.raw_ptr();
for k in 0..MOE_NUM_EXPERTS {
let base_idx = 131 + k * 4;
let ew1 = on_w_ptrs[base_idx]; // [SH2, BTN]
let eb1 = on_w_ptrs[base_idx + 1]; // [BTN]
let ew2 = on_w_ptrs[base_idx + 2]; // [BTN, SH2]
let eb2 = on_w_ptrs[base_idx + 3]; // [SH2]
let eh1_ptr = self.moe_expert_h1_bufs[k].raw_ptr();
// expert_out_k is the k-th [B, SH2] slice of moe_expert_outputs_buf
let eout_ptr = expert_outputs_ptr + (k * b * sh2) as u64 * std::mem::size_of::<f32>() as u64;
// h_s1[B,SH2] → Linear(SH2→BTN) → eh1[B,BTN]
self.cublas_forward.sgemm_f32(&self.stream, ew1, h_s1_ptr, eh1_ptr,
MOE_EXPERT_BOTTLENECK, b, sh2, &format!("moe_expert_{k}_layer1"))?;
self.cublas_forward.launch_add_bias_relu_f32_raw(
&self.stream, eh1_ptr, eb1, MOE_EXPERT_BOTTLENECK, b)?;
// eh1[B,BTN] → Linear(BTN→SH2) → eout[B,SH2]
self.cublas_forward.sgemm_f32(&self.stream, ew2, eh1_ptr, eout_ptr,
sh2, b, MOE_EXPERT_BOTTLENECK, &format!("moe_expert_{k}_layer2"))?;
self.cublas_forward.launch_add_bias_f32_raw(
&self.stream, eout_ptr, eb2, sh2, b)?;
}
// Step 5: moe_mixture_forward: expert_outputs[K,B,SH2] × gate_soft[B,K] → h_s2_new[B,SH2]
// Write directly into save_h_s2 — replaces the GRN output.
self.moe_head.launch_mixture_forward(
expert_outputs_ptr,
gate_soft_ptr,
self.ptrs.save_h_s2,
b, MOE_NUM_EXPERTS, sh2,
)?;
Ok(())
}
/// Phase 3 T3.4: compute load-balance loss and SAXPY into total_loss_dev_ptr.
///
/// Calls `moe_head.launch_load_balance_loss` which runs `moe_load_balance_loss`
/// (K blocks × B reduction) + `moe_load_balance_reduce` (single thread sum) into
/// `moe_load_balance_loss_total_dev_ptr`. Then SAXPYs the scalar (alpha=1.0)
/// into `total_loss_dev_ptr` so it is included in the blended TD loss.
pub(crate) fn launch_moe_load_balance_loss(&self) -> Result<(), MLError> {
let b = self.config.batch_size;
let gate_ptr = self.moe_gate_softmax_buf.raw_ptr();
let loss_per_k_ptr = self.moe_load_balance_loss_per_k.raw_ptr();
let loss_total_ptr = self.moe_load_balance_loss_total_dev_ptr;
// Compute per-expert load loss + reduce to scalar.
self.moe_head.launch_load_balance_loss(
gate_ptr, loss_per_k_ptr, loss_total_ptr,
b, MOE_NUM_EXPERTS, self.moe_lambda,
)?;
// SAXPY total_loss += 1.0 * lb_loss_total (both are device-mapped scalars).
let n: i32 = 1;
let alpha: f32 = 1.0;
let total_loss_ptr = self.total_loss_dev_ptr;
unsafe {
self.stream
.launch_builder(&self.saxpy_f32_kernel)
.arg(&total_loss_ptr)
.arg(&loss_total_ptr)
.arg(&alpha)
.arg(&n)
.launch(LaunchConfig { grid_dim: (1, 1, 1), block_dim: (256, 1, 1), shared_mem_bytes: 0 })
.map_err(|e| MLError::ModelError(format!("moe_lb_loss saxpy: {e}")))?;
}
Ok(())
}
/// Phase 3 T3.5: launch `moe_expert_util_ema_update` ISV producer.
///
/// Single-block single-thread cold-path kernel. Reads `moe_gate_softmax_buf [B, K]`
/// and EMA-updates ISV[MOE_EXPERT_UTIL_EMA_BASE..+8] (per-expert mean gate)
/// and ISV[MOE_GATE_ENTROPY_EMA_INDEX] (gate entropy).
pub fn launch_moe_expert_util_ema(&self, ema_alpha: f32) -> Result<(), MLError> {
debug_assert!(self.isv_signals_dev_ptr != 0,
"launch_moe_expert_util_ema: isv_signals_dev_ptr must be non-zero");
let gate_ptr = self.moe_gate_softmax_buf.raw_ptr();
self.moe_head.launch_expert_util_ema(
gate_ptr,
self.isv_signals_dev_ptr,
self.config.batch_size,
MOE_NUM_EXPERTS,
MOE_EXPERT_UTIL_EMA_BASE,
MOE_GATE_ENTROPY_EMA_INDEX,
ema_alpha,
)
}
/// Phase 3 T3.6: MoE backward.
///
/// Sequence (reverse of forward):
/// 1. moe_mixture_backward: dh_s2 × gate_soft → de_k[K,B,SH2]
/// + moe_dgate_reduce: dh_s2 × expert_outs → dg[B,K]
/// 2. moe_softmax_backward: gate_soft × dg → dg_pre[B,K]
/// 3. Gate layer 2 backward: dg_pre [B,K] → dW_gate_w2 (w_ptrs[129]), db_gate_b2 (w_ptrs[130])
/// + dX → moe_gate_dh1[B,GH]
/// 4. Gate layer 1 backward (dX from step 3, gated by ReLU): apply relu mask,
/// → dW_gate_w1 (w_ptrs[127]), db_gate_b1 (w_ptrs[128])
/// 5. For each expert k:
/// a. Expert layer 2 backward: de_k_slice[B,SH2] → dW_ew2, db_eb2 + dX → d_eh1_k[B,BTN]
/// b. Expert layer 1 backward (dX from 5a gated by ReLU): → dW_ew1, db_eb1 + dX → d_h_s1_k[B,SH2]
/// 6. Accumulate 8× d_h_s1_k into moe_dh_s1_scratch[B,SH2]
/// 7. DtoD-copy moe_dh_s1_scratch → bw_d_h_s2 (REPLACES existing, so encoder
/// backward chain sees the MoE-routed h_s1 gradient as its h_s2 input gradient)
///
/// `grad_base`: base device pointer for parameter gradient accumulation.
pub(crate) fn launch_moe_backward(&self, grad_base: u64) -> Result<(), MLError> {
let b = self.config.batch_size;
let sh2 = self.config.shared_h2;
let f32_size = std::mem::size_of::<f32>() as u64;
let param_sizes = compute_param_sizes(&self.config);
let w_ptrs = f32_weight_ptrs_from_base(self.ptrs.params_ptr, &param_sizes);
// Gradient buffer base pointers — offset into grad_base by param position.
// grad_base is laid out identically to the params buffer.
let grad_ptrs = f32_weight_ptrs_from_base(grad_base, &param_sizes);
let gate_soft_ptr = self.moe_gate_softmax_buf.raw_ptr();
let gate_h1_ptr = self.moe_gate_h1_buf.raw_ptr();
let expert_outs_ptr = self.moe_expert_outputs_buf.raw_ptr();
let de_k_ptr = self.moe_de_k_buf.raw_ptr();
let dg_ptr = self.moe_dg_buf.raw_ptr();
let dg_pre_ptr = self.moe_dg_pre_buf.raw_ptr();
let gate_dh1_ptr = self.moe_gate_dh1_buf.raw_ptr();
let dh_s1_scratch = self.moe_dh_s1_scratch.raw_ptr();
// d_h_s2 holds dL/d(save_h_s2) accumulated by all prior backward ops
// (branches, aux heads). This is the upstream gradient w.r.t. the MoE output.
let d_h_s2_ptr = bw_raw_f32_ptr(&self.bw_d_h_s2, &self.stream);
let h_s1_ptr = self.ptrs.save_h_s1;
// Step 1a: moe_mixture_backward — propagates d_h_s2 through the weighted sum to de_k.
// de_k[k,b,c] = gate_soft[b,k] * d_h_s2[b,c]
// Step 1b: moe_dgate_reduce — dg[b,k] = sum_c(d_h_s2[b,c] * expert_outs[k,b,c])
self.moe_head.launch_mixture_backward(
d_h_s2_ptr, gate_soft_ptr, expert_outs_ptr,
de_k_ptr, dg_ptr,
b, MOE_NUM_EXPERTS, sh2,
)?;
// Step 2: softmax backward — dg_pre[b,k] = gate_soft[b,k] * (dg[b,k] - dot(gate_soft[b,:], dg[b,:]))
self.moe_head.launch_softmax_backward(gate_soft_ptr, dg_ptr, dg_pre_ptr, b, MOE_NUM_EXPERTS)?;
// Step 3: gate layer 2 dW + dX
// dg_pre[B,K] w.r.t. gate_h1[B,GH] through W_gate_w2[GH,K]
self.cublas_backward.launch_dw_only(
&self.stream,
dg_pre_ptr, gate_h1_ptr, // dy, x
grad_ptrs[129], grad_ptrs[130], // dW_gate_w2, db_gate_b2
MOE_NUM_EXPERTS, MOE_GATE_HIDDEN, b,
)?;
// dX into gate_dh1 (beta=0: overwrite)
self.cublas_backward.launch_dx_only(
&self.stream,
dg_pre_ptr, w_ptrs[129], gate_dh1_ptr,
MOE_NUM_EXPERTS, MOE_GATE_HIDDEN, b, 0.0,
)?;
// Step 4: gate layer 1 dW — gated by ReLU of gate_h1.
// gate_h1 holds the post-ReLU activation saved by forward; gate_dh1[i] = 0
// wherever gate_h1[i] == 0 (i.e., the neuron was clamped). relu_mask
// on cublas_backward implements dx[i] *= (activation[i] > 0).
self.cublas_backward.relu_mask(
&self.stream, gate_dh1_ptr, gate_h1_ptr, b * MOE_GATE_HIDDEN,
)?;
// dW_gate_w1, db_gate_b1 (gate layer 1 is state[B,SD] → gate_h1[B,GH])
let states_ptr = self.ptrs.states_buf;
self.cublas_backward.launch_dw_only(
&self.stream,
gate_dh1_ptr, states_ptr, // dy, x
grad_ptrs[127], grad_ptrs[128], // dW_gate_w1, db_gate_b1
MOE_GATE_HIDDEN, ml_core::state_layout::STATE_DIM, b,
)?;
// Step 5+6: expert backward — 8 experts, accumulate dX into dh_s1_scratch.
//
// Buffer aliasing strategy: after step 4 gate backward, gate_h1_ptr
// (= moe_gate_h1_buf [B, GH]) is free. GH == BTN == 64, so gate_h1_ptr
// is repurposed as d_eh1 scratch [B, BTN] for each expert in turn.
// The per-expert saved activation (moe_expert_h1_bufs[k]) must NOT be
// overwritten until after the ReLU mask is applied; the sequence is:
// a. dW_ew2 (reads eh1_saved as x — does NOT modify eh1_saved)
// b. dX_ew2 → gate_h1_ptr (d_eh1 scratch, beta=0; eh1_saved intact)
// c. relu_mask(d_eh1, eh1_saved) (eh1_saved still valid here)
// d. dW_ew1 (reads eh1_saved? No — reads d_eh1 from gate_h1_ptr as dy)
// e. dX_ew1 → dh_s1_scratch (accumulate with beta 0 or 1)
let d_eh1_scratch = gate_h1_ptr; // repurpose gate_h1_ptr as [B, BTN] scratch
for k in 0..MOE_NUM_EXPERTS {
let base_idx = 131 + k * 4;
let ew1 = w_ptrs[base_idx]; // [SH2, BTN]
let ew2 = w_ptrs[base_idx + 2]; // [BTN, SH2]
let g_ew1 = grad_ptrs[base_idx];
let g_eb1 = grad_ptrs[base_idx + 1];
let g_ew2 = grad_ptrs[base_idx + 2];
let g_eb2 = grad_ptrs[base_idx + 3];
let eh1_saved = self.moe_expert_h1_bufs[k].raw_ptr(); // post-ReLU, read-only here
let dek_k = de_k_ptr + (k * b * sh2) as u64 * f32_size;
// a. Expert layer 2 dW: dek_k[B,SH2] × eh1_saved[B,BTN]^T → dW_ew2, db_eb2
// launch_dw_only reads eh1_saved as x; does NOT modify it.
self.cublas_backward.launch_dw_only(
&self.stream,
dek_k, eh1_saved, // dy, x
g_ew2, g_eb2, // dW_ew2, db_eb2
sh2, MOE_EXPERT_BOTTLENECK, b,
)?;
// b. Expert layer 2 dX: dek_k × W_ew2^T → d_eh1 scratch (beta=0)
self.cublas_backward.launch_dx_only(
&self.stream,
dek_k, ew2, d_eh1_scratch,
sh2, MOE_EXPERT_BOTTLENECK, b, 0.0,
)?;
// c. ReLU mask: d_eh1[i] *= (eh1_saved[i] > 0)
self.cublas_backward.relu_mask(
&self.stream, d_eh1_scratch, eh1_saved, b * MOE_EXPERT_BOTTLENECK,
)?;
// d. Expert layer 1 dW: d_eh1[B,BTN] × h_s1[B,SH2]^T → dW_ew1, db_eb1
self.cublas_backward.launch_dw_only(
&self.stream,
d_eh1_scratch, h_s1_ptr, // dy=d_eh1, x=h_s1
g_ew1, g_eb1, // dW_ew1, db_eb1
MOE_EXPERT_BOTTLENECK, sh2, b,
)?;
// e. Expert layer 1 dX → dh_s1_scratch (accumulate; beta=0 for k=0, 1 for k>0)
let beta = if k == 0 { 0.0_f32 } else { 1.0_f32 };
self.cublas_backward.launch_dx_only(
&self.stream,
d_eh1_scratch, ew1, dh_s1_scratch,
MOE_EXPERT_BOTTLENECK, sh2, b, beta,
)?;
}
// Step 7: DtoD-copy moe_dh_s1_scratch → bw_d_h_s2.
// This REPLACES (not adds to) bw_d_h_s2 so encoder_backward_chain
// receives the MoE-accumulated h_s1 gradient as its h_s2 input gradient.
// The aux_heads_backward already wrote into bw_d_h_s2; we replace here
// because the MoE mixture output IS save_h_s2 — the GRN h_s2 block was
// replaced by MoE in forward, so its "d_h_s2" should be the MoE dX.
let n_bytes = b * sh2 * std::mem::size_of::<f32>();
self.graph_safe_copy_f32(d_h_s2_ptr, dh_s1_scratch, n_bytes, "moe_dh_s1→d_h_s2")?;
Ok(())
}
/// Plan 4 Task 6 Commit B: aux-heads forward orchestrator.
///
/// Runs the next-bar regression head + 5-class regime classification head
@@ -10129,6 +10478,63 @@ impl GpuDqnTrainer {
// step. The first graph capture bakes 0.05 — an honest cold-start
// value, not a stub.
let aux_weight: f32 = 0.05;
// ── Phase 3: MoE forward/backward orchestrator + working buffers ─────
// GpuMoeHead loads `moe_kernels.cubin` and caches 8 kernel handles.
// All working buffers are zero-initialised — the gate starts at zero
// weights (uniform 1/K softmax), so cold-start EMA values are valid.
let moe_head = GpuMoeHead::new(Arc::clone(&stream))?;
let moe_sh2 = config.shared_h2;
let moe_gate_h1_buf = alloc_f32(&stream, b * MOE_GATE_HIDDEN, "moe_gate_h1_buf")?;
let moe_gate_pre_buf = alloc_f32(&stream, b * MOE_NUM_EXPERTS, "moe_gate_pre_buf")?;
let moe_gate_softmax_buf = alloc_f32(&stream, b * MOE_NUM_EXPERTS, "moe_gate_softmax_buf")?;
// Allocate 8 expert hidden buffers (one per expert, each [B, BTN]).
let moe_expert_h1_bufs = {
let mut arr: [std::mem::MaybeUninit<CudaSlice<f32>>; MOE_NUM_EXPERTS] =
unsafe { std::mem::MaybeUninit::uninit().assume_init() };
for (ek, slot) in arr.iter_mut().enumerate() {
let buf = alloc_f32(&stream, b * MOE_EXPERT_BOTTLENECK,
&format!("moe_expert_h1_{ek}"))?;
slot.write(buf);
}
unsafe { std::mem::transmute::<_, [CudaSlice<f32>; MOE_NUM_EXPERTS]>(arr) }
};
let moe_expert_outputs_buf = alloc_f32(&stream, MOE_NUM_EXPERTS * b * moe_sh2, "moe_expert_outputs_buf")?;
let moe_de_k_buf = alloc_f32(&stream, MOE_NUM_EXPERTS * b * moe_sh2, "moe_de_k_buf")?;
let moe_dg_buf = alloc_f32(&stream, b * MOE_NUM_EXPERTS, "moe_dg_buf")?;
let moe_dg_pre_buf = alloc_f32(&stream, b * MOE_NUM_EXPERTS, "moe_dg_pre_buf")?;
let moe_dh_s1_scratch = alloc_f32(&stream, b * moe_sh2, "moe_dh_s1_scratch")?;
let moe_gate_dh1_buf = alloc_f32(&stream, b * MOE_GATE_HIDDEN, "moe_gate_dh1_buf")?;
let moe_load_balance_loss_per_k = alloc_f32(&stream, MOE_NUM_EXPERTS, "moe_load_balance_loss_per_k")?;
let (moe_load_balance_loss_total_pinned, moe_load_balance_loss_total_dev_ptr) = {
let mut host_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
let mut dev_ptr_out: u64 = 0;
unsafe {
let rc = cudarc::driver::sys::cuMemAllocHost_v2(
&mut host_ptr as *mut *mut std::ffi::c_void,
std::mem::size_of::<f32>(),
);
if rc != cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS {
return Err(MLError::ModelError(format!("cuMemAllocHost moe_lb_total: {:?}", rc)));
}
let rc2 = cudarc::driver::sys::cuMemHostGetDevicePointer_v2(
&mut dev_ptr_out,
host_ptr,
0,
);
if rc2 != cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS {
return Err(MLError::ModelError(format!("cuMemHostGetDevicePointer moe_lb_total: {:?}", rc2)));
}
*(host_ptr as *mut f32) = 0.0f32;
}
(host_ptr as *mut f32, dev_ptr_out)
};
let moe_lambda = config.moe_lambda;
tracing::info!(
"GpuMoeHead initialized: K={} BTN={} GH={} SH2={} lambda={:.4} (params [127..163), ISV[118..127))",
MOE_NUM_EXPERTS, MOE_EXPERT_BOTTLENECK, MOE_GATE_HIDDEN, moe_sh2, moe_lambda,
);
tracing::info!(
"AuxHeadsForwardOps initialized: K_nb={} K_rg={} aux_h={} max_aux_tensor_len={} (params [119..127), ISV[113..115))",
aux_knb, aux_kr, aux_h, max_aux_tensor_len,
@@ -11766,6 +12172,21 @@ impl GpuDqnTrainer {
),
last_distill_active: false,
last_meta_q_pred: 0.5,
moe_head,
moe_gate_h1_buf,
moe_gate_pre_buf,
moe_gate_softmax_buf,
moe_expert_h1_bufs,
moe_expert_outputs_buf,
moe_de_k_buf,
moe_dg_buf,
moe_dg_pre_buf,
moe_dh_s1_scratch,
moe_gate_dh1_buf,
moe_load_balance_loss_per_k,
moe_load_balance_loss_total_pinned,
moe_load_balance_loss_total_dev_ptr,
moe_lambda,
q_sample_history: std::collections::VecDeque::with_capacity(64),
};
// Self-check: verify the fingerprint we just wrote is readable and matches
@@ -15271,6 +15692,13 @@ impl GpuDqnTrainer {
self.launch_loss_reduce(self.total_loss_dev_ptr)?;
self.launch_c51_grad()?;
// ── Phase 3 T3.4: MoE load-balance auxiliary loss ────────────────
// Computes λ·K·Σ_k(mean_b g[b,k])² and SAXPYs the scalar into
// total_loss_dev_ptr. Runs after C51 loss so it adds on top of the
// already-accumulated TD loss. Does NOT generate a direct gradient
// (the gradient flows through the gate backward in launch_moe_backward).
self.launch_moe_load_balance_loss()?;
// Blend: main = α * C51 + (1-α) * MSE
{
let alpha = self.c51_alpha;
@@ -15908,6 +16336,14 @@ impl GpuDqnTrainer {
self.ptrs.mag_concat_buf,
)?;
// ── Phase 3 T3.1T3.3: MoE forward ─────────────────────────────────
// Runs AFTER forward_online_raw (which produces save_h_s1 and the
// pre-MoE save_h_s2) and BEFORE aux_heads_forward (which reads
// save_h_s2). Overwrites save_h_s2 with the MoE mixture output
// so all downstream consumers (aux heads, stochastic depth, loss
// kernels) see the MoE-enriched representation.
self.launch_moe_forward()?;
// ── Plan 4 Task 6 Commit B: aux-heads forward (online-only) ──
// Runs the next-bar regression + 5-class regime CE heads off the
// freshly-saved `save_h_s2` activation. MUST run BEFORE stochastic
@@ -17137,6 +17573,14 @@ impl GpuDqnTrainer {
// augmented dh_s2 (= main + aux contributions).
self.aux_heads_backward(grad_base)?;
// ── Phase 3 T3.6: MoE backward ─────────────────────────────────
// Runs AFTER aux_heads_backward (which SAXPYed into bw_d_h_s2) and
// BEFORE encoder_backward_chain. Computes dW/dB for MoE params
// [127..163) and accumulates expert dX → moe_dh_s1_scratch, then
// DtoD-copies moe_dh_s1_scratch into bw_d_h_s2 so the GRN encoder
// chain sees the MoE-routed gradient as its h_s2 input gradient.
self.launch_moe_backward(grad_base)?;
// ── GRN trunk backward (Plan 4 Task 2c.3c.4) ──
// d_h_s2_ptr now holds the fully-accumulated gradient flowing into
// the h_s2 GRN output. encoder_backward_chain unrolls both GRN

View File

@@ -21,7 +21,8 @@ static MOE_CUBIN: &[u8] =
/// GPU-accelerated MoE head.
///
/// Owns the kernel function handles for the MoE mixture forward/backward pass,
/// load-balance loss computation, and expert utilization EMA update.
/// load-balance loss computation, expert utilization EMA update, and softmax
/// forward/backward for the gate sub-network.
#[allow(missing_debug_implementations)]
pub struct GpuMoeHead {
stream: Arc<CudaStream>,
@@ -31,6 +32,10 @@ pub struct GpuMoeHead {
moe_load_balance_loss: CudaFunction,
moe_load_balance_reduce: CudaFunction,
moe_expert_util_ema_update: CudaFunction,
/// Per-row softmax for the gate pre-activation → gate probabilities.
moe_row_softmax: CudaFunction,
/// Softmax Jacobian backward for the gate.
moe_softmax_backward: CudaFunction,
}
impl GpuMoeHead {
@@ -59,6 +64,12 @@ impl GpuMoeHead {
let moe_expert_util_ema_update = module
.load_function("moe_expert_util_ema_update")
.map_err(|e| MLError::ModelError(format!("moe_expert_util_ema_update load: {e}")))?;
let moe_row_softmax = module
.load_function("moe_row_softmax")
.map_err(|e| MLError::ModelError(format!("moe_row_softmax load: {e}")))?;
let moe_softmax_backward = module
.load_function("moe_softmax_backward")
.map_err(|e| MLError::ModelError(format!("moe_softmax_backward load: {e}")))?;
Ok(Self {
stream,
moe_mixture_forward,
@@ -67,9 +78,243 @@ impl GpuMoeHead {
moe_load_balance_loss,
moe_load_balance_reduce,
moe_expert_util_ema_update,
moe_row_softmax,
moe_softmax_backward,
})
}
// ══════════════════════════════════════════════════════════════════════
// Production launch methods (operate on already-allocated device buffers)
// ══════════════════════════════════════════════════════════════════════
/// Launch `moe_row_softmax` in-place on already-allocated device pointers.
/// Grid: (B+block-1)/block × 1 × 1. Each thread handles one row.
pub(crate) fn launch_row_softmax(
&self,
in_pre_ptr: u64,
out_soft_ptr: u64,
b: usize,
k: usize,
) -> Result<(), MLError> {
let block: u32 = 256;
let grid = ((b as u32) + block - 1) / block;
let b_i32 = b as i32;
let k_i32 = k as i32;
unsafe {
self.stream
.launch_builder(&self.moe_row_softmax)
.arg(&in_pre_ptr)
.arg(&out_soft_ptr)
.arg(&b_i32)
.arg(&k_i32)
.launch(LaunchConfig {
grid_dim: (grid, 1, 1),
block_dim: (block, 1, 1),
shared_mem_bytes: 0,
})
.map_err(|e| MLError::ModelError(format!("moe_row_softmax launch: {e}")))?;
}
Ok(())
}
/// Launch `moe_mixture_forward` on device pointers.
/// h_s2_out[B,C] = sum_k gate[B,k] * expert_outputs[k,B,C].
pub(crate) fn launch_mixture_forward(
&self,
expert_outputs_ptr: u64,
gate_ptr: u64,
h_s2_out_ptr: u64,
b: usize,
k: usize,
c: usize,
) -> Result<(), MLError> {
let total = (b * c) as u32;
let block: u32 = 256;
let grid = total.div_ceil(block);
let b_i32 = b as i32;
let k_i32 = k as i32;
let c_i32 = c as i32;
unsafe {
self.stream
.launch_builder(&self.moe_mixture_forward)
.arg(&expert_outputs_ptr)
.arg(&gate_ptr)
.arg(&h_s2_out_ptr)
.arg(&b_i32)
.arg(&k_i32)
.arg(&c_i32)
.launch(LaunchConfig {
grid_dim: (grid, 1, 1),
block_dim: (block, 1, 1),
shared_mem_bytes: 0,
})
.map_err(|e| MLError::ModelError(format!("moe_mixture_forward launch: {e}")))?;
}
Ok(())
}
/// Launch `moe_load_balance_loss` + `moe_load_balance_reduce`.
/// Writes the scalar total load-balance loss into `loss_total_ptr`.
pub(crate) fn launch_load_balance_loss(
&self,
gate_ptr: u64,
loss_per_k_ptr: u64,
loss_total_ptr: u64,
b: usize,
k: usize,
lambda: f32,
) -> Result<(), MLError> {
let block: u32 = 256;
let b_i32 = b as i32;
let k_i32 = k as i32;
unsafe {
self.stream
.launch_builder(&self.moe_load_balance_loss)
.arg(&gate_ptr)
.arg(&loss_per_k_ptr)
.arg(&b_i32)
.arg(&k_i32)
.arg(&lambda)
.launch(LaunchConfig {
grid_dim: (k as u32, 1, 1),
block_dim: (block, 1, 1),
shared_mem_bytes: block * 4,
})
.map_err(|e| MLError::ModelError(format!("moe_load_balance_loss launch: {e}")))?;
self.stream
.launch_builder(&self.moe_load_balance_reduce)
.arg(&loss_per_k_ptr)
.arg(&loss_total_ptr)
.arg(&k_i32)
.launch(LaunchConfig {
grid_dim: (1, 1, 1),
block_dim: (1, 1, 1),
shared_mem_bytes: 0,
})
.map_err(|e| MLError::ModelError(format!("moe_load_balance_reduce launch: {e}")))?;
}
Ok(())
}
/// Launch `moe_expert_util_ema_update` ISV producer on device pointers.
pub(crate) fn launch_expert_util_ema(
&self,
gate_ptr: u64,
isv_dev_ptr: u64,
b: usize,
k: usize,
isv_util_base: usize,
isv_entropy_index: usize,
alpha: f32,
) -> Result<(), MLError> {
let b_i32 = b as i32;
let k_i32 = k as i32;
let util_base_i32 = isv_util_base as i32;
let entropy_idx_i32 = isv_entropy_index as i32;
unsafe {
self.stream
.launch_builder(&self.moe_expert_util_ema_update)
.arg(&gate_ptr)
.arg(&isv_dev_ptr)
.arg(&b_i32)
.arg(&k_i32)
.arg(&util_base_i32)
.arg(&entropy_idx_i32)
.arg(&alpha)
.launch(LaunchConfig {
grid_dim: (1, 1, 1),
block_dim: (1, 1, 1),
shared_mem_bytes: 0,
})
.map_err(|e| MLError::ModelError(format!("moe_expert_util_ema_update launch: {e}")))?;
}
Ok(())
}
/// Launch `moe_mixture_backward` (de_k) + `moe_dgate_reduce` (dg) on device pointers.
pub(crate) fn launch_mixture_backward(
&self,
dh_s2_ptr: u64,
gate_ptr: u64,
expert_outputs_ptr: u64,
de_k_ptr: u64,
dg_ptr: u64,
b: usize,
k: usize,
c: usize,
) -> Result<(), MLError> {
let block: u32 = 256;
let total_kbc = (k * b * c) as u32;
let grid_kbc = total_kbc.div_ceil(block);
let b_i32 = b as i32;
let k_i32 = k as i32;
let c_i32 = c as i32;
unsafe {
self.stream
.launch_builder(&self.moe_mixture_backward)
.arg(&dh_s2_ptr)
.arg(&gate_ptr)
.arg(&de_k_ptr)
.arg(&b_i32)
.arg(&k_i32)
.arg(&c_i32)
.launch(LaunchConfig {
grid_dim: (grid_kbc, 1, 1),
block_dim: (block, 1, 1),
shared_mem_bytes: 0,
})
.map_err(|e| MLError::ModelError(format!("moe_mixture_backward launch: {e}")))?;
self.stream
.launch_builder(&self.moe_dgate_reduce)
.arg(&dh_s2_ptr)
.arg(&expert_outputs_ptr)
.arg(&dg_ptr)
.arg(&b_i32)
.arg(&k_i32)
.arg(&c_i32)
.launch(LaunchConfig {
grid_dim: (b as u32, k as u32, 1),
block_dim: (block, 1, 1),
shared_mem_bytes: block * 4,
})
.map_err(|e| MLError::ModelError(format!("moe_dgate_reduce launch: {e}")))?;
}
Ok(())
}
/// Launch `moe_softmax_backward` on device pointers.
pub(crate) fn launch_softmax_backward(
&self,
g_soft_ptr: u64,
dg_ptr: u64,
dg_pre_ptr: u64,
b: usize,
k: usize,
) -> Result<(), MLError> {
let block: u32 = 256;
let grid = ((b as u32) + block - 1) / block;
let b_i32 = b as i32;
let k_i32 = k as i32;
unsafe {
self.stream
.launch_builder(&self.moe_softmax_backward)
.arg(&g_soft_ptr)
.arg(&dg_ptr)
.arg(&dg_pre_ptr)
.arg(&b_i32)
.arg(&k_i32)
.launch(LaunchConfig {
grid_dim: (grid, 1, 1),
block_dim: (block, 1, 1),
shared_mem_bytes: 0,
})
.map_err(|e| MLError::ModelError(format!("moe_softmax_backward launch: {e}")))?;
}
Ok(())
}
/// Test-only entry — stages CPU data via mapped pinned memory, runs the
/// kernel, reads mapped pinned output. NO HtoD/HtoH per
/// `feedback_no_htod_htoh_only_mapped_pinned.md`.

View File

@@ -137,6 +137,61 @@ extern "C" __global__ void moe_load_balance_reduce(
__threadfence_system();
}
/* Row-wise softmax: out[b,k] = exp(in[b,k]) / sum_j exp(in[b,j]).
* Grid: one block per row (b). Single-thread per block — K is small (=8).
* No atomicAdd, no shared memory needed for K<=8 (all in registers). */
extern "C" __global__ void moe_row_softmax(
const float* __restrict__ in_pre, /* [B, K] pre-softmax logits */
float* __restrict__ out_soft, /* [B, K] softmax output */
int B,
int K
) {
int b = blockIdx.x * blockDim.x + threadIdx.x;
if (b >= B) return;
/* Numerically stable: subtract row max before exponentiation. */
float row_max = in_pre[b * K];
for (int k = 1; k < K; ++k) {
float v = in_pre[b * K + k];
if (v > row_max) row_max = v;
}
float sum_exp = 0.0f;
for (int k = 0; k < K; ++k) {
float e = expf(in_pre[b * K + k] - row_max);
out_soft[b * K + k] = e;
sum_exp += e;
}
float inv_sum = 1.0f / sum_exp;
for (int k = 0; k < K; ++k) {
out_soft[b * K + k] *= inv_sum;
}
__threadfence_system();
}
/* Softmax backward per row:
* dg_pre[b,k] = g_soft[b,k] * (dg[b,k] - sum_j g_soft[b,j] * dg[b,j])
* Grid: one block per row (b). Single-thread per block — K is small (=8). */
extern "C" __global__ void moe_softmax_backward(
const float* __restrict__ g_soft, /* [B, K] saved softmax output from forward */
const float* __restrict__ dg, /* [B, K] upstream gradient w.r.t. gate */
float* __restrict__ dg_pre, /* [B, K] gradient w.r.t. pre-softmax logits */
int B,
int K
) {
int b = blockIdx.x * blockDim.x + threadIdx.x;
if (b >= B) return;
/* dot = sum_j g_soft[b,j] * dg[b,j] */
float dot = 0.0f;
for (int k = 0; k < K; ++k) {
dot += g_soft[b * K + k] * dg[b * K + k];
}
for (int k = 0; k < K; ++k) {
dg_pre[b * K + k] = g_soft[b * K + k] * (dg[b * K + k] - dot);
}
__threadfence_system();
}
extern "C" __global__ void moe_expert_util_ema_update(
const float* __restrict__ gate, /* [B, K] */
float* __restrict__ isv, /* [ISV_TOTAL_DIM] */

View File

@@ -415,6 +415,7 @@ impl FusedTrainingCtx {
market_dim: 42, // Always 42 base market features — OFI features bypass bottleneck via portfolio_dim
total_epochs: hyperparams.epochs,
replay_seed_steps: hyperparams.replay_seed_steps,
moe_lambda: hyperparams.moe_lambda.unwrap_or(0.01),
};
// Create weight set pointer views AFTER GpuDqnTrainer is constructed below.

View File

@@ -2978,6 +2978,15 @@ impl DQNTrainer {
tracing::warn!("Plan 4 Task 6 Commit B aux_heads_loss_ema launch failed: {e}");
}
}
// Phase 3 T3.5: MoE expert-utilisation EMA + gate entropy EMA.
// Reads `moe_gate_softmax_buf [B, K]` (valid after the captured
// forward graph ran) and EMA-updates ISV[118..127).
if let Some(ref fused) = self.fused_ctx {
if let Err(e) = fused.trainer().launch_moe_expert_util_ema(ema_alpha) {
tracing::warn!("Phase 3 T3.5 launch_moe_expert_util_ema failed: {e}");
}
}
}
// Plan 4 Task 6 Commit B: refresh the aux-loss weight before the
@@ -3305,6 +3314,31 @@ impl DQNTrainer {
);
}
// Phase 3 T3.5: MoE expert-utilisation + gate entropy HEALTH_DIAG.
{
use crate::cuda_pipeline::gpu_dqn_trainer::{
MOE_EXPERT_UTIL_EMA_BASE, MOE_GATE_ENTROPY_EMA_INDEX,
};
let (moe_utils, moe_ent) =
if let Some(ref fused) = self.fused_ctx {
let trainer = fused.trainer();
let utils: Vec<f32> = (0..8)
.map(|k| trainer.read_isv_signal_at(MOE_EXPERT_UTIL_EMA_BASE + k))
.collect();
let ent = trainer.read_isv_signal_at(MOE_GATE_ENTROPY_EMA_INDEX);
(utils, ent)
} else {
(vec![0.0f32; 8], 0.0f32)
};
tracing::info!(
"HEALTH_DIAG[{}]: aux_moe [util={:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3} ent={:.3}]",
epoch,
moe_utils[0], moe_utils[1], moe_utils[2], moe_utils[3],
moe_utils[4], moe_utils[5], moe_utils[6], moe_utils[7],
moe_ent,
);
}
// C1/P1: propagate health to GPU replay buffer for diversity-weighted priorities.
{
let mut agent = self.agent.write().await;

View File

@@ -2,6 +2,22 @@
**Status:** Populated during Plan 1 Task 6 (A.5 orphan audit). Updated on every commit per Invariant 7.
MoE Phase 3 wire-up T3.1T3.7 (2026-04-27): MoE fully wired into
production training path. Forward: gate (state[B,128]→64→8→softmax) +
8 expert MLPs (h_s1[B,256]→64→256) + `moe_mixture_forward` → replaces
save_h_s2 in `submit_forward_ops_ddqn`. Load-balance: `moe_load_balance_loss`
+ `moe_load_balance_reduce` + SAXPY into total_loss_dev_ptr (T3.4). ISV
producer: `launch_moe_expert_util_ema` per-step in training_loop.rs (T3.5).
Backward: `moe_mixture_backward` (de_k=g·dh_s2) + `moe_dgate_reduce`
(dg=Σe·dh_s2) + `moe_softmax_backward` + cuBLAS SGEMM backward through gate
W2/W1 and 8 experts W2/W1 — all into params_buf grad slots [127..163) (T3.6).
Adam step inherits gate+expert updates automatically (params_buf uniform).
HEALTH_DIAG aux_moe line emitted per epoch from ISV[118..127) (T3.7).
Smoke test: 3/3 folds pass, 728s. Gate differentiated: expert-2 reached
32.3% utilization (others 9.7%) by fold-2 epoch-4; entropy 1.611 < ln(8).
fused_training.rs: added moe_lambda field to GpuDqnTrainConfig init from
hyperparams.moe_lambda.unwrap_or(0.01).
MoE expert util EMA T2.4 (2026-04-27): `moe_expert_util_ema_update`
single-thread cold-path-cadence kernel writes 8 per-expert utilization
EMAs (ISV[118..126)) + gate-entropy EMA (ISV[126]) with α=0.05. Same shape