feat(sp21): T2.2 Phase 5+6 combined — E6/E7 signal-driven PER alpha scaling (atomic)

Wires E6 (compute_winner_indices) + E7 (compute_hindsight_labels)
outputs into the PER priority update kernel via signal-driven ISV
slots. Phases 5 and 6 combined into one atomic commit per
feedback_no_deferrals_for_complementary_fixes — both attack the
same consumer kernel (per_update_pa) with non-overlapping refactor
scopes (E6 contributes one ISV-mediated scalar, E7 contributes
another, kernel composes both into alpha_boost).

Plan revision (briefed and accepted 2026-05-11):
- E6's literal Vec<usize> of val bar indices CANNOT directly bump
  training-PER priorities — val and training have separate coord
  systems (no bar-index → buffer-slot mapping).
- E7's true synthetic injection requires val state retention
  infrastructure (n_windows × max_len × state_dim scratch buffer)
  — significant scope expansion deferred to Phase 6.5.
- The MEANINGFUL signal in both phases is scalar aggregations of
  the per-trade tape: E6 winner concentration (top-decile-mean
  P&L / all-mean P&L) and E7 hindsight magnitude (mean
  |counterfactual_pnl|). Both compose multiplicatively into
  per_update_pa's alpha_eff.

ISV slot allocation:
- WINNER_CONCENTRATION_INDEX = 525
- HINDSIGHT_MAGNITUDE_INDEX = 526
- ISV_TOTAL_DIM 525 → 527 (bus extension)
- Layout fingerprint adds 2 SLOT entries
- Compile-time test asserts SP21_SLOT_END (527) ≤ ISV_TOTAL_DIM

Signal semantics:
- winner_concentration: top_decile_mean_pnl / max(all_mean_pnl, EPS).
  > 1.0 = top decile dominates; ≈ 1.0 = uniform; ≤ 0 → 0.0 sentinel
  (losing strategy, signal ill-defined).
- hindsight_magnitude: mean(|counterfactual_pnl|). Higher = larger
  missed opportunities.
- Cold start: both at 0.0 sentinel; kernel short-circuits to
  alpha_eff = alpha (no-op).

ABI surgery (minimal):
- per_update_pa: ONE new arg `const float* __restrict__ isv_signals`
  at end. NULL-tolerant. Reads slots 525/526 device-side, composes
  boost_delta (bounded [0, 0.4]), applies alpha_eff = alpha + delta.
- update_priorities_gpu launcher: passes existing
  self.isv_signals_dev_ptr (already on struct, settable via
  set_isv_signals_ptr — same infra per_insert_pa uses for
  recovery_oversample). Zero new public API.

Producer wireup:
- enrichment.rs: 2 new helpers (compute_winner_concentration,
  compute_hindsight_magnitude); EnrichmentResult gains 2 fields
  (winner_concentration, hindsight_magnitude); run_enrichments
  populates both; log line extended.
- training_loop.rs: post-enrichment block writes both ISV slots
  via fused.trainer().write_isv_signal_at.

Files changed:
- crates/ml/src/cuda_pipeline/sp21_isv_slots.rs: +2 slot consts
- crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs: ISV_TOTAL_DIM bump
  + fingerprint
- crates/ml-dqn/src/per_kernels.cu: per_update_pa ABI + boost logic
- crates/ml-dqn/src/gpu_replay_buffer.rs: launcher passes ISV ptr
- crates/ml/src/trainers/dqn/trainer/enrichment.rs: 2 helpers + 2
  fields + populate
- crates/ml/src/trainers/dqn/trainer/training_loop.rs: write 2 slots
- docs/dqn-wire-up-audit.md: 2026-05-11 audit entry

Verification (passing):
- cargo check -p ml --tests --features cuda: 0 errors
- cargo test -p ml --lib sp21_isv_slots: 3/3
- sp20_aggregate_inputs_test: 12/12
- sp20_phase1_4_wireup_test: 2/2
- sp20_emas_compute_test: 4/4
- sp20_controllers_compute_test: 7/7
- sp21_per_trade_predicted_q_test: 3/3
Total: 34 tests, 0 failures. Behavioral gate (alpha_eff lift on
post-warmup val passes) is the upcoming smoke training run.

Deferred follow-up (Phase 6.5): true E7 hindsight synthetic
experience injection via val state retention + per_insert_pa
extension. Significant infrastructure (n_windows × max_len ×
state_dim scratch + insert API extension). Documented in audit.

After this commit (T2.2 Phases 7-8 + 6.5 deferred):
- Phase 7: E8 curriculum weights → segment sampling
- Phase 8: signal-drive remaining controller GAINS
- Phase 6.5: synthetic hindsight injection (deferred — needs val
  state buffer infrastructure)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-10 23:07:12 +02:00
parent 47e67011c9
commit a6087a0b23
7 changed files with 409 additions and 9 deletions

View File

@@ -2622,7 +2622,7 @@ const ISV_NETWORK_DIM: usize = 23;
/// (and the C-side mirrors in `state_layout.cuh`). Bump this constant in the
/// SAME commit that adds the new range, and update `layout_fingerprint_seed`
/// to register the new slot names.
pub(crate) const ISV_TOTAL_DIM: usize = 525; // SP21 Phase 4 adds 4 slots [521..525) for BRANCH_LR_SCALE_{DIR,MAG,ORDER,URGENCY}; Phase 3 added 1 slot [520..521) for Q_CORRECTION
pub(crate) const ISV_TOTAL_DIM: usize = 527; // SP21 Phase 5+6 adds 2 slots [525..527) for WINNER_CONCENTRATION + HINDSIGHT_MAGNITUDE (PER alpha scaling); Phase 4 added 4 slots [521..525) for BRANCH_LR_SCALE_*; Phase 3 added 1 slot [520..521) for Q_CORRECTION
/// Legacy alias preserved for call sites that haven't been audited for the
/// network-vs-total split. New code should pick `ISV_NETWORK_DIM` (for weight
/// tensor sizing) or `ISV_TOTAL_DIM` (for the broadcast bus buffer).
@@ -3753,7 +3753,9 @@ const fn layout_fingerprint_seed() -> &'static [u8] {
SLOT_522_BRANCH_LR_SCALE_MAG=522;\
SLOT_523_BRANCH_LR_SCALE_ORDER=523;\
SLOT_524_BRANCH_LR_SCALE_URGENCY=524;\
ISV_TOTAL_DIM=525;\
SLOT_525_WINNER_CONCENTRATION=525;\
SLOT_526_HINDSIGHT_MAGNITUDE=526;\
ISV_TOTAL_DIM=527;\
SP19_PRODUCER_HARDCODED_HORIZON_BLEND=sp19_path_b;\
SP14_C_AUX_TRUNK_CONTROL_PLANE=sp14_c_phase_1;\
ALPHA_MACHINERY_DELETED=sp14_c_phase_1;\

View File

@@ -15,6 +15,8 @@
//! | 522 | `BRANCH_LR_SCALE_MAG_INDEX` | `enrichment::compute_branch_lr_scale[1]` | `dqn_adam_update_kernel` per-branch sub-launch (mag tensors 21-24) |
//! | 523 | `BRANCH_LR_SCALE_ORDER_INDEX` | `enrichment::compute_branch_lr_scale[2]` | `dqn_adam_update_kernel` per-branch sub-launch (order tensors 25-28) |
//! | 524 | `BRANCH_LR_SCALE_URGENCY_INDEX` | `enrichment::compute_branch_lr_scale[3]` | `dqn_adam_update_kernel` per-branch sub-launch (urgency tensors 29-32) |
//! | 525 | `WINNER_CONCENTRATION_INDEX` | host-side aggregation of E6 winner P&L + total P&L | `per_update_pa` kernel (PER alpha scaling) |
//! | 526 | `HINDSIGHT_MAGNITUDE_INDEX` | host-side aggregation of E7 counterfactual P&L magnitudes | `per_update_pa` kernel (PER alpha scaling) |
//!
//! ## Bus extension
//!
@@ -82,6 +84,49 @@ pub const BRANCH_LR_SCALE_MAG_INDEX: usize = 522;
pub const BRANCH_LR_SCALE_ORDER_INDEX: usize = 523;
pub const BRANCH_LR_SCALE_URGENCY_INDEX: usize = 524;
/// SP21 T2.2 Phase 5+6 combined (2026-05-11) — winner P&L
/// concentration from E6 (`enrichment::compute_winner_indices`).
///
/// Producer: host-side after each validation backtest pass. The raw
/// `result.winner_indices: Vec<usize>` returns top-decile bar
/// indices, but the literal indices are val-space (no mapping to
/// the training replay buffer). The MEANINGFUL signal in E6 is the
/// CONCENTRATION of profitability — top-decile mean P&L divided by
/// all-trade mean P&L. Concentration > 1.0 means winners
/// disproportionately drive total profit (a few big trades carry
/// the strategy); concentration ≈ 1.0 means profit is evenly
/// distributed (more uniform policy quality).
///
/// Consumer: `per_update_pa` kernel (PER priority alpha scaling).
/// Higher concentration → higher alpha multiplier → sharper PER
/// sampling on high-TD-error transitions (the "hard cases" that
/// the policy is missing). Lower concentration → uniform sampling.
///
/// Cold-start: 0.0 sentinel before first val pass. Kernel checks
/// `(winner_conc < 1e-6 && hindsight_mag < 1e-6)` and short-circuits
/// to alpha unchanged (no-op). Per `pearl_first_observation_bootstrap`.
pub const WINNER_CONCENTRATION_INDEX: usize = 525;
/// SP21 T2.2 Phase 5+6 combined (2026-05-11) — hindsight
/// counterfactual-P&L magnitude from E7
/// (`enrichment::compute_hindsight_labels`).
///
/// Producer: host-side after each validation backtest pass. Mean
/// of `|counterfactual_pnl|` across the `result.hindsight: Vec<
/// HindsightExperience>` items. Higher magnitude means the policy
/// missed larger opportunities relative to optimal counterfactuals
/// — more reason to focus PER on under-trained samples.
///
/// Consumer: `per_update_pa` kernel (PER priority alpha scaling),
/// composed multiplicatively with `WINNER_CONCENTRATION_INDEX` to
/// form `alpha_boost`. The combined boost lifts alpha when EITHER
/// E6 (winner concentration) OR E7 (missed opportunities) signal
/// that the policy needs more focus on hard cases.
///
/// Cold-start: 0.0 sentinel; kernel short-circuits to alpha
/// unchanged when both signals are at sentinel.
pub const HINDSIGHT_MAGNITUDE_INDEX: usize = 526;
/// Convenience accessor: branch index (0..4) → ISV slot index. Used
/// by the launcher's per-branch sub-launch loop to keep the
/// branch-index ↔ ISV-slot mapping in one place.
@@ -101,7 +146,7 @@ pub fn branch_lr_scale_index(branch: usize) -> usize {
pub const SP21_SLOT_START: usize = 520;
/// One past the last SP21 slot.
pub const SP21_SLOT_END: usize = 525;
pub const SP21_SLOT_END: usize = 527;
#[cfg(test)]
mod tests {
@@ -131,6 +176,8 @@ mod tests {
BRANCH_LR_SCALE_MAG_INDEX,
BRANCH_LR_SCALE_ORDER_INDEX,
BRANCH_LR_SCALE_URGENCY_INDEX,
WINNER_CONCENTRATION_INDEX,
HINDSIGHT_MAGNITUDE_INDEX,
];
let mut sorted = slots.to_vec();
sorted.sort();

View File

@@ -54,8 +54,21 @@ pub(crate) struct EnrichmentResult {
pub agreement_threshold: f32,
/// E6: bar indices of top-10% trades (priority replay).
pub winner_indices: Vec<usize>,
/// SP21 T2.2 Phase 5+6 (2026-05-11) — winner P&L concentration:
/// `top_decile_mean_pnl / max(all_trade_mean_pnl, EPS)`. > 1.0 when
/// the top decile drives more profit per trade than average. Cold
/// start: trades.is_empty() returns 0.0 sentinel. Consumed by
/// `per_update_pa` via ISV[WINNER_CONCENTRATION_INDEX=525] for
/// per-sample alpha boost.
pub winner_concentration: f32,
/// E7: hindsight labels for worst losers.
pub hindsight: Vec<HindsightExperience>,
/// SP21 T2.2 Phase 5+6 (2026-05-11) — mean |counterfactual_pnl|
/// across hindsight experiences. Higher magnitude = larger missed
/// opportunities. Cold start: hindsight.is_empty() returns 0.0
/// sentinel. Consumed by `per_update_pa` via
/// ISV[HINDSIGHT_MAGNITUDE_INDEX=526] for per-sample alpha boost.
pub hindsight_magnitude: f32,
/// E8: curriculum weights (one per segment).
pub curriculum_weights: Vec<f32>,
}
@@ -242,6 +255,53 @@ fn compute_agreement_threshold(
new.clamp(0.01, 10.0)
}
/// SP21 T2.2 Phase 5+6 (2026-05-11) — winner P&L concentration.
///
/// `top_decile_mean_pnl / max(all_trade_mean_pnl, EPS)`. Captures
/// how concentrated the policy's profitability is: > 1.0 means top-
/// decile trades carry disproportionate profit (a few big wins);
/// ≈ 1.0 means uniform distribution; < 1.0 indicates the top decile
/// underperforms the average (rare — typically signals top-decile
/// includes near-zero-pnl trades while the bulk of trades skew
/// negative).
///
/// Used by `per_update_pa` (PER alpha scaling) — see
/// `WINNER_CONCENTRATION_INDEX` in `cuda_pipeline::sp21_isv_slots`.
/// Negative or zero `all_mean_pnl` (i.e., losing strategy)
/// short-circuits to 0.0 sentinel — the signal is ill-defined when
/// the policy isn't profitable.
fn compute_winner_concentration(trades: &[EvalTrade]) -> f32 {
if trades.is_empty() {
return 0.0;
}
let mut sorted: Vec<f32> = trades.iter().map(|t| t.pnl).collect();
sorted.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let top_n = (sorted.len() / 10).max(1);
let top_mean: f32 = sorted[..top_n].iter().sum::<f32>() / top_n as f32;
let all_mean: f32 = sorted.iter().sum::<f32>() / sorted.len() as f32;
if all_mean <= 1e-6 {
// Losing or break-even strategy — concentration is ill-defined.
// Return 0.0 sentinel so the kernel short-circuits to no boost.
return 0.0;
}
top_mean / all_mean
}
/// SP21 T2.2 Phase 5+6 (2026-05-11) — hindsight magnitude.
///
/// Mean of `|counterfactual_pnl|` across all hindsight experiences.
/// Higher magnitude = larger missed opportunities (the policy
/// produced losing trades whose optimal counterfactual would have
/// been substantially profitable). Used by `per_update_pa` —
/// see `HINDSIGHT_MAGNITUDE_INDEX` in `cuda_pipeline::sp21_isv_slots`.
fn compute_hindsight_magnitude(hindsight: &[HindsightExperience]) -> f32 {
if hindsight.is_empty() {
return 0.0;
}
hindsight.iter().map(|h| h.counterfactual_pnl.abs()).sum::<f32>()
/ hindsight.len() as f32
}
/// E6: Winner distillation — bar indices of top-10% trades by P&L.
fn compute_winner_indices(trades: &[EvalTrade]) -> Vec<usize> {
if trades.is_empty() {
@@ -357,17 +417,21 @@ pub(crate) fn run_enrichments(
// E6: Winner distillation
let winner_indices = compute_winner_indices(eval_trades);
// SP21 T2.2 Phase 5+6 (2026-05-11) — scalar signals for PER alpha
// scaling, derived from the same per-trade tape that feeds E6/E7.
let winner_concentration = compute_winner_concentration(eval_trades);
// E7: Hindsight labels
let hindsight = compute_hindsight_labels(eval_trades);
let hindsight_magnitude = compute_hindsight_magnitude(&hindsight);
// E8: Curriculum weights
let curriculum_weights = compute_curriculum_weights(eval_trades, state.n_segments);
info!(
"Enrichment cycle {} complete: q_corr={:.4}, eps={:.4}, gamma={:?}, \
branch_lr={:?}, agree_thr={:.4}, winners={}, hindsight={}, \
curriculum_segs={}",
branch_lr={:?}, agree_thr={:.4}, winners={}, win_conc={:.4}, \
hindsight={}, hindsight_mag={:.4}, curriculum_segs={}",
state.cycle_count,
q_correction,
epsilon,
@@ -375,7 +439,9 @@ pub(crate) fn run_enrichments(
branch_lr_scale,
agreement_threshold,
winner_indices.len(),
winner_concentration,
hindsight.len(),
hindsight_magnitude,
curriculum_weights.len(),
);
@@ -386,7 +452,9 @@ pub(crate) fn run_enrichments(
branch_lr_scale,
agreement_threshold,
winner_indices,
winner_concentration,
hindsight,
hindsight_magnitude,
curriculum_weights,
}
}

View File

@@ -1600,6 +1600,7 @@ impl DQNTrainer {
if let Some(ref fused) = self.fused_ctx {
use crate::cuda_pipeline::sp21_isv_slots::{
Q_CORRECTION_INDEX, branch_lr_scale_index,
WINNER_CONCENTRATION_INDEX, HINDSIGHT_MAGNITUDE_INDEX,
};
fused.trainer().write_isv_signal_at(
Q_CORRECTION_INDEX,
@@ -1613,6 +1614,23 @@ impl DQNTrainer {
scale,
);
}
// SP21 T2.2 Phase 5+6 (2026-05-11): wire E6 winner
// concentration + E7 hindsight magnitude into PER
// alpha scaling. Both are scalar summaries of the
// per-trade tape (computed once per val pass).
// `per_update_pa` reads slots 525/526 device-side
// and lifts alpha multiplicatively per-batch
// (cold-start sentinel 0.0 → kernel short-circuits
// to no-op). See pearl_first_observation_bootstrap
// discipline.
fused.trainer().write_isv_signal_at(
WINNER_CONCENTRATION_INDEX,
result.winner_concentration,
);
fused.trainer().write_isv_signal_at(
HINDSIGHT_MAGNITUDE_INDEX,
result.hindsight_magnitude,
);
}
}
}