feat: wire homeostatic_regularizer CUDA kernel into GpuDqnTrainer

Adds G16 homeostatic regularization that penalizes training observables
drifting from calibrated set-points. All 6 scalar signals use pinned
device-mapped memory (zero memcpy). Targets self-calibrate via EMA
during epochs 1-5, then freeze.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-16 01:08:24 +02:00
parent ae4b6c22a3
commit f103ae8bb5
3 changed files with 209 additions and 0 deletions

View File

@@ -4628,3 +4628,51 @@ extern "C" __global__ void predictive_coding_loss(
atomicAdd(loss_out, lambda_pred * mse / (float)(N - 1));
}
/* ================================================================== */
/* Kernel: homeostatic_regularizer (G16) */
/* ================================================================== */
/**
* Unified self-regulating penalty framework.
*
* For each observable k: penalty = lambda * error * |error| (quadratic, signed)
* where error = (observed - target) / max(|target|, eps).
* Budget-capped: total penalty gradient <= budget_fraction of C51 gradient.
*
* All pointers are pinned device-mapped — CPU writes observables/targets,
* GPU reads via dev_ptr. Zero memcpy.
*
* Grid: (1,1,1), Block: (32,1,1). Single warp, n_obs <= 8.
*/
extern "C" __global__ void homeostatic_regularizer(
const float* __restrict__ observables, /* [N_OBS] pinned device-mapped */
const float* __restrict__ targets, /* [N_OBS] pinned device-mapped */
float* __restrict__ penalties, /* [N_OBS] output penalty gradients */
float* __restrict__ total_penalty, /* [1] sum of |penalty_k| */
int n_obs,
float lambda_base, /* 0.01 */
float budget_max /* max total penalty magnitude */
) {
int k = threadIdx.x;
if (k >= n_obs) return;
float obs = observables[k];
float tgt = targets[k];
float denom = fmaxf(fabsf(tgt), 1e-6f);
float error = (obs - tgt) / denom;
/* Quadratic signed penalty — small drift = tiny, large drift = hard correction */
float pen = lambda_base * error * fabsf(error);
penalties[k] = pen;
/* Atomic sum for budget cap (single warp, minimal contention) */
atomicAdd(total_penalty, fabsf(pen));
__syncthreads();
/* Budget cap: scale all penalties if total exceeds budget */
float total = total_penalty[0];
if (total > budget_max && total > 1e-8f) {
penalties[k] *= budget_max / total;
}
}

View File

@@ -81,6 +81,10 @@ const MAMBA2_HISTORY_K: usize = 8; // Rolling history length
const MAMBA2_STATE_DIM: usize = 16; // SSM state dimension
const NEW_COMPONENT_WARMUP_STEPS: u32 = 500;
/// Homeostatic regularizer: number of observable signals.
/// Observables: [Q-mean, atom_util, branch_corr, trade_freq_normalized, Q-gap, Q-var]
const HOMEOSTATIC_N_OBS: usize = 6;
// ── Event tracking RAII guard ────────────────────────────────────────────────
/// RAII guard that disables cudarc event tracking on creation and
@@ -1257,6 +1261,22 @@ pub struct GpuDqnTrainer {
save_h_v_5bar: CudaSlice<f32>, // [B, VH]
save_h_v_20bar: CudaSlice<f32>, // [B, VH]
v_logits_blended: CudaSlice<f32>, // [B, NA]
// ── Homeostatic regularizer (G16) ──
/// Compiled homeostatic_regularizer kernel from experience_kernels.cubin.
homeostatic_kernel: CudaFunction,
/// [6] observable signals — pinned device-mapped. CPU writes, GPU reads via dev_ptr.
homeostatic_obs_pinned: *mut f32,
homeostatic_obs_dev_ptr: u64,
/// [6] target set-points — pinned device-mapped. CPU writes, GPU reads via dev_ptr.
homeostatic_targets_pinned: *mut f32,
homeostatic_targets_dev_ptr: u64,
/// [6] per-observable penalty output (device buffer).
homeostatic_penalties_buf: CudaSlice<f32>,
/// [1] total penalty scalar output (device buffer).
homeostatic_total_buf: CudaSlice<f32>,
/// Whether calibration from early epochs is complete (epochs > 5).
homeostatic_calibration_done: bool,
}
impl GpuDqnTrainer {
@@ -1420,6 +1440,68 @@ impl GpuDqnTrainer {
self.recompute_atom_positions()?;
Ok(())
}
// ── Homeostatic regularizer (G16) ────────────────────────────────────
/// Update observables from current training state (CPU write to pinned).
/// Slots: [0]=Q-mean, [1]=atom_util, [2..5] set by caller if available.
pub(crate) fn update_homeostatic_observables(&self) {
unsafe {
let obs = self.homeostatic_obs_pinned;
*obs.add(0) = *self.q_mean_scratch_pinned; // Q-mean
*obs.add(1) = self.utilization_ema; // atom utilization
// branch_corr [2], trade_freq [3], Q-gap [4], Q-var [5]
// are written by caller or left at previous value.
}
}
/// Calibrate targets from observations (epochs 1-5).
/// EMA blend with alpha=0.3 so targets track early training dynamics.
/// Q-mean target is always forced to 0.0 (centered Q-values).
pub(crate) fn calibrate_homeostatic_targets(&mut self, epoch: usize) {
if epoch > 5 || self.homeostatic_calibration_done {
self.homeostatic_calibration_done = true;
return;
}
let alpha = 0.3_f32;
unsafe {
for k in 0..HOMEOSTATIC_N_OBS {
let obs = *self.homeostatic_obs_pinned.add(k);
let old = *self.homeostatic_targets_pinned.add(k);
*self.homeostatic_targets_pinned.add(k) = (1.0 - alpha) * old + alpha * obs;
}
// Q-mean target always 0.0 — centered Q-values are the invariant.
*self.homeostatic_targets_pinned.add(0) = 0.0;
}
}
/// Run homeostatic regularizer kernel.
/// Computes per-observable penalties and a clamped total penalty scalar.
pub(crate) fn apply_homeostatic_regularization(&self) -> Result<(), MLError> {
// Zero total_penalty before reduction
unsafe {
cudarc::driver::sys::cuMemsetD8Async(
self.homeostatic_total_buf.raw_ptr(), 0, std::mem::size_of::<f32>(), self.stream.cu_stream(),
);
}
unsafe {
self.stream.launch_builder(&self.homeostatic_kernel)
.arg(&self.homeostatic_obs_dev_ptr)
.arg(&self.homeostatic_targets_dev_ptr)
.arg(&self.homeostatic_penalties_buf)
.arg(&self.homeostatic_total_buf)
.arg(&(HOMEOSTATIC_N_OBS as i32))
.arg(&0.01_f32) // lambda_base
.arg(&0.5_f32) // budget_max
.launch(LaunchConfig {
grid_dim: (1, 1, 1),
block_dim: (32, 1, 1),
shared_mem_bytes: 0,
})
.map_err(|e| MLError::ModelError(format!("homeostatic_regularizer: {e}")))?;
}
Ok(())
}
}
impl Drop for GpuDqnTrainer {
@@ -4670,6 +4752,67 @@ impl GpuDqnTrainer {
// Pessimistic Q-init REMOVED — incompatible with per-sample support.
// Xavier init gives near-zero value head output, correct for adaptive C51.
// ── Homeostatic regularizer (G16) ────────────────────────────────────
let cpbi_module_homeo = stream.context().load_cubin(EXPECTED_Q_CUBIN.to_vec())
.map_err(|e| MLError::ModelError(format!("cpbi cubin (homeostatic): {e}")))?;
let homeostatic_kernel = cpbi_module_homeo.load_function("homeostatic_regularizer")
.map_err(|e| MLError::ModelError(format!("homeostatic_regularizer load: {e}")))?;
let homeostatic_penalties_buf = stream.alloc_zeros::<f32>(HOMEOSTATIC_N_OBS)
.map_err(|e| MLError::ModelError(format!("alloc homeostatic_penalties: {e}")))?;
let homeostatic_total_buf = stream.alloc_zeros::<f32>(1)
.map_err(|e| MLError::ModelError(format!("alloc homeostatic_total: {e}")))?;
// Pinned device-mapped: observables [6]
let (homeostatic_obs_pinned, homeostatic_obs_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,
HOMEOSTATIC_N_OBS * std::mem::size_of::<f32>(),
);
assert_eq!(rc, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS, "cuMemAllocHost for homeostatic_obs");
let rc2 = cudarc::driver::sys::cuMemHostGetDevicePointer_v2(
&mut dev_ptr_out,
host_ptr,
0,
);
assert_eq!(rc2, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS, "cuMemHostGetDevicePointer for homeostatic_obs");
// Zero-initialize
std::ptr::write_bytes(host_ptr as *mut f32, 0, HOMEOSTATIC_N_OBS);
}
(host_ptr as *mut f32, dev_ptr_out)
};
// Pinned device-mapped: targets [6]
let (homeostatic_targets_pinned, homeostatic_targets_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,
HOMEOSTATIC_N_OBS * std::mem::size_of::<f32>(),
);
assert_eq!(rc, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS, "cuMemAllocHost for homeostatic_targets");
let rc2 = cudarc::driver::sys::cuMemHostGetDevicePointer_v2(
&mut dev_ptr_out,
host_ptr,
0,
);
assert_eq!(rc2, cudarc::driver::sys::cudaError_enum::CUDA_SUCCESS, "cuMemHostGetDevicePointer for homeostatic_targets");
// Sensible defaults before calibration: [Q-mean=0, atom_util=0.85, branch_corr=0.1, trade_freq=0, Q-gap=1.0, Q-var=0.5]
let tgt = host_ptr as *mut f32;
*tgt.add(0) = 0.0;
*tgt.add(1) = 0.85;
*tgt.add(2) = 0.1;
*tgt.add(3) = 0.0;
*tgt.add(4) = 1.0;
*tgt.add(5) = 0.5;
}
(host_ptr as *mut f32, dev_ptr_out)
};
info!("GpuDqnTrainer: homeostatic_regularizer kernel loaded ({} observables, pinned device-mapped)", HOMEOSTATIC_N_OBS);
// ── Multi-horizon value head buffers (5-bar and 20-bar) ──
let v_logits_5bar = stream.alloc_zeros::<f32>(b * config.num_atoms)
.map_err(|e| MLError::ModelError(format!("alloc v_logits_5bar: {e}")))?;
@@ -5038,6 +5181,14 @@ impl GpuDqnTrainer {
save_h_v_5bar,
save_h_v_20bar,
v_logits_blended,
homeostatic_kernel,
homeostatic_obs_pinned,
homeostatic_obs_dev_ptr,
homeostatic_targets_pinned,
homeostatic_targets_dev_ptr,
homeostatic_penalties_buf,
homeostatic_total_buf,
homeostatic_calibration_done: false,
})
}
@@ -6438,6 +6589,10 @@ impl GpuDqnTrainer {
self.utilization_ema = (1.0 - alpha) * self.utilization_ema + alpha * util;
}
// G16: Homeostatic regularizer — write observables and launch penalty kernel.
self.update_homeostatic_observables();
self.apply_homeostatic_regularization()?;
// Drop EventTrackingGuard before mutable borrow in step_atom_positions.
drop(_eg);

View File

@@ -1983,6 +1983,12 @@ impl FusedTrainingCtx {
.map_err(|e| anyhow::anyhow!("reduce_current_q_stats: {e}"))
}
/// Calibrate homeostatic targets from early-epoch observations (epochs 1-5).
/// Passthrough to GpuDqnTrainer::calibrate_homeostatic_targets.
pub(crate) fn calibrate_homeostatic_targets(&mut self, epoch: usize) {
self.trainer.calibrate_homeostatic_targets(epoch);
}
/// Run cuBLAS forward pass and return reference to GPU-resident Q-values.
///
/// Output shape: `[batch_size, total_actions]` in `q_out_buf`.