/* update_grad_norm_emas — GPU-only fast/slow grad-norm EMA update.
 *
 * SP4 Layer C close-out C1 redesigned (2026-05-01). Replaces the host-side
 * arithmetic block in `GpuDqnTrainer::update_adaptive_clip`
 * (gpu_dqn_trainer.rs ~line 22720) per `feedback_no_cpu_compute_strict`.
 * Single-thread, single-block — the EMA update is two scalar formulas, no
 * parallelism needed, mirroring `q_drift_rate_ema_kernel.cu` /
 * `moe_lambda_eff_kernel.cu` cold-path footprint.
 *
 * Reads:  grad_norm_dev[0]   — device-visible scalar populated by
 *                              `launch_grad_norm_finalize` earlier in the
 *                              same stream (sequential same-stream ordering,
 *                              no sync needed).
 *         fast_ema_dev[0]    — previous fast EMA (mapped-pinned scalar).
 *         slow_ema_dev[0]    — previous slow EMA (mapped-pinned scalar).
 * Writes: fast_ema_dev[0]    — updated fast EMA.
 *         slow_ema_dev[0]    — updated slow EMA.
 *
 * Cold-start sentinel: if previous EMA value is ≤ 0.0 (post-fold-reset for
 * fast, or initial cold start), assign the observation directly; otherwise
 * apply standard EMA recurrence
 *   new = (1 - α) × prev + α × obs
 * Matches the deleted host-side formula bit-for-bit (modulo IEEE-754 fp32
 * commutativity; both formulations use the same algebraic form).
 *
 * Mapped-pinned EMA buffers (`fast_ema_dev`, `slow_ema_dev`) are 1-element
 * device pointers backed by mapped-pinned host memory. The host-side
 * `fold_warmup_factor_kernel` consumer reads them via dev_ptr in its own
 * launch later in the same stream; `__threadfence_system()` ensures the
 * write is PCIe-visible to mapped-pinned host_ptr accesses
 * (`grad_norm_fast_ema_value` / `grad_norm_slow_ema_value` accessors used by
 * HEALTH_DIAG diagnostics).
 *
 * Per `pearl_cold_path_no_exception_to_gpu_drives.md`: cold-path scalar
 * arithmetic stays on GPU when its EMA inputs already live in mapped-pinned
 * host memory the device can read directly. Per `feedback_no_atomicadd.md`:
 * a single thread reads three scalars and writes two — no atomics needed.
 * Per `feedback_no_cpu_compute_strict.md`: any compute (EMA, reduction,
 * mean) belongs on GPU regardless of frequency, regardless of whether it's
 * hot-path or cold-path.
 */

extern "C" __global__ void update_grad_norm_emas_kernel(
    const float* __restrict__ grad_norm_dev,    /* [1] grad_norm_buf[0] */
    float*       __restrict__ fast_ema_dev,     /* [1] mapped-pinned fast EMA */
    float*       __restrict__ slow_ema_dev,     /* [1] mapped-pinned slow EMA */
    float fast_alpha,
    float slow_alpha
) {
    if (blockIdx.x != 0 || threadIdx.x != 0) return;

    const float obs = grad_norm_dev[0];

    const float fast_prev = fast_ema_dev[0];
    const float new_fast  = (fast_prev <= 0.0f)
        ? obs
        : ((1.0f - fast_alpha) * fast_prev + fast_alpha * obs);
    fast_ema_dev[0] = new_fast;

    const float slow_prev = slow_ema_dev[0];
    const float new_slow  = (slow_prev <= 0.0f)
        ? obs
        : ((1.0f - slow_alpha) * slow_prev + slow_alpha * obs);
    slow_ema_dev[0] = new_slow;

    __threadfence_system();  /* PCIe-visible to mapped-pinned host_ptr */
}
