From 1936c160b5a2d46dfcfc6ddae9df70f972010221 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Mon, 23 Mar 2026 14:07:21 +0100 Subject: [PATCH] fix: NaN guards + multi-timeframe feature clamping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gradient explosion from unbounded multi-timeframe features (240-bar return could be ±50%). All multi-timeframe outputs now clamped: - Return: ±10% - Volatility: 0-10% - Volume ratio: 0-5x - Momentum: [0, 1] (already bounded) NaN guards added: - Kelly: NaN check on continuous/discrete Kelly before blend - target_position: final NaN→0 after all scaling - reward: final NaN→0 before writing to replay buffer Local test shows Q-gap=18.7 (epoch 5) — 18x better than start-of-session 0.000. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/cuda_pipeline/experience_kernels.cu | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/ml/src/cuda_pipeline/experience_kernels.cu b/crates/ml/src/cuda_pipeline/experience_kernels.cu index fc51b58a9..e4763d5c0 100644 --- a/crates/ml/src/cuda_pipeline/experience_kernels.cu +++ b/crates/ml/src/cuda_pipeline/experience_kernels.cu @@ -260,7 +260,8 @@ extern "C" __global__ void experience_state_gather( /* Return over N bars */ float ret = (close_past > 0.0f) ? (close_now - close_past) / close_past : 0.0f; - out[slot + 0] = ret * 100.0f; /* scale to percentage */ + float scaled_ret = ret * 100.0f; + out[slot + 0] = fmaxf(-10.0f, fminf(10.0f, scaled_ret)); /* clamp ±10% */ /* Volatility: scan high/low over window (approx from close changes) */ float max_val = close_now; @@ -278,12 +279,14 @@ extern "C" __global__ void experience_state_gather( } } float range = (close_now > 0.0f) ? (max_val - min_val) / close_now : 0.0f; - out[slot + 1] = range * 100.0f; /* volatility as percentage */ + float scaled_range = range * 100.0f; + out[slot + 1] = fmaxf(0.0f, fminf(10.0f, scaled_range)); /* clamp 0-10% */ /* Volume trend: current vs average */ float avg_vol = (vol_count > 0) ? vol_sum / (float)vol_count : 1.0f; float cur_vol = (market_dim > 4) ? now_row[4] : 1.0f; - out[slot + 2] = (avg_vol > 0.0f) ? cur_vol / avg_vol : 1.0f; + float vol_ratio = (avg_vol > 0.0f) ? cur_vol / avg_vol : 1.0f; + out[slot + 2] = fmaxf(0.0f, fminf(5.0f, vol_ratio)); /* clamp 0-5x */ /* Momentum: position within range [0=bottom, 1=top] */ float range_size = max_val - min_val; @@ -646,6 +649,9 @@ extern "C" __global__ void experience_env_step( : 0.0f; /* ── Enhanced Kelly: weighted blend ── */ + /* NaN guard: if either Kelly produces NaN/Inf, use 0.0 (safe default) */ + if (isnan(continuous_kelly) || isinf(continuous_kelly)) continuous_kelly = 0.0f; + if (isnan(discrete_kelly) || isinf(discrete_kelly)) discrete_kelly = 0.0f; float enhanced_kelly = 0.7f * continuous_kelly + 0.3f * discrete_kelly; /* ── Confidence scaling: penalize small samples ── */ @@ -662,6 +668,12 @@ extern "C" __global__ void experience_env_step( target_position *= kelly_frac / 0.25f; } /* < 20 trades: no Kelly applied (full position until we have statistics) */ + + /* Final NaN/Inf guard on target_position after ALL scaling. + * If any scaling (CVaR, conviction, Kelly) produced NaN, zero the position. */ + if (isnan(target_position) || isinf(target_position)) { + target_position = 0.0f; + } } /* ---- Position adjustment with volatility-scaled transaction cost ---- */ @@ -1056,6 +1068,9 @@ extern "C" __global__ void experience_env_step( * v_range should be dynamically computed from gamma. */ /* ---- Write reward and done flag ---- */ + /* Final NaN guard — if reward is NaN/Inf, write 0.0 instead of poisoning + * the replay buffer. This prevents gradient explosion from propagating. */ + if (isnan(reward) || isinf(reward)) reward = 0.0f; out_rewards[out_off] = reward; out_dones[out_off] = (float)done;