#![allow(unsafe_code)] // Required for CudaSlice reinterpret-cast in GPU PER insert path
//! DQN main training loop — `train_with_data_full_loop_slices`.
//!
//! The main loop delegates to focused helper methods on `DQNTrainer`:
//! - `log_training_config`: one-time Rainbow/target-update logging
//! - `init_from_fxcache`: upload full dataset to GPU once (replaces per-fold init_gpu_data)
//! - `init_gpu_raw_buffers_from_slices`: upload raw cudarc targets/features (Phase 1b)
//! - `init_gpu_experience_collector`: build the zero-roundtrip CUDA collector (Phase 1c)
//! - `collect_gpu_experiences_slices`: run the GPU experience collection kernel (Phase 3)
//! - `run_training_steps_slices`: batched training from replay buffer with guard kernel
//! - `process_epoch_boundary`: single readback + safety checks at epoch end
//! - (sync_gpu_weights deleted — zero-copy: collector reads trainer's params_buf directly)
//! - `refresh_stale_per_priorities`: M2 PER staleness refresh
//! - `log_epoch_metrics_and_financials`: logging, Prometheus, QuestDB, VaR/CVaR
//! - `handle_epoch_checkpoints_and_early_stopping`: best-checkpoint + early stopping

use std::sync::Arc;

use anyhow::{Context, Result};
use cudarc::driver::{CudaSlice, DevicePtrMut};
use common::metrics::{questdb_sink, training_metrics};
use tracing::{debug, info, warn};

use crate::cuda_pipeline::mapped_pinned::MappedI32Buffer;
use crate::fxcache::TARGET_RAW_CLOSE;
use crate::cuda_pipeline::gpu_dqn_trainer::{
    EPOCH_IDX_INDEX, EPSILON_EFF_INDEX, GAMMA_DIR_EFF_INDEX, Q_ABS_REF_INDEX,
    Q_DIR_ABS_REF_INDEX, TLOB_REGIME_FOCUS_EMA_INDEX,
};
use crate::cuda_pipeline::sp5_isv_slots::{
    NOISY_SIGMA_BASE,
    ADAM_BETA1_BASE as SP5_ADAM_BETA1_BASE,
    ADAM_BETA2_BASE as SP5_ADAM_BETA2_BASE,
    ADAM_EPS_BASE   as SP5_ADAM_EPS_BASE,
};
use crate::dqn::logging::{log_epoch_start, log_epoch_end, log_training_progress};
use crate::dqn::target_update::convergence_half_life;
use crate::evaluation::metrics::calculate_var_cvar;
use crate::trainers::TargetUpdateMode;
use crate::TrainingMetrics;
use super::super::financials::compute_epoch_financials;
use super::super::monitoring::TrainingMonitor;
use super::DQNTrainer;

/// SP12 v3 (2026-05-04): per-epoch min-hold soft-penalty temperature schedule.
///
/// Computes the temperature T(e) controlling the smoothness of the
/// `deficit/(deficit + T)` saturation factor inside the
/// `experience_env_step` kernel. Annealing curriculum:
/// `T(e) = T_end + (T_start - T_end) × exp(-e / decay)`
///
/// With Phase 1 anchors `T_start=50.0`, `T_end=5.0`, `decay=20.0`
/// (state_layout.cuh::MIN_HOLD_TEMPERATURE_{START,END,DECAY}):
///
/// | epoch | T   | regime           |
/// |------:|----:|------------------|
/// | 0     | 50  | forgiving        |
/// | 20    | 21  | mid-transition   |
/// | 50    | 9   | sharp            |
/// | 100   | 5   | full-MFT enforce |
///
/// Per spec §design-2 — early training needs forgiveness so the policy
/// can learn reward gradients on short trades; late training needs
/// sharpness so the min-hold penalty bites and enforces commitment.
/// Constants live in CUDA headers; this Rust mirror duplicates them
/// (kernel side keeps T as a launch scalar so we can switch to an
/// ISV-driven formulation in Phase 2 without recompiling cubin).
pub(crate) fn min_hold_temperature_for_epoch(epoch: usize) -> f32 {
    const T_START: f32 = 50.0;
    const T_END: f32 = 5.0;
    const DECAY_RATE: f32 = 20.0;
    T_END + (T_START - T_END) * (-(epoch as f32) / DECAY_RATE).exp()
}

/// Normalized Shannon entropy of a discrete probability distribution.
///
/// Returns the entropy divided by log(N) where N is the number of bins, so
/// the result is in [0.0, 1.0]: 0.0 = policy fully collapsed to one bin,
/// 1.0 = uniform. Inputs need not sum to exactly 1 — the function
/// re-normalizes defensively.
fn shannon_entropy_normalized(dist: &[f32]) -> f32 {
    let sum: f32 = dist.iter().sum();
    if sum <= 0.0 || dist.len() < 2 {
        return 0.0;
    }
    let mut h = 0.0_f32;
    for &p in dist {
        let q = p / sum;
        if q > 1e-9 {
            h -= q * q.ln();
        }
    }
    h / (dist.len() as f32).ln()
}

/// SP13 P0b aux_w deficit + stagnation controller (extracted for testability).
///
/// Replaces the SP11-era inverted formula
///   `aux_w = (0.1 × learning_health × (1 - tanh(0.1 × sharpe))).clamp(0.05, 0.3)`
/// which lowered aux_w when sharpe was high (exact wrong direction for the
/// failure mode where high sharpe + low WR signals structural-extraction trap).
///
/// Mechanism per spec §"Change 5":
/// ```text
///   deficit       = max(0, target - short)
///   improvement   = max(0, short - long)
///   stagnation    = (deficit > 0.005) ? clamp(1 - improvement/deficit, 0, 1) : 0
///   aux_w_raw     = AUX_W_BASE × (1 + 5 × deficit) × (1 - 0.7 × stagnation)
///   aux_w         = clamp(aux_w_raw, [0.3 × base, 3.0 × base])
/// ```
///
/// The 0.005 deficit-floor on the stagnation gate avoids divide-by-near-zero
/// when at/above target — at that point we are satisfied, not stagnant, so
/// stagnation is forced to 0 and the (1 + 5 × deficit) term is also ≈ 1, so
/// `aux_w` collapses cleanly to the base.
///
/// SP14 A.3: stagnation gate skips epoch 0 of each fold (Pearl-A first-
/// observation bootstrap collapses short/long EMAs together at fold reset,
/// making `improvement = max(0, short - long) = 0` by construction; the gate
/// would fire on a non-stagnation and crush aux_w by 0.7×). Wait one full
/// epoch for the α=0.3 vs α=0.05 short/long EMA timescale split to produce
/// real `improvement` signal.
pub(crate) fn compute_aux_w_p0b(
    target: f32,
    short: f32,
    long: f32,
    epochs_in_fold: usize,
) -> f32 {
    use crate::cuda_pipeline::sp13_isv_slots::{
        AUX_W_BASE, AUX_W_DEFICIT_GAIN, AUX_W_STAGNATION_DECAY,
        AUX_W_HARD_FLOOR_RATIO, AUX_W_HARD_CEIL_RATIO,
    };
    let deficit     = (target - short).max(0.0);
    let improvement = (short - long).max(0.0);
    // SP14 A.3: skip stagnation in epoch 0 of each fold. Pearl-A first-
    // observation bootstrap forces short_ema = long_ema at fold reset,
    // making improvement = 0 by construction; the gate would fire on a
    // non-stagnation. Wait one full epoch for the α=0.3 vs α=0.05 EMA
    // timescale split to produce real improvement signal.
    let stagnation = if epochs_in_fold < 1 {
        0.0_f32
    } else if deficit > 0.005 {
        (1.0 - improvement / deficit).clamp(0.0, 1.0)
    } else {
        0.0
    };
    let aux_w_raw = AUX_W_BASE
        * (1.0 + AUX_W_DEFICIT_GAIN * deficit)
        * (1.0 - AUX_W_STAGNATION_DECAY * stagnation);
    aux_w_raw.clamp(
        AUX_W_BASE * AUX_W_HARD_FLOOR_RATIO,
        AUX_W_BASE * AUX_W_HARD_CEIL_RATIO,
    )
}

/// Metrics returned from `process_epoch_boundary` for downstream logging.
pub(crate) struct EpochBoundaryMetrics {
    pub avg_loss: f32,
    pub avg_grad: f32,
    pub avg_q: f64,
}

/// Metrics returned from `log_epoch_metrics_and_financials` for checkpointing.
pub(crate) struct EpochLogOutput {
    pub avg_loss: f64,
    pub avg_q_value: f64,
    pub avg_grad_norm: f64,
    pub epoch_sharpe: f64,
    pub val_loss: f64,
    pub q_min: f64,
    pub q_max: f64,
    pub q_mean: f64,
}

impl DQNTrainer {
    // (Old `train_with_data_full_loop` removed — `train_with_data_full_loop_slices` is the sole production path)
    // ═══════════════════════════════════════════════════════════════════════
    // Main training loop — fixed-size arrays, zero heap alloc per bar
    // ═══════════════════════════════════════════════════════════════════════

    /// Main training loop accepting `&[([f64; 42], [f64; 6])]`.
    ///
    /// Requires `gpu_data` and `targets_raw_cuda` to already be populated
    /// (via `init_from_fxcache`). The data is already resident on GPU.
    pub(crate) async fn train_with_data_full_loop_slices(
        &mut self,
        training_data: &[([f64; 42], [f64; 6])],
        checkpoint_callback: super::CheckpointCallbackHandle,
    ) -> Result<TrainingMetrics>
    {
        let start_time = std::time::Instant::now();
        let mut total_loss = 0.0;
        let mut total_q_value = 0.0;
        let mut total_gradient_norm = 0.0;
        let mut total_reward = 0.0;
        let mut total_action_counts = [0_usize; 12];
        let mut total_factored_action_counts = [0_usize; 108];

        self.log_training_config().await;

        // Decision Transformer pre-training on offline trajectories
        if self.hyperparams.dt_pretrain_epochs > 0 {
            info!(
                epochs = self.hyperparams.dt_pretrain_epochs,
                context_len = self.hyperparams.dt_context_len,
                embed_dim = self.hyperparams.dt_embed_dim,
                "Starting Decision Transformer pre-training"
            );

            // GPU raw buffers already uploaded via init_from_fxcache
            if self.targets_raw_cuda.is_none() || self.features_raw_cuda.is_none() {
                warn!("DT pre-training: GPU features/targets not available, skipping");
            } else if let Some(ref stream) = self.cuda_stream {
                let stream = Arc::clone(stream);

                let dt_state_dim: usize = 42;

                let dt_config = crate::cuda_pipeline::decision_transformer::DecisionTransformerConfig {
                    state_dim: dt_state_dim,
                    num_actions: 4, // direction branch: Short(0)/Hold(1)/Long(2)/Flat(3)
                    embed_dim: self.hyperparams.dt_embed_dim,
                    num_layers: self.hyperparams.dt_num_layers,
                    num_heads: 4,
                    context_len: self.hyperparams.dt_context_len,
                    dropout: 0.1,
                    batch_size: self.hyperparams.batch_size.min(256),
                };

                let mut dt = crate::cuda_pipeline::decision_transformer::DecisionTransformer::new(
                    stream, dt_config,
                ).map_err(|e| anyhow::anyhow!("DT init: {e}"))?;

                let num_bars = training_data.len();
                if let (Some(ref features_gpu), Some(ref targets_gpu)) =
                    (&self.features_raw_cuda, &self.targets_raw_cuda)
                {
                    let (trajectories, target_actions, num_batches) = dt
                        .build_dt_trajectories(
                            features_gpu,
                            targets_gpu,
                            num_bars,
                            f64::from(self.hyperparams.gamma),
                        )
                        .map_err(|e| anyhow::anyhow!("DT trajectory build: {e}"))?;

                    if num_batches == 0 {
                        warn!(
                            num_bars,
                            context_len = self.hyperparams.dt_context_len,
                            batch_size = dt.config().batch_size,
                            "DT pre-training: not enough data for a full batch, skipping"
                        );
                    } else {
                        let batch_size = dt.config().batch_size;
                        let context_len = dt.config().context_len;
                        let input_dim = dt.config().state_dim + 2;

                        for epoch in 0..self.hyperparams.dt_pretrain_epochs {
                            let mut epoch_loss = 0.0_f32;
                            let mut batch_count = 0_usize;

                            for batch_idx in 0..num_batches {
                                let ep_offset = batch_idx * batch_size;
                                let traj_elem_offset = ep_offset * context_len * input_dim;
                                let act_elem_offset = ep_offset * context_len;

                                let loss = dt.pretrain_step(
                                    &trajectories,
                                    &target_actions,
                                    batch_size,
                                    traj_elem_offset,
                                    act_elem_offset,
                                ).map_err(|e| anyhow::anyhow!("DT pretrain_step: {e}"))?;

                                epoch_loss += loss;
                                batch_count += 1;
                            }

                            let avg_loss = if batch_count > 0 {
                                epoch_loss / batch_count as f32
                            } else {
                                0.0
                            };

                            info!(
                                epoch = epoch + 1,
                                total_epochs = self.hyperparams.dt_pretrain_epochs,
                                avg_loss = format!("{avg_loss:.4}"),
                                batches = batch_count,
                                "DT pre-training"
                            );

                            training_metrics::set_epoch(
                                "dt_pretrain",
                                "loss",
                                avg_loss as f64,
                            );
                        }

                        info!(
                            epochs = self.hyperparams.dt_pretrain_epochs,
                            "DT pre-training complete"
                        );
                    }
                } else {
                    warn!("DT pre-training: GPU features/targets not available, skipping");
                }
            } else {
                warn!("DT pre-training: no CUDA stream available, skipping");
            }
        }

        // v8: Curriculum learning — compute difficulty scores and sort episode starts
        let curriculum_sorted_starts: Option<Vec<i32>> = if self.hyperparams.curriculum_enabled {
            if let (Some(ref collector), Some(ref targets_cuda)) = (&self.gpu_experience_collector, &self.targets_raw_cuda) {
                let total_bars = training_data.len();
                match collector.compute_difficulty_scores(targets_cuda, total_bars) {
                    Ok(scores_gpu) => {
                        let mut scores_cpu = vec![0.0_f32; total_bars];
                        if let Err(e) = collector.stream().memcpy_dtoh(&scores_gpu, &mut scores_cpu) {
                            warn!("Curriculum DtoH failed: {e}");
                            None
                        } else {
                            let mut indices: Vec<i32> = (0..total_bars as i32).collect();
                            indices.sort_by(|&a, &b| {
                                scores_cpu[a as usize].partial_cmp(&scores_cpu[b as usize])
                                    .unwrap_or(std::cmp::Ordering::Equal)
                            });
                            info!("v8: Curriculum learning initialized — {} bars sorted by difficulty", total_bars);
                            Some(indices)
                        }
                    }
                    Err(e) => {
                        warn!("Curriculum scoring failed (using default order): {e}");
                        None
                    }
                }
            } else { None }
        } else { None };

        // Training loop
        for epoch in 0..self.hyperparams.epochs {
            self.current_epoch = epoch;

            // Plan 1 Task 13: write epoch index to ISV for GPU tau/epsilon kernels.
            if let Some(ref fused) = self.fused_ctx {
                fused.write_isv_signal_at(EPOCH_IDX_INDEX, epoch as f32);
            }

            // Plan 1 Task 13: launch GPU tau update kernel at epoch boundary.
            // tau_final = tau * 0.2 is the stability floor (fast early, slow late).
            if let Some(ref fused) = self.fused_ctx {
                let tau_base  = self.hyperparams.tau as f32;
                let tau_final = tau_base * 0.2_f32;
                if let Err(e) = fused.launch_tau_update(tau_base, tau_final) {
                    tracing::warn!(epoch, "launch_tau_update failed (non-fatal): {e}");
                }
            }

            // Plan 1 Task 14: launch GPU epsilon update kernel at epoch boundary.
            if let Some(ref fused) = self.fused_ctx {
                let eps_start = self.hyperparams.epsilon_start as f32;
                let eps_end   = self.hyperparams.epsilon_end as f32;
                if let Err(e) = fused.launch_epsilon_update(eps_start, eps_end) {
                    tracing::warn!(epoch, "launch_epsilon_update failed (non-fatal): {e}");
                }
            }

            // Plan 2 Task 3 D.2: launch GPU per-branch gamma update at epoch boundary.
            // Kernel diverges each branch's gamma from the bootstrapped base value
            // using per-branch V-range spread + health. No CPU-side gamma params needed.
            if let Some(ref fused) = self.fused_ctx {
                if let Err(e) = fused.launch_per_branch_gamma_update() {
                    tracing::warn!(epoch, "launch_per_branch_gamma_update failed (non-fatal): {e}");
                }
            }

            // Plan 1 Task 11: launch GPU kelly_cap update kernel at epoch boundary.
            // Aggregates Kelly win/loss stats from portfolio_states across all envs;
            // writes mean half-Kelly × health-coupled safety to ISV[KELLY_CAP_EFF_INDEX=47].
            if let (Some(ref fused), Some(ref collector)) =
                (&self.fused_ctx, &self.gpu_experience_collector)
            {
                let ps_dev_ptr = collector.portfolio_states_dev_ptr();
                let n_envs     = collector.alloc_episodes() as i32;
                if let Err(e) = fused.launch_kelly_cap_update(ps_dev_ptr, n_envs) {
                    tracing::warn!(epoch, "launch_kelly_cap_update failed (non-fatal): {e}");
                }
                // SP5 Task A6: Pearl 6 reads portfolio_state directly; cross-fold-persistent (NO apply_pearls).
                // Called at the same epoch boundary as launch_kelly_cap_update (same ps_dev_ptr source).
                if let Err(e) = fused.trainer().launch_sp5_pearl_6_kelly(ps_dev_ptr, n_envs) {
                    tracing::warn!(error = %e, "SP5 Pearl 6 producer launch failed");
                }
            }

            // D.8 Plan 2 Task 6C: update TLOB regime focus EMA in ISV at epoch boundary.
            // Reads mean-max attention weight from GpuTlob (DtoH, once/epoch outside hot path).
            // EMA alpha=0.05 for slow smoothing.
            if let Some(ref fused) = self.fused_ctx {
                let mean_max = fused.gpu_tlob.mean_max_attention_weight(fused.batch_size());
                let prev = fused.read_isv_signal_at(TLOB_REGIME_FOCUS_EMA_INDEX);
                let ema = prev * 0.95 + mean_max * 0.05;
                fused.write_isv_signal_at(TLOB_REGIME_FOCUS_EMA_INDEX, ema);
            }

            // Collect pending async validation from PREVIOUS epoch.
            //
            // `pending_val_loss = Some(0.0)` is used as the "launched but not yet
            // consumed" sentinel set by `launch_validation_loss` last epoch. The
            // actual val_loss is computed here by `consume_validation_loss`,
            // which calls `cuEventSynchronize` on the evaluator's `eval_done_event`,
            // reads the mapped-pinned `metrics_buf`, and emits the HEALTH_DIAG
            // `val [...]` + `val_dir_dist` / `val_picked_dir_dist` /
            // `val_plan_isv_diag` lines. The host wait happens HERE — at the
            // start of the next epoch, AFTER training has already submitted its
            // initial epoch-boundary work to `cuda_stream`. Eval has been
            // draining on `validation_stream` since last epoch's launch, so the
            // event is typically already complete; the host wait is a no-op or
            // near-no-op in steady state. This is the point of the pipelining
            // (see `gpu_backtest_evaluator::launch_metrics_and_record_event`).
            if self.pending_val_loss.take().is_some() {
                match self.consume_validation_loss().await {
                    Ok(val) => {
                        self.cached_async_val_loss = Some(val);
                    }
                    Err(e) => {
                        tracing::warn!("Async validation consume failed: {e}");
                    }
                }
            }

            self.reset_epoch_state(epoch);

            // G9: Set regime dropout epoch seed so dropout pattern changes per epoch.
            // Plan 5 Task 5 Phase B: mix the global per-run seed (FOXHUNT_SEED, default 42) so
            // multi-seed Argo jobs produce divergent dropout masks. Cast to i32 (kernel signature);
            // the wrap is fine — we only need per-epoch + per-seed diversity, not full 64-bit entropy.
            if let Some(ref mut fused) = self.fused_ctx {
                let seed = (crate::cuda_pipeline::mix_seed(epoch as u64) as i32).wrapping_abs();
                fused.set_regime_dropout_seed(seed);
            }

            // C51 alpha — set BEFORE first graph capture only. After capture, alpha is baked
            // into the CUDA graph and set_c51_alpha() is a silent no-op.
            // WARNING: c51_warmup_epochs > 0 will NOT work — the ramp never takes effect
            // because the graph captures epoch 0's alpha permanently.
            if epoch == 0 {
                let alpha_max = self.hyperparams.c51_alpha_max;
                if self.hyperparams.c51_warmup_epochs > 0 {
                    tracing::warn!("c51_warmup_epochs={} is set but WILL NOT WORK — alpha is baked at graph capture time. Using alpha_max={} from epoch 0.",
                        self.hyperparams.c51_warmup_epochs, alpha_max);
                }
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.set_c51_alpha(alpha_max);
                }
            }

            // Q-gap warmup
            if self.hyperparams.q_gap_threshold > 0.0 {
                let warmup_epochs = 5.0_f32;
                let ramp = (epoch as f32 / warmup_epochs).min(1.0);
                let ramped_threshold = self.hyperparams.q_gap_threshold as f32 * ramp;
                if let Some(ref mut selector) = self.gpu_action_selector {
                    selector.set_q_gap_threshold(ramped_threshold);
                }
            }

            // Cosine LR schedule with warm restarts (SGDR, T_restart=20 epochs).
            // LR decays from base → 10% of base over T epochs, then resets.
            // At restart: half Adam momentum (preserve direction, reduce magnitude).
            {
                let t_restart: usize = 20;
                let t_local = (epoch - self.backtracking.cosine_epoch_offset) % t_restart;
                let lr_mult = {
                    let cosine = 0.5 * (1.0 + (std::f64::consts::PI * t_local as f64 / t_restart as f64).cos());
                    cosine.max(0.1)
                };
                let base_lr = self.hyperparams.learning_rate;
                let effective_lr = (base_lr * lr_mult) as f32;
                // Plan C Phase 2 follow-up K (2026-04-29): record the
                // cosine-effective LR as the baseline that the per-step
                // warmup-factor consumer composes with (it derives
                // lr_eff = effective_lr × max(0.05, ISV[130]) and writes
                // via set_lr each step). The set_lr call here remains
                // for the case where the very first step fires before
                // the warmup-factor producer has run (factor=0 cold-
                // start, but consumer applies floor=0.05 → lr_eff =
                // effective_lr × 0.05, dampened as intended).
                self.cosine_effective_lr_base = effective_lr;
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.set_lr(effective_lr);
                    if t_local == 0 && epoch > 0 {
                        if let Err(e) = fused.scale_adam_momentum(0.5) {
                            tracing::warn!(epoch, "Cosine LR restart: momentum scale failed: {e}");
                        } else {
                            tracing::info!(epoch, effective_lr, "Cosine LR warm restart: momentum halved, LR reset to base");
                        }
                    }
                }
            }

            // Plan 1 Task 9: GPU-driven atom position recompute (4-branch, single launch).
            if let Some(ref fused) = self.fused_ctx {
                if let Err(e) = fused.launch_atoms_update() {
                    tracing::warn!(epoch, "launch_atoms_update failed: {e}");
                }
            }

            log_epoch_start(epoch + 1, self.hyperparams.epochs, self.hyperparams.learning_rate);
            training_metrics::set_epoch("dqn", "current", (epoch + 1) as f64);

            let mut monitor = TrainingMonitor::new(epoch + 1);

            self.portfolio_tracker.reset_drawdown_tracking();

            let epoch_start = std::time::Instant::now();

            // ── Phase 1: GPU data already uploaded via init_from_fxcache ──
            let phase1_start = std::time::Instant::now();
            // Assert gpu_data + raw buffers are pre-populated (no re-upload)
            if self.gpu_data.is_none() {
                return Err(anyhow::anyhow!(
                    "train_with_data_full_loop_slices requires gpu_data (call init_from_fxcache first)"
                ));
            }
            if self.targets_raw_cuda.is_none() {
                return Err(anyhow::anyhow!(
                    "train_with_data_full_loop_slices requires targets_raw_cuda (call init_from_fxcache first)"
                ));
            }

            // ── Ensure fused CUDA context exists before experience collection ──
            // The collector reads weights directly from trainer's params_buf via raw pointer.
            // Must be initialized before Phase 1c (collector init) so the shared cuBLAS
            // handle can be passed through to CublasGemmSet inside the experience collector.
            //
            // On CUDA builds, fused_ctx is mandatory: there is no CPU fallback path and
            // every downstream stage (experience collector, PER gather, gradient kernels)
            // reads from buffers owned by FusedTrainingCtx. If construction fails (CUDA
            // OOM, driver error, etc.), we propagate the error instead of logging-and-
            // continuing — a silent `fused_ctx = None` state later surfaces as a
            // misleading "GPU experience collector MUST be active" error that obscures
            // the real CUDA root cause.
            if self.fused_ctx.is_none() && self.device.is_cuda() {
                if let Some(ref stream) = self.cuda_stream {
                    let agent = self.agent.read().await;
                    let ctx = super::super::fused_training::FusedTrainingCtx::new(
                        &self.device, &*agent, &self.hyperparams, self.current_batch_size, std::sync::Arc::clone(stream),
                    )
                        .map_err(|e| anyhow::anyhow!(
                            "Fused CUDA context init failed (cannot recover on CUDA builds — no CPU fallback): {e}"
                        ))?;
                    self.fused_ctx = Some(ctx);
                    drop(agent);
                    // Wire direct-to-trainer gather: PER gather writes directly to trainer buffers.
                    // fused_ctx is guaranteed Some here — we just assigned it above and would
                    // have returned Err otherwise.
                    let fused = self.fused_ctx.as_mut().expect("fused_ctx just assigned");
                    let mut agent_w = self.agent.write().await;
                    agent_w.memory_mut().gpu.set_trainer_buffers(
                        fused.trainer_states_buf_ptr(),
                        fused.trainer_next_states_buf_ptr(),
                        fused.trainer_actions_buf_ptr(),
                        fused.trainer_rewards_buf_ptr(),
                        fused.trainer_dones_buf_ptr(),
                        fused.trainer_is_weights_buf_ptr(),
                        // B1.1b: 8th arg — trainer's aux_nb_label_buf raw_ptr
                        // so PER gather writes -1/0/1 labels directly into
                        // the K=2 softmax CE consumer's i32 buffer.
                        fused.trainer_aux_sign_labels_buf_ptr(),
                        fused.trainer_state_dim_padded(),
                    );
                    fused.set_per_rng_step_dev_ptr(agent_w.memory().gpu.rng_step_dev_ptr());
                }
            }

            // ── Phase 1c: GPU experience collector init (once) ──
            // Requires fused_ctx to be initialized above so the shared cuBLAS handle
            // can be passed in (avoids creating a second independent handle).
            if let Some(ref fused_ctx) = self.fused_ctx {
                let shared_cublas = Arc::clone(fused_ctx.shared_cublas());
                self.init_gpu_experience_collector(shared_cublas).await?;
            }
            let phase1_ms = phase1_start.elapsed().as_secs_f64() * 1000.0;

            // v8: Curriculum — restrict episode starts to easy bars early, expand over training
            if let Some(ref sorted) = curriculum_sorted_starts {
                let warmup_frac = self.hyperparams.curriculum_warmup_fraction as f32;
                let total_epochs = self.hyperparams.epochs.max(1) as f32;
                let epoch_frac = (epoch as f32 / total_epochs).min(1.0);
                let usable_frac = if warmup_frac > 0.0 {
                    (warmup_frac + (1.0 - warmup_frac) * (epoch_frac / warmup_frac).min(1.0)).min(1.0)
                } else {
                    1.0
                };
                let usable_count = ((usable_frac * sorted.len() as f32) as usize).max(100);
                let restricted = &sorted[..usable_count.min(sorted.len())];

                if let Some(ref mut collector) = self.gpu_experience_collector {
                    // Subsample restricted sorted indices as episode starts
                    let alloc_cap = collector.alloc_episodes();
                    let n_episodes = alloc_cap.min(usable_count);
                    let stride = (usable_count / n_episodes).max(1);
                    let episode_starts: Vec<i32> = (0..n_episodes)
                        .map(|i| restricted[(i * stride).min(restricted.len() - 1)])
                        .collect();
                    if let Some(ref stream) = self.cuda_stream {
                        // Mapped pinned staging + DtoD — no HtoD per
                        // `feedback_no_htod_htoh_only_mapped_pinned.md`.
                        // The collector's `episode_starts_buf` is currently a
                        // `CudaSlice<i32>`; if Agent 2's merge reshapes it to
                        // a mapped pinned buffer this site simplifies to a
                        // direct `write_from_slice` (follow-up).
                        use cudarc::driver::DevicePtrMut;
                        let n = episode_starts.len();
                        if let Ok(staging) = unsafe { MappedI32Buffer::new(n) } {
                            staging.write_from_slice(&episode_starts);
                            let dst_buf = collector.episode_starts_buf_mut();
                            let n_bytes = n * std::mem::size_of::<i32>();
                            let (dst_ptr, _g) = dst_buf.device_ptr_mut(stream);
                            unsafe {
                                let _ = cudarc::driver::result::memcpy_dtod_async(
                                    dst_ptr, staging.dev_ptr, n_bytes, stream.cu_stream(),
                                );
                            }
                            let _ = stream.synchronize();
                        }
                    }
                    if epoch % 5 == 0 {
                        debug!(
                            epoch, usable_frac, usable_count,
                            n_episodes, "Curriculum: restricting episode starts"
                        );
                    }
                }
            }

            // Causal feature masking
            {
                let frac = self.hyperparams.feature_mask_fraction;
                self.epoch_feature_mask = None;
                let _ = frac; // suppress unused warning
            }

            // Vol normalization: compute from RAW close returns (targets[0]=close, targets[1]=next_close).
            // training_data features may be z-scored — using those would give vol≈1.0 (wrong).
            // Raw log returns from targets give actual market volatility for reward scaling.
            //
            // P5T5 Phase G fix: previous version used naive two-pass variance
            // `(r - mean).powi(2).sum() / (n-1)` which suffers catastrophic
            // cancellation on millions of small-magnitude returns and floors
            // the floor (1e-8) → inv_vol blows up to 1e8 → return features
            // get inflated 50,000× their intended unit-variance scale (cluster
            // saw label_scale=5253 instead of expected ~1.0).
            //
            // Welford's online algorithm computes variance in a single pass
            // and is numerically stable for any n. Plus explicit sanity-band
            // logging so the value is visible for any future deploy.
            self.epoch_vol_normalizer = if training_data.len() > 20 {
                // Welford's online mean + M2 (sum of squared diffs from mean).
                let mut count: u64 = 0;
                let mut mean: f64 = 0.0;
                let mut m2: f64 = 0.0;
                for w in training_data.windows(2) {
                    // Bug B fix (2026-05-02): post-`5a5dd0fed` `targets[0]` is
                    // log-return-normalized `preproc_close`, not raw_close.
                    // Welford needs raw price ratios — read `TARGET_RAW_CLOSE`
                    // (column 2 per `crate::fxcache::TARGET_*` constants).
                    let prev_close = w[0].1[TARGET_RAW_CLOSE];
                    let curr_close = w[1].1[TARGET_RAW_CLOSE];
                    if prev_close > 0.0 {
                        let r = (curr_close / prev_close).ln();
                        if r.is_finite() {
                            count += 1;
                            let delta = r - mean;
                            mean += delta / count as f64;
                            let delta2 = r - mean;
                            m2 += delta * delta2;
                        }
                    }
                }
                let raw_vol = if count > 1 {
                    (m2 / (count - 1) as f64).sqrt()
                } else {
                    0.0
                };
                // Sanity band: equity-index 1-min bars have per-bar vol of
                // ~1e-4 to ~1e-2. Anything outside [1e-5, 1e-1] indicates the
                // input data isn't actually raw closes (e.g., z-scored, dedup
                // collapse, integer-rounded prices). Log + clamp to a safe
                // default so training scale stays in the architecture's
                // designed range — Phase D's scale-invariant aux head
                // tolerates drift, but the trunk's bottleneck is calibrated
                // for normalized-return-scale ~1.0.
                const VOL_LOW: f64 = 1.0e-5;
                const VOL_HIGH: f64 = 1.0e-1;
                const VOL_DEFAULT: f64 = 5.0e-4; // typical equity-index 1-min
                let final_vol = if raw_vol < VOL_LOW || raw_vol > VOL_HIGH {
                    tracing::warn!(
                        "epoch_vol_normalizer raw value {:.3e} outside sanity band [{:.0e}, {:.0e}] \
                         (n_returns={}, mean={:.3e}); using default {:.3e}. \
                         Likely cause: targets[TARGET_RAW_CLOSE=2] not raw_close on this dataset.",
                        raw_vol, VOL_LOW, VOL_HIGH, count, mean, VOL_DEFAULT
                    );
                    VOL_DEFAULT
                } else {
                    raw_vol
                };
                tracing::info!(
                    target: "epoch_vol_normalizer",
                    "epoch={} vol_normalizer={:.3e} (raw={:.3e}, n_returns={}, mean_return={:.3e})",
                    epoch, final_vol, raw_vol, count, mean
                );
                final_vol as f32
            } else {
                0.0
            };

            // G2: Transaction cost curriculum — sigmoid ramp (gradual start/finish, steep middle)
            // Epoch 0:~3%, 5:~16%, 10:~50%, 15:~84%, 20:~97%, 25:~99.7%
            {
                self.cost_anneal_factor = 1.0 / (1.0 + (-((self.current_epoch as f32 - 10.0) / 3.0)).exp());
                if let Some(ref mut c) = self.gpu_experience_collector {
                    c.set_cost_anneal(self.cost_anneal_factor);
                }
            }

            // ── Phase 2: GPU experience collection ──
            let phase2_start = std::time::Instant::now();
            let gpu_experiences_collected = self.collect_gpu_experiences_slices(
                training_data,
            ).await?;
            let phase2_ms = phase2_start.elapsed().as_secs_f64() * 1000.0;

            if !gpu_experiences_collected {
                return Err(anyhow::anyhow!(
                    "GPU experience collector MUST be active for CUDA training. \
                     No CPU fallback path exists in CUDA builds."
                ));
            }

            // v8: TD(λ) is now wired in collect_experiences_gpu — self-bootstrap with
            // rewards as Q(s') approximation, overwrites n-step when td_lambda > 0.01.

            if let Some(ref mut collector) = self.gpu_experience_collector {
                let count = collector.last_experience_count();

                // Robust PopArt: extract median/IQR from the reward sort buffer.
                // extract_median_iqr reads 3 positions from the sorted buffer (tiny DtoH: 3 floats).
                if self.hyperparams.popart_robust {
                    match collector.extract_median_iqr(count) {
                        Ok((median, iqr)) => {
                            if let Some(ref mut fused) = self.fused_ctx {
                                fused.set_robust_popart_stats(median, iqr);
                            }
                            debug!(epoch, median, iqr, "Robust PopArt: median/IQR cached for epoch");
                        }
                        Err(e) => {
                            debug!("Robust PopArt median/IQR extraction failed (non-fatal): {e}");
                        }
                    }
                }
            }

            // D3/N3: Periodic shrink-and-perturb replaced by health-triggered path in
            // process_epoch_boundary. No periodic trigger here.

            // ── Phase 3: Batched training from replay buffer ──
            let phase3_start = std::time::Instant::now();
            let train_step_count = self.run_training_steps_slices(training_data.len()).await?;
            let phase3_ms = phase3_start.elapsed().as_secs_f64() * 1000.0;

            // Log GPU per-phase CUDA event timing.
            // Events are queried after process_epoch_boundary which does DtoH readbacks
            // that implicitly sync the stream. No explicit cuStreamSynchronize needed.

            // ── Phase 4: Epoch boundary + validation ──
            let phase4_start = std::time::Instant::now();
            let boundary = if train_step_count > 0 {
                let b = self.process_epoch_boundary(epoch, train_step_count, &mut monitor).await?;
                Some(b)
            } else {
                None
            };

            // Zero-copy: collector reads directly from trainer's params_buf — no sync needed.

            // Log GPU per-phase CUDA event timing after epoch boundary (DtoH readbacks sync stream).
            if let Some(ref mut fused) = self.fused_ctx {
                fused.log_phase_timing();
            }

            // PopArt variance shift — kept as a tracing hook. Tau is now fully
            // controlled by the cosine schedule (`compute_cosine_annealed_tau`)
            // combined with `apply_health_coupled_tau_floor` in fused_training.
            // The old "reset tau to base on PopArt shift" knob mutated config.tau
            // which broke the cosine schedule baseline; removed.
            if let Some(ref mut fused) = self.fused_ctx {
                if fused.should_reset_tau() {
                    tracing::info!("PopArt variance shift detected (tau schedule continues)");
                }
            }

            // M3: Q-value diagnostics — disabled, causes hang in single-threaded tokio.
            // Tuple is (gap_stats, per_action_avgs) with per_action_avgs length = 12
            // matching the kernel's dir×mag exposure layout (see metrics.rs).
            let q_diagnostics: Option<((f64, f64, f64), [f64; 12])> = None;

            // Three-Phase Training Pipeline
            let total_epochs = self.hyperparams.epochs;
            let phase3_start_epoch = total_epochs * 80 / 100;
            let in_phase3 = epoch >= phase3_start_epoch && total_epochs > 4;

            if in_phase3 {
                if let Some(ref mut c) = self.gpu_experience_collector {
                    c.set_expert_ratio(0.0);
                }
                if epoch == phase3_start_epoch {
                    if let Some(ref mut fused) = self.fused_ctx {
                        let alpha = self.hyperparams.shrink_perturb_alpha as f32;
                        let sigma = self.hyperparams.shrink_perturb_sigma as f32;
                        if let Err(e) = fused.shrink_and_perturb(alpha, sigma) {
                            tracing::warn!("Shrink-and-Perturb failed at Phase 3 start (non-fatal): {e}");
                        } else {
                            tracing::info!(
                                epoch,
                                phase3_start = phase3_start_epoch,
                                alpha,
                                sigma,
                                "Phase 3 (Refinement): Shrink-and-Perturb applied at phase boundary"
                            );
                        }
                    }
                }
            }

            // Plasticity maintenance: use config-defined interval (not epochs/4)
            // to avoid firing every epoch on short runs (3-5 epochs).
            // The periodic shrink-perturb at line 357 handles this already.

            // CVaR scales from IQN head
            if let Some(ref mut fused) = self.fused_ctx {
                let bs = fused.batch_size();
                let actions_clone = {
                    let actions = fused.actions_buf();
                    actions.clone()
                };
                let cvar_ptr = fused.compute_cvar_device_ptr(&actions_clone, bs, 0.05)
                    .unwrap_or(0);
                if let Some(ref mut collector) = self.gpu_experience_collector {
                    collector.set_cvar_scales(cvar_ptr);
                }
            }

            // ISV signals for adaptive hold enforcement
            if let Some(ref fused) = self.fused_ctx {
                let isv_ptr = fused.isv_signals_dev_ptr();
                if let Some(ref mut collector) = self.gpu_experience_collector {
                    collector.set_isv_signals_ptr(isv_ptr);
                }
            }

            // B.3 Plan 3 Task 8: bootstrap the seed-phase flag from the
            // pinned ISV BEFORE the first `collect_experiences_gpu` call.
            // The trainer's constructor cold-started ISV[SEED_STEPS_DONE]=0
            // and ISV[SEED_STEPS_TARGET]=config.replay_seed_steps; if the
            // target is non-zero, the first epoch starts in seed phase.
            // Subsequent epochs re-evaluate this flag at the per-epoch
            // boundary (see metric-emit block ~3000 lines below).
            if let Some(ref fused) = self.fused_ctx {
                use crate::cuda_pipeline::gpu_dqn_trainer::{
                    SEED_STEPS_DONE_INDEX, SEED_STEPS_TARGET_INDEX,
                };
                let seed_done   = fused.read_isv_signal_at(SEED_STEPS_DONE_INDEX);
                let seed_target = fused.read_isv_signal_at(SEED_STEPS_TARGET_INDEX);
                let in_seed_phase = seed_target > 0.0 && seed_done < seed_target;
                if let Some(ref mut collector) = self.gpu_experience_collector {
                    collector.set_seed_phase_active(in_seed_phase);
                }
                if in_seed_phase {
                    tracing::info!(
                        target: "dqn::B3",
                        seed_target = seed_target,
                        "GPU-only replay seed warm-start: scripted-policy phase begins"
                    );
                }
            }

            // Trade plan params + readiness for plan activation/enforcement
            if let Some(ref fused) = self.fused_ctx {
                let plan_ptr = fused.plan_params_buf_ptr();
                let readiness_ptr = fused.iqn_readiness_dev_ptr();
                if let Some(ref mut collector) = self.gpu_experience_collector {
                    collector.set_plan_params_ptr(plan_ptr);
                    collector.set_readiness_ptr(readiness_ptr);
                }
            }

            // SP11 Fix 39 (2026-05-04, A1.2): wire the per-bar saboteur
            // engagement signal buffer from the experience collector into
            // the trainer. The collector's `experience_env_step` writes
            // the per-bar signal at the END of each bar; the trainer's
            // `launch_sp11_saboteur_engagement_compute` reads via raw
            // device pointer and reduces to a per-step engagement
            // fraction → Pearls A+D → ISV[SABOTEUR_ENGAGEMENT_RATE_INDEX=358].
            // Per `feedback_wire_everything_up`: producer (collector)
            // and consumer (trainer) both wire in the same commit.
            if let Some(ref collector) = self.gpu_experience_collector {
                let (sab_dev_ptr, n_bars) = collector.sp11_saboteur_delta_reward_dev_ptr();
                if let Some(ref mut fused_mut) = self.fused_ctx {
                    fused_mut.trainer_mut()
                        .set_sp11_saboteur_delta_reward_buf(sab_dev_ptr, n_bars);
                }
            }

            // SP11 Fix 39 B1b fix-up (2026-05-04): wire the per-bar popart-
            // component-specific magnitude buffer from the experience
            // collector into the trainer. Mirror of the saboteur Δreward
            // wire-up above. The collector's `experience_env_step` writes
            // `r_popart` to this buffer at the voluntary-exit branch; the
            // trainer's `launch_sp11_popart_component_ema` reads via raw
            // device pointer and reduces to mean(|x|) → Pearls A+D →
            // ISV[POPART_COMPONENT_MAG_EMA_INDEX=360]. Resolves the slot-
            // 63 PopArt-input vs popart-component-magnitude overload that
            // B1b smoke surfaced. Spec §4 amendment "Why slot 360".
            if let Some(ref collector) = self.gpu_experience_collector {
                let (popart_dev_ptr, n_bars) = collector.sp11_popart_component_dev_ptr();
                if let Some(ref mut fused_mut) = self.fused_ctx {
                    fused_mut.trainer_mut()
                        .set_sp11_popart_component_buf(popart_dev_ptr, n_bars);
                }
            }

            // IQR exploration bonus from IQN quantile spread
            if let Some(ref fused) = self.fused_ctx {
                let iqr_ptr = fused.iqn_iqr_ptr().unwrap_or(0);
                let iqr_alpha = self.hyperparams.iqr_exploration_alpha as f32;
                if let Some(ref mut collector) = self.gpu_experience_collector {
                    collector.set_iqr_bonus(iqr_ptr, iqr_alpha);
                }
            }

            // Ensemble variance exploration bonus: high disagreement = high uncertainty = explore
            if let Some(ref fused) = self.fused_ctx {
                let beta = self.hyperparams.ensemble_exploration_beta as f32;
                if let Some((var_ptr, buf_len)) = fused.ensemble_var_ptr() {
                    if let Some(ref mut collector) = self.gpu_experience_collector {
                        collector.set_ensemble_variance_bonus(var_ptr, buf_len, beta);
                    }
                }
            }

            // IQN readiness for quantile Q-select: ramps 0→1 as IQN loss converges.
            // 0=exploring/optimistic (Q_90th), 1=exploiting/pessimistic (Q_10th).
            if let Some(ref fused) = self.fused_ctx {
                let readiness = fused.iqn_readiness();
                let util = fused.utilization_ema();
                if let Some(ref mut collector) = self.gpu_experience_collector {
                    collector.set_iqn_readiness(readiness);
                    collector.set_utilization_ema(util);
                }
            }

            // G4: Gamma annealing — adaptive horizon based on atom utilization.
            //
            // Prior direction (pre-2026-04-24 val-Flat-collapse fix #4):
            //   util > 0.6 → γ += 0.005  (extend horizon when healthy)
            //   util < 0.4 → γ -= 0.010  (shorten horizon when collapsed)
            //
            // That second arm creates a positive-feedback loop: atom
            // collapse (low util) → lower γ → `γ × delta_z` shrinks →
            // TD targets concentrate on fewer bins → deeper collapse.
            // Observed on train-bv2n5: atom util stuck at 1% for 16
            // epochs while this controller quietly nudged γ toward its
            // 0.90 floor, reinforcing the symptom it was meant to
            // mitigate. Per research-agent audit (2026-04-24).
            //
            // Plan 2 D.2: gamma is now GPU-computed per-branch at epoch boundary.
            // per_branch_gamma_update kernel diverges gamma per branch from V-range + health.
            // Log direction-branch gamma as representative value.
            {
                let gamma_eff = if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().read_isv_signal_at(GAMMA_DIR_EFF_INDEX)
                } else {
                    self.hyperparams.gamma
                };
                info!("G4 gamma_dir={:.3} (GPU-computed per-branch, ISV[43])", gamma_eff);
            }

            // Flush GPU-accumulated max priority
            if train_step_count > 0 {
                let mut agent = self.agent.write().await;
                if let Err(e) = agent.flush_max_priority() {
                    debug!("GPU max_priority flush failed (non-fatal): {}", e);
                }
            }
            let phase4_ms = phase4_start.elapsed().as_secs_f64() * 1000.0;

            let epoch_duration = epoch_start.elapsed();

            info!(
                "Epoch {}/{} phase breakdown: init={:.0}ms experience={:.0}ms training={:.0}ms validation={:.0}ms total={:.0}ms",
                epoch + 1, self.hyperparams.epochs,
                phase1_ms, phase2_ms, phase3_ms, phase4_ms,
                epoch_duration.as_secs_f64() * 1000.0,
            );

            let log_output = self.log_epoch_metrics_and_financials(
                epoch,
                train_step_count,
                &boundary,
                &mut monitor,
                epoch_duration,
                &mut total_action_counts,
                &mut total_factored_action_counts,
                q_diagnostics,
            ).await?;

            // ── Plan C T11 follow-up — F1 ep1 baseline diagnostic ─────────
            //
            // Capture the healthy state at the END of F1 ep1 (the epoch
            // immediately preceding the F1 ep2 explosion). Pairs with the
            // per-step FOLD_EXPLOSION_DIAG in the run-training-steps loop:
            // baseline log here records what's normal; per-step block
            // records what's broken — and the deltas pinpoint the kernel.
            // Single unconditional log (cold path, ep boundary).
            //
            // Diagnostic-only — remove with the per-step block once root
            // cause is identified. Not for prod.
            if self.current_fold == 1 && epoch == 0 {
                // Take the immutable-borrow values first, then release the
                // borrow before requesting `&mut` for the Q-range reduce.
                let (c51_loss, mse_loss, iqn_loss, atoms) = {
                    let fused_ref = self.fused_ctx.as_ref()
                        .expect("fused_ctx present at epoch boundary");
                    let (c, m, i) = fused_ref.explosion_diag_loss_components();
                    let a = fused_ref.explosion_diag_atom_range();
                    (c, m, i, a)
                };
                let (q_min, q_mean, q_max) = self.fused_ctx.as_mut()
                    .expect("fused_ctx present at epoch boundary")
                    .explosion_diag_q_range();
                tracing::info!(
                    target: "ml::trainers::dqn::trainer::training_loop",
                    "FOLD_EXPLOSION_DIAG[F1_END_EP1]: \
                     c51={:.4e} mse={:.4e} iqn={:.4e} \
                     q_min={:.3} q_mean={:.3} q_max={:.3} q_range={:.3} \
                     atom_d0=[{:.3},{:.3},dz={:.3}] atom_d2=[{:.3},{:.3},dz={:.3}]",
                    c51_loss, mse_loss, iqn_loss,
                    q_min, q_mean, q_max, (q_max - q_min),
                    atoms[0], atoms[1], atoms[2],
                    atoms[3], atoms[4], atoms[5],
                );
            }

            // Plan 5 Task 2 — A.4 regression-detection hard-stop.
            //
            // Run after the HEALTH_DIAG line was emitted by
            // `log_epoch_metrics_and_financials` so the band check sees the
            // same values that were just written to the log. `harvest_health_diag_metrics`
            // returns a small Vec<(name, value)> of the keys defined in
            // `config/metric-bands.toml` — unknown keys in the toml return
            // `None` from `update_and_check` (silent OK per Invariant 7).
            //
            // When ANY metric crosses the `2N` consecutive-error-band threshold,
            // we log a final TERMINATED_BY_REGRESSION line, emit ERROR-level
            // tracing under target=`dqn::regression`, and return
            // `CommonError::RegressionDetected` so the call stack unwinds
            // cleanly through the trainer's `Drop` impl (which tears down CUDA
            // resources). No SIGTERM raise — the error itself is the signal.
            if self.metric_bands.band_count() > 0 {
                let harvested = self.harvest_health_diag_metrics(epoch, &log_output);
                for (name, value) in harvested {
                    if let Some(term) = self.metric_bands.update_and_check(&name, value) {
                        match term {
                            super::monitoring::TerminationReason::RegressionError {
                                metric, value, band, consecutive,
                            } => {
                                tracing::error!(
                                    target: "dqn::regression",
                                    epoch = epoch,
                                    metric = %metric,
                                    value = value,
                                    consecutive = consecutive,
                                    band_error_low = band.error_low,
                                    band_error_high = band.error_high,
                                    "TERMINATED_BY_REGRESSION: metric `{metric}` outside error band \
                                     [{:.6e}, {:.6e}] for {consecutive} consecutive epochs (value={:.6e})",
                                    band.error_low, band.error_high, value,
                                );
                                tracing::info!(
                                    "HEALTH_DIAG[{}]: TERMINATED_BY_REGRESSION metric={} value={:.6e} \
                                     consecutive={} band_error=[{:.6e}, {:.6e}]",
                                    epoch, metric, value, consecutive, band.error_low, band.error_high,
                                );
                                return Err(anyhow::Error::new(
                                    common::error::CommonError::RegressionDetected {
                                        metric,
                                        value,
                                        band_warn_low: band.warn_low,
                                        band_warn_high: band.warn_high,
                                        band_error_low: band.error_low,
                                        band_error_high: band.error_high,
                                        consecutive,
                                    },
                                ));
                            }
                            super::monitoring::TerminationReason::RegressionWarn { .. } => {
                                // Warn variant currently never returned by
                                // update_and_check (warns only log). Kept for
                                // exhaustive matching.
                            }
                        }
                    }
                }
            }

            total_loss += log_output.avg_loss;
            total_q_value += log_output.avg_q_value;
            total_gradient_norm += log_output.avg_grad_norm;

            let epoch_avg_reward = if !monitor.reward_history.is_empty() {
                monitor.reward_history.iter().sum::<f32>() / monitor.reward_history.len() as f32
            } else {
                0.0
            };
            total_reward += epoch_avg_reward as f64;

            self.handle_epoch_checkpoints_and_early_stopping(
                epoch,
                train_step_count,
                &log_output,
                &checkpoint_callback,
            ).await?;

            // Task 10: Trajectory backtracking -- detect plateau, rewind, perturb
            // Use val_Sharpe (deterministic backtest), NOT epoch_sharpe (training Sharpe).
            // Training Sharpe oscillates even when the model is frozen, preventing detection.
            let val_sharpe_for_backtrack = -log_output.val_loss;  // val_loss = -sharpe

            // G3: Walk-forward validation — MVP logs concept, full multi-window deferred
            // When fully implemented, this replaces val_Sharpe with min-Sharpe across K windows.
            // For now, track single-window val_Sharpe as wf_min_sharpe.
            self.wf_min_sharpe = val_sharpe_for_backtrack;
            info!("G3 walk-forward: min_sharpe={:.2} (single-window MVP, K={} deferred)",
                  self.wf_min_sharpe, self.wf_num_windows);

            // ── Self-Improving Enrichment Phase ──
            {
                let val_bars = self.val_data.len().max(1);

                let eval_trades = super::enrichment::extract_eval_trades_from_metrics(
                    val_sharpe_for_backtrack as f32,
                    60000.0,  // approximate trade count
                    0.0,      // approximate P&L
                    0.5,      // approximate win rate
                    log_output.avg_q_value as f32,  // actual Q-mean from training step
                    val_bars,
                );

                if !eval_trades.is_empty() {
                    let result = super::enrichment::run_enrichments(
                        &mut self.enrichment,
                        &eval_trades,
                        val_sharpe_for_backtrack as f32,
                    );

                    // Apply E2: adaptive epsilon
                    self.hyperparams.epsilon_end = result.epsilon;

                    // Plan 1 Task 10: gamma is GPU-computed at epoch boundary.
                    // Eval-blended gamma_base can override hyperparams if enrichment
                    // provides a suggestion — but the GPU kernel applies health coupling.
                    if let Some(eval_gamma) = result.gamma {
                        // Update the config gamma_base with the eval-blended value
                        // so the next epoch's gamma_update kernel uses the blended base.
                        let blended = 0.9 * self.hyperparams.gamma + 0.1 * eval_gamma;
                        self.hyperparams.gamma = blended.clamp(0.85, 0.98);
                    }

                    // Apply E5: ensemble agreement threshold
                    if let Some(ref mut fused) = self.fused_ctx {
                        fused.set_var_ema(result.agreement_threshold);
                    }
                }
            }

            if self.run_backtracking_epoch_end(epoch, val_sharpe_for_backtrack)? {
                // PLATEAU_EXHAUSTED -- save best model and exit
                info!(
                    "PLATEAU_EXHAUSTED: restoring best model from epoch {} before exit",
                    self.backtracking.best_epoch
                );
                let _ = self.restore_best_gpu_params();
                // Drain any in-flight best-checkpoint workers first so the
                // restored-best ckpt write is the last one observed by the
                // disk/MinIO sink (deterministic ordering).
                self.await_pending_checkpoint_handles().await;
                let checkpoint_data = self.serialize_model().await?;
                // Synchronous callsite (cold path — fires ≤1× per training run on
                // plateau exhaustion). Lock the shared callback handle and invoke
                // inline; the async-checkpoint worker is mutually exclusive with
                // this branch (PLATEAU_EXHAUSTED breaks the epoch loop).
                let _ = checkpoint_callback.lock().expect(
                    "checkpoint_callback poisoned — worker thread panicked",
                )(epoch + 1, checkpoint_data, true);
                break;
            }

            if (epoch + 1) % self.hyperparams.checkpoint_frequency == 0 {
                info!(
                    "Saving periodic checkpoint at epoch {}/{}",
                    epoch + 1, self.hyperparams.epochs
                );
                // Drain async best-checkpoint workers first so the disk
                // sees them before the periodic write — ensures consumers
                // (e.g. MinIO listing) observe a consistent ordering.
                self.await_pending_checkpoint_handles().await;
                let checkpoint_data = self.serialize_model().await?;
                let checkpoint_size = checkpoint_data.len();
                // Synchronous callsite (cold path — fires every
                // `checkpoint_frequency` epochs, default 10). Same shared
                // callback handle as the async worker; the std::sync::Mutex
                // serialises this call against any in-flight worker save.
                let checkpoint_path = checkpoint_callback.lock().expect(
                    "checkpoint_callback poisoned — worker thread panicked",
                )(epoch + 1, checkpoint_data, false)
                    .context("Failed to save periodic checkpoint")?;
                info!(
                    "Periodic checkpoint saved: {} ({} bytes)",
                    checkpoint_path, checkpoint_size
                );
            }
        }

        // Drain any in-flight async validation launched by the last epoch's
        // reporting block. Without this, the LAST epoch's eval kernels are
        // still queued on `validation_stream` when training returns —
        // `eval_done_event` is recorded but never `synchronize`d, so the
        // mapped-pinned mirrors (`actions_history_pinned` etc.) carry the
        // SECOND-to-last epoch's data. Smoke tests / hyperopt that read
        // `last_eval_direction_dist` / `last_eval_magnitude_dist` after
        // training expect the LAST epoch's data — this consume preserves that.
        // Failure is non-fatal: the readers fall back to whatever the previous
        // consume left in the mirrors (one-epoch lag, same as in-loop).
        if self.pending_val_loss.take().is_some() {
            match self.consume_validation_loss().await {
                Ok(val) => {
                    self.cached_async_val_loss = Some(val);
                }
                Err(e) => {
                    tracing::warn!("Final async validation drain failed: {e}");
                }
            }
        }

        // Drain any in-flight async best-checkpoint workers before
        // returning. Guarantees the most recent improvement's safetensors
        // bytes are written to disk/MinIO before the caller observes the
        // returned `TrainingMetrics`. Without this drain, a fold transition
        // (or training-loop drop) could cancel pending writes and leave
        // the "best" file half-written.
        self.await_pending_checkpoint_handles().await;

        let training_duration = start_time.elapsed();

        let metrics = self.create_final_metrics(
            total_loss, total_q_value, total_gradient_norm, total_reward,
            self.hyperparams.epochs, training_duration, false,
            total_action_counts, total_factored_action_counts,
        ).await?;

        {
            let mut stored_metrics = self.metrics.write().await;
            *stored_metrics = metrics.clone();
        }

        info!(
            "Training completed in {:.2}s: final_loss={:.6}, avg_q_value={:.4}",
            training_duration.as_secs_f64(),
            metrics.loss,
            metrics.additional_metrics.get("avg_q_value").unwrap_or(&0.0)
        );

        info!("Best model summary:");
        info!(
            "  Best Sharpe: {:.4} at epoch {} (val_loss={:.6})",
            self.best_sharpe, self.best_epoch, self.best_val_loss
        );
        info!("  Best model checkpoint: best_model.safetensors");

        Ok(metrics)
    }

    // ═══════════════════════════════════════════════════════════════════════
    // Helper: one-time training configuration logging
    // ═══════════════════════════════════════════════════════════════════════

    pub(crate) async fn log_training_config(&mut self) {
        match self.hyperparams.target_update_mode {
            TargetUpdateMode::Soft => {
                let half_life = convergence_half_life(f64::from(self.hyperparams.tau));
                info!("WAVE 16: Using soft target updates (Polyak averaging)");
                info!("  Tau: {}", self.hyperparams.tau);
                info!("  Convergence half-life: {} steps", half_life as usize);
                info!("  Strategy: Smooth Q-value tracking (50-70% variance reduction)");
            },
            TargetUpdateMode::Hard => {
                info!("WAVE 16: Using hard target updates (legacy mode)");
                info!("  Update frequency: every 1000 steps");
                info!("  Warning: Sudden Q-value shifts may cause instability");
            },
        }

        info!("Rainbow DQN Components:");
        info!("  Double DQN (always enabled)");

        if true {
            info!("  Dueling Networks (value/advantage streams, hidden_dim={})", self.hyperparams.dueling_hidden_dim);
        } else {
            info!("  Dueling Networks: disabled");
        }

        if true {
            info!("  Prioritized Experience Replay (a={}, b={}->1.0)",
                  self.hyperparams.per_alpha, self.hyperparams.per_beta_start);
        } else {
            info!("  Prioritized Experience Replay: disabled");
        }

        if self.hyperparams.n_steps > 1 {
            info!("  N-Step Returns (n={})", self.hyperparams.n_steps);
        } else {
            info!("  N-Step Returns (n=1, standard TD)");
        }

        if true {
            info!("  Categorical DQN (atoms={}, V=[{}, {}])",
                  self.hyperparams.num_atoms, self.hyperparams.v_min, self.hyperparams.v_max);
        } else {
            info!("  Categorical DQN / C51: disabled");
        }

        if true {
            info!("  Noisy Networks (sigma_init={})", self.hyperparams.noisy_sigma_init);
        } else {
            info!("  Noisy Networks: disabled");
        }

        {
            // Plan 1 Task 14: read GPU-computed epsilon from ISV[EPSILON_EFF_INDEX].
            // epsilon_update kernel ran at epoch boundary with cosine schedule +
            // health-coupled boost; no CPU-side adaptive computation here.
            let (regime_stab, volatility) = if let Some(ref fused) = self.fused_ctx {
                fused.read_isv_regime()
            } else {
                (0.5, 0.5)
            };

            let adaptive_epsilon = if let Some(ref fused) = self.fused_ctx {
                fused.trainer().read_isv_signal_at(EPSILON_EFF_INDEX)
            } else {
                self.hyperparams.noisy_epsilon_floor.unwrap_or(0.05) as f32
            };
            let mut agent = self.agent.write().await;
            agent.set_epsilon(adaptive_epsilon as f64);

            // ISV-adaptive noisy sigma: volatile → more noise exploration
            let base_sigma = self.hyperparams.noisy_sigma_init as f32;
            let adaptive_sigma = base_sigma * (0.5 + 0.5 * volatility);
            agent.set_noise_sigma_scale(adaptive_sigma as f64);

            info!("  ISV-adaptive: ε={:.4} σ={:.4} (stability={:.2}, vol={:.2})",
                  adaptive_epsilon, adaptive_sigma, regime_stab, volatility);
        }
    }

    // ═══════════════════════════════════════════════════════════════════════
    // Helper: reset per-epoch state
    // ═══════════════════════════════════════════════════════════════════════

    pub(crate) fn reset_epoch_state(&mut self, _epoch: usize) {
        self.metrics_aggregator.reset();
        self.reward_fn.reset_epoch_state();
        self.portfolio_tracker.reset();

        // Reset Q-stats accumulators at epoch START (not end) so
        // log_epoch_metrics_and_financials can read the final values.
        self.epoch_q_min = f32::INFINITY;
        self.epoch_q_max = f32::NEG_INFINITY;
        self.epoch_q_variance = 0.0;
        self.epoch_q_gap = 0.0;
        self.epoch_atom_entropy = 0.0;
        self.epoch_atom_utilization = 0.0;

        // Task 0.9 — adaptive grad-clip "kicked in" latch. Fire detection
        // for the grad_clip controller is INTERVENTION-based: it fires iff
        // at least one step this epoch had raw_grad_norm > adaptive clip
        // value (i.e. the clip actually clamped a gradient). EMA ticks
        // alone do NOT count as fires. See controller_activity smoke test.
        self.grad_clip_kicked_this_epoch = false;

        // Task 2.5 Bug #1 — reset anti-LR multiplier at epoch start. The
        // anti-LR block re-computes `anti_mult` each epoch; if it does not
        // fire, the multiplier is 1.0 and the stale last value must not
        // leak across epochs into fire detection.
        self.last_anti_mult = 1.0;

        // GPU-persistent epoch state: set reset flags instead of CPU state mutation.
        if let Some(ref mut collector) = self.gpu_experience_collector {
            let flags: u32 = 1 | 2; // reset portfolio + DSR normalizer (always enabled)
            collector.set_reset_flags(flags);
        }
    }

    // (Old `init_gpu_data` and `init_gpu_raw_buffers` removed — use `init_from_fxcache` + `init_gpu_raw_buffers_from_slices`)

    /// Upload raw features + targets from contiguous fxcache slices.
    /// No tuple unpacking — flat_map directly from &[[f64; N]] arrays.
    pub(crate) async fn init_gpu_raw_buffers_from_slices(
        &mut self,
        features: &[[f64; 42]],
        targets: &[[f64; 6]],
    ) -> Result<()> {
        if self.targets_raw_cuda.is_some() {
            return Ok(());
        }
        // The mapped-pinned allocator only needs an active CUDA context on this
        // thread; it does not consume the stream. Keep the early-return for the
        // None-stream case to preserve prior semantics.
        if self.cuda_stream.is_none() {
            return Ok(());
        }

        let num_bars = features.len();
        let flat_targets: Vec<f32> = targets.iter()
            .flat_map(|t| t.iter().map(|&v| v as f32))
            .collect();
        let flat_features: Vec<f32> = features.iter()
            .flat_map(|f| f.iter().map(|&v| v as f32))
            .collect();

        // Mapped pinned (cuMemHostAlloc DEVICEMAP): host writes the f32 payload
        // via `host_ptr`; kernels read via `dev_ptr`. No HtoD/DtoD copy — the
        // device pointer aliases the same physical pages as the host pointer,
        // so the kernel sees the data after a stream-sync barrier (mapped-pinned
        // coherence). Replaces `clone_to_device_f32_via_pinned` per
        // `feedback_no_htod_htoh_only_mapped_pinned` and the `check_no_dtod_via_pinned`
        // pre-commit guard. Safety: cuda_stream-is-Some implies an active CUDA
        // context on this thread (DQNTrainer construction sets it).
        let targets_buf = unsafe {
            crate::cuda_pipeline::mapped_pinned::MappedF32Buffer::new(flat_targets.len())
        }
        .map_err(|e| anyhow::anyhow!(
            "targets_raw alloc ({} f32): {e}", flat_targets.len()
        ))?;
        targets_buf.write_from_slice(&flat_targets);
        self.targets_raw_cuda = Some(targets_buf);

        let features_buf = unsafe {
            crate::cuda_pipeline::mapped_pinned::MappedF32Buffer::new(flat_features.len())
        }
        .map_err(|e| anyhow::anyhow!(
            "features_raw alloc ({} f32): {e}", flat_features.len()
        ))?;
        features_buf.write_from_slice(&flat_features);
        self.features_raw_cuda = Some(features_buf);

        // DIAG_AUX_LABEL populate site removed (Fix 29) together with the
        // diagnostic block in gpu_dqn_trainer.rs::aux_heads_forward.

        tracing::info!("CUDA raw buffers uploaded from slices: {} bars x 42+6", num_bars);
        Ok(())
    }

    // ═══════════════════════════════════════════════════════════════════════
    // Helper: Phase 1c — GPU experience collector init (once)
    // ═══════════════════════════════════════════════════════════════════════

    pub(crate) async fn init_gpu_experience_collector(
        &mut self,
        shared_cublas: Arc<crate::cuda_pipeline::shared_cublas_handle::PerStreamCublasHandles>,
    ) -> Result<()> {
        if self.gpu_experience_collector.is_some() {
            return Ok(());
        }

        let stream = match self.cuda_stream {
            Some(ref s) => Arc::clone(s),
            None => return Ok(()),
        };

        use crate::cuda_pipeline::gpu_experience_collector::GpuExperienceCollector;

        // Read-lock agent to extract dueling network weights for GPU collector.
        let agent = self.agent.read().await;
        let dqn_ref: Option<&crate::dqn::DQN> = Some(agent.primary_dqn());

        let init_result = if let Some(dqn) = dqn_ref {
            let _state_dim = agent.get_state_dim();
            // market_dim = raw market features (always 42). OFI features are in a separate
            // buffer and appended by the CUDA state_gather kernel after portfolio + MTF.
            let market_dim: usize = 42;
            let num_atoms_max = self.hyperparams.num_atoms as usize;
            let kernel_dims = (market_dim, num_atoms_max);

            // Priority: branching > plain dueling > hybrid (distributional+dueling)
            if let (Some(online_br), Some(_target_br)) = (
                dqn.branching_q_network.as_ref(),
                dqn.branching_target_network.as_ref(),
            ) {
                let cfg = online_br.config();
                let dims = (
                    *cfg.shared_hidden_dims.first().unwrap_or(&256),
                    *cfg.shared_hidden_dims.get(1).unwrap_or(&256),
                    cfg.value_hidden_dim,
                    cfg.branch_hidden_dim,
                );
                let alloc_episodes = self.compute_alloc_episodes();
                Some(GpuExperienceCollector::new(
                    stream,
                    shared_cublas,
                    self.hyperparams.bottleneck_dim,
                    42,  // market_dim_cfg
                    self.hyperparams.initial_capital as f32,
                    self.hyperparams.avg_spread as f32,
                    self.hyperparams.cash_reserve_percent as f32,
                    dims,
                    kernel_dims,
                    alloc_episodes,
                    self.hyperparams.gpu_timesteps_per_episode,
                    self.hyperparams.curiosity_weight as f32,
                ))
            } else {
                return Err(anyhow::anyhow!(
                    "GPU experience collector FAILED: no dueling, hybrid, or branching networks — \
                     DQN agent has no Q-network. This is a configuration bug."
                ));
            }
        } else {
            None
        };
        drop(agent);

        if let Some(result) = init_result {
            match result {
                Ok(mut collector) => {
                    // Lazy-init GPU monitoring reducer on same stream as collector
                    if self.gpu_monitoring.is_none() {
                        match crate::cuda_pipeline::gpu_monitoring::GpuMonitoringReducer::new(collector.stream()) {
                            Ok(mon) => {
                                info!("GPU monitoring reducer initialized");
                                self.gpu_monitoring = Some(mon);
                            }
                            Err(e) => {
                                return Err(anyhow::anyhow!(
                                    "GPU monitoring reducer init FAILED: {e} — metrics pipeline must be GPU-resident"
                                ));
                            }
                        }
                    }
                    // Lazy-init reduction kernels for fused Q-value stats/diagnostics
                    if self.reduction_kernels.is_none() {
                        match ml_core::cuda_autograd::ReductionKernels::new(collector.stream()) {
                            Ok(rk) => {
                                info!("GPU reduction kernels initialized");
                                self.reduction_kernels = Some(rk);
                            }
                            Err(e) => {
                                return Err(anyhow::anyhow!(
                                    "GPU reduction kernels init FAILED: {e} — diagnostics pipeline must be GPU-resident"
                                ));
                            }
                        }
                    }
                    // Upload OFI features to GPU (unconditional — model requires order flow data).
                    let ofi = self.ofi_features.as_ref().ok_or_else(|| anyhow::anyhow!(
                        "OFI features missing at collector init — fxcache must have has_ofi=true"
                    ))?;
                    let flat: Vec<f32> = ofi.iter()
                        .flat_map(|f| f.iter().map(|&v| v as f32))
                        .collect();
                    collector.upload_ofi_features(&flat)?;
                    // Wire params + epsilon pointers from fused trainer (zero-copy weight access)
                    if let Some(ref fused_ctx) = self.fused_ctx {
                        collector.set_trainer_params_ptr(fused_ctx.params_flat_ptr());
                        collector.set_per_sample_epsilon_ptr(fused_ctx.per_sample_epsilon_ptr());
                        // Tile per_sample_support with initial v_range (will be re-tiled each epoch)
                        let vr = fused_ctx.eval_v_range();
                        collector.update_per_sample_support(vr[0], vr[1])
                            .map_err(|e| anyhow::anyhow!("init per_sample_support: {e}"))?;
                    }
                    // Wire curiosity weights to fused trainer for Q-penalty
                    if let Some(ref mut fused_ctx) = self.fused_ctx {
                        if collector.has_curiosity_trainer() {
                            fused_ctx.set_curiosity_weights(collector.curiosity_weight_set());
                            info!("Curiosity Q-penalty: weights wired to fused trainer (lambda={:.2})",
                                self.hyperparams.curiosity_q_penalty_lambda);
                        }
                        // SP4 Task A14: wire the main DQN trainer's mapped-
                        // pinned Pearl C buffers into all aux trainers
                        // (IQN, IQL hi/lo, Attn) and the curiosity trainer.
                        fused_ctx.wire_aux_trainer_pearl_c_buffers();
                        let nan_flags_ptr = fused_ctx.nan_flags_buf_ptr();
                        let engage_buf_ptr = fused_ctx.clamp_engage_per_block_buf_dev_ptr();
                        collector.set_curiosity_pearl_c_buffers(nan_flags_ptr, engage_buf_ptr);
                        info!("SP4 Pearl C: aux trainer engagement-counter buffers wired");
                        // SP4 Task A13.5 (2026-05-01): wire the trainer's
                        // mapped-pinned Pearls A+D buffers (producer_step_scratch_buf,
                        // wiener_state_buf, isv_signals_pinned) into the
                        // experience collector so launch_reward_component_ema_inplace
                        // can run the host-side Pearls A+D update for slots 63..69.
                        // Per `feedback_wire_everything_up.md`, the trainer-side
                        // orphan `launch_reward_component_ema` is deleted —
                        // this is the canonical (only) launch path now.
                        fused_ctx.wire_reward_component_pearls_buffers(&mut collector);
                        info!("SP4 Task A13.5: reward_component_ema Pearls A+D buffers wired");
                    }
                    self.gpu_experience_collector = Some(collector);
                }
                Err(e) => {
                    return Err(anyhow::anyhow!(
                        "GPU experience collector init FAILED (no CPU fallback): {e}"
                    ));
                }
            }
        }
        Ok(())
    }

    /// Episode count for GPU buffer allocation — uses configured gpu_n_episodes.
    fn compute_alloc_episodes(&self) -> usize {
        self.hyperparams.gpu_n_episodes.max(32)
    }

    // (Old `collect_gpu_experiences` removed — use `collect_gpu_experiences_slices`)
    // ═══════════════════════════════════════════════════════════════════════
    // Helper: Phase 3 — GPU experience collection
    // ═══════════════════════════════════════════════════════════════════════

    /// Collect GPU experiences from fixed-size `&[([f64; 42], [f64; 6])]` slices.
    /// Only `.len()` and `.0` (features) are accessed — the `[f64; 6]` targets
    /// are never read here (GPU raw buffers are already uploaded).
    pub(crate) async fn collect_gpu_experiences_slices(
        &mut self,
        training_data: &[([f64; 42], [f64; 6])],
    ) -> Result<bool> {
        // Feature normalization stats calculation epoch (kept for API compat)
        let _stats_collection_epochs = {
            let ratio_based = (self.hyperparams.epochs as f32 * self.hyperparams.feature_stats_collection_ratio) as usize;
            let capped = match self.hyperparams.max_feature_stats_epochs {
                Some(max_epochs) => ratio_based.min(max_epochs),
                None => ratio_based,
            };
            capped.max(1)
        };

        // Set expert demo ratio BEFORE borrowing collector (avoids borrow conflict)
        if self.hyperparams.expert_demo_ratio > 0.0 {
            use crate::trainers::dqn::expert_demos::ExpertDemoGenerator;
            let effective = ExpertDemoGenerator::effective_ratio(
                f64::from(self.hyperparams.expert_demo_ratio),
                self.hyperparams.expert_demo_decay_epochs,
                self.current_epoch,
            ) as f32;
            if let Some(ref mut c) = self.gpu_experience_collector {
                c.set_expert_ratio(effective);
            }
        }

        let (
            Some(ref mut collector),
            Some(ref features_buf),
            Some(ref targets_buf),
        ) = (
            &mut self.gpu_experience_collector,
            &self.features_raw_cuda,
            &self.targets_raw_cuda,
        ) else {
            return Ok(false);
        };

        use crate::cuda_pipeline::gpu_experience_collector::ExperienceCollectorConfig;

        let raw_sd = if !self.hyperparams.mbp10_data_dir.is_empty() { 53 } else { 45 };
        let aligned_sd = (raw_sd + 7) & !7;

        // Cache n_episodes — auto-scaled from VRAM, capped at collector's alloc size.
        let alloc_cap = collector.alloc_episodes() as i32;
        let n_episodes = if let Some(cached) = self.cached_n_episodes {
            cached.min(alloc_cap)
        } else {
            use ml_core::memory_optimization::detect_gpu_hardware;
            let computed = match detect_gpu_hardware() {
                Ok(hw) => {
                    let optimal = hw.optimal_n_episodes(
                        aligned_sd,
                        self.hyperparams.gpu_timesteps_per_episode,
                    );
                    let chosen = optimal.max(32).min(16384).min(alloc_cap as usize) as i32;
                    info!(
                        "GPU auto-scaled n_episodes: {} (SMs={}, VRAM={:.0}MB, alloc_cap={})",
                        chosen, hw.sm_count, hw.free_memory_mb, alloc_cap
                    );
                    chosen
                }
                Err(_) => alloc_cap.min(256),
            };
            self.cached_n_episodes = Some(computed);
            computed
        };

        let timesteps = self.hyperparams.gpu_timesteps_per_episode.min(1000) as i32;
        let total_bars = training_data.len() as i32;
        let usable_bars = (total_bars - timesteps).max(1);
        let stride = (usable_bars / n_episodes).max(1);

        collector.generate_episode_starts_gpu(
            n_episodes as usize, stride, usable_bars, self.current_epoch,
        ).map_err(|e| anyhow::anyhow!("GPU episode starts: {e}"))?;
        collector.generate_sim_params_gpu(
            n_episodes as usize,
            self.hyperparams.transaction_cost_multiplier as f32,
            self.hyperparams.avg_spread as f32,
            self.hyperparams.fill_ioc_fill_prob as f32,
            self.hyperparams.fill_limit_fill_min as f32,
            self.hyperparams.fill_limit_fill_max as f32,
        ).map_err(|e| anyhow::anyhow!("GPU sim params: {e}"))?;

        let mut episode_starts: Vec<i32> = (0..n_episodes)
            .map(|i| {
                let base = (i * stride).rem_euclid(usable_bars);
                base
            })
            .collect();

        // Curriculum learning: filter episode starts by difficulty
        let curriculum = self.hyperparams.curriculum_phase;
        if curriculum < 2 && !training_data.is_empty() {
            let adx_idx = 40;
            let adx_threshold = 30.0_f64;
            let original = episode_starts.clone();
            episode_starts.clear();

            for &start in &original {
                let bar = (start as usize).min(training_data.len().saturating_sub(1));
                let features = &training_data[bar].0;
                let adx = if features.len() > adx_idx { features[adx_idx] } else { 0.0 };

                match curriculum {
                    0 => {
                        if adx > adx_threshold {
                            episode_starts.push(start);
                        }
                    }
                    1 => {
                        episode_starts.push(start);
                        if adx > adx_threshold {
                            episode_starts.push(start);
                        }
                    }
                    _ => episode_starts.push(start),
                }
            }

            if episode_starts.len() < 16 {
                tracing::warn!(
                    curriculum,
                    filtered = episode_starts.len(),
                    original = original.len(),
                    "Curriculum phase filtered too aggressively, falling back to Full"
                );
                episode_starts = original;
            } else {
                tracing::debug!(
                    curriculum,
                    filtered = episode_starts.len(),
                    original = original.len(),
                    "Curriculum learning: episode starts filtered by ADX difficulty"
                );
            }
        }

        // Reset per-episode state before each epoch
        if let Err(e) = collector.reset_episodes(
            self.hyperparams.initial_capital as f32,
            self.hyperparams.avg_spread as f32,
            self.hyperparams.cash_reserve_percent as f32,
        ) {
            return Err(anyhow::anyhow!("GPU episode reset FAILED (no CPU fallback): {e}"));
        }

        // Tile per-sample, per-branch support [N, 4, 3] from current eval v_range
        // before experience collection. Reads v_min/v_max from fused trainer (which
        // may have been updated last epoch), falls back to config defaults for
        // epoch 0 / pre-fused-init. All 4 branches receive the same triple here;
        // see update_per_sample_support doc (Phase 2d).
        {
            let (v_min, v_max) = if let Some(ref fused_ctx) = self.fused_ctx {
                let vr = fused_ctx.eval_v_range();
                (vr[0], vr[1])
            } else {
                (self.hyperparams.v_min as f32, self.hyperparams.v_max as f32)
            };
            collector.update_per_sample_support(v_min, v_max)
                .map_err(|e| anyhow::anyhow!("update_per_sample_support: {e}"))?;
        }

        let agent = self.agent.read().await;
        let epsilon = agent.get_epsilon();
        drop(agent);

        let adversarial = self.adversarial_active;
        if adversarial {
            info!("Adversarial regime ACTIVE this epoch: 3x spread, 2x tx_cost, 0.5x fill");
        }

        let config = ExperienceCollectorConfig {
            n_episodes,
            timesteps_per_episode: timesteps,
            total_bars,
            epsilon,
            gamma: self.hyperparams.gamma as f32,
            max_position: self.max_position as f32,
            curiosity_scale: 1.0,
            tx_cost_multiplier: {
                let base = self.hyperparams.transaction_cost_multiplier as f32;
                if adversarial { base * 2.0 } else { base }
            },
            count_bonus_coefficient: self.hyperparams.count_bonus_coefficient
                .unwrap_or(0.0) as f32,
            q_clip_min: self.hyperparams.q_clip_min as f32,
            q_clip_max: self.hyperparams.q_clip_max as f32,
            huber_kappa: if true {
                self.hyperparams.huber_delta as f32
            } else {
                0.0
            },
            noisy_sigma_init: self.hyperparams.noisy_sigma_init as f32,
            num_atoms: self.hyperparams.num_atoms as i32,
            v_min: self.hyperparams.v_min as f32,
            v_max: self.hyperparams.v_max as f32,
            fill_median_spread: {
                let base = self.hyperparams.avg_spread as f32;
                if adversarial { base * 3.0 } else { base }
            },
            fill_median_vol: self.median_vol as f32,
            fill_ioc_fill_prob: {
                let base = self.hyperparams.fill_ioc_fill_prob as f32;
                if adversarial { base * 0.5 } else { base }
            },
            fill_limit_fill_min: self.hyperparams.fill_limit_fill_min as f32,
            fill_limit_fill_max: self.hyperparams.fill_limit_fill_max as f32,
            fill_spread_cost_frac: self.hyperparams.fill_spread_cost_frac as f32,
            fill_spread_capture_frac: self.hyperparams.fill_spread_capture_frac as f32,
            fill_simulation_enabled: self.median_vol > 0.0,
            q_gap_threshold: {
                let target = self.hyperparams.q_gap_threshold as f32;
                let epoch = self.current_epoch.min(10) as f32;
                target * (epoch / 10.0)
            },
            n_steps: self.hyperparams.n_steps as i32,
            holding_cost_rate: self.hyperparams.holding_cost_rate as f32,
            churn_threshold: self.hyperparams.churn_threshold_bars as f32,
            churn_penalty_scale: self.hyperparams.churn_penalty_scale as f32,
            spread_cost: {
                // spread_cost in POINTS (no contract_multiplier — PnL is also in points).
                // ES: 0.25 tick × 0.50 fill_frac = 0.125 points = half a tick spread.
                // Was: tick_size × contract_multiplier × frac = 6.25 (50× too high!)
                let base = (self.hyperparams.tick_size
                     * self.hyperparams.fill_spread_cost_frac) as f32;
                if adversarial { base * 3.0 } else { base }
            },
            contract_multiplier: self.hyperparams.contract_multiplier as f32,
            margin_pct: self.hyperparams.margin_pct as f32,
            dd_threshold: self.hyperparams.dd_threshold as f32,
            w_dd: self.hyperparams.w_dd as f32,
            time_reversal_mod: self.hyperparams.time_reversal_mod as i32,
            mirror_active: self.current_epoch % 2 == 1, // Re-enabled: now negates ALL 17 directional features (was only 4)
            feature_noise_scale: self.hyperparams.feature_noise_scale as f32,
            vol_normalizer: self.epoch_vol_normalizer,
            feature_mask: self.epoch_feature_mask.clone(),
            micro_reward_scale: self.hyperparams.micro_reward_scale as f32,
            price_confirm_weight: self.hyperparams.price_confirm_weight as f32,
            book_aggression_weight: self.hyperparams.book_aggression_weight as f32,
            hold_quality_weight: self.hyperparams.hold_quality_weight as f32,
            micro_reward_temp: self.hyperparams.micro_reward_temp as f32,
            current_epoch: self.current_epoch as i32,
            total_epochs: self.hyperparams.epochs as i32,
            eps_start: self.hyperparams.epsilon_start as f32,
            eps_end: self.hyperparams.epsilon_end as f32,
            eps_exp_mult: self.hyperparams.epsilon_magnitude_mult as f32,
            eps_ord_mult: self.hyperparams.epsilon_order_mult as f32,
            eps_urg_mult: self.hyperparams.epsilon_urgency_mult as f32,
            td_lambda: self.hyperparams.td_lambda as f32,
            max_trace_length: self.hyperparams.max_trace_length as i32,
            hindsight_fraction: self.hyperparams.hindsight_fraction as f32,
            hindsight_lookahead: self.hyperparams.hindsight_lookahead as i32,
            // SP5 Layer B (Pearl 3): read per-branch NoisyNet σ from ISV[210..214)
            // SP6 Pearl 3: read per-branch sigma directly from ISV[210..214).
            // No averaging. Cold-start floor 0.01 = Invariant 1 carve-out.
            // Falls back to static hyperparam repeated 4 times when fused_ctx
            // is unavailable (pre-initialization path).
            noise_sigma_per_branch: {
                let mut arr = [self.hyperparams.noise_sigma as f32; 4];
                if let Some(ctx) = self.fused_ctx.as_ref() {
                    for b in 0..4_usize {
                        arr[b] = ctx.read_isv_signal_at(NOISY_SIGMA_BASE + b).max(0.01_f32);
                    }
                }
                arr
            },
            // SP12 v3 (2026-05-04): per-trade event-driven reward composition.
            //   `min_hold_target` and `min_hold_penalty_max` are Phase 1 static
            // constants — defaults are read directly from ExperienceCollectorConfig::default()
            // (MIN_HOLD_TARGET=30, MIN_HOLD_PENALTY_MAX=3.0). Phase 2 lifts these
            // to ISV-driven controllers per feedback_isv_for_adaptive_bounds when/if
            // validation results indicate adaptive need.
            //   `min_hold_temperature` is computed PER EPOCH from the annealing
            // schedule. Curriculum: forgiving (T=50) at epoch 0 → sharp (T=5)
            // around epoch 100. Half-life ≈ MIN_HOLD_TEMPERATURE_DECAY (=20)
            // epochs. The kernel evaluates `deficit/(deficit + T)` per voluntary
            // exit; lower T narrows the transition into a sharper penalty cliff.
            min_hold_temperature: min_hold_temperature_for_epoch(self.current_epoch),
            ..Default::default()
        };

        let gpu_batch = collector.collect_experiences_gpu(
            features_buf, targets_buf, &episode_starts, &config,
        ).map_err(|e| anyhow::anyhow!(
            "GPU zero-roundtrip collection FAILED (no CPU fallback): {e}"
        ))?;

        // Bug 2 instrumentation removed — replaced by structural defense at the
        // data-loading layer (`safe_log_return` clamp, `validate_features`
        // pre-norm bound, `NormStats::normalize` post-norm clamp,
        // `validate_normalized_features` post-load gate, and bar-level sanity
        // in `load_all_bars`). The DIAG_BUG2 / DIAG_BUG2_v2 host-side downloads
        // existed to pin the leak path; the structural guards now make it
        // impossible for raw-price-magnitude values to reach state[0] in the
        // first place. See `docs/dqn-gpu-hot-path-audit.md` Fix 25.

        let count = gpu_batch.n_episodes * gpu_batch.timesteps * 2; // counterfactual doubles experiences

        // Rank normalization REMOVED — PopArt handles reward normalization in training.
        // Rank normalization destroyed magnitude info and never produced positive val_Sharpe.

        if let Some(ref stream) = self.cuda_stream {
            self.experience_done_event = Some(stream.record_event(None)
                .map_err(|e| anyhow::anyhow!("experience event record: {e}"))?);
        }

        if let Some(ref mut mon) = self.gpu_monitoring {
            // GPU monitoring expects b0_size for the exposure-bin dimensionality.
            // The 4-branch DQN uses b0=4 (direction); acquire it from the agent so
            // the kernel's `num_exposure_bins` matches the actual branch layout.
            let (b0, _, _, _) = {
                let ag = self.agent.read().await;
                ag.branch_sizes()
            };
            if let Err(e) = mon.reduce(
                collector.rewards_gpu(), collector.actions_gpu(), count, b0,
            ) {
                debug!("GPU monitoring reduce failed: {e}");
            }
            if let Some(ref stream) = self.cuda_stream {
                if stream.synchronize().is_err() {
                    warn!("GPU monitoring kernel crashed — disabling monitoring");
                    self.gpu_monitoring = None;
                }
            }
        }

        if count > 0 {
            // SP4 Layer B (Mech 9): ISV-driven post-Adam |p| clamp for
            // `curiosity_adam_step`. Curiosity params live in group 7
            // (`ParamGroup::Curiosity`); read `ISV[WEIGHT_BOUND[Curiosity]]`
            // with EPS_CLAMP_FLOOR = 1.0 ε for cold-start. Curiosity
            // collector doesn't own ISV — read from fctx here and thread the
            // bound through to the 4 sub-launches (W1/b1/W2/b2).
            let weight_clamp_max_abs: f32 = if let Some(ref fused) = self.fused_ctx {
                fused.read_isv_signal_at(crate::cuda_pipeline::weight_bound(
                    crate::cuda_pipeline::ParamGroup::Curiosity.idx()))
                    .max(crate::cuda_pipeline::EPS_CLAMP_FLOOR)
            } else {
                // No fctx → no ISV → disable Mech 9. Reachable only on the
                // CPU-only build path (CUDA build asserts fctx Some at
                // line 442). 0=disabled keeps curiosity functional without
                // unbounded clamps.
                0.0_f32
            };
            // SP5 Pearl 4: per-group Adam β1/β2/ε for Curiosity (ParamGroup
            // index 7). ISV[ADAM_BETA1_BASE+7=233], ISV[ADAM_BETA2_BASE+7=241],
            // ISV[ADAM_EPS_BASE+7=249]. Curiosity collector doesn't own ISV —
            // read here and thread through to `train_on_collector_buffers`.
            let cur_g = crate::cuda_pipeline::ParamGroup::Curiosity.idx();
            let (cur_beta1, cur_beta2, cur_epsilon) = if let Some(ref fused) = self.fused_ctx {
                (
                    fused.read_isv_signal_at(SP5_ADAM_BETA1_BASE + cur_g).clamp(0.85_f32, 0.95_f32),
                    fused.read_isv_signal_at(SP5_ADAM_BETA2_BASE + cur_g).clamp(0.99_f32, 0.9995_f32),
                    fused.read_isv_signal_at(SP5_ADAM_EPS_BASE   + cur_g).clamp(1e-10_f32, 1e-6_f32),
                )
            } else {
                (0.9_f32, 0.999_f32, 1e-8_f32)
            };
            if let Err(e) = collector.train_curiosity_gpu(
                gpu_batch.n_episodes, gpu_batch.timesteps,
                weight_clamp_max_abs,
                cur_beta1, cur_beta2, cur_epsilon,
            ) {
                debug!("GPU curiosity training failed (non-fatal): {e}");
            }
            // SP4 Task A15: Pearl C post-Adam engagement-rate check for
            // curiosity trainer (Layer A). Curiosity has 4 sub-launches
            // (W1/b1/W2/b2) and `pearl_c_post_adam_engagement_check`
            // sums all 4 sub-launch ranges before computing the rate.
            if let Some(ref mut fused) = self.fused_ctx {
                let curiosity_total = crate::cuda_pipeline::gpu_curiosity_trainer
                    ::GpuCuriosityTrainer::total_params();
                if let Err(e) = fused.trainer_mut().pearl_c_post_adam_engagement_check(
                    crate::cuda_pipeline::ParamGroup::Curiosity,
                    curiosity_total,
                ) {
                    tracing::warn!("SP4 Pearl C check (Curiosity) failed (non-fatal): {e}");
                }
            }
            if let Some(ref stream) = self.cuda_stream {
                match stream.record_event(None) {
                    Ok(event) => {
                        if !event.is_complete() {
                            if let Err(e) = event.synchronize() {
                                warn!("Curiosity kernel async error detected: {e} — disabling curiosity for this run");
                                self.curiosity_module = None;
                                collector.disable_curiosity();
                            }
                        }
                    }
                    Err(e) => {
                        warn!("Curiosity event record failed: {e} — disabling curiosity for this run");
                        self.curiosity_module = None;
                        collector.disable_curiosity();
                    }
                }
            }
        }

        if count > 0 {
            let total = gpu_batch.n_episodes * gpu_batch.timesteps * 2; // counterfactual doubles experiences

            let actions_u32: &CudaSlice<u32> = unsafe {
                &*(&gpu_batch.actions as *const CudaSlice<i32> as *const CudaSlice<u32>)
            };

            let mut agent = self.agent.write().await;
            agent.primary_dqn_mut().memory.gpu.insert_batch(
                &gpu_batch.states,
                &gpu_batch.next_states,
                actions_u32,
                &gpu_batch.rewards,
                &gpu_batch.dones,
                &gpu_batch.aux_sign_labels,
                total,
            ).map_err(|e| anyhow::anyhow!("GPU PER insert_batch: {e}"))?;
        }

        match collector.read_epoch_dsr_state() {
            Ok((dsr_a, dsr_b)) => {
                self.reward_fn.sync_dsr_from_gpu(dsr_a as f64, dsr_b as f64);
                debug!(
                    "DSR GPU->CPU sync: ema_return={:.6}, ema_return_sq={:.6}",
                    dsr_a, dsr_b
                );
            }
            Err(e) => {
                debug!("DSR epoch_state readback failed (non-fatal): {e}");
            }
        }

        Ok(true)
    }

    // ═══════════════════════════════════════════════════════════════════════
    // Helper: Batched training steps with guard kernel
    // ═══════════════════════════════════════════════════════════════════════

    /// Runs all training steps for one epoch, returning the step count.
    /// Accepts `num_bars` directly (GPU data already resident via `init_from_fxcache`).
    pub(crate) async fn run_training_steps_slices(
        &mut self,
        num_bars: usize,
    ) -> Result<usize> {
        let batch_size = self.hyperparams.batch_size;
        let num_training_steps = if self.can_train().await? {
            (num_bars / batch_size).max(1)
        } else {
            0
        };

        let mut train_step_count = 0;

        // Lazy-init fused CUDA training context.
        //
        // On CUDA builds, fused_ctx is mandatory (no CPU fallback). A construction
        // failure here is propagated rather than logged-and-continued so that the
        // original CUDA error surfaces up the call chain. Silently leaving
        // `fused_ctx = None` on a later batch-size-change path would resurface as
        // a misleading "GPU experience collector MUST be active" error elsewhere.
        if num_training_steps > 0 {
            let needs_init = match &self.fused_ctx {
                None => self.device.is_cuda(),
                Some(ctx) => ctx.batch_size() != self.current_batch_size,
            };
            if needs_init {
                if let Some(ref stream) = self.cuda_stream {
                    if self.fused_ctx.is_some() {
                        tracing::info!("Fused CUDA context: batch_size changed, recreating");
                        self.fused_ctx = None;
                    }
                    let agent = self.agent.read().await;
                    let ctx = super::super::fused_training::FusedTrainingCtx::new(
                        &self.device, &*agent, &self.hyperparams, self.current_batch_size, std::sync::Arc::clone(stream),
                    )
                        .map_err(|e| anyhow::anyhow!(
                            "Fused CUDA context init failed (cannot recover on CUDA builds — no CPU fallback): {e}"
                        ))?;
                    // Zero-copy: wire collector to read weights directly from trainer's params_buf.
                    if let Some(ref mut collector) = self.gpu_experience_collector {
                        collector.set_trainer_params_ptr(ctx.params_flat_ptr());
                        // Tile per_sample_support with current v_range
                        let vr = ctx.eval_v_range();
                        if let Err(e) = collector.update_per_sample_support(vr[0], vr[1]) {
                            tracing::error!("update_per_sample_support on fused re-init: {e}");
                        }
                    }
                    self.fused_ctx = Some(ctx);
                    drop(agent);
                    // Wire direct-to-trainer gather: PER gather writes directly to trainer buffers.
                    // fused_ctx is guaranteed Some here — we just assigned it above and would
                    // have returned Err otherwise.
                    let fused = self.fused_ctx.as_mut().expect("fused_ctx just assigned");
                    let mut agent_w = self.agent.write().await;
                    agent_w.memory_mut().gpu.set_trainer_buffers(
                        fused.trainer_states_buf_ptr(),
                        fused.trainer_next_states_buf_ptr(),
                        fused.trainer_actions_buf_ptr(),
                        fused.trainer_rewards_buf_ptr(),
                        fused.trainer_dones_buf_ptr(),
                        fused.trainer_is_weights_buf_ptr(),
                        // B1.1b: 8th arg — trainer's aux_nb_label_buf raw_ptr
                        // so PER gather writes -1/0/1 labels directly into
                        // the K=2 softmax CE consumer's i32 buffer.
                        fused.trainer_aux_sign_labels_buf_ptr(),
                        fused.trainer_state_dim_padded(),
                    );
                    fused.set_per_rng_step_dev_ptr(agent_w.memory().gpu.rng_step_dev_ptr());
                }
            }
        }

        // Lazy-init + reset guard accumulators for this epoch
        {
            if self.training_guard.is_none() && self.device.is_cuda() {
                match crate::cuda_pipeline::gpu_training_guard::GpuTrainingGuard::from_stream(Arc::clone(self.cuda_stream.as_ref().ok_or_else(|| anyhow::anyhow!("CUDA stream for training guard"))?)) {
                    Ok(g) => {
                        info!("GPU training guard initialized (epoch loop)");
                        self.training_guard = Some(g);
                    }
                    Err(e) => return Err(anyhow::anyhow!("GPU training guard init: {e}")),
                }
            }
            if let Some(ref mut guard) = self.training_guard {
                guard.reset_accumulators()
                    .map_err(|e| anyhow::anyhow!("guard reset: {e}"))?;
            }
        }

        let guard_collapse_thresh =
            self.hyperparams.learning_rate as f32
                * self.hyperparams.gradient_collapse_multiplier as f32;
        let guard_past_warmup = {
            let ws = (self.collapse_warmup_buffer_size as f64 * 0.2) as u64;
            self.gradient_logging_step as u64 > ws
        };

        let sample_total_us = 0_u64;
        let mut fused_total_us = 0_u64;
        let mut guard_total_us = 0_u64;

        // Wait for experience collection GPU kernels to complete before sampling.
        if let Some(event) = self.experience_done_event.take() {
            if !event.is_complete() {
                event.synchronize()
                    .map_err(|e| anyhow::anyhow!("experience event wait: {e}"))?;
            }
        }

        // Acquire write lock ONCE for the entire training loop — not per-step.
        // Per-step lock acquisition was burning 696s/epoch due to async yield overhead.
        let mut agent = self.agent.write().await;

        // nsys: start profiling capture for the training loop.
        // Only active when nsys is launched with --capture-range=cudaProfilerApi.
        #[cfg(feature = "cuda")]
        unsafe { cudarc::driver::sys::cuProfilerStart(); }

        for _step in 0..num_training_steps {

            {
                let buffer = agent.memory();
                if !buffer.can_sample(self.current_batch_size) {
                    break;
                }
            }
            // Vaccine batch sampling removed — vaccine now uses prev_grad_buf
            // inside maintenance_child (graphed). No ungraphed PER sampling needed.

            {
                let fused_start = std::time::Instant::now();
                let fused = self.fused_ctx.as_mut()
                    .expect("Fused CUDA training is the only production path");
                // PER sampling is inside run_full_step (per_sample child graph).
                let _fused_result = fused.run_full_step(&mut *agent, &self.device)
                    .context("Fused CUDA training step failed")?;

                let step_us = fused_start.elapsed().as_micros() as u64;
                fused_total_us += step_us;
                if train_step_count < 5 || train_step_count % 50 == 0 {
                    tracing::info!(
                        step = train_step_count,
                        step_ms = step_us / 1000,
                        "STEP_TIMING"
                    );
                }

                let guard_start = std::time::Instant::now();
                // Run guard every 5th step — host-side pinned reads only, zero GPU kernel.
                // Loss + grad_norm are from pinned readback (one-step delay due to
                // double-buffering). Skip first 5 steps to avoid reading zeros.
                if train_step_count >= 5 && train_step_count % 5 == 0 {
                if let Some(ref mut guard) = self.training_guard {
                    let fused = self.fused_ctx.as_ref().unwrap();
                    let scalars = fused.last_readback_scalars();
                    let gr = guard.check_host(
                        scalars.total_loss,
                        scalars.grad_norm,
                        1e6_f32,
                        guard_collapse_thresh,
                        !guard_past_warmup,
                    );
                    if gr.halt_nan {
                        if let Some(ref mut fused) = self.fused_ctx {
                            if let Ok(flags) = fused.read_nan_flags() {
                                // Index → buffer name. Must match
                                // GpuDqnTrainer::run_nan_checks_pre_forward /
                                // run_nan_checks_post_forward /
                                // run_nan_checks_post_backward (Task 4 — slots
                                // 24-35). See docs/dqn-backward-nan-audit.md
                                // per-slot accessor table for slot 24-47
                                // semantics. SP1 Phase B 48-slot layout: slots
                                // 0-23 forward-path (existing); slots 24-35
                                // backward-path (wired by Task 4); slots 36-47
                                // reserved headroom for SP2/SP3.
                                let names = [
                                    "states_buf",            // 0  input states (post-upload)
                                    "on_v_logits",           // 1  cuBLAS fwd: value stream
                                    "on_b_logits",           // 2  cuBLAS fwd: branch advantage
                                    "mse_loss_scalar",       // 3  MSE loss [1]
                                    "params_buf_pre_fwd",    // 4  f32 params (pre-forward)
                                    "params_ptr_pre_fwd",    // 5  same buffer (redundant pre-fwd)
                                    "grad_buf",              // 6  cuBLAS bwd output
                                    "save_current_lp",       // 7  C51 online softmax probs
                                    "save_projected",        // 8  C51 target distribution
                                    "moe_gate_softmax",      // 9  MoE gate over 8 experts
                                    "aux_nb_loss_scalar",    // 10 aux next-bar MSE [1]
                                    "aux_rg_loss_scalar",    // 11 aux regime CE [1]
                                    "save_h_s2",             // 12 trunk output (input to FCs)
                                    "rsv13", "rsv14", "rsv15", // 13-15 reserved
                                    "grn_h_s2_elu_post",     // 16 GRN h_s2 post-ELU activation
                                    "grn_h_s2_linear_b_out", // 17 GRN h_s2 Linear_b output
                                    "grn_h_s2_glu_sigmoid",  // 18 GRN h_s2 GLU gate sigmoid
                                    "grn_h_s2_ln_rstd",      // 19 GRN h_s2 LN reciprocal-stddev
                                    "grn_h_s2_ln_normed",    // 20 GRN h_s2 LN normed (pre-affine)
                                    "rsv21", "rsv22", "rsv23", // reserved (h_s1 stages / future)
                                    "d_value_logits_buf",    // 24 post-c51_grad value gradient
                                    "d_adv_logits_buf",      // 25 post-c51_grad branch advantage gradient
                                    "iqn_trunk_m",           // 26 apply_iqn_trunk_gradient cuBLAS bwd output
                                    "iqn_d_h_s2_ptr",        // 27 IQN backward dh_s2 input (= gpu_iqn_head::d_h_s2_buf)
                                    "d_branch_logits_buf",   // 28 production IQN backward output (iqn_quantile_huber_loss)
                                    "cql_d_value_logits",    // 29 CQL gradient output
                                    "aux_dh_s2_nb_buf",      // 30 aux next-bar backward dh_s2
                                    "ensemble_d_logits_buf", // 31 ensemble (DEFERRED — owner: FusedDqnTraining; instrument only after Phase B saxpy guards verified; see audit per-slot table)
                                    "bn_d_concat_buf",       // 32 bottleneck Linear backward dy
                                    "bw_d_h_s2_post_main",   // 33 bw_d_h_s2 after main backward chain
                                    "bw_d_h_s2_post_aux",    // 34 bw_d_h_s2 after aux head SAXPY
                                    "bw_d_h_s2_post_iqn",    // 35 bw_d_h_s2 after IQN DtoD
                                    // SP3 Mech 5 (slots 36-47): Adam-centric diagnostic threshold checks.
                                    "trunk_adam_m_max",     // 36
                                    "value_adam_m_max",     // 37
                                    "branch_adam_m_max",    // 38
                                    "iqn_adam_m_max",       // 39
                                    "trunk_adam_v_max",     // 40
                                    "value_adam_v_max",     // 41
                                    "branch_adam_v_max",    // 42
                                    "iqn_adam_v_max",       // 43
                                    "trunk_weight_max",     // 44
                                    "heads_weight_max",     // 45
                                    "target_q_post_clip",   // 46
                                    "atom_span_max",        // 47
                                    // SP3 close-out v2 (slot 48): IQN online_params
                                    // weight diagnostic — the slot-26 GEMM blind
                                    // spot that hid the F1 step-3540 NaN.
                                    "iqn_weight_max",       // 48
                                    // SP3 Mech 10 (slot 49): post-trunk h_s2 max-abs
                                    // sentinel — fires at 1e3 × H_S2_RMS_EMA.max(1)
                                    // (clamp at 100×, diagnostic at 1000×). Stays
                                    // quiet under normal Mech 10 operation.
                                    "h_s2_max_abs",         // 49
                                ];
                                let flagged: Vec<String> = flags.iter().enumerate()
                                    .filter(|(_, &f)| f != 0)
                                    .map(|(i, _)| format!("{}={}", i, names[i]))
                                    .collect();
                                tracing::error!(
                                    "NaN SOURCE at step {}: flagged=[{}] (empty = NaN entered via \
                                     loss-component buffer not in checks 0..49; expand coverage)",
                                    train_step_count, flagged.join(", ")
                                );
                            }
                        }
                        return Err(anyhow::anyhow!(
                            "NaN/Inf at step {}: loss={}, grad={}",
                            train_step_count, gr.raw_loss, gr.raw_grad_norm
                        ));
                    }
                    if gr.halt_grad_collapse {
                        // Plan C T11 diagnostic extension (2026-04-29; SP1 Phase B
                        // expansion 24→48): the existing nan_flags_buf [48]
                        // system runs NaN checks every step inside the captured
                        // graph. Slots 0-23 are forward-path (13 base + 5
                        // GRN-stage P additions + 6 reserved), slots 24-35 are
                        // backward-path (Task 4 wires per-step checks via
                        // run_nan_checks_post_backward), slots 36-47 are
                        // reserved headroom for SP2/SP3. `read_nan_flags()` was
                        // previously only called on `halt_nan`. The training
                        // guard's halt_nan path checks pinned readback `grad_norm`
                        // — and when a kernel produces NaN that is then clamped to
                        // 0 by downstream gradient sanitization, the pinned scalar
                        // reads 0 (finite), so halt_nan never fires. The collapse
                        // path (raw_grad_norm < 0.01) fires instead, masking the
                        // underlying NaN.
                        // Read the flags here too — gated to suspicious-zero
                        // (`raw_grad_norm < 1e-6`) so legitimate near-zero
                        // gradients (genuine collapse) get a separate "no NaN
                        // flags set" log distinguishing the two cases.
                        if gr.raw_grad_norm < 1e-6 {
                            if let Some(ref mut fused) = self.fused_ctx {
                                if let Ok(flags) = fused.read_nan_flags() {
                                    // Index → buffer name. Must match
                                    // GpuDqnTrainer::run_nan_checks_pre_forward /
                                    // run_nan_checks_post_forward /
                                    // run_nan_checks_post_backward (Task 4 —
                                    // slots 24-35). See
                                    // docs/dqn-backward-nan-audit.md per-slot
                                    // accessor table for slot 24-47 semantics.
                                    // SP1 Phase B 48-slot layout: slots 0-23
                                    // forward-path (existing); slots 24-35
                                    // backward-path (wired by Task 4); slots
                                    // 36-47 reserved headroom for SP2/SP3.
                                    let names = [
                                        "states_buf",            // 0  input states (post-upload)
                                        "on_v_logits",           // 1  cuBLAS fwd: value stream
                                        "on_b_logits",           // 2  cuBLAS fwd: branch advantage
                                        "mse_loss_scalar",       // 3  MSE loss [1]
                                        "params_buf_pre_fwd",    // 4  f32 params (pre-forward)
                                        "params_ptr_pre_fwd",    // 5  same buffer (redundant pre-fwd)
                                        "grad_buf",              // 6  cuBLAS bwd output
                                        "save_current_lp",       // 7  C51 online softmax probs
                                        "save_projected",        // 8  C51 target distribution
                                        "moe_gate_softmax",      // 9  MoE gate over 8 experts
                                        "aux_nb_loss_scalar",    // 10 aux next-bar MSE [1]
                                        "aux_rg_loss_scalar",    // 11 aux regime CE [1]
                                        "save_h_s2",             // 12 trunk output (input to FCs)
                                        "rsv13", "rsv14", "rsv15", // 13-15 reserved
                                        "grn_h_s2_elu_post",     // 16 GRN h_s2 post-ELU activation
                                        "grn_h_s2_linear_b_out", // 17 GRN h_s2 Linear_b output
                                        "grn_h_s2_glu_sigmoid",  // 18 GRN h_s2 GLU gate sigmoid
                                        "grn_h_s2_ln_rstd",      // 19 GRN h_s2 LN reciprocal-stddev
                                        "grn_h_s2_ln_normed",    // 20 GRN h_s2 LN normed (pre-affine)
                                        "rsv21", "rsv22", "rsv23", // reserved (h_s1 stages / future)
                                        "d_value_logits_buf",    // 24 post-c51_grad value gradient
                                        "d_adv_logits_buf",      // 25 post-c51_grad branch advantage gradient
                                        "iqn_trunk_m",           // 26 apply_iqn_trunk_gradient cuBLAS bwd output
                                        "iqn_d_h_s2_ptr",        // 27 IQN backward dh_s2 input (= gpu_iqn_head::d_h_s2_buf)
                                        "d_branch_logits_buf",   // 28 production IQN backward output (iqn_quantile_huber_loss)
                                        "cql_d_value_logits",    // 29 CQL gradient output
                                        "aux_dh_s2_nb_buf",      // 30 aux next-bar backward dh_s2
                                        "ensemble_d_logits_buf", // 31 ensemble (DEFERRED — owner: FusedDqnTraining; instrument only after Phase B saxpy guards verified; see audit per-slot table)
                                        "bn_d_concat_buf",       // 32 bottleneck Linear backward dy
                                        "bw_d_h_s2_post_main",   // 33 bw_d_h_s2 after main backward chain
                                        "bw_d_h_s2_post_aux",    // 34 bw_d_h_s2 after aux head SAXPY
                                        "bw_d_h_s2_post_iqn",    // 35 bw_d_h_s2 after IQN DtoD
                                        // SP3 Mech 5 (slots 36-47): Adam-centric diagnostic threshold checks.
                                        "trunk_adam_m_max",     // 36
                                        "value_adam_m_max",     // 37
                                        "branch_adam_m_max",    // 38
                                        "iqn_adam_m_max",       // 39
                                        "trunk_adam_v_max",     // 40
                                        "value_adam_v_max",     // 41
                                        "branch_adam_v_max",    // 42
                                        "iqn_adam_v_max",       // 43
                                        "trunk_weight_max",     // 44
                                        "heads_weight_max",     // 45
                                        "target_q_post_clip",   // 46
                                        "atom_span_max",        // 47
                                        // SP3 close-out v2 (slot 48): IQN online_params
                                        // weight diagnostic — the slot-26 GEMM blind
                                        // spot that hid the F1 step-3540 NaN.
                                        "iqn_weight_max",       // 48
                                        // SP3 Mech 10 (slot 49): post-trunk h_s2 max-abs
                                        // sentinel — fires at 1e3 × H_S2_RMS_EMA.max(1)
                                        // (clamp at 100×, diagnostic at 1000×). Stays
                                        // quiet under normal Mech 10 operation.
                                        "h_s2_max_abs",         // 49
                                    ];
                                    let flagged: Vec<String> = flags.iter().enumerate()
                                        .filter(|(_, &f)| f != 0)
                                        .map(|(i, _)| format!("{}={}", i, names[i]))
                                        .collect();
                                    if !flagged.is_empty() {
                                        tracing::error!(
                                            "NaN-CLAMPED-TO-ZERO at step {} (grad collapse path): \
                                             flagged=[{}] — the collapse is NaN-derived, not \
                                             legitimate; this is the F1 ep2 explosion source",
                                            train_step_count, flagged.join(", ")
                                        );
                                    } else {
                                        tracing::warn!(
                                            "Genuine grad collapse at step {} (no NaN flags set; \
                                             legitimate near-zero gradients)",
                                            train_step_count
                                        );
                                    }
                                }
                            }
                        }

                        agent.check_gradient_collapse(gr.raw_grad_norm).map_err(|e| {
                            tracing::info!("Early stopping (gradient collapse): {}", e);
                            anyhow::anyhow!("Early stopping: {}", e)
                        })?;
                    }
                    // Feed observed grad norm into EMA-based adaptive clip.
                    // The next Adam graph replay will read the updated clip from the device buffer.
                    //
                    // Task 0.9: set `grad_clip_kicked_this_epoch` iff this step's
                    // raw_grad_norm actually exceeded the clip threshold that was
                    // active FOR THIS step (i.e. before we feed the new observation
                    // into the EMA below). The clip value is only load-bearing if it
                    // actually clamped a gradient — EMA ticks of the threshold are
                    // schedule drift, not interventions.
                    if let Some(ref mut fused) = self.fused_ctx {
                        let active_clip = fused.adaptive_clip_value();
                        if gr.raw_grad_norm.is_finite()
                            && active_clip.is_finite()
                            && gr.raw_grad_norm > active_clip
                        {
                            self.grad_clip_kicked_this_epoch = true;
                        }
                        // Update the EMA-based adaptive clip + the fast/slow
                        // grad-norm EMAs that drive Plan C K's fold-warmup
                        // factor (single observation, three consumers).
                        fused.update_adaptive_clip(gr.raw_grad_norm);

                        // Plan C Phase 2 follow-up K (2026-04-29): apply the
                        // adaptive fold-boundary warmup factor to BOTH lr_eff
                        // and clip_eff. ISV[FOLD_WARMUP_FACTOR_INDEX] is
                        // produced once per step by `launch_fold_warmup_factor`
                        // alongside the other ISV producers (see the
                        // post-step ISV producer block lower in this loop);
                        // by the time we land here on the NEXT step the slot
                        // already holds the most recent valid factor.
                        //
                        // Numerical-stability bounds (Invariant 1 carve-outs
                        // per `feedback_isv_for_adaptive_bounds.md`):
                        //   MIN_WARMUP_LR_FRAC = 0.05 — lr never below 5%
                        //   MIN_CLIP_FRAC      = 0.1  — clip never below 10%
                        // Both consumers are MONOTONE (factor=1 → no
                        // dampening; factor<1 → dampening; factor never
                        // excites lr/clip past baseline). Healthy runs
                        // (steady-state factor=1) are unaffected.
                        const MIN_WARMUP_LR_FRAC: f32 = 0.05;
                        const MIN_CLIP_FRAC: f32 = 0.1;
                        let warmup_factor = fused.read_isv_signal_at(
                            crate::cuda_pipeline::gpu_dqn_trainer::FOLD_WARMUP_FACTOR_INDEX,
                        ).clamp(0.0, 1.0);
                        // LR consumer: lr_eff = cosine_effective_lr × max(0.05, factor).
                        // Compose with the cosine schedule's per-epoch baseline
                        // (stored in `self.cosine_effective_lr_base` by the
                        // cosine LR block at epoch start) so per-step warmup
                        // damping multiplies the cosine schedule rather than
                        // overriding it.
                        let lr_base = self.cosine_effective_lr_base;
                        let lr_warmup_scale = MIN_WARMUP_LR_FRAC.max(warmup_factor);
                        let lr_eff = lr_base * lr_warmup_scale;
                        fused.set_lr(lr_eff);
                        // Clip consumer: clip_eff = clip_base × (0.1 + 0.9 × factor).
                        // `update_adaptive_clip` just wrote `clip_base = ema × 2`
                        // to the pinned slot above; rescale that value
                        // in-place by the warmup multiplier. The pinned
                        // write through `set_active_clip` is GPU-visible
                        // immediately on the next kernel read.
                        let clip_base = fused.adaptive_clip_value();
                        if clip_base.is_finite() && clip_base > 0.0 {
                            let clip_warmup_scale =
                                MIN_CLIP_FRAC + (1.0 - MIN_CLIP_FRAC) * warmup_factor;
                            let clip_eff = clip_base * clip_warmup_scale;
                            fused.set_active_clip(clip_eff);
                        }
                    }

                    // ── Plan C T11 follow-up — F1 ep2 explosion diagnostic ─
                    //
                    // After 6+ defensive layers (A.1+A.2+A.3+F+H+K+warmup+N)
                    // F1 ep2 still explodes with grad_norm=2.66T while F1 ep1
                    // trains cleanly. None of our defenses catch the explosion
                    // path; we need data to pinpoint WHICH signal explodes
                    // first. Per-step diagnostic gated to fold >= 1: when
                    // grad_norm > EXPLOSION_GRAD_THRESH (1000) OR jumps
                    // EXPLOSION_GRAD_RATIO (5×) from the prior guard step,
                    // fire FOLD_EXPLOSION_DIAG with loss decomposition + Q
                    // range + atom positions. Once triggered, continue
                    // emitting for STEPS_AFTER_EXPLOSION steps so the
                    // explosion trajectory is captured (not just the first
                    // crossing).
                    //
                    // Diagnostic-only — remove this block + its companion
                    // accessors (fused_training.rs::explosion_diag_*,
                    // gpu_dqn_trainer.rs::{c51,mse}_loss_pinned_value +
                    // stream_for_diag) + the four `DQNTrainer` fields
                    // (current_fold, last_logged_grad_norm,
                    // explosion_diag_steps_remaining + their reset wiring)
                    // once the root cause is identified. Not for prod.
                    {
                        const EXPLOSION_GRAD_THRESH: f32 = 1000.0;
                        const EXPLOSION_GRAD_RATIO: f32 = 5.0;
                        const STEPS_AFTER_EXPLOSION: u32 = 5;

                        let raw = gr.raw_grad_norm;
                        let prev = self.last_logged_grad_norm.unwrap_or(0.0);
                        let trigger_fired = self.current_fold >= 1
                            && raw.is_finite()
                            && (raw > EXPLOSION_GRAD_THRESH
                                || (prev.is_finite()
                                    && prev > 0.0
                                    && raw > prev * EXPLOSION_GRAD_RATIO));

                        if trigger_fired {
                            self.explosion_diag_steps_remaining = STEPS_AFTER_EXPLOSION;
                        }

                        // Emit on trigger fire OR while still in the
                        // post-trigger trajectory window.
                        let should_log = self.current_fold >= 1
                            && (trigger_fired || self.explosion_diag_steps_remaining > 0);

                        if should_log {
                            // Take immutable-borrow values first, release
                            // the &self borrow, then take &mut for Q-range.
                            let (c51_loss, mse_loss, iqn_loss, atoms) = {
                                let fused_ref = self.fused_ctx.as_ref()
                                    .expect("fused_ctx present in guard branch");
                                let (c, m, i) = fused_ref.explosion_diag_loss_components();
                                let a = fused_ref.explosion_diag_atom_range();
                                (c, m, i, a)
                            };
                            // Q range: cold-path reduce + 28B DtoH.
                            let (q_min, q_mean, q_max) = self.fused_ctx.as_mut()
                                .expect("fused_ctx present in guard branch")
                                .explosion_diag_q_range();

                            tracing::warn!(
                                target: "ml::trainers::dqn::trainer::training_loop",
                                "FOLD_EXPLOSION_DIAG[fold={}, ep={}, step={}]: \
                                 grad_norm={:.4e} (prev={:.4e}, ratio={:.2}) \
                                 total_loss={:.4e} c51={:.4e} mse={:.4e} iqn={:.4e} \
                                 q_min={:.3} q_mean={:.3} q_max={:.3} q_range={:.3} \
                                 atom_d0=[{:.3},{:.3},dz={:.3}] atom_d2=[{:.3},{:.3},dz={:.3}] \
                                 trigger={} traj_remaining={}",
                                self.current_fold,
                                self.current_epoch,
                                train_step_count,
                                raw,
                                prev,
                                if prev > 0.0 { raw / prev } else { f32::NAN },
                                gr.raw_loss,
                                c51_loss,
                                mse_loss,
                                iqn_loss,
                                q_min, q_mean, q_max, (q_max - q_min),
                                atoms[0], atoms[1], atoms[2],
                                atoms[3], atoms[4], atoms[5],
                                trigger_fired,
                                self.explosion_diag_steps_remaining,
                            );

                            if self.explosion_diag_steps_remaining > 0 {
                                self.explosion_diag_steps_remaining -= 1;
                            }
                        }

                        if raw.is_finite() {
                            self.last_logged_grad_norm = Some(raw);
                        }
                    }
                }
                } // end guard frequency check
                guard_total_us += guard_start.elapsed().as_micros() as u64;

                // Q-stats computed at epoch boundary only (process_epoch_boundary).
                // Per-step Q-stats sync was creating GPU pipeline bubbles.
                if false {
                if let Some(ref mut fused) = self.fused_ctx {
                    if let Ok(stats) = fused.reduce_current_q_stats() {
                        self.cached_avg_q = stats.q_mean as f64;
                        if stats.q_min < self.epoch_q_min { self.epoch_q_min = stats.q_min; }
                        if stats.q_max > self.epoch_q_max { self.epoch_q_max = stats.q_max; }
                        self.epoch_q_variance = self.epoch_q_variance.max(stats.q_variance);
                        self.epoch_q_gap = self.epoch_q_gap.max(stats.avg_max_q as f32 - stats.q_mean);
                        self.epoch_atom_entropy = stats.atom_entropy;
                        self.epoch_atom_utilization = stats.atom_utilization;

                        // Per-branch Q-stats for the ISV v-range EMA updater.
                        // Per-branch Q-gaps pinned in reduce_current_q_stats (48B DtoH readback).
                        let per_branch_q_gaps = fused.get_per_branch_q_gaps();
                        if let Ok(per_branch_stats) = fused.reduce_current_q_stats_per_branch() {
                            fused.update_eval_v_range(&per_branch_stats, per_branch_q_gaps);
                        }
                    }
                }
                } // end if false (Q-stats disabled per-step)
                train_step_count += 1;
                self.gradient_logging_step += 1;
            }
        }

        if let Some(ref mut fused) = self.fused_ctx {
            let _ = fused.flush_readback();
            // Refresh IQN IQR from the last training step's quantile estimates.
            // Epoch-boundary only — compute_iqr involves DtoH + HtoD which are
            // forbidden on the per-step hot path (moved here from submit_aux_ops).
            if let Err(e) = fused.refresh_iqn_iqr() {
                tracing::warn!("refresh_iqn_iqr at epoch boundary failed (non-fatal): {e}");
            }
            // Final Q-stats for this epoch (synchronous — no flush needed)
            if let Ok(stats) = fused.reduce_current_q_stats() {
                self.cached_avg_q = stats.q_mean as f64;
                if stats.q_min < self.epoch_q_min { self.epoch_q_min = stats.q_min; }
                if stats.q_max > self.epoch_q_max { self.epoch_q_max = stats.q_max; }
                self.epoch_q_variance = self.epoch_q_variance.max(stats.q_variance);
                self.epoch_q_gap = self.epoch_q_gap.max(stats.avg_max_q as f32 - stats.q_mean);
                self.epoch_atom_entropy = stats.atom_entropy;
                self.epoch_atom_utilization = stats.atom_utilization;

                // Propagate into GPU-side per-branch Q-gap EMA buffer.
                // This is the buffer `health_ema.q_gap_ema` is eventually
                // computed from (via per_branch_q_gap_ema() → q_gap_signal
                // → HealthEmaTrackers::update). Without this call, the
                // per-step Q-stats path (disabled for pipeline reasons) is
                // the only thing that updates the buffer — so it stays at
                // zero and the downstream health composition believes
                // q_gap=0 even when raw epoch q_gap peaks above 1.0.
                // Plan 2 Task 1 C.1: launch q_quantile_reduce AFTER populate_q_out
                // (done inside reduce_current_q_stats above) and BEFORE
                // update_eval_v_range reads the percentile slots from ISV.
                // EMA alpha 0.1: 10% of each new epoch's percentile estimate.
                if let Err(e) = fused.launch_q_quantile_reduce(0.1) {
                    tracing::warn!(
                        "launch_q_quantile_reduce at epoch-boundary failed (non-fatal): {e}"
                    );
                }
                let per_branch_q_gaps = fused.get_per_branch_q_gaps();
                match fused.reduce_current_q_stats_per_branch() {
                    Ok(per_branch_stats) => {
                        fused.update_eval_v_range(&per_branch_stats, per_branch_q_gaps);
                    }
                    Err(e) => {
                        tracing::warn!(
                            "reduce_current_q_stats_per_branch failed at epoch-boundary: {e} — \
                             ISV v-range update skipped this step"
                        );
                    }
                }
            }
        }

        if train_step_count > 0 {
            info!(
                "Training step breakdown ({} steps): sample={:.0}ms fused={:.0}ms guard={:.0}ms (per-step: sample={:.1}ms fused={:.1}ms guard={:.1}ms)",
                train_step_count,
                sample_total_us as f64 / 1000.0,
                fused_total_us as f64 / 1000.0,
                guard_total_us as f64 / 1000.0,
                sample_total_us as f64 / 1000.0 / train_step_count as f64,
                fused_total_us as f64 / 1000.0 / train_step_count as f64,
                guard_total_us as f64 / 1000.0 / train_step_count as f64,
            );
        }
        Ok(train_step_count)
    }

    // ═══════════════════════════════════════════════════════════════════════
    // Helper: Epoch boundary readback + safety checks
    // ═══════════════════════════════════════════════════════════════════════

    pub(crate) async fn process_epoch_boundary(
        &mut self,
        epoch: usize,
        train_step_count: usize,
        monitor: &mut TrainingMonitor,
    ) -> Result<EpochBoundaryMetrics> {

        let (avg_loss, avg_grad) = if let Some(ref mut fused) = self.fused_ctx {
            let (al, ag) = fused.read_and_reset_accumulators();
            info!("EPOCH_DIAG[{}]: avg_loss={:.6}, avg_grad={:.6}", epoch, al, ag);
            (al as f32, ag as f32)
        } else {
            (0.0_f32, 0.0_f32)
        };

        // Snapshot absolute per-branch grad L2 norms + their ratio BEFORE
        // `estimate_avg_q_value_with_early_stopping` runs — that path replays
        // `eval_forward_exec`, the same captured graph as `forward_child`, whose
        // first op is `cuMemsetD32Async(grad_buf, 0, total_params)` (see
        // `submit_forward_ops_main`). Without this early read, the HEALTH_DIAG
        // accessor downstream would see an all-zero grad_buf — not "direction
        // starved" or "magnitude zero", but a timing-induced wipe. Cache the
        // real post-last-step values here and consume them in the HEALTH_DIAG
        // block further down.
        let (grad_dir_abs_cached, grad_mag_abs_cached) = self.fused_ctx
            .as_mut()
            .map(|f| f.grad_norms_dir_mag_abs())
            .unwrap_or((0.0, 0.0));
        let grad_ratio_mag_dir_cached = self.fused_ctx
            .as_mut()
            .map(|f| f.grad_ratio_mag_dir())
            .unwrap_or(0.0);

        let avg_q = {
            let agent_clone = Arc::clone(&self.agent);
            let mut agent = agent_clone.write().await;
            self.estimate_avg_q_value_with_early_stopping(&mut agent).await?
        };

        // Epoch-level safety checks
        if !avg_loss.is_finite() || !avg_grad.is_finite() {
            let msg = format!(
                "SAFETY: NaN/Inf in epoch {} avg — loss={:.6}, grad={:.6}",
                epoch, avg_loss, avg_grad
            );
            match self.safety_level {
                crate::safety::SafetyLevel::Strict => {
                    return Err(anyhow::anyhow!("{} (stopping training)", msg));
                },
                crate::safety::SafetyLevel::Normal | crate::safety::SafetyLevel::Permissive => {
                    debug!("{} (continuing training)", msg);
                },
            }
        }

        // Q-value explosion guard: halt if Q-values exceed configured clip range
        let q_clip_min = f64::from(self.hyperparams.q_clip_min);
        let q_clip_max = f64::from(self.hyperparams.q_clip_max);
        if avg_q < q_clip_min || avg_q > q_clip_max {
            return Err(anyhow::anyhow!(
                "Q-value explosion: avg_q={:.4} outside clip range [{}, {}]",
                avg_q, q_clip_min, q_clip_max
            ));
        }

        // Loss history (epoch-level average)
        self.safety_loss_history.push_back(avg_loss);
        if self.safety_loss_history.len() > 30 {
            self.safety_loss_history.pop_front();
        }

        let n = train_step_count as f64;

        // Metrics aggregator
        let current_avg_reward = if !monitor.reward_history.is_empty() {
            monitor.reward_history.iter().sum::<f32>() / monitor.reward_history.len() as f32
        } else {
            0.0
        };
        self.metrics_aggregator.record(avg_loss, avg_q as f32, current_avg_reward);
        self.metrics_aggregator.record_gradient(avg_grad);

        if self.metrics_aggregator.should_log(&self.logging_config) {
            let aggregated = self.metrics_aggregator.aggregate_and_clear();
            log_training_progress(&aggregated);
        }

        // Gradient diagnostics at epoch boundary
        {
            let mut agent = self.agent.write().await;
            agent.log_diagnostics(avg_grad)
                .map_err(|e| {
                    tracing::info!("Early stopping (gradient collapse): {}", e);
                    anyhow::anyhow!("Early stopping: {}", e)
                })?;
        }

        // ── LearningHealth computation (Layer 1) ──────────────────────────────
        // Source signal adaptations vs. plan spec (A3 known deviations):
        //   - q_gap: mean of per_branch_q_gap_ema (4 branches) via fused_ctx —
        //     NOTE: per_branch_q_gap_ema() performs a synchronous stream flush + DtoH
        //     (cuStreamSynchronize + cuMemcpyDtoH_v2). Acceptable at epoch boundary
        //     because it runs once per epoch; DO NOT move into a per-step loop.
        //   - q_var: epoch_q_max - epoch_q_min proxy instead of QValueStatsResult.q_variance
        //     (q_variance is not separately tracked as a scalar field; range is a sufficient proxy).
        //   - grad_consistency: scalar proxy (successive grad_norm delta) instead of full Adam
        //     vector cosine similarity — per-component Adam buffers prevent flat-vector approach.
        //   - spectral_gap: coarse max/min ratio on q_readback_pinned instead of full cuSOLVER SVD.
        //   - ens_disagreement: 0.1 placeholder until D6/N6 wires real ensemble pairwise Q-gap.
        {
            use crate::cuda_pipeline::learning_health::HealthComponents;

            // Per-branch Q-gap mean as q_gap signal (4 values, epoch-scoped DtoH).
            let q_gap_signal: f32 = if let Some(ref fused) = self.fused_ctx {
                let gaps = fused.per_branch_q_gap_ema();
                ((gaps[0] + gaps[1] + gaps[2] + gaps[3]) * 0.25).max(0.0)
            } else {
                0.0
            };

            // q_var proxy: range (max - min) of per-step Q values this epoch. If no
            // steps ran (epoch_q_min still INFINITY), fall back to 0.0 rather than
            // propagating NaN/-inf into health normalization.
            let q_var_signal = if self.epoch_q_min.is_finite() && self.epoch_q_max.is_finite() {
                (self.epoch_q_max - self.epoch_q_min).max(0.0)
            } else {
                0.0
            };

            // Atom utilization from q_readback_pinned[6] (one-step lag).
            let atom_util = self.fused_ctx.as_ref()
                .map(|f| f.read_atom_utilization())
                .unwrap_or(0.0);

            // F3: Spectral gap — sigma_1/sigma_2 via rolling Gram + power iteration.
            let spectral_gap = self.fused_ctx.as_mut()
                .map(|f| f.compute_q_spectral_gap())
                .unwrap_or(1.0);

            let grad_norm = avg_grad.abs();

            // Ensemble disagreement: dispersion across the 4 per-branch Q-gaps as a
            // proxy for true per-head ensemble disagreement (per-head Q readback isn't
            // wired in this codebase). When all branches collapse to similar Q-gap, the
            // range shrinks → low disagreement → D6/N6 oracle score rises. When
            // branches preserve diverse Q-gaps (healthy), the range is wider. Maps
            // directly into the smoothstep(0.01, 0.1) thresholds in from_raw and the
            // D6 oracle's own smoothstep with the same window.
            let ens_disagreement = if let Some(ref fused) = self.fused_ctx {
                let gaps = fused.per_branch_q_gap_ema();
                let mut gmin = f32::INFINITY;
                let mut gmax = f32::NEG_INFINITY;
                for &g in &gaps {
                    if g.is_finite() {
                        if g < gmin { gmin = g; }
                        if g > gmax { gmax = g; }
                    }
                }
                if gmin.is_finite() && gmax.is_finite() { (gmax - gmin).max(0.0) } else { 0.1 }
            } else {
                0.1
            };

            // Update EMAs.
            self.health_ema.update(q_gap_signal, q_var_signal, grad_norm);
            // F2: true vector cosine across aggregated Adam m-state, with scalar fallback.
            let grad_consistency = if let Some(ref fused) = self.fused_ctx {
                match fused.read_adam_m_flat_sample(256) {
                    Ok(vec) if !vec.is_empty() => self.health_ema.update_grad_consistency_vec(&vec),
                    _ => self.health_ema.update_grad_consistency(grad_norm),  // fall back to scalar
                }
            } else {
                self.health_ema.update_grad_consistency(grad_norm)
            };

            let raw = HealthComponents {
                q_gap: self.health_ema.q_gap_ema,
                q_var: self.health_ema.q_var_ema,
                atom_util,
                grad_norm: self.health_ema.grad_norm_ema,
                ens_disagreement,
                grad_consistency,
                spectral_gap,
            };
            let health_value = self.learning_health.update(&raw);

            // SP5 Layer D Task D4 (2026-05-02): atomic D2 wiring. After the
            // host-side `LearningHealth::update` produces its scalar (kept for
            // the host-scalar consumers — HEALTH_DIAG, PPO/eval read paths,
            // host-side warmup/clamp wrapper that lives outside D4 scope per
            // `feedback_no_partial_refactor.md`), launch
            // `health_composition_kernel` as a PARALLEL GPU producer so
            // ISV[290..294) (composed health_score + 3 normalised intermediates)
            // are populated for any GPU-side consumer that prefers the
            // Pearl-A+D-smoothed view. Per `feedback_no_cpu_compute_strict.md`
            // the kernel reproduces the same `from_raw` + `compose` chain
            // bit-for-bit so behavior is preserved within float precision
            // modulo the additional Pearls smoothing layer.
            if let Some(ref fused) = self.fused_ctx {
                if let Err(e) = fused.trainer().launch_health_composition(
                    raw.q_gap,
                    raw.q_var,
                    raw.atom_util,
                    raw.grad_norm,
                    raw.ens_disagreement,
                    raw.grad_consistency,
                    raw.spectral_gap,
                ) {
                    tracing::warn!(error = %e, "SP5 D4 health_composition launch failed");
                }
            }

            // Cache for later tasks (A4 logging, B1, B4, G4 etc.)
            self.last_q_gap = Some(raw.q_gap);
            self.last_q_var = Some(raw.q_var);
            self.last_grad_norm = Some(raw.grad_norm);
            self.last_ens_disagreement = Some(raw.ens_disagreement);
            self.last_atom_util = Some(raw.atom_util);

            // Broadcast to GPU at ISV[LEARNING_HEALTH_INDEX=12].
            if let Some(ref fused) = self.fused_ctx {
                fused.write_isv_signal_at(
                    crate::cuda_pipeline::gpu_dqn_trainer::LEARNING_HEALTH_INDEX,
                    health_value,
                );
            }

            // B1/G2: propagate last cql_alpha_eff for logging.
            if let Some(ref fused) = self.fused_ctx {
                self.last_cql_alpha_eff = Some(fused.last_cql_alpha_eff());
            }

            // Plan 1 Task 13: tau_eff now read from ISV[TAU_EFF_INDEX] (GPU-computed).
            if let Some(ref fused) = self.fused_ctx {
                use crate::cuda_pipeline::gpu_dqn_trainer::TAU_EFF_INDEX;
                self.last_tau_eff = Some(fused.trainer().read_isv_signal_at(TAU_EFF_INDEX));
            }

            // B3/G4: propagate SARSA tau factor for logging.
            if let Some(ref fused) = self.fused_ctx {
                self.last_sarsa_tau_factor = Some(fused.last_sarsa_tau_factor());
            }

            // B4/G5 + SP7 (2026-05-03): propagate adaptive gradient budgets
            // for logging. CQL/C51 reads pull from `lb_budget_effective_buf`
            // (the GPU-dispatched values that actually fed the SAXPY/scale
            // ops this step) — not the host-cached fields, which were
            // populated by `compute_adaptive_budgets` at CUDA-Graph capture
            // time and would freeze at the cold-start bootstrap. IQN/ENS
            // still come from host caches (out-of-scope for SP7 capture-
            // freeze fix per audit Fix 33).
            if let Some(ref fused) = self.fused_ctx {
                self.last_iqn_budget_eff = Some(fused.last_iqn_budget_eff());
                self.last_cql_budget_eff = Some(fused.last_cql_budget_eff());
                self.last_c51_budget_eff = Some(fused.last_c51_budget_eff());
                // SP6 Pearl 2: cache per-branch budget arrays.
                self.last_c51_budget_per_branch = Some(fused.last_c51_budget_per_branch());
                self.last_iqn_budget_per_branch = Some(fused.trainer().last_iqn_budget_per_branch);
                self.last_cql_budget_per_branch = Some(fused.last_cql_budget_per_branch());
            }

            // Plan 2 D.2: propagate direction-branch gamma_eff from ISV for logging.
            if let Some(ref fused) = self.fused_ctx {
                self.last_gamma_eff = Some(fused.trainer().read_isv_signal_at(GAMMA_DIR_EFF_INDEX));
            }

            // D1/N1: snapshot high-q_gap checkpoints and pull toward best during collapse.
            //
            // Snapshot gate: purely q_gap-based (see SnapshotRing::maybe_snapshot). q_gap
            // is the direct measure of directional Q-differentiation — the thing distillation
            // exists to preserve. The previous health-gate + winrate-fallback combo missed
            // captures in runs where health's multi-component EMA was degenerate despite
            // strong q_gap peaks, or where high-winrate epochs didn't overlap with
            // high-q_gap epochs. Using q_gap directly eliminates both indirection layers.
            //
            // Distill trigger (separate from snapshot gate): fire when health < 0.55 OR
            // meta-Q predicts collapse > 0.5. Distill fires per-step from the aux-op lane
            // (see fused_training::submit_aux_ops) with alpha amortized across the epoch.
            if let Some(ref mut fused) = self.fused_ctx {
                // Snapshot is the ONLY host-side decision at epoch boundary —
                // the SAXPY kernel itself runs per-step from the aux-op phase
                // and reads ISV[LEARNING_HEALTH_INDEX] directly to compute
                // alpha in-kernel. No CPU-side alpha plumbing required.
                //
                // Use raw epoch-max q_gap (same value as the "Epoch N/10:
                // Q-gap=…" log line), NOT self.last_q_gap which is the
                // health EMA — the EMA has been degenerate (stuck at 0) in
                // observed runs even when the raw q_gap peaked above 1.0.
                let q_gap_for_snapshot = self.epoch_q_gap;
                let _ = fused.maybe_snapshot_qnet(
                    health_value, q_gap_for_snapshot, epoch as u32,
                );
                self.last_distill_active = Some(fused.last_distill_active());
            }

            // D2/N2 + F5: Q-gap barrier constraint.
            // min_required = 0.05 × health; barrier_loss = 0.5 × max(0, min_required − q_gap)².
            // F5: gradient is LIVE — per-step, the barrier kernel injects gradient into the
            // direction branch logits (via cql_d_* buffers) whenever barrier > 0, piggybacking
            // on the CQL backward in submit_aux_ops. This epoch-boundary block just records
            // the scalar for HEALTH_DIAG logging.
            {
                let q_gap_current = self.health_ema.q_gap_ema;
                let min_required = 0.05_f32 * health_value;
                let barrier = (min_required - q_gap_current).max(0.0);
                self.last_barrier_loss = Some(0.5 * barrier * barrier);
            }

            // D3/N3: Health-triggered plasticity injection.
            // Track consecutive unhealthy epochs; after 3, run shrink_perturb to break
            // frozen representations and reset the counter.
            //
            // Trigger conditions (OR semantics):
            //   - health_value < 0.3  (generic training-health collapse)
            //   - action_entropy < 0.3  (policy-narrowing / action-collapse — a form
            //     of representational freeze the health metric can miss when Q-values
            //     separate but the argmax stays pinned to a single branch)
            {
                let low_entropy = self.last_action_entropy.map(|e| e < 0.3).unwrap_or(false);
                let is_unhealthy = health_value < 0.3 || low_entropy;
                if is_unhealthy {
                    self.unhealthy_epoch_count += 1;
                } else {
                    self.unhealthy_epoch_count = 0;
                }

                if self.unhealthy_epoch_count >= 3 {
                    tracing::info!(
                        "Plasticity injection triggered: health={:.2} entropy={:.2} for {} consecutive epochs",
                        health_value,
                        self.last_action_entropy.unwrap_or(1.0),
                        self.unhealthy_epoch_count
                    );
                    if let Some(ref mut fused) = self.fused_ctx {
                        let alpha = self.hyperparams.shrink_perturb_alpha as f32;
                        let sigma = self.hyperparams.shrink_perturb_sigma as f32;
                        match fused.shrink_and_perturb(alpha, sigma) {
                            Ok(()) => tracing::info!(epoch, alpha, sigma, "D3/N3 health-triggered shrink_and_perturb applied"),
                            Err(e) => tracing::warn!(epoch, "D3/N3 shrink_and_perturb failed (non-fatal): {e}"),
                        }
                    }
                    self.unhealthy_epoch_count = 0; // reset after trigger
                    self.last_plasticity_ready = Some(false); // cooldown state
                } else {
                    self.last_plasticity_ready = Some(true);
                }
            }

            // D5/N5: Information Bottleneck penalty (scalar-only — informational).
            // IB penalizes lack of state-dependence in Q (low variance across batch).
            // Minimum viable: host-side scalar computed from cached q_var_ema, surfaced
            // via HEALTH_DIAG as `ib={..}`. Full backward-through-variance is deferred.
            {
                let ib_weight = 0.05_f32 * (1.0 - health_value).max(0.0);
                let min_q_var = 0.01_f32;
                let q_var_current = self.health_ema.q_var_ema;
                let ib_penalty_raw = (min_q_var - q_var_current).max(0.0);
                self.last_ib_penalty = Some(ib_weight * ib_penalty_raw);
            }

            // D6/N6: Ensemble as collapse oracle.
            // High ensemble_collapse_score ⇒ ensembles agree (often on uniform during collapse).
            // When score > 0.8 AND we haven't already triggered N3 recently, force a plasticity
            // injection — this shortcut catches collapse even when the moving-average of
            // individual health inputs hasn't accumulated to 3 unhealthy epochs yet.
            // Uses last_ens_disagreement (Path C scalar proxy — populated by A3 readback).
            {
                use crate::cuda_pipeline::learning_health::smoothstep;
                let mean_pairwise_gap = self.last_ens_disagreement.unwrap_or(0.1);
                let ensemble_collapse_score = 1.0 - smoothstep(0.01, 0.1, mean_pairwise_gap);
                self.last_ensemble_collapse_score = Some(ensemble_collapse_score);

                // D6 firing conditions:
                //   1. Warmup guard — wait 5 epochs (3 warmup + 2 buffer) so per-branch
                //      gaps have a chance to differentiate from random init.
                //   2. Not already on the D3 unhealthy-streak plasticity path.
                //   3. Temporal outcome agreement — the signal alone is not enough; we
                //      require evidence the *outcome* is actually bad. Either the most
                //      recent WinRate is < 0.45 (network is losing trades) OR the
                //      meta-Q predictor signals collapse-imminent (> 0.5). Without
                //      this gate, D6 destroys models that have converged to a
                //      consistent, profitable policy (low per-branch variance can mean
                //      either collapse OR convergence — WinRate/meta-Q disambiguate).
                let outcome_bad = self.last_epoch_win_rate < 0.45
                    || self.last_meta_q_pred.unwrap_or(0.0) > 0.5;
                if ensemble_collapse_score > 0.8
                    && self.unhealthy_epoch_count < 3
                    && self.learning_health.epoch > 5
                    && outcome_bad {
                    tracing::info!(
                        "Ensemble collapse detected (score={:.2}): forcing plasticity injection",
                        ensemble_collapse_score
                    );
                    if let Some(ref mut fused) = self.fused_ctx {
                        let alpha = self.hyperparams.shrink_perturb_alpha as f32;
                        let sigma = self.hyperparams.shrink_perturb_sigma as f32;
                        match fused.shrink_and_perturb(alpha, sigma) {
                            Ok(()) => {
                                self.unhealthy_epoch_count = 0;
                                self.last_plasticity_ready = Some(false);
                            }
                            Err(e) => tracing::warn!("D6/N6 shrink_and_perturb failed: {e}"),
                        }
                    }
                }
            }

            // D7/N7: Contrarian override state machine.
            // When WinRate < 0.40 for 5 consecutive epochs AND health < 0.3, flip to argmin
            // for 2 epochs to escape the mirror-image attractor. WinRate >= 0.45 resets.
            // Note: last_epoch_win_rate is carried from previous epoch's financials computation
            // (which runs after process_epoch_boundary). Epoch 0 defaults to 0.5 (no trigger).
            {
                let winrate = self.last_epoch_win_rate;

                if winrate < 0.40 {
                    self.low_winrate_count += 1;
                } else if winrate >= 0.45 {
                    self.contrarian_active = false;
                    self.low_winrate_count = 0;
                }

                if self.low_winrate_count >= 5 && health_value < 0.3 && !self.contrarian_active {
                    tracing::warn!(
                        "CONTRARIAN OVERRIDE activated: WinRate={:.1}% for {} epochs, health={:.2}",
                        winrate * 100.0, self.low_winrate_count, health_value
                    );
                    self.contrarian_active = true;
                    self.contrarian_remaining_epochs = 2;
                }

                if self.contrarian_active {
                    if self.contrarian_remaining_epochs == 0 {
                        tracing::info!(
                            "CONTRARIAN OVERRIDE deactivated: window expired"
                        );
                        self.contrarian_active = false;
                    } else {
                        self.contrarian_remaining_epochs -= 1;
                    }
                }

                self.last_contrarian_active = Some(self.contrarian_active);

                // D7/N7 Part B: forward the flag to the experience collector so the
                // kernel negates Q values during Boltzmann sampling.
                if let Some(ref mut collector) = self.gpu_experience_collector {
                    collector.set_contrarian_active(self.contrarian_active);
                }
            }

            // D8/N8: Meta-Q collapse predictor — leading indicator, NOT fed back into LearningHealth.
            {
                use crate::cuda_pipeline::meta_q_network::{MetaQInput, META_Q_LOOKAHEAD};

                // Build input from current health components. regime_stability read via fused_ctx.
                let (_, regime_stability) = if let Some(ref fused) = self.fused_ctx {
                    fused.read_isv_regime_via_trainer()
                } else {
                    (0.5_f32, 0.5_f32)
                };

                let meta_q_input = MetaQInput {
                    q_gap_ema: self.health_ema.q_gap_ema,
                    log_grad_norm: self.health_ema.grad_norm_ema.max(1e-3).ln(),
                    atom_util: self.last_atom_util.unwrap_or(0.0),
                    ens_disagreement: self.last_ens_disagreement.unwrap_or(0.1),
                    regime_stability,
                    spectral_gap_norm: self.learning_health.components.spectral_gap_norm,
                    grad_consistency: self.learning_health.components.grad_consistency_norm,
                };

                // Predict collapse probability
                let meta_q_pred = self.meta_q.predict(&meta_q_input);
                self.last_meta_q_pred = Some(meta_q_pred);
                // Push the prediction into the GPU trainer so F4/F5 aux-op kernels
                // can amplify their weights based on predicted collapse.
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.set_meta_q_pred(meta_q_pred);
                }

                // Push current (input, epoch) into pending — label assigned K epochs later
                let cur_epoch_u32 = self.current_epoch as u32;
                self.pending_meta_q_samples.push_back((meta_q_input, cur_epoch_u32));

                // Track health history for label resolution
                if self.health_history.len() >= 2 * META_Q_LOOKAHEAD {
                    self.health_history.pop_front();
                }
                self.health_history.push_back((cur_epoch_u32, health_value));

                // Resolve labels for samples that are K epochs old
                while let Some((pending_input, pending_epoch)) = self.pending_meta_q_samples.front().cloned() {
                    if cur_epoch_u32 < pending_epoch + META_Q_LOOKAHEAD as u32 { break; }
                    let collapsed = self.health_history.iter()
                        .filter(|(ep, _)| *ep > pending_epoch && *ep <= pending_epoch + META_Q_LOOKAHEAD as u32)
                        .any(|(_, h)| *h < 0.3);
                    let label = if collapsed { 1.0 } else { 0.0 };
                    self.meta_q.push_sample(pending_input, label);
                    self.pending_meta_q_samples.pop_front();
                }

                // Train one step
                self.meta_q.train_step();
            }

            // HEALTH_DIAG: components are [0, 1] normalized. effective = hyperparams after health-adaptation. novels = mechanism states.
            // diag = pure monitoring signals (not in reward): sharpe EMA (formerly w_dsr
            // reward input) and action entropy normalized to [0,1] (formerly
            // position_entropy_weight reward). See Phase 1 inventory better-form taxonomy.
            // gems = G12 predictive_coding loss (active in training). G6 + G10
            // were measured (commit 61ab27ff3) and found redundant with existing
            // mechanisms (NoisyNets, spectral_norm); their readbacks were removed.
            let g12_loss = if let Some(ref fused) = self.fused_ctx {
                fused.read_gem_g12_loss()
            } else {
                0.0
            };
            // Task 0.4 — per-branch grad-norm ratio (magnitude/direction). H4 signal.
            // Read at the TOP of `process_epoch_boundary` (before avg_q's eval
            // forward replay wipes grad_buf via the captured memset) and cached.
            let grad_ratio_mag_dir = grad_ratio_mag_dir_cached;
            // Task 2.0 confirmation — absolute per-branch norms (dir, mag).
            // `grad_ratio_mag_dir` coerces to 0.0 whenever dir < 1e-9, so the
            // ratio alone can't tell "direction starved" from "magnitude zero".
            // Surface the raw L2 norms here so HEALTH_DIAG can print both in
            // scientific notation, distinguishing 1e-12 from exact zero.
            // Cached at the TOP of `process_epoch_boundary` (same reason as
            // the ratio above — eval-forward replay zeroes grad_buf).
            let grad_dir_abs = grad_dir_abs_cached;
            let grad_mag_abs = grad_mag_abs_cached;

            // Task 2.0 (+ extension) — per-component grad decomposition across
            // 9 measurement points: IQN / CQL / CQL-SAXPY / C51 / C51-budget-
            // scale / Distillation / Recursive-confidence / Predictive-coding /
            // Ensemble. Refresh the cached norms from the pinned result slot
            // populated by the in-graph reduction kernel on the last step of
            // this epoch, then compute mag/dir ratios per component. Non-fatal
            // on failure (warn + stale values) — same pattern as
            // update_q_mag_means_cached.
            let (
                grad_mag_iqn, grad_mag_cql, grad_mag_cql_sx,
                grad_mag_c51, grad_mag_c51_bs,
                grad_mag_distill, grad_mag_rec, grad_mag_pred,
                grad_mag_ens,
                // Task 2.Z — absolute trunk-slice L2 norms per component
                // (iqn, cql, cql_sx, c51, c51_bs, distill, rec, pred, ens).
                grad_trunk_norms,
            ) = if let Some(fused) = self.fused_ctx.as_mut() {
                if let Err(e) = fused.refresh_grad_component_norms() {
                    tracing::warn!(
                        "HEALTH_DIAG grad_component_norms refresh failed: {e}"
                    );
                }
                (
                    fused.grad_mag_iqn_ratio(),
                    fused.grad_mag_cql_ratio(),
                    fused.grad_mag_cql_sx_ratio(),
                    fused.grad_mag_c51_ratio(),
                    fused.grad_mag_c51_bs_ratio(),
                    fused.grad_mag_distill_ratio(),
                    fused.grad_mag_rec_ratio(),
                    fused.grad_mag_pred_ratio(),
                    fused.grad_mag_ens_ratio(),
                    fused.grad_trunk_norms_by_component(),
                )
            } else {
                (0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32,
                 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32,
                 [0.0_f32; 9])
            };

            // Task 0.6 — per-branch NoisyNets σ mean. H7 detection signal.
            // 0 = direction head, 1 = magnitude head.
            let (sigma_mag, sigma_dir) = {
                let agent = self.agent.read().await;
                (
                    agent.branch_noisy_sigma_mean(1),
                    agent.branch_noisy_sigma_mean(0),
                )
            };
            // Track 4 sigma_mean = overall NoisyNets σ across mag + dir branches.
            let sigma_mean_overall = (sigma_mag + sigma_dir) * 0.5;

            // E.5 Plan 4 Task 5 Mode A + follow-up: attention-focus + target-drift
            // ISV diagnostics. Launch BEFORE HEALTH_DIAG emit + before the VSN/drift
            // reads below so the ISV slots reflect the current epoch. Both kernels
            // are GPU-only (per spec §4.C.6 + pearl_cold_path_no_exception_to_gpu_drives):
            //   * VSN mag/dir slices: GPU-reduced from the live params buffer → ISV[87,88]
            //   * Mamba2 retention: GPU-reduced from mamba2_h_enriched → ISV[89]
            //   * Target drift mag/dir: GPU-reduced RMS(target - online) → ISV[92,93]
            // Diagnostic only.
            if let Some(ref f) = self.fused_ctx {
                let bs = f.batch_size();
                let (vsn_mag_start, vsn_mag_len, vsn_dir_start, vsn_dir_len)
                    = f.vsn_branch_slice_indices();
                let (br_mag_start, br_mag_len, br_dir_start, br_dir_len)
                    = f.branch_param_slice_indices();
                let (mamba2_ptr, mamba2_n) = f.mamba2_h_enriched_device(bs);
                let params_ptr = f.params_buf_device();
                let target_params_ptr = f.target_params_buf_device();
                if let Some(ref mut c) = self.gpu_experience_collector {
                    let ema_alpha = 0.05_f32;
                    if let Err(e) = c.launch_attention_focus_ema_inplace(
                        params_ptr,
                        vsn_mag_start, vsn_mag_len, vsn_dir_start, vsn_dir_len,
                        mamba2_ptr, mamba2_n,
                        ema_alpha,
                    ) {
                        tracing::warn!("E.5 attention_focus_ema launch failed: {e}");
                    }
                    if let Err(e) = c.launch_target_drift_ema_inplace(
                        params_ptr, target_params_ptr,
                        br_mag_start, br_mag_len, br_dir_start, br_dir_len,
                        ema_alpha,
                    ) {
                        tracing::warn!("target_drift_ema launch failed: {e}");
                    }
                }
            }

            // Read VSN + drift scalars from ISV slots (replaces legacy CPU-DtoH
            // per_branch_vsn_mean / per_branch_target_drift). The two GPU EMA
            // kernels above wrote to pinned device-mapped ISV memory; host reads
            // are coherent without explicit DtoH.
            let (vsn_mag, vsn_dir, drift_mag, drift_dir) = {
                use crate::cuda_pipeline::gpu_dqn_trainer::{
                    VSN_MAG_EMA_INDEX, VSN_DIR_EMA_INDEX,
                    TARGET_DRIFT_MAG_EMA_INDEX, TARGET_DRIFT_DIR_EMA_INDEX,
                };
                if let Some(ref f) = self.fused_ctx {
                    (
                        f.read_isv_signal_at(VSN_MAG_EMA_INDEX),
                        f.read_isv_signal_at(VSN_DIR_EMA_INDEX),
                        f.read_isv_signal_at(TARGET_DRIFT_MAG_EMA_INDEX),
                        f.read_isv_signal_at(TARGET_DRIFT_DIR_EMA_INDEX),
                    )
                } else {
                    (0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32)
                }
            };

            // Task 0.3 — per-magnitude Q-value buckets (q_full / q_half /
            // q_quarter). Host-side mean of the magnitude-branch columns of
            // `q_out_buf` (cols [b0 .. b0+b1]) averaged over the batch. Cold
            // path: a single dtoh of ~B*total_actions floats at epoch
            // boundary, invoked here via `update_q_mag_means_cached` — values
            // are then read from the cached `[q_full, q_half, q_quarter]`.
            let q_buckets = if let Some(fused) = self.fused_ctx.as_mut() {
                match fused.update_q_mag_means_cached() {
                    Ok(()) => fused.q_magnitude_bucket_means(),
                    Err(e) => {
                        tracing::warn!(
                            "HEALTH_DIAG q_magnitude_bucket_means refresh failed: {e}"
                        );
                        fused.q_magnitude_bucket_means()
                    }
                }
            } else {
                [0.0_f32; 3]
            };
            let q_full = q_buckets[0];
            let q_half = q_buckets[1];
            let q_quarter = q_buckets[2];

            // Task 0.3 — var_scale_mean / kelly_f_mean / avg_win_ratio.
            //
            // * `var_scale_mean`: epoch mean of per-sample `1 / (1 + sqrt(Var[Q]))`
            //   written by `experience_env_step` at line 1422 to the collector's
            //   `var_scale_per_sample` buffer. The host reducer averages only
            //   over samples where the kernel wrote a positive value (i.e.
            //   q_variance was live for that sample); unreached / Var[Q]-disabled
            //   slots stay at the default 0.0f and are filtered out. A returned
            //   0.0 means "no variance scaling happened this epoch" — the honest
            //   semantic, not a stub.
            //
            // * `kelly_f_mean` + `avg_win_ratio`: computed from the most
            //   recent `TradeStats` (aggregated ps[14..18] across episodes).
            //   `trade_stats_history` is appended to later in THIS same
            //   epoch — so on epoch `e` the last entry holds epoch `e-1`'s
            //   trade stats (empty on epoch 0 — both fields = 0). Same
            //   Kelly formula as the kernel: `b = avg_win / avg_loss`,
            //   `kelly_f = (b·p − (1−p))/b`, clamped [0,1], then half-Kelly.
            //   `avg_win_ratio = (sum_wins/win_count) / max(sum_losses/loss_count, 0.001)`
            //   per plan formula (line 239).
            let var_scale_mean: f32 = match self.gpu_experience_collector.as_mut() {
                Some(c) => match c.var_scale_epoch_mean() {
                    Ok(v) => v,
                    Err(e) => {
                        tracing::warn!("HEALTH_DIAG var_scale_epoch_mean readback failed: {e}");
                        0.0
                    }
                },
                None => 0.0,
            };
            let (kelly_f_mean, avg_win_ratio) = if let Some(ts) = self.trade_stats_history.last() {
                let w = ts.winning_trades as f64;
                let l = ts.losing_trades as f64;
                let total = w + l;
                if total > 0.0 && w > 0.0 && l > 0.0 {
                    let avg_win = ts.sum_wins / w;
                    let avg_loss = ts.sum_losses / l;
                    let win_rate = w / total;
                    let b = avg_win / avg_loss.max(0.001);
                    let kelly_raw = (b * win_rate - (1.0 - win_rate)) / b.max(0.001);
                    let kelly_clamped = kelly_raw.clamp(0.0, 1.0);
                    let kelly_half = 0.5 * kelly_clamped;
                    let ratio = avg_win / avg_loss.max(0.001);
                    (kelly_half as f32, ratio as f32)
                } else {
                    (0.0_f32, 0.0_f32)
                }
            } else {
                (0.0_f32, 0.0_f32)
            };

            // Download GPU monitoring summary and populate the local per-epoch
            // monitor BEFORE the HEALTH_DIAG block reads `monitor.action_counts`.
            // Previously this block ran after HEALTH_DIAG, so every epoch's HEALTH_DIAG
            // saw zero action_counts and dist_q/h/f + ent_dir were always 0.
            if let Some(ref mon) = self.gpu_monitoring {
                if let Ok(summary) = mon.download_summary() {
                    if summary.total_experiences > 0 {
                        info!(
                            "GPU epoch summary: mean_reward={:.6}, std={:.6}, sharpe={:.3}, trades={}, actions={:?}",
                            summary.mean_reward, summary.reward_std, summary.sharpe_estimate,
                            summary.total_trades, summary.action_counts
                        );
                        // Diagnostic: Shannon entropy of action distribution, normalized to [0, 1].
                        // Replaces the deleted position_entropy_weight reward — narrowing policy
                        // is a signal to monitor and (optionally) trigger plasticity injection,
                        // not to reward directly. Populated now so THIS epoch's HEALTH_DIAG emits it.
                        let total_actions: usize = summary.action_counts.iter().sum();
                        let entropy_norm = if total_actions > 0 {
                            let n_bins = summary.action_counts.len() as f32;
                            let ln_n = n_bins.max(2.0).ln();
                            let total_f = total_actions as f32;
                            let mut h = 0.0_f32;
                            for &count in summary.action_counts.iter() {
                                if count > 0 {
                                    let p = count as f32 / total_f;
                                    h -= p * p.ln();
                                }
                            }
                            (h / ln_n).clamp(0.0, 1.0)
                        } else {
                            0.0
                        };
                        self.last_action_entropy = Some(entropy_norm);
                        monitor.track_reward(summary.mean_reward);
                        self.observed_reward_std = summary.reward_std;
                        self.observed_reward_mean = summary.mean_reward;
                        // Feed all 3 branch distributions from GPU into the monitor.
                        // These are the actual model-selected actions, not
                        // deterministic OrderRouter re-derivations.
                        for (idx, &count) in summary.action_counts.iter().enumerate() {
                            monitor.action_counts[idx] += count;
                        }
                        for (idx, &count) in summary.order_counts.iter().enumerate() {
                            monitor.order_type_counts[idx] += count;
                        }
                        for (idx, &count) in summary.urgency_counts.iter().enumerate() {
                            monitor.urgency_counts[idx] += count;
                        }

                        // SP8 (Fix 36, 2026-05-03): GPU-only train_active_frac
                        // canary producer. Runs AFTER `mon.download_summary()`
                        // (which syncs the monitoring reducer's collector stream
                        // — settling the GPU summary buffer device-visibly), so
                        // the trainer-stream producer reads a stable
                        // `monitoring_summary[5..17)` block. Replaces the host-
                        // side compute previously at this site (deleted, see
                        // commit body for Fix 36) per
                        // `feedback_no_cpu_compute_strict.md`. Pearls A+D
                        // smooth into ISV[TRAIN_ACTIVE_FRAC_INDEX=321] which
                        // drives the downstream `loss_balance_max_budget_compute`
                        // each training step.
                        if let Some(ref fused) = self.fused_ctx {
                            let summary_dev_ptr = mon.summary_dev_ptr();
                            if let Err(e) = fused.trainer()
                                .launch_train_active_frac_compute(summary_dev_ptr)
                            {
                                tracing::warn!(error = %e,
                                    "SP8 train_active_frac_compute launch failed");
                            }
                            // SP9 (Fix 37, 2026-05-03): intent vs eval Full
                            // divergence producer — reads the same monitoring
                            // summary block plus the SP9 `EVAL_DIST_F_INDEX`
                            // slot (populated each val window from
                            // `consume_validation_loss`). Per-epoch cadence
                            // matches `train_active_frac` since both feed the
                            // same loss-balance / Kelly-cap controller chain.
                            // When `EVAL_DIST_F_INDEX` is still at fold-reset
                            // sentinel zero (pre-first-validation), the
                            // divergence kernel writes EPS-floored values that
                            // Pearl A's bootstrap will replace on first real
                            // observation.
                            if let Err(e) = fused.trainer()
                                .launch_intent_eval_divergence_compute(summary_dev_ptr)
                            {
                                tracing::warn!(error = %e,
                                    "SP9 intent_eval_divergence_compute launch failed");
                            }
                            // SP9 (Fix 37, 2026-05-03): Kelly cold-start
                            // warmup-floor producer chain (5 launches).
                            // Drives ISV[KELLY_WARMUP_FLOOR_INDEX=330]
                            // consumed by `unified_env_step_core` in
                            // `trade_physics.cuh` as an adaptive max-floor
                            // on the Kelly cap. Per-epoch cadence — the
                            // floor is read by experience-collection rollouts
                            // that fire after this metrics block. Per
                            // `pearl_cold_start_exit_signal_or.md`: OR'd
                            // statistical / behavioral / temporal
                            // confidence relaxes the floor as any single
                            // axis fires.
                            if let Err(e) = fused.trainer()
                                .launch_sp9_kelly_warmup_floor()
                            {
                                tracing::warn!(error = %e,
                                    "SP9 kelly_warmup_floor producer chain launch failed");
                            }

                            // C.2 Reward-component attribution EMA (Plan 3
                            // Task 1, spec §4.C.2). GPU-side: launch
                            // `reward_component_ema` kernel (single-block,
                            // 6 threads). Reads `reward_components_per_sample`
                            // [N*L, 6] written by `collect_experiences_gpu`,
                            // reduces mean(|r_c|) into ISV[63..69) (popart at
                            // 63, cf 64, trail 65, micro 66, opp_cost 67,
                            // bonus 68) AND per-component variance via
                            // Welford into ISV[362..367) (cf var 362 ..
                            // bonus var 366; popart-var lives at 361 and is
                            // produced by `launch_sp11_popart_component_ema`
                            // below). Resets the per-sample component buffer.
                            //
                            // SP11 launch-order fix-up (2026-05-04): MUST run
                            // BEFORE the SP11 canary chain below — the mag-
                            // ratio canary at `launch_sp11_mag_ratio_compute`
                            // reads ISV[REWARD_POPART_EMA_INDEX+1..+6) =
                            // [64..68] (mag) and ISV[REWARD_COMPONENT_VAR_
                            // EMA_BASE+1..+6) = [362..367) (var) for the
                            // non-popart axes (1..5). Previously this kernel
                            // launched at the C.2 site below the canary,
                            // leaving those slots at sentinel-0 on ep1 (ep0
                            // had no segment_complete fires) → z[c]=0 for
                            // c=1..5 → ratio[0]=1.0 → popart dominates →
                            // controller saturates → sharpe collapses. Smoke
                            // `smoke-test-4rbv9` on b3b4d0278 confirmed the
                            // structurally-no-op behaviour. The kernel itself
                            // is unchanged; only the launch site moved.
                            // Other launches at the original C.2 site
                            // (trade_attempt_rate, plan_threshold,
                            // seed_step_counter, cql_alpha_seed) stay there —
                            // different consumers, no pre-canary constraint.
                            // Per `feedback_no_partial_refactor.md`, this is
                            // a single atomic move. ISV pointer must be
                            // non-zero; noop otherwise (no collector or ISV
                            // not set).
                            {
                                let n_main = self.gpu_experience_collector
                                    .as_ref()
                                    .map(|c| c.alloc_episodes() * c.alloc_timesteps())
                                    .unwrap_or(0);
                                if let Some(ref mut c) = self.gpu_experience_collector {
                                    if let Err(e) =
                                        c.launch_reward_component_ema_inplace(n_main)
                                    {
                                        tracing::warn!(
                                            "C.2 reward_component_ema launch failed \
                                             (SP11 canary input): {e}"
                                        );
                                    }
                                }
                            }

                            // SP11 Fix 39 B1b fix-up (2026-05-04): popart-
                            // component-specific magnitude EMA producer.
                            // Reads the per-bar `popart_component_per_sample`
                            // buffer (wired in via
                            // `set_sp11_popart_component_buf` above) and
                            // block-tree-reduces mean(|x|) into ISV[POPART_
                            // COMPONENT_MAG_EMA_INDEX=360] via Pearls A+D.
                            // MUST run BEFORE `launch_sp11_mag_ratio_compute`
                            // below so the canary's first read of slot 360
                            // sees a populated value (or sentinel 0 on the
                            // very first step — Pearl A bootstrap fires
                            // first-observation replacement on the second).
                            // Resolves the slot-63 PopArt-input vs popart-
                            // component-magnitude overload that B1b smoke
                            // surfaced. Spec §4 amendment "Why slot 360".
                            if let Err(e) = fused.trainer()
                                .launch_sp11_popart_component_ema()
                            {
                                tracing::warn!(error = %e,
                                    "SP11 popart_component_ema launch failed");
                            }

                            // SP11 Fix 39 (2026-05-04, A1.3 + B1b fix-up):
                            // per-component reward-magnitude-ratio canary
                            // producer. Post-fix-up reads NON-CONTIGUOUS
                            // sources:
                            //   axis 0 (popart) ← ISV[POPART_COMPONENT_MAG_EMA_INDEX=360]
                            //   axis 1..5      ← ISV[REWARD_POPART_EMA_INDEX+1..+6)
                            //                    (cf=64, trail=65, micro=66,
                            //                     opp_cost=67, bonus=68)
                            // Pearls A+D smooth into
                            // ISV[REWARD_COMPONENT_MAG_RATIO_BASE..+6) and
                            // ISV[PNL_REWARD_MAGNITUDE_EMA_INDEX=359]
                            // (PnL signal scale = popart-component
                            // magnitude post-fix-up, not contaminated
                            // total-reward magnitude). Per-epoch cadence —
                            // matches the SP4 reward-component EMA producer
                            // firing pattern.
                            if let Err(e) = fused.trainer()
                                .launch_sp11_mag_ratio_compute()
                            {
                                tracing::warn!(error = %e,
                                    "SP11 mag_ratio_compute launch failed");
                            }

                            // SP13 v3 P0a.T3 (2026-05-04): Hold-pricing
                            // controller. Reads the observed Hold-pick rate
                            // EMA at ISV[HOLD_RATE_OBSERVED_EMA_INDEX=382]
                            // (populated by `hold_rate_observer_kernel` +
                            // chained Pearls A+D — observer launch wires
                            // alongside the dir_acc launch in P0a.T4) and
                            // the static Hold-rate target at
                            // ISV[HOLD_RATE_TARGET_INDEX=381] (constructor-
                            // initialised to 0.20). Computes the priced
                            // per-bar Hold cost
                            //   excess  = max(0, observed − target)
                            //   cost    = HOLD_COST_BASE × (1 + 5 × excess)
                            //   cost    = clamp(cost, base × 0.5, base × 5.0)
                            // and writes it back to ISV[HOLD_COST_INDEX=380].
                            // The reward composition site in
                            // `experience_kernels.cu` reads slot 380 each
                            // bar where action == DIR_HOLD and subtracts
                            // the cost — pricing the action so the policy
                            // uses Hold deliberately rather than as a free
                            // CQL-bias-anchored default.
                            //
                            // Cold-start (epoch 1, slot 382 still at fold-
                            // reset sentinel 0.0): observed=0, target=0.20,
                            // excess=0 → cost = base × 1.0 = HOLD_COST_BASE,
                            // clamped to [base × 0.5, base × 5.0] →
                            // baseline carry cost. Pearl A's first-
                            // observation replacement consumes the sentinel
                            // when the observer first fires.
                            //
                            // Per `feedback_isv_for_adaptive_bounds`: base
                            // cost is the only constant; rate target,
                            // observed rate, and cost output are ISV-driven.
                            // Per `feedback_no_cpu_compute_strict`: the
                            // formula here is host-side controller arithmetic
                            // on already-smoothed scalars — no per-sample
                            // GPU compute path is bypassed (the observer +
                            // Pearls A+D + per-bar reward subtraction all
                            // run on GPU).
                            {
                                use crate::cuda_pipeline::sp5_isv_slots::{
                                    HOLD_COST_INDEX, HOLD_RATE_TARGET_INDEX,
                                    HOLD_RATE_OBSERVED_EMA_INDEX,
                                    HOLD_COST_BASE, HOLD_COST_CONTROLLER_GAIN,
                                    HOLD_COST_FLOOR_RATIO, HOLD_COST_CEIL_RATIO,
                                };
                                let trainer = fused.trainer();
                                let observed = trainer.read_isv_signal_at(HOLD_RATE_OBSERVED_EMA_INDEX);
                                let target   = trainer.read_isv_signal_at(HOLD_RATE_TARGET_INDEX);
                                let excess   = (observed - target).max(0.0);
                                let cost     = HOLD_COST_BASE * (1.0 + HOLD_COST_CONTROLLER_GAIN * excess);
                                let cost     = cost.clamp(
                                    HOLD_COST_BASE * HOLD_COST_FLOOR_RATIO,
                                    HOLD_COST_BASE * HOLD_COST_CEIL_RATIO,
                                );
                                trainer.write_isv_signal_at(HOLD_COST_INDEX, cost);
                            }

                            // SP11 Fix 39 (2026-05-04, A1.2): per-bar
                            // saboteur engagement canary producer. Reads
                            // the per-bar saboteur Δreward signal written
                            // by `experience_env_step` (wired into the
                            // trainer at init via
                            // `set_sp11_saboteur_delta_reward_buf`). When
                            // the saboteur is inactive this epoch the
                            // signal stays at zero across all bars, the
                            // engagement fraction reads zero, and Pearl A
                            // holds the EMA at sentinel. Pearls A+D smooth
                            // into ISV[SABOTEUR_ENGAGEMENT_RATE_INDEX=358].
                            //
                            // Pre-condition: launch_sp11_mag_ratio_compute
                            // ran above — so PNL_REWARD_MAGNITUDE_EMA_INDEX
                            // has its first observation populated by the
                            // time the engagement kernel reads it as the
                            // signal-relative threshold base.
                            if let Err(e) = fused.trainer()
                                .launch_sp11_saboteur_engagement_compute()
                            {
                                tracing::warn!(error = %e,
                                    "SP11 saboteur_engagement_compute launch failed");
                            }

                            // SP11 Fix 39 (2026-05-04, Task A2): reward-
                            // subsystem controller. Reads all 5 canary
                            // ISV slots [350..360) (val-sharpe Δ +
                            // variance, 6 mag-ratios, saboteur engagement,
                            // PnL magnitude EMA) and writes 10 outputs
                            // through chained Pearls A+D into ISV[340..350).
                            //
                            // Pre-conditions (must run AFTER, on the same
                            // stream): the 3 A1 canary launches above
                            // populate slots [350..360) for THIS step.
                            // The controller depends on canary outputs
                            // being current, so this launch lands AFTER
                            // them in the same per-epoch metrics block
                            // and BEFORE the HEALTH_DIAG emit so the
                            // operator sees fresh controller outputs.
                            //
                            // No consumer reads ISV[340..350) yet — Layer
                            // A is additive; consumer migration lands
                            // atomically in B1 per
                            // `feedback_no_partial_refactor.md`. Training
                            // behaviour is unchanged from A1; the only
                            // observable change is the new HEALTH_DIAG
                            // `sp11_reward` line emitted below.
                            if let Err(e) = fused.trainer()
                                .launch_sp11_reward_subsystem_controller()
                            {
                                tracing::warn!(error = %e,
                                    "SP11 reward_subsystem_controller launch failed");
                            }
                        }
                    }
                }
            }

            // Track 1 action distribution: per-magnitude usage across the epoch.
            // Layout: action_counts[12], exp_idx = dir*3 + mag with
            //   dir ∈ {0=Short, 1=Hold, 2=Long, 3=Flat}.
            // Quarter = mag 0 (indices 0, 3, 6, 9)
            // Half    = mag 1 (indices 1, 4, 7, 10)
            // Full    = mag 2 (indices 2, 5, 8, 11)
            // BUT: Hold (dir=1) and Flat (dir=3) force mag=0 in the kernel —
            // their non-zero mag slots are always empty by construction. To
            // measure "magnitude preference when the policy actually picks a
            // tradable direction", exclude Hold and Flat from the denominator:
            //   denom = Σ (Short + Long) bins = indices [0..3] ∪ [6..9]
            // This is what the smoke test actually wants to measure.
            let action_total_epoch: usize = monitor.action_counts.iter().sum();
            let trade_total: usize = monitor.action_counts[0..3].iter().sum::<usize>()
                + monitor.action_counts[6..9].iter().sum::<usize>();
            let (dist_q, dist_h, dist_f) = if trade_total > 0 {
                let t = trade_total as f32;
                let q = (monitor.action_counts[0] + monitor.action_counts[6]) as f32 / t;
                let h = (monitor.action_counts[1] + monitor.action_counts[7]) as f32 / t;
                let f = (monitor.action_counts[2] + monitor.action_counts[8]) as f32 / t;
                (q, h, f)
            } else {
                (0.0_f32, 0.0_f32, 0.0_f32)
            };
            // Cache for smoke-test accessors (magnitude_distribution test).
            self.last_magnitude_dist = [dist_q, dist_h, dist_f];

            // Track 4 exploration entropy — per-branch action entropy normalized
            // to [0, 1]. Magnitude branch uses the [dist_q, dist_h, dist_f] above;
            // direction is summed across magnitudes per direction bin.
            // Layout (dir*3 + mag, 4 directions): dir 0=Short [0..3], dir 1=Hold
            // [3..6], dir 2=Long [6..9], dir 3=Flat [9..12].
            let ent_mag = shannon_entropy_normalized(&[dist_q, dist_h, dist_f]);
            let (dir0, dir1, dir2, dir3) = if action_total_epoch > 0 {
                let t = action_total_epoch as f32;
                let d0 = monitor.action_counts[0..3].iter().sum::<usize>() as f32 / t;
                let d1 = monitor.action_counts[3..6].iter().sum::<usize>() as f32 / t;
                let d2 = monitor.action_counts[6..9].iter().sum::<usize>() as f32 / t;
                let d3 = monitor.action_counts[9..12].iter().sum::<usize>() as f32 / t;
                (d0, d1, d2, d3)
            } else {
                (0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32)
            };
            let ent_dir = shannon_entropy_normalized(&[dir0, dir1, dir2, dir3]);
            // Task 0.14: append to exploration entropy history for smoke-test readback.
            self.explore_entropy_mag_history.push((epoch as u32, ent_mag));

            // SP8 (Fix 36, 2026-05-03): host-side `last_train_active_frac`
            // computation REMOVED. The canary now lives at
            // `ISV[TRAIN_ACTIVE_FRAC_INDEX]` (Pearls A+D smoothed),
            // produced by `train_active_frac_compute_kernel` launched
            // earlier in the GPU monitoring summary block (after
            // `mon.download_summary()` settles the GPU summary buffer).
            // Read via the `last_train_active_frac()` accessor on
            // `DQNTrainer` per `feedback_no_cpu_compute_strict.md` and
            // Fix 36's atomic refactor scope (kernel + slot allocation +
            // consumer in one commit per `feedback_no_partial_refactor.md`).

            // Task 0.9: adaptive-controller fire detection.
            //
            // A controller "fires" iff it made an ADAPTIVE INTERVENTION this
            // epoch — not merely because its observable output changed. The
            // V7 audit asks "is this controller load-bearing (keeps training
            // on the rails) or diagnostic (catches edge cases)?". Pure
            // schedules (curricula) and per-step EMA trackers would tick
            // every epoch by construction, which would incorrectly flag
            // them as load-bearing under a delta-only semantic.
            //
            // Per-controller semantics:
            //  - anti_lr, tau, gamma, cql_alpha: genuine adaptive controllers
            //    whose observable output changes iff they made a decision to
            //    intervene. Delta-vs-prior-epoch is the right signal.
            //    Sentinel-NaN prev (first epoch) → no fire (no baseline).
            //  - grad_clip: EMA-smoothed threshold that recomputes every
            //    training step. The threshold is ONLY load-bearing if it
            //    actually clamped a gradient (raw_grad_norm > clip). That
            //    latch is set in run_training_steps_slices; here we just
            //    read it.
            //  - cost_anneal: pure deterministic sigmoid of current_epoch
            //    (crate::trainers::dqn::trainer::training_loop line ~441).
            //    No adaptive/reactive component, so it CANNOT be
            //    load-bearing in the V7 sense. Always reported as 0 fires.
            //    Still logged so the [CTRL_FIRE] line shows the full 6-tuple.
            //
            // LR used for THIS epoch's training. anti-LR runs later in the loop
            // (computing LR for next epoch) so scheduler value here is the
            // one that was actually in effect.
            let cur_lr = self.lr_scheduler.get_lr();
            let cur_tau = self.last_tau_eff.unwrap_or(f32::NAN);
            let cur_gamma = self.last_gamma_eff.unwrap_or(f32::NAN);
            let cur_clip = self.fused_ctx.as_ref().map(|f| f.adaptive_clip_value()).unwrap_or(f32::NAN);
            let cur_cql = self.last_cql_alpha_eff.unwrap_or(f32::NAN);
            let cur_cost = self.cost_anneal_factor;

            let prev = self.prev_controller_values;
            // Task 2.5 Bug #1 — anti-LR controller fires iff it applied a
            // non-unity multiplier THIS epoch. The previous implementation
            // compared scheduler LR to prev-epoch scheduler LR, which under
            // Cosine/Linear/Exponential decay ticks every epoch from pure
            // scheduler drift, yielding 100% false-positive fire rate.
            // Detect the *intervention* via last_anti_mult (stored in the
            // anti-LR block, reset to 1.0 at epoch start).
            let fire_lr = (self.last_anti_mult - 1.0).abs() > 0.01;
            let fire_tau = prev.tau.is_finite() && (cur_tau - prev.tau).abs() > 1e-6;
            let fire_gamma = prev.gamma.is_finite() && (cur_gamma - prev.gamma).abs() > 1e-4;
            // grad_clip: intervention-based (clip actually clamped ≥ 1 step).
            let fire_clip = self.grad_clip_kicked_this_epoch;
            let fire_cql = prev.cql_alpha.is_finite() && (cur_cql - prev.cql_alpha).abs() > 1e-5;
            // cost_anneal: pure curriculum schedule → never load-bearing.
            let fire_cost = false;

            if fire_lr { self.controller_fire_counts.anti_lr += 1; }
            if fire_tau { self.controller_fire_counts.tau += 1; }
            if fire_gamma { self.controller_fire_counts.gamma += 1; }
            if fire_clip { self.controller_fire_counts.grad_clip += 1; }
            if fire_cql { self.controller_fire_counts.cql_alpha += 1; }
            if fire_cost { self.controller_fire_counts.cost_anneal += 1; }
            self.controller_total_epochs += 1;
            self.prev_controller_values = super::ControllerPrevValues {
                lr: cur_lr,
                tau: cur_tau,
                gamma: cur_gamma,
                grad_clip: cur_clip,
                cql_alpha: cur_cql,
                cost_anneal: cur_cost,
            };

            // fire_frac = max fire-rate across all controllers (worst-case "load-bearing"
            // signal). Running fraction = fires / total_epochs for the max-firing
            // controller. Captures whether *any* controller is above 50% threshold
            // (the controller_activity smoke gate).
            let total = self.controller_total_epochs.max(1) as f32;
            let rates = [
                self.controller_fire_counts.anti_lr as f32 / total,
                self.controller_fire_counts.tau as f32 / total,
                self.controller_fire_counts.gamma as f32 / total,
                self.controller_fire_counts.grad_clip as f32 / total,
                self.controller_fire_counts.cql_alpha as f32 / total,
                self.controller_fire_counts.cost_anneal as f32 / total,
            ];
            let fire_frac = rates.iter().copied().fold(0.0_f32, f32::max);

            // Task 0.8: reward-term contribution audit (Track 2).
            // MUST run BEFORE trail_fire_and_hold_per_mag because the reward
            // audit peeks at Task 0.5's trail_triggered_per_sample +
            // traded_per_sample buffers WITHOUT resetting them, and the
            // per-mag reducer below both reads AND resets them. Order:
            //   1. reward_contrib_fractions  (peek trail buffers, reset own)
            //   2. trail_fire_and_hold_per_mag (read+reset trail buffers)
            // PopArt slot is filled from the host-side FusedTrainingCtx
            // state (|Δvar| / |var_pre|) because PopArt mean/var live there.
            let mut reward_contrib = match self.gpu_experience_collector.as_mut() {
                Some(c) => match c.reward_contrib_fractions() {
                    Ok(v) => v,
                    Err(e) => {
                        tracing::warn!("HEALTH_DIAG reward_contrib readback failed: {e}");
                        [0.0_f32; 4]
                    }
                },
                None => [0.0_f32; 4],
            };
            // B.2 Plan 3 Task 3 (spec §4.B.2): launch site for the
            // trade_attempt_rate_ema kernel — reads flat_to_pos_per_sample and
            // adaptive-EMA-updates ISV[TRADE_ATTEMPT_RATE_EMA_INDEX]. α_base
            // matches the reward-EMA convention (0.05).
            //
            // SP11 launch-order fix-up (2026-05-04): the C.2
            // `launch_reward_component_ema_inplace` kernel that previously
            // ran here was MOVED to before the SP11 canary chain (see the
            // SP11 producer block above, just before
            // `launch_sp11_popart_component_ema`). Reason: the SP11 mag-
            // ratio canary reads ISV[64..68] (cf/trail/micro/opp_cost/
            // bonus mag EMAs) and ISV[362..366] (paired variances), both
            // of which are POPULATED by `launch_reward_component_ema_inplace`.
            // With the kernel still here, those slots held stale (or
            // sentinel-0) values when the canary fired earlier in the same
            // step → z-score collapsed to the linear formula and B1b
            // smoke `smoke-test-4rbv9` saw bit-identical w_pop=2.0 to the
            // pre-z-score linear-formula smoke. The other launches in this
            // block (trade_attempt_rate, plan_threshold, seed_step_counter,
            // cql_alpha_seed) STAY here — their consumers fire elsewhere
            // in the loop and have no pre-canary timing constraint.
            {
                let n_main = self.gpu_experience_collector.as_ref().map(|c| {
                    c.alloc_episodes() * c.alloc_timesteps()
                }).unwrap_or(0);
                let ema_alpha = 0.05_f32;
                if let Some(ref mut c) = self.gpu_experience_collector {
                    if let Err(e) = c.launch_trade_attempt_rate_ema_inplace(n_main, ema_alpha) {
                        tracing::warn!("B.2 trade_attempt_rate_ema launch failed: {e}");
                    }
                    // B.4 Plan 3 Task 4 (spec §4.B.4): same launch site for
                    // the plan_threshold_update kernel — reads
                    // readiness_per_sample, EMAs the mean into
                    // ISV[READINESS_EMA_INDEX=75], and derives
                    // ISV[PLAN_THRESHOLD_INDEX=49] = max(0.1, 0.5 × ema).
                    // Same α_base as the other Plan 3 EMA producers.
                    if let Err(e) = c.launch_plan_threshold_update_inplace(n_main, ema_alpha) {
                        tracing::warn!("B.4 plan_threshold_update launch failed: {e}");
                    }
                    // B.3 Plan 3 Task 8 (spec §4.B.3): GPU-driven seed-phase
                    // progress tracker. Increments
                    // ISV[SEED_STEPS_DONE_INDEX=83] by `n_main` and EMAs
                    // `max(0, 1 - DONE/TARGET)` into
                    // ISV[SEED_FRAC_EMA_INDEX=84]. Cold-path single-thread
                    // kernel; same α_base as the other Plan 3 producers.
                    if let Err(e) = c.launch_seed_step_counter_update_inplace(n_main, ema_alpha) {
                        tracing::warn!("B.3 seed_step_counter_update launch failed: {e}");
                    }
                    // C.5 Plan 3 Task 9 (spec §4.C.5): GPU-driven CQL α ramp
                    // coupled to ISV[SEED_FRAC_EMA]. EMAs
                    // ISV[CQL_ALPHA_INDEX=48] toward
                    // `cql_alpha_final × max(0, 1 - seed_frac)`. Producer-
                    // only upgrade — CQL gradient kernel consumer (already
                    // reading slot 48 per Plan 1 Task 12) unchanged.
                    let cql_alpha_final = self.hyperparams.cql_alpha as f32;
                    if let Err(e) = c.launch_cql_alpha_seed_update_inplace(cql_alpha_final, ema_alpha) {
                        tracing::warn!("C.5 cql_alpha_seed_update launch failed: {e}");
                    }
                }

                // Plan 4 Task 2c.3c.5: per-step H_S2 RMS EMA into ISV[96].
                // Single-block 256-thread shmem-reduction kernel reads the
                // online trunk's `save_h_s2 [B, SH2]` (populated by the most
                // recent forward) and EMAs sqrt(sum_sq / N) into ISV[96] at
                // α=0.05. Producer-only — 2c.3c.6 wires the consumer in
                // `mag_concat_qdir`'s adaptive-scale path. Same launch
                // cadence as the Plan 3 producers above.
                if let Some(ref fused) = self.fused_ctx {
                    if let Err(e) = fused.trainer().launch_h_s2_rms_ema() {
                        tracing::warn!("Plan 4 2c.3c.5 h_s2_rms_ema launch failed: {e}");
                    }
                }

                // Plan C Phase 2 follow-up K (2026-04-29): per-step adaptive
                // fold-boundary warmup factor producer. Reads the fast/slow
                // grad-norm EMA mapped-pinned scalars (filled by
                // `update_adaptive_clip` in the per-step guard block above)
                // and writes ISV[FOLD_WARMUP_FACTOR_INDEX=130] = clamp(fast/slow, 0, 1).
                // Two consumers below derive lr_eff (via set_lr) and
                // clip_eff (via update_adaptive_clip's pinned slot). Same
                // launch cadence as h_s2_rms_ema — once per step.
                if let Some(ref fused) = self.fused_ctx {
                    if let Err(e) = fused.trainer().launch_fold_warmup_factor() {
                        tracing::warn!("Plan C K fold_warmup_factor launch failed: {e}");
                    }
                }

                // Plan 4 Task 3 (E.3): per-step IQN multi-quantile diagnostic
                // EMAs into ISV[99..103). 4-block kernel reads the IQN online
                // forward's `save_q_online [TBA, B*Q]` (populated by
                // `GpuIqnHead::execute_training_pipeline` after each step) and
                // EMAs mean |Q| at the four off-median fixed-τ positions
                // {0.05, 0.25, 0.75, 0.95}. The median (τ=0.50) is intentionally
                // skipped — already in the greedy-Q diagnostic. Same launch
                // cadence as `h_s2_rms_ema` above. No-op when IQN is inactive.
                if let Some(ref fused) = self.fused_ctx {
                    if let Err(e) = fused.launch_iqn_quantile_ema() {
                        tracing::warn!("Plan 4 Task 3 iqn_quantile_ema launch failed: {e}");
                    }
                }

                // Plan 4 Task 1B-iii: per-step VSN per-group mask EMA into
                // ISV[VSN_MASK_GROUP_0_EMA_INDEX..VSN_MASK_GROUP_5_EMA_INDEX]
                // (slots 105..111). Single-block 256-thread shmem-reduction
                // kernel reads `vsn_mask_buf [B, 6]` saved by the
                // online-on-states VSN forward this step and EMAs the per-
                // group batch mean into the matching ISV slots. Producer-only
                // in this commit — diagnostic surface for HEALTH_DIAG / VSN
                // focus monitoring; no consumer kernel reads slots [105..111).
                // Same launch cadence as `h_s2_rms_ema` / `iqn_quantile_ema`.
                if let Some(ref fused) = self.fused_ctx {
                    if let Err(e) = fused.trainer().launch_vsn_mask_ema() {
                        tracing::warn!("Plan 4 1B-iii vsn_mask_ema launch failed: {e}");
                    }
                }

                // Plan 4 Task 6 Commit B: per-step aux-heads loss EMA into
                // ISV[AUX_NEXT_BAR_MSE_EMA_INDEX=113] +
                // ISV[AUX_REGIME_CE_EMA_INDEX=114]. Single-thread single-block
                // ISV producer reads the just-populated `aux_nb_loss_scalar_buf`
                // + `aux_rg_loss_scalar_buf` (filled by `aux_*_loss_reduce`
                // inside the captured forward graph) and EMAs them into the
                // two ISV slots. Same launch cadence as the other producers
                // above. Honors the post-cascade-fix invariant (commit
                // a5f23b28f): ISV producer launches MUST run BEFORE the
                // HEALTH_DIAG line emit further down in this loop body.
                if let Some(ref fused) = self.fused_ctx {
                    if let Err(e) = fused.trainer().launch_aux_heads_loss_ema() {
                        tracing::warn!("Plan 4 Task 6 Commit B aux_heads_loss_ema launch failed: {e}");
                    }
                }

                // SP13 Phase 0a P0a.T4 (2026-05-04): per-step aux
                // directional-accuracy producer chain. SP13 B1.1a
                // (2026-05-05) flipped the upstream head from K=1 MSE
                // regression to K=2 softmax CE classification — this
                // orchestrator now reads the captured-forward-graph
                // `aux_nb_softmax_buf [B, K]` + `aux_nb_label_buf [B] i32`
                // (just populated above by the same per-step block that
                // fires `launch_aux_heads_loss_ema`), reduces them into
                // the trainer's mapped-pinned `aux_dir_acc_buf [6]`
                // (was [3]; B1.1a added n_down/n_up/n_skip for
                // HEALTH_DIAG observability), then chains:
                //   - fixed-α EMA (α=0.3, sentinel 0.5) → ISV[373]
                //   - fixed-α EMA (α=0.05, sentinel 0.5) → ISV[374]
                //   - mean(softmax[1]-softmax[0]) → ISV[375]
                //     (structural [-1, +1] bound; no runtime tanh)
                // All on the same stream as the producer, so no host
                // sync is required between captured graph and consumer.
                // Producer-only — no consumer kernel reads slots 373/374/
                // 375 in this commit; Phase 0b wires the controller and
                // the Q-head input layer that reads slot 375. Same
                // post-cascade-fix invariant as `launch_aux_heads_loss_ema`:
                // ISV producer launches MUST run BEFORE the HEALTH_DIAG
                // line emit further down.
                //
                // B1.1a known degraded state: labels stay zero-init
                // (`aux_nb_label_buf` is `alloc_zeros`-i32 every step;
                // every sample receives label 0 = "down") until B1.1b
                // lands the producer kernel that fills real -1/0/1 from
                // the price trajectory. Until then, the model converges
                // on "predict class 0 (down) everywhere" — the cascade
                // is internally consistent (every consumer migrated
                // atomically per `feedback_no_partial_refactor`); the
                // labels are placeholder.
                if let Some(ref fused) = self.fused_ctx {
                    if let Err(e) = fused.trainer().launch_sp13_aux_dir_metrics() {
                        tracing::warn!("SP13 P0a.T4 aux_dir_metrics launch failed: {e}");
                    }
                }

                // SP14 Layer B Task B.11 (2026-05-05): EGF producer chain.
                //
                // Three launches; the first two run per training step here, the
                // third (gradient_hack_detect) fires once per epoch in
                // `process_epoch_boundary`. Order is load-bearing:
                //
                //   1. q_disagreement_update_kernel — reads the captured-graph
                //      `aux_nb_softmax_buf [B, 2]` (aux head forward this step)
                //      + `q_out_buf [B, 13]` (populated by the captured
                //      `populate_q_out` in `submit_aux_ops`) and updates
                //      ISV[Q_DISAGREEMENT_SHORT/LONG_EMA = 383/384] +
                //      ISV[Q_DISAGREEMENT_VARIANCE_EMA = 389]. Same launch
                //      cadence as `launch_sp13_aux_dir_metrics` above —
                //      producer-only.
                //
                //   2. alpha_grad_compute_kernel — consumes #1's ISV[383] /
                //      ISV[389] outputs AND `launch_sp13_aux_dir_metrics`'s
                //      ISV[373] / ISV[374] outputs to compute the EGF gate
                //      ISV[ALPHA_GRAD_SMOOTHED = 393]. This is the kernel that
                //      activates the EGF wire — pre-B.11 the slot stayed at
                //      sentinel 0.0 so the backward wire-column scaling
                //      (B.10) effectively zeroed gradient flow. Post-B.11 the
                //      slot tracks the live gate value ∈ [0, 1] each step.
                //
                // The backward consumer (`launch_sp14_scale_wire_col` inside
                // `launch_cublas_backward_to`) reads ISV[393] inside the
                // CAPTURED graph. Because these two producer launches run
                // OUTSIDE the captured graph (after `run_full_step` returns,
                // identical position to `launch_sp13_aux_dir_metrics`), the
                // backward consumer at training step N reads the value
                // written by step N-1's producer chain (one-step lag). This
                // is the same one-step-lag pattern as the forward wire
                // (`launch_sp14_dir_concat_qaux` consumes the prior step's
                // `aux_nb_softmax_buf`). Bootstrap effect: at step 0, ISV[393]
                // is still 0.0 → wire column zeroed; from step 1 onward the
                // gate value accrues.
                //
                // SP14 L1+L2 follow-up (2026-05-05): warmup_gate parameter
                // and `fold_step_counter` ramp deleted per
                // `feedback_isv_for_adaptive_bounds`. The variance-driven
                // k_aux/k_q already provide warmup behavior — when var_aux
                // and var_q EMAs are high (cold-start, EMAs still moving),
                // k → K_MIN → flat sigmoid → gate ≈ 0.5; when EMAs settle,
                // k → K_BASE → sharp sigmoid → gates respond correctly.
                //
                // Constants per plan §2581-2585:
                //   * α_short = 0.3 (fast EMA, ~3-step half-life)
                //   * α_long  = 0.05 (slow EMA, ~14-step half-life)
                //   * α_var   = 0.05 (Welford EMA blend; shared by var_q
                //                     in this kernel and var_alpha + var_aux
                //                     in alpha_grad_compute)
                //
                // var_aux producer gap (from B.4 status report) is now closed:
                // alpha_grad_compute_kernel writes ISV[VAR_AUX = 388] inline
                // (Welford on aux_short vs aux_long), so the adaptive k_aux
                // branch is no longer degenerate at K_BASE_AUX = 20.
                if let Some(ref fused) = self.fused_ctx {
                    let alpha_short_qd: f32 = 0.3;
                    let alpha_long_qd:  f32 = 0.05;
                    let alpha_var:      f32 = 0.05;
                    if let Err(e) = fused.trainer().launch_sp14_q_disagreement_update(
                        alpha_short_qd, alpha_long_qd, alpha_var,
                    ) {
                        tracing::warn!("SP14 B.11 q_disagreement_update launch failed: {e}");
                    }
                    if let Err(e) = fused.trainer().launch_sp14_alpha_grad_compute(alpha_var) {
                        tracing::warn!("SP14 B.11 alpha_grad_compute 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() {
                        tracing::warn!("Phase 3 T3.5 launch_moe_expert_util_ema failed: {e}");
                    }
                }

                // Adaptive MoE load-balance λ controller (2026-04-27).
                // MUST run AFTER `launch_moe_expert_util_ema` so the gate-
                // entropy EMA fed into the controller is fresh for the NEXT
                // step's `moe_load_balance_loss` consumer. Reads ISV[126]
                // (entropy EMA), writes ISV[MOE_LAMBDA_EFF_INDEX=128].
                // Per `pearl_blend_formulas_must_have_permanent_floor.md`,
                // λ_floor is a permanent minimum.
                if let Some(ref fused) = self.fused_ctx {
                    if let Err(e) = fused.trainer().launch_moe_lambda_eff_update() {
                        tracing::warn!("launch_moe_lambda_eff_update failed: {e}");
                    }
                }

                // SP4 Layer A Tasks A10 + A11: per-step ISV-bound producer
                // launches for the 40 new SP4 slots (TARGET_Q_BOUND, ATOM_POS
                // ×4 branches, GRAD_CLIP_BOUND, H_S2_BOUND, and 8 param-group
                // oracles spanning WEIGHT/ADAM_M/ADAM_V/WD_RATE per group plus
                // L1_LAMBDA[trunk]). Layer A is observability-only — no
                // consumer reads these slots yet, so behavior is unchanged.
                // All five launches go cold-path here next to the rest of
                // the per-step ISV producers; Layer B may relocate the
                // captured-graph eligible ones (target_q_p99 in particular)
                // once the Mech 1 pre-clamp consumer lands.
                //
                // Producer ordering — like the surrounding ISV producers,
                // these must run BEFORE the HEALTH_DIAG line emit (commit
                // a5f23b28f post-cascade-fix invariant) and BEFORE any
                // future consumer kernel reads the bound slots.
                if let Some(ref fused) = self.fused_ctx {
                    if let Err(e) = fused.trainer().launch_sp4_target_q_p99() {
                        tracing::warn!("SP4 A10 target_q_p99 launch failed: {e}");
                    }
                    if let Err(e) = fused.trainer().launch_sp4_atom_pos_p99_all_branches() {
                        tracing::warn!("SP4 A10 atom_pos_p99_all_branches launch failed: {e}");
                    }
                    if let Err(e) = fused.trainer().launch_sp4_grad_norm_p99() {
                        tracing::warn!("SP4 A10 grad_norm_p99 launch failed: {e}");
                    }
                    if let Err(e) = fused.trainer().launch_sp4_h_s2_p99() {
                        tracing::warn!("SP4 A10 h_s2_p99 launch failed: {e}");
                    }

                    // SP4 A11: param_group_oracle Pearl B over 8 groups
                    // (DQN trunk + 4 branches groups 0-2; IQN, IQL hi/lo,
                    // Attn, Curiosity groups 3-7). Curiosity (group 7)
                    // descriptor is built from `gpu_experience_collector`'s
                    // `curiosity_weight_set` + `curiosity_trainer` pair —
                    // both `None` together (curiosity disabled at collector
                    // init) yields an empty group descriptor and the
                    // launcher silently skips it.
                    let curiosity_weights = self.gpu_experience_collector.as_ref()
                        .map(|c| c.curiosity_weight_set());
                    let curiosity_trainer = self.gpu_experience_collector.as_ref()
                        .and_then(|c| c.curiosity_trainer());
                    let aux_buffers = fused.build_sp4_aux_buffers(
                        curiosity_weights,
                        curiosity_trainer,
                    );
                    if let Err(e) = fused.trainer().launch_sp4_param_group_oracles_all_groups(&aux_buffers) {
                        tracing::warn!("SP4 A11 param_group_oracles_all_groups launch failed: {e}");
                    }

                    // SP5 Task A1: q_branch_stats → pearl_1_atom → apply_pearls_ad ×24
                    // Runs after compute_expected_q has populated q_out_buf [B, 13].
                    // Producer-only (Layer A) — no consumer reads ISV[174..190) or
                    // ISV[218..226) in this commit.
                    if let Err(e) = fused.trainer().launch_sp5_pearl_1_atom() {
                        tracing::warn!("SP5 A1 launch_sp5_pearl_1_atom failed: {e}");
                    }
                    // SP5 Task A2: pearl_3_sigma → apply_pearls_ad ×8
                    // Reads ATOM_V_HALF (Pearl 1 output) + BRANCH_ENTROPY (q_branch_stats).
                    // Must run AFTER launch_sp5_pearl_1_atom (Pearl 3 reads Pearl 1's v_half).
                    // Producer-only (Layer A) — no consumer reads ISV[210..218) in this commit.
                    if let Err(e) = fused.trainer().launch_sp5_pearl_3_sigma() {
                        tracing::warn!("SP5 A2 launch_sp5_pearl_3_sigma failed: {e}");
                    }
                    // SP5 Task A3: pearl_2_budget → apply_pearls_ad ×20
                    // Must run AFTER launch_sp5_pearl_1_atom (Q_VAR) AND
                    // launch_sp5_pearl_3_sigma (NOISY_SIGMA).
                    // Producer-only (Layer A) — no consumer reads ISV[190..210) in this commit.
                    if let Err(e) = fused.trainer().launch_sp5_pearl_2_budget() {
                        tracing::warn!(error = %e, "SP5 Pearl 2 producer launch failed");
                    }
                    // SP8 (Fix 36, 2026-05-03): adaptive MAX_BUDGET cap.
                    // Reads ISV[TRAIN_ACTIVE_FRAC_INDEX] (per-epoch canary
                    // produced by `train_active_frac_compute_kernel` in the
                    // metrics block) and writes per-(head, branch)
                    // ISV[LB_MAX_BUDGET_{CQL,C51}_BASE..+4) via Pearls A+D.
                    // Must run BEFORE `launch_loss_balance_controller` so the
                    // controller reads a fresh cap each step. Replaces the
                    // hardcoded `MAX_BUDGET=1.0f` constant per
                    // `pearl_controller_anchors_isv_driven.md`.
                    if let Err(e) = fused.trainer().launch_max_budget_compute() {
                        tracing::warn!(error = %e, "SP8 max_budget_compute launch failed");
                    }
                    // SP7 Task 7 (2026-05-03): loss-balance controller —
                    // drives BUDGET_CQL_BASE and BUDGET_C51_BASE outcome-style
                    // based on grad-norm ratio relative to IQN, flatness-
                    // modulated per branch. Must run AFTER:
                    //   * launch_sp5_pearl_2_budget (FLATNESS_BASE populated)
                    //   * grad_decomp_launch_iqn / cql_sx / c51 (pinned norm
                    //     slots populated by the in-step backward pass)
                    //   * launch_max_budget_compute (SP8 Fix 36 — fresh
                    //     ISV[LB_MAX_BUDGET_{CQL,C51}_BASE+b] for the cap)
                    if let Err(e) = fused.trainer().launch_loss_balance_controller() {
                        tracing::warn!(error = %e, "SP7 loss-balance controller launch failed");
                    }
                    // SP5 Task A4: pearl_4_adam_hparams → apply_pearls_ad ×24
                    // Reads grad_buf (populated in this step's backward pass) + grad_prev_buf.
                    // No ordering dependency on Pearls 1/2/3 — reads gradient state, not ISV.
                    // Writes ISV[226..250) = β1[8] + β2[8] + ε[8] (ALPHA_META_PEARL_4=5e-4).
                    if let Err(e) = fused.trainer().launch_sp5_pearl_4_adam_hparams() {
                        tracing::warn!(error = %e, "SP5 Pearl 4 producer launch failed");
                    }
                    // SP5 Task A5: pearl_5_iqn_tau → apply_pearls_ad ×20
                    // Reads save_q_online (forward-pass output); independent of Pearls 1/2/3/4 ISV outputs.
                    // Writes ISV[250..270) = τ[4 branches × 5 quantiles] (ALPHA_META=1e-3).
                    if let Err(e) = fused.trainer().launch_sp5_pearl_5_iqn_tau() {
                        tracing::warn!(error = %e, "SP5 Pearl 5 producer launch failed");
                    }
                    // SP5 Task A7: pearl_8_trail → apply_pearls_ad ×4
                    // Reads features[bar_idx * market_dim + 9] (ATR_NORM, fxcache col 9);
                    // writes ISV[TRAIL_DIST_PER_DIR_BASE=270..274) = trail_dist[4 dirs] (ALPHA_META=1e-3).
                    // bar_idx = features_raw_cuda.len()/market_dim - 1: last bar of fold window —
                    // stable ATR proxy for the replay-based (non-sequential) training loop.
                    // Layer A simplification: Short and Long share the same current-bar ATR.
                    // features_raw_cuda lives on DQNTrainer; passed as dev ptr.
                    if let Some(ref features_buf) = self.features_raw_cuda {
                        let market_dim_usize = fused.trainer().config().market_dim.max(1);
                        // MappedF32Buffer.len is a pub field (not a method) — the
                        // staging buffer aliases host pages, so the element count
                        // equals the f32 length passed to ::new().
                        let inferred_num_bars = features_buf.len / market_dim_usize;
                        if inferred_num_bars > 0 {
                            // Mapped pinned exposes dev_ptr directly (CUdeviceptr).
                            // No StreamGuard / device_ptr() borrow needed — these
                            // pages have no SyncOnDrop bookkeeping.
                            let feat_dev_ptr = features_buf.dev_ptr;
                            let market_dim_i32 = market_dim_usize as i32;
                            let bar_idx_i32    = (inferred_num_bars - 1) as i32;
                            if let Err(e) = fused.trainer().launch_sp5_pearl_8_trail(
                                feat_dev_ptr,
                                market_dim_i32,
                                bar_idx_i32,
                            ) {
                                tracing::warn!(error = %e, "SP5 Pearl 8 producer launch failed");
                            }
                        }
                    }
                    // SP5 Task A8: pearl_1_ext_num_atoms → apply_pearls_ad ×4
                    // Reads ATOM_V_HALF (Pearl 1 from A1, populated earlier in this step's launch chain).
                    // Maps v_half to discrete {64, 32, 16} via threshold cascade; Pearls A+D smooths.
                    // Writes ISV[ATOM_NUM_ATOMS_BASE=274..278) = num_atoms[4 branches] (ALPHA_META=1e-3).
                    if let Err(e) = fused.trainer().launch_sp5_pearl_1_ext_num_atoms() {
                        tracing::warn!(error = %e, "SP5 Pearl 1-ext producer launch failed");
                    }
                }
            }

            // SP13 P0b (2026-05-04): aux_w deficit + stagnation controller.
            // Replaces SP11-era inverted formula
            //   aux_w = (0.1 × learning_health × (1 - tanh(0.1 × sharpe_ema))).clamp(0.05, 0.3)
            // which LOWERED aux when sharpe was high — exactly wrong for the
            // failure mode (high sharpe + low WR is the structural-extraction
            // trap; aux head needs MORE gradient when policy is over-fitting
            // a noise structure, not less). New mechanism per spec §"Change 5":
            //   deficit       = max(0, target_dir_acc - short_ema)
            //   improvement   = max(0, short_ema - long_ema)
            //   stagnation    = (deficit > 0.005) ? clamp(1 - improvement/deficit, 0, 1) : 0
            //   aux_w_raw     = base × (1 + 5 × deficit) × (1 - 0.7 × stagnation)
            //   aux_w         = clamp(aux_w_raw, [0.3 × base, 3.0 × base])
            // Stagnation decay backs aux_w to floor when the short EMA fails
            // to pull ahead of the long EMA — "we tried, it didn't help, stop
            // destabilising Q." Hard floor 0.3× base prevents permanent
            // suppression (the SP11 inversion bug). The setter is idempotent
            // and does NOT invalidate the captured graph — new value takes
            // effect on the next re-capture (fold boundary, lr change, etc.).
            if let Some(ref mut fused) = self.fused_ctx {
                use crate::cuda_pipeline::sp13_isv_slots::{
                    TARGET_DIR_ACC_INDEX, AUX_DIR_ACC_SHORT_EMA_INDEX,
                    AUX_DIR_ACC_LONG_EMA_INDEX,
                };
                let target    = fused.read_isv_signal_at(TARGET_DIR_ACC_INDEX);
                let short_ema = fused.read_isv_signal_at(AUX_DIR_ACC_SHORT_EMA_INDEX);
                let long_ema  = fused.read_isv_signal_at(AUX_DIR_ACC_LONG_EMA_INDEX);
                // SP14 A.3: pass fold-local epoch counter so the stagnation
                // gate stays disabled in epoch 0 (Pearl-A first-observation
                // bootstrap forces short_ema = long_ema at fold reset). The
                // outer `for epoch in 0..self.hyperparams.epochs` loop is
                // re-entered fresh each fold via `train_fold_from_slices`
                // (see trainer/mod.rs:1132 fold loop), so `epoch` here is
                // already the count of completed epochs in the current fold.
                let aux_w = compute_aux_w_p0b(target, short_ema, long_ema, epoch);
                tracing::debug!(
                    "aux_w controller: target={:.3} short={:.3} long={:.3} aux_w={:.3}",
                    target, short_ema, long_ema, aux_w,
                );
                fused.set_aux_weight(aux_w);
            }

            // B.2 Plan 3 Task 3: freeze TRADE_TARGET_RATE at the configured
            // epoch from the measured TRADE_ATTEMPT_RATE_EMA. Floored at 0.001
            // so novelty = max(0, 1 - attempt/target) can never stick at 1.0.
            // The target slot stays at 0.0 until this freeze fires; the kernel's
            // fmaxf(1e-4f, ...) guard keeps the ratio numerically sane in the
            // warmup window.
            {
                const TRADE_TARGET_RATE_FREEZE_EPOCH: usize = 5;
                use crate::cuda_pipeline::gpu_dqn_trainer::{
                    TRADE_ATTEMPT_RATE_EMA_INDEX, TRADE_TARGET_RATE_INDEX,
                };
                if epoch == TRADE_TARGET_RATE_FREEZE_EPOCH {
                    if let Some(ref fused) = self.fused_ctx {
                        let measured = fused.read_isv_signal_at(TRADE_ATTEMPT_RATE_EMA_INDEX);
                        let rate_for_target = measured.max(0.001);
                        fused.write_isv_signal_at(TRADE_TARGET_RATE_INDEX, rate_for_target);
                        tracing::info!(
                            target: "dqn::B2",
                            "TRADE_TARGET_RATE frozen at epoch {} to {:.4} (measured {:.4})",
                            epoch, rate_for_target, measured
                        );
                    }
                }
            }

            // Populate PopArt drift (slot 0): |current_var − prev_var| / |prev_var|.
            // Uses FusedTrainingCtx::read_popart_variance(). Slot stays at 0.0
            // in three real, semantically-meaningful cases: (a) first epoch
            // before PopArt has a prior reading; (b) current epoch's variance
            // has not warmed up yet (cur_var == 0); (c) PopArt itself is
            // disabled so cur_var is always 0. In ALL three cases `0.0` is
            // the HONEST SIGNAL — "no PopArt drift this epoch" — not a stub.
            // `prev_reward_contrib_popart_var` is the trainer-owned previous
            // reading (not the one consumed by should_reset_tau — separate to
            // avoid interference).
            if let Some(ref fused) = self.fused_ctx {
                let cur_var = fused.read_popart_variance();
                if cur_var > 0.0 && self.prev_reward_contrib_popart_var > 0.0 {
                    // Guard above ensures `prev > 0.0`, so no `.abs()` / `.max(1e-12)`
                    // needed on the denominator. `.abs()` on `delta` IS meaningful —
                    // variance can decrease between epochs.
                    let pre = self.prev_reward_contrib_popart_var;
                    let delta = (cur_var - pre).abs();
                    reward_contrib[0] = (delta / pre) as f32;
                }
                // Update prev for next epoch (even on warmup where cur_var = 0 —
                // then next epoch's cur_var > 0 case will have prev = 0 and
                // reward_contrib[0] stays 0 for that boundary, correct behavior).
                self.prev_reward_contrib_popart_var = cur_var;
            }
            // Cache for the smoke-test accessor reward_component_audit_summary.
            // Smoke expects all 4 values finite + >= 0 — the readback already
            // enforces that (absolute-value sums, rate = a / b with b >= 0).
            // Task 2.4: slot count shrank 5→4 when R6 clamp relocated to C51.
            self.last_reward_contrib = reward_contrib;

            // H6: per-magnitude trailing-stop fire rate + hold-at-exit means.
            // Reduces GPU-resident per-sample buffers populated by env_step this
            // epoch (race-free: kernel writes only [i*L+t], host reduces + resets).
            // Collector may be absent early in training — default to zeros.
            let (trail_rates, hold_means) = match self.gpu_experience_collector.as_mut() {
                Some(c) => match c.trail_fire_and_hold_per_mag() {
                    Ok(v) => v,
                    Err(e) => {
                        tracing::warn!("HEALTH_DIAG trail readback failed (zeroing for this epoch): {e}");
                        ([0.0_f32; 3], [0.0_f32; 3])
                    }
                },
                None => ([0.0_f32; 3], [0.0_f32; 3]),
            };

            // Task 2.X prerequisite: per-magnitude win rate (Σ profitable / Σ
            // closing) + realized step-return variance (Var[step_ret | mag]).
            // Reduces the four per-sample buffers written by env_step this
            // epoch using the same race-free pattern as Task 0.5 / 0.8
            // (per-sample array + host reduce + reset). Feeds HEALTH_DIAG's
            // new `mag_stats` group so the Task 2.X decision tree can
            // distinguish Q-estimation bias from genuine data-disfavor at
            // L40S scale.
            //
            // Collector may be absent early in training — default to zeros.
            // Early epochs may ALSO return all-zero win rates if no trades
            // closed yet — that is the honest signal, not a stub.
            let (mag_wr, mag_var) = match self.gpu_experience_collector.as_mut() {
                Some(c) => match c.per_magnitude_winrate_and_variance() {
                    Ok(v) => v,
                    Err(e) => {
                        tracing::warn!(
                            "HEALTH_DIAG mag_stats readback failed (zeroing for this epoch): {e}"
                        );
                        ([0.0_f32; 3], [0.0_f32; 3])
                    }
                },
                None => ([0.0_f32; 3], [0.0_f32; 3]),
            };

            tracing::info!(
                "HEALTH_DIAG[{}]: health={:.2} components [q_gap={:.2} q_var={:.2} atoms={:.2} grad_stable={:.2} ens_agree={:.2} grad_cos={:.2} spectral={:.2}] effective [cql_alpha={:.4} iqn_budget={:.2} cql_budget={:.2} c51_budget={:.2} tau={:.5} sarsa_tau={:.2} gamma={:.3} cf_ratio={:.2}] novels [distill={} barrier={:.3} plasticity={} ib={:.3} ensemble_collapse={:.2} contrarian={} meta_q_pred={:.2}] diag [sharpe_ema={:.3} action_entropy={:.2}] gems [g12_predictive={:.4}] mag [q_full={:.3} q_half={:.3} q_quarter={:.3} var_scale={:.3} kelly_f={:.3} avg_win_ratio={:.3} grad_ratio_mag_dir={:.4} dist_q={:.3} dist_h={:.3} dist_f={:.3}] grad_split_bwd [iqn={:.4} cql={:.4} c51={:.4} ens={:.4}] grad_split_aux [distill={:.4} rec={:.4} pred={:.4} cql_sx={:.4} c51_bs={:.4}] grad_trunk [iqn={:.4} ens={:.4} c51={:.4} cql={:.4} distill={:.4} rec={:.4} pred={:.4} cql_sx={:.4} c51_bs={:.4}] grad_abs [dir={:.6e} mag={:.6e}] trail [fire_q={:.3} fire_h={:.3} fire_f={:.3} hold_q={:.2} hold_h={:.2} hold_f={:.2}] mag_stats [wr_q={:.3} wr_h={:.3} wr_f={:.3} var_q={:.4e} var_h={:.4e} var_f={:.4e}] noisy [vsn_mag={:.3} vsn_dir={:.3} sigma_mag={:.4} sigma_dir={:.4} drift_mag={:.3} drift_dir={:.3}] eval_dist [eq={:.3} eh={:.3} ef={:.3}] reward_contrib [popart={:.3} cf={:.3} trail_r={:.3} micro={:.3}] controller [anti_lr={} tau={} gamma={} clip={} cql={} cost={} fire_frac={:.2}] explore [ent_mag={:.2} ent_dir={:.2} sigma_mean={:.4}]",
                epoch,
                health_value,
                self.learning_health.components.q_gap_norm,
                self.learning_health.components.q_var_norm,
                self.learning_health.components.atom_util_norm,
                self.learning_health.components.grad_stable,
                self.learning_health.components.ens_agree,
                self.learning_health.components.grad_consistency_norm,
                self.learning_health.components.spectral_gap_norm,
                self.last_cql_alpha_eff.unwrap_or(0.0),
                self.last_iqn_budget_eff.unwrap_or(0.40),
                self.last_cql_budget_eff.unwrap_or(0.00),
                self.last_c51_budget_eff.unwrap_or(0.55),
                self.last_tau_eff.unwrap_or(0.005),
                self.last_sarsa_tau_factor.unwrap_or(1.0),
                self.last_gamma_eff.unwrap_or(0.99),
                self.last_cf_ratio_eff.unwrap_or(0.5),
                if self.last_distill_active.unwrap_or(false) { "on" } else { "off" },
                self.last_barrier_loss.unwrap_or(0.0),
                if self.last_plasticity_ready.unwrap_or(true) { "ready" } else { "cooldown" },
                self.last_ib_penalty.unwrap_or(0.0),
                self.last_ensemble_collapse_score.unwrap_or(0.0),
                if self.last_contrarian_active.unwrap_or(false) { "on" } else { "off" },
                self.last_meta_q_pred.unwrap_or(0.0),
                self.training_sharpe_ema,
                self.last_action_entropy.unwrap_or(1.0),
                g12_loss,
                // Track 1 — magnitude (10 f32): q_full, q_half, q_quarter, var_scale,
                // kelly_f, avg_win_ratio, grad_ratio_mag_dir, dist_q, dist_h, dist_f.
                // * q_full/half/quarter: mean of q_out_buf mag-branch cols
                //   [b0..b0+b1] over the batch (dtoh at epoch boundary).
                // * var_scale: epoch mean of per-sample `1/(1+sqrt(Var[Q]))`
                //   written by experience_env_step at line 1422 (0.0 when
                //   Var[Q] is disabled for all samples — semantic, not a stub).
                // * kelly_f, avg_win_ratio: real (from trade_stats_history last()).
                // * grad_ratio_mag_dir: real (Task 0.4); action_dist wired.
                q_full, q_half, q_quarter,
                var_scale_mean,
                kelly_f_mean,
                avg_win_ratio,
                grad_ratio_mag_dir,
                dist_q, dist_h, dist_f,
                // Task 2.0 (+ extension) — per-component grad decomposition
                // (9 f32 split across two HEALTH_DIAG groups to keep each
                // under ~500 chars and separate backward writers from aux-
                // graph writers + multiplicative scalings):
                //
                //   grad_split_bwd [iqn cql c51 ens]
                //     The four original Task 2.0 measurement points — full
                //     backward-gradient contributions from each loss family,
                //     reported as mag_norm / dir_norm ratios over branch
                //     tensors 8..16 only.
                //
                //   grad_split_aux [distill rec pred cql_sx c51_bs]
                //     The five Task 2.0 extension points inside the aux-graph
                //     gap between CQL and Ens: three auxiliary-loss writers
                //     (distill / recursive-confidence / predictive-coding)
                //     and two multiplicative scalings (cql_sx = apply_cql_
                //     saxpy; c51_bs = apply_c51_budget_scale). Localizes
                //     exactly which stage zeroes magnitude gradient.
                //
                //   grad_trunk [iqn ens c51 cql distill rec pred cql_sx c51_bs]
                //     Task 2.Z diagnostic extension — absolute L2 norms of
                //     each component's additive delta on TRUNK tensors 0..4
                //     (w_s1, b_s1, w_s2, b_s2). IQN and Ens are the two
                //     components wired to SAXPY into the trunk (via
                //     `apply_iqn_trunk_gradient` + `apply_ensemble_
                //     diversity_backward`); all other components write
                //     exclusively to branch heads and should report
                //     near-zero here. Required because grad_split_bwd /
                //     grad_split_aux structurally measure branch heads
                //     only, so IQN + Ens reported `0.0000` in those groups
                //     regardless of their actual trunk contribution.
                //
                // In-graph pinned-snapshot + reduction-kernel pipeline;
                // zero-copy readback at epoch boundary. Feeds Task 2.1
                // decision tree for the H4 fix.
                grad_mag_iqn, grad_mag_cql, grad_mag_c51, grad_mag_ens,
                grad_mag_distill, grad_mag_rec, grad_mag_pred,
                grad_mag_cql_sx, grad_mag_c51_bs,
                // Task 2.Z diagnostic — absolute TRUNK-slice L2 norms per
                // loss component (tensors 0..4 = w_s1, b_s1, w_s2, b_s2).
                // Ordered as [iqn, ens, c51, cql, distill, rec, pred, cql_sx,
                // c51_bs] — IQN + Ens first because they are the two
                // components that actually SAXPY into trunk (via
                // `apply_iqn_trunk_gradient` at gpu_dqn_trainer.rs:4882 and
                // `apply_ensemble_diversity_backward` at 5051); the remaining
                // seven slots are near-zero baselines (C51/CQL/etc. write
                // exclusively to branch heads 8..16) and are included so the
                // measurement is symmetric across the same 9 components
                // tracked by grad_split_bwd + grad_split_aux.
                //
                // grad_trunk_norms index order matches the mag/dir caches:
                // [iqn, cql, cql_sx, c51, c51_bs, distill, rec, pred, ens].
                grad_trunk_norms[0], grad_trunk_norms[8], grad_trunk_norms[3],
                grad_trunk_norms[1], grad_trunk_norms[5], grad_trunk_norms[6],
                grad_trunk_norms[7], grad_trunk_norms[2], grad_trunk_norms[4],
                // Task 2.0 confirmation — absolute per-branch grad L2 norms
                // (dir, mag) in scientific notation. Disambiguates the
                // `grad_ratio_mag_dir` threshold collapse (0.0 when dir < 1e-9).
                grad_dir_abs, grad_mag_abs,
                // Track 1 — trail (6 f32): fire_q/h/f, hold_q/h/f (H6 — Task 0.5).
                // Real values from GPU experience collector's per-sample buffers.
                trail_rates[0], trail_rates[1], trail_rates[2],
                hold_means[0], hold_means[1], hold_means[2],
                // Task 2.X prerequisite — mag_stats (6 f32): win rate per
                // magnitude (wr_q/h/f = Σ profitable / Σ closing) + realized
                // step-return variance per magnitude (var_q/h/f =
                // Var[step_ret | action_mag == bin]). Feeds the scoping-doc
                // decision tree for distinguishing Q-estimation bias (C) from
                // genuine data-disfavor (A) in the Full=0% eval collapse.
                mag_wr[0], mag_wr[1], mag_wr[2],
                mag_var[0], mag_var[1], mag_var[2],
                // Track 1 — noisy/VSN/drift (6 f32): vsn_mag, vsn_dir, sigma_mag,
                // sigma_dir, drift_mag, drift_dir. All real (Task 0.6):
                //   * vsn_mag/dir: mean |w| across each branch's VSN projection
                //     weights (H7 surrogate — live mask not materialized).
                //   * sigma_mag/dir: NoisyNets σ mean across fc+out layers.
                //   * drift_mag/dir: RMS ‖target − online‖ across each branch's
                //     4 weight tensors (host-computed cold path; no atomics).
                vsn_mag, vsn_dir, sigma_mag, sigma_dir, drift_mag, drift_dir,
                // Track 1 — eval dist (3 f32): per-magnitude action distribution
                // observed in the most recent validation backtest. H10 signal.
                self.last_eval_magnitude_dist[0],
                self.last_eval_magnitude_dist[1],
                self.last_eval_magnitude_dist[2],
                // Track 2 — reward contrib (4 f32): popart drift, cf_flip rate,
                // trail rate, micro additive ratio (Task 0.8).
                // History: `segment_patience` slot deleted in Task 0.8 (kernel
                // never wired a patience term); `loss_aversion` slot deleted in
                // Task 2.4 (R6 clamp relocated to c51_loss_kernel.cu::
                // block_bellman_project_f, reward-layer diagnostic no longer
                // has source signal).
                reward_contrib[0], reward_contrib[1], reward_contrib[2],
                reward_contrib[3],
                // Track 3 — controllers (6 bool per-epoch fire + 1 f32 max running rate)
                fire_lr, fire_tau, fire_gamma, fire_clip, fire_cql, fire_cost, fire_frac,
                // Track 4 — explore (3 f32): ent_mag, ent_dir, sigma_mean.
                // sigma_mean = overall NoisyNets σ across mag + dir branches (Task 0.6).
                ent_mag, ent_dir, sigma_mean_overall,
            );

            // Task #212 — pure-policy magnitude intent distribution BEFORE
            // Kelly cap clipping. Sourced from `last_eval_intent_magnitude_dist`
            // (cached by `metrics.rs::compute_validation_loss` from
            // `gpu_evaluator.read_eval_intent_magnitude_distribution`, which
            // reads `intent_mag_buf` — the action-select kernel's pre-cap
            // mag_idx). Paired with the `eval_dist [eq=...]` segment in the
            // big info! line above so operators can distinguish "policy isn't
            // learning Full" (both metrics near zero) from "Kelly cap is
            // suppressing Full" (intent_dist_f ≫ eval_dist_f). Per memory
            // pearl `project_magnitude_eval_collapse_kelly_capped.md`: pure
            // observability — Kelly cap math itself is unchanged.
            tracing::info!(
                "HEALTH_DIAG[{}]: intent_dist [iq={:.3} ih={:.3} if={:.3}]",
                epoch,
                self.last_eval_intent_magnitude_dist[0],
                self.last_eval_intent_magnitude_dist[1],
                self.last_eval_intent_magnitude_dist[2],
            );

            // SP9 (Fix 37, 2026-05-03): Kelly cold-start warmup floor
            // observability per `feedback_no_hiding`. Splits the OR'd
            // confidence formula so operators can see which axis is firing:
            //   * `kelly_warmup_floor`        — the final adaptive floor
            //     value written to ISV[KELLY_WARMUP_FLOOR_INDEX] each epoch
            //   * `intent_eval_divergence`    — behavioral-axis numerator
            //     (intent_f / eval_f); high under val-Flat-collapse
            //   * `combined_confidence`       — emitted as 3 components:
            //     `[stat=X bhv=Y tmp=Z]` so we can see exactly which axis
            //     dominates the cold-start exit
            //   * 3 EMA targets for self-tuning visibility
            // All values are read from the GPU via mapped-pinned ISV per
            // `feedback_no_htod_htoh_only_mapped_pinned`.
            {
                use crate::cuda_pipeline::sp5_isv_slots::{
                    KELLY_WARMUP_FLOOR_INDEX,
                    Q_VAR_MAG_EMA_INDEX,
                    INTENT_EVAL_DIVERGENCE_INDEX,
                    KELLY_SAMPLE_COUNT_TARGET_INDEX,
                    KELLY_DIVERGENCE_TARGET_INDEX,
                    KELLY_TEMPORAL_TARGET_INDEX,
                    KELLY_SAMPLE_COUNT_INDEX,
                };
                use crate::cuda_pipeline::gpu_dqn_trainer::EPOCH_IDX_INDEX;
                let (warmup_floor, divergence, q_var_mag_ema,
                     stat_count_tgt, div_tgt, temp_tgt,
                     kelly_samples, epoch_idx_isv) =
                    if let Some(ref fused) = self.fused_ctx {
                        let trainer = fused.trainer();
                        (
                            trainer.read_isv_signal_at(KELLY_WARMUP_FLOOR_INDEX),
                            trainer.read_isv_signal_at(INTENT_EVAL_DIVERGENCE_INDEX),
                            trainer.read_isv_signal_at(Q_VAR_MAG_EMA_INDEX),
                            trainer.read_isv_signal_at(KELLY_SAMPLE_COUNT_TARGET_INDEX),
                            trainer.read_isv_signal_at(KELLY_DIVERGENCE_TARGET_INDEX),
                            trainer.read_isv_signal_at(KELLY_TEMPORAL_TARGET_INDEX),
                            trainer.read_isv_signal_at(KELLY_SAMPLE_COUNT_INDEX),
                            trainer.read_isv_signal_at(EPOCH_IDX_INDEX),
                        )
                    } else {
                        (0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32,
                         0.0_f32, 0.0_f32, 0.0_f32)
                    };
                // Reproduce the kernel's confidence axes for visibility.
                let eps_div = 1e-6_f32;
                let stat_conf = (kelly_samples / stat_count_tgt.max(eps_div))
                    .clamp(0.0, 1.0);
                let bhv_conf  = (1.0 - divergence / div_tgt.max(eps_div))
                    .clamp(0.0, 1.0);
                let tmp_conf  = (epoch_idx_isv / temp_tgt.max(eps_div))
                    .clamp(0.0, 1.0);
                tracing::info!(
                    "HEALTH_DIAG[{}]: sp9_kelly_warmup [floor={:.4} divergence={:.4} q_var_mag_ema={:.4e} conf [stat={:.3} bhv={:.3} tmp={:.3}] targets [stat_count_tgt={:.2} div_tgt={:.4} temp_tgt={:.2}]]",
                    epoch,
                    warmup_floor, divergence, q_var_mag_ema,
                    stat_conf, bhv_conf, tmp_conf,
                    stat_count_tgt, div_tgt, temp_tgt,
                );
            }

            // SP11 Fix 39 (2026-05-04, Task A2): reward-subsystem
            // controller observability. Emits the 10 controller outputs
            // (6 component weights + curiosity_pressure +
            // saboteur_intensity_mult + reward_weight_floor +
            // curiosity_bound) and the inline-computed `improvement_z`
            // canary derivation so operators can see whether the
            // controller is acting on a fresh val-sharpe trend signal or
            // a Pearl-A-sentinel-bootstrap state.
            //
            // `improvement_z` is NOT in ISV — the controller consumes it
            // internally (sigmoid mapping → improving/stagnant). For
            // HEALTH_DIAG visibility we re-derive it here from the same
            // canary slots the kernel reads:
            //   z = delta_ema / max(sqrt(var_ema), 1e-6)
            // which is the kernel's `dz` formula verbatim per spec §3.4.
            // Per `feedback_trust_code_not_docs.md` the formula stays in
            // lockstep with the kernel — if the kernel changes, this line
            // updates in the same commit.
            //
            // No consumer reads ISV[340..350) yet (Layer A is additive),
            // so all weights show their FoldReset sentinel zero on the
            // first-emit epoch then trend toward populated values via
            // Pearl A bootstrap. The chained `apply_pearls_ad_kernel`
            // smoothing means the line shows the SMOOTHED outputs
            // (consumer-side reads), not raw scratch values, per spec
            // §3.4.1.
            {
                use crate::cuda_pipeline::sp5_isv_slots::{
                    REWARD_POPART_WEIGHT_INDEX, REWARD_CF_WEIGHT_INDEX,
                    REWARD_TRAIL_WEIGHT_INDEX, REWARD_MICRO_WEIGHT_INDEX,
                    REWARD_OPP_COST_WEIGHT_INDEX, REWARD_BONUS_WEIGHT_INDEX,
                    CURIOSITY_PRESSURE_INDEX, SABOTEUR_INTENSITY_MULT_INDEX,
                    REWARD_WEIGHT_FLOOR_INDEX, CURIOSITY_BOUND_INDEX,
                    VAL_SHARPE_DELTA_EMA_INDEX, VAL_SHARPE_VAR_EMA_INDEX,
                };
                let (w_pop, w_cf, w_tr, w_mi, w_oc, w_bn,
                     curiosity_p, sab_mult, w_floor, curiosity_b,
                     delta_ema, var_ema) =
                    if let Some(ref fused) = self.fused_ctx {
                        let trainer = fused.trainer();
                        (
                            trainer.read_isv_signal_at(REWARD_POPART_WEIGHT_INDEX),
                            trainer.read_isv_signal_at(REWARD_CF_WEIGHT_INDEX),
                            trainer.read_isv_signal_at(REWARD_TRAIL_WEIGHT_INDEX),
                            trainer.read_isv_signal_at(REWARD_MICRO_WEIGHT_INDEX),
                            trainer.read_isv_signal_at(REWARD_OPP_COST_WEIGHT_INDEX),
                            trainer.read_isv_signal_at(REWARD_BONUS_WEIGHT_INDEX),
                            trainer.read_isv_signal_at(CURIOSITY_PRESSURE_INDEX),
                            trainer.read_isv_signal_at(SABOTEUR_INTENSITY_MULT_INDEX),
                            trainer.read_isv_signal_at(REWARD_WEIGHT_FLOOR_INDEX),
                            trainer.read_isv_signal_at(CURIOSITY_BOUND_INDEX),
                            trainer.read_isv_signal_at(VAL_SHARPE_DELTA_EMA_INDEX),
                            trainer.read_isv_signal_at(VAL_SHARPE_VAR_EMA_INDEX),
                        )
                    } else {
                        (0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32,
                         0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32,
                         0.0_f32, 0.0_f32)
                    };
                let improvement_z = delta_ema / var_ema.sqrt().max(1e-6_f32);
                tracing::info!(
                    "HEALTH_DIAG[{}]: sp11_reward [w_pop={:.3} w_cf={:.3} w_tr={:.3} w_mi={:.3} w_oc={:.3} w_bn={:.3}] [curiosity_p={:.3} sab_mult={:.3} w_floor={:.3} curiosity_b={:.3}] [improvement_z={:.3}]",
                    epoch,
                    w_pop, w_cf, w_tr, w_mi, w_oc, w_bn,
                    curiosity_p, sab_mult, w_floor, curiosity_b,
                    improvement_z,
                );

                // SP12 v3 (2026-05-04): per-trade event-driven reward composition diagnostic.
                // Mirrors the constants emitted into the experience_env_step kernel for
                // this epoch — lets `multi_seed_validation` / smoke pipelines verify
                // (1) the asymmetric cap is in force [pos=+5, neg=-10], (2) the
                // min-hold target / penalty_max anchors match the spec defaults, and
                // (3) the temperature annealing schedule is on the expected curve
                // (forgiving early → sharp late). Same emit cadence as sp11_reward
                // above (per-epoch on the metrics path).
                tracing::info!(
                    "HEALTH_DIAG[{}]: sp12_event_reward [pos_cap={:.1} neg_cap={:.1} min_hold_tgt={:.1} min_hold_T={:.2} min_hold_pen={:.1}]",
                    epoch,
                    5.0_f32,    // REWARD_POS_CAP — Invariant-1 anchor in state_layout.cuh
                    -10.0_f32,  // REWARD_NEG_CAP
                    30.0_f32,   // MIN_HOLD_TARGET (Phase 1 default; Phase 2 lifts to ISV)
                    min_hold_temperature_for_epoch(epoch),
                    3.0_f32,    // MIN_HOLD_PENALTY_MAX
                );
            }

            // SP6 Pearl 2: per-branch budget HEALTH_DIAG lines — enables Layer C debugging
            // by showing how much each branch's loss budget differs from the trunk mean.
            if let Some(arr) = self.last_c51_budget_per_branch {
                tracing::info!(
                    "HEALTH_DIAG[{}]: c51_budget_per_branch [dir={:.4} mag={:.4} ord={:.4} urg={:.4}]",
                    epoch, arr[0], arr[1], arr[2], arr[3],
                );
            }
            if let Some(arr) = self.last_iqn_budget_per_branch {
                tracing::info!(
                    "HEALTH_DIAG[{}]: iqn_budget_per_branch [dir={:.4} mag={:.4} ord={:.4} urg={:.4}]",
                    epoch, arr[0], arr[1], arr[2], arr[3],
                );
            }
            if let Some(arr) = self.last_cql_budget_per_branch {
                tracing::info!(
                    "HEALTH_DIAG[{}]: cql_budget_per_branch [dir={:.4} mag={:.4} ord={:.4} urg={:.4}]",
                    epoch, arr[0], arr[1], arr[2], arr[3],
                );
            }

            // SP7 observability: per-branch Q-variance HEALTH_DIAG line.
            // Reads ISV[Q_VAR_PER_BRANCH_BASE..+4) = ISV[222..226), written by
            // `q_branch_stats_kernel.cu` (scratch slot 2 per branch) and routed into
            // ISV via `apply_pearls_ad_kernel` in `launch_sp5_pearl_1_atom`.
            // This is the ACTUAL signal the SP7 controller's flatness gate reads
            // from `FLATNESS_BASE = ISV[206..210)` (derived from Q-variance here).
            // Semantically distinct from `mag_stats [var_q/h/f]` in the main
            // HEALTH_DIAG line, which is the realized step-return variance per
            // magnitude bin from `gpu_experience_collector.per_magnitude_winrate_and_variance()`.
            {
                use crate::cuda_pipeline::sp5_isv_slots::Q_VAR_PER_BRANCH_BASE;
                let (qv_dir, qv_mag, qv_ord, qv_urg) = if let Some(ref fused) = self.fused_ctx {
                    let trainer = fused.trainer();
                    (
                        trainer.read_isv_signal_at(Q_VAR_PER_BRANCH_BASE),
                        trainer.read_isv_signal_at(Q_VAR_PER_BRANCH_BASE + 1),
                        trainer.read_isv_signal_at(Q_VAR_PER_BRANCH_BASE + 2),
                        trainer.read_isv_signal_at(Q_VAR_PER_BRANCH_BASE + 3),
                    )
                } else {
                    (0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32)
                };
                tracing::info!(
                    "HEALTH_DIAG[{}]: q_var_per_branch [dir={:.4} mag={:.4} ord={:.4} urg={:.4}]",
                    epoch, qv_dir, qv_mag, qv_ord, qv_urg,
                );
            }

            // SP7 observability (grad_decomp_pinned): surface the exact contents of
            // grad_decomp_result_pinned[0..3, 12..15, 36..48] (iqn/cql_raw/c51 ×
            // [mag, dir, trunk]) that the SP7 loss-balance controller read in the
            // last step of this epoch.  The values are already cached in the
            // DQNTrainer host arrays by `refresh_grad_component_norms` (called ~300
            // lines above in the epoch-boundary block); reading them here costs
            // zero extra synchronisation.
            //
            // Component index mapping (matches `grad_decomp_result_pinned` layout):
            //   IQN     = component 0  → pinned offsets  [0, 1, 2]
            //   CQL_RAW = component 1  → pinned offsets  [3, 4, 5]   (Path A 2026-05-03)
            //   C51     = component 3  → pinned offsets  [9, 10, 11]
            //
            // SP7 Path A (2026-05-03): label changed `cql_sx` → `cql_raw` to
            // reflect the new producer. `cql_sx` (component 2, post-SAXPY
            // budget-scaled delta on `grad_buf`) is no longer the SP7 reference;
            // `cql_raw` (component 1, raw norm of `cql_grad_scratch` written by
            // `cql_raw_norm_compute`) is — see audit doc Fix 31 SP7 Path A.
            //
            // This disambiguates "grad_decomp not populated at SP7 read time"
            // (all zeros here) from "values non-zero but cold-start branch fires
            // for another reason" (non-zeros here, lb_active stays 0.0).
            {
                let (iqn_m, iqn_d, iqn_t,
                     cql_raw_m, cql_raw_d, cql_raw_t,
                     c51_m, c51_d, c51_t) = if let Some(ref fused) = self.fused_ctx {
                    let trainer = fused.trainer();
                    let mag   = trainer.grad_component_norms_mag();
                    let dir   = trainer.grad_component_norms_dir();
                    let trunk = trainer.grad_component_norms_trunk();
                    (mag[0], dir[0], trunk[0],   // IQN
                     mag[1], dir[1], trunk[1],   // CQL_RAW (Path A — was [2] CQL_SX)
                     mag[3], dir[3], trunk[3])   // C51
                } else {
                    (0.0_f32, 0.0_f32, 0.0_f32,
                     0.0_f32, 0.0_f32, 0.0_f32,
                     0.0_f32, 0.0_f32, 0.0_f32)
                };
                tracing::info!(
                    "HEALTH_DIAG[{}]: grad_decomp_pinned [iqn:[m={:.4} d={:.4} t={:.4}] cql_raw:[m={:.4} d={:.4} t={:.4}] c51:[m={:.4} d={:.4} t={:.4}]]",
                    epoch,
                    iqn_m, iqn_d, iqn_t,
                    cql_raw_m, cql_raw_d, cql_raw_t,
                    c51_m, c51_d, c51_t,
                );
            }

            // SP7 observability (lb_active_per_branch): surface the per-(head,
            // branch) activation-flag state written by the SP7 controller and
            // smoothed into ISV[LB_{CQL,C51}_ACTIVE_BASE..+4) by
            // `apply_pearls_ad_kernel`.  Values are 0.0 (cold-start, controller
            // has not yet fired on this (head, branch)) or approaching 1.0
            // (active path executed at least once this fold).
            //
            // Paired with `grad_decomp_pinned` above: if both are all-zero the
            // pinned buffer was empty at SP7's read site; if grad_decomp is
            // non-zero but lb_active stays 0.0 the cold-start branch fired for
            // a different reason (e.g. Welford-α cold-start or EPS_DIV guard).
            {
                use crate::cuda_pipeline::sp5_isv_slots::{
                    LB_CQL_ACTIVE_BASE, LB_C51_ACTIVE_BASE,
                };
                let (cql_d, cql_m, cql_o, cql_u,
                     c51_d, c51_m, c51_o, c51_u) = if let Some(ref fused) = self.fused_ctx {
                    let trainer = fused.trainer();
                    (
                        trainer.read_isv_signal_at(LB_CQL_ACTIVE_BASE),
                        trainer.read_isv_signal_at(LB_CQL_ACTIVE_BASE + 1),
                        trainer.read_isv_signal_at(LB_CQL_ACTIVE_BASE + 2),
                        trainer.read_isv_signal_at(LB_CQL_ACTIVE_BASE + 3),
                        trainer.read_isv_signal_at(LB_C51_ACTIVE_BASE),
                        trainer.read_isv_signal_at(LB_C51_ACTIVE_BASE + 1),
                        trainer.read_isv_signal_at(LB_C51_ACTIVE_BASE + 2),
                        trainer.read_isv_signal_at(LB_C51_ACTIVE_BASE + 3),
                    )
                } else {
                    (0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32,
                     0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32)
                };
                tracing::info!(
                    "HEALTH_DIAG[{}]: lb_active_per_branch [cql:[d={:.4} m={:.4} o={:.4} u={:.4}] c51:[d={:.4} m={:.4} o={:.4} u={:.4}]]",
                    epoch,
                    cql_d, cql_m, cql_o, cql_u,
                    c51_d, c51_m, c51_o, c51_u,
                );
            }

            // C.2 Plan 3 Task 1 (spec §4.C.2): reward_split HEALTH_DIAG line.
            // Reads 6 ISV EMA slots updated by the GPU reward_component_ema kernel
            // launched just above. CPU-side code only reads; GPU wrote the values.
            {
                use crate::cuda_pipeline::gpu_dqn_trainer::{
                    REWARD_POPART_EMA_INDEX, REWARD_CF_EMA_INDEX, REWARD_TRAIL_EMA_INDEX,
                    REWARD_MICRO_EMA_INDEX, REWARD_OPP_COST_EMA_INDEX, REWARD_BONUS_EMA_INDEX,
                };
                let (rp, rc, rt, rm, ro, rb) = if let Some(ref fused) = self.fused_ctx {
                    let trainer = fused.trainer();
                    (
                        trainer.read_isv_signal_at(REWARD_POPART_EMA_INDEX),
                        trainer.read_isv_signal_at(REWARD_CF_EMA_INDEX),
                        trainer.read_isv_signal_at(REWARD_TRAIL_EMA_INDEX),
                        trainer.read_isv_signal_at(REWARD_MICRO_EMA_INDEX),
                        trainer.read_isv_signal_at(REWARD_OPP_COST_EMA_INDEX),
                        trainer.read_isv_signal_at(REWARD_BONUS_EMA_INDEX),
                    )
                } else {
                    (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
                };
                tracing::info!(
                    "HEALTH_DIAG[{}]: reward_split [popart={:.4} cf={:.4} trail={:.4} micro={:.4} opp_cost={:.4} bonus={:.4}]",
                    epoch, rp, rc, rt, rm, ro, rb,
                );
            }

            // Plan 4 Task 6 Commit B: aux-heads diagnostic line. Reads
            // ISV[AUX_NEXT_BAR_MSE_EMA_INDEX=113] +
            // ISV[AUX_REGIME_CE_EMA_INDEX=114] (populated by
            // `aux_heads_loss_ema_update` launched per-step above) and
            // the host-side `aux_weight` baked into the most-recently-
            // captured graph. Cold-start (epoch 0 of each fold): loss EMAs
            // at 0.0, aux_weight at 0.05.
            //
            // SP13 B1.0 (2026-05-05): the former `label_scale` field
            // (read from retired ISV[117]) was dropped together with that
            // slot. Labels are z-normalised at the data layer so
            // monitoring the scale was redundant.
            {
                use crate::cuda_pipeline::gpu_dqn_trainer::{
                    AUX_NEXT_BAR_MSE_EMA_INDEX, AUX_REGIME_CE_EMA_INDEX,
                };
                let (aux_nb_mse, aux_rg_ce, aux_w) =
                    if let Some(ref fused) = self.fused_ctx {
                        let trainer = fused.trainer();
                        (
                            trainer.read_isv_signal_at(AUX_NEXT_BAR_MSE_EMA_INDEX),
                            trainer.read_isv_signal_at(AUX_REGIME_CE_EMA_INDEX),
                            trainer.aux_weight(),
                        )
                    } else {
                        (0.0, 0.0, 0.0)
                    };
                tracing::info!(
                    "HEALTH_DIAG[{}]: aux [next_bar_mse={:.3e} regime_ce={:.3e} w={:.3}]",
                    epoch, aux_nb_mse, aux_rg_ce, aux_w,
                );
            }

            // SP13 Phase 0a P0a.T4 (2026-05-04): aux directional-accuracy +
            // Hold-pricing HEALTH_DIAG. Reads ISV slots populated by the
            // per-step producer chains above:
            //   ISV[373..375) — aux dir-acc fast/slow EMAs (α=0.3 / 0.05)
            //   ISV[375]      — mean(tanh(aux_pred)) per-step state
            //   ISV[372]      — directional accuracy target (static 0.55)
            //   ISV[380..383) — Hold-pricing controller output / target /
            //                   observed-rate EMA (slot 380 written by the
            //                   per-epoch host controller at training_loop:3604;
            //                   slot 382 written by the per-step
            //                   gpu_experience_collector chain)
            // Diagnostic only — exposes the SP13 chain's signal flow before
            // Phase 0b wires the consumers (aux-w controller, dir-skill
            // bonus, Q-head slot-375 input).
            {
                use crate::cuda_pipeline::sp13_isv_slots::{
                    TARGET_DIR_ACC_INDEX,
                    AUX_DIR_ACC_SHORT_EMA_INDEX, AUX_DIR_ACC_LONG_EMA_INDEX,
                    AUX_DIR_PREDICTION_INDEX,
                    HOLD_COST_INDEX, HOLD_RATE_TARGET_INDEX,
                    HOLD_RATE_OBSERVED_EMA_INDEX,
                };
                let (
                    target_dir_acc, dir_short, dir_long, aux_pred_tanh,
                    hold_cost, hold_target, hold_observed,
                ) = if let Some(ref fused) = self.fused_ctx {
                    let trainer = fused.trainer();
                    (
                        trainer.read_isv_signal_at(TARGET_DIR_ACC_INDEX),
                        trainer.read_isv_signal_at(AUX_DIR_ACC_SHORT_EMA_INDEX),
                        trainer.read_isv_signal_at(AUX_DIR_ACC_LONG_EMA_INDEX),
                        trainer.read_isv_signal_at(AUX_DIR_PREDICTION_INDEX),
                        trainer.read_isv_signal_at(HOLD_COST_INDEX),
                        trainer.read_isv_signal_at(HOLD_RATE_TARGET_INDEX),
                        trainer.read_isv_signal_at(HOLD_RATE_OBSERVED_EMA_INDEX),
                    )
                } else {
                    (0.55, 0.5, 0.5, 0.0, 0.001, 0.20, 0.0)
                };
                tracing::info!(
                    "HEALTH_DIAG[{}]: aux_dir_acc target={:.4} short={:.4} long={:.4} pred_tanh={:.4}",
                    epoch, target_dir_acc, dir_short, dir_long, aux_pred_tanh,
                );
                tracing::info!(
                    "HEALTH_DIAG[{}]: hold_pricing observed_rate={:.4} target={:.4} cost={:.6}",
                    epoch, hold_observed, hold_target, hold_cost,
                );

                // SP13 B1.1a (2026-05-05): per-class label distribution
                // diagnostic. The 6-element `aux_dir_acc_buf` exposes
                // `n_down / n_up / n_skip` directly from the GPU dir-acc
                // reduce; B1.1a labels are zero-init (producer wires in
                // B1.1b), so this line shows `n_down=B, n_up=0, n_skip=0`
                // until B1.1b lands. Once B1.1b's producer fills real
                // -1/0/1 labels, the distribution becomes diagnostic for
                // the price-trajectory binarisation. `mask_frac = n_skip
                // / B` is the fraction of bars excluded from the CE
                // numerator. The `aux_dir_acc` short/long EMAs are read
                // above; the per-class counts here add the missing
                // observability that B1.1b's producer + smoke A will
                // need.
                let (n_down, n_up, n_skip, mask_frac) = if let Some(ref fused) = self.fused_ctx {
                    let dist = fused.trainer().aux_dir_acc_buf.read_all();
                    // Layout: [dir_acc, pos_pred_frac, pos_label_frac, n_down, n_up, n_skip]
                    let n_down = dist.get(3).copied().unwrap_or(0.0);
                    let n_up   = dist.get(4).copied().unwrap_or(0.0);
                    let n_skip = dist.get(5).copied().unwrap_or(0.0);
                    let total  = (n_down + n_up + n_skip).max(1.0);
                    (n_down, n_up, n_skip, n_skip / total)
                } else {
                    (0.0, 0.0, 0.0, 0.0)
                };
                tracing::info!(
                    "HEALTH_DIAG[{}]: aux_b1_diag dir_acc_short={:.4} dir_acc_long={:.4} n_down={:.0} n_up={:.0} n_skip={:.0} mask_frac={:.4}",
                    epoch, dir_short, dir_long, n_down, n_up, n_skip, mask_frac,
                );
            }

            // Phase 3 T3.5: MoE expert-utilisation + gate entropy HEALTH_DIAG.
            // Extended 2026-04-27: also emits the adaptive λ_eff slot so the
            // controller is observable per epoch — see `launch_moe_lambda_eff_update`.
            {
                use crate::cuda_pipeline::gpu_dqn_trainer::{
                    MOE_EXPERT_UTIL_EMA_BASE, MOE_GATE_ENTROPY_EMA_INDEX,
                    MOE_LAMBDA_EFF_INDEX,
                };
                let (moe_utils, moe_ent, moe_lambda_eff) =
                    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);
                        let lam = trainer.read_isv_signal_at(MOE_LAMBDA_EFF_INDEX);
                        (utils, ent, lam)
                    } else {
                        (vec![0.0f32; 8], 0.0f32, 0.0f32)
                    };
                tracing::info!(
                    "HEALTH_DIAG[{}]: aux_moe [util={:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3},{:.3} ent={:.3} λ_eff={:.4}]",
                    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, moe_lambda_eff,
                );
            }

            // SP14 Layer B Task B.12 (2026-05-05): per-epoch EGF pearl diagnostic.
            // Reads the 13 SP14 ISV slots that the EGF pearl populates each step
            // (producers in experience_kernels.cu launched by the B.1–B.11 chain).
            // gate1/gate2 sigmoid outputs are intentionally omitted — recomputing
            // sigmoid host-side would violate feedback_no_cpu_compute_strict; the
            // inputs (k_aux, k_q, q_dis_short, var_*) are sufficient for a reader
            // to infer the sigmoid outputs and diagnose gradient-hacking events.
            {
                use crate::cuda_pipeline::sp14_isv_slots::{
                    ALPHA_GRAD_SMOOTHED_INDEX, ALPHA_GRAD_RAW_INDEX,
                    BETA_RATE_LIMITER_ADAPTIVE_INDEX,
                    K_AUX_ADAPTIVE_INDEX, K_Q_ADAPTIVE_INDEX,
                    AUX_DIR_ACC_VARIANCE_EMA_INDEX, Q_DISAGREEMENT_VARIANCE_EMA_INDEX,
                    ALPHA_GRAD_RAW_VARIANCE_EMA_INDEX,
                    Q_DISAGREEMENT_SHORT_EMA_INDEX, Q_DISAGREEMENT_LONG_EMA_INDEX,
                    GATE1_OPEN_STATE_INDEX, AUX_DIR_ACC_POST_OPEN_MIN_INDEX,
                    GRADIENT_HACK_LOCKOUT_REMAINING_INDEX,
                };
                let (
                    alpha_smoothed, alpha_raw, beta_adaptive,
                    k_aux_curr, k_q_curr,
                    var_aux, var_q, var_alpha,
                    q_dis_short, q_dis_long,
                    gate1_state, post_open_min, lockout,
                ) = if let Some(ref fused) = self.fused_ctx {
                    let trainer = fused.trainer();
                    (
                        trainer.read_isv_signal_at(ALPHA_GRAD_SMOOTHED_INDEX),
                        trainer.read_isv_signal_at(ALPHA_GRAD_RAW_INDEX),
                        trainer.read_isv_signal_at(BETA_RATE_LIMITER_ADAPTIVE_INDEX),
                        trainer.read_isv_signal_at(K_AUX_ADAPTIVE_INDEX),
                        trainer.read_isv_signal_at(K_Q_ADAPTIVE_INDEX),
                        trainer.read_isv_signal_at(AUX_DIR_ACC_VARIANCE_EMA_INDEX),
                        trainer.read_isv_signal_at(Q_DISAGREEMENT_VARIANCE_EMA_INDEX),
                        trainer.read_isv_signal_at(ALPHA_GRAD_RAW_VARIANCE_EMA_INDEX),
                        trainer.read_isv_signal_at(Q_DISAGREEMENT_SHORT_EMA_INDEX),
                        trainer.read_isv_signal_at(Q_DISAGREEMENT_LONG_EMA_INDEX),
                        trainer.read_isv_signal_at(GATE1_OPEN_STATE_INDEX),
                        trainer.read_isv_signal_at(AUX_DIR_ACC_POST_OPEN_MIN_INDEX),
                        trainer.read_isv_signal_at(GRADIENT_HACK_LOCKOUT_REMAINING_INDEX),
                    )
                } else {
                    (0.0, 0.0, 0.5, 20.0, 15.0, 0.0, 0.0, 0.0, 0.5, 0.5, 0.0, 1.0, 0.0)
                };
                tracing::info!(
                    "HEALTH_DIAG[{}]: pearl_egf_diag α_smoothed={:.4} α_raw={:.4} β={:.3} \
                     k_aux={:.2} k_q={:.2} \
                     var_aux={:.5} var_q={:.5} var_α={:.5} \
                     q_dis_s={:.4} q_dis_l={:.4} \
                     gate1={} post_open_min={:.3} lockout={}",
                    epoch,
                    alpha_smoothed, alpha_raw, beta_adaptive,
                    k_aux_curr, k_q_curr,
                    var_aux, var_q, var_alpha,
                    q_dis_short, q_dis_long,
                    if gate1_state == 1.0 { "open" } else { "closed" },
                    post_open_min, lockout as u32,
                );
            }

            // C1/P1: propagate health to GPU replay buffer for diversity-weighted priorities.
            {
                let mut agent = self.agent.write().await;
                agent.primary_dqn_mut().memory.gpu.set_learning_health(health_value);
            }

            // D4/N4: propagate health to GPU experience collector for adaptive CF curriculum.
            if let Some(ref mut collector) = self.gpu_experience_collector {
                collector.set_learning_health(health_value);
                self.last_cf_ratio_eff = Some(collector.last_cf_ratio_eff());
            }

            // B.3 Plan 3 Task 8: per-epoch seed-phase orchestration.
            // Cold-path CPU read of ISV[SEED_STEPS_DONE/TARGET] to decide
            // whether the next `collect_experiences_gpu` should dispatch the
            // scripted-policy kernel (DONE < TARGET) or the network's
            // `experience_action_select`. The action computation itself stays
            // 100% GPU; only the per-epoch dispatch decision is CPU.
            if let Some(ref fused) = self.fused_ctx {
                use crate::cuda_pipeline::gpu_dqn_trainer::{
                    SEED_STEPS_DONE_INDEX, SEED_STEPS_TARGET_INDEX,
                };
                let seed_done   = fused.read_isv_signal_at(SEED_STEPS_DONE_INDEX);
                let seed_target = fused.read_isv_signal_at(SEED_STEPS_TARGET_INDEX);
                let in_seed_phase = seed_target > 0.0 && seed_done < seed_target;
                if let Some(ref mut collector) = self.gpu_experience_collector {
                    collector.set_seed_phase_active(in_seed_phase);
                }
                tracing::debug!(
                    target: "dqn::B3",
                    epoch = epoch,
                    seed_done = seed_done,
                    seed_target = seed_target,
                    in_seed_phase = in_seed_phase,
                    "seed-phase orchestration"
                );
            }
        }

        // Feed per-step Q-value extremes into monitor (real min/max, not single-sample).
        if self.epoch_q_min.is_finite() && self.epoch_q_max.is_finite() {
            tracing::debug!(
                "epoch_q_stats: min={:.6} max={:.6} avg_q={:.6}",
                self.epoch_q_min, self.epoch_q_max, avg_q
            );
            monitor.track_q_value_range(self.epoch_q_min as f64);
            monitor.track_q_value_range(self.epoch_q_max as f64);

            // Per-sample C51 support is computed by IQL each step — no batch-level adaptation needed.
            if let Some(ref mut fused) = self.fused_ctx {
                let clip = fused.adaptive_clip_value();
                let max_clip = self.hyperparams.gradient_clip_norm.unwrap_or(10000.0) as f32;
                if clip < max_clip {
                    tracing::info!(clip = %format!("{:.1}", clip), "Adaptive gradient clip active");
                }
            }
        }
        monitor.track_q_value_range(avg_q);
        // Q-stats reset moved to reset_epoch_state() — runs at the START of the
        // next epoch, so log_epoch_metrics_and_financials can read the final values.

        // SP14 Layer B Task B.11 (2026-05-05): EGF anti-mesa-optimization
        // circuit breaker. Single-launch state machine that decrements the
        // lockout counter (ISV[GRADIENT_HACK_LOCKOUT_REMAINING = 395]) by
        // 1.0 per epoch, tracks the post-Schmitt-open minimum of
        // aux_dir_acc (ISV[AUX_DIR_ACC_POST_OPEN_MIN = 394]), and forces
        // ISV[GATE1_OPEN_STATE = 391] to 0 if the joint hacking signature
        // fires (aux drops > LOCKOUT_TRIGGER_DROP=0.05 below open-thresh
        // AND q_disagreement rises > LOCKOUT_TRIGGER_DIS_RISE=0.10 above
        // baseline). See `gradient_hack_detect_kernel.cu` header for full
        // detection rationale.
        //
        // MUST run AFTER all per-step alpha_grad_compute launches in the
        // epoch (the circuit breaker reads the epoch-final ISV[391] gate
        // state). Stream order is enforced by sequential same-stream
        // submission — alpha_grad runs in `run_training_steps_slices` on
        // the trainer stream; this launch goes to the same stream.
        //
        // Producer-only at the trainer level — the kernel mutates ISV
        // state consumed by NEXT epoch's alpha_grad_compute via the
        // persistent slot 391 + 395 + 394 reads. No host readback.
        if let Some(ref fused) = self.fused_ctx {
            if let Err(e) = fused.trainer().launch_sp14_gradient_hack_detect() {
                tracing::warn!("SP14 B.11 gradient_hack_detect launch failed: {e}");
            }
        }

        // Return raw accumulated totals for epoch metric computation
        // The caller computes per-step averages from epoch_loss/n, etc.
        Ok(EpochBoundaryMetrics {
            avg_loss: (avg_loss as f64 * n) as f32,
            avg_grad: (avg_grad as f64 * n) as f32,
            avg_q: avg_q * n,
        })
    }

    // sync_gpu_weights deleted — zero-copy: collector reads directly from trainer's params_buf.

    // ═══════════════════════════════════════════════════════════════════════
    // Helper: M2 — Refresh stale PER priorities
    // ═══════════════════════════════════════════════════════════════════════

    // ═══════════════════════════════════════════════════════════════════════
    // Helper: Epoch metrics logging + financials + Prometheus + QuestDB
    // ═══════════════════════════════════════════════════════════════════════

    #[allow(clippy::too_many_arguments)]
    pub(crate) async fn log_epoch_metrics_and_financials(
        &mut self,
        epoch: usize,
        train_step_count: usize,
        boundary: &Option<EpochBoundaryMetrics>,
        monitor: &mut TrainingMonitor,
        epoch_duration: std::time::Duration,
        total_action_counts: &mut [usize; 12],
        total_factored_action_counts: &mut [usize; 108],
        mut q_diagnostics: Option<((f64, f64, f64), [f64; 12])>,
    ) -> Result<EpochLogOutput> {
        // Calculate epoch metrics (average over training steps)
        let (epoch_loss, epoch_q_value, epoch_gradient_norm) = match boundary {
            Some(b) => (b.avg_loss as f64, b.avg_q, b.avg_grad as f64),
            None => (0.0, 0.0, 0.0),
        };

        let (avg_loss, avg_q_value, avg_grad_norm) = if train_step_count > 0 {
            (
                epoch_loss / train_step_count as f64,
                epoch_q_value / train_step_count as f64,
                epoch_gradient_norm / train_step_count as f64,
            )
        } else {
            (0.0, 0.0, 0.0)
        };

        // Log epoch end — now uses current-epoch per-trade reward from GPU monitoring.
        use crate::dqn::logging::AggregatedMetrics;
        let epoch_avg_reward = if !monitor.reward_history.is_empty() {
            monitor.reward_history.iter().sum::<f32>() / monitor.reward_history.len() as f32
        } else {
            0.0
        };
        let epoch_metrics = AggregatedMetrics {
            mean_loss: avg_loss as f32,
            std_loss: 0.0,
            mean_q_value: avg_q_value as f32,
            std_q_value: 0.0,
            mean_reward: epoch_avg_reward,
            mean_gradient_norm: avg_grad_norm as f32,
            batch_count: train_step_count,
        };
        log_epoch_end(epoch + 1, &epoch_metrics, epoch_duration.as_secs_f64());

        // Collect GPU trade stats (cold path, once per epoch)
        let trade_stats = if let Some(ref mut collector) = self.gpu_experience_collector {
            match collector.collect_trade_stats() {
                Ok(ts) => {
                    info!(
                        "GPU trades={} wins={} losses={} PF={:.3}",
                        ts.total_trades,
                        ts.winning_trades,
                        ts.losing_trades,
                        if ts.sum_losses > 1e-10 { ts.sum_wins / ts.sum_losses } else { 0.0 },
                    );
                    ts
                }
                Err(e) => {
                    warn!("collect_trade_stats failed (using defaults): {e}");
                    crate::cuda_pipeline::gpu_experience_collector::TradeStats::default()
                }
            }
        } else {
            crate::cuda_pipeline::gpu_experience_collector::TradeStats::default()
        };
        self.trade_stats_history.push(trade_stats.clone());

        // Reward statistics every 10 epochs
        if (epoch + 1) % 10 == 0 && !monitor.reward_history.is_empty() {
            let rewards = &monitor.reward_history;
            let reward_mean = rewards.iter().sum::<f32>() / rewards.len() as f32;
            let reward_variance = rewards.iter()
                .map(|r| (r - reward_mean).powi(2))
                .sum::<f32>() / rewards.len() as f32;
            let reward_std = reward_variance.sqrt();
            let reward_min = rewards.iter().copied().fold(f32::INFINITY, f32::min);
            let reward_max = rewards.iter().copied().fold(f32::NEG_INFINITY, f32::max);
            let non_zero_count = rewards.iter().filter(|&&r| r.abs() > 1e-9).count();
            let non_zero_pct = (non_zero_count as f32 / rewards.len() as f32) * 100.0;

            info!(
                "REWARD_STATS: epoch={}, mean={:.6}, std={:.6}, min={:.6}, max={:.6}, non_zero={}/{} ({:.1}%)",
                epoch + 1, reward_mean, reward_std, reward_min, reward_max,
                non_zero_count, rewards.len(), non_zero_pct
            );
        }

        // OFI feature diagnostic (every 10 epochs).
        // Layout per state_layout.rs: OFI at [42..62) = raw[42..50) + delta[50..58) + book_agg[58] + log_dur[59] + accel[60] + toxicity[61].
        if (epoch + 1) % 10 == 1 {
            if let Some(ref fused) = self.fused_ctx {
                match fused.read_state_sample(0) {
                    Ok(sample) if sample.len() >= ml_core::state_layout::STATE_DIM => {
                        let ofi_start = ml_core::state_layout::OFI_START;
                        let ofi_raw: &[f32] = &sample[ofi_start..ofi_start + 8];
                        let ofi_delta: &[f32] = &sample[ofi_start + 8..ofi_start + 16];
                        let book_agg = sample[ofi_start + 16];
                        let log_dur = sample[ofi_start + 17];
                        info!(
                            "OFI_DIAG: raw_mean={:.4}, delta_mean={:.4}, book_agg={:.4}, log_dur={:.4}",
                            ofi_raw.iter().sum::<f32>() / 8.0,
                            ofi_delta.iter().sum::<f32>() / 8.0,
                            book_agg, log_dur
                        );
                    }
                    Ok(_) => {} // state too short, skip
                    Err(_) => {} // GPU read failed, non-fatal
                }
            }
        }

        // Safety plateau detection
        if self.safety_loss_history.len() >= 10 {
            let recent_losses: Vec<f32> = self.safety_loss_history
                .iter().rev().take(10).copied().collect();

            let mean: f32 = recent_losses.iter().sum::<f32>() / 10.0;
            let variance: f32 = recent_losses.iter()
                .map(|&x| (x - mean).powi(2))
                .sum::<f32>() / 10.0;
            let std_dev = variance.sqrt();

            if std_dev < mean * 0.01 && mean > 1e-6 {
                self.safety_loss_plateau_counter += 1;
                if self.safety_loss_plateau_counter >= 10 {
                    debug!(
                        "SAFETY: Training stuck for {} epochs (loss variance: {:.6}, mean: {:.6})",
                        self.safety_loss_plateau_counter, std_dev, mean
                    );
                }
            } else {
                self.safety_loss_plateau_counter = 0;
            }
        }

        // Accumulate action counts
        for (i, count) in monitor.action_counts.iter().enumerate() {
            total_action_counts[i] += count;
        }
        for (i, count) in monitor.factored_action_counts.iter().enumerate() {
            total_factored_action_counts[i] += count;
        }

        // Monitoring validation
        if let Err(e) = monitor.validate_all() {
            return Err(e);
        }

        let current_epsilon = self.get_epsilon().await?;
        let (q_min, q_max, q_mean) = monitor.get_q_value_stats();

        info!(
            "Epoch {}/{}: train_loss={:.6}, Q-value={:.4}, grad_norm={:.6}, train_steps={}, epsilon={:.4}, duration={:.2}s",
            epoch + 1, self.hyperparams.epochs,
            avg_loss, avg_q_value, avg_grad_norm, train_step_count,
            current_epsilon, epoch_duration.as_secs_f64()
        );

        // Task 2.5 Bug #3 — removed dead `if !true { agent.update_epsilon() }`
        // block. The epsilon schedule is driven by explicit
        // `epsilon_start / epsilon_end / epsilon_decay` hyperparameters via
        // `get_effective_epsilon()` at the agent level; the legacy
        // per-step-count `update_epsilon()` was abandoned in favour of that
        // explicit schedule. Guarded-by-`!true` is a disabled-toggle pattern
        // per feedback_no_feature_flags — dead code is deleted.

        // Noisy sigma schedule
        if let Some(ref mut scheduler) = self.noisy_sigma_scheduler {
            for _ in 0..train_step_count {
                scheduler.step();
            }
            let sigma_scale = scheduler.get_sigma();
            let mut agent = self.agent.write().await;
            agent.set_noise_sigma_scale(sigma_scale);
        }

        // Reset count-based exploration bonus
        {
            let mut agent = self.agent.write().await;
            agent.reset_count_bonus();
        }

        // LR scheduler
        self.lr_scheduler.step();
        let mut current_lr = self.lr_scheduler.get_lr();

        // #24 Anti-intuitive LR: ASYMMETRIC temporal filter.
        //
        // When training has been doing well, INCREASE LR to escape overfit
        // minima; when struggling, DECREASE LR to stabilize. Original impl fed
        // on raw last-epoch Sharpe — noisy, caused 1M+ grad spikes as the
        // controller flipped every epoch (train-br8cb Fold 0).
        //
        // A symmetric 5-window mean killed the noise but also killed exploration
        // (train-v82b2): RL Sharpe is plentiful-bad and rare-good in early
        // epochs, so the mean stays negative even when individual epochs find
        // real improvements. The controller locked into 0.3× LR and never let
        // the model kick out of its initial plateau.
        //
        // Correct structure is asymmetric:
        //   Good signal → fast response (short window, take max)
        //                 — a single genuinely good epoch is meaningful and
        //                   rare; don't wait for it to be sustained before
        //                   letting the model exploit it.
        //   Bad signal  → slow response (long window, take mean)
        //                 — bad epochs are noisy and common; only dampen LR
        //                   when we're confident the model is stuck.
        //
        // Both windows derive from `anti_lr_warmup` (one knob for the whole
        // controller): full window for bad, half window for good. No new
        // hyperparameter — the asymmetry is structural, not tuned.
        let anti_lr_warmup = 5;
        if epoch >= anti_lr_warmup && !self.sharpe_history.is_empty() {
            let history_len = self.sharpe_history.len();
            let long_window  = anti_lr_warmup.min(history_len);
            let short_window = (anti_lr_warmup / 2).max(1).min(history_len);

            // Fast detector: best of recent few — responsive to new gains.
            let recent_max: f64 = self.sharpe_history
                .iter()
                .rev()
                .take(short_window)
                .copied()
                .fold(f64::NEG_INFINITY, f64::max);

            // Slow detector: mean over longer window — filters noise.
            let sustained_mean: f64 = self.sharpe_history
                .iter()
                .rev()
                .take(long_window)
                .sum::<f64>() / long_window as f64;

            let thresh = f64::from(self.hyperparams.anti_lr_sharpe_threshold);
            let base_lr = self.lr_scheduler.get_initial_lr();
            // "Good" wins ties — we prefer exploration over dampening when
            // both triggers fire (rare but possible in a noisy window).
            let anti_mult: f64 = if recent_max > thresh {
                f64::from(self.hyperparams.anti_lr_good_mult)
            } else if sustained_mean < -thresh {
                f64::from(self.hyperparams.anti_lr_bad_mult)
            } else {
                1.0
            };
            // Task 2.5 Bug #1 — persist the actual anti-LR intervention so
            // fire detection observes the controller acting, not scheduler
            // drift. Reset to 1.0 at epoch start in reset_epoch_state.
            self.last_anti_mult = anti_mult as f32;
            current_lr = (current_lr * anti_mult).clamp(base_lr * 0.1, base_lr * 5.0);
            if (anti_mult - 1.0).abs() > 0.01 {
                info!(
                    epoch = epoch + 1,
                    recent_max = %format!("{:.3}", recent_max),
                    sustained_mean = %format!("{:.3}", sustained_mean),
                    anti_mult = %format!("{:.1}x", anti_mult),
                    lr = %format!("{:.2e}", current_lr),
                    "Anti-intuitive LR adjustment"
                );
            }
        }

        if epoch % 10 == 0 {
            info!(
                "Learning rate scheduled update: epoch={}, lr={:.2e} (initial={:.2e})",
                epoch + 1, current_lr, self.lr_scheduler.get_initial_lr()
            );
        }
        let initial_lr = self.lr_scheduler.get_initial_lr();
        if initial_lr > 0.0 {
            let decay_factor = current_lr / initial_lr;
            self.agent.write().await.update_learning_rate(decay_factor)?;
        }
        training_metrics::set_learning_rate("dqn", "current", current_lr);

        // Q-value diagnostics
        if train_step_count > 0 {
            info!(
                "Epoch {}/{}: Q-value range=[{:.4}, {:.4}], mean={:.4}, Q-gap={:.2e}, Q-var={:.2e}, atoms={:.0}%ent/{:.0}%util",
                epoch + 1, self.hyperparams.epochs, q_min, q_max, q_mean,
                self.epoch_q_gap, self.epoch_q_variance,
                self.epoch_atom_entropy * 100.0, self.epoch_atom_utilization * 100.0,
            );

            const Q_VALUE_WARNING_THRESHOLD: f64 = 500_000.0;
            if q_max > Q_VALUE_WARNING_THRESHOLD {
                warn!(
                    "Q-value explosion detected at epoch {}: max Q-value {:.2e} exceeds threshold {:.2e}",
                    epoch + 1, q_max, Q_VALUE_WARNING_THRESHOLD
                );
                warn!("Consider:");
                warn!("  Reducing learning rate (current: {:.2e})", self.hyperparams.learning_rate);
                warn!("  Enabling target network soft updates (Polyak averaging, tau=0.005)");
                warn!("  Adjusting reward scaling");
                training_metrics::record_gradient_explosion("dqn", "current");
            }

            // M3: Combined Q-value diagnostics
            if let Some(((gap_mean, gap_min, gap_max), per_action_avgs)) = q_diagnostics.take()
            {
                info!(
                    "Epoch {}/{}: Q-value gap (best-2nd): mean={:.4}, min={:.4}, max={:.4}",
                    epoch + 1, self.hyperparams.epochs,
                    gap_mean, gap_min, gap_max
                );

                // 12 labels matching the dir*3 + mag layout (dir: Short/Hold/Long/Flat).
                // Hold and Flat magnitude slots are masked to mag=0 by the kernel, so
                // H_Half/H_Full/F_Half/F_Full are structurally empty — listed here so
                // index → name is a straight lookup everywhere in monitoring.
                let names = [
                    "S_Small", "S_Half", "S_Full",   // dir=0 Short
                    "H_Small", "H_Half", "H_Full",   // dir=1 Hold
                    "L_Small", "L_Half", "L_Full",   // dir=2 Long
                    "F_Small", "F_Half", "F_Full",   // dir=3 Flat
                ];
                let parts: Vec<String> = names.iter()
                    .zip(per_action_avgs.iter())
                    .map(|(n, q)| format!("{}={:.4}", n, q))
                    .collect();
                info!(
                    "Epoch {}/{}: Per-action Q: {}",
                    epoch + 1, self.hyperparams.epochs, parts.join(", ")
                );
            }
        }

        // Validation loss — async on separate stream when available.
        //
        // Per-epoch flow (post async-pipeline-fix 2026-04-28):
        //   1. Reporting time (here): consume cached value from PREVIOUS epoch's
        //      async launch, then submit THIS epoch's eval kernels via
        //      `launch_validation_loss` and immediately return — no host wait.
        //      Set `pending_val_loss = Some(0.0)` as the "launched" sentinel
        //      (the value 0.0 is NEVER read; the real val_loss comes from
        //      `consume_validation_loss` at the next epoch's start).
        //   2. Next epoch's start: takes the sentinel, calls
        //      `consume_validation_loss` which does `event.synchronize()` and
        //      parses the mapped-pinned `metrics_buf`. That's the SOLE host
        //      wait per epoch boundary.
        //
        // Epoch 0 (no cached val yet): synchronous launch + consume to seed the
        // pipeline. Wall-clock ~25-30s slower than steady-state epoch 1+, but
        // unavoidable for bootstrap correctness. From epoch 1 onward, the
        // host-side eval cost is amortised via the cross-stream pipelining.
        let val_loss = if self.validation_stream.is_some() && self.cached_async_val_loss.is_some() {
            let cached = self.cached_async_val_loss.unwrap_or(0.0);
            match self.launch_validation_loss().await {
                Ok(()) => {
                    // Sentinel — picked up by the consume block at the next
                    // epoch's start. Value is irrelevant; only the `is_some()`
                    // matters for routing the next-epoch consume call.
                    self.pending_val_loss = Some(0.0);
                }
                Err(e) => { tracing::warn!("Async validation launch failed: {e}"); }
            }
            cached
        } else {
            // Epoch 0 or no validation stream — sync launch + consume to seed
            // `cached_async_val_loss` for the next epoch's async path.
            self.launch_validation_loss().await?;
            let val = self.consume_validation_loss().await?;
            self.cached_async_val_loss = Some(val);
            val
        };
        let val_sharpe = -val_loss; // val_loss = -sharpe (lower is better convention)
        info!(
            "Epoch {}/{}: val_Sharpe={:.2} (deterministic backtest on fixed validation window)",
            epoch + 1, self.hyperparams.epochs, val_sharpe,
        );

        // SP11 Fix 39 (2026-05-04, A1.1): launch the val-sharpe Δ + variance
        // canary producer chain. Rotates `val_sharpe_history_pinned[1]`
        // (current) into `[0]` (previous), writes the freshly-computed
        // `val_sharpe` to `[1]`, then launches the producer kernel which
        // computes `(curr - prev)` + squared-deviation against the prior
        // `ISV[VAL_SHARPE_DELTA_EMA_INDEX]` and chains Pearls A+D into
        // ISV[VAL_SHARPE_DELTA_EMA_INDEX=350, VAL_SHARPE_VAR_EMA_INDEX=351].
        //
        // The host-side write through `host_slice_mut()` is a permitted
        // CPU↔GPU path: it writes to mapped-pinned memory directly (no
        // `htod_copy`), and writes a literal already-computed value (no
        // host-side compute), so it does NOT violate
        // `feedback_no_htod_htoh_only_mapped_pinned` or
        // `feedback_no_cpu_compute_strict`.
        //
        // Layer A is additive — no consumer reads either canary slot in
        // this commit. The downstream controller wires in Layer A2.
        if let Some(ref mut fused) = self.fused_ctx {
            let trainer_mut = fused.trainer_mut();
            {
                let history = trainer_mut.val_sharpe_history_pinned.host_slice_mut();
                // Rotate: prev ← curr, then curr ← new observation.
                history[0] = history[1];
                history[1] = val_sharpe as f32;
            }
            if let Err(e) = trainer_mut.launch_sp11_val_sharpe_delta_compute() {
                tracing::warn!(error = %e,
                    "SP11 val_sharpe_delta_compute launch failed");
            }
        }
        // Source-of-truth alignment: read the EXACT annualization factor the
        // kernel applied (`sqrt(bars_per_day × trading_days_per_year)`) rather
        // than recomputing from `self.hyperparams.bars_per_day` here, which
        // could drift if hyperopt or downstream callers override the
        // evaluator's config between construction and metric emit. The
        // evaluator's getter returns the same f32 stored at init time.
        let val_sharpe_raw = match self.gpu_evaluator.as_ref() {
            Some(ev) => {
                let ann = f64::from(ev.annualization_factor());
                if ann > 1e-10 { val_sharpe / ann } else { 0.0 }
            }
            None => 0.0,
        };
        info!("  val_Sharpe_raw={:.6} (un-annualized per-bar)", val_sharpe_raw);

        // C.3 Plan 3 Task 7: state-distribution KL between train sample and val
        // batch (OFI block). Launches once per validation epoch — must run AFTER
        // `launch_validation_loss` populates the evaluator's `chunked_states_buf`
        // and BEFORE the next training epoch consumes ISV[STATE_KL_AMP=79]. The
        // val state pointer comes from the evaluator's chunked buffer; the train
        // states are the experience-collector's own `states_out` (last collect
        // pass). Both buffers live on the same CUDA context; the launch enqueues
        // on the collector's stream.
        {
            let (val_ptr, n_val) = match self.gpu_evaluator.as_ref() {
                Some(e) => e.val_state_sample(),
                None => (0_u64, 0_usize),
            };
            if val_ptr != 0 && n_val > 0 {
                if let Some(ref mut c) = self.gpu_experience_collector {
                    let n_train = c.alloc_episodes() * c.alloc_timesteps();
                    let state_dim_padded = ml_core::state_layout::STATE_DIM_PADDED;
                    let ofi_begin = ml_core::state_layout::OFI_START;
                    let ofi_end   = ofi_begin + ml_core::state_layout::OFI_DIM;
                    let ema_alpha = 0.05_f32;
                    if let Err(e) = c.launch_state_kl_inplace(
                        val_ptr, n_train, n_val,
                        state_dim_padded, ofi_begin, ofi_end,
                        ema_alpha,
                    ) {
                        tracing::warn!("C.3 state_kl_moment_match launch failed: {e}");
                    }
                }
            }
        }

        // Prometheus metrics
        training_metrics::set_epoch("dqn", "current", (epoch + 1) as f64);
        training_metrics::set_epoch_loss("dqn", "current", avg_loss);
        training_metrics::set_validation_loss("dqn", "current", val_loss);
        if epoch_duration.as_secs_f64() > 0.0 {
            training_metrics::set_batches_per_second(
                "dqn", "current",
                train_step_count as f64 / epoch_duration.as_secs_f64(),
            );
        }
        training_metrics::set_q_value_stats("dqn", "current", q_mean, q_max);
        training_metrics::set_gradient_norm("dqn", "current", avg_grad_norm);
        if let Some(ref fused) = self.fused_ctx {
            let (c51, combined) = fused.grad_norm_diagnostics();
            training_metrics::set_grad_norm_c51("dqn", "current", c51 as f64);
            training_metrics::set_grad_norm_combined("dqn", "current", combined as f64);
        }
        training_metrics::set_epoch_duration("dqn", "current", epoch_duration.as_secs_f64());
        {
            let agent = self.agent.read().await;
            if let Ok(buf_size) = agent.get_replay_buffer_size() {
                training_metrics::set_replay_buffer_size("dqn", "current", buf_size as f64);
            }
        }

        // Adaptive tau per-epoch — REMOVED.
        //
        // Previous logic detected `q_mean - prev_q_mean > 0.005` ("drift") and
        // *doubled* tau (faster target-network tracking), which is the wrong
        // direction for stability: higher tau means the bootstrap target
        // chases the online Q more aggressively, amplifying runaway and
        // causing even-vs-odd-epoch oscillation (observed 2026-04-21).
        //
        // Even if the sign were correct, the override mutated `config.tau`
        // (the *base* of the per-step cosine schedule), so each "adjustment"
        // compounded across epochs. The cosine schedule combined with
        // `apply_health_coupled_tau_floor` now controls target-network speed
        // deterministically; no per-epoch override.
        //
        // Q-growth is still tracked via `prev_epoch_q_mean` in case future
        // diagnostics want it, but nothing mutates tau from this value.
        //
        // PRODUCTION SAFETY: Q-drift kill criterion.
        //
        // A model whose Q-values blow up over a few epochs is catastrophically
        // unsafe for live trading — Kelly cap floats with Q-confidence, so a
        // runaway Q drives oversized positions. Even one trading session with
        // such a model can blow up a strategy. We must never deploy a model
        // that exhibits this dynamic, so we halt training the moment we
        // detect it.
        //
        // Criterion (Plan C Phase 2 follow-up H, 2026-04-29 — replaces the
        // earlier `ratio = |q_mean| / |prev_q_mean|` test): trip on BOTH
        //   (a) `|q_mean|` past an ISV-derived production-unsafe floor, AND
        //   (b) the new `q_mean` sample is a robust-statistics outlier vs
        //       the recent rolling-window q_mean history (median + MAD).
        //
        // Why MAD-deviation replaces the prior ratio test (Goal H):
        // The prior test computed `ratio = |q_mean| / |prev_q_mean|` and
        // tripped on `ratio > 2.0`. Near zero crossings (legitimate cold-
        // start has |prev_q_mean| ≈ 0.01–0.1) the ratio explodes for any
        // non-tiny `q_mean` — smoke-test-n9xzr fired with `prev=−0.0795,
        // curr=0.7647, ratio=9.62×` despite this being natural cold-start
        // growth, not geometric runaway. Median + MAD over a small window
        // are robust to a single outlier (50%-breakdown estimators) and
        // adapt to recent variability rather than the brittle prev-epoch
        // baseline:
        //   * Cold-start (q_mean transitioning from near-zero): each new
        //     sample tracks the median, deviation stays low. No false
        //     positives.
        //   * Genuine geometric runaway (q_mean → 50 from 0.5 in one
        //     epoch): the new sample sits >> MAD from the median;
        //     deviation explodes; kill fires correctly.
        //   * Stable convergence: q_mean stays near median; deviation
        //     stays low; no false positives.
        //
        // Adaptive floor (unchanged from prior commit, Goal F now wires the
        // producers to fire per-step): `kill_floor = 3.0 × max(ISV[16],
        // ISV[21])` with a `max(0.5, …)` cold-start hard-floor. Both ISV
        // slots are EMAs of `max(|Q_mean|)` produced by `q_mag_bin_means_
        // reduce` / `q_dir_bin_means_reduce` (now launched alongside
        // `update_isv_signals` per training step in `fused_training.rs`),
        // so they reflect the model's currently observed healthy Q scale.
        // The `3.0×` multiplier is architectural: empirically RL Q-
        // divergence shows up at roughly 2–4× normal scale on the same
        // trajectory; healthy oscillations stay inside ~1.5×. Per
        // `feedback_isv_for_adaptive_bounds.md` the absolute level comes
        // from runtime ISV; the multiplier is the architectural "drift
        // starts here" factor, not a tuned magnitude. The `0.5` cold-start
        // hard-floor is a numerical-stability bound (Invariant 1 carve-
        // out) sitting safely below the ISV cold-start observed in
        // healthy training (~0.05–0.5 by epoch 3).
        //
        // Window/threshold choices declared as named consts at the top of
        // this method (see `Q_DRIFT_*`); they are architectural bounds, not
        // hand-tuned magnitudes (per `feedback_no_quickfixes.md`).
        //
        // Skip until the window has ≥ Q_DRIFT_WARMUP_SAMPLES priors.
        // Median/MAD on 2 samples gives a degenerate `|range|/2` estimator
        // that trips on any monotonic trajectory (including legitimate
        // cold-start growth from the policy converging on a non-zero Q
        // scale). Three priors is the minimum where MAD genuinely measures
        // central spread rather than the half-range. DO NOT skip on fold-
        // boundary epochs — fold-boundary divergence is precisely the
        // failure mode this criterion is designed to catch.
        //
        // Window size: 5 epochs. Matches the smoke's 5-epochs-per-fold and
        // gives MAD enough samples to be a meaningful estimator without
        // averaging across regime shifts. Reset at fold boundary in
        // `reset_for_fold` alongside `prev_epoch_q_mean` (A.1 pattern).
        //
        // Threshold: 4.0 MADs. For Gaussian noise `1 MAD ≈ 0.6745 σ`, so
        // `4 MAD ≈ 2.7 σ` of Gaussian — a clear outlier without firing on
        // every legitimate dip. We use the literal MAD count (not converted
        // to σ) because q_mean is non-Gaussian during cold-start.
        //
        // MIN_DEVIATION: 0.01. When the window happens to be near-constant
        // (MAD ≈ 0), bound the denominator so a single new sample doesn't
        // trip purely on its absolute value. Pairs with the floor check.
        const Q_DRIFT_WINDOW_SIZE: usize = 5;
        const Q_DRIFT_WARMUP_SAMPLES: usize = 3;
        const Q_DRIFT_DEVIATION_THRESHOLD: f64 = 4.0;
        const Q_DRIFT_MIN_DEVIATION: f64 = 0.01;

        // Read both per-branch |Q| reference EMAs from the pinned/device-
        // mapped ISV bus (zero-copy, mapped-pinned page — see
        // `feedback_no_htod_htoh_only_mapped_pinned.md`). Take max() so
        // either branch's healthy scale anchors the floor; using min() would
        // let an under-active branch suppress drift on the other.
        let (q_abs_ref_mag, q_abs_ref_dir) = if let Some(ref fused) = self.fused_ctx {
            let mag = fused.trainer().read_isv_signal_at(Q_ABS_REF_INDEX).max(0.0);
            let dir = fused.trainer().read_isv_signal_at(Q_DIR_ABS_REF_INDEX).max(0.0);
            (mag, dir)
        } else {
            (0.0_f32, 0.0_f32)
        };
        let q_abs_ref_max = q_abs_ref_mag.max(q_abs_ref_dir);
        let kill_floor = (3.0_f64 * q_abs_ref_max as f64).max(0.5);

        // Need at least Q_DRIFT_WARMUP_SAMPLES prior entries before the
        // deviation test is well-defined (smaller window degenerates to
        // half-range MAD that trips on monotonic trajectories). Push the
        // new sample after the test so the criterion measures the new
        // sample against the window of priors, not against itself.
        if self.q_mean_window.len() >= Q_DRIFT_WARMUP_SAMPLES {
            // Robust statistics: median + MAD on the prior window.
            let mut sorted: Vec<f64> = self.q_mean_window.iter().copied().collect();
            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
            let n = sorted.len();
            let median = if n % 2 == 1 {
                sorted[n / 2]
            } else {
                0.5 * (sorted[n / 2 - 1] + sorted[n / 2])
            };
            let mut abs_dev: Vec<f64> = sorted.iter().map(|x| (x - median).abs()).collect();
            abs_dev.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
            let mad = if n % 2 == 1 {
                abs_dev[n / 2]
            } else {
                0.5 * (abs_dev[n / 2 - 1] + abs_dev[n / 2])
            };

            let curr_abs = q_mean.abs();
            let denom = mad.max(Q_DRIFT_MIN_DEVIATION);
            let q_deviation = (q_mean - median).abs() / denom;
            if curr_abs > kill_floor && q_deviation > Q_DRIFT_DEVIATION_THRESHOLD {
                let win_str: Vec<String> = self.q_mean_window.iter()
                    .map(|x| format!("{:.4}", x))
                    .collect();
                return Err(anyhow::anyhow!(
                    "Q-drift kill criterion triggered at epoch {}: \
                     q_mean={:.4} deviates {:.2} MADs from rolling median {:.4} \
                     (window=[{}], MAD={:.4}, threshold={:.1}); \
                     adaptive floor {:.4} = max(0.5, 3× max(ISV[16]={:.4}, ISV[21]={:.4})) \
                     and |q_mean|={:.4} exceeds it. \
                     A model with this dynamic is unsafe for production trading \
                     (Kelly cap → oversized positions). Halting training; \
                     model is rejected from deployment.",
                    epoch + 1, q_mean, q_deviation, median, win_str.join(", "),
                    mad, Q_DRIFT_DEVIATION_THRESHOLD,
                    kill_floor, q_abs_ref_mag, q_abs_ref_dir, curr_abs,
                ));
            }
        }

        // Append after the test so the new sample joins the window for
        // subsequent epochs; pop oldest when over capacity.
        self.q_mean_window.push_back(q_mean);
        if self.q_mean_window.len() > Q_DRIFT_WINDOW_SIZE {
            self.q_mean_window.pop_front();
        }

        // Plan C Phase 2 follow-up A.2 (2026-04-29): adaptive Polyak-tau
        // drift signal producer. Computes
        //   drift_rate = |q_mean(t) - q_mean(t-1)| / max(|q_mean(t-1)|,
        //                ISV[Q_ABS_REF] + ISV[Q_DIR_ABS_REF], 1e-6)
        // clipped to [0, 4] and writes ISV[Q_DRIFT_RATE_INDEX=129]. The
        // next epoch's `launch_tau_update` (called at the top of the next
        // iteration of this loop) reads the slot and multiplies the
        // cosine-scheduled tau_base by `1/(1+ISV[129])` so the effective
        // Polyak rate dampens monotonically as q-drift rises.
        //
        // Gated on `prev_epoch_q_mean.abs() > 1e-6` — same criterion the
        // kill-check above uses to skip the first epoch (no meaningful
        // delta). Cold-start ISV[129]=0.0 (constructor + FoldReset), so a
        // skipped launch keeps the dampening factor at 1.0 (no-op) until a
        // real prev value exists. Per `feedback_no_partial_refactor.md`:
        // every consumer of the new contract migrates in this commit.
        if self.prev_epoch_q_mean.abs() > 1e-6 {
            if let Some(ref fused) = self.fused_ctx {
                let q_curr_f = q_mean as f32;
                let q_prev_f = self.prev_epoch_q_mean as f32;
                if let Err(e) = fused.trainer().launch_q_drift_rate_ema(q_curr_f, q_prev_f) {
                    tracing::warn!(epoch, "launch_q_drift_rate_ema failed (non-fatal): {e}");
                }
            }
        }

        self.prev_epoch_q_mean = q_mean;

        // Action diversity — per-branch from GPU monitoring (not re-derived through OrderRouter)
        let (b0, b1, b2, b3) = {
            let ag = self.agent.read().await;
            ag.branch_sizes()
        };
        let total_factored_space = b0.max(1) * b1.max(1) * b2.max(1) * b3.max(1);

        let epoch_total: usize = monitor.action_counts.iter().sum();
        // Diversity threshold: 1% of DIRECTIONAL actions (Short + Long only).
        // With Flat/Hold-dominant policies, using total inflates the threshold and
        // mechanically kills diversity when non-trading > 80%. The metric should
        // measure magnitude diversity WITHIN directional (tradable) positions,
        // not overall share.
        //
        // 12-bin layout: dir(4)×mag(3) with dir ∈ {Short=0, Hold=1, Long=2, Flat=3}
        // and exp_idx = dir*3 + mag. Non-directional cells:
        //   Hold = indices 3,4,5  (mag forced to 0 by kernel → only index 3 populated)
        //   Flat = indices 9,10,11 (mag forced to 0 → only index 9 populated)
        // Both buckets are excluded from the directional denominator and given the
        // "any actions" pass-through rule below.
        let flat_count: usize = monitor.action_counts[9..12].iter().sum();
        let hold_count: usize = monitor.action_counts[3..6].iter().sum();
        let directional_total = epoch_total.saturating_sub(flat_count + hold_count).max(1);
        let active_threshold = (directional_total as f64 * 0.01).max(1.0);
        // action_counts[12]: dir×mag combos. Hold (3..6) and Flat (9..12) cells always
        // count if non-zero — they're rare by the forced-mag=0 kernel convention, so
        // any presence is meaningful.
        let active_dirmag = monitor.action_counts.iter().enumerate()
            .filter(|&(i, &c)| {
                if (3..6).contains(&i) || (9..12).contains(&i) {
                    c > 0 // Hold / Flat cells: count if any actions
                } else {
                    c as f64 >= active_threshold // Directional cells: threshold
                }
            }).count();
        let active_ord = monitor.order_type_counts.iter()
            .filter(|&&c| c as f64 >= active_threshold).count();
        let active_urg = monitor.urgency_counts.iter()
            .filter(|&&c| c as f64 >= active_threshold).count();
        let active_factored = active_dirmag * active_ord * active_urg;
        let diversity_pct = (active_factored as f64 / total_factored_space as f64) * 100.0;

        // Max dir*mag = b0 * b1 = 12 for the 4-branch DQN (dir=4, mag=3).
        // Epsilon exploration can hit any combo. Hold/Flat collapse their
        // magnitude slots to mag=0 in the kernel, so the effective reachable
        // set is 8 unique cells (Short×{S,H,F}, Long×{S,H,F}, Hold, Flat).
        let max_dirmag = b0 * b1;
        info!(
            "Epoch {}/{}: Action diversity={}/{} ({:.1}%) — dir*mag={}/{} order={}/{} urgency={}/{}",
            epoch + 1, self.hyperparams.epochs,
            active_factored, total_factored_space, diversity_pct,
            active_dirmag, max_dirmag, active_ord, b2, active_urg, b3,
        );

        // Epoch entropy
        // Entropy over dir*mag distribution (normalized to [0,1] where 1.0 = uniform)
        let epoch_entropy = if epoch_total > 0 {
            let mut h = 0.0_f64;
            for &count in monitor.action_counts.iter() {
                if count > 0 {
                    let p = count as f64 / epoch_total as f64;
                    h -= p * p.ln();
                }
            }
            h / ((b0 * b1) as f64).ln() // normalize by log(b0*b1) = log(12) for dir*mag combos
        } else {
            0.0
        };

        info!(
            "  Exploration: entropy={:.3} (1.0=uniform), epsilon={:.4}, noisy_nets={}, count_bonus={}",
            epoch_entropy,
            self.get_epsilon().await.unwrap_or(0.0),
            true,
            self.hyperparams.count_bonus_coefficient.unwrap_or(0.0) > 0.0,
        );

        training_metrics::set_action_entropy("dqn", "current", epoch_entropy);
        training_metrics::set_action_diversity("dqn", "current", diversity_pct / 100.0);

        if active_dirmag < 2 {
            warn!(
                "LOW ACTION DIVERSITY: {}/{} dir*mag combos, entropy={:.3}",
                active_dirmag, b0 * b1, epoch_entropy,
            );
        }

        // Episode stats
        let (mean_len, std_len, min_len, max_len, exit_counts) = monitor.get_episode_stats();
        if !monitor.episode_lengths.is_empty() {
            let total_episodes = monitor.episode_lengths.len();

            info!(
                "WAVE P2 Episode Stats [Epoch {}]: {} episodes, length: mean={:.1}+/-{:.1}, min={}, max={}",
                epoch + 1, total_episodes, mean_len, std_len, min_len, max_len
            );

            info!(
                "  Exit breakdown: profit={}({:.1}%), stop={}({:.1}%), time={}({:.1}%), boundary={}({:.1}%)",
                exit_counts[0], (exit_counts[0] as f64 / total_episodes as f64) * 100.0,
                exit_counts[1], (exit_counts[1] as f64 / total_episodes as f64) * 100.0,
                exit_counts[2], (exit_counts[2] as f64 / total_episodes as f64) * 100.0,
                exit_counts[3], (exit_counts[3] as f64 / total_episodes as f64) * 100.0,
            );
        }

        // VaR/CVaR from GPU trade stats step_returns
        if trade_stats.step_returns.len() > 20 {
            let returns = &trade_stats.step_returns;

            let (var_95, cvar_95) = calculate_var_cvar(returns, 0.05);
            let (var_99, cvar_99) = calculate_var_cvar(returns, 0.01);

            info!(
                "Epoch {}/{}: Risk Metrics - VaR(95%)={:.4}%, CVaR(95%)={:.4}%, VaR(99%)={:.4}%, CVaR(99%)={:.4}% (from {} step returns)",
                epoch + 1, self.hyperparams.epochs,
                var_95 * 100.0, cvar_95 * 100.0,
                var_99 * 100.0, cvar_99 * 100.0,
                returns.len()
            );
        }

        // Financial metrics from GPU trade stats
        let epoch_max_dd;
        let epoch_sharpe = {
            let financials = compute_epoch_financials(
                &trade_stats,
                &monitor.action_counts,
                100_000.0,
                f64::from(self.hyperparams.bars_per_day),
            );
            training_metrics::set_epoch_financial_metrics(
                "dqn", "current",
                financials.sharpe, financials.sortino, financials.win_rate,
                financials.max_drawdown, financials.profit_factor,
                financials.total_return, financials.avg_return,
                financials.total_trades as f64,
            );
            training_metrics::set_epoch_action_distribution(
                "dqn", "current",
                financials.buy_pct, financials.sell_pct, financials.hold_pct,
            );
            // `total_return` is log-space cumulative growth across every
            // per-bar step_return (financials.rs:80-94). With ~4M step
            // returns in a fold-convergence run, even sub-bps positive
            // bars compound to absurd magnitudes (observed: 1.93e37%).
            // Math is correct; display needs scientific notation to render
            // compactly without hiding the magnitude.
            info!(
                "Epoch {}/{}: Sharpe={:.2} Sharpe_raw={:.6} WinRate={:.1}% MaxDD={:.3}% PF={:.2} Return={:+.3e}% Trades={}",
                epoch + 1, self.hyperparams.epochs,
                financials.sharpe, financials.sharpe_raw,
                financials.win_rate * 100.0,
                financials.max_drawdown * 100.0, financials.profit_factor,
                financials.total_return * 100.0, financials.total_trades,
            );
            // QuestDB epoch row
            {
                let bps = if epoch_duration.as_secs_f64() > 0.0 {
                    train_step_count as f64 / epoch_duration.as_secs_f64()
                } else {
                    0.0
                };
                let buf_size = {
                    let agent = self.agent.read().await;
                    agent.get_replay_buffer_size().unwrap_or(0) as f64
                };
                let run_id = std::env::var("HOSTNAME").unwrap_or_else(|_| "local".to_owned());
                questdb_sink::record_training_epoch(&questdb_sink::EpochRecord {
                    model: "dqn",
                    fold: "current",
                    run_id: &run_id,
                    symbol: "",
                    epoch: (epoch + 1) as u32,
                    loss: avg_loss,
                    val_loss,
                    batches_per_second: bps,
                    epoch_duration_secs: epoch_duration.as_secs_f64(),
                    q_mean,
                    q_max,
                    gradient_norm: avg_grad_norm,
                    learning_rate: current_lr,
                    replay_buffer_size: buf_size,
                    action_entropy: epoch_entropy,
                    action_diversity: diversity_pct / 100.0,
                    sharpe: financials.sharpe,
                    sortino: financials.sortino,
                    win_rate: financials.win_rate,
                    max_drawdown: financials.max_drawdown,
                    profit_factor: financials.profit_factor,
                    total_return: financials.total_return,
                    avg_return: financials.avg_return,
                    total_trades: financials.total_trades as f64,
                });
            }

            epoch_max_dd = financials.max_drawdown;
            // D7/N7: carry win_rate into next epoch's process_epoch_boundary.
            self.last_epoch_win_rate = financials.win_rate as f32;

            // SP5 Layer D Task D4 (2026-05-02): atomic D1 wiring. After
            // `compute_epoch_financials` produces its host scalar (which still
            // drives the QuestDB/Prometheus/HEALTH_DIAG host-side paths),
            // launch `pnl_aggregation_kernel` so ISV[286..290) (PNL_TOTAL /
            // MEAN / VAR / MAX_DD) are populated as a parallel GPU producer
            // per `feedback_no_cpu_compute_strict.md`. Per the D1-rewrite
            // commit (`66f7e64d1`) the kernel consumes per-bar
            // `step_returns + done_flags` plus already-reduced per-trade
            // scalars — exact match for `compute_epoch_financials`'s inputs.
            //
            // The host-side aggregation walk in `financials.rs` STAYS — D1
            // documents itself as a parallel GPU producer that publishes to
            // ISV; the host scalar continues to be computed for non-ISV
            // consumers. Per `feedback_no_partial_refactor.md` the host walk
            // is explicitly out-of-scope for D4 (a follow-up "D5" task can
            // collapse it once consumers are migrated to ISV reads).
            //
            // Per `feedback_no_htod_htoh_only_mapped_pinned.md`, allocate
            // mapped-pinned f32 buffers and write via `host_slice_mut` (a
            // host-to-host write within the mapped pinned page — the kernel
            // sees the data via `dev_ptr` after a stream-fence boundary).
            // This is a once-per-epoch cold-path allocation; matches the
            // existing pattern at `training_loop.rs:1318` for targets/features.
            if let Some(ref fused) = self.fused_ctx {
                let n_bars = trade_stats.step_returns.len();
                if n_bars > 0
                    && trade_stats.done_flags.len() == n_bars
                {
                    let alloc_returns = unsafe {
                        crate::cuda_pipeline::mapped_pinned::MappedF32Buffer::new(n_bars)
                    };
                    let alloc_dones = unsafe {
                        crate::cuda_pipeline::mapped_pinned::MappedF32Buffer::new(n_bars)
                    };
                    match (alloc_returns, alloc_dones) {
                        (Ok(mut returns_buf), Ok(mut dones_buf)) => {
                            // f64 → f32 cast (matches the f32 host buffer in
                            // `gpu_experience_collector::collect_trade_stats`
                            // before the lossy `as f64` upcast at line 3008/3020).
                            {
                                let dst = returns_buf.host_slice_mut();
                                for (i, &r) in trade_stats.step_returns.iter().enumerate() {
                                    dst[i] = r as f32;
                                }
                            }
                            {
                                let dst = dones_buf.host_slice_mut();
                                for (i, &d) in trade_stats.done_flags.iter().enumerate() {
                                    dst[i] = d as f32;
                                }
                            }
                            let n_trades_i32 = trade_stats.total_trades as i32;
                            if let Err(e) = fused.trainer().launch_sp5_pnl_aggregation(
                                &returns_buf,
                                &dones_buf,
                                n_bars as i32,
                                n_trades_i32,
                                trade_stats.sum_returns as f32,
                                trade_stats.sum_sq_returns as f32,
                                100_000.0_f32,
                            ) {
                                tracing::warn!(error = %e,
                                    "SP5 D4 pnl_aggregation launch failed");
                            }
                        }
                        (Err(e), _) | (_, Err(e)) => {
                            tracing::warn!(error = %e,
                                "SP5 D4 pnl_aggregation MappedF32Buffer alloc failed");
                        }
                    }
                }
            }

            financials.sharpe
        };

        // ── SP5 Layer D Task D4 (2026-05-02): atomic D3 wiring ──
        // Replaces two host-side EMA arithmetic blocks (max_dd_ema +
        // low_dd_ratio at the prior 5043-5070 site, training_sharpe_ema at
        // the prior 5091-5099 site) with a single GPU kernel launch per
        // `feedback_no_cpu_compute_strict.md` and `feedback_no_partial_refactor.md`.
        //
        // The kernel reproduces all three EMA recurrences bit-for-bit:
        //   tid=0 training_sharpe_ema  α = clamp(0.05 + 0.3·|err|, 0, 0.5);
        //                              `_initialized=false` ⇒ replace.
        //   tid=1 max_dd_ema           α = 0.1; `prev<1e-12` ⇒ replace.
        //   tid=2 low_dd_ratio         α = 0.15 over binary `is_low`,
        //                              reading tid=1's *new* max_dd_ema via
        //                              `__syncthreads()` (matches the host's
        //                              read-after-write at the prior 5050).
        //
        // After the launcher completes its Pearls A+D smoothing chain into
        // ISV[294..297), we sync the stream and read the smoothed values
        // back into the host scalars so the rest of the codebase
        // (HEALTH_DIAG emit at line ~3873, smoke tests at
        // `td_propagation.rs:126/135/155` + `generalization.rs:102/103`,
        // adaptive-DSR aux-weight controller at line ~3746, registry log
        // emit at line ~6491) continues to read from the same field
        // names — they now hold the ISV-Pearl-smoothed values instead of
        // the raw host EMAs. Behavior is preserved within float precision
        // modulo the additional Pearl A+D smoothing layer (a deliberate
        // SP5 architectural change documented in
        // `pearl_first_observation_bootstrap.md` and
        // `pearl_wiener_optimal_adaptive_alpha.md`).
        //
        // Per the SP5 plan §D Task D4 brief, the host-side warmup wrapper
        // and `[0.2, 0.95]` clamp inside `LearningHealth::update`
        // (learning_health.rs:114-124) stay — D2 already publishes the
        // composed health to ISV[290..294) as a parallel producer; the
        // host scalar continues to drive non-ISV consumers (PPO, eval).
        // Likewise `HealthEmaTrackers::update` (metrics.rs:23-25) stays —
        // it produces D2's inputs and would need a 4th kernel ("D5") to
        // eliminate. Out of D4 scope per the dispatch brief.
        if let Some(ref fused) = self.fused_ctx {
            let prev_sharpe = self.training_sharpe_ema;
            let prev_max_dd = self.max_dd_ema as f32;
            let prev_low_dd_ratio = self.low_dd_ratio as f32;
            let sharpe_initialized = self.training_sharpe_ema_initialized;
            if let Err(e) = fused.trainer().launch_training_metrics_ema(
                epoch_sharpe as f32,
                epoch_max_dd as f32,
                prev_sharpe,
                prev_max_dd,
                prev_low_dd_ratio,
                sharpe_initialized,
            ) {
                tracing::warn!(error = %e,
                    "SP5 D4 training_metrics_ema launch failed");
            } else {
                // Sync stream so the post-Pearls ISV[294..297) writes are
                // visible to host reads via mapped pinned `isv_signals_pinned`.
                // Cold-path (once per epoch); matches the existing
                // `per_branch_q_gap_ema()` sync pattern (gpu_dqn_trainer.rs:18475).
                fused.trainer().synchronize_isv_stream();
                use crate::cuda_pipeline::sp5_isv_slots::{
                    TRAINING_SHARPE_EMA_INDEX, MAX_DD_EMA_INDEX, LOW_DD_RATIO_INDEX,
                };
                let trainer = fused.trainer();
                self.training_sharpe_ema =
                    trainer.read_isv_signal_at(TRAINING_SHARPE_EMA_INDEX);
                self.max_dd_ema =
                    trainer.read_isv_signal_at(MAX_DD_EMA_INDEX) as f64;
                self.low_dd_ratio =
                    trainer.read_isv_signal_at(LOW_DD_RATIO_INDEX) as f64;
                // Set the `_initialized` flag once the first non-zero
                // observation has been processed (Pearl-A's first-
                // observation replacement gates this exactly the same way
                // the host sentinel did — the smoke-test assertion in
                // `generalization.rs:103` continues to pass after the
                // first complete training run).
                self.training_sharpe_ema_initialized = true;
            }
        }

        // ── Adversarial regime: fully adaptive with hysteresis ──
        // Activates when 60%+ of recent epochs had dd below half the EMA.
        // Deactivates when ratio drops below 40%. No fixed counters.
        // Reads `self.low_dd_ratio` populated by the D3 kernel above.
        {
            let max_dd = epoch_max_dd;
            let thresh = self.max_dd_ema * 0.5;

            if !self.adversarial_active && self.low_dd_ratio > 0.6 {
                self.adversarial_active = true;
                info!(
                    low_dd_ratio = %format!("{:.2}", self.low_dd_ratio),
                    max_dd = %format!("{:.4}%", max_dd * 100.0),
                    thresh = %format!("{:.4}%", thresh * 100.0),
                    "ADVERSARIAL regime ACTIVATED — sustained low drawdown"
                );
            } else if self.adversarial_active && self.low_dd_ratio < 0.4 {
                self.adversarial_active = false;
                info!(
                    low_dd_ratio = %format!("{:.2}", self.low_dd_ratio),
                    max_dd = %format!("{:.4}%", max_dd * 100.0),
                    "ADVERSARIAL regime deactivated — drawdown recovered"
                );
            }
        }

        // #20 Lottery ticket pruning: compute mask at pruning epoch
        if let Some(ref mut fused) = self.fused_ctx {
            fused.compute_pruning_mask(epoch)
                .map_err(|e| anyhow::anyhow!("Pruning mask compute: {e}"))?;
        }

        // #33 Adversarial self-play: update saboteur with trader's epoch Sharpe
        self.saboteur.update(epoch, epoch_sharpe as f32);

        // Track metrics for early stopping
        self.loss_history.push(avg_loss);
        self.q_value_history.push(avg_q_value);
        self.val_loss_history.push(val_loss);
        self.sharpe_history.push(epoch_sharpe);

        // ISV audit 2026-04-23: broadcast `self.training_sharpe_ema` (now
        // sourced from ISV[294] via the D3 kernel above) into ISV slot 22
        // (`SHARPE_EMA_INDEX`). Consumed on-GPU by `isv_signal_update` to
        // drive slot 12 (`learning_health`) via sigmoid(0.1 × sharpe_ema)
        // EMA. Many other GPU consumers also read ISV[22] (state_kl_divergence,
        // q_drift, target_drift, mamba2_retention, etc. — see
        // `gpu_experience_collector.rs::SHARPE_EMA_INDEX` callers); the
        // broadcast keeps the existing slot[22] contract intact while D3
        // adds slot[294] as the primary GPU-EMA producer.
        if let Some(ref fused) = self.fused_ctx {
            fused.write_isv_signal_at(
                crate::cuda_pipeline::gpu_dqn_trainer::SHARPE_EMA_INDEX,
                self.training_sharpe_ema,
            );
        }

        // Limit history vectors to prevent unbounded growth
        const MAX_HISTORY_LEN: usize = 100;
        if self.loss_history.len() > MAX_HISTORY_LEN {
            self.loss_history.drain(0..50);
        }
        if self.q_value_history.len() > MAX_HISTORY_LEN {
            self.q_value_history.drain(0..50);
        }
        if self.val_loss_history.len() > MAX_HISTORY_LEN {
            self.val_loss_history.drain(0..50);
        }
        if self.sharpe_history.len() > MAX_HISTORY_LEN {
            self.sharpe_history.drain(0..50);
        }

        Ok(EpochLogOutput {
            avg_loss,
            avg_q_value,
            avg_grad_norm,
            epoch_sharpe,
            val_loss,
            q_min,
            q_max,
            q_mean,
        })
    }

    // ═══════════════════════════════════════════════════════════════════════
    // Helper: Checkpoint saving + early stopping checks
    // Returns Err for early stopping (caller propagates), Ok(()) to continue
    // ═══════════════════════════════════════════════════════════════════════

    pub(crate) async fn handle_epoch_checkpoints_and_early_stopping(
        &mut self,
        epoch: usize,
        train_step_count: usize,
        log_output: &EpochLogOutput,
        checkpoint_callback: &super::CheckpointCallbackHandle,
    ) -> Result<()>
    {
        // C4 FIX: Save best model checkpoint when Sharpe improves
        if train_step_count > 0 && log_output.epoch_sharpe > self.best_sharpe {
            self.best_sharpe = log_output.epoch_sharpe;
            self.best_val_loss = log_output.val_loss;
            self.best_epoch = epoch + 1;

            info!(
                "New best Sharpe: {:.4} at epoch {} (val_loss={:.6})",
                log_output.epoch_sharpe, epoch + 1, log_output.val_loss,
            );

            // GPU-native best model snapshot: async DtoD copy of params_flat.
            // Used by the evaluator to read the best model's weights directly
            // from GPU memory — no Candle VarMap, no safetensors roundtrip.
            // Synchronous DtoD — fast, source of truth for restore_best_gpu_params.
            match self.save_best_gpu_params() {
                Ok(()) => tracing::info!("GPU best-model snapshot saved (epoch {})", epoch + 1),
                Err(e) => tracing::warn!("Failed to save GPU best params snapshot (non-fatal): {e}"),
            }

            // Snapshot weights into a mapped-pinned host buffer (DtoD-into-mapped
            // + single stream sync — see `snapshot_model_to_pinned`). Output is
            // a `Send + 'static` byte buffer + per-tensor metadata, ready for
            // off-thread safetensors construction.
            let snap = self.snapshot_model_to_pinned().await
                .context("Failed to snapshot model to mapped-pinned buffer")?;

            // Verify snapshot integrity. The bytes are now Vec<u8> on the host;
            // safetensors construction happens off the training-loop thread.
            if self.safety_level != crate::safety::SafetyLevel::Permissive
                && snap.host_bytes.is_empty()
            {
                let msg = "SAFETY: Snapshot empty — best-checkpoint save aborted";
                match self.safety_level {
                    crate::safety::SafetyLevel::Strict => {
                        return Err(anyhow::anyhow!("{}", msg));
                    },
                    crate::safety::SafetyLevel::Normal => {
                        debug!("{} (continuing anyway)", msg);
                    },
                    _ => {},
                }
            }

            // Off-thread serialize + checkpoint_callback. spawn_blocking owns
            // an `Arc::clone` of the callback handle; the std::sync::Mutex
            // serialises any concurrent callsite (cold-path periodic /
            // early-stop callsites in the same training run lock the same
            // handle inline). The JoinHandle is stored on `self` so training
            // end can await it before returning — guarantees the final best
            // ckpt is fully written to disk/MinIO before the caller proceeds.
            let cb_clone = std::sync::Arc::clone(checkpoint_callback);
            let saved_epoch = epoch + 1;
            let snap_size_est = snap.host_bytes.len() as f64;
            let ckpt_start = std::time::Instant::now();
            let join = tokio::task::spawn_blocking(move || -> Result<()> {
                let bytes = super::DQNTrainer::serialize_snapshot_bytes(&snap)
                    .context("Worker: serialize_snapshot_bytes failed")?;
                let ckpt_size = bytes.len() as f64;
                let path = cb_clone.lock()
                    .map_err(|e| anyhow::anyhow!(
                        "Worker: checkpoint_callback mutex poisoned: {e}"
                    ))?
                    (saved_epoch, bytes, true)
                    .context("Worker: checkpoint_callback failed")?;
                training_metrics::record_checkpoint_save(
                    "dqn", "current",
                    ckpt_start.elapsed().as_secs_f64(),
                    ckpt_size,
                );
                info!(
                    "Best model saved to: {} (off-thread, {:.0} bytes)",
                    path, ckpt_size,
                );
                Ok(())
            });
            // Park the JoinHandle on `self`. Awaited by the training-loop
            // owner at exit (success + early-stop branches) to guarantee
            // disk/MinIO write completion before training returns.
            self.pending_checkpoint_handles.push(join);
            debug!(
                "Best-checkpoint dispatched off-thread (epoch {}, snap {:.0} bytes)",
                saved_epoch, snap_size_est,
            );
        }

        // Early stopping checks (skip if no training occurred)
        if train_step_count > 0 {
            let old_should_stop = self.check_early_stopping(log_output.avg_q_value, epoch);
            let patience_should_stop = if self.hyperparams.early_stopping_enabled
                && epoch + 1 >= self.hyperparams.min_epochs_before_stopping {
                self.early_stopping.should_stop(-log_output.epoch_sharpe, epoch)
            } else {
                false
            };

            if let Some(stop_reason) = old_should_stop {
                warn!(
                    "Early stopping triggered at epoch {}/{}: {}",
                    epoch + 1, self.hyperparams.epochs, stop_reason
                );
                info!(
                    "Final metrics: loss={:.6}, Q-value={:.4}",
                    log_output.avg_loss, log_output.avg_q_value
                );

                // Drain any in-flight best-checkpoint workers before
                // serialising the early-stop checkpoint — keeps disk
                // ordering deterministic (best ckpts written first).
                self.await_pending_checkpoint_handles().await;

                let checkpoint_data = self.serialize_model().await
                    .context("Failed to serialize model for early stopping checkpoint")?;
                let checkpoint_size = checkpoint_data.len();
                let checkpoint_path = checkpoint_callback.lock().expect(
                    "checkpoint_callback poisoned — worker thread panicked",
                )(epoch + 1, checkpoint_data, false)
                    .context("Failed to save early stopping checkpoint")?;
                info!(
                    "Early stopping checkpoint saved to: {} ({} bytes)",
                    checkpoint_path, checkpoint_size
                );

                return Err(anyhow::anyhow!(
                    "Training terminated by early stopping at epoch {}/{}: {}",
                    epoch + 1, self.hyperparams.epochs, stop_reason
                ));
            }

            if patience_should_stop {
                warn!(
                    "WAVE 24 Early stopping (patience) triggered at epoch {}/{}! No improvement for {} epochs",
                    epoch + 1, self.hyperparams.epochs, self.hyperparams.gradient_collapse_patience
                );
                info!("Best Sharpe: {:.4} at epoch {} (best val_loss proxy: {:.6})",
                    self.best_sharpe, self.best_epoch, self.early_stopping.get_best_val_loss());

                self.await_pending_checkpoint_handles().await;
                let checkpoint_data = self.serialize_model().await?;
                let checkpoint_path = checkpoint_callback.lock().expect(
                    "checkpoint_callback poisoned — worker thread panicked",
                )(epoch + 1, checkpoint_data, false)?;
                info!("Patience-based early stopping checkpoint saved to: {}", checkpoint_path);

                return Err(anyhow::anyhow!("Training terminated by patience-based early stopping at epoch {}", epoch + 1));
            }
        }

        Ok(())
    }

    // ═══════════════════════════════════════════════════════════════════════
    // Task 10: Trajectory Backtracking helpers
    // ═══════════════════════════════════════════════════════════════════════

    /// Detect whether Q-gap is frozen (all branches have near-zero velocity).
    fn is_q_gap_frozen(&self) -> bool {
        if let Some(ref fused) = self.fused_ctx {
            let emas = fused.per_branch_q_gap_ema();
            let current = fused.get_per_branch_q_gaps();
            let threshold = 0.001_f32;
            current.iter().zip(emas.iter()).all(|(gap, ema)| (gap - ema).abs() < threshold)
        } else {
            false
        }
    }

    /// Save a trajectory checkpoint when improvement is detected.
    fn save_backtracking_checkpoint(&mut self, epoch: usize, improvement_rate: f32, val_sharpe: f32) -> anyhow::Result<()> {
        if let Some(ref fused) = self.fused_ctx {
            // DtoD copy — checkpoint stays on GPU, zero CPU involvement
            let params_gpu = fused.clone_params_gpu()?;
            let target_params_gpu = fused.clone_target_params_gpu()?;

            let checkpoint = super::TrajectoryCheckpoint {
                epoch,
                val_sharpe,
                improvement_rate,
                params_gpu,
                target_params_gpu,
                adam_step: fused.adam_step_val(),
                eval_q_mean_ema: fused.eval_q_mean_ema(),
                eval_q_std_ema: fused.eval_q_std_ema(),
                per_branch_q_gap_ema: fused.per_branch_q_gap_ema(),
            };

            // Sort by val_Sharpe (highest first), keep top-3.
            // Rewind should go to the BEST state, not the fastest-improving.
            // Epoch 0 (improvement=17.3, val_Sharpe=17) is useless as a rewind target.
            // Epoch 20 (improvement=0.4, val_Sharpe=45) is the BEST rewind target.
            self.backtracking.checkpoints.push(checkpoint);
            self.backtracking.checkpoints.sort_by(|a, b|
                b.val_sharpe.partial_cmp(&a.val_sharpe).unwrap_or(std::cmp::Ordering::Equal)
            );
            if self.backtracking.checkpoints.len() > self.backtracking.max_checkpoints {
                self.backtracking.checkpoints.pop();
            }

            info!(epoch, improvement_rate, checkpoints = self.backtracking.checkpoints.len(),
                "Backtracking checkpoint saved (val_Sharpe={val_sharpe:.2})");
        }
        Ok(())
    }

    /// Execute a rewind to the best checkpoint with the current perturbation strategy.
    fn execute_rewind(&mut self, epoch: usize) -> anyhow::Result<()> {
        let checkpoint_idx = self.backtracking.rewind_count.min(self.backtracking.checkpoints.len() - 1);
        let strategy = self.backtracking.current_strategy();

        // Log before borrowing checkpoint (strategy is Copy)
        let ckpt_epoch = self.backtracking.checkpoints[checkpoint_idx].epoch;
        info!(
            epoch,
            rewind_to = ckpt_epoch,
            strategy = ?strategy,
            rewind_count = self.backtracking.rewind_count,
            route_idx = self.backtracking.route_idx,
            "TRAJECTORY BACKTRACK: rewinding to epoch {} with {:?}",
            ckpt_epoch, strategy
        );

        if let Some(ref mut fused) = self.fused_ctx {
            // DtoD restore from GPU checkpoint — zero CPU involvement
            fused.restore_params_from_gpu(&self.backtracking.checkpoints[checkpoint_idx].params_gpu)?;
            fused.restore_target_params_from_gpu(&self.backtracking.checkpoints[checkpoint_idx].target_params_gpu)?;

            // Restore EMA state
            let ema_mean = self.backtracking.checkpoints[checkpoint_idx].eval_q_mean_ema;
            let ema_std = self.backtracking.checkpoints[checkpoint_idx].eval_q_std_ema;
            let ema_gaps = self.backtracking.checkpoints[checkpoint_idx].per_branch_q_gap_ema;
            fused.set_eval_q_mean_ema(ema_mean);
            fused.set_eval_q_std_ema(ema_std);
            fused.set_per_branch_q_gap_ema(ema_gaps);

            // Apply perturbation strategy
            match strategy {
                super::PerturbationStrategy::ResetAdam => {
                    fused.reset_adam_state()?;
                    info!("Perturbation: Adam momentum reset (m=0, v=0)");
                }
                super::PerturbationStrategy::ShrinkPerturbBranches => {
                    fused.shrink_and_perturb(0.9, 0.1)?;
                    info!("Perturbation: shrink-perturb (alpha=0.9, sigma=0.1)");
                }
                super::PerturbationStrategy::TemperatureBoost => {
                    self.backtracking.temp_boost_remaining = 5;
                    info!("Perturbation: temperature boost (2x for 5 epochs)");
                }
                super::PerturbationStrategy::LearningRateBoost => {
                    self.backtracking.original_lr = self.hyperparams.learning_rate;
                    self.hyperparams.learning_rate *= 2.0;
                    self.backtracking.lr_boost_remaining = 10;
                    info!(lr = self.hyperparams.learning_rate, "Perturbation: LR boost (2x for 10 epochs)");
                }
            }
        }

        self.backtracking.last_rewind_epoch = epoch;
        self.backtracking.route_active = true;
        self.backtracking.route_start_epoch = epoch;
        self.backtracking.route_start_sharpe = self.backtracking.best_sharpe;
        self.backtracking.plateau_epochs = 0;
        // Reset cosine LR cycle so the rewound model starts at full LR, not mid-decay.
        self.backtracking.cosine_epoch_offset = epoch;

        Ok(())
    }

    /// Run backtracking logic at the end of each epoch.
    /// Returns `true` if PLATEAU_EXHAUSTED — caller should break the epoch loop.
    fn run_backtracking_epoch_end(&mut self, epoch: usize, val_sharpe: f64) -> anyhow::Result<bool> {
        let val_sharpe_f32 = val_sharpe as f32;

        // 1. Track best Sharpe
        if val_sharpe_f32 > self.backtracking.best_sharpe {
            self.backtracking.best_sharpe = val_sharpe_f32;
            self.backtracking.best_epoch = epoch;
        }

        // 2. Save checkpoint when improving
        // Use prev_val_sharpe from backtracking state (tracks val_Sharpe, not training Sharpe)
        let improvement_rate = val_sharpe_f32 - self.backtracking.prev_val_sharpe;
        if improvement_rate > 0.01 && !self.backtracking.route_active {
            self.save_backtracking_checkpoint(epoch, improvement_rate, val_sharpe_f32)?;
        }

        // 3. Detect plateau: BOTH Q-gap frozen AND val_Sharpe stagnating.
        // Uses val_Sharpe (deterministic backtest), NOT training Sharpe.
        // Training Sharpe oscillates even when the model is truly frozen.
        let q_gap_frozen = self.is_q_gap_frozen();
        let sharpe_frozen = (val_sharpe_f32 - self.backtracking.prev_val_sharpe).abs() < 0.01;
        self.backtracking.prev_val_sharpe = val_sharpe_f32;  // update for next epoch
        if q_gap_frozen && sharpe_frozen {
            self.backtracking.plateau_epochs += 1;
            if self.backtracking.plateau_epochs > 0 {
                tracing::info!(
                    epoch, plateau_epochs = self.backtracking.plateau_epochs,
                    q_gap_frozen, sharpe_frozen,
                    checkpoints = self.backtracking.checkpoints.len(),
                    "Backtracking: plateau detection ({}/{})",
                    self.backtracking.plateau_epochs, self.backtracking.plateau_threshold,
                );
            }
        } else {
            self.backtracking.plateau_epochs = 0;
        }

        // 4. Evaluate active route
        if self.backtracking.route_active
            && epoch.saturating_sub(self.backtracking.route_start_epoch) >= self.backtracking.route_eval_epochs
        {
            let route_improvement = (val_sharpe_f32 - self.backtracking.route_start_sharpe)
                / self.backtracking.route_eval_epochs as f32;

            if route_improvement >= self.backtracking.min_improvement_rate {
                // Route succeeded -- continue training normally
                info!(epoch, route_improvement, "Backtracking route succeeded -- continuing");
                self.backtracking.route_active = false;
                self.backtracking.plateau_epochs = 0;
            } else {
                // Route failed -- try next route or next rewind
                info!(epoch, route_improvement, "Backtracking route failed -- trying next");
                self.backtracking.route_idx += 1;

                if self.backtracking.route_idx >= self.backtracking.routes_per_rewind {
                    // All routes for this rewind exhausted -- try next checkpoint
                    self.backtracking.rewind_count += 1;
                    self.backtracking.route_idx = 0;
                }

                self.backtracking.route_active = false;
                self.backtracking.plateau_epochs = self.backtracking.plateau_threshold; // re-trigger immediately
            }
        }

        // 5. Trigger rewind if plateau detected.
        //
        // Lightweight fix first: at exactly plateau_threshold, try branch Adam momentum
        // reset (zeroes m/v for indices 8-41, preserves trunk momentum). This may break
        // Adam fixed point without changing weights. Give it 3 epochs to take effect.
        // Full rewind triggers at plateau_threshold + 3.
        // Branch Adam reset doesn't need checkpoints — it's a lightweight in-place fix.
        if self.backtracking.plateau_epochs == self.backtracking.plateau_threshold
            && !self.backtracking.route_active
            && epoch.saturating_sub(self.backtracking.last_rewind_epoch) >= self.backtracking.min_rewind_interval
        {
            if let Some(ref mut fused) = self.fused_ctx {
                if let Err(e) = fused.reset_branch_adam_momentum() {
                    tracing::warn!(epoch, "Backtracking: branch Adam momentum reset failed: {e}");
                } else {
                    tracing::info!(epoch, "Backtracking: branch Adam momentum reset (lightweight fix, giving 3 epochs before full rewind)");
                }
            }
            // Do NOT execute_rewind yet — wait 3 more epochs
        }

        if self.backtracking.plateau_epochs >= self.backtracking.plateau_threshold + 3
            && !self.backtracking.route_active
            && epoch.saturating_sub(self.backtracking.last_rewind_epoch) >= self.backtracking.min_rewind_interval
            && !self.backtracking.checkpoints.is_empty()
        {
            if self.backtracking.is_exhausted() {
                // All attempts exhausted -- early termination
                info!(
                    best_sharpe = self.backtracking.best_sharpe,
                    best_epoch = self.backtracking.best_epoch,
                    rewinds = self.backtracking.rewind_count,
                    "PLATEAU_EXHAUSTED: {}x{} = {} attempts. Best val_Sharpe={:.2} at epoch {}. Structural change needed.",
                    self.backtracking.max_rewinds,
                    self.backtracking.routes_per_rewind,
                    self.backtracking.max_rewinds * self.backtracking.routes_per_rewind,
                    self.backtracking.best_sharpe,
                    self.backtracking.best_epoch,
                );
                return Ok(true); // signal caller to break
            }

            // Rewind to best checkpoint
            self.execute_rewind(epoch)?;
        }

        // 6. Handle temperature/LR boost cooldown
        if self.backtracking.temp_boost_remaining > 0 {
            self.backtracking.temp_boost_remaining -= 1;
        }
        if self.backtracking.lr_boost_remaining > 0 {
            self.backtracking.lr_boost_remaining -= 1;
            if self.backtracking.lr_boost_remaining == 0 {
                // Restore original LR
                self.hyperparams.learning_rate = self.backtracking.original_lr;
                info!(lr = self.hyperparams.learning_rate, "LR boost expired -- restored original learning rate");
            }
        }

        Ok(false)
    }

    /// Dispatch a named-state reset to its owning subsystem.
    ///
    /// Invariant: every entry in the StateResetRegistry's FoldReset category
    /// has a match arm here. Unknown names are a bug (the registry entry
    /// should have been added in the same commit as the state it describes).
    pub(crate) fn reset_named_state(&mut self, name: &str, fold_idx: u32) -> Result<(), crate::MLError> {
        match name {
            "kelly_stats" => {
                // Kelly stats are held in portfolio state ps[14..18] — cleared
                // per-window by the env_step kernel's fold_start detection.
                // No-op here; the per-window reset logic handles it.
            }
            "eval_q_mean_ema" | "eval_q_std_ema" | "isv_v_range_slots" => {
                // All three names map to the same reset — the EMA slots and ISV
                // v-range slots are a single logical unit. The registry lists them
                // separately so future tasks can migrate them independently;
                // for now one call covers all three.
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.reset_eval_v_range_state();
                }
            }
            "isv_iqn_loss_initial" | "isv_iqn_loss_ema" | "isv_iqn_readiness" => {
                // Three coupled scalars on `GpuDqnTrainer` (iqn_loss_initial,
                // iqn_loss_ema, iqn_readiness + pinned device-mapped slot)
                // form a single streaming-improvement gauge. Listed
                // separately in the registry so future tasks can migrate
                // them independently; all three currently dispatch to the
                // same `reset_iqn_readiness_state` helper. Closes Fix 3 of
                // the 2026-04-28 fold-boundary audit — without this reset
                // the readiness fraction is computed against a fold-0
                // anchor for every subsequent fold.
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.reset_iqn_readiness_state();
                }
            }
            "isv_learning_health" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::LEARNING_HEALTH_INDEX,
                        0.5,
                    );
                }
            }
            "isv_sharpe_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::SHARPE_EMA_INDEX,
                        0.0,
                    );
                }
            }
            "isv_q_means" => {
                if let Some(ref fused) = self.fused_ctx {
                    // Zero the 9 Q-mean EMA slots [13..22).
                    for idx in 13..22 {
                        fused.trainer().write_isv_signal_at(idx, 0.0);
                    }
                }
            }
            "isv_q_quantiles" => {
                // Reset Q-quantile EMA slots to v_min/v_max bootstrap at fold boundary.
                // q_p05 = v_min, q_p95 = v_max — matches cold-start atom range
                // so the first update_eval_v_range after fold start sees meaningful bounds.
                if let Some(ref fused) = self.fused_ctx {
                    let v_min_f = self.hyperparams.v_min as f32;
                    let v_max_f = self.hyperparams.v_max as f32;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(
                            crate::cuda_pipeline::gpu_dqn_trainer::Q_P05_DIR_INDEX + b,
                            v_min_f,
                        );
                        fused.trainer().write_isv_signal_at(
                            crate::cuda_pipeline::gpu_dqn_trainer::Q_P95_DIR_INDEX + b,
                            v_max_f,
                        );
                    }
                }
            }
            "isv_epoch_idx" => {
                // Reset epoch-idx slot at fold boundary; epoch loop re-populates on next start.
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::EPOCH_IDX_INDEX,
                        0.0,
                    );
                }
            }
            "isv_epsilon_eff" | "isv_tau_eff" | "isv_kelly_cap_eff" => {
                // GPU-written adaptive outputs. Reset to 0.0 at fold boundary;
                // GPU kernels (epsilon_update, tau_update, kelly_cap_update)
                // re-populate on next epoch-boundary launch.
                if let Some(ref fused) = self.fused_ctx {
                    let idx = match name {
                        "isv_epsilon_eff"   => crate::cuda_pipeline::gpu_dqn_trainer::EPSILON_EFF_INDEX,
                        "isv_tau_eff"       => crate::cuda_pipeline::gpu_dqn_trainer::TAU_EFF_INDEX,
                        "isv_kelly_cap_eff" => crate::cuda_pipeline::gpu_dqn_trainer::KELLY_CAP_EFF_INDEX,
                        _ => unreachable!(),
                    };
                    fused.trainer().write_isv_signal_at(idx, 0.0);
                }
            }
            // C.2 Plan 3 Task 1: reward-component EMA slots reset at fold boundary.
            // 6 slots [63..69); GPU kernel re-populates from the first epoch's
            // reward_components_per_sample buffer on next collect_experiences_gpu.
            "isv_reward_component_emas" => {
                if let Some(ref fused) = self.fused_ctx {
                    for idx in crate::cuda_pipeline::gpu_dqn_trainer::REWARD_POPART_EMA_INDEX
                        ..crate::cuda_pipeline::gpu_dqn_trainer::REWARD_POPART_EMA_INDEX
                            + crate::cuda_pipeline::gpu_dqn_trainer::REWARD_COMPONENT_COUNT
                    {
                        fused.trainer().write_isv_signal_at(idx, 0.0);
                    }
                }
            }
            // B.2 Plan 3 Task 3: trade-attempt novelty slots reset at fold
            // boundary. The attempt-EMA rebuilds from scratch on the next fold;
            // the target slot stays 0.0 until epoch 5 freezes it from the new
            // fold's measured rate.
            "isv_trade_attempt_rate_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::TRADE_ATTEMPT_RATE_EMA_INDEX,
                        0.0,
                    );
                }
            }
            "isv_trade_target_rate" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::TRADE_TARGET_RATE_INDEX,
                        0.0,
                    );
                }
            }
            // B.4 Plan 3 Task 4: producer of slot 49 is now the GPU
            // plan_threshold_update kernel. At a fold boundary we restore
            // the cold-start values for both slots so consumers continue
            // to see a sensible 0.5 gate before the kernel's first fire on
            // the new fold.
            "isv_plan_threshold" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::PLAN_THRESHOLD_INDEX,
                        0.5,
                    );
                }
            }
            "isv_readiness_ema" => {
                // Cold-start to 1.0 so the first kernel fire on the new
                // fold derives `threshold = max(0.1, 0.5 × 1.0) = 0.5`,
                // matching the cold-start value of slot 49 above.
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::READINESS_EMA_INDEX,
                        1.0,
                    );
                }
            }
            // C.3 Plan 3 Task 7: state-distribution KL slots. Mirror the
            // constructor cold-start values — KL EMA at 0.0, amp at 1.0
            // (the no-op neutral element behind the fmaxf(1.0, …) consumer
            // guards). The kernel rebuilds the KL EMA from scratch on the
            // new fold; the amp EMA tracks ratio-of-self so it self-stabilises
            // toward 1 even in the absence of a reset.
            "isv_state_kl_train_val_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::STATE_KL_TRAIN_VAL_EMA_INDEX,
                        0.0,
                    );
                }
            }
            "isv_state_kl_amplification" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::STATE_KL_AMPLIFICATION_INDEX,
                        1.0,
                    );
                }
            }
            // B.3 Plan 3 Task 8: seed-phase progress slots reset at fold
            // boundary. SEED_STEPS_DONE → 0 and SEED_FRAC_EMA → 1.0 mirror
            // the constructor cold-start so the new fold begins fully in
            // seed phase (the GPU `seed_step_counter_update` kernel
            // re-decays them as the fold's experience collection proceeds).
            // SEED_STEPS_TARGET is SchemaContract — never reset here.
            "isv_seed_steps_done" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::SEED_STEPS_DONE_INDEX,
                        0.0,
                    );
                }
            }
            "isv_seed_frac_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::SEED_FRAC_EMA_INDEX,
                        1.0,
                    );
                }
            }
            // C.5 Plan 3 Task 9: CQL α slot promoted to FoldReset.
            // Constructor cold-starts to config.cql_alpha; the GPU
            // `cql_alpha_seed_update` kernel ramps it toward
            // `config.cql_alpha × (1 - seed_frac)` over training. At fold
            // boundary we re-apply the cold-start so the new fold's seed
            // phase starts from the same configured base. Producer-only
            // upgrade — CQL gradient kernel consumer (already reading
            // slot 48 per Plan 1 Task 12) unchanged.
            "isv_cql_alpha" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::CQL_ALPHA_INDEX,
                        self.hyperparams.cql_alpha as f32,
                    );
                }
            }
            // E.5 Plan 4 Task 5 Mode A: attention-focus interpretability
            // EMAs. Reset to 0.0 at fold boundary, matching the constructor
            // cold-start. The `attention_focus_ema_update` GPU kernel
            // re-EMAs from the per-epoch host scalars (vsn_mag, vsn_dir,
            // mamba2_retention) so the slots ramp up cleanly from epoch 1
            // of the new fold.
            "isv_vsn_mag_ema" | "isv_vsn_dir_ema" | "isv_mamba2_retention_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    let idx = match name {
                        "isv_vsn_mag_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MAG_EMA_INDEX,
                        "isv_vsn_dir_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::VSN_DIR_EMA_INDEX,
                        "isv_mamba2_retention_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::MAMBA2_RETENTION_EMA_INDEX,
                        _ => unreachable!(),
                    };
                    fused.trainer().write_isv_signal_at(idx, 0.0);
                }
            }
            // Plan 4 Task 2c.3c.5: trunk-RMS EMA producer slot. SP4 Task A13.0
            // retrofit reset to Pearl A sentinel 0.0 at fold boundary — the
            // companion `sp4_wiener_state` bulk reset zeros `state.x_lag` in
            // lockstep so the first `h_s2_rms_ema_update` fire on the new fold
            // sees `prev_x_mean == 0 AND state.x_lag == 0` and triggers Pearl A's
            // first-observation replacement rather than EMAing against a stale
            // cold-start. Per `feedback_no_partial_refactor.md` — the sentinel
            // contract migrates both halves together.
            "isv_h_s2_rms_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::H_S2_RMS_EMA_INDEX,
                        0.0,
                    );
                }
            }
            // Plan 4 Task 3 (E.3): IQN multi-quantile diagnostic EMA slots.
            // Reset to 0.0 at fold boundary so the first iqn_quantile_ema_update
            // fire on the new fold EMAs the measured per-quantile Q toward 0
            // rather than carrying forward a stale fold-N estimate. Diagnostic
            // only — no consumer kernel reads slots 99..103 in this commit.
            "isv_iqn_q_p05_ema" | "isv_iqn_q_p25_ema"
            | "isv_iqn_q_p75_ema" | "isv_iqn_q_p95_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    let idx = match name {
                        "isv_iqn_q_p05_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::IQN_Q_P05_EMA_INDEX,
                        "isv_iqn_q_p25_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::IQN_Q_P25_EMA_INDEX,
                        "isv_iqn_q_p75_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::IQN_Q_P75_EMA_INDEX,
                        "isv_iqn_q_p95_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::IQN_Q_P95_EMA_INDEX,
                        _ => unreachable!(),
                    };
                    fused.trainer().write_isv_signal_at(idx, 0.0);
                }
            }
            // Plan 4 Task 1B-ii: VSN per-group mask-EMA producer slots
            // (one per `FEATURE_GROUP_RANGES` entry). SP4 Task A13.3 retrofit
            // reset to Pearl A sentinel 0.0 at fold boundary — the companion
            // `sp4_wiener_state` bulk reset zeros `state.x_lag` in lockstep so
            // the first `vsn_mask_ema_update` fire on the new fold sees
            // `prev_x_mean == 0 AND state.x_lag == 0` and triggers Pearl A's
            // first-observation replacement. Per `feedback_no_partial_refactor.md`
            // — the sentinel contract migrates both halves together.
            // Diagnostic only — no consumer kernel reads slots [105..111) in
            // this commit nor in 1B-iii (the consumer attaches in a later
            // `attention_monitor` upgrade).
            "isv_vsn_mask_g0_ema" | "isv_vsn_mask_g1_ema" | "isv_vsn_mask_g2_ema"
            | "isv_vsn_mask_g3_ema" | "isv_vsn_mask_g4_ema" | "isv_vsn_mask_g5_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    let idx = match name {
                        "isv_vsn_mask_g0_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_0_EMA_INDEX,
                        "isv_vsn_mask_g1_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_1_EMA_INDEX,
                        "isv_vsn_mask_g2_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_2_EMA_INDEX,
                        "isv_vsn_mask_g3_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_3_EMA_INDEX,
                        "isv_vsn_mask_g4_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_4_EMA_INDEX,
                        "isv_vsn_mask_g5_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::VSN_MASK_GROUP_5_EMA_INDEX,
                        _ => unreachable!(),
                    };
                    fused.trainer().write_isv_signal_at(idx, 0.0_f32);
                }
            }
            // Plan 4 Task 6 Commit A: aux-head loss EMA producer slots
            // (next-bar regression MSE + regime classification CE). Reset to
            // 0.0 at fold boundary so the first `aux_heads_loss_ema_update`
            // fire on the new fold EMAs the measured per-head loss toward 0
            // rather than carrying a stale fold-N estimate. Diagnostic only —
            // no GPU consumer kernel reads slots [113..115) in either Commit
            // A or B (Commit B's HEALTH_DIAG line is a CPU-side text
            // emitter, not a kernel input). Producer kernel landed in
            // Commit A; launch site wires in Commit B.
            "isv_aux_next_bar_mse_ema" | "isv_aux_regime_ce_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    let idx = match name {
                        "isv_aux_next_bar_mse_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::AUX_NEXT_BAR_MSE_EMA_INDEX,
                        "isv_aux_regime_ce_ema"
                            => crate::cuda_pipeline::gpu_dqn_trainer::AUX_REGIME_CE_EMA_INDEX,
                        _ => unreachable!(),
                    };
                    fused.trainer().write_isv_signal_at(idx, 0.0);
                }
            }
            // SP13 B1.0 (2026-05-05): the former `isv_aux_label_scale_ema`
            // arm is retired together with ISV[117]. The slot is no
            // longer written by any producer (`aux_label_scale_ema_update`
            // kernel was deleted) and no longer read by any consumer
            // (`aux_next_bar_loss_reduce` / `aux_next_bar_backward`
            // flipped to scale-free); nothing to reset at fold boundary.
            // Removing the registry entry above made this arm dead, and
            // dispatch on the retired name now correctly falls into the
            // unknown-name guard.
            // SP4 Task A13.2 retrofit: per-expert MoE utilisation EMA slots.
            // Reset to Pearl A sentinel 0.0 at fold boundary — the companion
            // `sp4_wiener_state` bulk reset zeros `state.x_lag` in lockstep so
            // the first `moe_expert_util_ema_update` fire on the new fold sees
            // `prev_x_mean == 0 AND state.x_lag == 0` and triggers Pearl A's
            // first-observation replacement rather than EMAing against a
            // uniform 1/K cold-start. Per `feedback_no_partial_refactor.md` —
            // the sentinel contract migrates both halves together.
            "isv_moe_expert_util_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    for k in 0..crate::cuda_pipeline::gpu_dqn_trainer::MOE_EXPERT_UTIL_EMA_COUNT {
                        fused.trainer().write_isv_signal_at(
                            crate::cuda_pipeline::gpu_dqn_trainer::MOE_EXPERT_UTIL_EMA_BASE + k,
                            0.0_f32,
                        );
                    }
                }
            }
            // SP4 Task A13.2 retrofit: gate-distribution entropy EMA slot.
            // Reset to Pearl A sentinel 0.0 at fold boundary — same
            // sentinel-contract rationale as `isv_moe_expert_util_ema` above
            // (paired Wiener-state reset zeros `state.x_lag` in lockstep).
            "isv_moe_gate_entropy_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::MOE_GATE_ENTROPY_EMA_INDEX,
                        0.0_f32,
                    );
                }
            }
            "isv_moe_lambda_eff" => {
                // Adaptive load-balance λ controller — fold reset writes
                // ISV[MOE_LAMBDA_EFF_INDEX] to the configured floor so the
                // new fold begins at the legacy 0.01 baseline; the per-step
                // controller (`moe_lambda_eff_update`) raises λ_eff up to
                // (floor + max_extra) as the gate-entropy EMA decays.
                if let Some(ref fused) = self.fused_ctx {
                    let trainer = fused.trainer();
                    let floor = trainer.config().moe_lambda_floor;
                    trainer.write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::MOE_LAMBDA_EFF_INDEX,
                        floor,
                    );
                }
            }
            "isv_q_drift_rate" => {
                // Plan C Phase 2 follow-up A.2 (2026-04-29): adaptive
                // Polyak-tau drift signal. Reset to 0.0 at fold boundary so
                // the new fold's first `tau_update` consumer sees a no-op
                // dampening factor of 1.0 (`drift_factor = 1/(1+0) = 1`)
                // until `q_drift_rate_ema_update` fires with a meaningful
                // delta. Companion to A.1's `prev_epoch_q_mean = 0.0`
                // reset in `DQNTrainer::reset_for_fold` — both ensure no
                // cross-fold leakage of drift state.
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::Q_DRIFT_RATE_INDEX,
                        0.0_f32,
                    );
                }
            }
            "isv_fold_warmup_factor" => {
                // Plan C Phase 2 follow-up K (2026-04-29): adaptive
                // fold-boundary warmup factor. Reset to 0.0 at fold
                // boundary so the new fold begins under heavy lr/clip
                // damping (consumer floors: lr_eff = 0.05 × lr_base,
                // clip_eff = 0.1 × clip_base). Companion FoldReset entry
                // `isv_grad_norm_fast_ema` zeroes the kernel's numerator
                // EMA in lockstep so the ratio starts at 0; the slow EMA
                // persists across folds.
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::FOLD_WARMUP_FACTOR_INDEX,
                        0.0_f32,
                    );
                }
            }
            "isv_grad_norm_fast_ema" => {
                // Plan C Phase 2 follow-up K (2026-04-29): fast grad-norm
                // EMA mapped-pinned scalar (numerator of the
                // fold-warmup-factor ratio). Companion to
                // `isv_fold_warmup_factor` — both reset in lockstep per
                // `feedback_no_partial_refactor.md` so the ratio starts
                // at 0 (fast=0 over slow≈baseline). Slow EMA persists.
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.trainer_mut().reset_fast_grad_norm_ema();
                }
            }
            // ── SP4 Layer A Task A12 (2026-04-30): adaptive bound +
            // Wiener-state + Pearl C engagement-counter fold-boundary
            // dispatch. Each registry entry resets one logical group via
            // `write_isv_signal_at` (single ISV slot) or `write_isv_signal_at`
            // in a loop (multi-slot families) or `reset_sp4_*` bulk helpers
            // (mapped-pinned buffers). Cold-start value is 0.0 in all cases
            // — Pearl A's first-observation sentinel fires on the new fold's
            // first producer launch (`prev_x_mean == 0 AND state.x_lag == 0`).
            "sp4_target_q_bound" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::sp4_isv_slots::TARGET_Q_BOUND_INDEX,
                        0.0_f32,
                    );
                }
            }
            "sp4_atom_pos_bounds" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp4_isv_slots::{ATOM_POS_BOUND_BASE, SP4_BRANCH_COUNT};
                    for b in 0..SP4_BRANCH_COUNT {
                        fused.trainer().write_isv_signal_at(ATOM_POS_BOUND_BASE + b, 0.0_f32);
                    }
                }
            }
            "sp4_weight_bounds" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp4_isv_slots::{WEIGHT_BOUND_BASE, SP4_PARAM_GROUP_COUNT};
                    for g in 0..SP4_PARAM_GROUP_COUNT {
                        fused.trainer().write_isv_signal_at(WEIGHT_BOUND_BASE + g, 0.0_f32);
                    }
                }
            }
            "sp4_adam_m_bounds" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp4_isv_slots::{ADAM_M_BOUND_BASE, SP4_PARAM_GROUP_COUNT};
                    for g in 0..SP4_PARAM_GROUP_COUNT {
                        fused.trainer().write_isv_signal_at(ADAM_M_BOUND_BASE + g, 0.0_f32);
                    }
                }
            }
            "sp4_adam_v_bounds" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp4_isv_slots::{ADAM_V_BOUND_BASE, SP4_PARAM_GROUP_COUNT};
                    for g in 0..SP4_PARAM_GROUP_COUNT {
                        fused.trainer().write_isv_signal_at(ADAM_V_BOUND_BASE + g, 0.0_f32);
                    }
                }
            }
            "sp4_wd_rate_bounds" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp4_isv_slots::{WD_RATE_BASE, SP4_PARAM_GROUP_COUNT};
                    for g in 0..SP4_PARAM_GROUP_COUNT {
                        fused.trainer().write_isv_signal_at(WD_RATE_BASE + g, 0.0_f32);
                    }
                }
            }
            "sp4_grad_clip_bound" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::sp4_isv_slots::GRAD_CLIP_BOUND_INDEX,
                        0.0_f32,
                    );
                }
            }
            "sp4_h_s2_bound" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::sp4_isv_slots::H_S2_BOUND_INDEX,
                        0.0_f32,
                    );
                }
            }
            "sp4_l1_lambda_trunk" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::sp4_isv_slots::L1_LAMBDA_TRUNK_INDEX,
                        0.0_f32,
                    );
                }
            }
            "sp4_bw_d_h_s2_bound" => {
                // Layer C #260 (2026-05-01): backward-grad sanitiser bound
                // for `bw_d_h_s2`. Pearl A sentinel 0 at fold boundary so the
                // new fold's first `launch_sp4_bw_d_h_s2_p99` observation
                // replaces directly. Companion `sp4_wiener_state` bulk reset
                // zeros the slot-69 wiener triple in lockstep so both halves
                // of the sentinel contract reset together.
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::sp4_isv_slots::BW_D_H_S2_BOUND_INDEX,
                        0.0_f32,
                    );
                }
            }
            "sp4_q_dir_grad_bound" => {
                // Layer C #260 (2026-05-01): backward-grad sanitiser bound
                // for `d_value_logits ∪ d_adv_logits` (multi-sub-buffer p99).
                // Pearl A sentinel 0 at fold boundary so the new fold's first
                // `launch_sp4_q_dir_grad_p99` observation replaces directly.
                // Companion `sp4_wiener_state` bulk reset zeros the slot-70
                // wiener triple in lockstep so both halves of the sentinel
                // contract reset together.
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::sp4_isv_slots::Q_DIR_GRAD_BOUND_INDEX,
                        0.0_f32,
                    );
                }
            }
            "sp4_wiener_state" => {
                // Bulk write_bytes(0) on `wiener_state_buf` (213 mapped-
                // pinned f32 = 71 producers × 3-tuple). Pearl A's first-
                // observation sentinel requires `state.x_lag == 0` on the
                // new fold's first producer launch, in lockstep with the
                // SP4 ISV bound slots above (and the 29 Task-A13 retrofit
                // ISV slots + 2 Layer-C-#260 SP1-multiplier-replacement
                // slots, all of which now also reset to 0.0).
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.trainer_mut().reset_sp4_wiener_state();
                }
            }
            "sp4_clamp_engage_counters" => {
                // Bulk write_bytes(0) on `clamp_engage_per_block_buf`
                // (2816 mapped-pinned i32 — Task A14/A15 grew it from
                // 2048 to accommodate Curiosity's 4 distinct sub-launch
                // offset slots). Each fold restarts Pearl C's
                // engagement-rate tracking from a clean 0 baseline.
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.trainer_mut().reset_sp4_clamp_engage_counters();
                }
            }
            "sp4_pearl_c_rate_deficit_state" => {
                // Bulk write_bytes(0) on `pearl_c_rate_deficit_state_buf`
                // (24 mapped-pinned f32 = 8 groups × {sample_var, diff_var,
                // x_lag}). Pearl A's first-observation sentinel requires
                // `state.x_lag == 0` on the new fold's first engagement-
                // rate-deficit observation; reset paired with the host-
                // only `pearl_c_rate_deficit_ema` below.
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.trainer_mut().reset_sp4_pearl_c_rate_deficit_state();
                }
            }
            "sp4_pearl_c_rate_deficit_ema" => {
                // Reset the host-only `pearl_c_rate_deficit_ema [f32; 8]`
                // EMA surrogate to all zeros. Companion to
                // sp4_pearl_c_rate_deficit_state — both halves of Pearl A's
                // sentinel contract reset together.
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.trainer_mut().reset_sp4_pearl_c_rate_deficit_ema();
                }
            }
            "isv_gamma_dir_eff" | "isv_gamma_mag_eff" | "isv_gamma_ord_eff" | "isv_gamma_urg_eff" => {
                // D.2 per-branch gamma slots. Reset to 0.0 at fold boundary;
                // per_branch_gamma_update GPU kernel re-populates on next epoch-boundary launch.
                if let Some(ref fused) = self.fused_ctx {
                    let idx = match name {
                        "isv_gamma_dir_eff" => crate::cuda_pipeline::gpu_dqn_trainer::GAMMA_DIR_EFF_INDEX,
                        "isv_gamma_mag_eff" => crate::cuda_pipeline::gpu_dqn_trainer::GAMMA_MAG_EFF_INDEX,
                        "isv_gamma_ord_eff" => crate::cuda_pipeline::gpu_dqn_trainer::GAMMA_ORD_EFF_INDEX,
                        "isv_gamma_urg_eff" => crate::cuda_pipeline::gpu_dqn_trainer::GAMMA_URG_EFF_INDEX,
                        _ => unreachable!(),
                    };
                    fused.trainer().write_isv_signal_at(idx, 0.0);
                }
            }
            // ── D.5 SoftReset dispatch ─────────────────────────────────────
            // Both entries use bootstrap-write semantics: CPU writes the known-
            // good bootstrap value at fold boundary; the existing
            // `grad_balance_isv_update` GPU kernel's adaptive-rate EMA then
            // converges from that bootstrap toward the observed per-branch norms
            // over subsequent epochs. This is the Option A design — the EMA
            // time constant (~1/α, α ∈ [0.01, 0.30]) provides the implicit
            // decay from bootstrap, satisfying the decay_bars=500 intent without
            // a new ISV slot or a CPU-side adaptive computation.
            //
            // CPU-born input rule (§4.C.6): the bootstrap values 1.0 and 2.0 are
            // fixed structural constants (same values used at construction in
            // gpu_dqn_trainer.rs), not computed adaptive outputs. Writing them
            // here is a schedule input, not an adaptive feedback loop.
            "isv_grad_balance_targets" => {
                if let Some(ref fused) = self.fused_ctx {
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(
                            crate::cuda_pipeline::gpu_dqn_trainer::GRAD_NORM_TARGET_DIR_INDEX + b,
                            1.0_f32,
                        );
                    }
                }
            }
            "isv_grad_scale_limit" => {
                if let Some(ref fused) = self.fused_ctx {
                    fused.trainer().write_isv_signal_at(
                        crate::cuda_pipeline::gpu_dqn_trainer::GRAD_SCALE_LIMIT_INDEX,
                        2.0_f32,
                    );
                }
            }
            // ── SP5 FoldReset dispatch (2026-05-02) ────────────────────────
            // Each SP5 ISV-range entry zeros its slot range to fire Pearl A's
            // first-observation sentinel on the new fold's first producer
            // launch (sentinel: `prev_x_mean == 0 AND state.x_lag == 0` →
            // first-observation replacement). Pearl 6 (slots 280..286) is
            // INTENTIONALLY ABSENT — those slots are cross-fold-persistent
            // (the whole point of A6); see project_magnitude_eval_collapse_kelly_capped.
            "sp5_atom_v_center" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::ATOM_V_CENTER_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(ATOM_V_CENTER_BASE + b, 0.0);
                    }
                }
            }
            "sp5_atom_v_half" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::ATOM_V_HALF_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(ATOM_V_HALF_BASE + b, 0.0);
                    }
                }
            }
            "sp5_atom_headroom" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::ATOM_HEADROOM_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(ATOM_HEADROOM_BASE + b, 0.0);
                    }
                }
            }
            "sp5_atom_clip_rate" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::ATOM_CLIP_RATE_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(ATOM_CLIP_RATE_BASE + b, 0.0);
                    }
                }
            }
            "sp5_branch_entropy" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::BRANCH_ENTROPY_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(BRANCH_ENTROPY_BASE + b, 0.0);
                    }
                }
            }
            "sp5_q_var_per_branch" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::Q_VAR_PER_BRANCH_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(Q_VAR_PER_BRANCH_BASE + b, 0.0);
                    }
                }
            }
            "sp5_wiener_state" => {
                // No-op: `reset_sp4_wiener_state` already bulk-zeros the
                // FULL `wiener_state_buf [0..(71 + SP5_PRODUCER_COUNT) × 3)`
                // which includes the SP5 producer block at offsets
                // [71 × 3 .. (71 + SP5_PRODUCER_COUNT) × 3). Buffer size
                // is parameterised on the runtime `SP5_PRODUCER_COUNT`
                // constant (`cuda_pipeline::sp5_isv_slots`) and grows as
                // each SP follow-up adds producers — do NOT hardcode a
                // float count here, it will go stale (B1b fix-up landed
                // slot 360 → SP5_PRODUCER_COUNT=187 → buffer = 774 floats,
                // SP5 block [213..774)). The registry entry exists for
                // documentation symmetry with the SP4 entry; the actual
                // byte-zero is performed by the SP4 arm. Pearl 6 (slots
                // 280..286) is intentionally exempt — its wiener-state
                // offsets [525..543) are unused by design
                // (apply_pearls_ad_kernel is not called for Pearl 6).
            }
            "sp5_noisy_sigma" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::NOISY_SIGMA_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(NOISY_SIGMA_BASE + b, 0.0);
                    }
                }
            }
            "sp5_sigma_fraction" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::SIGMA_FRACTION_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(SIGMA_FRACTION_BASE + b, 0.0);
                    }
                }
            }
            "sp5_budget_c51" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::BUDGET_C51_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(BUDGET_C51_BASE + b, 0.0);
                    }
                }
            }
            "sp5_budget_iqn" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::BUDGET_IQN_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(BUDGET_IQN_BASE + b, 0.0);
                    }
                }
            }
            "sp5_budget_cql" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::BUDGET_CQL_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(BUDGET_CQL_BASE + b, 0.0);
                    }
                }
            }
            "sp5_budget_ens" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::BUDGET_ENS_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(BUDGET_ENS_BASE + b, 0.0);
                    }
                }
            }
            "sp5_flatness" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::FLATNESS_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(FLATNESS_BASE + b, 0.0);
                    }
                }
            }
            "sp5_adam_beta1" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::ADAM_BETA1_BASE;
                    for g in 0..8 {
                        fused.trainer().write_isv_signal_at(ADAM_BETA1_BASE + g, 0.0);
                    }
                }
            }
            "sp5_adam_beta2" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::ADAM_BETA2_BASE;
                    for g in 0..8 {
                        fused.trainer().write_isv_signal_at(ADAM_BETA2_BASE + g, 0.0);
                    }
                }
            }
            "sp5_adam_eps" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::ADAM_EPS_BASE;
                    for g in 0..8 {
                        fused.trainer().write_isv_signal_at(ADAM_EPS_BASE + g, 0.0);
                    }
                }
            }
            "sp5_grad_prev_buf" => {
                // Bulk-zero `grad_prev_buf_per_group` so the new fold's
                // first `grad_cosine_sim_update` sees cos_sim=0 → low-
                // stability envelope floor → clean Pearl 4 cold-start.
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.trainer_mut().reset_sp5_grad_prev_buf();
                }
            }
            "sp5_iqn_tau" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::IQN_TAU_BASE;
                    for i in 0..20 {
                        // 4 branches × 5 quantiles = 20 slots.
                        fused.trainer().write_isv_signal_at(IQN_TAU_BASE + i, 0.0);
                    }
                }
            }
            "sp5_trail_dist_per_dir" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::TRAIL_DIST_PER_DIR_BASE;
                    for d in 0..4 {
                        fused.trainer().write_isv_signal_at(TRAIL_DIST_PER_DIR_BASE + d, 0.0);
                    }
                }
            }
            "sp5_atom_num_atoms" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::ATOM_NUM_ATOMS_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(ATOM_NUM_ATOMS_BASE + b, 0.0);
                    }
                }
            }
            // SP7 Task 7 (2026-05-03): loss-balance controller per-branch
            // Wiener-state EMAs. Sentinel 0 at fold boundary so the
            // controller's `old_budget < EPS_DIV` cold-start branch fires
            // and α resets cleanly when budgets re-bootstrap.
            "sp7_lb_diff_var_cql" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::LB_DIFF_VAR_CQL_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(LB_DIFF_VAR_CQL_BASE + b, 0.0);
                    }
                }
            }
            "sp7_lb_sample_var_cql" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::LB_SAMPLE_VAR_CQL_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(LB_SAMPLE_VAR_CQL_BASE + b, 0.0);
                    }
                }
            }
            "sp7_lb_diff_var_c51" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::LB_DIFF_VAR_C51_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(LB_DIFF_VAR_C51_BASE + b, 0.0);
                    }
                }
            }
            "sp7_lb_sample_var_c51" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::LB_SAMPLE_VAR_C51_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(LB_SAMPLE_VAR_C51_BASE + b, 0.0);
                    }
                }
            }
            // SP7 activation-flag fix (2026-05-03): per-(head, branch)
            // activation flags. Zero at fold boundary so the new fold
            // re-activates from scratch as grads populate.
            "sp7_lb_cql_active" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::LB_CQL_ACTIVE_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(LB_CQL_ACTIVE_BASE + b, 0.0);
                    }
                }
            }
            "sp7_lb_c51_active" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::LB_C51_ACTIVE_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(LB_C51_ACTIVE_BASE + b, 0.0);
                    }
                }
            }
            // SP8 (Fix 36, 2026-05-03): adaptive MAX_BUDGET cap registry
            // entries. Three FoldReset entries — sentinel 0 at fold boundary
            // so Pearl A's first-observation replacement fires on the new
            // fold's first producer launch (per
            // `pearl_first_observation_bootstrap.md`). Wiener-state companion
            // is reset by the existing bulk memset of `wiener_state_buf`
            // covered by the `sp4_wiener_state` registry entry's dispatch arm.
            "sp8_train_active_frac" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::TRAIN_ACTIVE_FRAC_INDEX;
                    fused.trainer().write_isv_signal_at(TRAIN_ACTIVE_FRAC_INDEX, 0.0);
                }
            }
            "sp8_lb_max_budget_cql" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::LB_MAX_BUDGET_CQL_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(LB_MAX_BUDGET_CQL_BASE + b, 0.0);
                    }
                }
            }
            "sp8_lb_max_budget_c51" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::LB_MAX_BUDGET_C51_BASE;
                    for b in 0..4 {
                        fused.trainer().write_isv_signal_at(LB_MAX_BUDGET_C51_BASE + b, 0.0);
                    }
                }
            }
            // SP9 (Fix 37, 2026-05-03): 9 ISV slots — Kelly cold-start
            // warmup floor + 3 EMA targets + intent/eval divergence + 3
            // eval_dist mag bins. All FoldReset sentinel 0 so Pearl A's
            // first-observation replacement fires on the new fold's first
            // producer launch (per `pearl_first_observation_bootstrap.md`).
            // Wiener-state companion is reset by the existing bulk memset of
            // `wiener_state_buf` covered by the `sp4_wiener_state` registry
            // entry's dispatch arm.
            "sp9_kelly_warmup_floor" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::KELLY_WARMUP_FLOOR_INDEX;
                    fused.trainer().write_isv_signal_at(KELLY_WARMUP_FLOOR_INDEX, 0.0);
                }
            }
            "sp9_q_var_mag_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::Q_VAR_MAG_EMA_INDEX;
                    fused.trainer().write_isv_signal_at(Q_VAR_MAG_EMA_INDEX, 0.0);
                }
            }
            "sp9_intent_eval_divergence" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::INTENT_EVAL_DIVERGENCE_INDEX;
                    fused.trainer().write_isv_signal_at(INTENT_EVAL_DIVERGENCE_INDEX, 0.0);
                }
            }
            "sp9_kelly_sample_count_target" => {
                // SP9 Fix 37.1 (2026-05-03): Invariant-1 numerical anchor —
                // not a stateful EMA. Rewrite the constructor's value at
                // fold boundary so the new fold sees the correct
                // sample-sufficiency threshold (100 trades) for the
                // statistical-confidence axis. Per
                // `feedback_isv_for_adaptive_bounds.md`.
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::KELLY_SAMPLE_COUNT_TARGET_INDEX;
                    fused.trainer().write_isv_signal_at(KELLY_SAMPLE_COUNT_TARGET_INDEX, 100.0);
                }
            }
            "sp9_kelly_divergence_target" => {
                // SP9 Fix 37.1 (2026-05-03): Invariant-1 numerical anchor —
                // not a stateful EMA. Rewrite the constructor's value at
                // fold boundary so the new fold sees the correct
                // divergence threshold (2.0) for the behavioral-confidence
                // axis. Per `feedback_isv_for_adaptive_bounds.md`.
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::KELLY_DIVERGENCE_TARGET_INDEX;
                    fused.trainer().write_isv_signal_at(KELLY_DIVERGENCE_TARGET_INDEX, 2.0);
                }
            }
            "sp9_kelly_temporal_target" => {
                // SP9 Fix 37.1 (2026-05-03): Invariant-1 numerical anchor —
                // not a stateful EMA. Rewrite the constructor's value at
                // fold boundary so the new fold sees the correct
                // fold-temporal threshold (5 epochs) for the
                // temporal-confidence axis. Per
                // `feedback_isv_for_adaptive_bounds.md`.
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::KELLY_TEMPORAL_TARGET_INDEX;
                    fused.trainer().write_isv_signal_at(KELLY_TEMPORAL_TARGET_INDEX, 5.0);
                }
            }
            "sp9_eval_dist_q" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::EVAL_DIST_Q_INDEX;
                    fused.trainer().write_isv_signal_at(EVAL_DIST_Q_INDEX, 0.0);
                }
            }
            "sp9_eval_dist_h" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::EVAL_DIST_H_INDEX;
                    fused.trainer().write_isv_signal_at(EVAL_DIST_H_INDEX, 0.0);
                }
            }
            "sp9_eval_dist_f" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::EVAL_DIST_F_INDEX;
                    fused.trainer().write_isv_signal_at(EVAL_DIST_F_INDEX, 0.0);
                }
            }
            "sp10_eval_thompson_temp" => {
                // SP10 (Fix 38, 2026-05-03): FoldReset sentinel 0 so
                // Pearl A's first-observation replacement fires on the
                // new fold's first `intent_eval_divergence_compute_kernel`
                // launch. Wiener-state companion is reset by the existing
                // bulk memset of `wiener_state_buf` covered by the
                // `sp4_wiener_state` registry entry's dispatch arm
                // (SP5_PRODUCER_COUNT linear span grew 165 → 166, the
                // buffer expanded in lockstep).
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::EVAL_THOMPSON_TEMP_INDEX;
                    fused.trainer().write_isv_signal_at(EVAL_THOMPSON_TEMP_INDEX, 0.0);
                }
            }
            // ── SP11 (Fix 39, 2026-05-04): reward-subsystem controller ─────
            //
            // 15 registry entries covering 20 slots [340..360). All are
            // FoldReset sentinel 0 so Pearl A's first-observation
            // replacement fires on the new fold's first producer launch
            // (per `pearl_first_observation_bootstrap.md`). Wiener-state
            // companion is reset by the existing bulk memset of
            // `wiener_state_buf` covered by the `sp4_wiener_state`
            // registry entry's dispatch arm — `SP5_PRODUCER_COUNT` linear
            // span grew 166 → 186, the buffer expanded in lockstep.
            //
            // No consumer reads any of these slots in A0 — the trainer
            // continues to behave identically. Layer A1/A2 introduce the
            // producer kernels and Layer B atomically migrates consumers.
            "sp11_reward_popart_weight" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::REWARD_POPART_WEIGHT_INDEX;
                    fused.trainer().write_isv_signal_at(REWARD_POPART_WEIGHT_INDEX, 0.0);
                }
            }
            "sp11_reward_cf_weight" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::REWARD_CF_WEIGHT_INDEX;
                    fused.trainer().write_isv_signal_at(REWARD_CF_WEIGHT_INDEX, 0.0);
                }
            }
            "sp11_reward_trail_weight" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::REWARD_TRAIL_WEIGHT_INDEX;
                    fused.trainer().write_isv_signal_at(REWARD_TRAIL_WEIGHT_INDEX, 0.0);
                }
            }
            "sp11_reward_micro_weight" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::REWARD_MICRO_WEIGHT_INDEX;
                    fused.trainer().write_isv_signal_at(REWARD_MICRO_WEIGHT_INDEX, 0.0);
                }
            }
            "sp11_reward_opp_cost_weight" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::REWARD_OPP_COST_WEIGHT_INDEX;
                    fused.trainer().write_isv_signal_at(REWARD_OPP_COST_WEIGHT_INDEX, 0.0);
                }
            }
            "sp11_reward_bonus_weight" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::REWARD_BONUS_WEIGHT_INDEX;
                    fused.trainer().write_isv_signal_at(REWARD_BONUS_WEIGHT_INDEX, 0.0);
                }
            }
            "sp11_curiosity_pressure" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::CURIOSITY_PRESSURE_INDEX;
                    fused.trainer().write_isv_signal_at(CURIOSITY_PRESSURE_INDEX, 0.0);
                }
            }
            "sp11_saboteur_intensity_mult" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::SABOTEUR_INTENSITY_MULT_INDEX;
                    fused.trainer().write_isv_signal_at(SABOTEUR_INTENSITY_MULT_INDEX, 0.0);
                }
            }
            "sp11_reward_weight_floor" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::REWARD_WEIGHT_FLOOR_INDEX;
                    fused.trainer().write_isv_signal_at(REWARD_WEIGHT_FLOOR_INDEX, 0.0);
                }
            }
            "sp11_curiosity_bound" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::CURIOSITY_BOUND_INDEX;
                    fused.trainer().write_isv_signal_at(CURIOSITY_BOUND_INDEX, 0.0);
                }
            }
            "sp11_val_sharpe_delta_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::VAL_SHARPE_DELTA_EMA_INDEX;
                    fused.trainer().write_isv_signal_at(VAL_SHARPE_DELTA_EMA_INDEX, 0.0);
                }
            }
            "sp11_val_sharpe_var_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::VAL_SHARPE_VAR_EMA_INDEX;
                    fused.trainer().write_isv_signal_at(VAL_SHARPE_VAR_EMA_INDEX, 0.0);
                }
            }
            "sp11_reward_component_mag_ratios" => {
                // Block-reset the 6 contiguous mag-ratio canaries at
                // ISV[REWARD_COMPONENT_MAG_RATIO_BASE=352..358). Matches
                // the `sp5_pnl_aggregation`/`sp5_health_composition`
                // block-reset pattern (one registry entry, one dispatch
                // arm, multi-slot zero-write).
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::{
                        REWARD_COMPONENT_MAG_RATIO_BASE, REWARD_COMPONENT_MAG_RATIO_COUNT,
                    };
                    for i in 0..REWARD_COMPONENT_MAG_RATIO_COUNT {
                        fused.trainer().write_isv_signal_at(
                            REWARD_COMPONENT_MAG_RATIO_BASE + i,
                            0.0,
                        );
                    }
                }
            }
            "sp11_saboteur_engagement_rate" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::SABOTEUR_ENGAGEMENT_RATE_INDEX;
                    fused.trainer().write_isv_signal_at(SABOTEUR_ENGAGEMENT_RATE_INDEX, 0.0);
                }
            }
            "sp11_pnl_reward_magnitude_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::PNL_REWARD_MAGNITUDE_EMA_INDEX;
                    fused.trainer().write_isv_signal_at(PNL_REWARD_MAGNITUDE_EMA_INDEX, 0.0);
                }
            }
            // SP11 Fix 39 B1b fix-up (2026-05-04): popart-component-specific
            // magnitude EMA. Pearl A sentinel-bootstrap fires on the first
            // popart_component_ema_kernel launch of the new fold; the per-
            // bar buffer accumulates fresh popart magnitudes from the new
            // fold's voluntary-exit bars, mean(|x|) feeds Pearls A+D, slot
            // 360 transitions from sentinel 0 → first-observation value.
            // Companion Wiener-state slot for the offset
            // (SP4_PRODUCER_COUNT*3 + (360-SP5_SLOT_BASE)*3) is reset by
            // the existing bulk memset of `wiener_state_buf` covered by
            // the `sp5_wiener_state` registry entry's dispatch arm. Spec
            // §4 amendment "Why slot 360".
            "sp11_popart_component_mag_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::POPART_COMPONENT_MAG_EMA_INDEX;
                    fused.trainer().write_isv_signal_at(POPART_COMPONENT_MAG_EMA_INDEX, 0.0);
                }
            }
            // SP11 Fix 39 B1b smoke-recovery (2026-05-04, spec §4 amendment
            // 'Why z-score' lines 564-619): per-reward-component variance
            // EMAs at ISV[REWARD_COMPONENT_VAR_EMA_BASE=361..367). Block-
            // reset all 6 slots so the new fold's first popart_component_ema
            // and reward_component_ema launches see Pearl A's sentinel and
            // first-observation-replace the variance from the new fold's
            // reward-component samples (paired with the magnitude EMA's
            // bootstrap path). Companion Wiener-state for offsets
            // (SP4_PRODUCER_COUNT*3 + (361..367 - SP5_SLOT_BASE)*3) is
            // reset by the bulk memset of `wiener_state_buf` in the
            // `sp5_wiener_state` registry entry's dispatch arm — both halves
            // of the Pearl A sentinel contract reset together per
            // `feedback_no_partial_refactor.md`.
            "sp11_reward_component_var_emas" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::{
                        REWARD_COMPONENT_VAR_EMA_BASE, REWARD_COMPONENT_VAR_EMA_COUNT,
                    };
                    for i in 0..REWARD_COMPONENT_VAR_EMA_COUNT {
                        fused.trainer().write_isv_signal_at(
                            REWARD_COMPONENT_VAR_EMA_BASE + i,
                            0.0,
                        );
                    }
                }
            }
            // SP13 (2026-05-04) Phase 0a: directional-accuracy EMAs.
            // Sentinel 0.5 (NOT 0.0) is the random-guessing baseline for
            // binary directional accuracy. Pearl A's first-observation
            // replacement fires on the first epoch of the new fold so the
            // EMA tracks the new fold's signal without bias from the
            // previous fold's terminal value. Companion Wiener-state slots
            // (offsets in `wiener_state_buf`) are reset by the existing bulk
            // memset covered by the `sp5_wiener_state` registry entry's
            // dispatch arm — both halves of the Pearl A sentinel contract
            // reset together per `feedback_no_partial_refactor.md`.
            "sp13_aux_dir_acc_short_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::{
                        AUX_DIR_ACC_SHORT_EMA_INDEX, DIR_ACC_EMA_SENTINEL,
                    };
                    fused.trainer().write_isv_signal_at(
                        AUX_DIR_ACC_SHORT_EMA_INDEX,
                        DIR_ACC_EMA_SENTINEL,
                    );
                }
            }
            "sp13_aux_dir_acc_long_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::{
                        AUX_DIR_ACC_LONG_EMA_INDEX, DIR_ACC_EMA_SENTINEL,
                    };
                    fused.trainer().write_isv_signal_at(
                        AUX_DIR_ACC_LONG_EMA_INDEX,
                        DIR_ACC_EMA_SENTINEL,
                    );
                }
            }
            // SP13 v3 P0a.T3 (2026-05-04): Hold-pricing observed-rate EMA.
            // Sentinel 0.0 — no Hold-pick observations at fold start. The
            // companion wiener-state slot (offset = SP4_PRODUCER_COUNT × 3
            // + (382 − SP5_SLOT_BASE) × 3) resets in the existing bulk
            // memset covered by the `sp5_wiener_state` registry entry's
            // dispatch arm; both halves of the Pearl A sentinel contract
            // reset together per `feedback_no_partial_refactor.md`.
            "sp13_hold_rate_observed_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::HOLD_RATE_OBSERVED_EMA_INDEX;
                    fused.trainer().write_isv_signal_at(
                        HOLD_RATE_OBSERVED_EMA_INDEX,
                        0.0,
                    );
                }
            }
            // SP14 (2026-05-05): EGF pearl fold-reset arms — 11 slots.
            // Slots 392 (ALPHA_GRAD_RAW) and 395 (GRADIENT_HACK_LOCKOUT_REMAINING)
            // are intentionally absent: raw is recomputed every step from the
            // three Welford variance EMAs; lockout decays at epoch boundary and
            // must NOT be force-reset mid-fold.
            "sp14_q_disagreement_short_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        Q_DISAGREEMENT_SHORT_EMA_INDEX, SENTINEL_Q_DISAGREEMENT,
                    };
                    fused.trainer().write_isv_signal_at(
                        Q_DISAGREEMENT_SHORT_EMA_INDEX,
                        SENTINEL_Q_DISAGREEMENT,
                    );
                }
            }
            "sp14_q_disagreement_long_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        Q_DISAGREEMENT_LONG_EMA_INDEX, SENTINEL_Q_DISAGREEMENT,
                    };
                    fused.trainer().write_isv_signal_at(
                        Q_DISAGREEMENT_LONG_EMA_INDEX,
                        SENTINEL_Q_DISAGREEMENT,
                    );
                }
            }
            "sp14_k_aux_adaptive" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        K_AUX_ADAPTIVE_INDEX, K_BASE_AUX,
                    };
                    fused.trainer().write_isv_signal_at(
                        K_AUX_ADAPTIVE_INDEX,
                        K_BASE_AUX,
                    );
                }
            }
            "sp14_k_q_adaptive" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        K_Q_ADAPTIVE_INDEX, K_BASE_Q,
                    };
                    fused.trainer().write_isv_signal_at(
                        K_Q_ADAPTIVE_INDEX,
                        K_BASE_Q,
                    );
                }
            }
            "sp14_beta_rate_limiter_adaptive" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        BETA_RATE_LIMITER_ADAPTIVE_INDEX, BETA_BASE,
                    };
                    fused.trainer().write_isv_signal_at(
                        BETA_RATE_LIMITER_ADAPTIVE_INDEX,
                        BETA_BASE,
                    );
                }
            }
            "sp14_aux_dir_acc_variance_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        AUX_DIR_ACC_VARIANCE_EMA_INDEX, SENTINEL_VARIANCE,
                    };
                    fused.trainer().write_isv_signal_at(
                        AUX_DIR_ACC_VARIANCE_EMA_INDEX,
                        SENTINEL_VARIANCE,
                    );
                }
            }
            "sp14_q_disagreement_variance_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        Q_DISAGREEMENT_VARIANCE_EMA_INDEX, SENTINEL_VARIANCE,
                    };
                    fused.trainer().write_isv_signal_at(
                        Q_DISAGREEMENT_VARIANCE_EMA_INDEX,
                        SENTINEL_VARIANCE,
                    );
                }
            }
            "sp14_alpha_grad_raw_variance_ema" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        ALPHA_GRAD_RAW_VARIANCE_EMA_INDEX, SENTINEL_VARIANCE,
                    };
                    fused.trainer().write_isv_signal_at(
                        ALPHA_GRAD_RAW_VARIANCE_EMA_INDEX,
                        SENTINEL_VARIANCE,
                    );
                }
            }
            "sp14_gate1_open_state" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        GATE1_OPEN_STATE_INDEX, SENTINEL_GATE1_CLOSED,
                    };
                    fused.trainer().write_isv_signal_at(
                        GATE1_OPEN_STATE_INDEX,
                        SENTINEL_GATE1_CLOSED,
                    );
                }
            }
            "sp14_alpha_grad_smoothed" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        ALPHA_GRAD_SMOOTHED_INDEX, SENTINEL_ALPHA_GRAD,
                    };
                    fused.trainer().write_isv_signal_at(
                        ALPHA_GRAD_SMOOTHED_INDEX,
                        SENTINEL_ALPHA_GRAD,
                    );
                }
            }
            "sp14_aux_dir_acc_post_open_min" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp14_isv_slots::{
                        AUX_DIR_ACC_POST_OPEN_MIN_INDEX, SENTINEL_POST_OPEN_MIN,
                    };
                    fused.trainer().write_isv_signal_at(
                        AUX_DIR_ACC_POST_OPEN_MIN_INDEX,
                        SENTINEL_POST_OPEN_MIN,
                    );
                }
            }
            // SP15 Phase 1.2 (2026-05-06): cost-net sharpe slots.
            // OFI_IMPACT_LAMBDA_INDEX=407 is an Invariant-1 anchor (NOT
            // a stateful EMA) — rewrite the constructor's value at fold
            // boundary so the cost kernel functions correctly on the new
            // fold's first launch (must never reach sentinel 0, which
            // would silently disable the OFI-impact term). Per
            // `feedback_isv_for_adaptive_bounds.md`. Per-fold ISV refit
            // may overwrite this in later phases.
            "sp15_ofi_impact_lambda" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::OFI_IMPACT_LAMBDA_INDEX;
                    fused.trainer().write_isv_signal_at(
                        OFI_IMPACT_LAMBDA_INDEX,
                        2.0e-4,
                    );
                }
            }
            // COST_PER_BAR_AVG_INDEX=408 is a stateful kernel output —
            // FoldReset sentinel 0 so Pearl A bootstraps from the new
            // fold's first `launch_sp15_cost_net_sharpe` per
            // `pearl_first_observation_bootstrap.md`.
            "sp15_cost_per_bar_avg" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::COST_PER_BAR_AVG_INDEX;
                    fused.trainer().write_isv_signal_at(COST_PER_BAR_AVG_INDEX, 0.0);
                }
            }
            // SP15 Phase 1.3 (2026-05-06): drawdown reporting slots [401..407).
            // Five sentinel-0 stateful outputs and one Invariant-1 floor
            // (calmar slot 405 stores `max(dd_max, 1e-4)` — sentinel 0 would
            // yield ±inf at the host composer's `mean_pnl / value-here`
            // division before the first kernel launch overwrites the slot).
            // Per spec §6.3. Reads PS_PEAK_EQUITY (slot 7) + PS_PREV_EQUITY
            // (slot 9) from the existing position state buffer — NO new ISV
            // equity slot (plan v2 critical review).
            "sp15_dd_current" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::DD_CURRENT_INDEX;
                    fused.trainer().write_isv_signal_at(DD_CURRENT_INDEX, 0.0);
                }
            }
            "sp15_dd_max" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::DD_MAX_INDEX;
                    fused.trainer().write_isv_signal_at(DD_MAX_INDEX, 0.0);
                }
            }
            "sp15_dd_recovery_bars" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::DD_RECOVERY_BARS_INDEX;
                    fused.trainer().write_isv_signal_at(DD_RECOVERY_BARS_INDEX, 0.0);
                }
            }
            "sp15_dd_persistence" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::DD_PERSISTENCE_INDEX;
                    fused.trainer().write_isv_signal_at(DD_PERSISTENCE_INDEX, 0.0);
                }
            }
            "sp15_calmar" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::CALMAR_INDEX;
                    // Sentinel 1e-4 (NOT 0) — the kernel's calmar floor
                    // doubles as the cold-start denominator so the very
                    // first calmar division at fold start uses the floor
                    // rather than ±inf.
                    fused.trainer().write_isv_signal_at(CALMAR_INDEX, 1.0e-4);
                }
            }
            "sp15_dd_pct" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::DD_PCT_INDEX;
                    fused.trainer().write_isv_signal_at(DD_PCT_INDEX, 0.0);
                }
            }
            // SP15 Phase 3.1 (2026-05-06): r_quality + r_discipline split
            // dispatch arms (per spec §8.2 (3.1) post-amendment-2 fix).
            // ALPHA_SPLIT_INDEX=417 is an Invariant-1 anchor (NOT a stateful
            // EMA) — rewrite the constructor's 0.5 sentinel at fold boundary
            // so the composer's cold-start absorber re-engages on each new
            // fold; deriving from `grad_norm_q / (grad_norm_q + grad_norm_d
            // + ε)` at boot would produce 0/0=0 from zero grad-norm EMAs.
            // Per `feedback_isv_for_adaptive_bounds.md`. Per-fold ISV refit
            // may overwrite this in later phases.
            "sp15_alpha_split" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::ALPHA_SPLIT_INDEX;
                    fused.trainer().write_isv_signal_at(ALPHA_SPLIT_INDEX, 0.5);
                }
            }
            // GRAD_NORM_QUALITY_INDEX=418 / GRAD_NORM_DISCIPLINE_INDEX=419
            // are stateful EMAs — FoldReset sentinel 0 so Pearl A
            // bootstraps from the new fold's first observation per
            // `pearl_first_observation_bootstrap.md`. The companion
            // `sp15_alpha_warm_count` reset re-arms the warm-up gate so
            // the producer kernel re-enters no-op state until both EMAs
            // accumulate ≥100 non-zero observations on the new fold.
            "sp15_grad_norm_quality" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::GRAD_NORM_QUALITY_INDEX;
                    fused.trainer().write_isv_signal_at(GRAD_NORM_QUALITY_INDEX, 0.0);
                }
            }
            "sp15_grad_norm_discipline" => {
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp15_isv_slots::GRAD_NORM_DISCIPLINE_INDEX;
                    fused.trainer().write_isv_signal_at(GRAD_NORM_DISCIPLINE_INDEX, 0.0);
                }
            }
            // sp15_alpha_warm_count is a non-ISV mapped-pinned scratch
            // buffer on the trainer struct. Reset to 0.0 via
            // `host_slice_mut().fill(0.0)` (allowed under
            // `feedback_no_htod_htoh_only_mapped_pinned.md`'s allowed-write
            // rule, NOT a host-side compute) — mirrors the
            // `sp11_novelty_hash` pattern of resetting a non-ISV
            // mapped-pinned buffer at fold boundary. The new fold's first
            // composer launch must re-enter the warm-up window from step 0
            // so the cold-start absorber holds α at 0.5 long enough for
            // the new fold's grad-norm EMAs to accumulate.
            "sp15_alpha_warm_count" => {
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.trainer_mut().sp15_alpha_warm_count.host_slice_mut().fill(0.0);
                }
            }
            // SP11 Task A2 (2026-05-04): novelty visit-count hash table
            // reset arm — closes the A0 deferral per the registry entry's
            // docstring. 1M f32 slots, zero-filled via mapped-pinned host
            // slice (CPU writes a literal zero — allowed under
            // `feedback_no_htod_htoh_only_mapped_pinned.md`'s allowed-write
            // rule, NOT a host-side compute).
            //
            // The companion projection matrix `novelty_simhash_proj` is
            // NOT reset here — it's the hash function (frozen at trainer
            // init from `SP11_PROJ_SEED_SALT`) and must outlive every
            // fold for reproducibility. See the registry entry header.
            "sp11_novelty_hash" => {
                if let Some(ref mut fused) = self.fused_ctx {
                    fused.trainer_mut().novelty_hash_buf.host_slice_mut().fill(0.0);
                }
            }
            "sp5_pnl_aggregation" => {
                // Layer D Task D1: zero ISV[286..290) — total/mean/var/max_dd —
                // so the new fold's first `launch_sp5_pnl_aggregation` triggers
                // Pearl A's first-observation replacement (per `pearl_first_observation_bootstrap.md`).
                // Wiener-state companion is reset by the existing bulk memset
                // of `wiener_state_buf[0..SP5_WIENER_TOTAL_FLOATS=561)` covered
                // by the `sp4_wiener_state` registry entry's dispatch arm.
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::{
                        PNL_TOTAL_INDEX, PNL_MEAN_INDEX, PNL_VAR_INDEX, PNL_MAX_DD_INDEX,
                    };
                    fused.trainer().write_isv_signal_at(PNL_TOTAL_INDEX, 0.0);
                    fused.trainer().write_isv_signal_at(PNL_MEAN_INDEX, 0.0);
                    fused.trainer().write_isv_signal_at(PNL_VAR_INDEX, 0.0);
                    fused.trainer().write_isv_signal_at(PNL_MAX_DD_INDEX, 0.0);
                }
            }
            "sp5_health_composition" => {
                // Layer D Task D2: zero ISV[290..294) — composed health score +
                // 3 normalised intermediates (q_gap_norm, q_var_norm,
                // grad_norm_norm = grad_stable) — so the new fold's first
                // `launch_health_composition` triggers Pearl A's
                // first-observation replacement (per `pearl_first_observation_bootstrap.md`).
                // Wiener-state companion is reset by the existing bulk memset
                // of `wiener_state_buf[0..SP5_WIENER_TOTAL_FLOATS=582)` covered
                // by the `sp4_wiener_state` registry entry's dispatch arm.
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::{
                        HEALTH_SCORE_INDEX, Q_GAP_NORM_INDEX, Q_VAR_NORM_INDEX, GRAD_NORM_NORM_INDEX,
                    };
                    fused.trainer().write_isv_signal_at(HEALTH_SCORE_INDEX, 0.0);
                    fused.trainer().write_isv_signal_at(Q_GAP_NORM_INDEX, 0.0);
                    fused.trainer().write_isv_signal_at(Q_VAR_NORM_INDEX, 0.0);
                    fused.trainer().write_isv_signal_at(GRAD_NORM_NORM_INDEX, 0.0);
                }
            }
            "sp5_training_metrics_ema" => {
                // Layer D Task D3: zero ISV[294..297) — training_sharpe_ema /
                // max_dd_ema / low_dd_ratio — so the new fold's first
                // `launch_training_metrics_ema` triggers Pearl A's
                // first-observation replacement (per `pearl_first_observation_bootstrap.md`).
                // Wiener-state companion is reset by the existing bulk memset
                // of `wiener_state_buf[0..SP5_WIENER_TOTAL_FLOATS=582)` covered
                // by the `sp4_wiener_state` registry entry's dispatch arm.
                // The host-side scalar companions (`self.training_sharpe_ema` +
                // `self.training_sharpe_ema_initialized`, `self.max_dd_ema`,
                // `self.low_dd_ratio`) reset alongside as part of the host-side
                // per-fold logic — D4's atomic call-site refactor will tie
                // the host-side reset into this dispatch arm directly. In
                // additive D3, only the ISV slots reset here.
                if let Some(ref fused) = self.fused_ctx {
                    use crate::cuda_pipeline::sp5_isv_slots::{
                        TRAINING_SHARPE_EMA_INDEX, MAX_DD_EMA_INDEX, LOW_DD_RATIO_INDEX,
                    };
                    fused.trainer().write_isv_signal_at(TRAINING_SHARPE_EMA_INDEX, 0.0);
                    fused.trainer().write_isv_signal_at(MAX_DD_EMA_INDEX, 0.0);
                    fused.trainer().write_isv_signal_at(LOW_DD_RATIO_INDEX, 0.0);
                }
            }
            _ => {
                return Err(crate::MLError::ModelError(format!(
                    "StateResetRegistry reset dispatch: unknown name '{}'. \
                     Every FoldReset/SoftReset entry must have a dispatch arm in reset_named_state.",
                    name
                )));
            }
        }
        tracing::debug!(
            target: "state_reset_registry",
            fold = fold_idx,
            name = name,
            "fold-reset applied"
        );
        Ok(())
    }

    /// Plan 5 Task 2 — collect the per-epoch HEALTH_DIAG metrics that the
    /// regression-detection registry should evaluate.
    ///
    /// Returns a `Vec<(metric_name, value)>` whose names MUST match the keys
    /// in `config/metric-bands.toml` exactly. The set is deliberately a
    /// subset of the full HEALTH_DIAG output — only the values that are
    /// stable across the warmup → steady-state transition and that we have
    /// known-good baseline distributions for. Adding a new key here without
    /// also adding a band to the toml is harmless (the registry returns
    /// `None` for unknown keys).
    ///
    /// All values are read from `self` / `log_output` directly — no GPU
    /// readback is performed (the values were already harvested by
    /// `log_epoch_metrics_and_financials` and the HEALTH_DIAG block above).
    pub(crate) fn harvest_health_diag_metrics(
        &self,
        _epoch: usize,
        log_output: &EpochLogOutput,
    ) -> Vec<(String, f64)> {
        let mut out: Vec<(String, f64)> = Vec::with_capacity(16);

        // ── Core training metrics (load-bearing for stability) ──
        out.push(("train_loss".to_owned(), log_output.avg_loss));
        out.push(("avg_q_value".to_owned(), log_output.avg_q_value));
        out.push(("avg_grad_norm".to_owned(), log_output.avg_grad_norm));
        out.push(("train_sharpe".to_owned(), log_output.epoch_sharpe));
        // val_loss is `-sharpe` per the existing convention; surface as
        // `val_sharpe` for the band file (more intuitive band semantics).
        out.push(("val_sharpe".to_owned(), -log_output.val_loss));
        out.push(("q_min".to_owned(), log_output.q_min));
        out.push(("q_max".to_owned(), log_output.q_max));
        out.push(("q_mean".to_owned(), log_output.q_mean));

        // ── Effective controller outputs (cached during HEALTH_DIAG block) ──
        if let Some(v) = self.last_cql_alpha_eff {
            out.push(("cql_alpha_eff".to_owned(), v as f64));
        }
        if let Some(v) = self.last_iqn_budget_eff {
            out.push(("iqn_budget_eff".to_owned(), v as f64));
        }
        if let Some(v) = self.last_c51_budget_eff {
            out.push(("c51_budget_eff".to_owned(), v as f64));
        }
        if let Some(v) = self.last_tau_eff {
            out.push(("tau_eff".to_owned(), v as f64));
        }
        if let Some(v) = self.last_gamma_eff {
            out.push(("gamma_eff".to_owned(), v as f64));
        }

        // ── Diagnostic signals from HEALTH_DIAG diag/explore blocks ──
        out.push(("training_sharpe_ema".to_owned(), self.training_sharpe_ema as f64));
        if let Some(v) = self.last_action_entropy {
            out.push(("action_entropy".to_owned(), v as f64));
        }

        // ── ISV slot EMAs that have controllers we want to band-monitor ──
        // Read once per epoch via the cold-path ISV accessor; matches the
        // values logged in HEALTH_DIAG.
        if let Some(ref fused) = self.fused_ctx {
            use crate::cuda_pipeline::gpu_dqn_trainer::{
                AUX_NEXT_BAR_MSE_EMA_INDEX, AUX_REGIME_CE_EMA_INDEX,
                REWARD_POPART_EMA_INDEX, REWARD_CF_EMA_INDEX,
                REWARD_TRAIL_EMA_INDEX, REWARD_MICRO_EMA_INDEX,
            };
            let trainer = fused.trainer();
            out.push((
                "aux_next_bar_mse_ema".to_owned(),
                trainer.read_isv_signal_at(AUX_NEXT_BAR_MSE_EMA_INDEX) as f64,
            ));
            out.push((
                "aux_regime_ce_ema".to_owned(),
                trainer.read_isv_signal_at(AUX_REGIME_CE_EMA_INDEX) as f64,
            ));
            out.push((
                "isv_reward_popart_ema".to_owned(),
                trainer.read_isv_signal_at(REWARD_POPART_EMA_INDEX) as f64,
            ));
            out.push((
                "isv_reward_cf_ema".to_owned(),
                trainer.read_isv_signal_at(REWARD_CF_EMA_INDEX) as f64,
            ));
            out.push((
                "isv_reward_trail_ema".to_owned(),
                trainer.read_isv_signal_at(REWARD_TRAIL_EMA_INDEX) as f64,
            ));
            out.push((
                "isv_reward_micro_ema".to_owned(),
                trainer.read_isv_signal_at(REWARD_MICRO_EMA_INDEX) as f64,
            ));
        }

        out
    }
}

