Files
foxhunt/crates/ml-alpha/cuda/log_pi_at_action.cu
jgrusewski d3175711b9 feat(rl): SP20 P4 — N_ACTIONS 9→11 with HalfFlat actions
Action enum extended:
  a9  = HalfFlatLong   (close ⌈|pos|/2⌉ of long position, no-op if not long)
  a10 = HalfFlatShort  (close ⌈|pos|/2⌉ of short position, no-op if not short)

`actions_to_market_targets.cu` extended with a9/a10 handlers:
  HalfFlatLong (pos > 0):  side=1 sell, size=max(1, (position_lots+1)/2)
  HalfFlatShort (pos < 0): side=0 buy,  size=max(1, (|position_lots|+1)/2)

Round-up division ensures min 1 lot closes — single-lot positions
fully close on HalfFlat (the half rounds up to 1).

N_ACTIONS=9 → 11 propagated to all 10 .cu kernels:
  argmax_expected_q, bellman_target_projection, dqn_distributional_q,
  log_pi_at_action, ppo_clipped_surrogate, rl_action_kernel,
  rl_entropy_coef_controller, rl_pi_action_kernel, rl_q_pi_agree_b,
  rl_q_pi_distill_grad

Rust-side N_ACTIONS const bumped to 11 in src/rl/common.rs.

CLI alpha_rl_train.rs action_hist + windowed_act_hist refactored
to reference `N_ACTIONS` const instead of literal 11. Caught DURING
this commit's dogfood: an intermediate state had `[0u32; 11]` but
left `(0..9).contains(&a)` unchanged — HalfFlat samples silently
dropped (a9/a10 showed 0% in diag despite P_MIN=0.02 floor
guaranteeing 2% each). Fix uses N_ACTIONS const everywhere; new
pearl `feedback_use_consts_not_literals_for_structural_dims`
codifies the meta-pattern (Rust code mirroring kernel structural
dims must reference the const, NEVER duplicate the literal —
audit-isv only scans .cu files, this class of bug is currently
unaudited in .rs).

Audits PASS:
  audit-isv: all kernel #defines allowlisted (BOOK_LEVELS,
             ACTION_*, structural dims)
  audit-wiring: all 4 actions in manifest (TrailTighten, TrailLoosen,
                HalfFlatLong, HalfFlatShort) have consumers

Local 1k-step smoke (RTX 3050 Ti, 13.5s):
  * Exit 0, 0 NaN/inf, 16000/16000 samples accounted for
  * Action distribution: all 11 used in 7-11% range
  * HalfFL=7.99%, HalfFS=7.51% — π samples them under multinomial
  * Trail=19.34% — agent continues to value trail-stop actions

Trail-stop check (rl_trail_stop_check.cu) currently still routes
force-close through a3/a4 (FlatFromLong/Short) rather than per-unit
partial-flat via a9/a10. That routing upgrade is SP20 P5b follow-up
work — it requires the per-unit close_unit_index buffer wiring per
spec §3 P5. Adding a9/a10 to the action space is the foundational
prerequisite; consumer kernel uses come with P5b.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 17:17:33 +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 11
// 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;
}