Files
foxhunt/crates/ml-alpha/cuda/log_pi_at_action.cu
jgrusewski 27feb94a49 feat(rl): R4 — GPU-resident action sampling kernels
Closes defect #5 prerequisites for the GPU-pure step_with_lobsim that
lands in R6. Replaces the host Thompson + host argmax + host
log-softmax loops the flawed Phase F shipped in step_with_lobsim
(which violated feedback_cpu_is_read_only with 5 DtoH copies + host
per-action loops + HtoD action upload per training step).

Three new kernels:

1. rl_action_kernel.cu — Thompson sampler over the C51 atom
   distribution. One block per batch, N_ACTIONS=9 threads. Each thread
   softmaxes its action's Q_N_ATOMS=21 atoms, samples one atom via CDF
   walk, writes sampled return to shared mem. Thread 0 argmaxes over
   per-action sampled returns + writes actions[b] + advances per-batch
   PRNG state.

   PRNG: per-batch xorshift32 state in prng_state_d (allocated +
   host-seeded from cfg.dqn_seed via ChaCha8 at trainer init per
   pearl_scoped_init_seed_for_reproducibility, with .max(1) guard
   since xorshift32 freezes at 0). Each per-action thread XORs its
   action index (golden-ratio mixed) into a thread-local copy of the
   per-batch state — no inter-thread race, reproducible by
   (cfg.dqn_seed, b_size, step_count). No cuRAND dep.

2. argmax_expected_q.cu — Bellman-target argmax over expected Q per
   action. Same layout as rl_action_kernel but deterministic (no
   PRNG). Per pearl_thompson_for_distributional_action_selection:
   Thompson for rollout (rl_action_kernel), argmax for Bellman target
   (this kernel) — distinct kernels, distinct ISV consumers.

3. log_pi_at_action.cu — per-batch log π(actions[b] | s_b) via
   log-softmax + lookup. One thread per batch entry (N_ACTIONS=9 is
   small enough for a per-thread sequential loop). Feeds the PPO
   importance ratio in R6.

IntegratedTrainer gains:
- 3 cubin includes (rl_action_kernel, argmax_expected_q, log_pi_at_action)
- 3 module/function field pairs
- 2 new device buffers populated at init:
    prng_state_d: CudaSlice<u32> of length n_batch
    atom_supports_d: CudaSlice<f32> of length Q_N_ATOMS=21,
      values [Q_V_MIN, Q_V_MIN + step, …, Q_V_MAX] = linspace(-1, +1, 21)
- 3 launcher methods:
    launch_rl_action_kernel(q_logits_d, actions_d, b_size)
    launch_argmax_expected_q(q_logits_d, next_actions_d, b_size)
    launch_log_pi_at_action(pi_logits_d, actions_d, log_pi_out_d, b_size)

GPU-oracle tests in tests/r4_action_kernels.rs (per
feedback_no_cpu_test_fallbacks every oracle is analytical, not a CPU
reference):

  R4.1: Thompson under sharp distribution (action 5 has logit=20 on
        atom 20 / support +1.0; others have logit=20 on atom 0 /
        support −1.0) collapses to argmax — per-action dominant-atom
        probability ≈ 1 − 4e-8, so 100/100 trials should pick action
        5. Assert ≥99/100 (tolerates one fp-rounding edge near
        u ≈ 1.0 in CDF walk).
  R4.2: argmax_expected_q picks the rewarded action under the same
        sharp distribution. Negative invariant: swap dominant atom to
        action 2 → next_action follows.
  R4.3: log_pi_at_action with π logits dominant at action 3 (logit=20,
        others=0) → log π(3) ≈ 0 within 1e-4. Negative invariant: log
        π(other action) ≈ −20 within 1e-3.

Build cache-bust v27.

cargo check + cargo build --tests on ml-alpha green (heads_bit_equiv
pre-existing failure persists).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 10:04:58 +02:00

56 lines
2.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// log_pi_at_action.cu — GPU-resident per-batch log π(action | s)
// computation for the PPO importance-ratio path (Phase R4 of the
// integrated RL trainer rebuild;
// see docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
//
// Replaces the host-side `pi_logits[action] log_sum_exp(pi_logits)`
// loop the flawed Phase F shipped in step_with_lobsim — which
// violated `feedback_cpu_is_read_only` by DtoH-copying π logits and
// computing log-softmax on CPU.
//
// Per-batch: log_pi[b] = pi_logits[b, actions[b]] log Σ_a exp(pi_logits[b, a]).
// Numerically stabilised via max-shift before exp.
//
// One thread per batch entry; element-wise. No reduction across
// batches, no atomics. N_ACTIONS = 9 fits in a per-thread sequential
// loop comfortably.
#define N_ACTIONS 9
// Inputs:
// pi_logits [b_size, N_ACTIONS] row-major
// actions [b_size] index into [0, N_ACTIONS)
// Outputs:
// log_pi_out [b_size]
extern "C" __global__ void log_pi_at_action(
const float* __restrict__ pi_logits,
const int* __restrict__ actions,
float* __restrict__ log_pi_out,
int b_size
) {
const int b = blockIdx.x * blockDim.x + threadIdx.x;
if (b >= b_size) return;
const int row_off = b * N_ACTIONS;
float max_l = -INFINITY;
#pragma unroll
for (int i = 0; i < N_ACTIONS; ++i) {
const float l = pi_logits[row_off + i];
if (l > max_l) max_l = l;
}
float sum_exp = 0.0f;
#pragma unroll
for (int i = 0; i < N_ACTIONS; ++i) {
sum_exp += expf(pi_logits[row_off + i] - max_l);
}
// log-sum-exp identity: log Σ exp(x) = max + log Σ exp(x max)
const float log_norm = max_l + logf(sum_exp);
const int chosen = actions[b];
// No bounds check on `chosen` per `feedback_no_quickfixes`: the
// upstream Thompson kernel (rl_action_kernel) writes into
// [0, N_ACTIONS) by construction. A bounds check here would mask
// an upstream contract violation; let it crash visibly.
log_pi_out[b] = pi_logits[row_off + chosen] - log_norm;
}