// aux_trunk.cu — single-bucket CfC step (forward + backward) at AUX_HIDDEN=64.
//
// Per `pearl_separate_aux_trunk_when_shared_starves`: this is the kernel
// behind a SECOND, smaller CfC trunk used for outcome-supervision (D-labels).
// It runs in parallel with the main per-branch CfC trunk on the same encoder
// output, with INDEPENDENT parameters (no shared w_in/w_rec/b/tau) so that
// BCE gradient flow on the main trunk does not starve the aux supervision.
//
// Design contract:
//   • Single bucket — no per-horizon channel splitting, no
//     channels_in_bucket lookup, no bucket_dim_k. Aux supervision is
//     per-K at the head (B4 wiring), not at the τ-state.
//   • Hidden dim is AUX_HIDDEN=64 (half of main trunk's HIDDEN_DIM=128).
//   • Algorithm matches `cfc_step_per_branch.cu`: continuous-time CfC
//     decay update `h_new = h_old·decay + (1-decay)·tanh(pre)`, with
//     `decay = exp(-dt / max(tau, 1e-6))`.
//   • Input feature dim is parameterised (`feat_dim` runtime argument)
//     so a single cubin handles both raw-snap input (FEATURE_DIM=40) and
//     post-encoder input (HIDDEN_DIM=128). The host wrapper passes the
//     correct dim; the kernel iterates `feat_dim` in the input MVM.
//
// Per `feedback_no_atomicadd.md`: no atomicAdd anywhere. Each thread
// writes to its own (batch, c) slot in forward, and to its own per-batch
// grad slice in backward. Cross-batch reduction is the caller's
// responsibility via the existing `reduce_axis0` infrastructure.
//
// Per `pearl_cooperative_staging_eliminates_redundant_reads`: the per-
// batch `x` and `h_old` rows are SHARED across all output channels in a
// block. Stage them into shared memory once at block entry so the K-loop
// reads from smem instead of issuing AUX_HIDDEN × redundant DRAM reads.
//
// Per `pearl_no_host_branches_in_captured_graph`: no host branching;
// the kernel takes feat_dim as a runtime arg but does not consult any
// host-side state during execution.
//
// Per `feedback_nvidia_grade_perf_for_kernels.md`: warp-uniform branches
// (the only conditional is `batch < B`, identical for every thread in a
// block since batch == blockIdx.x), no atomicAdd, coalesced loads via
// cooperative staging.

#define AUX_HIDDEN 64
#define MAX_FEAT_DIM 128   // upper bound for shared-memory staging; covers
                            // both FEATURE_DIM=40 and HIDDEN_DIM=128 callers

// ─────────────────────────────────────────────────────────────────────
// aux_trunk_fwd: forward pass.
//
// Launch:
//   grid  = (B, 1, 1)
//   block = (AUX_HIDDEN = 64, 1, 1)
//   shared_mem_bytes = (feat_dim + AUX_HIDDEN) * sizeof(float)
//
// Per (batch, c) thread computes:
//   pre = b[c] + Σ_k W_in[c, k] * x[batch, k] + Σ_k W_rec[c, k] * h_old[batch, k]
//   decay = exp(-dt / max(tau[c], 1e-6))
//   h_new[batch, c] = h_old[batch, c] * decay + (1 - decay) * tanh(pre)
//
// W_in is [AUX_HIDDEN × feat_dim], W_rec is [AUX_HIDDEN × AUX_HIDDEN].
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void aux_trunk_fwd(
    const float* __restrict__ w_in,    // [AUX_HIDDEN × feat_dim]
    const float* __restrict__ w_rec,   // [AUX_HIDDEN × AUX_HIDDEN]
    const float* __restrict__ b,       // [AUX_HIDDEN]
    const float* __restrict__ tau,     // [AUX_HIDDEN]
    const float* __restrict__ x,       // [B × feat_dim]
    const float* __restrict__ h_old,   // [B × AUX_HIDDEN]
    float dt,
    int B,
    int feat_dim,
    float* __restrict__ h_new          // [B × AUX_HIDDEN]
) {
    extern __shared__ float smem[];
    float* x_local     = smem;                 // [feat_dim]
    float* h_old_local = smem + feat_dim;      // [AUX_HIDDEN]

    int batch = blockIdx.x;
    int c     = threadIdx.x;

    if (batch >= B) return;

    // Cooperative staging of per-batch x[batch, *] and h_old[batch, *].
    // Each thread loads ceil(feat_dim / AUX_HIDDEN) = up to ~2 x slots
    // and exactly one h_old slot (since blockDim.x == AUX_HIDDEN).
    for (int i = c; i < feat_dim; i += AUX_HIDDEN) {
        x_local[i] = x[batch * feat_dim + i];
    }
    h_old_local[c] = h_old[batch * AUX_HIDDEN + c];
    __syncthreads();

    // pre = b[c] + Σ_k W_in[c, k] * x_local[k] + Σ_k W_rec[c, k] * h_old_local[k]
    float pre = b[c];
    for (int k = 0; k < feat_dim; ++k) {
        pre += w_in[c * feat_dim + k] * x_local[k];
    }
    for (int k = 0; k < AUX_HIDDEN; ++k) {
        pre += w_rec[c * AUX_HIDDEN + k] * h_old_local[k];
    }

    float decay = expf(-dt / fmaxf(tau[c], 1e-6f));
    h_new[batch * AUX_HIDDEN + c] =
        h_old_local[c] * decay + (1.0f - decay) * tanhf(pre);
}

// ─────────────────────────────────────────────────────────────────────
// aux_trunk_bwd: backward pass.
//
// Launch:
//   grid  = (B, 1, 1)
//   block = (AUX_HIDDEN = 64, 1, 1)
//   shared_mem_bytes = (feat_dim + AUX_HIDDEN) * sizeof(float)
//
// Per-batch grad slices accumulation semantics (SDD-3 Layer B5 callers):
//   * grad_w_in / grad_w_rec / grad_b / grad_tau — `+=` per K-loop launch.
//     Multiple K-iterations sum into the same per-batch scratch (each step
//     has different x/h_old → different contribution), and reduce_axis0
//     at end of step collapses across B.
//   * grad_h_old / grad_x — `=` overwrite (consumed immediately by the
//     caller after each per-step launch: grad_h_old carries into k-1,
//     grad_x is accumulated into the encoder grad slot or discarded).
// Caller must zero the four param-grad scratches at step start (the
// trainer does this alongside the rest of its per-step scratch memsets).
//
// grad shapes:
//   grad_w_in   : [B × AUX_HIDDEN × feat_dim]    per-batch scratch
//   grad_w_rec  : [B × AUX_HIDDEN × AUX_HIDDEN]  per-batch scratch
//   grad_b      : [B × AUX_HIDDEN]               per-batch scratch
//   grad_tau    : [B × AUX_HIDDEN]               per-batch scratch
//   grad_h_old  : [B × AUX_HIDDEN]               per-batch (decay
//                                                 contribution only;
//                                                 cross-channel reduction
//                                                 deferred to caller if
//                                                 unrolled K>1)
//   grad_x      : [B × feat_dim]                 per-batch (reduced
//                                                 across c via shared-
//                                                 memory tree-reduce)
//
// grad_x cross-channel reduction is done inside this kernel via a
// block-resident shared-memory tree-reduce (no atomicAdd per
// `feedback_no_atomicadd`).
// ─────────────────────────────────────────────────────────────────────
extern "C" __global__ void aux_trunk_bwd(
    const float* __restrict__ w_in,         // [AUX_HIDDEN × feat_dim]
    const float* __restrict__ w_rec,        // [AUX_HIDDEN × AUX_HIDDEN]
    const float* __restrict__ b,            // [AUX_HIDDEN]
    const float* __restrict__ tau,          // [AUX_HIDDEN]
    const float* __restrict__ x,            // [B × feat_dim]
    const float* __restrict__ h_old,        // [B × AUX_HIDDEN]
    const float* __restrict__ grad_h_new,   // [B × AUX_HIDDEN]
    float dt,
    int B,
    int feat_dim,
    float* __restrict__ grad_w_in,    // [B × AUX_HIDDEN × feat_dim]
    float* __restrict__ grad_w_rec,   // [B × AUX_HIDDEN × AUX_HIDDEN]
    float* __restrict__ grad_b,       // [B × AUX_HIDDEN]
    float* __restrict__ grad_tau,     // [B × AUX_HIDDEN]
    float* __restrict__ grad_h_old,   // [B × AUX_HIDDEN]
    float* __restrict__ grad_x        // [B × feat_dim]
) {
    // Shared layout:
    //   [0 .. feat_dim)              : x_local
    //   [feat_dim .. feat_dim+AH)    : h_old_local
    //   [feat_dim+AH .. feat_dim+AH+AH) : d_pre_smem (per-channel d_pre
    //                                     for cross-channel grad_x reduce)
    extern __shared__ float smem[];
    float* x_local     = smem;
    float* h_old_local = smem + feat_dim;
    float* d_pre_smem  = smem + feat_dim + AUX_HIDDEN;

    int batch = blockIdx.x;
    int c     = threadIdx.x;

    if (batch >= B) return;

    // Cooperative staging — identical pattern to forward.
    for (int i = c; i < feat_dim; i += AUX_HIDDEN) {
        x_local[i] = x[batch * feat_dim + i];
    }
    h_old_local[c] = h_old[batch * AUX_HIDDEN + c];
    __syncthreads();

    // Recompute forward pre + tanh to derive d_pre.
    float pre = b[c];
    for (int k = 0; k < feat_dim; ++k) {
        pre += w_in[c * feat_dim + k] * x_local[k];
    }
    for (int k = 0; k < AUX_HIDDEN; ++k) {
        pre += w_rec[c * AUX_HIDDEN + k] * h_old_local[k];
    }
    const float tau_eps = 1e-6f;
    const float tau_raw = tau[c];
    const float tau_eff = fmaxf(tau_raw, tau_eps);
    const float decay   = expf(-dt / tau_eff);
    const float s       = tanhf(pre);
    const float dh      = grad_h_new[batch * AUX_HIDDEN + c];
    const float d_pre   = dh * (1.0f - decay) * (1.0f - s * s);
    const float d_decay = dh * (h_old_local[c] - s);

    // Per-batch grad-scratch writes. Each thread (batch, c) is the sole
    // writer of grad_*[batch, c, ...] — no race, no atomicAdd.
    // Param-grad scratches use `+=` so the trainer's reverse-K loop sums
    // contributions across all K positions (each with different x/h_old
    // through the same params).
    grad_b[batch * AUX_HIDDEN + c] += d_pre;

    // grad_tau receives 0 when tau hits the clamp floor
    // (strict d(max)/d(tau) = 0 below tau_eps).
    const float gate = (tau_raw > tau_eps) ? 1.0f : 0.0f;
    grad_tau[batch * AUX_HIDDEN + c] +=
        gate * d_decay * decay * dt / (tau_eff * tau_eff);

    // grad_w_in[batch, c, k] += d_pre * x_local[k]
    for (int k = 0; k < feat_dim; ++k) {
        const long long off =
            (long long)batch * AUX_HIDDEN * feat_dim
            + (long long)c * feat_dim + k;
        grad_w_in[off] += d_pre * x_local[k];
    }
    // grad_w_rec[batch, c, k] += d_pre * h_old_local[k]
    for (int k = 0; k < AUX_HIDDEN; ++k) {
        const long long off =
            (long long)batch * AUX_HIDDEN * AUX_HIDDEN
            + (long long)c * AUX_HIDDEN + k;
        grad_w_rec[off] += d_pre * h_old_local[k];
    }

    // grad_h_old[batch, c] direct decay contribution. OVERWRITE — the
    // trainer's per-step DtoD copy reads this immediately into the next
    // K iteration's carry buffer; no cross-K accumulation here.
    // (Cross-channel term Σ_j d_pre[j] * W_rec[j, c] is left for the
    // caller to add if BPTT through h_old is needed — for K=1 per-step
    // aux supervision this direct term is sufficient.)
    grad_h_old[batch * AUX_HIDDEN + c] = dh * decay;

    // grad_x[batch, k] = Σ_c d_pre[c] * W_in[c, k]
    // Cross-channel reduction via shared memory: stash d_pre to smem,
    // then have each thread compute its assigned k-slice by summing
    // across c. This avoids atomicAdd while keeping the reduction
    // block-local. (Per pearl_no_atomicadd + the block tree-reduce
    // pattern used in reduce_axis0.cu.)
    d_pre_smem[c] = d_pre;
    __syncthreads();

    // Each of the AUX_HIDDEN threads handles a strided subset of feat_dim
    // output positions. With feat_dim ≤ MAX_FEAT_DIM = 128 and
    // AUX_HIDDEN = 64, each thread covers at most 2 k positions.
    for (int k = c; k < feat_dim; k += AUX_HIDDEN) {
        float acc = 0.0f;
        for (int j = 0; j < AUX_HIDDEN; ++j) {
            acc += d_pre_smem[j] * w_in[j * feat_dim + k];
        }
        grad_x[batch * feat_dim + k] = acc;
    }
}
