Files
foxhunt/crates/ml-alpha/cuda/argmax_expected_q.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

87 lines
2.9 KiB
Plaintext

// argmax_expected_q.cu — GPU-resident argmax over expected Q-values
// per action (Phase R4 of the integrated RL trainer rebuild;
// see docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
//
// Companion to `rl_action_kernel.cu`. Per
// `pearl_thompson_for_distributional_action_selection`: Thompson is
// used for ROLLOUT action selection (rl_action_kernel) and argmax over
// EXPECTED Q is used for the Bellman-target argmax (this kernel). The
// distinction matters for the canonical Double-DQN-style backup:
//
// target Q = Q_target(s_{t+1}, argmax_a E[Q_online(s_{t+1}, a)])
//
// Replaces the host-side `argmax_f32(&action_expected)` loop the
// flawed Phase F shipped in step_with_lobsim — which violated
// `feedback_cpu_is_read_only` by DtoH-copying q_logits and reducing
// on CPU.
//
// Per-batch: softmax over each action's atoms, accumulate
// `expected[a] = Σ_i p_i · support[i]`, then argmax over the 9
// per-action expected values. Deterministic (no PRNG).
//
// Per `feedback_no_atomicadd`: thread 0 writes to `next_actions[b]`
// after __syncthreads.
#define N_ACTIONS 11
#define Q_N_ATOMS 21
// One block per batch (grid_dim.x = b_size). N_ACTIONS threads per
// block; each thread computes its action's expected Q and writes to
// shared mem; thread 0 argmaxes + writes next_actions[b].
//
// Inputs:
// q_logits [b_size, N_ACTIONS, Q_N_ATOMS] row-major
// atom_supports [Q_N_ATOMS]
// Outputs:
// next_actions [b_size] argmax over expected Q per batch
extern "C" __global__ void argmax_expected_q(
const float* __restrict__ q_logits,
const float* __restrict__ atom_supports,
int* __restrict__ next_actions,
int b_size
) {
const int b = blockIdx.x;
const int a = threadIdx.x;
if (b >= b_size) return;
if (a >= N_ACTIONS) return;
__shared__ float expected_q[N_ACTIONS];
// Softmax + expected value over this action's atoms.
const int row_off = (b * N_ACTIONS + a) * Q_N_ATOMS;
float max_l = -INFINITY;
#pragma unroll
for (int i = 0; i < Q_N_ATOMS; ++i) {
const float l = q_logits[row_off + i];
if (l > max_l) max_l = l;
}
float sum_exp = 0.0f;
#pragma unroll
for (int i = 0; i < Q_N_ATOMS; ++i) {
sum_exp += expf(q_logits[row_off + i] - max_l);
}
if (!isfinite(sum_exp) || sum_exp <= 0.0f) sum_exp = 1.0f;
float ev = 0.0f;
#pragma unroll
for (int i = 0; i < Q_N_ATOMS; ++i) {
const float p = expf(q_logits[row_off + i] - max_l) / sum_exp;
ev += p * atom_supports[i];
}
expected_q[a] = ev;
__syncthreads();
if (a == 0) {
int best_a = 0;
float best_v = expected_q[0];
#pragma unroll
for (int i = 1; i < N_ACTIONS; ++i) {
if (expected_q[i] > best_v) {
best_v = expected_q[i];
best_a = i;
}
}
next_actions[b] = best_a;
}
}