/**
 * grad_decomp_kernel — Task 2.0 per-component gradient decomposition.
 *
 * Computes L2 norms of (current − snapshot) restricted to three slices of
 * `grad_buf` and writes three f32 results to a pinned device-mapped output
 * slot:
 *   result_out[0] = ‖Δ‖ on the magnitude branch slice (tensors 12..16)
 *   result_out[1] = ‖Δ‖ on the direction branch slice (tensors 8..12)
 *   result_out[2] = ‖Δ‖ on the trunk slice (tensors 0..4 — w_s1, b_s1,
 *                   w_s2, b_s2)
 *
 * Launched once per loss component (9 measurement points: IQN / CQL /
 * CQL-SAXPY / C51 / C51-budget-scale / Distillation / Recursive-confidence
 * / Predictive-coding / Ensemble) — always AFTER the component's backward
 * kernels finish writing into `grad_buf`. The matching BEFORE-backward
 * snapshot is produced by the `copy_f32` kernel over the three byte ranges
 * so that `current − snapshot` isolates exactly the component's additive
 * delta.
 *
 * The trunk slice was added (Task 2.Z diagnostic) so the measurement
 * pipeline can report IQN and Ens contributions — both `apply_iqn_trunk_
 * gradient` and `apply_ensemble_diversity_backward` SAXPY exclusively into
 * the trunk tensors 0..4, which sit entirely OUTSIDE the branch-head range
 * measured by the prior two-slot kernel. Without this third slot, IQN
 * and Ens always report 0.0000 on HEALTH_DIAG regardless of the actual
 * contribution magnitude.
 *
 * All reductions are block-internal via shared-memory tree reduction —
 * NO atomicAdd (Task 0.5 / 0.8 no-atomics rule on the captured path).
 *
 * Layout:
 *   current       — points to grad_buf[0] (f32, length total_params)
 *   snapshot      — points to the per-component snapshot (f32, length
 *                   trunk_len + dir_len + mag_len), laid out as:
 *                     [0                        .. trunk_len)                   = trunk slice
 *                     [trunk_len                .. trunk_len+dir_len)           = direction slice
 *                     [trunk_len+dir_len        .. trunk_len+dir_len+mag_len)   = magnitude slice
 *                   indexed from zero; the caller is responsible for
 *                   having copied the corresponding grad_buf ranges into
 *                   this buffer BEFORE the component's backward fired.
 *   grad_trunk_start/len, grad_dir_start/len, grad_mag_start/len
 *                 — element offsets + lengths into `current` for each
 *                   slice (so the kernel can read both sides of the
 *                   subtraction from the correct places).
 *   result_out    — [3] output: (mag_norm, dir_norm, trunk_norm) — mag/dir
 *                   order chosen to match the grad_ratio_mag_dir
 *                   convention; trunk appended last so existing pinned-
 *                   slot consumers continue to read mag at [0] and dir
 *                   at [1] unchanged.
 *
 * Launch config: one block, 256 threads.
 */
extern "C" __global__ void grad_component_delta_norm(
    const float* __restrict__ current,
    const float* __restrict__ snapshot,
    int grad_trunk_start, int trunk_len,
    int grad_dir_start, int dir_len,
    int grad_mag_start, int mag_len,
    float* __restrict__ result_out
) {
    __shared__ float sum_mag[256];
    __shared__ float sum_dir[256];
    __shared__ float sum_trunk[256];
    int tid = threadIdx.x;
    sum_mag[tid] = 0.0f;
    sum_dir[tid] = 0.0f;
    sum_trunk[tid] = 0.0f;

    // Trunk slice — snapshot offset starts at 0.
    for (int i = tid; i < trunk_len; i += blockDim.x) {
        float d = current[grad_trunk_start + i] - snapshot[i];
        sum_trunk[tid] += d * d;
    }
    // Direction slice — snapshot offset starts at trunk_len.
    for (int i = tid; i < dir_len; i += blockDim.x) {
        float d = current[grad_dir_start + i] - snapshot[trunk_len + i];
        sum_dir[tid] += d * d;
    }
    // Magnitude slice — snapshot offset starts at trunk_len + dir_len.
    for (int i = tid; i < mag_len; i += blockDim.x) {
        float d = current[grad_mag_start + i] - snapshot[trunk_len + dir_len + i];
        sum_mag[tid] += d * d;
    }
    __syncthreads();

    // Block-level tree reduction (256 → 1).
    for (int s = blockDim.x / 2; s > 0; s >>= 1) {
        if (tid < s) {
            sum_mag[tid] += sum_mag[tid + s];
            sum_dir[tid] += sum_dir[tid + s];
            sum_trunk[tid] += sum_trunk[tid + s];
        }
        __syncthreads();
    }
    if (tid == 0) {
        result_out[0] = sqrtf(sum_mag[0]);
        result_out[1] = sqrtf(sum_dir[0]);
        result_out[2] = sqrtf(sum_trunk[0]);
    }
}
