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>
87 lines
2.9 KiB
Plaintext
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;
|
|
}
|
|
}
|