// rl_pi_grad_blend.cu — Phase 3D-C (2026-06-03): PPO surrogate × Q-distill
// gradient blend operator.
//
// Replaces the pre-Q-distill zero-fill of `ss_pi_grad_logits_d` with a
// conditional scale/zero step that lets the PPO clipped-surrogate
// gradient flow into π alongside the Q-distill term:
//
//     ENABLED (slot RL_PPO_SURROGATE_ENABLED_INDEX > 0.5):
//       pi_grad[i] *= weight (slot RL_PPO_SURROGATE_WEIGHT_INDEX)
//     DISABLED:
//       pi_grad[i]  = 0.0    (legacy zero-then-distill path)
//
// Q-distill then ADDS (`pi_grad_logits[i] += ...`) on top, producing:
//   pi_grad[i] = weight * grad_PPO[i] + grad_Q_distill[i]
//
// Why this is needed: per Goodhart-Skalse 2024 (Causes of Misalignment),
// the four-stage Q→π attenuation chain (V → Bellman target → softmax(Q/τ)
// → KL distill) cannot transmit small persistent fees back to π. Cao et
// al. 2026 (arXiv:2603.29086) showed restoring a direct policy-gradient
// channel (weight 1e-3 to 1e-2) cuts turnover 96% across TD3 / PPO / SAC
// when paired with quadratic impact costs.
//
// Per `feedback_no_atomicadd`: element-wise, one thread per logit.
// Per `feedback_no_nvrtc`: pre-compiled cubin via build.rs.

#include <stdint.h>

#define RL_PPO_SURROGATE_WEIGHT_INDEX   797
#define RL_PPO_SURROGATE_ENABLED_INDEX  798

extern "C" __global__ void rl_pi_grad_blend(
    float*       __restrict__ pi_grad_logits, // [B × N_ACTIONS] IN/OUT
    const float* __restrict__ isv,            // ISV bus (RO)
    int                       b_size,
    int                       n_actions
) {
    const int i = blockIdx.x * blockDim.x + threadIdx.x;
    const int total = b_size * n_actions;
    if (i >= total) return;

    const float enabled = isv[RL_PPO_SURROGATE_ENABLED_INDEX];
    if (enabled > 0.5f) {
        const float w = isv[RL_PPO_SURROGATE_WEIGHT_INDEX];
        pi_grad_logits[i] *= w;
    } else {
        pi_grad_logits[i] = 0.0f;
    }
}
