/* ══════════════════════════════════════════════════════════════════════════
 * SP13 Layer B — Commit B1.1b producer kernel for aux next-bar sign label
 * (2026-05-05). SP14 Layer C Phase C.4b (2026-05-08): migrated from
 * fixed-`lookahead` HFT-scale label `(p_{t+1} > p_t)` to ISV-driven
 * multi-bar horizon `(p_{t+H} > p_t)` where H is read from
 * `ISV[isv_h_idx = AUX_PRED_HORIZON_BARS_INDEX]` (slot 450).
 *
 * Rationale: aux's original next-bar prediction is HFT-scale
 * microstructure noise — unlearnable at our HFT-MFT trading frequency
 * (multi-bar holds). With H read from ISV, the adaptive producer
 * (`aux_horizon_update_kernel`) drives H from observed avg winning hold
 * time so the label semantic matches actual position lifetime. See
 * `2026-05-07-sp14-layer-c-separate-aux-trunk.md` §C.4b for the design.
 *
 * Replaces the B0 `alloc_zeros::<i32>(total)` placeholder for
 * `aux_sign_labels` in `GpuExperienceCollector::collect_experiences_gpu`.
 * The B0 plumbing carried `0` everywhere; with B1.1a the K=2 softmax CE
 * head consumes -1/0/1 labels (-1 = mask/skip, 0 = down/flat, 1 = up).
 * Without this producer, the model trains on "all bars are class 0" — the
 * known degraded behavior between B1.1a and B1.1b. This kernel computes
 * the real label per experience from the price trajectory.
 *
 * Per-thread O(1) map: read `targets[bar*6+2]` (raw_close column — see
 * `dt_kernels.cu:803` and `scripted_policy_kernel.cu:56` for the canonical
 * column layout: 0=preproc_close, 1=preproc_next, 2=raw_close, 3=raw_next,
 * 4=raw_open, 5=mid_open) at `bar` and `bar+H`, classify by
 * strict greater-than — flat/down both map to label 0 because the K=2
 * softmax can only distinguish "up" from "not-up". The skip sentinel -1
 * fires when the lookahead window would run past `total_bars`; the CE
 * loss reduce kernel masks it (`label < 0` → row excluded from
 * mean + B_valid count per `aux_heads_kernel.cu`).
 *
 * H clamping: `H = clamp(round(isv[isv_h_idx]), 1, 240)` — fundamental
 * floor (cannot predict the past) and ceiling (rollout buffer guard).
 * Clamping is on-device per `feedback_isv_for_adaptive_bounds.md`.
 *
 * Pure map; no atomicAdd, no reduction, no shared memory — block
 * tree-reduce N/A here per `feedback_no_atomicadd.md`. GPU-only data
 * read/write per `feedback_cpu_is_read_only.md`. Inputs are mapped-pinned
 * (targets, isv) or device-allocated (bar_indices, out_labels) per
 * `feedback_no_htod_htoh_only_mapped_pinned.md`.
 *
 * Launch config: grid=(ceil(total / 256)), block=(256). Mirrors
 * `gather_i32_scalar` (graph_utility_kernels.cu:96) and
 * `hindsight_relabel_kernel` (experience_kernels.cu) — same tiling
 * convention used everywhere a per-experience map kernel runs in this
 * pipeline.
 *
 * Args:
 *   targets       — `[total_bars, 6]` mapped-pinned f32 (raw_close at col 2).
 *   bar_indices   — `[total]` mapped-pinned i32, bar index for each
 *                   experience (constructed by host in
 *                   `collect_experiences_gpu`'s pre-existing
 *                   `bar_indices_pinned` fill loop).
 *   isv           — `[ISV_TOTAL_DIM]` mapped-pinned f32. The kernel reads
 *                   `isv[isv_h_idx]` once per thread (broadcast value).
 *   isv_h_idx     — index of the adaptive horizon slot
 *                   (AUX_PRED_HORIZON_BARS_INDEX = 450).
 *   out_labels    — `[total]` device i32; receives -1/0/1 per experience.
 *   total         — number of experiences.
 *   total_bars    — total bars in the data series (for skip-sentinel
 *                   bound check: bar+H >= total_bars ⇒ -1).
 * ══════════════════════════════════════════════════════════════════════════ */
extern "C" __global__
void aux_sign_label_kernel(
    const float* __restrict__ targets,
    const int*   __restrict__ bar_indices,
    const float* __restrict__ isv,
    int          isv_h_idx,
    int*         __restrict__ out_labels,
    int total,
    int total_bars)
{
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    if (tid >= total) return;

    /* Read H from ISV (broadcast value — every thread reads the same
     * cell). Round-to-nearest then clamp into [1, 240] per
     * `feedback_isv_for_adaptive_bounds.md`. */
    int H = (int)__float2int_rn(isv[isv_h_idx]);
    if (H < 1)   H = 1;
    if (H > 240) H = 240;

    int bar = bar_indices[tid];
    if (bar < 0 || bar + H >= total_bars) {
        out_labels[tid] = -1;  // skip sentinel — no lookahead window available
        return;
    }

    float p_now = targets[bar * 6 + 2];
    float p_fut = targets[(bar + H) * 6 + 2];

    // Strict greater-than tie-break: ties (p_fut == p_now, e.g. flat)
    // map to "down"=0 because the K=2 head can only distinguish "up"
    // from "not-up". Asymmetry is intentional and matches the brief.
    out_labels[tid] = (p_fut > p_now) ? 1 : 0;
}
