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>
100 lines
3.9 KiB
Plaintext
100 lines
3.9 KiB
Plaintext
// 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;
|
||
}
|
||
}
|