//! Phase E.1 GPU kernel launchers (Task 11, 2026-05-15).
//!
//! Rust-side launchers for the two CUDA kernels compiled by `build.rs`:
//!
//! - `alpha_kill_criteria_compute_kernel` (`alpha_kill_criteria.cu`) —
//!   writes 4 raw diagnostic observations (Q-spread, action entropy,
//!   return-vs-random, early-Q-movement) into scratch_out[0..4]. The caller
//!   is responsible for chaining `apply_pearls_ad_kernel` (n_slots=4)
//!   afterward to smooth into ISV slots 539..542. See
//!   [`super::alpha_isv_slots`] for the slot reservation.
//!
//! - `alpha_munchausen_target_kernel` (`alpha_munchausen_target.cu`) —
//!   Munchausen-augmented DQN target per Vieillard et al. 2020. One thread
//!   per batch sample; reads Q_online(s, ·) and Q_target(s', ·), writes
//!   `target_out[b] = r + α_m · max(τ · log π(a|s), log_clip_min) + γ · V_soft(s')`.
//!
//! These launchers follow the `launch_apply_pearls` pattern from
//! [`super::sp4_wiener_ema`] — they take the pre-loaded `CudaFunction` as
//! a parameter (module loading happens once at trainer construction). All
//! buffer arguments are raw `u64` device pointers; passing host or
//! mapped-pinned pointers will fault the kernel since both kernels
//! dereference directly in device address space.

#![allow(unsafe_code)] // CUDA kernel launch via cudarc requires unsafe

use crate::cuda_pipeline::alpha_isv_slots::{
    RANDOM_BASELINE_MEAN_INDEX, RANDOM_BASELINE_STD_INDEX,
};
use crate::MLError;

/// Launch `alpha_kill_criteria_compute_kernel` on `stream`.
///
/// The kernel reads `q_values[batch, n_actions]`, `action_counts[n_actions]`,
/// and `scalar_inputs[3]` (mapped-pinned: rollout_reward_mean, q_init_norm,
/// q_early_step_norm). It writes 4 raw observations into
/// `scratch_out[0..4]` and issues `__threadfence_system()` before returning.
///
/// Caller MUST chain `apply_pearls_ad_kernel` on the same stream after this
/// to smooth the 4 outputs into ISV slots 539..542. The slot indices for
/// `random_baseline_mean` (547) and `random_baseline_std` (548) are read
/// from `super::alpha_isv_slots` and passed as kernel args — the kernel
/// itself reads ISV at those slots to compute return_vs_random.
///
/// # Safety
///
/// All buffer pointers MUST be valid device pointers. `scratch_out_dev`
/// MUST point at a writable [4]-float region; passing a shorter buffer
/// will corrupt adjacent memory.
pub unsafe fn launch_alpha_kill_criteria(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    q_values_dev: u64,
    action_counts_dev: u64,
    scalar_inputs_dev: u64,
    isv_dev: u64,
    batch: i32,
    n_actions: i32,
    scratch_out_dev: u64,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(batch >= 0, "launch_alpha_kill_criteria: batch must be non-negative");
    debug_assert!(n_actions > 0, "launch_alpha_kill_criteria: n_actions must be positive");
    debug_assert!(q_values_dev != 0, "q_values_dev must be a valid device pointer");
    debug_assert!(action_counts_dev != 0, "action_counts_dev must be a valid device pointer");
    debug_assert!(scalar_inputs_dev != 0, "scalar_inputs_dev must be a valid device pointer");
    debug_assert!(isv_dev != 0, "isv_dev must be a valid device pointer");
    debug_assert!(scratch_out_dev != 0, "scratch_out_dev must be a valid device pointer");

    let cfg = LaunchConfig {
        grid_dim: (1, 1, 1),
        block_dim: (1, 1, 1),
        shared_mem_bytes: 0,
    };
    let baseline_mean_slot: i32 = RANDOM_BASELINE_MEAN_INDEX as i32;
    let baseline_std_slot: i32 = RANDOM_BASELINE_STD_INDEX as i32;
    stream
        .launch_builder(kernel)
        .arg(&q_values_dev)
        .arg(&action_counts_dev)
        .arg(&scalar_inputs_dev)
        .arg(&isv_dev)
        .arg(&baseline_mean_slot)
        .arg(&baseline_std_slot)
        .arg(&batch)
        .arg(&n_actions)
        .arg(&scratch_out_dev)
        .launch(cfg)
        .map_err(|e| {
            MLError::ModelError(format!("alpha_kill_criteria launch: {e}"))
        })?;
    Ok(())
}

/// Launch `alpha_linear_q_forward_kernel` on `stream`.
///
/// Computes `Q = X · W^T + b` for a batch of states. Grid: ceil(batch *
/// n_actions / 256); Block: 256.
///
/// # Safety
///
/// All buffer pointers MUST be valid device pointers. `q_dev` MUST point
/// at a writable `[batch * n_actions]`-float region.
pub unsafe fn launch_alpha_linear_q_forward(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    w_dev: u64,
    b_dev: u64,
    x_dev: u64,
    q_dev: u64,
    batch: i32,
    state_dim: i32,
    n_actions: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(batch > 0, "batch must be positive");
    debug_assert!(state_dim > 0, "state_dim must be positive");
    debug_assert!(n_actions > 0, "n_actions must be positive");

    const BLOCK: u32 = 256;
    let total = (batch as u32) * (n_actions as u32);
    let grid_x = (total + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&w_dev)
        .arg(&b_dev)
        .arg(&x_dev)
        .arg(&q_dev)
        .arg(&batch)
        .arg(&state_dim)
        .arg(&n_actions)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_linear_q_forward launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_linear_q_grad_kernel` on `stream`.
///
/// Computes sparse MSE-TD gradients `dW = scale · (Q − target) ⊗ X^T`
/// and `db = scale · (Q − target)`, restricted to the taken action per
/// sample.
///
/// # Safety
///
/// All buffer pointers MUST be valid device pointers. `dw_dev` MUST point
/// at a writable `[n_actions * state_dim]`-float region; `db_dev` at
/// `[n_actions]`.
pub unsafe fn launch_alpha_linear_q_grad(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    q_dev: u64,
    target_dev: u64,
    actions_dev: u64,
    x_dev: u64,
    dw_dev: u64,
    db_dev: u64,
    batch: i32,
    state_dim: i32,
    n_actions: i32,
    scale: f32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(batch > 0, "batch must be positive");
    debug_assert!(state_dim > 0, "state_dim must be positive");
    debug_assert!(n_actions > 0, "n_actions must be positive");

    const BLOCK: u32 = 256;
    let total = (n_actions as u32) * (state_dim as u32) + (n_actions as u32);
    let grid_x = (total + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&q_dev)
        .arg(&target_dev)
        .arg(&actions_dev)
        .arg(&x_dev)
        .arg(&dw_dev)
        .arg(&db_dev)
        .arg(&batch)
        .arg(&state_dim)
        .arg(&n_actions)
        .arg(&scale)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_linear_q_grad launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_linear_q_sgd_step_kernel` on `stream`.
///
/// Element-wise `params -= lr · grad`. Works for either the weight
/// matrix or the bias vector — caller passes `n = numel`.
///
/// # Safety
///
/// `params_dev` and `grad_dev` MUST be valid device pointers to buffers
/// of at least `n` floats. `params_dev` is modified in place.
pub unsafe fn launch_alpha_linear_q_sgd_step(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    params_dev: u64,
    grad_dev: u64,
    lr: f32,
    n: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(n > 0, "n must be positive");

    const BLOCK: u32 = 256;
    let grid_x = ((n as u32) + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&params_dev)
        .arg(&grad_dev)
        .arg(&lr)
        .arg(&n)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_linear_q_sgd_step launch: {e}")))?;
    Ok(())
}

/// Launch `stacker_threshold_controller_update` on `stream`.
///
/// Phase E.2 Task 16. Single-block, single-thread controller that runs
/// once per rollout-end boundary. Reads per-rollout scalars (trade
/// count, total decisions, realized Sharpe), updates ISV slots 543
/// (threshold), 545 (observed-rate EMA), 546 (Kelly attenuation) via
/// engagement-rate self-correction (P-controller).
///
/// # Safety
///
/// `isv_dev` and `wiener_state_dev` MUST be valid device pointers.
/// `wiener_state_dev` MUST point at a `[3]`-float buffer
/// (`[sample_var, diff_var, x_lag]` for slot 545's Pearl D state).
pub unsafe fn launch_stacker_threshold_controller(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    rollout_trade_count: f32,
    rollout_total_decisions: f32,
    rollout_realized_sharpe: f32,
    target_sharpe: f32,
    k_threshold: f32,
    k_atten: f32,
    wiener_alpha_floor: f32,
    alpha_meta: f32,
    stacker_threshold_idx: i32,
    trade_rate_target_idx: i32,
    trade_rate_observed_idx: i32,
    stacker_kelly_atten_idx: i32,
    // T16 regime args. Pass -1 / -1 / any-float to disable the regime scale.
    regime_vol_ema_idx: i32,
    regime_vol_ref_idx: i32,
    regime_scale_floor: f32,
    // T16 floor-controller args. Pass -1 / -1 to disable the floor update.
    regime_vol_ref_floor_idx: i32,
    regime_vol_obs_min_idx: i32,
    floor_target_ratio: f32,
    floor_update_rate: f32,
    vol_obs_min_sentinel: f32,
    isv_dev: u64,
    wiener_state_dev: u64,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(isv_dev != 0, "isv_dev must be a valid device pointer");
    debug_assert!(wiener_state_dev != 0, "wiener_state_dev must be a valid device pointer");
    debug_assert!(wiener_alpha_floor >= 0.0 && wiener_alpha_floor < 1.0,
        "wiener_alpha_floor must be in [0, 1)");
    debug_assert!(alpha_meta > 0.0 && alpha_meta < 1.0,
        "alpha_meta must be in (0, 1)");
    debug_assert!(regime_scale_floor > 0.0 && regime_scale_floor <= 1.0,
        "regime_scale_floor must be in (0, 1]");

    let cfg = LaunchConfig {
        grid_dim: (1, 1, 1),
        block_dim: (1, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&rollout_trade_count)
        .arg(&rollout_total_decisions)
        .arg(&rollout_realized_sharpe)
        .arg(&target_sharpe)
        .arg(&k_threshold)
        .arg(&k_atten)
        .arg(&wiener_alpha_floor)
        .arg(&alpha_meta)
        .arg(&stacker_threshold_idx)
        .arg(&trade_rate_target_idx)
        .arg(&trade_rate_observed_idx)
        .arg(&stacker_kelly_atten_idx)
        .arg(&regime_vol_ema_idx)
        .arg(&regime_vol_ref_idx)
        .arg(&regime_scale_floor)
        .arg(&regime_vol_ref_floor_idx)
        .arg(&regime_vol_obs_min_idx)
        .arg(&floor_target_ratio)
        .arg(&floor_update_rate)
        .arg(&vol_obs_min_sentinel)
        .arg(&isv_dev)
        .arg(&wiener_state_dev)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("stacker_threshold_controller launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_clip_inplace_kernel` on `stream`. Element-wise
/// clamp(x, -bound, +bound) for gradient clipping or activation
/// stabilization.
///
/// # Safety
///
/// `buf_dev` MUST be a valid device pointer to `n` floats. Modified in place.
pub unsafe fn launch_alpha_clip_inplace(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    buf_dev: u64,
    bound: f32,
    n: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(n > 0, "n must be positive");
    debug_assert!(bound > 0.0, "bound must be positive");

    const BLOCK: u32 = 256;
    let grid_x = ((n as u32) + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&buf_dev)
        .arg(&bound)
        .arg(&n)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_clip_inplace launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_munchausen_target_kernel` on `stream`.
///
/// Computes the Munchausen-augmented target for each batch sample:
///   `target[i] = r[i] + α_m · max(τ · log π(a|s), log_clip_min) + γ · V_soft(s')`
/// (or `r + m` if `dones[i] >= 0.5`).
///
/// Grid configuration: one thread per batch sample, block_dim=256. This
/// matches the existing target/loss kernels' parallelism pattern.
///
/// `α_m`, `τ`, `log_clip_min` are passed as scalar args so a controller
/// can ISV-drive them per phase. Typical Vieillard et al. values:
/// α_m=0.9, τ=0.03, log_clip_min=-1.0.
///
/// # Safety
///
/// All buffer pointers MUST be valid device pointers. `target_out_dev` MUST
/// point at a writable [batch]-float region.
pub unsafe fn launch_alpha_munchausen_target(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    q_next_dev: u64,
    q_current_dev: u64,
    actions_dev: u64,
    rewards_dev: u64,
    dones_dev: u64,
    gamma: f32,
    alpha_m: f32,
    tau: f32,
    log_clip_min: f32,
    target_out_dev: u64,
    batch: i32,
    n_actions: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(batch > 0, "launch_alpha_munchausen_target: batch must be positive");
    debug_assert!(n_actions > 0, "launch_alpha_munchausen_target: n_actions must be positive");
    debug_assert!(tau > 0.0, "tau must be positive (Boltzmann temperature)");
    debug_assert!(gamma >= 0.0 && gamma <= 1.0, "gamma must be in [0, 1]");
    debug_assert!(alpha_m >= 0.0, "alpha_m must be non-negative");
    debug_assert!(q_next_dev != 0, "q_next_dev must be a valid device pointer");
    debug_assert!(q_current_dev != 0, "q_current_dev must be a valid device pointer");
    debug_assert!(actions_dev != 0, "actions_dev must be a valid device pointer");
    debug_assert!(rewards_dev != 0, "rewards_dev must be a valid device pointer");
    debug_assert!(dones_dev != 0, "dones_dev must be a valid device pointer");
    debug_assert!(target_out_dev != 0, "target_out_dev must be a valid device pointer");

    const BLOCK: u32 = 256;
    let grid_x = ((batch as u32).max(1) + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x, 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&q_next_dev)
        .arg(&q_current_dev)
        .arg(&actions_dev)
        .arg(&rewards_dev)
        .arg(&dones_dev)
        .arg(&gamma)
        .arg(&alpha_m)
        .arg(&tau)
        .arg(&log_clip_min)
        .arg(&target_out_dev)
        .arg(&batch)
        .arg(&n_actions)
        .launch(cfg)
        .map_err(|e| {
            MLError::ModelError(format!("alpha_munchausen_target launch: {e}"))
        })?;
    Ok(())
}

/// Precompiled Munchausen target cubin, embedded at compile time by build.rs.
static ALPHA_MUNCHAUSEN_TARGET_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/alpha_munchausen_target.cubin"));

/// Precompiled kill-criteria producer cubin, embedded at compile time by build.rs.
static ALPHA_KILL_CRITERIA_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/alpha_kill_criteria.cubin"));

/// Precompiled canonical Pearls A+D applicator cubin (test-only access; the
/// production path loads this through the trainer's module init).
#[cfg(test)]
static APPLY_PEARLS_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/apply_pearls_kernel.cubin"));

/// Precompiled linear Q-network kernel cubin (forward / grad / SGD-step).
/// Loaded by the Phase E.1 H=600 DQN smoke (Task 12).
pub static ALPHA_LINEAR_Q_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/alpha_linear_q.cubin"));

/// Precompiled stacker-threshold + Kelly-attenuation controller cubin.
/// Phase E.2 Task 16. Loaded once at trainer startup; launched at each
/// rollout-end boundary.
pub static STACKER_THRESHOLD_CONTROLLER_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/stacker_threshold_controller.cubin"));

/// Precompiled vanilla C51 distributional Q cubin. Phase E.3 follow-up
/// (2026-05-15). Three kernels: forward (logits → softmax across atoms),
/// Bellman categorical projection, CE gradient.
pub static ALPHA_C51_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/alpha_c51.cubin"));

/// Precompiled circular-buffer push kernel for the sliding-window state
/// input to Mamba2 temporal encoder. Phase E.4.A.6.
pub static ALPHA_WINDOW_PUSH_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/alpha_window_push.cubin"));

/// Precompiled vol-EMA regime detection kernel. Phase E.4.A.T16.
pub static ALPHA_REGIME_VOL_UPDATE_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/alpha_regime_vol_update.cubin"));

/// Launch `alpha_regime_vol_update_kernel`. Reads B current/previous
/// mids, computes the cross-env mean squared log-return, and updates
/// ISV slots 549 (vol_ema, Wiener-α with 0.4 floor + Pearl A bootstrap)
/// and 550 (vol_ref, slow-tracking reference). One block, ≤256 threads
/// — B is typically 50, fits easily.
///
/// # Safety
/// `mids_curr_dev`, `mids_prev_dev`, `isv_dev` must be valid device
/// pointers. The kernel does not atomicAdd and contains no host
/// branches inside its execution.
pub unsafe fn launch_alpha_regime_vol_update(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    mids_curr_dev: u64,
    mids_prev_dev: u64,
    isv_dev: u64,
    b: i32,
    vol_ema_index: i32,
    vol_ref_index: i32,
    vol_ref_samples_index: i32,
    vol_ref_floor_index: i32,
    vol_obs_min_index: i32,
    bootstrap_samples: i32,
    ref_track_rate: f32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};
    debug_assert!(b > 0 && b <= 256, "B must be in (0, 256]; got {b}");
    debug_assert!(bootstrap_samples > 0, "bootstrap_samples must be positive");
    let cfg = LaunchConfig {
        grid_dim: (1, 1, 1),
        block_dim: (256, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&mids_curr_dev)
        .arg(&mids_prev_dev)
        .arg(&isv_dev)
        .arg(&b)
        .arg(&vol_ema_index)
        .arg(&vol_ref_index)
        .arg(&vol_ref_samples_index)
        .arg(&vol_ref_floor_index)
        .arg(&vol_obs_min_index)
        .arg(&bootstrap_samples)
        .arg(&ref_track_rate)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_regime_vol_update launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_window_push_kernel`. Shifts the window's K slots left
/// by one (slot 1 → slot 0, slot 2 → slot 1, …, slot K-1 → slot K-2)
/// and inserts the new state at slot K-1. After the call,
/// `window[0..K-1, :]` is chronological (slot 0 = oldest, slot K-1 =
/// newest) — matches Mamba2Block's `[B, K, in_dim]` input contract.
///
/// One thread per state feature; each thread sequentially shifts its
/// column's K-1 slots and writes the new value at K-1. No data races
/// (each thread owns its column).
///
/// # Safety
/// `state_in_dev` and `window_dev` MUST be valid device pointers.
/// `window_dev` capacity ≥ `K * state_dim` floats.
pub unsafe fn launch_alpha_window_push(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    state_in_dev: u64,
    window_dev: u64,
    k: i32,
    state_dim: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(state_dim > 0, "state_dim must be positive");
    debug_assert!(k > 1, "K must be > 1 (need at least one shift)");

    const BLOCK: u32 = 32;
    let grid_x = ((state_dim as u32) + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&state_in_dev)
        .arg(&window_dev)
        .arg(&k)
        .arg(&state_dim)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_window_push launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_c51_forward_kernel`. One thread per (sample, action);
/// each thread sequentially computes n_atoms logits + softmax. Per-thread
/// scratch lives in local memory (MAX_N_ATOMS = 64 floats hardcoded in
/// the kernel — caller's `n_atoms` MUST be ≤ 64).
///
/// # Safety
/// All buffer pointers MUST be valid device pointers. `probs_dev` must
/// point at a writable `[batch * n_actions * n_atoms]`-float region.
pub unsafe fn launch_alpha_c51_forward(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    w_dev: u64,
    b_dev: u64,
    x_dev: u64,
    probs_dev: u64,
    batch: i32,
    state_dim: i32,
    n_actions: i32,
    n_atoms: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(batch > 0, "batch must be positive");
    debug_assert!(state_dim > 0, "state_dim must be positive");
    debug_assert!(n_actions > 0, "n_actions must be positive");
    debug_assert!(n_atoms > 0 && n_atoms <= 64, "n_atoms must be in (0, 64]");

    const BLOCK: u32 = 256;
    let total = (batch as u32) * (n_actions as u32);
    let grid_x = (total + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&w_dev)
        .arg(&b_dev)
        .arg(&x_dev)
        .arg(&probs_dev)
        .arg(&batch)
        .arg(&state_dim)
        .arg(&n_actions)
        .arg(&n_atoms)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_c51_forward launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_c51_project_kernel`. Categorical Bellman projection: for
/// each batch sample, computes greedy action under the target distribution
/// then projects the shifted/scaled support `r + γ·z` onto the fixed atom
/// grid. One thread per sample.
///
/// # Safety
/// All buffer pointers MUST be valid device pointers. `m_dev` must point
/// at a writable `[batch * n_atoms]`-float region — overwritten.
pub unsafe fn launch_alpha_c51_project(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    probs_next_dev: u64,
    rewards_dev: u64,
    dones_dev: u64,
    v_min: f32,
    v_max: f32,
    gamma: f32,
    delta_z: f32,
    m_dev: u64,
    batch: i32,
    n_actions: i32,
    n_atoms: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(batch > 0, "batch must be positive");
    debug_assert!(n_actions > 0, "n_actions must be positive");
    debug_assert!(n_atoms > 0 && n_atoms <= 64, "n_atoms must be in (0, 64]");
    debug_assert!(v_max > v_min, "v_max must exceed v_min");
    debug_assert!(delta_z > 0.0, "delta_z must be positive");
    debug_assert!((0.0..=1.0).contains(&gamma), "gamma must be in [0, 1]");

    const BLOCK: u32 = 256;
    let grid_x = ((batch as u32) + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&probs_next_dev)
        .arg(&rewards_dev)
        .arg(&dones_dev)
        .arg(&v_min)
        .arg(&v_max)
        .arg(&gamma)
        .arg(&delta_z)
        .arg(&m_dev)
        .arg(&batch)
        .arg(&n_actions)
        .arg(&n_atoms)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_c51_project launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_c51_expected_q_kernel`. Computes scalar E[Z(s,a)] from
/// the C51 categorical distribution; one thread per (sample, action).
/// Used to bridge C51 outputs to the legacy scalar-Q kill-criteria kernel.
///
/// # Safety
/// All buffer pointers MUST be valid device pointers. `q_out_dev` must
/// point at a writable `[batch * n_actions]`-float region.
pub unsafe fn launch_alpha_c51_expected_q(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    probs_dev: u64,
    v_min: f32,
    delta_z: f32,
    q_out_dev: u64,
    batch: i32,
    n_actions: i32,
    n_atoms: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(batch > 0, "batch must be positive");
    debug_assert!(n_actions > 0, "n_actions must be positive");
    debug_assert!(n_atoms > 0 && n_atoms <= 64, "n_atoms must be in (0, 64]");
    debug_assert!(delta_z > 0.0, "delta_z must be positive");

    const BLOCK: u32 = 256;
    let total = (batch as u32) * (n_actions as u32);
    let grid_x = (total + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&probs_dev)
        .arg(&v_min)
        .arg(&delta_z)
        .arg(&q_out_dev)
        .arg(&batch)
        .arg(&n_actions)
        .arg(&n_atoms)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_c51_expected_q launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_h_enriched_store_batched_kernel`. Copies a batch of
/// `B` h_enriched vectors of width `hidden_dim` into a destination
/// buffer at a step-aligned row offset. Used to build the training-
/// time h_enriched buffer of shape `[H+1, B, hidden_dim]` (step-outer).
///
/// # Safety
/// `src_dev` and `buf_dev` must be valid device pointers.
pub unsafe fn launch_alpha_h_enriched_store_batched(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    src_dev: u64,
    buf_dev: u64,
    step_row_offset: i32,
    b: i32,
    hidden_dim: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};
    debug_assert!(b > 0 && hidden_dim > 0);
    debug_assert!(step_row_offset >= 0);
    const BLOCK: u32 = 32;
    let grid_x = ((hidden_dim as u32) + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), b as u32, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&src_dev)
        .arg(&buf_dev)
        .arg(&step_row_offset)
        .arg(&b)
        .arg(&hidden_dim)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_h_enriched_store_batched launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_train_window_store_batched_kernel`. Copies a batch of
/// `B` windows of shape `[K, state_dim]` into a step-aligned slice of a
/// destination buffer of shape `[H*B, K, state_dim]`. Lets the training
/// loop re-run a single batched Mamba2 forward over all visited windows
/// (T10 backward chain).
///
/// # Safety
/// `src_dev` and `buf_dev` must be valid device pointers.
pub unsafe fn launch_alpha_train_window_store_batched(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    src_dev: u64,
    buf_dev: u64,
    step_row_offset: i32,
    b: i32,
    k: i32,
    state_dim: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};
    debug_assert!(b > 0 && k > 0 && state_dim > 0);
    debug_assert!(step_row_offset >= 0);
    const BLOCK: u32 = 32;
    let grid_x = ((state_dim as u32) + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), k as u32, b as u32),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&src_dev)
        .arg(&buf_dev)
        .arg(&step_row_offset)
        .arg(&b)
        .arg(&k)
        .arg(&state_dim)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_train_window_store_batched launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_window_push_batched_kernel`. Same chronological
/// shift+insert as `alpha_window_push_kernel` but across B parallel
/// windows. One thread per (batch, feature). Used by the batched-eval
/// backtest path where N=cli.n_eval_episodes environments run in
/// lockstep.
///
/// # Safety
/// `states_in_dev` and `windows_dev` MUST be valid device pointers.
/// `windows_dev` capacity ≥ `B * K * state_dim` floats.
pub unsafe fn launch_alpha_window_push_batched(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    states_in_dev: u64,
    windows_dev: u64,
    b: i32,
    k: i32,
    state_dim: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(b > 0, "B must be positive");
    debug_assert!(k > 1, "K must be > 1");
    debug_assert!(state_dim > 0, "state_dim positive");

    const BLOCK: u32 = 32;
    let grid_x = ((state_dim as u32) + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), b as u32, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&states_in_dev)
        .arg(&windows_dev)
        .arg(&b)
        .arg(&k)
        .arg(&state_dim)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_window_push_batched launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_h_enriched_store_kernel`. Copies `src[hidden_dim]`
/// (typically a Mamba2 cache.h_enriched device buffer) into
/// `buf[slot_offset..slot_offset+hidden_dim]`. Replaces the per-step
/// dtoh/htod sequence per `feedback_cpu_is_read_only`. Pure GPU.
///
/// # Safety
/// `src_dev` and `buf_dev` MUST be valid device pointers.
pub unsafe fn launch_alpha_h_enriched_store(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    src_dev: u64,
    buf_dev: u64,
    slot_offset: i32,
    hidden_dim: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(hidden_dim > 0, "hidden_dim must be positive");
    debug_assert!(slot_offset >= 0, "slot_offset must be non-negative");

    const BLOCK: u32 = 32;
    let grid_x = ((hidden_dim as u32) + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&src_dev)
        .arg(&buf_dev)
        .arg(&slot_offset)
        .arg(&hidden_dim)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_h_enriched_store launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_c51_grad_input_kernel`. Computes dL/d_input for the
/// C51 layer (gradient w.r.t. the upstream encoder's output).
/// Threads: one per (batch, input feature). Needed to chain C51's
/// gradient into Mamba2's backward pass in `--temporal`.
///
/// # Safety
/// All buffer pointers MUST be valid device pointers. `d_input_dev`
/// must point at a writable `[batch * state_dim]`-float region.
pub unsafe fn launch_alpha_c51_grad_input(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    probs_dev: u64,
    m_dev: u64,
    actions_dev: u64,
    w_dev: u64,
    d_input_dev: u64,
    batch: i32,
    state_dim: i32,
    n_actions: i32,
    n_atoms: i32,
    scale: f32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(batch > 0, "batch must be positive");
    debug_assert!(state_dim > 0, "state_dim must be positive");
    debug_assert!(n_actions > 0, "n_actions must be positive");
    debug_assert!(n_atoms > 0 && n_atoms <= 64, "n_atoms must be in (0, 64]");

    const BLOCK: u32 = 256;
    let total = (batch as u32) * (state_dim as u32);
    let grid_x = (total + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&probs_dev)
        .arg(&m_dev)
        .arg(&actions_dev)
        .arg(&w_dev)
        .arg(&d_input_dev)
        .arg(&batch)
        .arg(&state_dim)
        .arg(&n_actions)
        .arg(&n_atoms)
        .arg(&scale)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_c51_grad_input launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_c51_thompson_select_kernel`. GPU action selector with
/// confidence gate. Replaces CPU `epsilon_greedy_gated` per
/// `feedback_cpu_is_read_only`. One thread per batch sample; writes the
/// chosen action index into `out_action_dev`.
///
/// # Safety
/// All buffer pointers MUST be valid device pointers. `out_action_dev`
/// must point at a writable `[batch]`-int region — overwritten.
pub unsafe fn launch_alpha_c51_thompson_select(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    probs_dev: u64,
    states_dev: u64,
    threshold: f32,
    conf_idx: i32,
    state_dim: i32,
    v_min: f32,
    delta_z: f32,
    base_seed: u32,
    out_action_dev: u64,
    batch: i32,
    n_actions: i32,
    n_atoms: i32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(batch > 0, "batch must be positive");
    debug_assert!(state_dim > 0, "state_dim must be positive");
    debug_assert!(n_actions > 0, "n_actions must be positive");
    debug_assert!(n_atoms > 0 && n_atoms <= 64, "n_atoms must be in (0, 64]");
    debug_assert!(conf_idx >= 0 && conf_idx < state_dim, "conf_idx in range");
    debug_assert!(delta_z > 0.0, "delta_z must be positive");

    const BLOCK: u32 = 256;
    let grid_x = ((batch as u32) + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&probs_dev)
        .arg(&states_dev)
        .arg(&threshold)
        .arg(&conf_idx)
        .arg(&state_dim)
        .arg(&v_min)
        .arg(&delta_z)
        .arg(&base_seed)
        .arg(&out_action_dev)
        .arg(&batch)
        .arg(&n_actions)
        .arg(&n_atoms)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_c51_thompson_select launch: {e}")))?;
    Ok(())
}

/// Launch `alpha_c51_grad_kernel`. CE gradient: `dW[ak, j] += scale ·
/// 𝟙{a_taken[b]==a} · (p[b,a,k] − m[b,k]) · X[b,j]`. Flat-thread layout
/// over (`n_actions·n_atoms·state_dim` + `n_actions·n_atoms`) range.
///
/// # Safety
/// All buffer pointers MUST be valid device pointers. `dW_dev` must point
/// at a writable `[n_actions * n_atoms * state_dim]`-float region; `db_dev`
/// at `[n_actions * n_atoms]`. Both overwritten.
pub unsafe fn launch_alpha_c51_grad(
    stream: &cudarc::driver::CudaStream,
    kernel: &cudarc::driver::CudaFunction,
    probs_dev: u64,
    m_dev: u64,
    actions_dev: u64,
    x_dev: u64,
    dw_dev: u64,
    db_dev: u64,
    batch: i32,
    state_dim: i32,
    n_actions: i32,
    n_atoms: i32,
    scale: f32,
) -> Result<(), MLError> {
    use cudarc::driver::{LaunchConfig, PushKernelArg};

    debug_assert!(batch > 0, "batch must be positive");
    debug_assert!(state_dim > 0, "state_dim must be positive");
    debug_assert!(n_actions > 0, "n_actions must be positive");
    debug_assert!(n_atoms > 0 && n_atoms <= 64, "n_atoms must be in (0, 64]");

    const BLOCK: u32 = 256;
    let total_w = (n_actions as u32) * (n_atoms as u32) * (state_dim as u32);
    let total_b = (n_actions as u32) * (n_atoms as u32);
    let total = total_w + total_b;
    let grid_x = (total + BLOCK - 1) / BLOCK;
    let cfg = LaunchConfig {
        grid_dim: (grid_x.max(1), 1, 1),
        block_dim: (BLOCK, 1, 1),
        shared_mem_bytes: 0,
    };
    stream
        .launch_builder(kernel)
        .arg(&probs_dev)
        .arg(&m_dev)
        .arg(&actions_dev)
        .arg(&x_dev)
        .arg(&dw_dev)
        .arg(&db_dev)
        .arg(&batch)
        .arg(&state_dim)
        .arg(&n_actions)
        .arg(&n_atoms)
        .arg(&scale)
        .launch(cfg)
        .map_err(|e| MLError::ModelError(format!("alpha_c51_grad launch: {e}")))?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use cudarc::driver::{CudaContext, DevicePtr, DevicePtrMut};

    #[test]
    fn module_compiles_clean() {
        // Tautology — witnesses that the launchers + alpha_isv_slots are coherent.
        assert!(true);
    }

    /// GPU smoke for `alpha_window_push_kernel`: pushes three states
    /// in sequence into a 4-slot buffer and verifies chronological
    /// shift+insert: slot 0 = state_a (oldest, shifted left twice),
    /// slot 1 = state_b (oldest now), slot 2 = state_c (middle),
    /// slot 3 = state_c+1 (newest). Phase E.4.A.6 verification.
    #[test]
    fn alpha_window_push_shift_and_insert() -> Result<(), MLError> {
        let ctx = CudaContext::new(0).map_err(|e| MLError::ModelError(format!("ctx: {e}")))?;
        let stream = ctx.default_stream();
        let module = ctx
            .load_cubin(ALPHA_WINDOW_PUSH_CUBIN.to_vec())
            .map_err(|e| MLError::ModelError(format!("cubin: {e}")))?;
        let kernel = module
            .load_function("alpha_window_push_kernel")
            .map_err(|e| MLError::ModelError(format!("kfn: {e}")))?;

        const K: usize = 4;
        const D: usize = 3;
        let mut window_dev = stream.alloc_zeros::<f32>(K * D)
            .map_err(|e| MLError::ModelError(format!("alloc window: {e}")))?;

        let states: [Vec<f32>; 3] = [
            vec![1.0, 2.0, 3.0],
            vec![10.0, 20.0, 30.0],
            vec![100.0, 200.0, 300.0],
        ];

        for state in &states {
            let s_dev = stream.clone_htod(state)
                .map_err(|e| MLError::ModelError(format!("htod: {e}")))?;
            let (s_ptr, _g0) = s_dev.device_ptr(&stream);
            let (w_ptr, _g1) = window_dev.device_ptr_mut(&stream);
            unsafe {
                launch_alpha_window_push(&stream, &kernel, s_ptr, w_ptr, K as i32, D as i32)?;
            }
        }
        stream.synchronize().map_err(|e| MLError::ModelError(format!("sync: {e}")))?;
        let window_host = stream.clone_dtoh(&window_dev)
            .map_err(|e| MLError::ModelError(format!("dtoh: {e}")))?;
        // After 3 pushes into 4-slot buffer:
        //   slot 0 = zero (initial pre-shift state, shifted left twice)
        //   slot 1 = states[0]   (shifted from slot 3 → slot 2 → slot 1)
        //   slot 2 = states[1]   (shifted from slot 3 → slot 2)
        //   slot 3 = states[2]   (newest, just inserted)
        assert_eq!(&window_host[0..3], &[0.0, 0.0, 0.0], "slot 0 = initial zero shifted");
        assert_eq!(&window_host[3..6], &[1.0, 2.0, 3.0], "slot 1 = states[0]");
        assert_eq!(&window_host[6..9], &[10.0, 20.0, 30.0], "slot 2 = states[1]");
        assert_eq!(&window_host[9..12], &[100.0, 200.0, 300.0], "slot 3 = newest");
        Ok(())
    }

    /// End-to-end GPU smoke for the Munchausen target launcher.
    ///
    /// Loads the cubin, allocates synthetic inputs (batch=2, n_actions=3 with
    /// known-by-hand Q-spreads and a terminal flag on sample 1), invokes the
    /// kernel via `launch_alpha_munchausen_target`, and validates the two
    /// per-sample targets against hand-computed expected values.
    ///
    /// Sample 0 hand-math (Q_current = [1,2,3], a_taken=0, τ=1):
    ///   max(Q)=3; sum_exp = exp(-2)+exp(-1)+exp(0) = 0.1353+0.3679+1.0000 = 1.5032
    ///   τ·log π(0|s) = (1−3) − 1·log(1.5032) = −2 − 0.4076 = −2.4076
    ///   m = 0.9 · max(−2.4076, −1.0) = 0.9 · −1.0 = −0.9
    ///   V_soft(Q_next=[10,20,30]) = 30 + 1·log(exp(-20)+exp(-10)+1) ≈ 30.0
    ///   target = 1.0 + (−0.9) + 0.99 · 30.0 = 29.8
    ///
    /// Sample 1 (terminal, Q_current all zero):
    ///   π uniform = 1/3; τ·log π = log(1/3) = −1.0986
    ///   m = 0.9 · max(−1.0986, −1.0) = 0.9 · −1.0 = −0.9
    ///   V_soft = 0 (terminal)
    ///   target = 2.0 + (−0.9) + 0 = 1.1
    #[test]
    fn munchausen_target_smoke_matches_hand_computation() {
        let ctx = match CudaContext::new(0) {
            Ok(c) => c,
            Err(e) => {
                eprintln!("Skipping GPU smoke — no CUDA device 0: {e}");
                return;
            }
        };
        let stream = ctx.default_stream();

        let module = ctx
            .load_cubin(ALPHA_MUNCHAUSEN_TARGET_CUBIN.to_vec())
            .expect("munchausen cubin module load");
        let kernel = module
            .load_function("alpha_munchausen_target_kernel")
            .expect("munchausen kernel load");

        let batch: i32 = 2;
        let n_actions: i32 = 3;
        let q_current: Vec<f32> = vec![1.0, 2.0, 3.0, 0.0, 0.0, 0.0];
        let q_next: Vec<f32> = vec![10.0, 20.0, 30.0, 0.0, 0.0, 0.0];
        let actions: Vec<i32> = vec![0, 1];
        let rewards: Vec<f32> = vec![1.0, 2.0];
        let dones: Vec<f32> = vec![0.0, 1.0];

        let q_current_dev = stream.clone_htod(&q_current).expect("upload q_current");
        let q_next_dev = stream.clone_htod(&q_next).expect("upload q_next");
        let actions_dev = stream.clone_htod(&actions).expect("upload actions");
        let rewards_dev = stream.clone_htod(&rewards).expect("upload rewards");
        let dones_dev = stream.clone_htod(&dones).expect("upload dones");
        let mut target_out_dev = stream
            .alloc_zeros::<f32>(batch as usize)
            .expect("alloc target_out");

        // Launch in a scoped block so the device-ptr guards drop before the
        // download step (otherwise the mutable borrow on target_out_dev would
        // prevent the &target_out_dev read in clone_dtoh).
        {
            let (q_next_ptr, _g0) = q_next_dev.device_ptr(&stream);
            let (q_current_ptr, _g1) = q_current_dev.device_ptr(&stream);
            let (actions_ptr, _g2) = actions_dev.device_ptr(&stream);
            let (rewards_ptr, _g3) = rewards_dev.device_ptr(&stream);
            let (dones_ptr, _g4) = dones_dev.device_ptr(&stream);
            let (target_out_ptr, _g5) = target_out_dev.device_ptr_mut(&stream);

            unsafe {
                launch_alpha_munchausen_target(
                    &stream,
                    &kernel,
                    q_next_ptr,
                    q_current_ptr,
                    actions_ptr,
                    rewards_ptr,
                    dones_ptr,
                    0.99,  // gamma
                    0.9,   // alpha_m
                    1.0,   // tau
                    -1.0,  // log_clip_min
                    target_out_ptr,
                    batch,
                    n_actions,
                )
                .expect("kernel launch");
            }
        }
        stream.synchronize().expect("stream sync");

        let target_out = stream
            .clone_dtoh(&target_out_dev)
            .expect("download target_out");
        assert_eq!(target_out.len(), batch as usize);

        let expected = [29.8_f32, 1.1_f32];
        for (i, (got, exp)) in target_out.iter().zip(expected.iter()).enumerate() {
            assert!(
                (got - exp).abs() < 0.05,
                "sample {i}: expected {exp:.3}, got {got:.3} (full output: {target_out:?})"
            );
        }
    }

    /// End-to-end GPU smoke for the kill-criteria producer launcher.
    ///
    /// Loads the cubin, allocates synthetic inputs covering ALL four output
    /// observations (with hand-computed expected values), launches the
    /// kernel via `launch_alpha_kill_criteria`, and validates the raw
    /// scratch outputs against the hand math. Does NOT chain
    /// `apply_pearls_ad_kernel` — the smoothing path is the canonical SP4
    /// applicator and is already covered elsewhere; this test isolates the
    /// kill-criteria arithmetic.
    ///
    /// Hand-math (batch=2, n_actions=3):
    ///   - q_values = [[1, 2, 3], [5, 5, 5]]
    ///     row 0: mean=2, var=2/3, std≈0.8165, ratio=0.8165/2=0.4082
    ///     row 1: mean=5, var=0,   std=0,      ratio=0/5=0
    ///     batch_mean(ratio) = (0.4082 + 0)/2 = 0.2041
    ///   - action_counts = [10, 30, 60], total=100 → p=[0.1, 0.3, 0.6]
    ///     entropy = -[0.1·ln(0.1) + 0.3·ln(0.3) + 0.6·ln(0.6)]
    ///             = -[(0.1)(-2.3026) + (0.3)(-1.2040) + (0.6)(-0.5108)]
    ///             = -[-0.2303 - 0.3612 - 0.3065] = 0.8980
    ///   - rollout_R=100, isv[547]=-5185, isv[548]=4953
    ///     return_vs_random = (100 - (-5185)) / 4953 = 5285/4953 = 1.0670
    ///   - q_init=50, q_early=55
    ///     early_movement = |55 - 50| / |50| = 0.1
    ///
    /// Expected scratch_out = [0.2041, 0.8980, 1.0670, 0.1].
    #[test]
    fn kill_criteria_smoke_matches_hand_computation() {
        use crate::cuda_pipeline::alpha_isv_slots::{
            RANDOM_BASELINE_MEAN_INDEX, RANDOM_BASELINE_STD_INDEX,
        };

        let ctx = match CudaContext::new(0) {
            Ok(c) => c,
            Err(e) => {
                eprintln!("Skipping GPU smoke — no CUDA device 0: {e}");
                return;
            }
        };
        let stream = ctx.default_stream();

        let module = ctx
            .load_cubin(ALPHA_KILL_CRITERIA_CUBIN.to_vec())
            .expect("kill_criteria cubin module load");
        let kernel = module
            .load_function("alpha_kill_criteria_compute_kernel")
            .expect("kill_criteria kernel load");

        let batch: i32 = 2;
        let n_actions: i32 = 3;

        // q_values: row 0 spreads 1..3; row 1 is constant 5.
        let q_values: Vec<f32> = vec![1.0, 2.0, 3.0, 5.0, 5.0, 5.0];
        // Non-uniform action counts → non-maximal entropy.
        let action_counts: Vec<i32> = vec![10, 30, 60];
        // scalar_inputs: [rollout_reward_mean, q_init_norm, q_early_step_norm]
        let scalar_inputs: Vec<f32> = vec![100.0, 50.0, 55.0];

        // Construct an ISV buffer with the random-baseline anchors at slots
        // 547 and 548. Using the Task 7c committed values so the test
        // exercises the production slot-indexing path with real numbers.
        let mut isv_host: Vec<f32> = vec![0.0; 552];
        isv_host[RANDOM_BASELINE_MEAN_INDEX] = -5185.0;
        isv_host[RANDOM_BASELINE_STD_INDEX] = 4953.0;

        let q_values_dev = stream.clone_htod(&q_values).expect("upload q_values");
        let action_counts_dev = stream
            .clone_htod(&action_counts)
            .expect("upload action_counts");
        let scalar_inputs_dev = stream
            .clone_htod(&scalar_inputs)
            .expect("upload scalar_inputs");
        let isv_dev = stream.clone_htod(&isv_host).expect("upload isv");
        let mut scratch_out_dev = stream.alloc_zeros::<f32>(4).expect("alloc scratch_out");

        // Scoped block so device-ptr guards drop before the download.
        {
            let (q_values_ptr, _g0) = q_values_dev.device_ptr(&stream);
            let (action_counts_ptr, _g1) = action_counts_dev.device_ptr(&stream);
            let (scalar_inputs_ptr, _g2) = scalar_inputs_dev.device_ptr(&stream);
            let (isv_ptr, _g3) = isv_dev.device_ptr(&stream);
            let (scratch_out_ptr, _g4) = scratch_out_dev.device_ptr_mut(&stream);

            unsafe {
                launch_alpha_kill_criteria(
                    &stream,
                    &kernel,
                    q_values_ptr,
                    action_counts_ptr,
                    scalar_inputs_ptr,
                    isv_ptr,
                    batch,
                    n_actions,
                    scratch_out_ptr,
                )
                .expect("kill_criteria launch");
            }
        }
        stream.synchronize().expect("stream sync");

        let scratch_out = stream
            .clone_dtoh(&scratch_out_dev)
            .expect("download scratch_out");
        assert_eq!(scratch_out.len(), 4);

        let expected = [0.2041_f32, 0.8980_f32, 1.0670_f32, 0.1_f32];
        let labels = ["q_spread", "action_entropy", "return_vs_random", "early_movement"];
        for (i, ((got, exp), label)) in scratch_out
            .iter()
            .zip(expected.iter())
            .zip(labels.iter())
            .enumerate()
        {
            assert!(
                (got - exp).abs() < 0.01,
                "scratch_out[{i}] = {label}: expected {exp:.4}, got {got:.4} \
                 (full output: {scratch_out:?})"
            );
        }
    }

    /// End-to-end **chained pipeline** smoke for Task 12 wiring (2026-05-15).
    ///
    /// Validates that the kill-criteria producer correctly composes with the
    /// canonical SP4 `apply_pearls_ad_kernel` on the same stream:
    ///
    ///   t+0   alpha_kill_criteria_compute_kernel  → scratch[0..4]
    ///   t+1   apply_pearls_ad_kernel(n_slots=4)   → ISV[539..543]
    ///
    /// This is the gating composition for Task 12 — the H=600 DQN smoke
    /// will use this exact pattern at each rollout boundary. Failures here
    /// would manifest as bogus ISV values (e.g., scratch read before write
    /// completes due to stream-ordering violation, or wrong Pearls index
    /// base).
    ///
    /// Hand-math (same synthetic inputs as the standalone kill-criteria
    /// smoke, run twice with stationary inputs):
    ///
    ///   Iter 1:  Pearl A bootstrap (sentinel detect: prev_x_mean==0 AND
    ///            x_lag==0) → ISV[539..542] = [0.2041, 0.8980, 1.0670, 0.1]
    ///            (raw observations).
    ///
    ///   Iter 2:  Pearl D update with same inputs. dx_mean = dx_step = 0
    ///            → new_sample_var = new_diff_var = 0 → α* = 0 → ISV
    ///            unchanged. Stationary signal stays at the bootstrap value.
    ///
    /// If iteration 2 drifted from iteration 1's value, the Wiener state
    /// would be wrong; if iteration 1 produced 0 instead of the
    /// observations, Pearl A's sentinel bootstrap would be broken;
    /// if iteration 2 produced 0, the producer's scratch write wouldn't be
    /// visible to the applicator (stream-ordering bug).
    #[test]
    fn kill_criteria_chained_with_pearls_populates_isv_slots() {
        use crate::cuda_pipeline::alpha_isv_slots::{
            ACTION_ENTROPY_EMA_INDEX, ALPHA_ISV_BLOCK_LO, EARLY_Q_MOVEMENT_EMA_INDEX,
            Q_SPREAD_EMA_INDEX, RANDOM_BASELINE_MEAN_INDEX, RANDOM_BASELINE_STD_INDEX,
            RETURN_VS_RANDOM_EMA_INDEX,
        };
        // (launch_apply_pearls is invoked inside the run_chained_iter helper
        // so we don't re-import it at the top-level test scope.)

        let ctx = match CudaContext::new(0) {
            Ok(c) => c,
            Err(e) => {
                eprintln!("Skipping GPU pipeline smoke — no CUDA device 0: {e}");
                return;
            }
        };
        let stream = ctx.default_stream();

        let kc_module = ctx
            .load_cubin(ALPHA_KILL_CRITERIA_CUBIN.to_vec())
            .expect("kill_criteria cubin load");
        let kc_kernel = kc_module
            .load_function("alpha_kill_criteria_compute_kernel")
            .expect("kill_criteria kernel load");
        let pearls_module = ctx
            .load_cubin(APPLY_PEARLS_CUBIN.to_vec())
            .expect("apply_pearls cubin load");
        let pearls_kernel = pearls_module
            .load_function("apply_pearls_ad_kernel")
            .expect("apply_pearls_ad kernel load");

        let batch: i32 = 2;
        let n_actions: i32 = 3;

        // Same synthetic inputs as the standalone smoke for hand-math reuse.
        let q_values: Vec<f32> = vec![1.0, 2.0, 3.0, 5.0, 5.0, 5.0];
        let action_counts: Vec<i32> = vec![10, 30, 60];
        let scalar_inputs: Vec<f32> = vec![100.0, 50.0, 55.0];

        // ISV buffer with the Task 7c baseline anchors at slots 547/548.
        // Sized to 552 so it covers RANDOM_BASELINE_STD_INDEX (548) plus the
        // 4 diagnostic slots 539..542 that Pearls will write into.
        let mut isv_host: Vec<f32> = vec![0.0; 552];
        isv_host[RANDOM_BASELINE_MEAN_INDEX] = -5185.0;
        isv_host[RANDOM_BASELINE_STD_INDEX] = 4953.0;
        // Slots 539..542 start at sentinel 0 so Pearl A triggers on iter 1.

        let q_values_dev = stream.clone_htod(&q_values).expect("upload q_values");
        let action_counts_dev = stream
            .clone_htod(&action_counts)
            .expect("upload action_counts");
        let scalar_inputs_dev = stream
            .clone_htod(&scalar_inputs)
            .expect("upload scalar_inputs");
        let mut isv_dev = stream.clone_htod(&isv_host).expect("upload isv");
        let mut scratch_dev = stream.alloc_zeros::<f32>(4).expect("alloc scratch");
        // Wiener state: 4 slots × [sample_var, diff_var, x_lag] = 12 floats.
        let mut wiener_dev = stream.alloc_zeros::<f32>(12).expect("alloc wiener");

        // --- Iter 1: Pearl A bootstrap ---
        run_chained_iter(
            &stream,
            &kc_kernel,
            &pearls_kernel,
            &q_values_dev,
            &action_counts_dev,
            &scalar_inputs_dev,
            &mut isv_dev,
            &mut scratch_dev,
            &mut wiener_dev,
            batch,
            n_actions,
        );
        stream.synchronize().expect("sync iter 1");

        let isv_after_1 = stream.clone_dtoh(&isv_dev).expect("download isv iter 1");
        let iter_1_obs = [
            isv_after_1[Q_SPREAD_EMA_INDEX],
            isv_after_1[ACTION_ENTROPY_EMA_INDEX],
            isv_after_1[RETURN_VS_RANDOM_EMA_INDEX],
            isv_after_1[EARLY_Q_MOVEMENT_EMA_INDEX],
        ];
        let expected_iter_1 = [0.2041_f32, 0.8980_f32, 1.0670_f32, 0.1_f32];
        let labels = ["q_spread", "action_entropy", "return_vs_random", "early_movement"];
        for (i, ((got, exp), label)) in iter_1_obs
            .iter()
            .zip(expected_iter_1.iter())
            .zip(labels.iter())
            .enumerate()
        {
            assert!(
                (got - exp).abs() < 0.01,
                "Iter 1 (Pearl A bootstrap) ISV[{}] = {label}: expected {exp:.4}, got {got:.4}",
                ALPHA_ISV_BLOCK_LO + i
            );
        }

        // --- Iter 2: Pearl D stationary update (same inputs → ISV unchanged) ---
        run_chained_iter(
            &stream,
            &kc_kernel,
            &pearls_kernel,
            &q_values_dev,
            &action_counts_dev,
            &scalar_inputs_dev,
            &mut isv_dev,
            &mut scratch_dev,
            &mut wiener_dev,
            batch,
            n_actions,
        );
        stream.synchronize().expect("sync iter 2");

        let isv_after_2 = stream.clone_dtoh(&isv_dev).expect("download isv iter 2");
        let iter_2_obs = [
            isv_after_2[Q_SPREAD_EMA_INDEX],
            isv_after_2[ACTION_ENTROPY_EMA_INDEX],
            isv_after_2[RETURN_VS_RANDOM_EMA_INDEX],
            isv_after_2[EARLY_Q_MOVEMENT_EMA_INDEX],
        ];
        for (i, (g1, g2)) in iter_1_obs.iter().zip(iter_2_obs.iter()).enumerate() {
            assert!(
                (g1 - g2).abs() < 1.0e-4,
                "Iter 2 (Pearl D stationary) ISV[{}] = {}: expected ~{:.4} (unchanged), got {:.4}",
                ALPHA_ISV_BLOCK_LO + i,
                labels[i],
                g1,
                g2
            );
        }
        // Anchor slots (547/548) must remain at Task 7c values throughout —
        // Pearls only writes 539..542 so these should be untouched.
        assert_eq!(
            isv_after_2[RANDOM_BASELINE_MEAN_INDEX], -5185.0,
            "anchor slot 547 was unexpectedly modified"
        );
        assert_eq!(
            isv_after_2[RANDOM_BASELINE_STD_INDEX], 4953.0,
            "anchor slot 548 was unexpectedly modified"
        );

    }

    /// Helper: one iteration of the chained pipeline. Producer → Pearls
    /// applicator on the same stream. Factored out so the two iterations
    /// in the test above are byte-identical apart from the Wiener state's
    /// evolution.
    #[allow(clippy::too_many_arguments)]
    fn run_chained_iter(
        stream: &cudarc::driver::CudaStream,
        kc_kernel: &cudarc::driver::CudaFunction,
        pearls_kernel: &cudarc::driver::CudaFunction,
        q_values_dev: &cudarc::driver::CudaSlice<f32>,
        action_counts_dev: &cudarc::driver::CudaSlice<i32>,
        scalar_inputs_dev: &cudarc::driver::CudaSlice<f32>,
        isv_dev: &mut cudarc::driver::CudaSlice<f32>,
        scratch_dev: &mut cudarc::driver::CudaSlice<f32>,
        wiener_dev: &mut cudarc::driver::CudaSlice<f32>,
        batch: i32,
        n_actions: i32,
    ) {
        use crate::cuda_pipeline::alpha_isv_slots::ALPHA_ISV_BLOCK_LO;
        use crate::cuda_pipeline::sp4_wiener_ema::{launch_apply_pearls, ALPHA_META};

        let (q_values_ptr, _g0) = q_values_dev.device_ptr(stream);
        let (action_counts_ptr, _g1) = action_counts_dev.device_ptr(stream);
        let (scalar_inputs_ptr, _g2) = scalar_inputs_dev.device_ptr(stream);
        let (isv_ptr_ro, _g3) = isv_dev.device_ptr(stream);
        let (scratch_ptr_mut, _g4) = scratch_dev.device_ptr_mut(stream);

        unsafe {
            launch_alpha_kill_criteria(
                stream,
                kc_kernel,
                q_values_ptr,
                action_counts_ptr,
                scalar_inputs_ptr,
                isv_ptr_ro,
                batch,
                n_actions,
                scratch_ptr_mut,
            )
            .expect("kill_criteria launch");
        }
        drop(_g0);
        drop(_g1);
        drop(_g2);
        drop(_g3);
        drop(_g4);

        let (scratch_ptr_ro, _g5) = scratch_dev.device_ptr(stream);
        let (isv_ptr_mut, _g6) = isv_dev.device_ptr_mut(stream);
        let (wiener_ptr_mut, _g7) = wiener_dev.device_ptr_mut(stream);

        unsafe {
            launch_apply_pearls(
                stream,
                pearls_kernel,
                scratch_ptr_ro,
                0, // scratch_idx_base
                isv_ptr_mut,
                ALPHA_ISV_BLOCK_LO as i32, // 539
                wiener_ptr_mut,
                0, // wiener_offset_base
                4, // n_slots — Q_SPREAD..EARLY_Q_MOVEMENT
                ALPHA_META,
            )
            .expect("apply_pearls launch");
        }
    }

    /// End-to-end GPU smoke for the linear Q-network kernels (Task 12a).
    ///
    /// Validates forward → grad → SGD against hand-computed expected values.
    ///
    /// Setup: W = [[1, 0], [0, 1], [0, 0]] (3 actions, 2 state dims),
    ///        b = [0.1, 0.2, 0.3]
    ///        X = [[2, 3], [4, 5]] (batch=2)
    ///
    /// Forward (hand-math):
    ///   Q[0, 0] = 1·2 + 0·3 + 0.1 = 2.1
    ///   Q[0, 1] = 0·2 + 1·3 + 0.2 = 3.2
    ///   Q[0, 2] = 0·2 + 0·3 + 0.3 = 0.3
    ///   Q[1, 0] = 1·4 + 0·5 + 0.1 = 4.1
    ///   Q[1, 1] = 0·4 + 1·5 + 0.2 = 5.2
    ///   Q[1, 2] = 0·4 + 0·5 + 0.3 = 0.3
    ///
    /// Grad (actions=[0, 1], target=[3.0, 5.0], scale=1.0):
    ///   td_err[0, action 0] = 2.1 - 3.0 = -0.9
    ///   td_err[1, action 1] = 5.2 - 5.0 = +0.2
    ///   dW[0, 0] = -0.9·2 = -1.8    (only sample 0 taken action 0)
    ///   dW[0, 1] = -0.9·3 = -2.7
    ///   dW[1, 0] = +0.2·4 = +0.8    (only sample 1 taken action 1)
    ///   dW[1, 1] = +0.2·5 = +1.0
    ///   dW[2, *] = 0                (action 2 never taken)
    ///   db[0] = -0.9, db[1] = +0.2, db[2] = 0
    ///
    /// SGD (lr=0.1): W = W - 0.1·dW
    ///   W[0, 0]: 1.0 - 0.1·(-1.8) = 1.18
    ///   W[0, 1]: 0.0 - 0.1·(-2.7) = 0.27
    ///   W[1, 0]: 0.0 - 0.1·(+0.8) = -0.08
    ///   W[1, 1]: 1.0 - 0.1·(+1.0) = 0.90
    ///   W[2, *]: unchanged
    #[test]
    fn linear_q_forward_grad_sgd_round_trip_matches_hand_math() {
        let ctx = match CudaContext::new(0) {
            Ok(c) => c,
            Err(e) => {
                eprintln!("Skipping GPU smoke — no CUDA device 0: {e}");
                return;
            }
        };
        let stream = ctx.default_stream();

        let module = ctx
            .load_cubin(ALPHA_LINEAR_Q_CUBIN.to_vec())
            .expect("alpha_linear_q cubin load");
        let fwd_kernel = module
            .load_function("alpha_linear_q_forward_kernel")
            .expect("forward kernel load");
        let grad_kernel = module
            .load_function("alpha_linear_q_grad_kernel")
            .expect("grad kernel load");
        let sgd_kernel = module
            .load_function("alpha_linear_q_sgd_step_kernel")
            .expect("sgd kernel load");

        let batch: i32 = 2;
        let state_dim: i32 = 2;
        let n_actions: i32 = 3;

        // Row-major: W[i, j] at W[i * state_dim + j].
        let w_init: Vec<f32> = vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
        let b_init: Vec<f32> = vec![0.1, 0.2, 0.3];
        let x: Vec<f32> = vec![2.0, 3.0, 4.0, 5.0];
        let actions: Vec<i32> = vec![0, 1];
        let target: Vec<f32> = vec![3.0, 5.0];

        let mut w_dev = stream.clone_htod(&w_init).expect("upload W");
        let mut b_dev = stream.clone_htod(&b_init).expect("upload b");
        let x_dev = stream.clone_htod(&x).expect("upload X");
        let actions_dev = stream.clone_htod(&actions).expect("upload actions");
        let target_dev = stream.clone_htod(&target).expect("upload target");
        let mut q_dev = stream
            .alloc_zeros::<f32>((batch * n_actions) as usize)
            .expect("alloc Q");
        let mut dw_dev = stream
            .alloc_zeros::<f32>((n_actions * state_dim) as usize)
            .expect("alloc dW");
        let mut db_dev = stream
            .alloc_zeros::<f32>(n_actions as usize)
            .expect("alloc db");

        // --- Forward ---
        {
            let (w_ptr, _g0) = w_dev.device_ptr(&stream);
            let (b_ptr, _g1) = b_dev.device_ptr(&stream);
            let (x_ptr, _g2) = x_dev.device_ptr(&stream);
            let (q_ptr, _g3) = q_dev.device_ptr_mut(&stream);
            unsafe {
                launch_alpha_linear_q_forward(
                    &stream, &fwd_kernel,
                    w_ptr, b_ptr, x_ptr, q_ptr,
                    batch, state_dim, n_actions,
                )
                .expect("forward launch");
            }
        }
        stream.synchronize().expect("sync after forward");
        let q_after = stream.clone_dtoh(&q_dev).expect("download Q");
        let q_expected = [2.1_f32, 3.2, 0.3, 4.1, 5.2, 0.3];
        for (i, (g, e)) in q_after.iter().zip(q_expected.iter()).enumerate() {
            assert!(
                (g - e).abs() < 1e-4,
                "forward Q[{i}] = {g} expected {e}"
            );
        }

        // --- Grad ---
        {
            let (q_ptr, _g0) = q_dev.device_ptr(&stream);
            let (target_ptr, _g1) = target_dev.device_ptr(&stream);
            let (actions_ptr, _g2) = actions_dev.device_ptr(&stream);
            let (x_ptr, _g3) = x_dev.device_ptr(&stream);
            let (dw_ptr, _g4) = dw_dev.device_ptr_mut(&stream);
            let (db_ptr, _g5) = db_dev.device_ptr_mut(&stream);
            unsafe {
                launch_alpha_linear_q_grad(
                    &stream, &grad_kernel,
                    q_ptr, target_ptr, actions_ptr, x_ptr,
                    dw_ptr, db_ptr,
                    batch, state_dim, n_actions,
                    1.0, // scale
                )
                .expect("grad launch");
            }
        }
        stream.synchronize().expect("sync after grad");
        let dw_after = stream.clone_dtoh(&dw_dev).expect("download dW");
        let db_after = stream.clone_dtoh(&db_dev).expect("download db");
        let dw_expected = [-1.8_f32, -2.7, 0.8, 1.0, 0.0, 0.0];
        let db_expected = [-0.9_f32, 0.2, 0.0];
        for (i, (g, e)) in dw_after.iter().zip(dw_expected.iter()).enumerate() {
            assert!(
                (g - e).abs() < 1e-4,
                "grad dW[{i}] = {g} expected {e}"
            );
        }
        for (i, (g, e)) in db_after.iter().zip(db_expected.iter()).enumerate() {
            assert!(
                (g - e).abs() < 1e-4,
                "grad db[{i}] = {g} expected {e}"
            );
        }

        // --- SGD step: W -= 0.1 · dW; b -= 0.1 · db ---
        {
            let (w_ptr, _g0) = w_dev.device_ptr_mut(&stream);
            let (dw_ptr, _g1) = dw_dev.device_ptr(&stream);
            unsafe {
                launch_alpha_linear_q_sgd_step(
                    &stream, &sgd_kernel,
                    w_ptr, dw_ptr, 0.1,
                    n_actions * state_dim,
                )
                .expect("sgd W launch");
            }
        }
        {
            let (b_ptr, _g0) = b_dev.device_ptr_mut(&stream);
            let (db_ptr, _g1) = db_dev.device_ptr(&stream);
            unsafe {
                launch_alpha_linear_q_sgd_step(
                    &stream, &sgd_kernel,
                    b_ptr, db_ptr, 0.1,
                    n_actions,
                )
                .expect("sgd b launch");
            }
        }
        stream.synchronize().expect("sync after sgd");
        let w_after = stream.clone_dtoh(&w_dev).expect("download W");
        let b_after = stream.clone_dtoh(&b_dev).expect("download b");
        let w_expected = [1.18_f32, 0.27, -0.08, 0.90, 0.0, 0.0];
        let b_expected = [0.19_f32, 0.18, 0.30];
        for (i, (g, e)) in w_after.iter().zip(w_expected.iter()).enumerate() {
            assert!(
                (g - e).abs() < 1e-4,
                "sgd W[{i}] = {g} expected {e}"
            );
        }
        for (i, (g, e)) in b_after.iter().zip(b_expected.iter()).enumerate() {
            assert!(
                (g - e).abs() < 1e-4,
                "sgd b[{i}] = {g} expected {e}"
            );
        }
    }

    /// End-to-end GPU smoke for the stacker-threshold + Kelly-attenuation
    /// controller (Phase E.2 Task 16). Two rollouts with synthetic stats
    /// and hand-computed expected ISV transitions.
    ///
    /// Setup:
    ///   - ISV[543] (threshold)     = 0.05 initial
    ///   - ISV[544] (target rate)   = 0.10 anchor
    ///   - ISV[545] (observed EMA)  = 0.0  sentinel
    ///   - ISV[546] (Kelly atten)   = 0.0  sentinel
    ///   - target_sharpe = 0.5, k_threshold = 0.01, k_atten = 0.005,
    ///     wiener_alpha_floor = 0.4, alpha_meta = 0.1
    ///
    /// **Iter 1 (Pearl A bootstrap):**
    ///   - rollout: 30 trades / 100 decisions → observed = 0.30
    ///   - rollout_realized_sharpe = 0.7
    ///   - Pearl A: ISV[545] ← 0.30, wiener_state[2] ← 0.30
    ///   - err_rate = 0.30 - 0.10 = 0.20
    ///   - ISV[543] ← clamp(0, 0.5, 0.05 + 0.01·0.20) = 0.052
    ///   - err_sharpe = 0.7 - 0.5 = 0.2
    ///   - sentinel ISV[546]=0 → start = 1.0; clamp(0.1, 1.0, 1.0 + 0.005·0.2) = 1.0
    ///
    /// **Iter 2 (Pearl D update):**
    ///   - rollout: 5 trades / 100 decisions → observed = 0.05
    ///   - rollout_realized_sharpe = 0.3
    ///   - sample_diff = 0.05 - 0.30 = -0.25
    ///   - x_lag_diff = 0.05 - 0.30 = -0.25
    ///   - new_sample_var = (1-0.1)·0 + 0.1·0.0625 = 0.00625
    ///   - new_diff_var = (1-0.1)·0 + 0.1·0.0625 = 0.00625
    ///   - alpha_opt_raw = 0.00625 / (0.0125 + 1e-8) ≈ 0.5
    ///   - alpha_opt = max(0.5, 0.4) = 0.5
    ///   - ISV[545] ← (1-0.5)·0.30 + 0.5·0.05 = 0.175
    ///   - err_rate = 0.175 - 0.10 = 0.075
    ///   - ISV[543] ← clamp(0, 0.5, 0.052 + 0.01·0.075) = 0.05275
    ///   - err_sharpe = 0.3 - 0.5 = -0.2
    ///   - ISV[546] ← clamp(0.1, 1.0, 1.0 + 0.005·-0.2) = 0.999
    #[test]
    fn stacker_threshold_controller_smoke_matches_hand_computation() {
        use crate::cuda_pipeline::alpha_isv_slots::{
            STACKER_KELLY_ATTENUATION_INDEX, STACKER_THRESHOLD_INDEX,
            TRADE_RATE_OBSERVED_EMA_INDEX, TRADE_RATE_TARGET_INDEX,
        };

        let ctx = match CudaContext::new(0) {
            Ok(c) => c,
            Err(e) => {
                eprintln!("Skipping GPU smoke — no CUDA device 0: {e}");
                return;
            }
        };
        let stream = ctx.default_stream();

        let module = ctx
            .load_cubin(STACKER_THRESHOLD_CONTROLLER_CUBIN.to_vec())
            .expect("controller cubin load");
        let kernel = module
            .load_function("stacker_threshold_controller_update")
            .expect("controller kernel load");

        // ISV buffer sized to cover slot 546 + slack.
        let mut isv_host = vec![0.0_f32; 552];
        isv_host[STACKER_THRESHOLD_INDEX] = 0.05;
        isv_host[TRADE_RATE_TARGET_INDEX] = 0.10;
        // Slot 545 + 546 start at sentinel 0.

        let mut isv_dev = stream.clone_htod(&isv_host).expect("upload isv");
        let mut wiener_dev = stream
            .alloc_zeros::<f32>(3)
            .expect("alloc wiener state");

        let target_sharpe = 0.5_f32;
        let k_threshold = 0.01_f32;
        let k_atten = 0.005_f32;
        let wiener_alpha_floor = 0.4_f32;
        let alpha_meta = 0.1_f32;

        // --- Iter 1 ---
        {
            let (isv_ptr, _g0) = isv_dev.device_ptr_mut(&stream);
            let (w_ptr, _g1) = wiener_dev.device_ptr_mut(&stream);
            unsafe {
                launch_stacker_threshold_controller(
                    &stream,
                    &kernel,
                    30.0,   // trade_count
                    100.0,  // total_decisions
                    0.7,    // realized_sharpe
                    target_sharpe,
                    k_threshold,
                    k_atten,
                    wiener_alpha_floor,
                    alpha_meta,
                    STACKER_THRESHOLD_INDEX as i32,
                    TRADE_RATE_TARGET_INDEX as i32,
                    TRADE_RATE_OBSERVED_EMA_INDEX as i32,
                    STACKER_KELLY_ATTENUATION_INDEX as i32,
                    -1,   // regime_vol_ema_idx (disabled in test)
                    -1,   // regime_vol_ref_idx (disabled in test)
                    0.25, // regime_scale_floor (unused when disabled)
                    -1,   // regime_vol_ref_floor_idx (disabled in test)
                    -1,   // regime_vol_obs_min_idx (disabled in test)
                    0.5,  // floor_target_ratio (unused when disabled)
                    0.1,  // floor_update_rate (unused when disabled)
                    1.0e10, // vol_obs_min_sentinel (unused when disabled)
                    isv_ptr,
                    w_ptr,
                )
                .expect("controller iter 1");
            }
        }
        stream.synchronize().expect("sync iter 1");
        let isv1 = stream.clone_dtoh(&isv_dev).expect("dtoh iter 1");
        assert!(
            (isv1[TRADE_RATE_OBSERVED_EMA_INDEX] - 0.30).abs() < 1e-5,
            "iter 1 Pearl A: observed_ema = {} (expected 0.30)",
            isv1[TRADE_RATE_OBSERVED_EMA_INDEX]
        );
        assert!(
            (isv1[STACKER_THRESHOLD_INDEX] - 0.052).abs() < 1e-5,
            "iter 1 threshold = {} (expected 0.052)",
            isv1[STACKER_THRESHOLD_INDEX]
        );
        assert!(
            (isv1[STACKER_KELLY_ATTENUATION_INDEX] - 1.0).abs() < 1e-5,
            "iter 1 atten = {} (expected 1.0 — clamped to ceiling)",
            isv1[STACKER_KELLY_ATTENUATION_INDEX]
        );

        // --- Iter 2 (Pearl D) ---
        {
            let (isv_ptr, _g0) = isv_dev.device_ptr_mut(&stream);
            let (w_ptr, _g1) = wiener_dev.device_ptr_mut(&stream);
            unsafe {
                launch_stacker_threshold_controller(
                    &stream,
                    &kernel,
                    5.0,    // trade_count
                    100.0,  // total_decisions
                    0.3,    // realized_sharpe
                    target_sharpe,
                    k_threshold,
                    k_atten,
                    wiener_alpha_floor,
                    alpha_meta,
                    STACKER_THRESHOLD_INDEX as i32,
                    TRADE_RATE_TARGET_INDEX as i32,
                    TRADE_RATE_OBSERVED_EMA_INDEX as i32,
                    STACKER_KELLY_ATTENUATION_INDEX as i32,
                    -1,   // regime_vol_ema_idx (disabled in test)
                    -1,   // regime_vol_ref_idx (disabled in test)
                    0.25, // regime_scale_floor (unused when disabled)
                    -1,   // regime_vol_ref_floor_idx (disabled in test)
                    -1,   // regime_vol_obs_min_idx (disabled in test)
                    0.5,  // floor_target_ratio (unused when disabled)
                    0.1,  // floor_update_rate (unused when disabled)
                    1.0e10, // vol_obs_min_sentinel (unused when disabled)
                    isv_ptr,
                    w_ptr,
                )
                .expect("controller iter 2");
            }
        }
        stream.synchronize().expect("sync iter 2");
        let isv2 = stream.clone_dtoh(&isv_dev).expect("dtoh iter 2");
        assert!(
            (isv2[TRADE_RATE_OBSERVED_EMA_INDEX] - 0.175).abs() < 1e-4,
            "iter 2 Pearl D: observed_ema = {} (expected 0.175)",
            isv2[TRADE_RATE_OBSERVED_EMA_INDEX]
        );
        assert!(
            (isv2[STACKER_THRESHOLD_INDEX] - 0.05275).abs() < 1e-5,
            "iter 2 threshold = {} (expected 0.05275)",
            isv2[STACKER_THRESHOLD_INDEX]
        );
        assert!(
            (isv2[STACKER_KELLY_ATTENUATION_INDEX] - 0.999).abs() < 1e-5,
            "iter 2 atten = {} (expected 0.999)",
            isv2[STACKER_KELLY_ATTENUATION_INDEX]
        );

        // Target slot 544 must be unchanged (TrainingPersist anchor).
        assert_eq!(
            isv2[TRADE_RATE_TARGET_INDEX], 0.10,
            "target slot 544 must not be modified by the controller"
        );
    }
}
