/* reward_component_ema — GPU-driven per-component |reward| step
 * observation into producer_step_scratch_buf for Pearls A+D consumption.
 *
 * Plan 3 Task 1, spec §4.C.2; SP4 Layer A Task A13.5 retrofit (2026-05-01).
 * GPU-drives-CPU-reads (spec §4.C.6).
 *
 * SP11 B1b smoke-recovery (2026-05-04, spec §4 amendment lines 564-619):
 * also computes per-component variance for c=1..5 via two-pass Welford
 * against the just-computed mean. Variance for c=0 (popart) is NOT
 * computed here — slot 63 is the pre-SP11 invariant total-reward
 * magnitude EMA (PopArt's normalisation input), NOT popart-component
 * magnitude. Popart-component variance lives at scratch slot 287 and is
 * produced by `popart_component_ema_kernel` from the dedicated
 * `popart_component_per_sample` GPU buffer (B1b fix-up).
 *
 * Reads per-sample per-component rewards from `reward_components_per_sample`
 * [n_samples * 7] (SP22 H6 Phase 3 stride change; this kernel still
 * processes components 0..5 only, the 7th component aux_align is
 * handled by the separate `reward_aux_align_ema_kernel` writing
 * directly to ISV[REWARD_AUX_ALIGN_EMA_INDEX=536]), where the
 * 6 components processed here are laid out as:
 *
 *   [c=0] popart  — final on-policy reward (pre-PopArt; denominator signal)
 *   [c=1] cf      — counterfactual replay reward written to the CF slot
 *   [c=2] trail   — 0.0 placeholder (future trailing-stop term)
 *   [c=3] micro   — dense OFI-alignment micro-reward (R5)
 *   [c=4] opp_cost — Flat opportunity cost (Plan 3 Task 2 B.1; ISV[21]-scaled)
 *   [c=5] bonus    — 0.0 placeholder (future trade-attempt/timing bonus)
 *   [c=6] aux_align — SP22 H6 Phase 3 (NOT processed by this kernel;
 *                     see `reward_aux_align_ema_kernel` for the EMA
 *                     into ISV[REWARD_AUX_ALIGN_EMA_INDEX=536])
 *
 * Reduces mean |r_c| over the batch per component, then writes the step
 * observation to `scratch_buf[scratch_first_index + c]` for c in 0..6.
 * The host launcher (`launch_reward_component_ema_inplace` on
 * `GpuExperienceCollector`) follows up with the GPU `apply_pearls_ad_kernel`
 * on the same stream (single launch, n_slots=6), mapping each scratch slot
 * to ISV[REWARD_POPART_EMA_INDEX + c]. 2026-05-01 GPU-Pearls refactor —
 * graph-capture-compatible (no host sync between kernels).
 *
 * SP11 B1b smoke-recovery: ALSO writes per-component variance for c=1..5
 * to `scratch_buf[var_scratch_first_index + (c - 1)]` (5 contiguous floats).
 * The launcher chains a second `apply_pearls_ad_kernel` (n_slots=5) into
 * ISV[REWARD_COMPONENT_VAR_EMA_BASE+1..+6) = ISV[362..367). Component 0
 * (popart) variance is sourced from the separate `popart_component_ema_kernel`
 * (see B1b fix-up rationale) and lands at ISV[REWARD_COMPONENT_VAR_EMA_BASE=361].
 *
 * **α dropped per SP4 — Pearls A+D adapt α from per-slot signal-vs-noise
 * variance.** Cross-boundary wiring: the trainer's mapped-pinned
 * `producer_step_scratch_buf` + `wiener_state_buf` + `isv_signals_pinned`
 * are threaded into the collector via
 * `set_reward_component_pearls_buffers`, called once by
 * `FusedTrainingCtx::wire_reward_component_pearls_buffers` after
 * collector construction (mirrors A14/A15 Pearl C wiring path).
 *
 * Single-block kernel (one thread per component). Cold-path — once per
 * training step. Cost: 6 threads × n_samples additions for the mean
 * pass + 5 threads × n_samples deviations for the variance pass + 11
 * scratch writes total (6 mag + 5 var). No atomicAdd — each thread
 * owns its component's accumulator independently.
 */

extern "C" __global__ void reward_component_ema(
    const float* __restrict__ reward_components, /* [n_samples * 7] row-major (SP22 H6 Phase 3 stride; kernel reads only c=0..5) */
    int          n_samples,
    float*       __restrict__ scratch_buf,        /* producer_step_scratch_buf */
    int          scratch_first_index,             /* slot 63 — first of 6 contiguous mag slots */
    /* SP11 B1b smoke-recovery: scratch slot for variance outputs c=1..5
     * (5 contiguous floats). Component 0 (popart) variance lives at the
     * separate popart_component_ema_kernel's output. */
    int          var_scratch_first_index          /* slot 288 — first of 5 contiguous var slots (cf, trail, micro, opp_cost, bonus) */
) {
    /* Per-thread one component (REWARD_COMPONENT_COUNT=6 in production layout —
     * see `REWARD_COMPONENT_COUNT` in `gpu_dqn_trainer.rs`). The host launcher
     * sets `block_dim = (REWARD_COMPONENT_COUNT, 1, 1)`; the literal 6 below
     * is intentional kernel-side — keeping it as a compile-time literal allows
     * `nvcc` to fully unroll the row-stride arithmetic. The host-side
     * `debug_assert_eq!(REWARD_COMPONENT_COUNT, 6, ...)` guards the contract. */
    int c = (int)threadIdx.x;
    if (c >= 6 || blockIdx.x != 0) return;

    /* ── Pass 1: mean |r| over batch for this component. ────────────── */
    float acc = 0.0f;
    for (int idx = 0; idx < n_samples; ++idx) {
        acc += fabsf(reward_components[idx * 7 + c]);
    }
    float mean_abs = acc / fmaxf(1.0f, (float)n_samples);

    scratch_buf[scratch_first_index + c] = mean_abs;

    /* ── Pass 2: var |r| via Welford against the just-computed mean. ──
     * Component 0 (popart) variance is NOT written here — see file
     * header for the B1b fix-up rationale (slot 63 vs slot 360
     * separation). Threads c=1..5 each own one variance-output slot. */
    if (c >= 1 && c <= 5) {
        float v_acc = 0.0f;
        for (int idx = 0; idx < n_samples; ++idx) {
            float d = fabsf(reward_components[idx * 7 + c]) - mean_abs;
            v_acc += d * d;
        }
        float var_abs = v_acc / fmaxf(1.0f, (float)n_samples);
        /* Component 1 (cf) → var_scratch_first_index + 0,
         * component 5 (bonus) → var_scratch_first_index + 4. */
        scratch_buf[var_scratch_first_index + (c - 1)] = var_abs;
    }

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