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

100 lines
3.9 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.
// rl_q_pi_agree_b.cu — per-batch fraction of (argmax_a Q(s, a) ==
// argmax_a π(a|s)) folded into a streaming EMA at
// ISV[RL_Q_ARG_VS_PI_AGREE_INDEX = 407].
//
// Wires the previously-dead diagnostic slot 407. The slot's docstring
// promised "alarm fires if value < 0.5" but no producer ever wrote to
// it across all 16+ smokes through commit fe58b9f60 — slot stayed at
// sentinel 0.0 forever.
//
// What it measures: how often the DQN-derived greedy action agrees
// with the PPO-derived mode action. High agreement (≈1.0) = Q and π
// have learned the same policy ranking → they're consistent. Low
// agreement (<0.5) = Q and π disagree → either Q is wrong (PPO is
// outracing it) or π is wrong (DQN is more grounded). Either way an
// alarm-worthy diagnostic.
//
// Design: per-batch argmax over the q_logits / pi_logits tensors,
// then count matches, divide by b_size, EMA into slot 407 with
// LR_LOSS_EMA_ALPHA = 0.05 (slow smoothing, same as the LR
// controller's loss EMA — agreement signal is slow-moving).
//
// At b_size=1 this is trivial: one comparison per step, EMA'd over
// time. At b_size>1 it gives the fraction across the batch.
//
// Per `pearl_first_observation_bootstrap`: sentinel-zero state means
// "first observation"; replace directly with the current step's
// agreement (don't blend against zero).
//
// Per `pearl_no_atomicadd`: single-block kernel; tree reduce over
// per-thread argmax-match indicators.
#define RL_Q_ARG_VS_PI_AGREE_INDEX 407
#define Q_N_ATOMS 21
#define N_ACTIONS 11
#define AGREE_EMA_ALPHA 0.05f
extern "C" __global__ void rl_q_pi_agree_b(
const float* __restrict__ q_logits, // [b_size × N_ACTIONS × Q_N_ATOMS]
const float* __restrict__ pi_logits, // [b_size × N_ACTIONS]
float* __restrict__ isv, // [≥ 408]
int b_size
) {
extern __shared__ float s_match[];
const int tid = threadIdx.x;
// Per-thread: compare argmax(Q[tid]) to argmax(π[tid]) if tid < b_size,
// else contribute 0.0 (mean is taken over b_size real entries below).
float match = 0.0f;
if (tid < b_size) {
// Q argmax = argmax_a Σ_z atom[z] · softmax(Q_logits[a, z])
// Simpler approximation: argmax_a max_z Q_logits[a, z] (atom-mode).
// For the agreement diagnostic this is sufficient — we only need
// ranking consistency, not calibrated expectations.
int q_arg = 0;
float q_max = -INFINITY;
for (int a = 0; a < N_ACTIONS; ++a) {
float ma = -INFINITY;
for (int z = 0; z < Q_N_ATOMS; ++z) {
const float v = q_logits[tid * N_ACTIONS * Q_N_ATOMS
+ a * Q_N_ATOMS + z];
if (v > ma) ma = v;
}
if (ma > q_max) { q_max = ma; q_arg = a; }
}
int pi_arg = 0;
float pi_max = pi_logits[tid * N_ACTIONS + 0];
for (int a = 1; a < N_ACTIONS; ++a) {
const float v = pi_logits[tid * N_ACTIONS + a];
if (v > pi_max) { pi_max = v; pi_arg = a; }
}
match = (q_arg == pi_arg) ? 1.0f : 0.0f;
}
s_match[tid] = match;
__syncthreads();
// Tree reduce sum (caller enforces block_dim is power of 2 ≥ b_size).
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
if (tid < stride) {
s_match[tid] += s_match[tid + stride];
}
__syncthreads();
}
if (tid == 0) {
const float agree_frac = s_match[0] / (float)b_size;
const float prev = isv[RL_Q_ARG_VS_PI_AGREE_INDEX];
float out;
if (prev == 0.0f) {
// First-observation replace per pearl_first_observation_bootstrap.
out = agree_frac;
} else {
out = (1.0f - AGREE_EMA_ALPHA) * prev
+ AGREE_EMA_ALPHA * agree_frac;
}
isv[RL_Q_ARG_VS_PI_AGREE_INDEX] = out;
}
}