// ema_update_per_step.cu — per-step EMA producer for ISV-resident
// adaptive-control EMAs that update every step regardless of episode
// boundary (Phase R3 of the integrated RL trainer rebuild;
// see docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
//
// Companion to `ema_update_on_done.cu`. This variant has NO done-gate
// — it consumes the per-batch raw signal on every step. Used for EMAs
// that should update continuously rather than only at trade closes:
//
//   * KL(π_new ‖ π_old) EMA (input to rl_ppo_clip_controller, ISV[419])
//   * Observed action entropy H(π) EMA (input to rl_entropy_coef_controller,
//     ISV[420])
//   * Advantage variance ratio EMA (input to rl_rollout_steps_controller,
//     ISV[421])
//
// Generic over the ISV slot — caller passes `slot_index` + Wiener-α.
// Bootstrap discipline + reduction strategy are identical to
// `ema_update_on_done.cu`; only the gate differs (here, every batch
// entry contributes, so the per-batch mean is the unweighted average).
//
// Per `feedback_no_atomicadd`: shared-memory tree reduction in a
// single block; no atomics. Caller passes
// `shared_mem_bytes = b_size * sizeof(float)`.

extern "C" __global__ void ema_update_per_step(
    float* __restrict__ isv,         // ISV bus (≥ slot_index + 1)
    int                  slot_index, // which ISV slot to update
    float                alpha,      // Wiener-α (host-floored ≥ 0.4)
    const float* __restrict__ obs,   // [b_size] per-batch raw signal
    int                  b_size
) {
    extern __shared__ float smem[]; // [b_size]

    const int tid = threadIdx.x;
    if (tid >= b_size) return;

    smem[tid] = obs[tid];
    __syncthreads();

    // Tree reduction (block-level, no atomicAdd per
    // feedback_no_atomicadd).
    for (int stride = 1; stride < b_size; stride *= 2) {
        if ((tid % (stride * 2)) == 0 && (tid + stride) < b_size) {
            smem[tid] += smem[tid + stride];
        }
        __syncthreads();
    }

    if (tid == 0) {
        const float mean_obs = smem[0] / (float)b_size;
        const float prev     = isv[slot_index];
        if (prev == 0.0f) {
            // First-observation bootstrap per pearl_first_observation_bootstrap.
            // Defer if mean_obs == 0 so we don't write a sentinel that
            // would be re-bootstrapped on the next call.
            if (mean_obs != 0.0f) {
                isv[slot_index] = mean_obs;
            }
        } else {
            // Wiener-α blend; caller pre-floored α at 0.4 per
            // pearl_wiener_alpha_floor_for_nonstationary.
            isv[slot_index] = (1.0f - alpha) * prev + alpha * mean_obs;
        }
    }
}
