feat: DQN gems — spectral decoupling, manifold mixup, regime replay, family hyperopt

Phase 1: Remove 5 DQNConfig boolean flags (use_soft_updates, use_iqn,
enable_q_value_clipping, use_cvar_action_selection, use_count_bonus).
All features now unconditionally active — no dead toggle branches.

Phase 2: Add spectral decoupling (L2 on Q-value logits, Pezeshki 2021)
and manifold mixup (Beta-sampled distribution interpolation with atomic
barrier sync) directly in C51 CUDA loss kernel. Zero CPU involvement.

Phase 3: Tag Experience transitions with market regime (ADX/CUSUM GPU
classifier kernel). Add regime-biased PER sampling via rejection with
IS weight correction. Decay factor controls cross-regime bleeding.

Phase 4: 6 family intensity scalars for hyperopt (adversarial,
regularization, augmentation, loss shaping, ensemble, causal). Scales
34 generalization params through 6 PSO dimensions instead of 34.
Search space: 24D → 30D (families additive, individual params kept).

20 files changed, +563/-69 lines. Full workspace compiles clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-31 00:39:23 +02:00
parent a86168cdea
commit 56373f0941
21 changed files with 1586 additions and 69 deletions

View File

@@ -33,6 +33,7 @@ q_gap_threshold = 0.05
[replay_buffer]
buffer_size = 50000
min_replay_size = 500
regime_replay_decay = 0.3
[early_stopping]
enabled = true
@@ -66,6 +67,7 @@ cql_alpha = 1.0
curiosity_weight = 0.1
iqn_lambda = 0.25
spectral_norm_sigma_max = 1.5
spectral_decoupling_lambda = 0.01
gradient_clip_norm = 1.0
[generalization]
@@ -98,6 +100,14 @@ enable_mirror_universe = true
position_entropy_weight = 0.01
trade_clustering_penalty = 0.05
ensemble_disagreement_penalty = 0.3
mixup_alpha = 0.2
# Family intensity scalars for hyperopt (1.0 = neutral, range [0.0, 2.0])
adversarial_intensity = 1.0
regularization_intensity = 1.0
augmentation_intensity = 1.0
loss_shaping_intensity = 1.0
ensemble_intensity = 1.0
causal_intensity = 1.0
[reward]
loss_aversion = 1.5

View File

@@ -38,6 +38,7 @@ buffer_size = 500000
min_replay_size = 1000
per_alpha = 0.6
per_beta_start = 0.4
regime_replay_decay = 0.3
[early_stopping]
enabled = true
@@ -71,6 +72,7 @@ cql_alpha = 1.0
curiosity_weight = 0.1
iqn_lambda = 0.25
spectral_norm_sigma_max = 1.5
spectral_decoupling_lambda = 0.01
gradient_clip_norm = 1.0
[generalization]
@@ -103,6 +105,14 @@ enable_mirror_universe = true
position_entropy_weight = 0.01
trade_clustering_penalty = 0.05
ensemble_disagreement_penalty = 0.3
mixup_alpha = 0.2
# Family intensity scalars for hyperopt (1.0 = neutral, range [0.0, 2.0])
adversarial_intensity = 1.0
regularization_intensity = 1.0
augmentation_intensity = 1.0
loss_shaping_intensity = 1.0
ensemble_intensity = 1.0
causal_intensity = 1.0
[reward]
loss_aversion = 1.5

View File

@@ -37,6 +37,7 @@ q_gap_threshold = 0.05
[replay_buffer]
buffer_size = 10000
min_replay_size = 100
regime_replay_decay = 0.3
[early_stopping]
enabled = true
@@ -70,6 +71,7 @@ cql_alpha = 1.0
curiosity_weight = 0.1
iqn_lambda = 0.25
spectral_norm_sigma_max = 1.5
spectral_decoupling_lambda = 0.01
gradient_clip_norm = 1.0
[generalization]
@@ -95,6 +97,14 @@ adversarial_warmup_epochs = 100 # Beyond smoketest duration (don't activate s
adversarial_checkpoint_interval = 100
pruning_epoch = 100 # Beyond 50-epoch test (don't prune tiny network)
pruning_fraction = 0.5 # Less aggressive (50% vs production 70%)
mixup_alpha = 0.2
# Family intensity scalars for hyperopt (1.0 = neutral, range [0.0, 2.0])
adversarial_intensity = 1.0
regularization_intensity = 1.0
augmentation_intensity = 1.0
loss_shaping_intensity = 1.0
ensemble_intensity = 1.0
causal_intensity = 1.0
[reward]
loss_aversion = 1.5

View File

@@ -20,11 +20,34 @@ pub struct Experience {
pub done: bool,
/// Experience timestamp
pub timestamp: u64,
/// Market regime at transition time: 0=Trending, 1=Ranging, 2=Volatile
pub regime: u8,
}
/// Classify market regime from a state feature vector.
///
/// Uses ADX (index 40) and CUSUM (index 41) to determine the market regime:
/// - 0 = Trending (ADX > 0.25)
/// - 1 = Ranging (default / low ADX, low CUSUM)
/// - 2 = Volatile (|CUSUM| > 0.7)
///
/// Returns 1 (Ranging) as safe default when the state vector is too short.
pub fn classify_regime_from_state(state: &[f32]) -> u8 {
let adx = if state.len() > 40 { state[40] } else { 0.0 };
let cusum = if state.len() > 41 { state[41] } else { 0.0 };
if adx > 0.25 {
0 // Trending
} else if cusum.abs() > 0.7 {
2 // Volatile
} else {
1 // Ranging
}
}
impl Experience {
/// Create a new experience
pub fn new(state: Vec<f32>, action: u8, reward: f32, next_state: Vec<f32>, done: bool) -> Self {
let regime = classify_regime_from_state(&state);
Self {
state,
action,
@@ -35,6 +58,7 @@ impl Experience {
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64,
regime,
}
}

View File

@@ -113,6 +113,7 @@ impl NStepBuffer {
next_state: first.next_state,
done: first.done,
timestamp: first.timestamp,
regime: first.regime,
});
}
@@ -139,6 +140,7 @@ impl NStepBuffer {
next_state: newest.next_state.clone(),
done: newest.done,
timestamp: first.timestamp,
regime: first.regime,
})
}
@@ -181,6 +183,7 @@ impl NStepBuffer {
next_state,
done,
timestamp: first.timestamp,
regime: first.regime,
});
}
@@ -226,6 +229,7 @@ mod tests {
next_state: vec![state_val + 1.0; 32],
done,
timestamp: chrono::Utc::now().timestamp_nanos_opt().unwrap() as u64,
regime: 1, // Ranging default for tests
}
}

View File

@@ -744,6 +744,85 @@ impl PrioritizedReplayBuffer {
}
}
/// Sample with regime affinity weighting via rejection sampling.
///
/// Oversamples 3x from standard PER, accepts each candidate with probability
/// `decay^|regime_distance|`, and corrects IS weights for the selection bias.
/// Falls back to standard PER sampling to fill any shortfall.
///
/// # Arguments
/// * `batch_size` - Target number of experiences to return
/// * `current_regime` - Active market regime (0=Trending, 1=Ranging, 2=Volatile)
/// * `decay` - Per-unit regime distance acceptance decay (0.0=same-regime only, 1.0=unbiased)
pub fn sample_regime_biased(
&self,
batch_size: usize,
current_regime: u8,
decay: f32,
) -> Result<(Vec<Experience>, Vec<f32>, Vec<usize>), MLError> {
let size = self.size.load(Ordering::Acquire);
if size < batch_size {
return Err(MLError::TrainingError(format!(
"Not enough experiences for regime-biased sampling: {} < {}",
size, batch_size
)));
}
// Oversample 3x from standard PER (capped at buffer size)
let oversample = (batch_size * 3).min(size);
let (candidates, cand_weights, cand_indices) = self.sample(oversample)?;
let mut rng = self.rng.lock();
let mut selected_exp = Vec::with_capacity(batch_size);
let mut selected_weights = Vec::with_capacity(batch_size);
let mut selected_indices = Vec::with_capacity(batch_size);
for (i, exp) in candidates.into_iter().enumerate() {
if selected_exp.len() >= batch_size {
break;
}
let regime_dist = (exp.regime as i8 - current_regime as i8).unsigned_abs();
let regime_factor = decay.powi(regime_dist as i32);
if rng.gen::<f32>() < regime_factor {
// Correct IS weight for regime selection bias
selected_weights.push(cand_weights[i] / regime_factor.max(1e-6));
selected_indices.push(cand_indices[i]);
selected_exp.push(exp);
}
}
// If not enough accepted, fill remainder from standard PER (unbiased)
if selected_exp.len() < batch_size {
let remaining = batch_size - selected_exp.len();
drop(rng); // release lock before calling sample()
let (extra_exp, extra_w, extra_idx) = self.sample(remaining)?;
for (i, exp) in extra_exp.into_iter().enumerate() {
if selected_exp.len() >= batch_size {
break;
}
selected_exp.push(exp);
selected_weights.push(extra_w[i]);
selected_indices.push(extra_idx[i]);
}
}
// Normalize weights so max = 1.0
let max_w = selected_weights
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max);
if max_w > 0.0 {
for w in &mut selected_weights {
*w /= max_w;
}
}
Ok((selected_exp, selected_weights, selected_indices))
}
/// Get current training step
pub fn training_step(&self) -> usize {
self.training_step.load(Ordering::Acquire)

View File

@@ -919,7 +919,6 @@ fn evaluate_dqn_fold(
(10.0 / (1.0 - gamma) * 1.2).clamp(20.0, 300.0)
}) as f32,
iqn_num_quantiles: hp_usize(hp, "num_quantiles").unwrap_or(64),
use_cvar_action_selection: false,
..DQNConfig::default()
};
@@ -1088,7 +1087,6 @@ fn evaluate_dqn_fold_gpu(
(10.0 / (1.0 - gamma) * 1.2).clamp(20.0, 300.0)
}) as f32,
iqn_num_quantiles: hp_usize(hp, "num_quantiles").unwrap_or(64),
use_cvar_action_selection: false,
..DQNConfig::default()
};

View File

@@ -421,7 +421,6 @@ impl DqnBenchmarkRunner {
tau: 1.0,
tau_final: 1.0, // No annealing for hard updates
tau_anneal_steps: 0,
use_soft_updates: false,
// Rainbow DQN warmup period (disabled for benchmarking)
warmup_steps: 0, // No warmup for fast benchmarks
@@ -450,7 +449,6 @@ impl DqnBenchmarkRunner {
noisy_sigma_init: 0.5,
// BUG #37 FIX: Q-value clipping (prevents step-level explosions)
enable_q_value_clipping: true,
q_value_clip_min: -500.0,
q_value_clip_max: 500.0,
@@ -459,11 +457,9 @@ impl DqnBenchmarkRunner {
gradient_collapse_patience: 5,
cql_alpha: 1.0,
use_iqn: false,
iqn_num_quantiles: 64,
iqn_kappa: 1.0,
iqn_embedding_dim: 64,
use_cvar_action_selection: false,
cvar_alpha: 0.05,
minimum_profit_factor: 1.5,

View File

@@ -206,7 +206,15 @@ extern "C" __global__ void c51_loss_batched(
/* ── #27 Ensemble disagreement Q-penalty ─────────────────────── */
const float* __restrict__ ensemble_std, /* [B] per-sample ensemble std (0=no penalty) */
float ensemble_disagreement_weight /* scale reward by 1/(1+weight*std) (0.0=disabled) */
float ensemble_disagreement_weight, /* scale reward by 1/(1+weight*std) (0.0=disabled) */
/* ── Spectral decoupling: L2 penalty on Q-value logit magnitudes ── */
float spectral_decoupling_lambda, /* Pezeshki et al. 2021 (0.0=disabled, 0.01=default) */
/* ── Manifold Mixup: interpolate projected C51 target distributions ── */
float mixup_alpha, /* Beta(alpha,alpha) mixing coeff. 0.0=disabled */
unsigned int mixup_seed, /* per-batch RNG seed */
int* __restrict__ mixup_barrier /* [NUM_BRANCHES] atomic counters for inter-block sync */
) {
extern __shared__ float shmem_f[];
@@ -295,6 +303,7 @@ extern "C" __global__ void c51_loss_batched(
}
float total_ce = 0.0f;
float total_logit_sq = 0.0f; /* Spectral decoupling: accumulates ||logits||^2 across all branches */
for (int d = 0; d < NUM_BRANCHES; d++) {
int n_d = branch_sizes[d];
@@ -322,6 +331,26 @@ extern "C" __global__ void c51_loss_batched(
}
__syncthreads();
/* ── Spectral decoupling: accumulate sum of squared logits across ALL actions ──
* Penalizes overconfident Q-value logit magnitudes (Pezeshki et al. 2021).
* Computed on the raw dueling logits (V + A_a - mean(A)) BEFORE softmax.
* Iterates over all actions in this branch to get full coverage. */
if (spectral_decoupling_lambda > 0.0f) {
float local_sq = 0.0f;
for (int a = 0; a < n_d; a++) {
for (int j = tid; j < num_atoms; j += BLOCK_THREADS) {
float a_mean = 0.0f;
for (int aa = 0; aa < n_d; aa++)
a_mean += shmem_adv[aa * num_atoms + j];
a_mean /= (float)n_d;
float logit = shmem_val[j] + shmem_adv[a * num_atoms + j] - a_mean;
local_sq += logit * logit;
}
}
float branch_sq = block_reduce_sum_f(local_sq, shmem_reduce, tid);
total_logit_sq += branch_sq;
}
block_log_softmax_f(shmem_lp, shmem_lp, shmem_reduce, tid, num_atoms);
/* Copy and save current log-probs */
@@ -424,6 +453,69 @@ extern "C" __global__ void c51_loss_batched(
save_projected[save_off + j] = bf16(shmem_lp[j]);
__syncthreads();
/* ═══ Manifold Mixup: interpolate projected target distributions ═══
* Verma et al. 2019 — mix pairs of C51 target distributions to
* smooth the learned manifold and improve generalization.
*
* Uses save_projected buffer for cross-sample reads with an atomic
* inter-block barrier to ensure all samples have written their
* projected distributions before any sample reads a partner's.
*
* Lambda ~ Beta(alpha, alpha) via ratio of Kumaraswamy-like rvs. */
if (mixup_alpha > 0.0f) {
/* Inter-block barrier: all blocks must finish writing save_projected
* for branch d before any block reads a partner's projection.
* Pattern: atomicAdd → spin until counter == batch_size. */
__threadfence(); /* ensure save_projected writes are globally visible */
if (tid == 0) {
atomicAdd(&mixup_barrier[d], 1);
}
__syncthreads();
if (tid == 0) {
/* Spin until all blocks for this branch have arrived */
while (atomicAdd(&mixup_barrier[d], 0) < batch_size) {
/* busy-wait — lightweight since all blocks execute in ~lockstep */
}
}
__syncthreads();
__threadfence(); /* ensure we see partner's save_projected writes */
/* Pseudo-random partner selection via multiplicative hash (Knuth) */
unsigned int h = (unsigned int)sample_id;
h ^= mixup_seed + (unsigned int)d * 2654435761u;
h *= 2654435761u;
h ^= h >> 16;
h *= 0x85ebca6bu;
h ^= h >> 13;
int partner = (int)(h % (unsigned int)batch_size);
/* Sample lambda from Beta(alpha, alpha) approximation:
* lambda = u1^(1/a) / (u1^(1/a) + u2^(1/a)) where u1,u2 ~ Uniform(0,1)
* For symmetric Beta(a,a), this yields the correct distribution. */
unsigned int rng_state = mixup_seed ^ ((unsigned int)sample_id * 1103515245u + 12345u);
rng_state = rng_state * 1664525u + 1013904223u;
float u1 = (float)(rng_state & 0x7FFFFFu) / (float)0x7FFFFFu;
u1 = fmaxf(u1, 1e-7f);
rng_state = rng_state * 1664525u + 1013904223u;
float u2 = (float)(rng_state & 0x7FFFFFu) / (float)0x7FFFFFu;
u2 = fmaxf(u2, 1e-7f);
float inv_alpha = 1.0f / mixup_alpha;
float v1 = powf(u1, inv_alpha);
float v2 = powf(u2, inv_alpha);
float lambda = v1 / (v1 + v2 + 1e-7f);
/* Read partner's projected distribution from save_projected (BF16 → float)
* and mix: proj_mixed = lambda * proj_self + (1-lambda) * proj_partner */
long long partner_save_off = ((long long)partner * NUM_BRANCHES + d) * num_atoms;
float one_minus_lambda = 1.0f - lambda;
for (int j = tid; j < num_atoms; j += BLOCK_THREADS) {
float my_proj = shmem_lp[j];
float partner_proj = __bfloat162float(save_projected[partner_save_off + j]);
shmem_lp[j] = lambda * my_proj + one_minus_lambda * partner_proj;
}
__syncthreads();
}
/* ═══ STEP e: Cross-entropy loss ═════════════════════════ */
float local_ce = 0.0f;
@@ -437,6 +529,18 @@ extern "C" __global__ void c51_loss_batched(
float avg_ce = total_ce / (float)NUM_BRANCHES;
/* ── Spectral decoupling: add L2 logit magnitude penalty ──────────────
* SD_penalty = lambda * ||logits||^2 / num_logits_total
* This penalizes the raw Q-value logits (before softmax) across all
* branches and all actions, preventing overconfident predictions without
* affecting gradient flow through the weight matrices.
* Reference: Pezeshki et al. 2021, "Gradient Starvation". */
if (spectral_decoupling_lambda > 0.0f) {
int total_logits_count = (b0_size + b1_size + b2_size) * num_atoms;
float sd_penalty = spectral_decoupling_lambda * total_logit_sq / (float)total_logits_count;
avg_ce += sd_penalty;
}
/* #18 Asymmetric DD loss: penalize overconfident Q-values during drawdown.
* For C51, overconfidence = high cross-entropy (distribution mismatch).
* Scale CE up when in drawdown — makes the loss surface steeper for

View File

@@ -1892,3 +1892,47 @@ extern "C" __global__ void expert_action_override(
int a2 = current % b2_size;
out_actions[i] = expert_a0 * b1_size * b2_size + a1 * b2_size + a2;
}
/* ================================================================== */
/* Kernel: classify_regime_gpu */
/* ================================================================== */
/**
* Classify market regime from state features on GPU.
*
* ADX at index `adx_idx` (typically 40), CUSUM at index `cusum_idx`
* (typically 41) of the state vector. Classification logic mirrors
* the CPU-side classify_regime_from_state() in experience.rs:
*
* ADX > adx_threshold => 0 (Trending)
* |CUSUM| > cusum_threshold => 2 (Volatile)
* else => 1 (Ranging)
*
* Grid: ceil(batch_size / 256), Block: 256. One thread per sample.
*/
extern "C" __global__ void classify_regime_gpu(
const float* __restrict__ states, /* [batch_size, state_dim] */
unsigned char* __restrict__ regimes, /* [batch_size] output */
int batch_size,
int state_dim,
int adx_idx, /* typically 40 */
int cusum_idx, /* typically 41 */
float adx_threshold, /* typically 0.25 */
float cusum_threshold /* typically 0.7 */
) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= batch_size) return;
float adx = (adx_idx < state_dim) ? states[idx * state_dim + adx_idx] : 0.0f;
float cusum = (cusum_idx < state_dim) ? states[idx * state_dim + cusum_idx] : 0.0f;
unsigned char regime;
if (adx > adx_threshold) {
regime = 0; /* Trending */
} else if (fabsf(cusum) > cusum_threshold) {
regime = 2; /* Volatile */
} else {
regime = 1; /* Ranging */
}
regimes[idx] = regime;
}

View File

@@ -157,6 +157,9 @@ pub struct GpuDqnTrainConfig {
/// Default 3.0 (permits natural Xavier-init scaling, prevents explosion).
/// Lower values = tighter constraint = more Q-value stability but less capacity.
pub spectral_norm_sigma_max: f32,
/// Spectral decoupling: L2 penalty on Q-value logit magnitudes.
/// Pezeshki et al. 2021. Default: 0.01.
pub spectral_decoupling_lambda: f32,
/// N-step returns (default: 1). When > 1, the experience collector pre-computes
/// R_n = sum(gamma^i * r_{t+i}) and the C51 Bellman uses gamma^n.
pub n_steps: usize,
@@ -189,6 +192,10 @@ pub struct GpuDqnTrainConfig {
pub ensemble_disagreement_penalty: f32,
/// #21 Stochastic depth drop probability. 0.0 = disabled, 0.2 = 20% drop per layer.
pub stochastic_depth_prob: f32,
/// Manifold Mixup alpha for Beta distribution.
/// Mixes pairs of C51 target distributions during training.
/// 0.0 = disabled. Range: [0.0, 2.0]. Default: 0.2.
pub mixup_alpha: f32,
/// #34 Enable causal intervention training.
pub enable_causal_intervention: bool,
/// #34 Causal regularization weight.
@@ -239,6 +246,7 @@ impl Default for GpuDqnTrainConfig {
weight_decay: 1e-4, // Standard L2 regularization strength
max_grad_norm: 10.0,
spectral_norm_sigma_max: 3.0,
spectral_decoupling_lambda: 0.01,
iqn_lambda: 0.25,
iqn_num_quantiles: 0,
iqn_embedding_dim: 64,
@@ -250,6 +258,7 @@ impl Default for GpuDqnTrainConfig {
asymmetric_dd_weight: 0.0,
ensemble_disagreement_penalty: 0.0,
stochastic_depth_prob: 0.0,
mixup_alpha: 0.2,
pruning_epoch: 50,
pruning_fraction: 0.7,
enable_causal_intervention: true, // always on
@@ -682,6 +691,10 @@ pub struct GpuDqnTrainer {
/// #21 Stochastic depth drop probability (0.0=disabled, 0.2=20% drop).
stochastic_depth_prob: f32,
/// Manifold Mixup: atomic barrier counters [NUM_BRANCHES] for inter-block sync.
/// Zeroed before each C51 loss launch. Only used when mixup_alpha > 0.
mixup_barrier_buf: CudaSlice<i32>,
/// #27 Per-sample ensemble std for disagreement Q-penalty.
/// Populated by `upload_ensemble_std()` before loss kernel launch.
/// Zero-filled when ensemble is disabled.
@@ -2422,6 +2435,10 @@ impl GpuDqnTrainer {
let sd_seed = (std::process::id() as u32).wrapping_mul(2654435761);
stream.memcpy_htod(&[sd_seed], &mut stochastic_depth_rng_state)
.map_err(|e| MLError::ModelError(format!("seed sd_rng: {e}")))?;
// Manifold Mixup: atomic barrier counters [NUM_BRANCHES=3] for inter-block sync
let mixup_barrier_buf = stream.alloc_zeros::<i32>(3)
.map_err(|e| MLError::ModelError(format!("alloc mixup_barrier: {e}")))?;
let curiosity_inference_func = {
static CURIOSITY_INFERENCE_CUBIN: &[u8] = include_bytes!(
concat!(env!("OUT_DIR"), "/curiosity_inference_kernel.cubin")
@@ -2692,6 +2709,7 @@ impl GpuDqnTrainer {
stochastic_depth_rng_kernel,
stochastic_depth_rng_state,
stochastic_depth_prob: sd_prob,
mixup_barrier_buf,
ensemble_std_buf,
ensemble_disagreement_weight: ens_weight,
curiosity_inference_func,
@@ -3871,6 +3889,9 @@ impl GpuDqnTrainer {
self.launch_mse_grad_to_scratch()?;
// C51 path → main buffers (already zeroed above)
// Zero Manifold Mixup inter-block barrier counters [NUM_BRANCHES=3] before C51 launch.
self.stream.memset_zeros(&mut self.mixup_barrier_buf)
.map_err(|e| MLError::ModelError(format!("zero mixup_barrier: {e}")))?;
self.launch_c51_loss()?;
self.launch_c51_grad()?;
@@ -4412,6 +4433,9 @@ impl GpuDqnTrainer {
let shmem_floats = na + na + max_branch * na + na + na + na + 8;
let shmem_bytes = (shmem_floats * std::mem::size_of::<f32>()) as u32;
// Manifold Mixup: per-batch seed derived from adam_step (deterministic, no rand dep)
let mixup_seed: u32 = (self.adam_step as u32).wrapping_mul(0x9E3779B9);
unsafe {
self.stream
.launch_builder(kernel)
@@ -4460,6 +4484,12 @@ impl GpuDqnTrainer {
// ── #27 Ensemble disagreement (2) ──
.arg(&self.ensemble_std_buf)
.arg(&self.ensemble_disagreement_weight)
// ── Spectral decoupling (1) ──
.arg(&self.config.spectral_decoupling_lambda)
// ── Manifold Mixup (3): alpha, per-batch RNG seed, atomic barrier ──
.arg(&self.config.mixup_alpha)
.arg(&mixup_seed)
.arg(&self.mixup_barrier_buf)
.launch(LaunchConfig {
grid_dim: (b as u32, 1, 1),
block_dim: (256, 1, 1),
@@ -4617,6 +4647,8 @@ impl GpuDqnTrainer {
// ── #27 Ensemble disagreement (2) ──
.arg(&self.ensemble_std_buf)
.arg(&self.ensemble_disagreement_weight)
// ── Spectral decoupling (1) ──
.arg(&self.config.spectral_decoupling_lambda)
.launch(LaunchConfig {
grid_dim: (b as u32, 1, 1),
block_dim: (256, 1, 1),

View File

@@ -159,7 +159,10 @@ extern "C" __global__ void mse_loss_batched(
/* ── #27 Ensemble disagreement Q-penalty ─────────────────────── */
const float* __restrict__ ensemble_std, /* [B] per-sample ensemble std (0=no penalty) */
float ensemble_disagreement_weight /* Q_target -= weight * std (0.0=disabled) */
float ensemble_disagreement_weight, /* Q_target -= weight * std (0.0=disabled) */
/* ── Spectral decoupling: L2 penalty on Q-value logit magnitudes ── */
float spectral_decoupling_lambda /* Pezeshki et al. 2021 (0.0=disabled, 0.01=default) */
) {
/* Shared memory is now float (4 bytes/elem) — doubled from BF16 version */
extern __shared__ float shmem_f[];
@@ -244,6 +247,7 @@ extern "C" __global__ void mse_loss_batched(
float total_mse = 0.0f;
float total_abs_td = 0.0f;
float total_logit_sq = 0.0f; /* Spectral decoupling: accumulates ||logits||^2 across all branches */
for (int d = 0; d < NUM_BRANCHES; d++) {
int n_d = branch_sizes[d];
@@ -274,6 +278,26 @@ extern "C" __global__ void mse_loss_batched(
}
__syncthreads();
/* ── Spectral decoupling: accumulate sum of squared logits across ALL actions ──
* Penalizes overconfident Q-value logit magnitudes (Pezeshki et al. 2021).
* Computed on the raw dueling logits (V + A_a - mean(A)) BEFORE softmax.
* Iterates over all actions in this branch to get full coverage. */
if (spectral_decoupling_lambda > 0.0f) {
float local_sq = 0.0f;
for (int a = 0; a < n_d; a++) {
for (int j = tid; j < num_atoms; j += BLOCK_THREADS) {
float a_mean = 0.0f;
for (int aa = 0; aa < n_d; aa++)
a_mean += shmem_adv[aa * num_atoms + j];
a_mean /= (float)n_d;
float logit = shmem_val[j] + shmem_adv[a * num_atoms + j] - a_mean;
local_sq += logit * logit;
}
}
float branch_sq = block_reduce_sum_f(local_sq, shmem_reduce, tid);
total_logit_sq += branch_sq;
}
/* softmax + E[Q], saving probs to save_current_lp (BF16 output) */
float online_eq = block_softmax_expected_q_f(
shmem_lp, shmem_support, shmem_reduce,
@@ -388,6 +412,18 @@ extern "C" __global__ void mse_loss_batched(
float avg_mse = total_mse / (float)NUM_BRANCHES;
float avg_td = total_abs_td / (float)NUM_BRANCHES;
/* ── Spectral decoupling: add L2 logit magnitude penalty ──────────────
* SD_penalty = lambda * ||logits||^2 / num_logits_total
* This penalizes the raw Q-value logits (before softmax) across all
* branches and all actions, preventing overconfident predictions without
* affecting gradient flow through the weight matrices.
* Reference: Pezeshki et al. 2021, "Gradient Starvation". */
if (spectral_decoupling_lambda > 0.0f) {
int total_logits_count = (b0_size + b1_size + b2_size) * num_atoms;
float sd_penalty = spectral_decoupling_lambda * total_logit_sq / (float)total_logits_count;
avg_mse += sd_penalty;
}
if (tid == 0) {
float weighted_loss = avg_mse * is_weight;
per_sample_loss[sample_id] = bf16(weighted_loss);

View File

@@ -322,6 +322,18 @@ pub struct DQNParams {
/// Lower = tighter constraint (more stability, less capacity).
/// Default 3.0 (permits Xavier-init scaling, prevents Q-explosion).
pub spectral_norm_sigma_max: f64,
/// Spectral decoupling: L2 penalty on Q-value logit magnitudes. Default: 0.01.
pub spectral_decoupling_lambda: f64,
/// Regime-aware replay decay factor. PER priorities for cross-regime
/// transitions are multiplied by decay^|regime_distance|.
/// 1.0 = no bias. 0.0 = only current regime. Default: 0.3.
pub regime_replay_decay: f64,
/// Manifold Mixup alpha for Beta distribution.
/// Mixes pairs of C51 target distributions during training (Verma et al. 2019).
/// 0.0 = disabled. Range: [0.0, 2.0]. Default: 0.2.
pub mixup_alpha: f64,
/// Hidden dimension base for dynamic network sizing.
/// Network shape: [base, base/2, base/4]. Range: [256, 4096], step=256.
@@ -394,9 +406,6 @@ pub struct DQNParams {
/// Encourages exploration of novel state-action pairs via prediction error.
pub curiosity_weight_tunable: f64,
/// Enable CVaR (Conditional Value-at-Risk) action selection (0.0 = off, 1.0 = on)
/// Stored as f64 for hyperopt, rounded to bool in build_hyperparams.
pub use_cvar_action_selection: f64,
/// CVaR alpha confidence level (0.01-0.20). Lower = more risk-averse.
/// 0.05 = optimize for worst 5% outcomes.
@@ -405,6 +414,20 @@ pub struct DQNParams {
/// Minimum bars to hold a position before allowing exit or reversal (integer, [2, 20]).
/// Lets PSO discover optimal hold duration for ES futures regime characteristics.
pub min_hold_bars: usize,
// ── Family intensity scalars (6D hyperopt expansion) ──
/// Adversarial family intensity: scales saboteur perturbation, warmup, interval. [0.0, 2.0]
pub adversarial_intensity: f64,
/// Regularization family intensity: scales dropout, spectral norm, stochastic depth, spectral decoupling. [0.0, 2.0]
pub regularization_intensity: f64,
/// Data augmentation family intensity: scales feature masking, feature noise. [0.0, 2.0]
pub augmentation_intensity: f64,
/// Loss shaping family intensity: scales asymmetric DD, regret blend, trade clustering, position entropy. [0.0, 2.0]
pub loss_shaping_intensity: f64,
/// Ensemble family intensity: scales ensemble disagreement penalty. [0.0, 2.0]
pub ensemble_intensity: f64,
/// Causal family intensity: scales causal weight, intervention interval. [0.0, 2.0]
pub causal_intensity: f64,
}
impl Default for DQNParams {
@@ -473,6 +496,9 @@ impl Default for DQNParams {
qr_kappa: 1.0,
iqn_lambda: 0.25, // Default: mild IQN regularization alongside C51
spectral_norm_sigma_max: 3.0, // Default: permits Xavier scaling
spectral_decoupling_lambda: 0.01, // Default: mild logit magnitude penalty (Pezeshki 2021)
regime_replay_decay: 0.3, // Default: moderate cross-regime decay
mixup_alpha: 0.2, // Manifold Mixup: Beta(0.2,0.2) target distribution mixing
hidden_dim_base: 256, // Conservative default (backward compatible)
noisy_epsilon_floor: 0.10, // Minimum 10% random exploration to prevent action collapse
count_bonus_coefficient: 0.05, // UCB directed exploration — complementary to NoisyNet
@@ -495,9 +521,15 @@ impl Default for DQNParams {
// Task 11: New hyperopt-tunable parameters
her_ratio: 0.2, // Default: 20% HER relabeling
curiosity_weight_tunable: 0.1, // Default: 10% curiosity-driven intrinsic reward
use_cvar_action_selection: 1.0, // Default: enabled (risk-aware IQN)
cvar_alpha: 0.05, // Default: worst 5% tail
min_hold_bars: 5, // Default: 5 bars minimum hold
// Family intensity scalars: 1.0 = neutral (no change to defaults)
adversarial_intensity: 1.0,
regularization_intensity: 1.0,
augmentation_intensity: 1.0,
loss_shaping_intensity: 1.0,
ensemble_intensity: 1.0,
causal_intensity: 1.0,
}
}
}
@@ -559,6 +591,12 @@ impl ParameterSpace for DQNParams {
iq, // 21: iqn_lambda
wdd, // 22: w_dd (drawdown penalty weight)
ddt, // 23: dd_threshold (drawdown start)
(0.0, 2.0), // 24: adversarial_intensity
(0.0, 2.0), // 25: regularization_intensity
(0.0, 2.0), // 26: augmentation_intensity
(0.0, 2.0), // 27: loss_shaping_intensity
(0.0, 2.0), // 28: ensemble_intensity
(0.0, 2.0), // 29: causal_intensity
];
// Phase Fast: fix architecture dims to single-point bounds
@@ -691,19 +729,28 @@ impl ParameterSpace for DQNParams {
gradient_accumulation_steps: 1,
dt_pretrain_epochs: 0,
spectral_norm_sigma_max: 5.0,
spectral_decoupling_lambda: 0.01,
regime_replay_decay: 0.3,
mixup_alpha: 0.2,
num_quantiles: 32,
qr_kappa: 1.0,
her_ratio: 0.3,
curiosity_weight_tunable: 0.1,
use_cvar_action_selection: 0.0,
cvar_alpha: 0.05,
// Family intensity scalars (indices 24-29, linear [0.0, 2.0])
adversarial_intensity: if x.len() > 24 { x[24].clamp(0.0, 2.0) } else { 1.0 },
regularization_intensity: if x.len() > 25 { x[25].clamp(0.0, 2.0) } else { 1.0 },
augmentation_intensity: if x.len() > 26 { x[26].clamp(0.0, 2.0) } else { 1.0 },
loss_shaping_intensity: if x.len() > 27 { x[27].clamp(0.0, 2.0) } else { 1.0 },
ensemble_intensity: if x.len() > 28 { x[28].clamp(0.0, 2.0) } else { 1.0 },
causal_intensity: if x.len() > 29 { x[29].clamp(0.0, 2.0) } else { 1.0 },
};
Ok(params)
}
fn to_continuous(&self) -> Vec<f64> {
// 24D search space (consolidated from 46D)
// 30D search space (24 original + 6 family intensities)
vec![
self.learning_rate.ln(), // 0: learning_rate (log)
self.batch_size as f64, // 1: batch_size
@@ -729,11 +776,17 @@ impl ParameterSpace for DQNParams {
self.iqn_lambda, // 21: iqn_lambda
self.w_dd, // 22: w_dd
self.dd_threshold, // 23: dd_threshold
self.adversarial_intensity, // 24: adversarial_intensity
self.regularization_intensity, // 25: regularization_intensity
self.augmentation_intensity, // 26: augmentation_intensity
self.loss_shaping_intensity, // 27: loss_shaping_intensity
self.ensemble_intensity, // 28: ensemble_intensity
self.causal_intensity, // 29: causal_intensity
]
}
fn param_names() -> Vec<&'static str> {
// 24 tuned parameters
// 30 tuned parameters (24 original + 6 family intensities)
vec![
"learning_rate", // 0
"batch_size", // 1
@@ -759,6 +812,12 @@ impl ParameterSpace for DQNParams {
"iqn_lambda", // 21
"w_dd", // 22
"dd_threshold", // 23
"adversarial_intensity", // 24
"regularization_intensity", // 25
"augmentation_intensity", // 26
"loss_shaping_intensity", // 27
"ensemble_intensity", // 28
"causal_intensity", // 29
]
}
@@ -2504,7 +2563,7 @@ impl HyperparameterOptimizable for DQNTrainer {
};
// Create DQN hyperparameters from optimization params
let hyperparams = DQNHyperparameters {
let mut hyperparams = DQNHyperparameters {
learning_rate: params.learning_rate,
batch_size: params.batch_size,
gamma: params.gamma,
@@ -2588,6 +2647,7 @@ impl HyperparameterOptimizable for DQNTrainer {
// P0 CRITICAL: Prioritized Experience Replay (now tunable in hyperopt)
per_alpha: params.per_alpha, // Tunable 0.4-0.8 (default: 0.6)
per_beta_start: params.per_beta_start, // Tunable 0.2-0.6 (default: 0.4)
regime_replay_decay: params.regime_replay_decay, // Regime-aware replay bias (default: 0.3)
// Wave 2.1: Dueling Networks
dueling_hidden_dim: params.dueling_hidden_dim, // Wave 6.3: Use hyperopt value
@@ -2693,6 +2753,8 @@ impl HyperparameterOptimizable for DQNTrainer {
qr_kappa: params.qr_kappa,
iqn_lambda: params.iqn_lambda,
spectral_norm_sigma_max: params.spectral_norm_sigma_max,
spectral_decoupling_lambda: params.spectral_decoupling_lambda,
mixup_alpha: params.mixup_alpha,
// Conservative Q-Learning (CQL) — wired from hyperopt search space
cql_alpha: params.cql_alpha,
@@ -2769,13 +2831,23 @@ impl HyperparameterOptimizable for DQNTrainer {
// Decision Transformer pre-training: disabled for all hyperopt trials
dt_pretrain_epochs: params.dt_pretrain_epochs,
// Task 11: CVaR action selection — wired from hyperopt search space (indices 43-44)
use_cvar_action_selection: params.use_cvar_action_selection.round() >= 1.0,
cvar_alpha: params.cvar_alpha as f32,
// Task 4: min_hold_bars wired from hyperopt search space (index 45)
min_hold_bars: params.min_hold_bars,
// Family intensity scalars (indices 24-29 in 30D search space)
adversarial_intensity: params.adversarial_intensity,
regularization_intensity: params.regularization_intensity,
augmentation_intensity: params.augmentation_intensity,
loss_shaping_intensity: params.loss_shaping_intensity,
ensemble_intensity: params.ensemble_intensity,
causal_intensity: params.causal_intensity,
..Default::default()
};
// Apply family intensity scaling AFTER construction, BEFORE training.
// This multiplies the per-parameter defaults by the family scalar (1.0 = no change).
hyperparams.apply_family_scaling();
let data_path_str = self
.dbn_data_dir.to_str()
.ok_or_else(|| MLError::ConfigError("Invalid UTF-8 in data path".to_owned()))?;

View File

@@ -194,13 +194,7 @@ impl DQNAgentType {
}
}
/// Whether count-based exploration bonus is enabled.
pub fn use_count_bonus(&self) -> bool {
match self {
Self::Standard(agent) => agent.config.use_count_bonus,
Self::RegimeConditional(agent) => agent.config().use_count_bonus,
}
}
/// Number of discrete actions in the action space.
pub fn num_actions(&self) -> usize {
@@ -999,6 +993,26 @@ pub struct DQNHyperparameters {
/// are scaled by 1/(1-p) for expected-value correction. 0.0 = disabled.
pub stochastic_depth_prob: f64,
/// Manifold Mixup alpha for Beta distribution.
/// Mixes pairs of C51 target distributions during training.
/// 0.0 = disabled. Range: [0.0, 2.0]. Default: 0.2.
pub mixup_alpha: f64,
// ── Family intensity scalars for hyperopt (1.0 = defaults, range [0.0, 2.0]) ──
/// Adversarial family intensity: scales saboteur perturbation, warmup, interval.
pub adversarial_intensity: f64,
/// Regularization family intensity: scales dropout, spectral norm, stochastic depth, spectral decoupling.
pub regularization_intensity: f64,
/// Data augmentation family intensity: scales feature masking, feature noise.
pub augmentation_intensity: f64,
/// Loss shaping family intensity: scales asymmetric DD, regret blend, trade clustering, position entropy.
pub loss_shaping_intensity: f64,
/// Ensemble family intensity: scales ensemble disagreement penalty.
pub ensemble_intensity: f64,
/// Causal family intensity: scales causal weight, intervention interval.
pub causal_intensity: f64,
/// #18 Asymmetric drawdown loss: extra penalty on Q-overestimation when
/// portfolio is in drawdown. Weight for L_dd = max(0, Q_pred-Q_target) * dd^2.
/// 0.0 = disabled.
@@ -1036,6 +1050,11 @@ pub struct DQNHyperparameters {
pub per_alpha: f64,
pub per_beta_start: f64,
/// Regime-aware replay decay factor. PER priorities for cross-regime
/// transitions are multiplied by decay^|regime_distance|.
/// 1.0 = no bias. 0.0 = only current regime. Default: 0.3.
pub regime_replay_decay: f64,
// Wave 2.1: Dueling Networks (always enabled)
/// Hidden dimension for dueling value/advantage streams
pub dueling_hidden_dim: usize,
@@ -1200,6 +1219,10 @@ pub struct DQNHyperparameters {
/// Spectral norm σ_max — constrains ||W||_σσ_max.
/// Range [1.0, 10.0]. Default 3.0 (permits Xavier scaling, prevents Q-explosion).
pub spectral_norm_sigma_max: f64,
/// Spectral decoupling: L2 penalty on Q-value logit magnitudes.
/// Prevents overconfident Q-values. Pezeshki et al. 2021.
/// Range: [0.0, 0.1]. Default: 0.01.
pub spectral_decoupling_lambda: f64,
/// When true, IQN's bounded Huber loss is used for PER priorities instead of
/// C51's unbounded cross-entropy. Prevents the PER feedback explosion.
@@ -1363,11 +1386,6 @@ pub struct DQNHyperparameters {
pub dt_target_return: f64,
// CVaR (Conditional Value at Risk) action selection for IQN
/// Enable CVaR-aware action selection when IQN is active.
/// When true, actions are selected by optimizing the worst-case quantile tail
/// (risk-averse) instead of the mean quantile (risk-neutral).
/// Default: true (enables risk-aware position scaling via IQN head).
pub use_cvar_action_selection: bool,
/// CVaR confidence level alpha (0.0-1.0). Lower alpha = more risk-averse.
/// 0.05 = optimize for worst 5% outcomes (extreme caution).
/// 0.25 = optimize for worst 25% outcomes (moderate risk aversion).
@@ -1416,6 +1434,52 @@ impl DQNHyperparameters {
(self.reward_scale / (1.0 - self.gamma) * 1.2).clamp(20.0, 300.0)
}
/// Apply family intensity scalars to their respective parameters.
/// Call after constructing from hyperopt PSO vector.
/// Intensity 1.0 = no change. 0.5 = halved. 2.0 = doubled.
/// For interval/epoch params: higher intensity = more frequent (divide by intensity).
pub fn apply_family_scaling(&mut self) {
// Adversarial family
let ai = self.adversarial_intensity;
self.adversarial_dd_threshold *= ai;
if ai > 0.01 {
self.adversarial_warmup_epochs = (self.adversarial_warmup_epochs as f64 / ai) as usize;
self.adversarial_checkpoint_interval =
(self.adversarial_checkpoint_interval as f64 / ai) as usize;
}
// Regularization family
let ri = self.regularization_intensity;
self.dropout_initial = (self.dropout_initial * ri).clamp(0.0, 0.5);
self.spectral_norm_sigma_max *= ri;
self.stochastic_depth_prob = (self.stochastic_depth_prob * ri).clamp(0.0, 0.5);
self.spectral_decoupling_lambda *= ri;
// Data augmentation family
let di = self.augmentation_intensity;
self.feature_mask_fraction = (self.feature_mask_fraction * di).clamp(0.0, 0.8);
self.feature_noise_scale *= di;
// Loss shaping family
let li = self.loss_shaping_intensity;
self.asymmetric_dd_weight *= li;
self.regret_blend = (self.regret_blend * li).clamp(0.0, 1.0);
self.trade_clustering_penalty *= li;
self.position_entropy_weight *= li;
// Ensemble family
let ei = self.ensemble_intensity;
self.ensemble_disagreement_penalty *= ei;
// Causal family
let ci = self.causal_intensity;
self.causal_weight *= ci;
if ci > 0.01 {
self.causal_intervention_interval =
(self.causal_intervention_interval as f64 / ci) as usize;
}
}
/// Create conservative hyperparameters suitable for testing and development.
/// WARNING: These are NOT optimized for production. Use hyperopt results instead.
/// After DQN hyperopt completes, update ml/hyperparams/dqn_best.toml with optimal values.
@@ -1525,6 +1589,14 @@ impl DQNHyperparameters {
enable_gradient_vaccine: true, // #32: mathematical overfit guarantee (one production path)
bottleneck_dim: 2, // #31: 2D information compression (gem of gems)
stochastic_depth_prob: 0.2, // #21: 20% drop probability per hidden layer
mixup_alpha: 0.2, // Manifold Mixup: Beta(0.2,0.2) target distribution mixing
// Family intensity scalars: 1.0 = no change to defaults (neutral)
adversarial_intensity: 1.0,
regularization_intensity: 1.0,
augmentation_intensity: 1.0,
loss_shaping_intensity: 1.0,
ensemble_intensity: 1.0,
causal_intensity: 1.0,
asymmetric_dd_weight: 0.5, // #18: extra loss on Q-overestimation in drawdown
// Wave 16 Portfolio Features (default: ALL ENABLED)
@@ -1549,6 +1621,7 @@ impl DQNHyperparameters {
per_alpha: 0.6,
per_beta_start: 0.6, // Higher start → faster IS weight correction (was 0.4)
regime_replay_decay: 0.3, // Regime-aware replay: bias toward current regime
// Wave 2.1: Dueling Networks (WAVE 6.4: ENABLED BY DEFAULT)
dueling_hidden_dim: 128, // Default: 128 hidden units
@@ -1635,6 +1708,7 @@ impl DQNHyperparameters {
qr_kappa: 1.0, // Default: 1.0 (standard quantile Huber loss)
iqn_lambda: 0.25, // Default: mild IQN regularization alongside C51
spectral_norm_sigma_max: 3.0, // Default: permits Xavier scaling [1.0, 10.0]
spectral_decoupling_lambda: 0.01, // Default: mild logit magnitude penalty (Pezeshki 2021)
// Phase 3: GPU experience collection
gpu_n_episodes: 256, // Floor: optimal_n_episodes() scales up dynamically (H100→8192)
@@ -1709,8 +1783,6 @@ impl DQNHyperparameters {
dt_num_layers: 3,
dt_target_return: 2.0,
// CVaR action selection: enabled by default (risk-aware IQN action scoring)
use_cvar_action_selection: true,
cvar_alpha: 0.05, // Optimize for worst 5% quantile tail
// Contract specification: ES futures defaults
@@ -1801,7 +1873,6 @@ pub(crate) fn dqn_default_config() -> DQNConfig {
// Target Network Updates
target_update_freq: 1,
use_soft_updates: true,
tau: 0.001,
tau_final: 0.0001,
tau_anneal_steps: 100_000,
@@ -1815,7 +1886,6 @@ pub(crate) fn dqn_default_config() -> DQNConfig {
n_steps: 3,
// Stability & Safety
enable_q_value_clipping: true,
q_value_clip_min: -5.0, // Reward v4: Q-values in [-0.5, +0.5], 10x headroom
q_value_clip_max: 5.0,
gradient_collapse_multiplier: 100.0,
@@ -1826,11 +1896,9 @@ pub(crate) fn dqn_default_config() -> DQNConfig {
// CQL + IQN (2026 modernization)
cql_alpha: 1.0,
use_iqn: true,
iqn_num_quantiles: 64,
iqn_kappa: 1.0,
iqn_embedding_dim: 64,
use_cvar_action_selection: true,
cvar_alpha: 0.05,
minimum_profit_factor: 1.5,

View File

@@ -201,6 +201,8 @@ impl FusedTrainingCtx {
weight_decay: hyperparams.weight_decay as f32,
max_grad_norm: resolved_grad_norm as f32,
spectral_norm_sigma_max: hyperparams.spectral_norm_sigma_max as f32,
spectral_decoupling_lambda: hyperparams.spectral_decoupling_lambda as f32,
mixup_alpha: hyperparams.mixup_alpha as f32,
iqn_lambda: hyperparams.iqn_lambda as f32,
iqn_num_quantiles: if hyperparams.iqn_lambda > 0.0 { hyperparams.num_quantiles } else { 0 },
iqn_embedding_dim: dqn.config.iqn_embedding_dim,
@@ -634,20 +636,18 @@ impl FusedTrainingCtx {
{
let dqn = agent.primary_dqn_mut();
if dqn.config.use_soft_updates {
let tau = compute_cosine_annealed_tau(
dqn.get_training_steps(),
dqn.config.tau,
dqn.config.tau_final,
dqn.config.tau_anneal_steps,
);
let tau = compute_cosine_annealed_tau(
dqn.get_training_steps(),
dqn.config.tau,
dqn.config.tau_final,
dqn.config.tau_anneal_steps,
);
self.trainer.target_ema_update(
&self.online_dueling, &self.online_branching,
&mut self.target_dueling, &mut self.target_branching,
tau as f32,
).map_err(|e| anyhow::anyhow!("GPU EMA target update: {e}"))?;
}
self.trainer.target_ema_update(
&self.online_dueling, &self.online_branching,
&mut self.target_dueling, &mut self.target_branching,
tau as f32,
).map_err(|e| anyhow::anyhow!("GPU EMA target update: {e}"))?;
}
// ── Step 3b: Attention forward + backward + Adam (post-graph) ─────
@@ -739,16 +739,14 @@ impl FusedTrainingCtx {
// IQN target EMA (same schedule as DQN)
let dqn = agent.primary_dqn_mut();
if dqn.config.use_soft_updates {
let tau = compute_cosine_annealed_tau(
dqn.get_training_steps(),
dqn.config.tau,
dqn.config.tau_final,
dqn.config.tau_anneal_steps,
);
iqn.target_ema_update(tau as f32)
.map_err(|e| anyhow::anyhow!("IQN EMA update: {e}"))?;
}
let tau = compute_cosine_annealed_tau(
dqn.get_training_steps(),
dqn.config.tau,
dqn.config.tau_final,
dqn.config.tau_anneal_steps,
);
iqn.target_ema_update(tau as f32)
.map_err(|e| anyhow::anyhow!("IQN EMA update: {e}"))?;
}
Err(e) => {
tracing::warn!("IQN step failed (non-fatal): {e}");

View File

@@ -24,7 +24,6 @@ use crate::dqn::reward::{RewardConfig, RewardFunction};
use crate::features::microstructure_features::*;
use crate::labeling::triple_barrier::TripleBarrierEngine;
use crate::memory_optimization::auto_batch_size::{AutoBatchSizer, BatchSizeConfig};
use crate::trainers::TargetUpdateMode;
use crate::TrainingMetrics;
use super::super::config::{DQNAgentType, DQNHyperparameters};
use super::DQNTrainer;
@@ -268,7 +267,6 @@ impl DQNTrainer {
tau: hyperparams.tau,
tau_final: hyperparams.tau * 0.1, // Anneal to 10% of base tau
tau_anneal_steps: 100_000,
use_soft_updates: matches!(hyperparams.target_update_mode, TargetUpdateMode::Soft),
// Rainbow DQN warmup period
warmup_steps: hyperparams.warmup_steps,
@@ -297,7 +295,6 @@ impl DQNTrainer {
noisy_sigma_init: hyperparams.noisy_sigma_init, // Rainbow DQN standard: 0.5
// BUG #37 FIX: Q-value clipping (prevents step-level explosions)
enable_q_value_clipping: true,
q_value_clip_min: -500.0,
q_value_clip_max: 500.0,
@@ -312,7 +309,6 @@ impl DQNTrainer {
iqn_lambda: hyperparams.iqn_lambda as f32, // IQN dual-head loss weight (f64→f32)
branch_hidden_dim: hyperparams.branch_hidden_dim,
use_regime_conditioning: true, // Always enable per-regime IS weights for branching loss
use_cvar_action_selection: hyperparams.use_cvar_action_selection,
cvar_alpha: hyperparams.cvar_alpha,
#[allow(clippy::cast_possible_truncation)]
@@ -321,7 +317,6 @@ impl DQNTrainer {
dropout_rate: if hyperparams.enable_dropout_scheduler { hyperparams.dropout_initial } else { 0.0 },
entropy_coefficient: hyperparams.entropy_coefficient,
noisy_epsilon_floor: hyperparams.noisy_epsilon_floor.unwrap_or(0.0) as f32, // C2: NoisyNet handles exploration
use_count_bonus: hyperparams.count_bonus_coefficient.unwrap_or(0.0) > 0.0, // C3 FIX: enable when coefficient > 0
count_bonus_coefficient: hyperparams.count_bonus_coefficient.unwrap_or(0.0),
..DQNConfig::default()
};

View File

@@ -570,7 +570,6 @@ fn test_c3_search_space_is_22d() {
assert!(!names.contains(&"branch_hidden_dim"), "branch_hidden_dim should be fixed, not in search space");
assert!(!names.contains(&"curiosity_weight"), "curiosity_weight should be fixed, not in search space");
assert!(!names.contains(&"her_ratio"), "her_ratio should be fixed, not in search space");
assert!(!names.contains(&"use_cvar_action_selection"), "use_cvar_action_selection should be fixed, not in search space");
assert!(!names.contains(&"cvar_alpha"), "cvar_alpha should be fixed, not in search space");
}

View File

@@ -100,6 +100,7 @@ pub struct ReplayBufferSection {
pub min_replay_size: Option<usize>,
pub per_alpha: Option<f64>,
pub per_beta_start: Option<f64>,
pub regime_replay_decay: Option<f64>,
}
/// C51 distributional RL parameters.
@@ -179,6 +180,14 @@ pub struct GeneralizationSection {
pub position_entropy_weight: Option<f64>,
pub trade_clustering_penalty: Option<f64>,
pub ensemble_disagreement_penalty: Option<f64>,
pub mixup_alpha: Option<f64>,
// Family intensity scalars for hyperopt (1.0 = defaults, range [0.0, 2.0])
pub adversarial_intensity: Option<f64>,
pub regularization_intensity: Option<f64>,
pub augmentation_intensity: Option<f64>,
pub loss_shaping_intensity: Option<f64>,
pub ensemble_intensity: Option<f64>,
pub causal_intensity: Option<f64>,
}
/// Risk management and position-control parameters.
@@ -750,6 +759,9 @@ impl DqnTrainingProfile {
if let Some(v) = rb.per_beta_start {
hp.per_beta_start = v;
}
if let Some(v) = rb.regime_replay_decay {
hp.regime_replay_decay = v;
}
}
// [distributional]
@@ -839,6 +851,14 @@ impl DqnTrainingProfile {
if let Some(v) = g.position_entropy_weight { hp.position_entropy_weight = v; }
if let Some(v) = g.trade_clustering_penalty { hp.trade_clustering_penalty = v; }
if let Some(v) = g.ensemble_disagreement_penalty { hp.ensemble_disagreement_penalty = v; }
if let Some(v) = g.mixup_alpha { hp.mixup_alpha = v; }
// Family intensity scalars
if let Some(v) = g.adversarial_intensity { hp.adversarial_intensity = v; }
if let Some(v) = g.regularization_intensity { hp.regularization_intensity = v; }
if let Some(v) = g.augmentation_intensity { hp.augmentation_intensity = v; }
if let Some(v) = g.loss_shaping_intensity { hp.loss_shaping_intensity = v; }
if let Some(v) = g.ensemble_intensity { hp.ensemble_intensity = v; }
if let Some(v) = g.causal_intensity { hp.causal_intensity = v; }
}
// [risk]

View File

@@ -222,7 +222,6 @@ mod tests {
config.batch_size = 4;
config.min_replay_size = 4;
config.warmup_steps = 0;
config.use_iqn = false;
config.epsilon_start = 0.5;
config
}

View File

@@ -88,7 +88,6 @@ fn test_full_iqn_cql_training_loop() {
config.state_dim = 8;
config.num_actions = 3;
config.hidden_dims = vec![32, 16];
config.use_iqn = true;
config.iqn_num_quantiles = 16;
config.cql_alpha = 1.0;
config.batch_size = 8;
@@ -139,7 +138,6 @@ fn test_iqn_only_no_cql() {
config.state_dim = 8;
config.num_actions = 3;
config.hidden_dims = vec![16, 16];
config.use_iqn = true;
config.iqn_num_quantiles = 8;
config.batch_size = 4;
config.min_replay_size = 4;
@@ -168,11 +166,9 @@ fn test_cvar_action_selection_integration() {
config.state_dim = 8;
config.num_actions = 3;
config.hidden_dims = vec![16, 16];
config.use_iqn = true;
config.iqn_num_quantiles = 8;
config.epsilon_start = 0.0;
config.warmup_steps = 0;
config.use_cvar_action_selection = true;
config.cvar_alpha = 0.05;
let mut dqn = DQN::new(config).unwrap();

File diff suppressed because it is too large Load Diff