fix(class-a-p1-producer): adaptive Bayesian Kelly priors per-fold-end
Replace 4 hardcoded Bayesian priors in Kelly cap calculations
(prior_wins=2.0, prior_losses=2.0, prior_sum_wins=0.01,
prior_sum_losses=0.01) with ISV-driven slow-EMA values fed by a new
producer kernel that aggregates realized PS_KELLY_* fields across envs
from the same portfolio_state buffer kelly_cap_update_kernel reads
from at the same per-epoch boundary.
Slots [454..458):
- KELLY_PRIOR_WINS, KELLY_PRIOR_LOSSES,
KELLY_PRIOR_SUM_WINS, KELLY_PRIOR_SUM_LOSSES
ISV_TOTAL_DIM bumps 454 -> 458.
Producer:
- kelly_bayesian_priors_update_kernel.cu (per-fold-end / per-epoch
boundary, block-tree-reduce in shmem, no atomicAdd). Pearl-A
first-observation bootstrap (sentinels 2.0/2.0/0.01/0.01 match
pre-P1-Producer hardcoded values for bit-identical cold-start) +
alpha=0.005 slow EMA. Bounds counts in [0.5, 100], sums in [0.001,
1.0] per feedback_isv_for_adaptive_bounds. Launched RIGHT BEFORE
launch_kelly_cap_update so that kernel sees the freshly-blended
priors.
Consumers migrated atomically (same commit):
- kelly_cap_update_kernel.cu:39-42 -> ISV[454..458) with cold-start
fallback via kelly_prior_or_default helper (range guard).
- trade_physics.cuh::kelly_position_cap:304-307 -> NULL-tolerant
isv_signals_ptr threaded through apply_kelly_cap (single caller in
unified_env_step_core line 898 already had the bus pointer; mirrors
the existing kelly_f_smooth / kelly_warmup_floor_sp9 patterns).
State reset:
- 4 FoldReset registry entries (sp14_p1_kelly_prior_*) +
4 dispatch arms in training_loop.rs::reset_named_state.
Oracle tests (sp14_oracle_tests.rs, GPU-gated #[ignore]):
- Pearl-A bootstrap (sentinel -> REPLACE with aggregated targets)
- No realized trades -> ISV preserved bit-exactly
- Bounds clamp on extreme aggregates
- Slow EMA blend after bootstrap
CPU tests passing:
- sp14_p1_kelly_prior_slot_layout_locked
- all_sp14_p1_slots_fit_within_isv_total_dim
- every_fold_and_soft_reset_entry_has_dispatch_arm (C.10 lesson)
- layout_fingerprint_bumps_after_sp14_wire
DEFERRED — Item 2 (MIN_HOLD_TEMPERATURE EMA): the audit-spec said
"MIN_HOLD_TEMPERATURE = 0.5f hardcoded somewhere" but the actual code
has MIN_HOLD_TEMPERATURE_{START=50.0f, END=5.0f, DECAY=20.0f} as a
PER-EPOCH ANNEALING SCHEDULE driven by min_hold_temperature_for_epoch
in training_loop.rs:68-73. The kernel takes T as a runtime scalar
specifically to enable Phase 2 ISV-driven lift "without recompiling
cubin" per the SP12 v3 design comment. The audit's claim of "0.5f
hardcoded" does not match reality; the right Phase 2 signal is a
separate spec decision and should not be guessed at per
feedback_no_quickfixes. Reporting back per the prompt's hard rule #8.
Per feedback_isv_for_adaptive_bounds + feedback_no_partial_refactor +
pearl_controller_anchors_isv_driven.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -806,6 +806,27 @@ fn main() {
|
||||
// WR-stuck-at-46-48% plateau across 11 superprojects. Per-epoch
|
||||
// boundary launch.
|
||||
"reward_cap_update_kernel.cu",
|
||||
// Class A P1-Producer (2026-05-08): adaptive Bayesian Kelly priors
|
||||
// producer. Single-block kernel that sweeps the per-fold-end
|
||||
// `portfolio_state[n_envs, PS_STRIDE]` buffer (same source the
|
||||
// `kelly_cap_update_kernel` reads from at the same boundary),
|
||||
// aggregates the realized PS_KELLY_{WIN_COUNT, LOSS_COUNT,
|
||||
// SUM_WINS, SUM_LOSSES} fields across envs, and slow-EMA-blends
|
||||
// into ISV[454..458). Pearl-A first-observation bootstrap
|
||||
// (sentinels 2.0/2.0/0.01/0.01 match pre-P1-Producer hardcoded
|
||||
// values for bit-identical cold-start); α=0.005 slow EMA
|
||||
// thereafter (per-fold cadence — the Bayesian prior is a *prior
|
||||
// belief* and should change slowly across folds; a fast
|
||||
// "prior" would just be a noisy Kelly estimate). Bounds
|
||||
// counts∈[0.5, 100], sums∈[0.001, 1.0] are Category-1 dimensional
|
||||
// safety floors per `feedback_isv_for_adaptive_bounds`. Replaces
|
||||
// hardcoded `prior_wins=2.0f / prior_losses=2.0f /
|
||||
// prior_sum_wins=0.01f / prior_sum_losses=0.01f` in
|
||||
// kelly_cap_update_kernel.cu:39-42 + trade_physics.cuh::
|
||||
// kelly_position_cap:304-307 — Class A audit P1-Producer batch.
|
||||
// Block-tree-reduce (no atomicAdd) per
|
||||
// `feedback_no_atomicadd.md`. Per-epoch boundary launch.
|
||||
"kelly_bayesian_priors_update_kernel.cu",
|
||||
// SP14 Layer C Phase C.6 (2026-05-08): h_s2_aux RMS EMA producer.
|
||||
// Single-block 256-thread kernel computing RMS = sqrt(mean(x²))
|
||||
// over `h_s2_aux [B, SH2]` (aux trunk final output, no activation)
|
||||
|
||||
@@ -52,7 +52,8 @@ use crate::MLError;
|
||||
|
||||
use super::gpu_dqn_trainer::{
|
||||
AUX_HORIZON_UPDATE_CUBIN, AUX_TRUNK_BACKWARD_CUBIN, AUX_TRUNK_FORWARD_CUBIN,
|
||||
AVG_WIN_HOLD_TIME_UPDATE_CUBIN, H_S2_AUX_RMS_EMA_CUBIN, REWARD_CAP_UPDATE_CUBIN,
|
||||
AVG_WIN_HOLD_TIME_UPDATE_CUBIN, H_S2_AUX_RMS_EMA_CUBIN, KELLY_BAYESIAN_PRIORS_UPDATE_CUBIN,
|
||||
REWARD_CAP_UPDATE_CUBIN,
|
||||
};
|
||||
|
||||
/// Hidden width of the aux trunk's first internal layer (Linear_1 → ELU
|
||||
@@ -734,6 +735,128 @@ impl RewardCapUpdateOps {
|
||||
}
|
||||
}
|
||||
|
||||
/// Class A P1-Producer (2026-05-08): adaptive Bayesian Kelly priors producer.
|
||||
///
|
||||
/// Replaces the previously-hardcoded `prior_wins=2.0f, prior_losses=2.0f,
|
||||
/// prior_sum_wins=0.01f, prior_sum_losses=0.01f` constants used in
|
||||
/// `kelly_cap_update_kernel.cu` and `trade_physics.cuh::kelly_position_cap`
|
||||
/// with ISV-driven slow-EMA values fed by the
|
||||
/// `kelly_bayesian_priors_update_kernel` producer at per-fold-end (per-epoch
|
||||
/// boundary in the current scheduling). Aggregates the realized
|
||||
/// PS_KELLY_{WIN_COUNT, LOSS_COUNT, SUM_WINS, SUM_LOSSES} fields across all
|
||||
/// envs from the same `portfolio_state[n_envs, PS_STRIDE]` buffer that
|
||||
/// `kelly_cap_update_kernel` reads from at the same boundary, then
|
||||
/// slow-EMA-blends into ISV[KELLY_PRIOR_*_INDEX] = ISV[454..458).
|
||||
///
|
||||
/// Single-block 256-thread kernel; shmem block-tree-reduce
|
||||
/// (no atomicAdd per `feedback_no_atomicadd.md`). `CudaFunction` pre-loaded
|
||||
/// at construction per `pearl_no_host_branches_in_captured_graph.md`.
|
||||
///
|
||||
/// Pearls applied:
|
||||
/// - `feedback_no_atomicadd.md` — block-tree-reduce in shmem, single
|
||||
/// global write per slot from thread 0.
|
||||
/// - `pearl_first_observation_bootstrap.md` — sentinel match (within 1e-6
|
||||
/// of pre-P1-Producer hardcoded value) → REPLACE; otherwise EMA blend.
|
||||
/// - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||||
/// pre-loaded at construction; on-device guard for "no realized trades".
|
||||
/// - `feedback_no_stubs.md` — full body, no placeholder.
|
||||
/// - `feedback_isv_for_adaptive_bounds.md` — count bounds [0.5, 100],
|
||||
/// sum bounds [0.001, 1.0] are dimensional safety floors, NOT tuning.
|
||||
/// - `pearl_symmetric_clamp_audit.md` — bilateral
|
||||
/// `fmaxf(lo, fminf(x, hi))` clamp on each slot before writing.
|
||||
/// - `pearl_controller_anchors_isv_driven.md` — every controller anchor
|
||||
/// (priors are anchors for the Kelly cap formula) is ISV-driven.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub(crate) struct KellyBayesianPriorsUpdateOps {
|
||||
update_kernel: CudaFunction,
|
||||
}
|
||||
|
||||
impl KellyBayesianPriorsUpdateOps {
|
||||
/// Block dim used by the producer kernel — must match `BLK_DIM` in
|
||||
/// `kelly_bayesian_priors_update_kernel.cu`.
|
||||
const BLK_DIM: u32 = 256;
|
||||
|
||||
pub(crate) fn new(stream: &Arc<CudaStream>) -> Result<Self, MLError> {
|
||||
let context = stream.context();
|
||||
let module = context
|
||||
.load_cubin(KELLY_BAYESIAN_PRIORS_UPDATE_CUBIN.to_vec())
|
||||
.map_err(|e| MLError::ModelError(format!("kelly_bayesian_priors_update cubin load: {e}")))?;
|
||||
let update_kernel = module
|
||||
.load_function("kelly_bayesian_priors_update")
|
||||
.map_err(|e| MLError::ModelError(format!("kelly_bayesian_priors_update load: {e}")))?;
|
||||
Ok(Self { update_kernel })
|
||||
}
|
||||
|
||||
/// Launch the adaptive Bayesian-priors producer.
|
||||
///
|
||||
/// Args:
|
||||
/// - `portfolio_state_ptr`: f32 device ptr `[n_envs * ps_stride]` —
|
||||
/// the same buffer `kelly_cap_update_kernel` reads from. We use
|
||||
/// the four PS_KELLY_* fields per env.
|
||||
/// - `n_envs`: number of envs (alloc_episodes from the collector).
|
||||
/// - `ps_stride`: PS_STRIDE from state_layout.cuh (43 post-Plan-3-D.4c).
|
||||
/// - `isv_ptr`: ISV[ISV_TOTAL_DIM] device pointer (mapped-pinned).
|
||||
/// - `wins_idx`/`losses_idx`/`sum_wins_idx`/`sum_losses_idx`:
|
||||
/// KELLY_PRIOR_*_INDEX (454..458).
|
||||
/// - `sentinel_*`: SENTINEL_KELLY_PRIOR_* (2.0/2.0/0.01/0.01).
|
||||
/// - `count_min`/`count_max`: KELLY_PRIOR_COUNT_MIN/MAX (0.5, 100).
|
||||
/// - `sum_min`/`sum_max`: KELLY_PRIOR_SUM_MIN/MAX (0.001, 1.0).
|
||||
/// - `alpha`: KELLY_PRIOR_EMA_ALPHA (0.005, slow per-fold cadence).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn launch(
|
||||
&self,
|
||||
stream: &Arc<CudaStream>,
|
||||
portfolio_state_ptr: u64,
|
||||
n_envs: i32,
|
||||
ps_stride: i32,
|
||||
isv_ptr: u64,
|
||||
wins_idx: i32,
|
||||
losses_idx: i32,
|
||||
sum_wins_idx: i32,
|
||||
sum_losses_idx: i32,
|
||||
sentinel_wins: f32,
|
||||
sentinel_losses: f32,
|
||||
sentinel_sum_wins: f32,
|
||||
sentinel_sum_losses: f32,
|
||||
count_min: f32,
|
||||
count_max: f32,
|
||||
sum_min: f32,
|
||||
sum_max: f32,
|
||||
alpha: f32,
|
||||
) -> Result<(), MLError> {
|
||||
// 4 arrays × BLK_DIM × sizeof(f32) — see the kernel comment block.
|
||||
let smem_bytes = 4 * Self::BLK_DIM * std::mem::size_of::<f32>() as u32;
|
||||
unsafe {
|
||||
stream
|
||||
.launch_builder(&self.update_kernel)
|
||||
.arg(&portfolio_state_ptr)
|
||||
.arg(&n_envs)
|
||||
.arg(&ps_stride)
|
||||
.arg(&isv_ptr)
|
||||
.arg(&wins_idx)
|
||||
.arg(&losses_idx)
|
||||
.arg(&sum_wins_idx)
|
||||
.arg(&sum_losses_idx)
|
||||
.arg(&sentinel_wins)
|
||||
.arg(&sentinel_losses)
|
||||
.arg(&sentinel_sum_wins)
|
||||
.arg(&sentinel_sum_losses)
|
||||
.arg(&count_min)
|
||||
.arg(&count_max)
|
||||
.arg(&sum_min)
|
||||
.arg(&sum_max)
|
||||
.arg(&alpha)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (1, 1, 1),
|
||||
block_dim: (Self::BLK_DIM, 1, 1),
|
||||
shared_mem_bytes: smem_bytes,
|
||||
})
|
||||
.map_err(|e| MLError::ModelError(format!("kelly_bayesian_priors_update: {e}")))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// SP14 Layer C Phase C.6 (2026-05-08): h_s2_aux RMS EMA producer.
|
||||
///
|
||||
/// Computes `RMS(h_s2_aux) = sqrt(mean(h_s2_aux²))` over the
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,250 @@
|
||||
/* ══════════════════════════════════════════════════════════════════════════
|
||||
* Class A P1-Producer — adaptive Bayesian Kelly priors producer (2026-05-08).
|
||||
*
|
||||
* Replaces the hardcoded Bayesian priors used in Kelly cap calculations:
|
||||
* - `kelly_cap_update_kernel.cu:39-42` (epoch-boundary effective Kelly cap)
|
||||
* - `trade_physics.cuh::kelly_position_cap:304-307` (per-step env cap)
|
||||
*
|
||||
* Both consumers ran with `prior_wins=2.0f, prior_losses=2.0f,
|
||||
* prior_sum_wins=0.01f, prior_sum_losses=0.01f` — a static "I don't know"
|
||||
* Bayesian prior frozen at sprint-1 defaults. The cold-start of every new
|
||||
* fold ignored what prior folds had learned about the realized Kelly
|
||||
* distribution; the prior was *literally* the prior (in the Bayesian sense),
|
||||
* but it was a fixed scalar, not an adaptive belief.
|
||||
*
|
||||
* This producer aggregates realized Kelly stats from the existing
|
||||
* `portfolio_state[n_envs, PS_STRIDE]` buffer — same source the
|
||||
* `kelly_cap_update_kernel` already reads from at the same per-epoch
|
||||
* boundary. The four PS slots (`PS_KELLY_WIN_COUNT`,
|
||||
* `PS_KELLY_LOSS_COUNT`, `PS_KELLY_SUM_WINS`, `PS_KELLY_SUM_LOSSES`) are
|
||||
* realized counts/sums per env; we aggregate across envs and slow-EMA-blend
|
||||
* into ISV[454..458). On the next fold's cold-start path, the consumer
|
||||
* reads the EMA prior (a learned belief about the distribution shape) instead
|
||||
* of the fixed sprint-1 default.
|
||||
*
|
||||
* Ordering: launched per-fold-end (currently per-epoch boundary in the
|
||||
* scheduling chain in `training_loop.rs`) BEFORE `kelly_cap_update` so the
|
||||
* effective-Kelly-cap kernel sees the freshly-blended priors. The launch
|
||||
* site lives at the same epoch-boundary block as `kelly_cap_update` and
|
||||
* `reward_cap_update`.
|
||||
*
|
||||
* Algorithm:
|
||||
*
|
||||
* Phase 1 (block-tree-reduce): each thread of the single block sweeps
|
||||
* its tile of `n_envs` portfolio_state slots, accumulating
|
||||
* local_win_count, local_loss_count, local_sum_wins, local_sum_losses
|
||||
* from the four PS_KELLY_* fields. Block-tree-reduce in shmem (no
|
||||
* atomicAdd per `feedback_no_atomicadd.md`) yields the four sums in
|
||||
* thread 0.
|
||||
*
|
||||
* Phase 2 (single thread, EMA blend): if no realized trades observed
|
||||
* (total_win_count + total_loss_count == 0), keep ISV slots unchanged
|
||||
* (cold-start sentinel persists; consumers fall back to the static
|
||||
* defaults via the NULL-tolerant guard in `kelly_position_cap` /
|
||||
* `kelly_cap_update_kernel`). Otherwise:
|
||||
*
|
||||
* target_wins = total_win_count
|
||||
* target_losses = total_loss_count
|
||||
* target_sum_wins = total_sum_wins
|
||||
* target_sum_losses = total_sum_losses
|
||||
*
|
||||
* Pearl-A first-observation bootstrap: each slot independently checks
|
||||
* `|current - sentinel| < EPS_F`; if so, REPLACE with target. Otherwise
|
||||
* slow EMA blend with α=0.005 (per-fold cadence — the Bayesian prior
|
||||
* is a *prior belief* and should change slowly across folds; a fast
|
||||
* "prior" would just be a noisy Kelly estimate). Per
|
||||
* `pearl_first_observation_bootstrap.md` and
|
||||
* `pearl_controller_anchors_isv_driven.md`.
|
||||
*
|
||||
* Bilateral clamp on each slot per `pearl_symmetric_clamp_audit.md`:
|
||||
* - counts in [0.5, 100]: below 0.5 the prior is effectively absent
|
||||
* (kelly_f swings on first real trade); above 100 the prior
|
||||
* dominates 10+ real trades (the maturity threshold for warmup_floor
|
||||
* blend in kelly_position_cap).
|
||||
* - sums in [0.001, 1.0]: below 1e-3 hits the same numerical floor
|
||||
* as `fmaxf(avg_loss, 0.0001f)` in the Kelly formula; above 1.0
|
||||
* implies a per-trade win/loss > 100% of equity which would be
|
||||
* risk-management-failure territory anyway.
|
||||
*
|
||||
* Pearls + invariants:
|
||||
*
|
||||
* - `feedback_no_atomicadd.md` — single-block kernel; block-tree-reduce
|
||||
* in shmem; no atomics across blocks.
|
||||
*
|
||||
* - `pearl_no_host_branches_in_captured_graph.md` — `CudaFunction`
|
||||
* pre-loaded at construction; on-device guards for "no realized
|
||||
* trades" and Pearl-A sentinel detection.
|
||||
*
|
||||
* - `pearl_first_observation_bootstrap.md` — first valid observation
|
||||
* replaces sentinel directly; no blend.
|
||||
*
|
||||
* - `feedback_no_stubs.md` — full body, no return-zero placeholders.
|
||||
*
|
||||
* - `feedback_isv_for_adaptive_bounds.md` — count bounds [0.5, 100],
|
||||
* sum bounds [0.001, 1.0] are dimensional safety floors, NOT tuning.
|
||||
*
|
||||
* - `pearl_symmetric_clamp_audit.md` — bilateral
|
||||
* `fmaxf(lo, fminf(x, hi))` clamp on each slot before writing.
|
||||
*
|
||||
* - `pearl_controller_anchors_isv_driven.md` — every controller anchor
|
||||
* (the priors are anchors for the Kelly cap formula) is ISV-driven.
|
||||
*
|
||||
* Args:
|
||||
* portfolio_state — `[n_envs * ps_stride]` device f32; the
|
||||
* same buffer `kelly_cap_update_kernel`
|
||||
* already reads. We use the four
|
||||
* PS_KELLY_* fields per env.
|
||||
* n_envs — number of envs (alloc_episodes from the
|
||||
* collector).
|
||||
* ps_stride — PS_STRIDE from state_layout.cuh (43
|
||||
* post-Plan-3-D.4c).
|
||||
* isv — `[ISV_TOTAL_DIM]` device f32, modified
|
||||
* in place at indices `*_idx`.
|
||||
* wins_idx — KELLY_PRIOR_WINS_INDEX (454).
|
||||
* losses_idx — KELLY_PRIOR_LOSSES_INDEX (455).
|
||||
* sum_wins_idx — KELLY_PRIOR_SUM_WINS_INDEX (456).
|
||||
* sum_losses_idx — KELLY_PRIOR_SUM_LOSSES_INDEX (457).
|
||||
* sentinel_wins — SENTINEL_KELLY_PRIOR_WINS (2.0).
|
||||
* sentinel_losses — SENTINEL_KELLY_PRIOR_LOSSES (2.0).
|
||||
* sentinel_sum_wins — SENTINEL_KELLY_PRIOR_SUM_WINS (0.01).
|
||||
* sentinel_sum_losses — SENTINEL_KELLY_PRIOR_SUM_LOSSES (0.01).
|
||||
* count_min, count_max — KELLY_PRIOR_COUNT_MIN/MAX (0.5, 100).
|
||||
* sum_min, sum_max — KELLY_PRIOR_SUM_MIN/MAX (0.001, 1.0).
|
||||
* alpha — KELLY_PRIOR_EMA_ALPHA (0.005, slow).
|
||||
*
|
||||
* Launch: grid=(1, 1, 1), block=(BLK_DIM=256, 1, 1).
|
||||
* Shared memory: 4 * BLK_DIM floats:
|
||||
* sh_win_count — sum of PS_KELLY_WIN_COUNT across envs
|
||||
* sh_loss_count — sum of PS_KELLY_LOSS_COUNT across envs
|
||||
* sh_sum_wins — sum of PS_KELLY_SUM_WINS across envs
|
||||
* sh_sum_losses — sum of PS_KELLY_SUM_LOSSES across envs
|
||||
* ══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
#include "state_layout.cuh" /* PS_KELLY_WIN_COUNT, PS_KELLY_LOSS_COUNT,
|
||||
* PS_KELLY_SUM_WINS, PS_KELLY_SUM_LOSSES */
|
||||
|
||||
#define BLK_DIM 256
|
||||
#define EPS_F 1e-6f
|
||||
|
||||
extern "C" __global__
|
||||
void kelly_bayesian_priors_update(
|
||||
const float* __restrict__ portfolio_state,
|
||||
int n_envs,
|
||||
int ps_stride,
|
||||
float* __restrict__ isv,
|
||||
int wins_idx,
|
||||
int losses_idx,
|
||||
int sum_wins_idx,
|
||||
int sum_losses_idx,
|
||||
float sentinel_wins,
|
||||
float sentinel_losses,
|
||||
float sentinel_sum_wins,
|
||||
float sentinel_sum_losses,
|
||||
float count_min,
|
||||
float count_max,
|
||||
float sum_min,
|
||||
float sum_max,
|
||||
float alpha)
|
||||
{
|
||||
/* Single-block kernel — only block 0 does anything. */
|
||||
if (blockIdx.x != 0) return;
|
||||
|
||||
extern __shared__ float shmem[];
|
||||
float* sh_win_count = shmem; /* [BLK_DIM] */
|
||||
float* sh_loss_count = shmem + BLK_DIM; /* [BLK_DIM] */
|
||||
float* sh_sum_wins = shmem + 2 * BLK_DIM; /* [BLK_DIM] */
|
||||
float* sh_sum_losses = shmem + 3 * BLK_DIM; /* [BLK_DIM] */
|
||||
|
||||
int tid = threadIdx.x;
|
||||
|
||||
/* Phase 1a: stride-based sweep — accumulate the four Kelly stats
|
||||
* across all envs. PS_KELLY_* indices come from state_layout.cuh
|
||||
* (the same constants kelly_cap_update_kernel uses). Counts are
|
||||
* stored as floats in PS slots (matches Kelly stat accumulator
|
||||
* convention in apply_kelly_cap). */
|
||||
float local_wins = 0.0f;
|
||||
float local_losses = 0.0f;
|
||||
float local_sumw = 0.0f;
|
||||
float local_suml = 0.0f;
|
||||
for (int e = tid; e < n_envs; e += BLK_DIM) {
|
||||
const float* ps = portfolio_state + (long long)e * ps_stride;
|
||||
local_wins += ps[PS_KELLY_WIN_COUNT];
|
||||
local_losses += ps[PS_KELLY_LOSS_COUNT];
|
||||
local_sumw += ps[PS_KELLY_SUM_WINS];
|
||||
local_suml += ps[PS_KELLY_SUM_LOSSES];
|
||||
}
|
||||
sh_win_count[tid] = local_wins;
|
||||
sh_loss_count[tid] = local_losses;
|
||||
sh_sum_wins[tid] = local_sumw;
|
||||
sh_sum_losses[tid] = local_suml;
|
||||
__syncthreads();
|
||||
|
||||
/* Phase 1b: block-tree-reduce (no atomicAdd) — halve every iter.
|
||||
* All four streams fold via addition. */
|
||||
for (int s = BLK_DIM >> 1; s > 0; s >>= 1) {
|
||||
if (tid < s) {
|
||||
sh_win_count[tid] += sh_win_count[tid + s];
|
||||
sh_loss_count[tid] += sh_loss_count[tid + s];
|
||||
sh_sum_wins[tid] += sh_sum_wins[tid + s];
|
||||
sh_sum_losses[tid] += sh_sum_losses[tid + s];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
/* Phase 2: thread 0 finalises EMA + Pearl-A + clamp + writes the four slots. */
|
||||
if (tid != 0) return;
|
||||
|
||||
float total_wins = sh_win_count[0];
|
||||
float total_losses = sh_loss_count[0];
|
||||
float total_sumw = sh_sum_wins[0];
|
||||
float total_suml = sh_sum_losses[0];
|
||||
|
||||
/* Guard: no realized trades observed across all envs — keep ISV slots
|
||||
* unchanged (cold-start sentinel persists; consumers fall back to
|
||||
* the static defaults via the NULL-tolerant guard pattern). */
|
||||
if (total_wins + total_losses <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Bilateral pre-EMA clamp on the targets so the blended value stays in
|
||||
* the dimensional-safety bounds even before the post-EMA defensive
|
||||
* clamp. Per `pearl_symmetric_clamp_audit.md`. */
|
||||
float target_wins = fmaxf(count_min, fminf(total_wins, count_max));
|
||||
float target_losses = fmaxf(count_min, fminf(total_losses, count_max));
|
||||
float target_sumw = fmaxf(sum_min, fminf(total_sumw, sum_max));
|
||||
float target_suml = fmaxf(sum_min, fminf(total_suml, sum_max));
|
||||
|
||||
/* Pearl-A first-observation bootstrap on each slot independently.
|
||||
* The sentinel matches the pre-P1-Producer hardcoded constants
|
||||
* (2.0/2.0/0.01/0.01) for bit-identical cold-start behavior. */
|
||||
float cur_wins = isv[wins_idx];
|
||||
float cur_losses = isv[losses_idx];
|
||||
float cur_sumw = isv[sum_wins_idx];
|
||||
float cur_suml = isv[sum_losses_idx];
|
||||
|
||||
float blended_wins = (fabsf(cur_wins - sentinel_wins) < EPS_F)
|
||||
? target_wins
|
||||
: ((1.0f - alpha) * cur_wins + alpha * target_wins);
|
||||
float blended_losses = (fabsf(cur_losses - sentinel_losses) < EPS_F)
|
||||
? target_losses
|
||||
: ((1.0f - alpha) * cur_losses + alpha * target_losses);
|
||||
float blended_sumw = (fabsf(cur_sumw - sentinel_sum_wins) < EPS_F)
|
||||
? target_sumw
|
||||
: ((1.0f - alpha) * cur_sumw + alpha * target_sumw);
|
||||
float blended_suml = (fabsf(cur_suml - sentinel_sum_losses) < EPS_F)
|
||||
? target_suml
|
||||
: ((1.0f - alpha) * cur_suml + alpha * target_suml);
|
||||
|
||||
/* Defensive post-EMA bilateral clamp — handles malformed prior state
|
||||
* outside the dimensional-safety range. */
|
||||
blended_wins = fmaxf(count_min, fminf(blended_wins, count_max));
|
||||
blended_losses = fmaxf(count_min, fminf(blended_losses, count_max));
|
||||
blended_sumw = fmaxf(sum_min, fminf(blended_sumw, sum_max));
|
||||
blended_suml = fmaxf(sum_min, fminf(blended_suml, sum_max));
|
||||
|
||||
isv[wins_idx] = blended_wins;
|
||||
isv[losses_idx] = blended_losses;
|
||||
isv[sum_wins_idx] = blended_sumw;
|
||||
isv[sum_losses_idx] = blended_suml;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/* kelly_cap_update — effective Kelly cap, GPU-driven (Plan 1 Task 11).
|
||||
* Reads: ISV[LEARNING_HEALTH_INDEX=12] (safety multiplier coupled to health)
|
||||
* ISV[KELLY_PRIOR_*_INDEX=454..458) (Class A P1-Producer adaptive
|
||||
* Bayesian priors — slow-EMA from realized fold trades)
|
||||
* portfolio_state[n_envs, PS_STRIDE] — Kelly win/loss stats per env
|
||||
* Writes: ISV[KELLY_CAP_EFF_INDEX=47] (shifted +3 from 44 by D.2 per-branch gamma, Plan 2 Task 3)
|
||||
* Cold path (per-epoch boundary). Single-thread kernel.
|
||||
@@ -9,9 +11,14 @@
|
||||
* health-coupled safety multiplier. Result is a scalar cap in [0, 1].
|
||||
* Downstream kernel readers use this cap to limit position sizing.
|
||||
*
|
||||
* Bayesian priors (matching trade_physics.cuh):
|
||||
* prior_wins=2, prior_losses=2, prior_sum_wins=0.01, prior_sum_losses=0.01
|
||||
* This ensures the cap never collapses to zero on cold start.
|
||||
* Bayesian priors (Class A P1-Producer 2026-05-08): now read from
|
||||
* ISV[454..458) where they are produced by `kelly_bayesian_priors_update_kernel`
|
||||
* at the SAME per-epoch boundary, launched RIGHT BEFORE this kernel so the
|
||||
* cold-path Kelly cap sees the freshly-blended priors. Sentinels match
|
||||
* the pre-P1-Producer hardcoded constants
|
||||
* (2.0 / 2.0 / 0.01 / 0.01) for bit-identical cold-start behavior. The
|
||||
* fallback path below applies when an ISV slot is at sentinel (Pearl-A
|
||||
* pre-bootstrap) or out of the dimensional-safety range — defensive only.
|
||||
*/
|
||||
#include "state_layout.cuh"
|
||||
|
||||
@@ -27,6 +34,21 @@
|
||||
* Slot constant mirrors `sp5_isv_slots.rs::KELLY_WARMUP_FLOOR_INDEX = 330`. */
|
||||
#define SP9_KELLY_WARMUP_FLOOR_INDEX 330
|
||||
|
||||
/* Class A P1-Producer cold-start fallback helper. Returns the ISV slot value
|
||||
* if it is within the dimensional-safety range, else the static default
|
||||
* (the pre-P1-Producer hardcoded value for bit-identical cold-start). */
|
||||
__device__ __forceinline__ float kelly_prior_or_default(
|
||||
float isv_val,
|
||||
float fallback_default,
|
||||
float lo_bound,
|
||||
float hi_bound
|
||||
) {
|
||||
if (isv_val >= lo_bound && isv_val <= hi_bound) {
|
||||
return isv_val;
|
||||
}
|
||||
return fallback_default;
|
||||
}
|
||||
|
||||
extern "C" __global__ void kelly_cap_update(
|
||||
const float* __restrict__ isv,
|
||||
float* __restrict__ isv_out,
|
||||
@@ -36,10 +58,20 @@ extern "C" __global__ void kelly_cap_update(
|
||||
) {
|
||||
if (threadIdx.x != 0 || blockIdx.x != 0) return;
|
||||
|
||||
const float prior_wins = 2.0f;
|
||||
const float prior_losses = 2.0f;
|
||||
const float prior_sum_wins = 0.01f;
|
||||
const float prior_sum_losses = 0.01f;
|
||||
/* Class A P1-Producer (2026-05-08): adaptive Bayesian priors from
|
||||
* ISV[454..458). Producer kernel `kelly_bayesian_priors_update_kernel`
|
||||
* runs at the same per-epoch boundary RIGHT BEFORE this kernel.
|
||||
* Cold-start path: ISV slot at sentinel (Pearl-A pre-bootstrap) or
|
||||
* out-of-range → fall back to the static default (matches pre-P1-Producer
|
||||
* hardcoded values for bit-identical cold-start behavior). */
|
||||
const float prior_wins = kelly_prior_or_default(
|
||||
isv[KELLY_PRIOR_WINS_INDEX], KELLY_PRIOR_WINS_DEFAULT, 0.5f, 100.0f);
|
||||
const float prior_losses = kelly_prior_or_default(
|
||||
isv[KELLY_PRIOR_LOSSES_INDEX], KELLY_PRIOR_LOSSES_DEFAULT, 0.5f, 100.0f);
|
||||
const float prior_sum_wins = kelly_prior_or_default(
|
||||
isv[KELLY_PRIOR_SUM_WINS_INDEX], KELLY_PRIOR_SUM_WINS_DEFAULT, 0.001f, 1.0f);
|
||||
const float prior_sum_losses = kelly_prior_or_default(
|
||||
isv[KELLY_PRIOR_SUM_LOSSES_INDEX], KELLY_PRIOR_SUM_LOSSES_DEFAULT, 0.001f, 1.0f);
|
||||
|
||||
float health = fminf(fmaxf(isv[12], 0.0f), 1.0f);
|
||||
/* Safety multiplier: 0.5 + 0.5*health — collapses cap during degradation */
|
||||
|
||||
@@ -164,6 +164,65 @@ pub const REWARD_CAP_EMA_ALPHA: f32 = 0.01;
|
||||
pub const SP14_P0A_SLOT_BASE: usize = 452;
|
||||
pub const SP14_P0A_SLOT_END: usize = 454;
|
||||
|
||||
// ── Class A P1-Producer (2026-05-08): adaptive Bayesian Kelly priors ─────
|
||||
// Replaces the hardcoded
|
||||
// `prior_wins=2.0, prior_losses=2.0, prior_sum_wins=0.01, prior_sum_losses=0.01`
|
||||
// in `kelly_cap_update_kernel.cu:39-42` AND
|
||||
// `trade_physics.cuh::kelly_position_cap:304-307` with ISV-driven slow-EMA
|
||||
// values produced by `kelly_bayesian_priors_update_kernel.cu` at per-fold
|
||||
// boundary. The producer aggregates realized Kelly stats from
|
||||
// `portfolio_state[n_envs, PS_STRIDE]` (the same buffer
|
||||
// `kelly_cap_update_kernel` already reads from `PS_KELLY_*` indices) and
|
||||
// EMA-blends into the slot. The static defaults remain as Pearl-A
|
||||
// sentinels so cold-start (pre-first-observation) is bit-identical to the
|
||||
// pre-P1-Producer behavior.
|
||||
//
|
||||
// Why slow EMA at fold cadence (α=0.005): the Bayesian prior is a *prior
|
||||
// belief* about the Kelly distribution before observing the new fold's
|
||||
// trades. It should change slowly across folds — a fast-moving "prior"
|
||||
// would just be a noisy Kelly estimate. Per
|
||||
// `pearl_controller_anchors_isv_driven.md` and
|
||||
// `feedback_isv_for_adaptive_bounds.md` (the prior is a controller
|
||||
// anchor, not a tuning constant).
|
||||
//
|
||||
// Producer kernel: `kelly_bayesian_priors_update_kernel.cu`.
|
||||
// Consumers (atomic migration in same commit):
|
||||
// - `kelly_cap_update_kernel.cu:39-42` → ISV[454..458)
|
||||
// - `trade_physics.cuh::kelly_position_cap:304-307` → threaded
|
||||
// `isv_signals_ptr` argument with NULL-tolerant cold-start fallback.
|
||||
pub const KELLY_PRIOR_WINS_INDEX: usize = 454; // adaptive prior_wins (replaces 2.0f)
|
||||
pub const KELLY_PRIOR_LOSSES_INDEX: usize = 455; // adaptive prior_losses (replaces 2.0f)
|
||||
pub const KELLY_PRIOR_SUM_WINS_INDEX: usize = 456; // adaptive prior_sum_wins (replaces 0.01f)
|
||||
pub const KELLY_PRIOR_SUM_LOSSES_INDEX: usize = 457; // adaptive prior_sum_losses (replaces 0.01f)
|
||||
|
||||
// Sentinels — match pre-P1-Producer hardcoded values for bit-identical
|
||||
// cold-start behavior before the first valid observation lands.
|
||||
pub const SENTINEL_KELLY_PRIOR_WINS: f32 = 2.0;
|
||||
pub const SENTINEL_KELLY_PRIOR_LOSSES: f32 = 2.0;
|
||||
pub const SENTINEL_KELLY_PRIOR_SUM_WINS: f32 = 0.01;
|
||||
pub const SENTINEL_KELLY_PRIOR_SUM_LOSSES: f32 = 0.01;
|
||||
|
||||
// Bounds — Category-1 dimensional safety floors per
|
||||
// `feedback_isv_for_adaptive_bounds.md`. Counts in [0.5, 100]: below 0.5
|
||||
// the prior is effectively absent (kelly_f swings on first real trade);
|
||||
// above 100 the prior dominates 10+ real trades (the maturity threshold
|
||||
// for warmup_floor blend in kelly_position_cap). Sums in [0.001, 1.0]:
|
||||
// below 1e-3 hits the same numerical floor as `fmaxf(avg_loss, 0.0001f)`
|
||||
// in the Kelly formula; above 1.0 implies a per-trade win/loss > 100% of
|
||||
// equity which would be risk-management-failure territory anyway.
|
||||
pub const KELLY_PRIOR_COUNT_MIN: f32 = 0.5;
|
||||
pub const KELLY_PRIOR_COUNT_MAX: f32 = 100.0;
|
||||
pub const KELLY_PRIOR_SUM_MIN: f32 = 0.001;
|
||||
pub const KELLY_PRIOR_SUM_MAX: f32 = 1.0;
|
||||
|
||||
// EMA blend rate — per-fold cadence, slower than reward_cap's α=0.01.
|
||||
// The Bayesian prior is a prior belief; it should change slowly across
|
||||
// folds (a fast-moving "prior" would just be a noisy Kelly estimate).
|
||||
pub const KELLY_PRIOR_EMA_ALPHA: f32 = 0.005;
|
||||
|
||||
pub const SP14_P1_SLOT_BASE: usize = 454;
|
||||
pub const SP14_P1_SLOT_END: usize = 458;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -273,4 +332,42 @@ mod tests {
|
||||
SP14_P0A_SLOT_END, ISV_TOTAL_DIM,
|
||||
);
|
||||
}
|
||||
|
||||
/// Lock Class A P1-Producer (2026-05-08) adaptive Bayesian-prior slot
|
||||
/// layout. Four contiguous slots [454..458) replacing the hardcoded
|
||||
/// `prior_wins=2.0 / prior_losses=2.0 / prior_sum_wins=0.01 /
|
||||
/// prior_sum_losses=0.01` constants in `kelly_cap_update_kernel.cu` and
|
||||
/// `trade_physics.cuh::kelly_position_cap`. Sentinels match the
|
||||
/// pre-P1-Producer hardcoded values for bit-identical cold-start.
|
||||
#[test]
|
||||
fn sp14_p1_kelly_prior_slot_layout_locked() {
|
||||
assert_eq!(SP14_P1_SLOT_BASE, 454);
|
||||
assert_eq!(SP14_P1_SLOT_END, 458);
|
||||
assert_eq!(KELLY_PRIOR_WINS_INDEX, 454);
|
||||
assert_eq!(KELLY_PRIOR_LOSSES_INDEX, 455);
|
||||
assert_eq!(KELLY_PRIOR_SUM_WINS_INDEX, 456);
|
||||
assert_eq!(KELLY_PRIOR_SUM_LOSSES_INDEX, 457);
|
||||
// Sentinels match pre-P1-Producer hardcoded constants for
|
||||
// bit-identical cold-start behavior before first valid observation.
|
||||
assert_eq!(SENTINEL_KELLY_PRIOR_WINS, 2.0);
|
||||
assert_eq!(SENTINEL_KELLY_PRIOR_LOSSES, 2.0);
|
||||
assert_eq!(SENTINEL_KELLY_PRIOR_SUM_WINS, 0.01);
|
||||
assert_eq!(SENTINEL_KELLY_PRIOR_SUM_LOSSES, 0.01);
|
||||
// Category-1 dimensional safety: counts [0.5, 100], sums [0.001, 1.0].
|
||||
assert_eq!(KELLY_PRIOR_COUNT_MIN, 0.5);
|
||||
assert_eq!(KELLY_PRIOR_COUNT_MAX, 100.0);
|
||||
assert_eq!(KELLY_PRIOR_SUM_MIN, 0.001);
|
||||
assert_eq!(KELLY_PRIOR_SUM_MAX, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_sp14_p1_slots_fit_within_isv_total_dim() {
|
||||
use crate::cuda_pipeline::gpu_dqn_trainer::ISV_TOTAL_DIM;
|
||||
assert!(
|
||||
SP14_P1_SLOT_END <= ISV_TOTAL_DIM,
|
||||
"SP14_P1_SLOT_END={} exceeds ISV_TOTAL_DIM={} — bus too small for Class A P1-Producer Bayesian-prior slots; \
|
||||
bump ISV_TOTAL_DIM in gpu_dqn_trainer.rs (and update layout_fingerprint_seed()).",
|
||||
SP14_P1_SLOT_END, ISV_TOTAL_DIM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,13 @@
|
||||
#define ISV_HOLD_RATE_TARGET_IDX 381 // SP13 v3 target Hold-pick rate (static, default 0.20)
|
||||
#define ISV_HOLD_RATE_OBSERVED_EMA_IDX 382 // SP13 v3 observed Hold-pick rate EMA (per-fold, sentinel 0.0)
|
||||
|
||||
// NOTE: this macro is kept at 383 for the historic SP13-era boundary marker
|
||||
// and is referenced only in commentary inside .cu/.cuh files. The actual
|
||||
// ISV bus dimension is the Rust `ISV_TOTAL_DIM` constant in
|
||||
// `gpu_dqn_trainer.rs` (currently 458, post Class A P1-Producer
|
||||
// 2026-05-08). Kernels take `const float*` pointers and use raw indices —
|
||||
// no .cu/.cuh file dimensions an array with this macro. The Rust constant
|
||||
// is the single source of truth for the bus size.
|
||||
#define ISV_TOTAL_DIM 383
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -299,6 +306,36 @@
|
||||
#define REWARD_POS_CAP_MIN_BOUND 1.0f
|
||||
#define REWARD_POS_CAP_MAX_BOUND 50.0f
|
||||
|
||||
// Class A P1-Producer (2026-05-08) — adaptive Bayesian Kelly priors.
|
||||
// Replaces hardcoded `prior_wins=2.0f / prior_losses=2.0f /
|
||||
// prior_sum_wins=0.01f / prior_sum_losses=0.01f` in kelly_cap_update_kernel
|
||||
// and trade_physics.cuh::kelly_position_cap with ISV-driven slow-EMA values
|
||||
// fed by the `kelly_bayesian_priors_update_kernel` producer at per-fold
|
||||
// boundary (per-epoch boundary in current scheduling — same cadence as
|
||||
// kelly_cap_update). Sentinels match the pre-P1-Producer hardcoded values
|
||||
// so cold-start (pre-first-observation) is bit-identical to the prior
|
||||
// behavior. Mirrors `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs`
|
||||
// constants of the same names (locked by
|
||||
// `sp14_p1_kelly_prior_slot_layout_locked` test).
|
||||
#define KELLY_PRIOR_WINS_INDEX 454
|
||||
#define KELLY_PRIOR_LOSSES_INDEX 455
|
||||
#define KELLY_PRIOR_SUM_WINS_INDEX 456
|
||||
#define KELLY_PRIOR_SUM_LOSSES_INDEX 457
|
||||
#define SENTINEL_KELLY_PRIOR_WINS 2.0f
|
||||
#define SENTINEL_KELLY_PRIOR_LOSSES 2.0f
|
||||
#define SENTINEL_KELLY_PRIOR_SUM_WINS 0.01f
|
||||
#define SENTINEL_KELLY_PRIOR_SUM_LOSSES 0.01f
|
||||
// Cold-start fallback for the consumer-side ISV-NULL guard (mirrors the
|
||||
// existing `kelly_warmup_floor_sp9` and `kelly_f_smooth` patterns in
|
||||
// trade_physics.cuh). When `isv_signals_ptr == NULL` (validation envs that
|
||||
// don't pass the bus through) OR the slot value is at sentinel, the
|
||||
// consumer falls back to the static defaults below. Bit-identical
|
||||
// pre-P1-Producer behavior.
|
||||
#define KELLY_PRIOR_WINS_DEFAULT 2.0f
|
||||
#define KELLY_PRIOR_LOSSES_DEFAULT 2.0f
|
||||
#define KELLY_PRIOR_SUM_WINS_DEFAULT 0.01f
|
||||
#define KELLY_PRIOR_SUM_LOSSES_DEFAULT 0.01f
|
||||
|
||||
// ── Compile-time checks ──
|
||||
static_assert(SL_PADDING_START + SL_PADDING_DIM == SL_STATE_DIM,
|
||||
"State layout dimensions must sum to SL_STATE_DIM");
|
||||
|
||||
@@ -263,10 +263,21 @@ __device__ __forceinline__ float compute_drawdown(
|
||||
* the environment refuses to let the agent over-lever, rather than
|
||||
* scoring the agent for matching a formula.
|
||||
*
|
||||
* Priors (prior_wins=2, prior_losses=2, prior_sum_wins=0.01,
|
||||
* prior_sum_losses=0.01) prevent the cap from being degenerate on cold-start
|
||||
* runs where no trades have completed yet — without priors Kelly would
|
||||
* be undefined for the first ~10 trades.
|
||||
* Priors (Class A P1-Producer 2026-05-08): ISV-driven adaptive Bayesian
|
||||
* priors at ISV[454..458) replace the previously-hardcoded
|
||||
* `prior_wins=2.0f, prior_losses=2.0f, prior_sum_wins=0.01f,
|
||||
* prior_sum_losses=0.01f` constants. Producer kernel
|
||||
* `kelly_bayesian_priors_update_kernel` aggregates realized Kelly stats
|
||||
* across envs and slow-EMA-blends (α=0.005) into the four slots at the
|
||||
* same per-epoch boundary as `kelly_cap_update_kernel`. Sentinels match
|
||||
* the pre-P1-Producer hardcoded values for bit-identical cold-start.
|
||||
* NULL-tolerant (`isv_signals_ptr == NULL`) and out-of-range guards
|
||||
* fall back to the static defaults — the helper is safe to call from
|
||||
* validation envs that don't pass the bus through (matches the existing
|
||||
* `kelly_f_smooth` / `kelly_warmup_floor_sp9` pattern in
|
||||
* `unified_env_step_core`). The priors prevent the cap from being
|
||||
* degenerate on cold-start runs where no trades have completed yet —
|
||||
* without priors Kelly would be undefined for the first ~10 trades.
|
||||
*
|
||||
* The safety_multiplier is composed by the caller from two orthogonal
|
||||
* adaptive signals:
|
||||
@@ -299,12 +310,36 @@ __device__ __forceinline__ float kelly_position_cap(
|
||||
float max_position,
|
||||
float safety_multiplier,
|
||||
float conviction,
|
||||
float health_floor
|
||||
float health_floor,
|
||||
/* ── Class A P1-Producer (2026-05-08): adaptive Bayesian priors ──
|
||||
* NULL-tolerant: when NULL OR slot at sentinel/out-of-range, falls
|
||||
* back to the static defaults (matches pre-P1-Producer hardcoded
|
||||
* values for bit-identical cold-start). The fallback path mirrors
|
||||
* the existing `kelly_f_smooth` / `kelly_warmup_floor_sp9` pattern
|
||||
* already in use elsewhere in this file. */
|
||||
const float* __restrict__ isv_signals_ptr /* [ISV_TOTAL_DIM]; NULL = static defaults */
|
||||
) {
|
||||
const float prior_wins = 2.0f;
|
||||
const float prior_losses = 2.0f;
|
||||
const float prior_sum_wins = 0.01f;
|
||||
const float prior_sum_losses = 0.01f;
|
||||
/* Class A P1-Producer (2026-05-08): adaptive Bayesian priors. NULL
|
||||
* guard handles validation envs that don't pass the bus through;
|
||||
* range guard handles cold-start (sentinel) and malformed prior
|
||||
* state. Both fall back to the pre-P1-Producer static defaults
|
||||
* for bit-identical cold-start behavior. */
|
||||
float prior_wins, prior_losses, prior_sum_wins, prior_sum_losses;
|
||||
if (isv_signals_ptr != NULL) {
|
||||
float pw = isv_signals_ptr[KELLY_PRIOR_WINS_INDEX];
|
||||
float pl = isv_signals_ptr[KELLY_PRIOR_LOSSES_INDEX];
|
||||
float psw = isv_signals_ptr[KELLY_PRIOR_SUM_WINS_INDEX];
|
||||
float psl = isv_signals_ptr[KELLY_PRIOR_SUM_LOSSES_INDEX];
|
||||
prior_wins = (pw >= 0.5f && pw <= 100.0f) ? pw : KELLY_PRIOR_WINS_DEFAULT;
|
||||
prior_losses = (pl >= 0.5f && pl <= 100.0f) ? pl : KELLY_PRIOR_LOSSES_DEFAULT;
|
||||
prior_sum_wins = (psw >= 0.001f && psw <= 1.0f) ? psw : KELLY_PRIOR_SUM_WINS_DEFAULT;
|
||||
prior_sum_losses = (psl >= 0.001f && psl <= 1.0f) ? psl : KELLY_PRIOR_SUM_LOSSES_DEFAULT;
|
||||
} else {
|
||||
prior_wins = KELLY_PRIOR_WINS_DEFAULT;
|
||||
prior_losses = KELLY_PRIOR_LOSSES_DEFAULT;
|
||||
prior_sum_wins = KELLY_PRIOR_SUM_WINS_DEFAULT;
|
||||
prior_sum_losses = KELLY_PRIOR_SUM_LOSSES_DEFAULT;
|
||||
}
|
||||
|
||||
float eff_wins = win_count + prior_wins;
|
||||
float eff_losses = loss_count + prior_losses;
|
||||
@@ -389,7 +424,11 @@ __device__ __forceinline__ float kelly_position_cap(
|
||||
/* Apply Kelly cap: clamp a signed target_position to [-cap, +cap].
|
||||
* Convenience for call sites that want a single clamp call.
|
||||
* `conviction` ∈ [0, 1] threads through to kelly_position_cap's adaptive
|
||||
* warmup_floor — see docstring there. */
|
||||
* warmup_floor — see docstring there.
|
||||
* `isv_signals_ptr` threads through to kelly_position_cap's adaptive
|
||||
* Bayesian priors (Class A P1-Producer 2026-05-08). NULL-tolerant —
|
||||
* validation envs that don't pass the bus through fall back to the
|
||||
* pre-P1-Producer static defaults for bit-identical cold-start. */
|
||||
__device__ __forceinline__ float apply_kelly_cap(
|
||||
float target_position,
|
||||
float win_count,
|
||||
@@ -399,11 +438,12 @@ __device__ __forceinline__ float apply_kelly_cap(
|
||||
float max_position,
|
||||
float safety_multiplier,
|
||||
float conviction,
|
||||
float health_floor
|
||||
float health_floor,
|
||||
const float* __restrict__ isv_signals_ptr /* [ISV_TOTAL_DIM]; NULL = static defaults */
|
||||
) {
|
||||
float cap = kelly_position_cap(win_count, loss_count, sum_wins, sum_losses,
|
||||
max_position, safety_multiplier, conviction,
|
||||
health_floor);
|
||||
health_floor, isv_signals_ptr);
|
||||
return fmaxf(-cap, fminf(cap, target_position));
|
||||
}
|
||||
|
||||
@@ -861,7 +901,13 @@ __device__ __forceinline__ void unified_env_step_core(
|
||||
*sum_wins, *sum_losses,
|
||||
max_position_physics, safety,
|
||||
policy_conviction,
|
||||
health_safety_sp9
|
||||
health_safety_sp9,
|
||||
/* Class A P1-Producer (2026-05-08): adaptive Bayesian priors.
|
||||
* `isv_signals_ptr` is the existing parameter to
|
||||
* `unified_env_step_core` (already used for SP5 Pearl 6
|
||||
* `kelly_f_smooth` and SP9 Fix 37 `kelly_warmup_floor_sp9`
|
||||
* just above). NULL-tolerant on the consumer side. */
|
||||
isv_signals_ptr
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1046,6 +1046,48 @@ impl StateResetRegistry {
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "ISV[REWARD_NEG_CAP_ADAPTIVE_INDEX=453] — Class A P0-A adaptive negative reward cap (replaces hardcoded REWARD_NEG_CAP=-10.0f from state_layout.cuh:267). Produced by `reward_cap_update_kernel` as NEG = −REWARD_NEG_TO_POS_RATIO=2.0 × POS_CAP. The 2:1 ratio (Kahneman/Tversky meta-analysis ~2.0-2.25) is preserved at producer-time inside the kernel (single source of truth — no consumer applies the multiplier itself), per `pearl_audit_unboundedness_for_implicit_asymmetry`. FoldReset sentinel SENTINEL_REWARD_NEG_CAP=-10.0 (matches pre-P0-A hardcoded value for bit-identical cold-start). Resulting bounds [-100, -2] derived from POS bounds × ratio. Consumed alongside POS slot at the same 3 sites.",
|
||||
},
|
||||
// ── Class A P1-Producer (2026-05-08): adaptive Bayesian Kelly priors ──
|
||||
// Four slots [454..458) replacing the hardcoded
|
||||
// `prior_wins=2.0f / prior_losses=2.0f / prior_sum_wins=0.01f /
|
||||
// prior_sum_losses=0.01f` constants in
|
||||
// `kelly_cap_update_kernel.cu:39-42` and
|
||||
// `trade_physics.cuh::kelly_position_cap:304-307`. Producer
|
||||
// kernel `kelly_bayesian_priors_update_kernel` aggregates
|
||||
// realized PS_KELLY_{WIN_COUNT, LOSS_COUNT, SUM_WINS,
|
||||
// SUM_LOSSES} across envs from the same `portfolio_state` buffer
|
||||
// `kelly_cap_update_kernel` reads from at the same per-epoch
|
||||
// boundary, then slow-EMA-blends into the four slots. Pearl-A
|
||||
// bootstrap sentinels (2.0/2.0/0.01/0.01) match the
|
||||
// pre-P1-Producer hardcoded values for bit-identical cold-start
|
||||
// behavior — consumers fall back to the static defaults via the
|
||||
// NULL-tolerant + range guard pattern (mirrors the existing
|
||||
// `kelly_f_smooth` / `kelly_warmup_floor_sp9` patterns in
|
||||
// `unified_env_step_core`). Once the first valid observation
|
||||
// lands the adaptive path takes over via slow EMA (α=0.005;
|
||||
// per-fold cadence — the Bayesian prior is a *prior belief*
|
||||
// and should change slowly across folds). Per
|
||||
// `feedback_isv_for_adaptive_bounds.md` and
|
||||
// `pearl_controller_anchors_isv_driven.md`.
|
||||
RegistryEntry {
|
||||
name: "sp14_p1_kelly_prior_wins",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "ISV[KELLY_PRIOR_WINS_INDEX=454] — Class A P1-Producer adaptive Bayesian prior_wins (replaces hardcoded prior_wins=2.0f from kelly_cap_update_kernel.cu:39 + trade_physics.cuh::kelly_position_cap:304). Produced by `kelly_bayesian_priors_update_kernel` from the realized PS_KELLY_WIN_COUNT field aggregated across envs in the per-epoch `portfolio_state` buffer. FoldReset sentinel SENTINEL_KELLY_PRIOR_WINS=2.0 — Pearl-A first-observation bootstrap (matches pre-P1-Producer hardcoded value for bit-identical cold-start). α=0.005 slow EMA thereafter. Bounds [0.5, 100] — Category-1 dimensional safety floors per `feedback_isv_for_adaptive_bounds`. Consumed by kelly_cap_update_kernel (cold-path epoch-boundary effective Kelly cap) and trade_physics.cuh::kelly_position_cap (per-step env Kelly cap, NULL-tolerant via threaded `isv_signals_ptr`).",
|
||||
},
|
||||
RegistryEntry {
|
||||
name: "sp14_p1_kelly_prior_losses",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "ISV[KELLY_PRIOR_LOSSES_INDEX=455] — Class A P1-Producer adaptive Bayesian prior_losses (replaces hardcoded prior_losses=2.0f from kelly_cap_update_kernel.cu:40 + trade_physics.cuh::kelly_position_cap:305). Produced by `kelly_bayesian_priors_update_kernel` from the realized PS_KELLY_LOSS_COUNT field aggregated across envs in the per-epoch `portfolio_state` buffer. FoldReset sentinel SENTINEL_KELLY_PRIOR_LOSSES=2.0 — Pearl-A first-observation bootstrap (matches pre-P1-Producer hardcoded value for bit-identical cold-start). α=0.005 slow EMA thereafter. Bounds [0.5, 100] same as prior_wins. Consumed at the same 2 sites as prior_wins.",
|
||||
},
|
||||
RegistryEntry {
|
||||
name: "sp14_p1_kelly_prior_sum_wins",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "ISV[KELLY_PRIOR_SUM_WINS_INDEX=456] — Class A P1-Producer adaptive Bayesian prior_sum_wins (replaces hardcoded prior_sum_wins=0.01f from kelly_cap_update_kernel.cu:41 + trade_physics.cuh::kelly_position_cap:306). Produced by `kelly_bayesian_priors_update_kernel` from the realized PS_KELLY_SUM_WINS field aggregated across envs in the per-epoch `portfolio_state` buffer. FoldReset sentinel SENTINEL_KELLY_PRIOR_SUM_WINS=0.01 — Pearl-A first-observation bootstrap (matches pre-P1-Producer hardcoded value for bit-identical cold-start). α=0.005 slow EMA thereafter. Bounds [0.001, 1.0] — sum below 1e-3 hits the `fmaxf(avg_loss, 0.0001f)` numerical floor; sum above 1.0 implies > 100% per-trade equity which is risk-management-failure territory. Consumed at the same 2 sites as prior_wins.",
|
||||
},
|
||||
RegistryEntry {
|
||||
name: "sp14_p1_kelly_prior_sum_losses",
|
||||
category: ResetCategory::FoldReset,
|
||||
description: "ISV[KELLY_PRIOR_SUM_LOSSES_INDEX=457] — Class A P1-Producer adaptive Bayesian prior_sum_losses (replaces hardcoded prior_sum_losses=0.01f from kelly_cap_update_kernel.cu:42 + trade_physics.cuh::kelly_position_cap:307). Produced by `kelly_bayesian_priors_update_kernel` from the realized PS_KELLY_SUM_LOSSES field aggregated across envs in the per-epoch `portfolio_state` buffer. FoldReset sentinel SENTINEL_KELLY_PRIOR_SUM_LOSSES=0.01 — Pearl-A first-observation bootstrap (matches pre-P1-Producer hardcoded value for bit-identical cold-start). α=0.005 slow EMA thereafter. Bounds [0.001, 1.0] same as prior_sum_wins. Consumed at the same 2 sites as prior_wins.",
|
||||
},
|
||||
// ── SP15 Phase 1.2 (2026-05-06): cost-net sharpe slots ────────────
|
||||
// Two ISV slots [407, 408]:
|
||||
// - OFI_IMPACT_LAMBDA_INDEX=407: Invariant-1 anchor (NOT a
|
||||
|
||||
@@ -383,6 +383,18 @@ impl DQNTrainer {
|
||||
{
|
||||
let ps_dev_ptr = collector.portfolio_states_dev_ptr();
|
||||
let n_envs = collector.alloc_episodes() as i32;
|
||||
// Class A P1-Producer (2026-05-08): adaptive Bayesian Kelly
|
||||
// priors update. MUST run BEFORE `launch_kelly_cap_update`
|
||||
// so that kernel sees the freshly-blended priors. Sweeps
|
||||
// the same `portfolio_state` buffer with the same n_envs
|
||||
// and writes ISV[454..458). Pearl-A bootstrap + α=0.005
|
||||
// slow EMA. Replaces hardcoded
|
||||
// prior_wins/prior_losses/prior_sum_wins/prior_sum_losses
|
||||
// constants in kelly_cap_update_kernel.cu and
|
||||
// trade_physics.cuh::kelly_position_cap.
|
||||
if let Err(e) = fused.trainer().launch_kelly_bayesian_priors_update(ps_dev_ptr, n_envs) {
|
||||
tracing::warn!(epoch, "launch_kelly_bayesian_priors_update failed (non-fatal): {e}");
|
||||
}
|
||||
if let Err(e) = fused.launch_kelly_cap_update(ps_dev_ptr, n_envs) {
|
||||
tracing::warn!(epoch, "launch_kelly_cap_update failed (non-fatal): {e}");
|
||||
}
|
||||
@@ -8183,6 +8195,57 @@ impl DQNTrainer {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Class A P1-Producer (2026-05-08): adaptive Bayesian Kelly
|
||||
// priors. Four FoldReset slots [454..458) — sentinels match
|
||||
// pre-P1-Producer hardcoded values (2.0/2.0/0.01/0.01) so
|
||||
// the kernel's "first observation" check fires cleanly on
|
||||
// the new fold's first launch (avoids cross-fold EMA
|
||||
// contamination). Cold-start is bit-identical to
|
||||
// pre-P1-Producer until the first valid observation lands.
|
||||
"sp14_p1_kelly_prior_wins" => {
|
||||
if let Some(ref fused) = self.fused_ctx {
|
||||
use crate::cuda_pipeline::sp14_isv_slots::{
|
||||
KELLY_PRIOR_WINS_INDEX, SENTINEL_KELLY_PRIOR_WINS,
|
||||
};
|
||||
fused.trainer().write_isv_signal_at(
|
||||
KELLY_PRIOR_WINS_INDEX,
|
||||
SENTINEL_KELLY_PRIOR_WINS,
|
||||
);
|
||||
}
|
||||
}
|
||||
"sp14_p1_kelly_prior_losses" => {
|
||||
if let Some(ref fused) = self.fused_ctx {
|
||||
use crate::cuda_pipeline::sp14_isv_slots::{
|
||||
KELLY_PRIOR_LOSSES_INDEX, SENTINEL_KELLY_PRIOR_LOSSES,
|
||||
};
|
||||
fused.trainer().write_isv_signal_at(
|
||||
KELLY_PRIOR_LOSSES_INDEX,
|
||||
SENTINEL_KELLY_PRIOR_LOSSES,
|
||||
);
|
||||
}
|
||||
}
|
||||
"sp14_p1_kelly_prior_sum_wins" => {
|
||||
if let Some(ref fused) = self.fused_ctx {
|
||||
use crate::cuda_pipeline::sp14_isv_slots::{
|
||||
KELLY_PRIOR_SUM_WINS_INDEX, SENTINEL_KELLY_PRIOR_SUM_WINS,
|
||||
};
|
||||
fused.trainer().write_isv_signal_at(
|
||||
KELLY_PRIOR_SUM_WINS_INDEX,
|
||||
SENTINEL_KELLY_PRIOR_SUM_WINS,
|
||||
);
|
||||
}
|
||||
}
|
||||
"sp14_p1_kelly_prior_sum_losses" => {
|
||||
if let Some(ref fused) = self.fused_ctx {
|
||||
use crate::cuda_pipeline::sp14_isv_slots::{
|
||||
KELLY_PRIOR_SUM_LOSSES_INDEX, SENTINEL_KELLY_PRIOR_SUM_LOSSES,
|
||||
};
|
||||
fused.trainer().write_isv_signal_at(
|
||||
KELLY_PRIOR_SUM_LOSSES_INDEX,
|
||||
SENTINEL_KELLY_PRIOR_SUM_LOSSES,
|
||||
);
|
||||
}
|
||||
}
|
||||
// SP15 Phase 1.2 (2026-05-06): cost-net sharpe slots.
|
||||
// OFI_IMPACT_LAMBDA_INDEX=407 is an Invariant-1 anchor (NOT
|
||||
// a stateful EMA) — rewrite the constructor's value at fold
|
||||
|
||||
@@ -787,6 +787,396 @@ mod sp14_p0a_reward_cap_gpu {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Class A P1-Producer (2026-05-08) — adaptive Bayesian Kelly priors producer
|
||||
// tests.
|
||||
//
|
||||
// Verifies the `kelly_bayesian_priors_update_kernel` producer:
|
||||
// 1. Pearl-A first-observation bootstrap: ISV at sentinel → REPLACES with
|
||||
// the realized aggregated stats (no blend).
|
||||
// 2. Slow EMA (α=0.005) blend after bootstrap.
|
||||
// 3. Bounds enforced: counts in [0.5, 100], sums in [0.001, 1.0].
|
||||
// 4. No realized trades (zero PS_KELLY_* fields) → ISV slots preserved
|
||||
// bit-exactly.
|
||||
//
|
||||
// All tests are #[ignore = "requires GPU"]; gated under #[cfg(feature = "cuda")].
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
#[cfg(feature = "cuda")]
|
||||
#[allow(unsafe_code)] // CUDA kernel launch + mapped-pinned memory.
|
||||
mod sp14_p1_kelly_priors_gpu {
|
||||
use std::sync::Arc;
|
||||
|
||||
use cudarc::driver::{CudaContext, CudaFunction, CudaStream, LaunchConfig, PushKernelArg};
|
||||
use ml::cuda_pipeline::mapped_pinned::MappedF32Buffer;
|
||||
use ml::cuda_pipeline::sp14_isv_slots::{
|
||||
KELLY_PRIOR_COUNT_MAX, KELLY_PRIOR_COUNT_MIN, KELLY_PRIOR_EMA_ALPHA,
|
||||
KELLY_PRIOR_LOSSES_INDEX, KELLY_PRIOR_SUM_LOSSES_INDEX, KELLY_PRIOR_SUM_MAX,
|
||||
KELLY_PRIOR_SUM_MIN, KELLY_PRIOR_SUM_WINS_INDEX, KELLY_PRIOR_WINS_INDEX,
|
||||
SENTINEL_KELLY_PRIOR_LOSSES, SENTINEL_KELLY_PRIOR_SUM_LOSSES,
|
||||
SENTINEL_KELLY_PRIOR_SUM_WINS, SENTINEL_KELLY_PRIOR_WINS,
|
||||
};
|
||||
|
||||
// PS_STRIDE constant — matches state_layout.cuh::PS_STRIDE and the
|
||||
// value used by `launch_kelly_cap_update`. Grown 41→43 by Plan 3 D.4c.
|
||||
const PS_STRIDE: usize = 43;
|
||||
// PS_KELLY_* slot offsets within a single env's portfolio_state row.
|
||||
const PS_KELLY_WIN_COUNT: usize = 14;
|
||||
const PS_KELLY_LOSS_COUNT: usize = 15;
|
||||
const PS_KELLY_SUM_WINS: usize = 16;
|
||||
const PS_KELLY_SUM_LOSSES: usize = 17;
|
||||
|
||||
const KELLY_BAYESIAN_PRIORS_UPDATE_CUBIN: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/kelly_bayesian_priors_update_kernel.cubin"));
|
||||
|
||||
fn make_stream() -> Arc<CudaStream> {
|
||||
let ctx = CudaContext::new(0).expect("CUDA context — is a GPU available?");
|
||||
ctx.default_stream()
|
||||
}
|
||||
|
||||
fn load_kernel(stream: &Arc<CudaStream>) -> CudaFunction {
|
||||
let module = stream
|
||||
.context()
|
||||
.load_cubin(KELLY_BAYESIAN_PRIORS_UPDATE_CUBIN.to_vec())
|
||||
.expect("load kelly_bayesian_priors_update_kernel cubin");
|
||||
module
|
||||
.load_function("kelly_bayesian_priors_update")
|
||||
.expect("load kelly_bayesian_priors_update function")
|
||||
}
|
||||
|
||||
/// 4 arrays × 256 × 4 bytes = 4096 bytes shmem.
|
||||
const BLK_DIM: u32 = 256;
|
||||
const SMEM_BYTES: u32 = 4 * BLK_DIM * std::mem::size_of::<f32>() as u32;
|
||||
|
||||
/// Helper: launch with a fixed config.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn launch_kelly_priors(
|
||||
stream: &Arc<CudaStream>,
|
||||
kernel: &CudaFunction,
|
||||
portfolio_state_ptr: u64,
|
||||
n_envs: i32,
|
||||
ps_stride: i32,
|
||||
isv_ptr: u64,
|
||||
wins_idx: i32,
|
||||
losses_idx: i32,
|
||||
sum_wins_idx: i32,
|
||||
sum_losses_idx: i32,
|
||||
sentinel_wins: f32,
|
||||
sentinel_losses: f32,
|
||||
sentinel_sum_wins: f32,
|
||||
sentinel_sum_losses: f32,
|
||||
count_min: f32,
|
||||
count_max: f32,
|
||||
sum_min: f32,
|
||||
sum_max: f32,
|
||||
alpha: f32,
|
||||
) {
|
||||
unsafe {
|
||||
stream
|
||||
.launch_builder(kernel)
|
||||
.arg(&portfolio_state_ptr)
|
||||
.arg(&n_envs)
|
||||
.arg(&ps_stride)
|
||||
.arg(&isv_ptr)
|
||||
.arg(&wins_idx)
|
||||
.arg(&losses_idx)
|
||||
.arg(&sum_wins_idx)
|
||||
.arg(&sum_losses_idx)
|
||||
.arg(&sentinel_wins)
|
||||
.arg(&sentinel_losses)
|
||||
.arg(&sentinel_sum_wins)
|
||||
.arg(&sentinel_sum_losses)
|
||||
.arg(&count_min)
|
||||
.arg(&count_max)
|
||||
.arg(&sum_min)
|
||||
.arg(&sum_max)
|
||||
.arg(&alpha)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (1, 1, 1),
|
||||
block_dim: (BLK_DIM, 1, 1),
|
||||
shared_mem_bytes: SMEM_BYTES,
|
||||
})
|
||||
.expect("launch kelly_bayesian_priors_update");
|
||||
}
|
||||
stream.synchronize().expect("sync after kelly_bayesian_priors_update");
|
||||
}
|
||||
|
||||
/// Build a portfolio_state buffer of `n_envs * PS_STRIDE` entries with
|
||||
/// the four PS_KELLY_* fields populated per env from the parallel slices.
|
||||
/// All other PS_* fields are zero.
|
||||
fn build_portfolio_state(
|
||||
wins_per_env: &[f32],
|
||||
losses_per_env: &[f32],
|
||||
sum_wins_per_env: &[f32],
|
||||
sum_losses_per_env: &[f32],
|
||||
) -> Vec<f32> {
|
||||
let n = wins_per_env.len();
|
||||
assert_eq!(losses_per_env.len(), n);
|
||||
assert_eq!(sum_wins_per_env.len(), n);
|
||||
assert_eq!(sum_losses_per_env.len(), n);
|
||||
let mut ps = vec![0.0_f32; n * PS_STRIDE];
|
||||
for e in 0..n {
|
||||
let row_off = e * PS_STRIDE;
|
||||
ps[row_off + PS_KELLY_WIN_COUNT] = wins_per_env[e];
|
||||
ps[row_off + PS_KELLY_LOSS_COUNT] = losses_per_env[e];
|
||||
ps[row_off + PS_KELLY_SUM_WINS] = sum_wins_per_env[e];
|
||||
ps[row_off + PS_KELLY_SUM_LOSSES] = sum_losses_per_env[e];
|
||||
}
|
||||
ps
|
||||
}
|
||||
|
||||
/// Test 1 — Pearl-A first-observation bootstrap.
|
||||
///
|
||||
/// 4 envs with realized stats. Total wins = 1+2+3+4 = 10, losses = 0+1+0+1 = 2,
|
||||
/// sum_wins = 0.05+0.10+0.15+0.20 = 0.50, sum_losses = 0+0.03+0+0.05 = 0.08.
|
||||
/// Pre-EMA clamp: counts to [0.5, 100], sums to [0.001, 1.0] —
|
||||
/// 10/2/0.5/0.08 all in range, pass through unchanged. Cold-start
|
||||
/// (sentinels 2.0/2.0/0.01/0.01): Pearl-A REPLACES → ISV =
|
||||
/// (10, 2, 0.5, 0.08).
|
||||
#[test]
|
||||
#[ignore = "requires GPU"]
|
||||
fn kelly_priors_pearl_a_bootstrap() {
|
||||
let stream = make_stream();
|
||||
let kernel = load_kernel(&stream);
|
||||
|
||||
const ISV_DIM: usize = 1024;
|
||||
let mut isv = vec![0.0_f32; ISV_DIM];
|
||||
// Cold-start: sentinels (matches pre-P1-Producer constants).
|
||||
isv[KELLY_PRIOR_WINS_INDEX] = SENTINEL_KELLY_PRIOR_WINS;
|
||||
isv[KELLY_PRIOR_LOSSES_INDEX] = SENTINEL_KELLY_PRIOR_LOSSES;
|
||||
isv[KELLY_PRIOR_SUM_WINS_INDEX] = SENTINEL_KELLY_PRIOR_SUM_WINS;
|
||||
isv[KELLY_PRIOR_SUM_LOSSES_INDEX] = SENTINEL_KELLY_PRIOR_SUM_LOSSES;
|
||||
|
||||
let wins_per_env = [1.0_f32, 2.0, 3.0, 4.0];
|
||||
let losses_per_env = [0.0_f32, 1.0, 0.0, 1.0];
|
||||
let sum_wins_per_env = [0.05_f32, 0.10, 0.15, 0.20];
|
||||
let sum_losses_per_env = [0.0_f32, 0.03, 0.0, 0.05];
|
||||
let n_envs = wins_per_env.len();
|
||||
|
||||
let ps = build_portfolio_state(&wins_per_env, &losses_per_env,
|
||||
&sum_wins_per_env, &sum_losses_per_env);
|
||||
|
||||
let ps_buf = unsafe { MappedF32Buffer::new(ps.len()) }.expect("alloc ps");
|
||||
ps_buf.write_from_slice(&ps);
|
||||
let isv_buf = unsafe { MappedF32Buffer::new(ISV_DIM) }.expect("alloc isv");
|
||||
isv_buf.write_from_slice(&isv);
|
||||
|
||||
launch_kelly_priors(
|
||||
&stream, &kernel,
|
||||
ps_buf.dev_ptr, n_envs as i32, PS_STRIDE as i32, isv_buf.dev_ptr,
|
||||
KELLY_PRIOR_WINS_INDEX as i32,
|
||||
KELLY_PRIOR_LOSSES_INDEX as i32,
|
||||
KELLY_PRIOR_SUM_WINS_INDEX as i32,
|
||||
KELLY_PRIOR_SUM_LOSSES_INDEX as i32,
|
||||
SENTINEL_KELLY_PRIOR_WINS, SENTINEL_KELLY_PRIOR_LOSSES,
|
||||
SENTINEL_KELLY_PRIOR_SUM_WINS, SENTINEL_KELLY_PRIOR_SUM_LOSSES,
|
||||
KELLY_PRIOR_COUNT_MIN, KELLY_PRIOR_COUNT_MAX,
|
||||
KELLY_PRIOR_SUM_MIN, KELLY_PRIOR_SUM_MAX,
|
||||
KELLY_PRIOR_EMA_ALPHA,
|
||||
);
|
||||
|
||||
let result = isv_buf.read_all();
|
||||
let pw = result[KELLY_PRIOR_WINS_INDEX];
|
||||
let pl = result[KELLY_PRIOR_LOSSES_INDEX];
|
||||
let psw = result[KELLY_PRIOR_SUM_WINS_INDEX];
|
||||
let psl = result[KELLY_PRIOR_SUM_LOSSES_INDEX];
|
||||
|
||||
// Pearl-A: sentinel → REPLACE with target. Targets are the
|
||||
// aggregated totals clamped to dimensional-safety bounds (all
|
||||
// within range here, no clamping applied).
|
||||
assert!((pw - 10.0).abs() < 1e-4,
|
||||
"Pearl-A bootstrap: prior_wins expected 10.0, got {pw}");
|
||||
assert!((pl - 2.0).abs() < 1e-4,
|
||||
"Pearl-A bootstrap: prior_losses expected 2.0, got {pl}");
|
||||
assert!((psw - 0.50).abs() < 1e-4,
|
||||
"Pearl-A bootstrap: prior_sum_wins expected 0.50, got {psw}");
|
||||
assert!((psl - 0.08).abs() < 1e-4,
|
||||
"Pearl-A bootstrap: prior_sum_losses expected 0.08, got {psl}");
|
||||
}
|
||||
|
||||
/// Test 2 — No realized trades → ISV slots preserved bit-exactly.
|
||||
///
|
||||
/// All envs have zero PS_KELLY_* fields → total_wins + total_losses == 0.
|
||||
/// Kernel guard skips the EMA update; both slots stay at the seeded
|
||||
/// values (NOT sentinels — to verify the guard preserves arbitrary
|
||||
/// pre-existing state).
|
||||
#[test]
|
||||
#[ignore = "requires GPU"]
|
||||
fn kelly_priors_no_realized_trades_preserves_isv() {
|
||||
let stream = make_stream();
|
||||
let kernel = load_kernel(&stream);
|
||||
|
||||
const ISV_DIM: usize = 1024;
|
||||
// Seed with non-sentinel values to verify they survive the guard.
|
||||
const SEED_WINS: f32 = 7.0;
|
||||
const SEED_LOSSES: f32 = 5.0;
|
||||
const SEED_SUM_WINS: f32 = 0.25;
|
||||
const SEED_SUM_LOSSES: f32 = 0.15;
|
||||
|
||||
let mut isv = vec![0.0_f32; ISV_DIM];
|
||||
isv[KELLY_PRIOR_WINS_INDEX] = SEED_WINS;
|
||||
isv[KELLY_PRIOR_LOSSES_INDEX] = SEED_LOSSES;
|
||||
isv[KELLY_PRIOR_SUM_WINS_INDEX] = SEED_SUM_WINS;
|
||||
isv[KELLY_PRIOR_SUM_LOSSES_INDEX] = SEED_SUM_LOSSES;
|
||||
|
||||
// 3 envs with no realized trades.
|
||||
let n_envs: usize = 3;
|
||||
let ps = vec![0.0_f32; n_envs * PS_STRIDE];
|
||||
|
||||
let ps_buf = unsafe { MappedF32Buffer::new(ps.len()) }.expect("alloc ps");
|
||||
ps_buf.write_from_slice(&ps);
|
||||
let isv_buf = unsafe { MappedF32Buffer::new(ISV_DIM) }.expect("alloc isv");
|
||||
isv_buf.write_from_slice(&isv);
|
||||
|
||||
launch_kelly_priors(
|
||||
&stream, &kernel,
|
||||
ps_buf.dev_ptr, n_envs as i32, PS_STRIDE as i32, isv_buf.dev_ptr,
|
||||
KELLY_PRIOR_WINS_INDEX as i32,
|
||||
KELLY_PRIOR_LOSSES_INDEX as i32,
|
||||
KELLY_PRIOR_SUM_WINS_INDEX as i32,
|
||||
KELLY_PRIOR_SUM_LOSSES_INDEX as i32,
|
||||
SENTINEL_KELLY_PRIOR_WINS, SENTINEL_KELLY_PRIOR_LOSSES,
|
||||
SENTINEL_KELLY_PRIOR_SUM_WINS, SENTINEL_KELLY_PRIOR_SUM_LOSSES,
|
||||
KELLY_PRIOR_COUNT_MIN, KELLY_PRIOR_COUNT_MAX,
|
||||
KELLY_PRIOR_SUM_MIN, KELLY_PRIOR_SUM_MAX,
|
||||
KELLY_PRIOR_EMA_ALPHA,
|
||||
);
|
||||
|
||||
let result = isv_buf.read_all();
|
||||
assert_eq!(result[KELLY_PRIOR_WINS_INDEX], SEED_WINS,
|
||||
"prior_wins must be preserved bit-exactly when no realized trades");
|
||||
assert_eq!(result[KELLY_PRIOR_LOSSES_INDEX], SEED_LOSSES,
|
||||
"prior_losses must be preserved bit-exactly when no realized trades");
|
||||
assert_eq!(result[KELLY_PRIOR_SUM_WINS_INDEX], SEED_SUM_WINS,
|
||||
"prior_sum_wins must be preserved bit-exactly when no realized trades");
|
||||
assert_eq!(result[KELLY_PRIOR_SUM_LOSSES_INDEX], SEED_SUM_LOSSES,
|
||||
"prior_sum_losses must be preserved bit-exactly when no realized trades");
|
||||
}
|
||||
|
||||
/// Test 3 — Bounds enforcement: extreme aggregate clamps to upper/lower bounds.
|
||||
///
|
||||
/// One env with huge counts (1e6 wins, 1e6 losses) and tiny sums (1e-9).
|
||||
/// Pre-EMA clamp: wins/losses → 100 (count_max), sums → 0.001 (sum_min).
|
||||
/// Cold-start (sentinels): Pearl-A REPLACES with clamped targets.
|
||||
#[test]
|
||||
#[ignore = "requires GPU"]
|
||||
fn kelly_priors_bounds_clamp_extreme() {
|
||||
let stream = make_stream();
|
||||
let kernel = load_kernel(&stream);
|
||||
|
||||
const ISV_DIM: usize = 1024;
|
||||
let mut isv = vec![0.0_f32; ISV_DIM];
|
||||
isv[KELLY_PRIOR_WINS_INDEX] = SENTINEL_KELLY_PRIOR_WINS;
|
||||
isv[KELLY_PRIOR_LOSSES_INDEX] = SENTINEL_KELLY_PRIOR_LOSSES;
|
||||
isv[KELLY_PRIOR_SUM_WINS_INDEX] = SENTINEL_KELLY_PRIOR_SUM_WINS;
|
||||
isv[KELLY_PRIOR_SUM_LOSSES_INDEX] = SENTINEL_KELLY_PRIOR_SUM_LOSSES;
|
||||
|
||||
// Single env with extreme aggregates.
|
||||
let wins_per_env = [1.0e6_f32];
|
||||
let losses_per_env = [1.0e6_f32];
|
||||
let sum_wins_per_env = [1.0e-9_f32];
|
||||
let sum_losses_per_env = [1.0e-9_f32];
|
||||
|
||||
let ps = build_portfolio_state(&wins_per_env, &losses_per_env,
|
||||
&sum_wins_per_env, &sum_losses_per_env);
|
||||
|
||||
let ps_buf = unsafe { MappedF32Buffer::new(ps.len()) }.expect("alloc ps");
|
||||
ps_buf.write_from_slice(&ps);
|
||||
let isv_buf = unsafe { MappedF32Buffer::new(ISV_DIM) }.expect("alloc isv");
|
||||
isv_buf.write_from_slice(&isv);
|
||||
|
||||
launch_kelly_priors(
|
||||
&stream, &kernel,
|
||||
ps_buf.dev_ptr, 1, PS_STRIDE as i32, isv_buf.dev_ptr,
|
||||
KELLY_PRIOR_WINS_INDEX as i32,
|
||||
KELLY_PRIOR_LOSSES_INDEX as i32,
|
||||
KELLY_PRIOR_SUM_WINS_INDEX as i32,
|
||||
KELLY_PRIOR_SUM_LOSSES_INDEX as i32,
|
||||
SENTINEL_KELLY_PRIOR_WINS, SENTINEL_KELLY_PRIOR_LOSSES,
|
||||
SENTINEL_KELLY_PRIOR_SUM_WINS, SENTINEL_KELLY_PRIOR_SUM_LOSSES,
|
||||
KELLY_PRIOR_COUNT_MIN, KELLY_PRIOR_COUNT_MAX,
|
||||
KELLY_PRIOR_SUM_MIN, KELLY_PRIOR_SUM_MAX,
|
||||
KELLY_PRIOR_EMA_ALPHA,
|
||||
);
|
||||
|
||||
let result = isv_buf.read_all();
|
||||
let pw = result[KELLY_PRIOR_WINS_INDEX];
|
||||
let pl = result[KELLY_PRIOR_LOSSES_INDEX];
|
||||
let psw = result[KELLY_PRIOR_SUM_WINS_INDEX];
|
||||
let psl = result[KELLY_PRIOR_SUM_LOSSES_INDEX];
|
||||
|
||||
// 1e6 wins → clamped to count_max=100.
|
||||
assert!((pw - KELLY_PRIOR_COUNT_MAX).abs() < 1e-4,
|
||||
"prior_wins must clamp to count_max=100 on extreme; got {pw}");
|
||||
assert!((pl - KELLY_PRIOR_COUNT_MAX).abs() < 1e-4,
|
||||
"prior_losses must clamp to count_max=100 on extreme; got {pl}");
|
||||
// 1e-9 sums → clamped to sum_min=0.001.
|
||||
assert!((psw - KELLY_PRIOR_SUM_MIN).abs() < 1e-6,
|
||||
"prior_sum_wins must clamp to sum_min=0.001 on tiny; got {psw}");
|
||||
assert!((psl - KELLY_PRIOR_SUM_MIN).abs() < 1e-6,
|
||||
"prior_sum_losses must clamp to sum_min=0.001 on tiny; got {psl}");
|
||||
}
|
||||
|
||||
/// Test 4 — Slow EMA blend after bootstrap (α=0.005).
|
||||
///
|
||||
/// Pre-seed prior_wins=10.0 (NOT sentinel — bootstrap path NOT taken).
|
||||
/// Single env with 5 wins. Target = 5 (in range). EMA blend:
|
||||
/// (1 - 0.005) × 10 + 0.005 × 5 = 0.995 × 10 + 0.025 = 9.975.
|
||||
#[test]
|
||||
#[ignore = "requires GPU"]
|
||||
fn kelly_priors_slow_ema_after_bootstrap() {
|
||||
let stream = make_stream();
|
||||
let kernel = load_kernel(&stream);
|
||||
|
||||
const ISV_DIM: usize = 1024;
|
||||
const SEEDED_WINS: f32 = 10.0;
|
||||
// Other slots stay at sentinel — only test EMA on prior_wins.
|
||||
let mut isv = vec![0.0_f32; ISV_DIM];
|
||||
isv[KELLY_PRIOR_WINS_INDEX] = SEEDED_WINS;
|
||||
isv[KELLY_PRIOR_LOSSES_INDEX] = SENTINEL_KELLY_PRIOR_LOSSES;
|
||||
isv[KELLY_PRIOR_SUM_WINS_INDEX] = SENTINEL_KELLY_PRIOR_SUM_WINS;
|
||||
isv[KELLY_PRIOR_SUM_LOSSES_INDEX] = SENTINEL_KELLY_PRIOR_SUM_LOSSES;
|
||||
|
||||
// Single env with 5 wins.
|
||||
let wins_per_env = [5.0_f32];
|
||||
let losses_per_env = [1.0_f32];
|
||||
let sum_wins_per_env = [0.10_f32];
|
||||
let sum_losses_per_env = [0.02_f32];
|
||||
|
||||
let ps = build_portfolio_state(&wins_per_env, &losses_per_env,
|
||||
&sum_wins_per_env, &sum_losses_per_env);
|
||||
|
||||
let ps_buf = unsafe { MappedF32Buffer::new(ps.len()) }.expect("alloc ps");
|
||||
ps_buf.write_from_slice(&ps);
|
||||
let isv_buf = unsafe { MappedF32Buffer::new(ISV_DIM) }.expect("alloc isv");
|
||||
isv_buf.write_from_slice(&isv);
|
||||
|
||||
launch_kelly_priors(
|
||||
&stream, &kernel,
|
||||
ps_buf.dev_ptr, 1, PS_STRIDE as i32, isv_buf.dev_ptr,
|
||||
KELLY_PRIOR_WINS_INDEX as i32,
|
||||
KELLY_PRIOR_LOSSES_INDEX as i32,
|
||||
KELLY_PRIOR_SUM_WINS_INDEX as i32,
|
||||
KELLY_PRIOR_SUM_LOSSES_INDEX as i32,
|
||||
SENTINEL_KELLY_PRIOR_WINS, SENTINEL_KELLY_PRIOR_LOSSES,
|
||||
SENTINEL_KELLY_PRIOR_SUM_WINS, SENTINEL_KELLY_PRIOR_SUM_LOSSES,
|
||||
KELLY_PRIOR_COUNT_MIN, KELLY_PRIOR_COUNT_MAX,
|
||||
KELLY_PRIOR_SUM_MIN, KELLY_PRIOR_SUM_MAX,
|
||||
KELLY_PRIOR_EMA_ALPHA,
|
||||
);
|
||||
|
||||
let result = isv_buf.read_all();
|
||||
let pw = result[KELLY_PRIOR_WINS_INDEX];
|
||||
// EMA: 0.995 × 10 + 0.005 × 5 = 9.975.
|
||||
let expected = (1.0_f32 - KELLY_PRIOR_EMA_ALPHA) * SEEDED_WINS
|
||||
+ KELLY_PRIOR_EMA_ALPHA * 5.0_f32;
|
||||
assert!(
|
||||
(pw - expected).abs() < 1e-4,
|
||||
"Slow EMA blend (α={KELLY_PRIOR_EMA_ALPHA}): expected {expected}, got {pw}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Test B.8: Layout-fingerprint regression for direction Q-head input bump.
|
||||
//
|
||||
|
||||
@@ -7992,3 +7992,150 @@ P0-A made `REWARD_POS_CAP` adaptive via `ISV[REWARD_POS_CAP_ADAPTIVE_INDEX=452]`
|
||||
|
||||
1. **The legacy `compute_drawdown_penalty` is now partially adaptive but the SP15 `sp15_dd_penalty` quadratic path is fully ISV-driven via slots 420-421**. The two paths still coexist with different shapes (legacy: linear ramp scaled by POS_CAP; SP15: quadratic gated by λ_dd). The P1 batch already flagged this as future work. P0-A-downstream does not retire the legacy path — that's a separate refactor.
|
||||
2. **`MIN_HOLD_PENALTY_RATIO = 0.6f` is a magic number at the call site** rather than a `#define` in `state_layout.cuh`. Kept local because it's strictly a relationship between two existing constants (3.0 / 5.0 = 0.6), not a new tunable signal. If a future batch lifts the ratio itself to ISV, the constant should move to `state_layout.cuh` first.
|
||||
|
||||
## Class A P1-Producer batch — adaptive Bayesian Kelly priors (2026-05-08)
|
||||
|
||||
The Class A P1 batch (commit `c4b6d6ef2`) wired Item 4 (var_floor q_gap-only adaptive) and explicitly deferred Items 1-3 to "a producer batch" because they required new ISV slot allocations. This entry lands one of those deferred items: the adaptive Bayesian Kelly priors. Item 2 (MIN_HOLD_TEMPERATURE EMA) was deferred separately — the audit-spec was wrong about the consumer site (see Concerns below).
|
||||
|
||||
### Background
|
||||
|
||||
`kelly_cap_update_kernel.cu:39-42` and `trade_physics.cuh::kelly_position_cap:304-307` both ran with hardcoded Bayesian priors:
|
||||
|
||||
```c
|
||||
const float prior_wins = 2.0f;
|
||||
const float prior_losses = 2.0f;
|
||||
const float prior_sum_wins = 0.01f;
|
||||
const float prior_sum_losses = 0.01f;
|
||||
```
|
||||
|
||||
These are static "I don't know" priors frozen at sprint-1 defaults. Conceptually, Bayesian priors encode *prior belief* about the Kelly distribution before observing real trades. A static prior IS literally the prior — but it's also constant across folds, ignoring everything prior folds learned about the realized win/loss distribution shape.
|
||||
|
||||
The fix is an ISV-driven slow-EMA prior fed from the realized Kelly stats already aggregated in `portfolio_state[n_envs, PS_STRIDE]` (the same buffer `kelly_cap_update_kernel` reads from at the same per-epoch boundary). On the first fold, the sentinels match the pre-P1-Producer hardcoded values for bit-identical cold-start; once the first valid observation lands, the slow EMA blends in learned beliefs from realized distribution shape; on subsequent folds, the cold-start prior is no longer "I don't know" but "what we learned from the prior fold's distribution shape, slow-discounted."
|
||||
|
||||
### Slots allocated
|
||||
|
||||
| Slot | Constant | Sentinel | Bounds | Replaces |
|
||||
|-----:|----------|---------:|--------|----------|
|
||||
| 454 | `KELLY_PRIOR_WINS_INDEX` | 2.0 | [0.5, 100] | `prior_wins=2.0f` |
|
||||
| 455 | `KELLY_PRIOR_LOSSES_INDEX` | 2.0 | [0.5, 100] | `prior_losses=2.0f` |
|
||||
| 456 | `KELLY_PRIOR_SUM_WINS_INDEX` | 0.01 | [0.001, 1.0] | `prior_sum_wins=0.01f` |
|
||||
| 457 | `KELLY_PRIOR_SUM_LOSSES_INDEX` | 0.01 | [0.001, 1.0] | `prior_sum_losses=0.01f` |
|
||||
|
||||
`ISV_TOTAL_DIM` bumped 454 → 458. Layout fingerprint regenerated.
|
||||
|
||||
Bounds rationale (Category-1 dimensional safety per `feedback_isv_for_adaptive_bounds.md`):
|
||||
- Counts in [0.5, 100]: below 0.5 the prior is effectively absent (kelly_f swings on first real trade); above 100 the prior dominates 10+ real trades (the maturity threshold for warmup_floor blend in `kelly_position_cap`).
|
||||
- Sums in [0.001, 1.0]: below 1e-3 hits the same numerical floor as `fmaxf(avg_loss, 0.0001f)` in the Kelly formula; above 1.0 implies a per-trade win/loss > 100% of equity which would be risk-management-failure territory.
|
||||
|
||||
### Producer
|
||||
|
||||
`crates/ml/src/cuda_pipeline/kelly_bayesian_priors_update_kernel.cu` (NEW, 250 LOC):
|
||||
|
||||
- Single-block 256-thread kernel; block-tree-reduce in shmem (no `atomicAdd` per `feedback_no_atomicadd.md`).
|
||||
- Stride-based sweep accumulates four sums across all envs from the four `PS_KELLY_*` fields per env's portfolio_state row (PS_STRIDE=43 per state_layout.cuh).
|
||||
- Phase 2 (thread 0): pre-EMA bilateral clamp on each target slot, then per-slot Pearl-A first-observation bootstrap (sentinel match within 1e-6 → REPLACE; otherwise EMA blend), then defensive post-EMA bilateral clamp.
|
||||
- α=0.005 slow EMA (per-fold cadence; the Bayesian prior is a *prior belief* and should change slowly across folds — a fast-moving "prior" would just be a noisy Kelly estimate). Slower than `reward_cap_update`'s α=0.01.
|
||||
- Cold-start fallback: `total_wins + total_losses == 0` → kernel guard skips the EMA update and ISV slots remain unchanged (sentinel persists; consumers fall back to the `state_layout.cuh::KELLY_PRIOR_*_DEFAULT` macros via the NULL-tolerant + range guard pattern).
|
||||
|
||||
### Consumers migrated atomically
|
||||
|
||||
**Site A** — `kelly_cap_update_kernel.cu:39-42`:
|
||||
|
||||
```c
|
||||
// Before:
|
||||
const float prior_wins = 2.0f; /* + 3 more */
|
||||
|
||||
// After:
|
||||
const float prior_wins = kelly_prior_or_default(
|
||||
isv[KELLY_PRIOR_WINS_INDEX], KELLY_PRIOR_WINS_DEFAULT, 0.5f, 100.0f);
|
||||
/* + 3 more — same pattern */
|
||||
```
|
||||
|
||||
The `kelly_prior_or_default` device helper returns the ISV slot value if it's within the dimensional-safety range, else the `state_layout.cuh::KELLY_PRIOR_*_DEFAULT` macro (which equals the pre-P1-Producer hardcoded value). Cold-start (sentinel) is in-range so the macro path only fires for malformed prior state.
|
||||
|
||||
**Site B** — `trade_physics.cuh::kelly_position_cap:304-307`:
|
||||
|
||||
`kelly_position_cap` gained an `isv_signals_ptr` parameter (NULL-tolerant). The function reads the four ISV slots and falls back to `KELLY_PRIOR_*_DEFAULT` macros under either `isv_signals_ptr == NULL` (validation envs that don't pass the bus through) OR out-of-range (cold-start sentinel + malformed state). `apply_kelly_cap` (the only caller of `kelly_position_cap`) gained the same parameter and passes it through. The single call site in `unified_env_step_core` (line 898) already had `isv_signals_ptr` — it's the same parameter already used for SP5 Pearl 6 `kelly_f_smooth` and SP9 Fix 37 `kelly_warmup_floor_sp9` just above. No new parameter threading needed.
|
||||
|
||||
### Producer launch site
|
||||
|
||||
Wired in `training_loop.rs:393` RIGHT BEFORE `launch_kelly_cap_update`:
|
||||
|
||||
```rust
|
||||
if let Err(e) = fused.trainer().launch_kelly_bayesian_priors_update(ps_dev_ptr, n_envs) {
|
||||
tracing::warn!(epoch, "launch_kelly_bayesian_priors_update failed (non-fatal): {e}");
|
||||
}
|
||||
if let Err(e) = fused.launch_kelly_cap_update(ps_dev_ptr, n_envs) {
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
Order matters: the priors producer MUST run before the cap update so the cap kernel sees the freshly-blended priors.
|
||||
|
||||
### State reset registry + dispatch arms (C.10 lesson)
|
||||
|
||||
Four FoldReset entries (`sp14_p1_kelly_prior_*`) added to `state_reset_registry.rs`, plus four matching dispatch arms in `training_loop.rs::reset_named_state` that write the sentinels (2.0/2.0/0.01/0.01) at fold boundary. Without these dispatch arms, the FoldReset entry exists but no actual reset fires — the C.10 lesson where slot drifts across folds and the layout-fingerprint smoke test eventually catches it as a runtime crash.
|
||||
|
||||
The `every_fold_and_soft_reset_entry_has_dispatch_arm` regression test confirms all four new entries have arms.
|
||||
|
||||
### Sites modified
|
||||
|
||||
| File | LOC delta | Change |
|
||||
|------|-----------|--------|
|
||||
| `crates/ml/src/cuda_pipeline/sp14_isv_slots.rs` | +97 / -0 | 4 new slot constants + sentinels + bounds + EMA α + 2 new tests |
|
||||
| `crates/ml/src/cuda_pipeline/state_layout.cuh` | +37 / -0 | C #define mirrors for slots + sentinels + defaults |
|
||||
| `crates/ml/src/cuda_pipeline/kelly_bayesian_priors_update_kernel.cu` | +250 / -0 | NEW producer kernel |
|
||||
| `crates/ml/src/cuda_pipeline/kelly_cap_update_kernel.cu` | +37 / -8 | Consumer migration: ISV reads + helper for cold-start fallback |
|
||||
| `crates/ml/src/cuda_pipeline/trade_physics.cuh` | +63 / -9 | `kelly_position_cap` + `apply_kelly_cap` gain `isv_signals_ptr` parameter; consumer migration |
|
||||
| `crates/ml/src/cuda_pipeline/gpu_aux_trunk.rs` | +120 / -1 | `KellyBayesianPriorsUpdateOps` struct + `new()` + `launch()` |
|
||||
| `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` | +90 / -0 | Cubin static + struct field + constructor + `launch_kelly_bayesian_priors_update` wrapper |
|
||||
| `crates/ml/build.rs` | +21 / -0 | Manifest entry for new cubin |
|
||||
| `crates/ml/src/trainers/dqn/state_reset_registry.rs` | +35 / -0 | 4 FoldReset entries |
|
||||
| `crates/ml/src/trainers/dqn/trainer/training_loop.rs` | +63 / -0 | 4 dispatch arms + producer launch wiring |
|
||||
| `crates/ml/tests/sp14_oracle_tests.rs` | +390 / -0 | 4 GPU-gated oracle tests |
|
||||
| `docs/dqn-wire-up-audit.md` | +90 | This entry |
|
||||
|
||||
### Architectural decisions
|
||||
|
||||
1. **No new buffers**: producer reads from existing `portfolio_state[n_envs, PS_STRIDE]`, the same buffer `kelly_cap_update_kernel` already aggregates Kelly stats from at the same boundary. Symmetry preserved; no buffer-allocation surface area added.
|
||||
2. **Per-fold-end cadence (= per-epoch boundary in current scheduling)**: matches `kelly_cap_update`. The producer runs RIGHT BEFORE `kelly_cap_update` so that consumer sees the freshly-blended priors. Same ordering pattern as `aux_horizon_update_chain` runs before downstream consumers.
|
||||
3. **α=0.005 (slower than reward_cap's α=0.01)**: Bayesian prior is a *prior belief* and should change slowly across folds. A fast-moving "prior" would just be a noisy Kelly estimate, defeating the purpose.
|
||||
4. **NULL-tolerant + range guard on consumer side**: mirrors the existing `kelly_f_smooth` / `kelly_warmup_floor_sp9` patterns in `unified_env_step_core`. Validation envs that don't pass the bus through fall back to static defaults; in-range sentinels also fall back (within tolerance of the pre-P1-Producer hardcoded values). Single source of truth for the adaptive priors; consumers never re-derive bounds.
|
||||
5. **Atomic per-batch commit (Item 1 of audit) — Item 2 deferred**: the audit asked for two items combined; Item 1 is independently completable, Item 2's audit-spec was wrong (see Concerns). Per `feedback_no_partial_refactor`, Item 1 is committed atomically with all consumers migrated.
|
||||
|
||||
### Verification
|
||||
|
||||
- `SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check -p ml --tests --all-targets` — clean (warnings unchanged from prior baseline).
|
||||
- `cargo build -p ml --release` — clean.
|
||||
- `SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml --lib --release sp14` — 10/10 pass (including new `sp14_p1_kelly_prior_slot_layout_locked` + `all_sp14_p1_slots_fit_within_isv_total_dim`).
|
||||
- `cargo test -p ml --lib --release dispatch` — `every_fold_and_soft_reset_entry_has_dispatch_arm` passes (C.10 regression closed for the 4 new entries).
|
||||
- `cargo test -p ml --test sp14_oracle_tests --release layout_fingerprint` — `layout_fingerprint_bumps_after_sp14_wire` passes.
|
||||
- 4 new oracle tests in `sp14_oracle_tests.rs::sp14_p1_kelly_priors_gpu` (#[ignore = "requires GPU"]): Pearl-A bootstrap, no-realized-trades preserves ISV, bounds clamp, slow EMA after bootstrap. GPU smoke runs at deploy time.
|
||||
|
||||
### Cumulative WR-plateau fix series (continued)
|
||||
|
||||
- Class C bug 1 + P0-B (`8f218cab2`): replay buffer intent→realized + Kelly warmup floor wiring.
|
||||
- P0-C (`316db416b`): MIN_HOLD_TARGET adaptive from AVG_WIN_HOLD_TIME.
|
||||
- P0-A (`394de7d43`): REWARD_POS/NEG_CAP adaptive producer (slots 452-453).
|
||||
- P1 wiring (`c4b6d6ef2`): var_floor q_gap-only adaptive (1 of 4 wireable, 3 deferred for slot allocation).
|
||||
- P0-A-downstream (`657972a4b`): DD penalty + MIN_HOLD_PENALTY_MAX scale to POS_CAP_ADAPTIVE.
|
||||
- **P1-Producer (this commit): adaptive Bayesian Kelly priors (slots 454-457).**
|
||||
|
||||
### Concerns
|
||||
|
||||
1. **Item 2 (MIN_HOLD_TEMPERATURE EMA) DEFERRED — audit-spec error**: The prompt described "MIN_HOLD_TEMPERATURE = 0.5f hardcoded somewhere" but the actual code has `MIN_HOLD_TEMPERATURE_{START=50.0f, END=5.0f, DECAY=20.0f}` defined in `state_layout.cuh:270-272` and consumed via the **per-epoch annealing schedule** `min_hold_temperature_for_epoch(epoch)` in `training_loop.rs:68-73`:
|
||||
|
||||
```rust
|
||||
pub(crate) fn min_hold_temperature_for_epoch(epoch: usize) -> f32 {
|
||||
const T_START: f32 = 50.0;
|
||||
const T_END: f32 = 5.0;
|
||||
const DECAY_RATE: f32 = 20.0;
|
||||
T_END + (T_START - T_END) * (-(epoch as f32) / DECAY_RATE).exp()
|
||||
}
|
||||
```
|
||||
|
||||
This is already an epoch-driven adaptive schedule (50→5 over training) per the SP12 v3 design. The kernel takes T as a runtime scalar specifically to enable Phase 2 ISV-driven lift "without recompiling cubin" per the design comment in the same function — Phase 2 is anticipated, but the *signal* that should drive it is a separate spec decision. Per `feedback_no_quickfixes.md` (every issue gets a proper fix per established patterns), guessing at the right Phase 2 signal would not be principled — the right move is to defer until a follow-up spec defines the signal (likely a feedback control on observed hold-length distribution vs target band, but that's a design decision, not a wiring fix). Reporting back per the prompt's hard rule #8.
|
||||
|
||||
2. **Layout-fingerprint bump invalidates all pre-P1-Producer DQN checkpoints**: `LAYOUT_FINGERPRINT_CURRENT` regenerated from the new seed string with `KELLY_PRIOR_*=454..457` and `ISV_TOTAL_DIM=458`. Old checkpoints fail-fast on load (intentional, per spec §4.A.2 — no migration path). New training runs only.
|
||||
|
||||
3. **Validation envs receive the bus through `isv_signals_ptr`**: the migration extends `apply_kelly_cap` / `kelly_position_cap` signatures with `isv_signals_ptr`. NULL-tolerant for safety, but production code should pass the bus through so val and train see the same priors. `unified_env_step_core` (line 898) already does — same parameter as `kelly_f_smooth` / `kelly_warmup_floor_sp9` paths. No `backtest_env_kernel.cu` direct callers verified — the only caller in the codebase is `unified_env_step_core` (verified by `grep -nE "apply_kelly_cap\\("`).
|
||||
|
||||
Reference in New Issue
Block a user