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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user