// crates/ml/src/cuda_pipeline/hold_rate_observer_kernel.cu
//
// SP13 Phase 0a v3 (2026-05-04): Hold-rate observer.
//
// Single-block tree-reduce kernel that counts how many bars in a batch
// picked the Hold direction action and writes the resulting fraction
// `count(Hold) / batch_size ∈ [0, 1]` to `out_1[0]`. Pearls A+D then
// smooth this per-step observation into
// `ISV[HOLD_RATE_OBSERVED_EMA_INDEX=382]`; the host-side controller in
// `training_loop.rs` reads the EMA + the static target at
// `ISV[HOLD_RATE_TARGET_INDEX=381]` and writes the priced Hold cost back
// to `ISV[HOLD_COST_INDEX=380]`. The reward-composition site in
// `experience_kernels.cu` then subtracts the cost on every Hold-action
// bar — pricing the action so the policy uses Hold deliberately rather
// than as a free CQL-bias-anchored default.
//
// P0a.T4 (2026-05-04): the kernel reads the production PACKED factored
// `batch_actions [B]` tile (`int* action_idx`) and decodes direction
// inline as `dir = action_idx / (NUM_MAGNITUDES * NUM_ORD * NUM_URG)`.
// The earlier T3-staged signature took an unpacked `actions_dir [B]`
// tile that production never materialises; `experience_action_select`
// (the upstream producer) writes only the packed factored form to
// `out_actions[i] = dir*b1*b2*b3 + mag*b2*b3 + ord*b3 + urg`. Per
// `feedback_no_partial_refactor.md`, every consumer of the packed
// contract migrates atomically — this kernel + the T3 oracle tests are
// updated in lockstep with the launcher. Bucket sizes
// (`NUM_MAGNITUDES`, `NUM_ORD`, `NUM_URG`) live in `state_layout.cuh`
// so the kernel doesn't need an extra launcher arg.
//
// Sign convention:
//   - decoded direction `dir == DIR_HOLD` (=1) → contributes 1 to count
//   - any other direction (Short=0 / Long=2 / Flat=3) → contributes 0
//   - empty batch (`batch_size <= 0`) → out = 0.0 (Pearl A's first-
//     observation replacement consumes 0.0 cleanly because the
//     companion sentinel for slot 382 is also 0.0; see registry entry
//     `sp13_hold_rate_observed_ema`).
//
// Single block, BLOCK_SIZE=256, one shared-memory int array — no
// atomicAdd per `feedback_no_atomicadd.md`. Pure GPU compute per
// `feedback_no_cpu_compute_strict.md`. Mirrors the `aux_dir_acc_reduce`
// kernel's reducer shape (T2, 2026-05-04) at smaller surface — this
// kernel only needs ONE counter (Hold-count), the dir-acc kernel needed
// four (correct/pos_pred/pos_label/valid).
//
// `state_layout.cuh` provides the `DIR_HOLD` constant (==1) shared by
// every kernel that includes the header — the same constant
// `experience_action_select` writes when the policy picks Hold and the
// reward-composition site reads to gate the per-bar Hold-cost subtraction.

#include <cuda_runtime.h>

#include "state_layout.cuh"

#define BLOCK_SIZE 256

extern "C" __global__ void hold_rate_observer_kernel(
    /* Per-bar PACKED factored action tile [B], values in
     *   `[0, NUM_DIRECTIONS * NUM_MAGNITUDES * NUM_ORD * NUM_URG)` =
     *   `[0, 4 * 3 * 3 * 3) = [0, 108)` for the production branch sizes.
     * Encoding: `action_idx = dir*M*O*U + mag*O*U + ord*U + urg` —
     * matches `experience_action_select`'s `out_actions[i]` write at
     * `experience_kernels.cu:1453`. The launcher passes the post-
     * action-selection `batch_actions` buffer; the kernel strides each
     * thread over `[0, batch_size)` so any batch_size up to ~2^31 is
     * valid. */
    const int* __restrict__ batch_actions,
    /* Number of bars in the batch. */
    int batch_size,
    /* 1-element output buffer. Mapped-pinned in production (see the
     * launcher in `gpu_dqn_trainer.rs`). The kernel writes
     *   out_1[0] = count(decoded_dir == DIR_HOLD) / batch_size
     * in thread 0 after the tree-reduce completes. */
    float* __restrict__ out_1)
{
    /* Single-block reducer — guard against accidental multi-block launch. */
    if (blockIdx.x != 0) return;

    const int tid  = threadIdx.x;
    const int bdim = blockDim.x;

    /* Dynamic shared memory: one int array of `bdim` slots. The launcher
     * passes `shared_mem_bytes = bdim × sizeof(int)`. */
    extern __shared__ int sh_hold[];

    /* Per-thread strided accumulation over the batch. Decode the packed
     * factored action_idx and check direction; the divisor matches the
     * authoritative encoding in `experience_action_select`. */
    const int dir_divisor = NUM_MAGNITUDES * NUM_ORD * NUM_URG;
    int local_hold = 0;
    for (int i = tid; i < batch_size; i += bdim) {
        const int action_idx = batch_actions[i];
        const int dir        = action_idx / dir_divisor;
        if (dir == DIR_HOLD) local_hold += 1;
    }
    sh_hold[tid] = local_hold;
    __syncthreads();

    /* Standard log2(BLOCK_SIZE) tree reduction. One `__syncthreads()` per
     * pass keeps every thread's view consistent. */
    for (int s = bdim / 2; s > 0; s >>= 1) {
        if (tid < s) {
            sh_hold[tid] += sh_hold[tid + s];
        }
        __syncthreads();
    }

    /* Thread 0 finalises. Empty batch → 0.0 sentinel matches the slot-382
     * fold-reset sentinel (see `sp13_hold_rate_observed_ema` registry
     * entry); Pearl A's first-observation replacement consumes 0.0 cleanly. */
    if (tid == 0) {
        out_1[0] = (batch_size > 0)
            ? ((float)sh_hold[0] / (float)batch_size)
            : 0.0f;
        __threadfence_system();
    }
}
