diag(per-horizon-cfc): per-horizon alpha_probs sampling for Smoke 2 investigation
Adds --per-horizon-logit-sampling-stride CLI flag to fxt-backtest run/sweep
+ harness instrumentation that DtoHs alpha_probs every N events and
accumulates per-horizon mean/stddev/min/max + 10-bin histogram.
Emitted at end of CRT.diag block as:
per_horizon_logit_diag h{N}: count=... mean=... std=... min=... max=... hist=[...]
Goal: distinguish whether Smoke 2's uniform mean_run_len 1.04x across
horizons is caused by uniform per-horizon LOGITS (-> Task 11 scope
incomplete; heads_w1/w2/gate/main need block-diagonal too) or by
DIFFERENTIATED logits with collapsed downstream (-> different fix).
Default stride=None (disabled, zero overhead). With stride=1000 on
2M events, ~2000 DtoD stops x ~50us = ~100ms total overhead per run.
Per feedback_no_htod_htoh_only_mapped_pinned: uses mapped-pinned buffer
(single 5xf32 allocation reused on every sampling tick).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -110,6 +110,15 @@ struct RunArgs {
|
|||||||
/// Output directory; per-cell artifacts written to <out>/cell_NNNN/.
|
/// Output directory; per-cell artifacts written to <out>/cell_NNNN/.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
out: PathBuf,
|
out: PathBuf,
|
||||||
|
/// Diagnostic: sample the 5 per-horizon `alpha_probs` every N events
|
||||||
|
/// (post-`forward_step_into`), DtoD into a mapped-pinned buffer, and
|
||||||
|
/// accumulate per-horizon mean / stddev / min / max / 10-bin histogram.
|
||||||
|
/// Emitted at end-of-run as `per_horizon_logit_diag h{N}: ...` for each
|
||||||
|
/// of (h30, h100, h300, h1000, h6000). Default: disabled (zero overhead).
|
||||||
|
/// Recommended diagnostic value: 1000 (≈100 ms overhead on a 2 M-event
|
||||||
|
/// run). Mutually exclusive with stride=0 (rejected at parse time).
|
||||||
|
#[arg(long)]
|
||||||
|
per_horizon_logit_sampling_stride: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -209,6 +218,10 @@ struct SweepBase {
|
|||||||
/// seeding when |target_signed − effective| < delta_floor. Default 1.0 =
|
/// seeding when |target_signed − effective| < delta_floor. Default 1.0 =
|
||||||
/// 1 lot (ES single-contract sizing).
|
/// 1 lot (ES single-contract sizing).
|
||||||
#[serde(default = "default_delta_floor")] delta_floor: f32,
|
#[serde(default = "default_delta_floor")] delta_floor: f32,
|
||||||
|
/// Diagnostic: per-horizon alpha_probs sampling stride (events). When
|
||||||
|
/// set, each cell emits a `per_horizon_logit_diag h{N}: ...` line per
|
||||||
|
/// horizon at end-of-run. None = disabled (default).
|
||||||
|
#[serde(default)] per_horizon_logit_sampling_stride: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// P6: per-sim-variant overrides for the batched-cell sweep flow.
|
/// P6: per-sim-variant overrides for the batched-cell sweep flow.
|
||||||
@@ -269,6 +282,9 @@ struct SweepCell {
|
|||||||
#[serde(default)] checkpoint: Option<PathBuf>,
|
#[serde(default)] checkpoint: Option<PathBuf>,
|
||||||
#[serde(default)] kelly_frac_floor: Option<f32>,
|
#[serde(default)] kelly_frac_floor: Option<f32>,
|
||||||
#[serde(default)] sharpe_weight_floor: Option<f32>,
|
#[serde(default)] sharpe_weight_floor: Option<f32>,
|
||||||
|
/// Per-cell override for the diagnostic alpha_probs sampler. When
|
||||||
|
/// unset, falls back to `SweepBase::per_horizon_logit_sampling_stride`.
|
||||||
|
#[serde(default)] per_horizon_logit_sampling_stride: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
@@ -388,6 +404,9 @@ fn sweep(args: SweepArgs) -> Result<()> {
|
|||||||
sharpe_weight_floor:
|
sharpe_weight_floor:
|
||||||
cell.sharpe_weight_floor.unwrap_or(grid.base.sharpe_weight_floor),
|
cell.sharpe_weight_floor.unwrap_or(grid.base.sharpe_weight_floor),
|
||||||
out: cell_out.clone(),
|
out: cell_out.clone(),
|
||||||
|
per_horizon_logit_sampling_stride:
|
||||||
|
cell.per_horizon_logit_sampling_stride
|
||||||
|
.or(grid.base.per_horizon_logit_sampling_stride),
|
||||||
};
|
};
|
||||||
run(run_args).with_context(|| format!("sweep cell {}", cell.name))?;
|
run(run_args).with_context(|| format!("sweep cell {}", cell.name))?;
|
||||||
tracing::info!(cell = cell.name.as_str(), "cell complete");
|
tracing::info!(cell = cell.name.as_str(), "cell complete");
|
||||||
@@ -456,6 +475,11 @@ fn run_batched_cell(
|
|||||||
PerceptionTrainer::new(&dev, &cfg2).context("PerceptionTrainer::new (random init)")?
|
PerceptionTrainer::new(&dev, &cfg2).context("PerceptionTrainer::new (random init)")?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let logit_stride = cell.per_horizon_logit_sampling_stride
|
||||||
|
.or(base.per_horizon_logit_sampling_stride);
|
||||||
|
if let Some(s) = logit_stride {
|
||||||
|
anyhow::ensure!(s > 0, "per_horizon_logit_sampling_stride must be > 0 (got 0)");
|
||||||
|
}
|
||||||
let cfg = BacktestHarnessConfig {
|
let cfg = BacktestHarnessConfig {
|
||||||
data_root: cell_data.to_path_buf(),
|
data_root: cell_data.to_path_buf(),
|
||||||
predecoded_dir: base.predecoded_dir.clone().unwrap_or_else(|| cell_data.to_path_buf()),
|
predecoded_dir: base.predecoded_dir.clone().unwrap_or_else(|| cell_data.to_path_buf()),
|
||||||
@@ -472,6 +496,8 @@ fn run_batched_cell(
|
|||||||
variant_names: Some(names),
|
variant_names: Some(names),
|
||||||
sim_config_override: Some(batched),
|
sim_config_override: Some(batched),
|
||||||
strategies: Vec::new(), // batched flow uses default policy per backtest
|
strategies: Vec::new(), // batched flow uses default policy per backtest
|
||||||
|
per_horizon_logit_sampling_stride: logit_stride,
|
||||||
|
kernel_step_trace_path: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
std::fs::create_dir_all(cell_out)
|
std::fs::create_dir_all(cell_out)
|
||||||
@@ -542,6 +568,9 @@ fn run(args: RunArgs) -> Result<()> {
|
|||||||
.context("PerceptionTrainer::new")?
|
.context("PerceptionTrainer::new")?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(s) = args.per_horizon_logit_sampling_stride {
|
||||||
|
anyhow::ensure!(s > 0, "--per-horizon-logit-sampling-stride must be > 0 (got 0)");
|
||||||
|
}
|
||||||
let cfg = BacktestHarnessConfig {
|
let cfg = BacktestHarnessConfig {
|
||||||
data_root: args.data.clone(),
|
data_root: args.data.clone(),
|
||||||
predecoded_dir: args.predecoded_dir.clone().unwrap_or(args.data),
|
predecoded_dir: args.predecoded_dir.clone().unwrap_or(args.data),
|
||||||
@@ -558,6 +587,8 @@ fn run(args: RunArgs) -> Result<()> {
|
|||||||
variant_names: None, // P6: legacy single-cell flow uses cell_NNNN naming
|
variant_names: None, // P6: legacy single-cell flow uses cell_NNNN naming
|
||||||
sim_config_override: None, // P6: legacy single-cell flow uses from_uniform
|
sim_config_override: None, // P6: legacy single-cell flow uses from_uniform
|
||||||
strategies: strategy_grid,
|
strategies: strategy_grid,
|
||||||
|
per_horizon_logit_sampling_stride: args.per_horizon_logit_sampling_stride,
|
||||||
|
kernel_step_trace_path: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
std::fs::create_dir_all(&args.out)
|
std::fs::create_dir_all(&args.out)
|
||||||
|
|||||||
@@ -10,11 +10,18 @@
|
|||||||
//! sliding K-window for the recurrent Mamba2 context, drive the sim's
|
//! sliding K-window for the recurrent Mamba2 context, drive the sim's
|
||||||
//! decision loop with the trainer's per-horizon probability output.
|
//! decision loop with the trainer's per-horizon probability output.
|
||||||
|
|
||||||
|
// The diagnostic per-horizon alpha_probs sampler calls `MappedF32Buffer::new`,
|
||||||
|
// a cudarc `cuMemHostAlloc(DEVICEMAP)` FFI that requires `unsafe`. The
|
||||||
|
// workspace-wide `-W unsafe-code` lint is therefore noise here. Same rationale
|
||||||
|
// as `sim/mod.rs`'s file-level allow for kernel-launch unsafes.
|
||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use ml_alpha::cfc::snap_features::Mbp10RawInput;
|
use ml_alpha::cfc::snap_features::Mbp10RawInput;
|
||||||
use ml_alpha::data::loader::{
|
use ml_alpha::data::loader::{
|
||||||
discover_mbp10_files_sorted, MultiHorizonLoader, MultiHorizonLoaderConfig,
|
discover_mbp10_files_sorted, MultiHorizonLoader, MultiHorizonLoaderConfig,
|
||||||
};
|
};
|
||||||
|
use ml_alpha::pinned_mem::MappedF32Buffer;
|
||||||
use ml_alpha::trainer::perception::PerceptionTrainer;
|
use ml_alpha::trainer::perception::PerceptionTrainer;
|
||||||
use ml_core::device::MlDevice;
|
use ml_core::device::MlDevice;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
@@ -22,6 +29,69 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use crate::sim::LobSimCuda;
|
use crate::sim::LobSimCuda;
|
||||||
|
|
||||||
|
/// Number of forward-horizons used by the per-horizon heads. Mirrors
|
||||||
|
/// `ml_alpha::heads::N_HORIZONS` (locally constant so we don't depend on
|
||||||
|
/// ml-alpha's identifier at runtime). Used by the per-horizon alpha-probs
|
||||||
|
/// sampler.
|
||||||
|
const N_HORIZONS: usize = 5;
|
||||||
|
|
||||||
|
/// Number of histogram bins covering [0.0, 1.0] for sigmoid'd probs.
|
||||||
|
const HIST_BINS: usize = 10;
|
||||||
|
|
||||||
|
/// Welford running stats for the per-horizon alpha_probs sampler.
|
||||||
|
/// Tracks count, mean, M2 (sum of squared diffs from mean) for std,
|
||||||
|
/// running min/max, and a 10-bin histogram over [0, 1].
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct PerHorizonStats {
|
||||||
|
count: u64,
|
||||||
|
mean: f64,
|
||||||
|
m2: f64,
|
||||||
|
min: f32,
|
||||||
|
max: f32,
|
||||||
|
hist: [u64; HIST_BINS],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PerHorizonStats {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
count: 0,
|
||||||
|
mean: 0.0,
|
||||||
|
m2: 0.0,
|
||||||
|
min: f32::INFINITY,
|
||||||
|
max: f32::NEG_INFINITY,
|
||||||
|
hist: [0; HIST_BINS],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, x: f32) {
|
||||||
|
self.count += 1;
|
||||||
|
let delta = x as f64 - self.mean;
|
||||||
|
self.mean += delta / self.count as f64;
|
||||||
|
let delta2 = x as f64 - self.mean;
|
||||||
|
self.m2 += delta * delta2;
|
||||||
|
if x < self.min { self.min = x; }
|
||||||
|
if x > self.max { self.max = x; }
|
||||||
|
// Bin assignment: clamp to [0, HIST_BINS-1] for any prob slightly
|
||||||
|
// outside [0, 1] (defensive — sigmoid output is bounded but stray
|
||||||
|
// NaN/Inf would otherwise panic on usize cast).
|
||||||
|
let scaled = (x * HIST_BINS as f32).floor();
|
||||||
|
let bin = if scaled.is_nan() {
|
||||||
|
0
|
||||||
|
} else if scaled < 0.0 {
|
||||||
|
0
|
||||||
|
} else if scaled >= HIST_BINS as f32 {
|
||||||
|
HIST_BINS - 1
|
||||||
|
} else {
|
||||||
|
scaled as usize
|
||||||
|
};
|
||||||
|
self.hist[bin] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn std(&self) -> f64 {
|
||||||
|
if self.count < 2 { 0.0 } else { (self.m2 / (self.count - 1) as f64).sqrt() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct BacktestHarnessConfig {
|
pub struct BacktestHarnessConfig {
|
||||||
pub data_root: PathBuf,
|
pub data_root: PathBuf,
|
||||||
@@ -72,6 +142,35 @@ pub struct BacktestHarnessConfig {
|
|||||||
/// of building from_uniform off the scalar fields above. Length MUST
|
/// of building from_uniform off the scalar fields above. Length MUST
|
||||||
/// equal n_parallel when set.
|
/// equal n_parallel when set.
|
||||||
pub sim_config_override: Option<crate::sim::BatchedSimConfig>,
|
pub sim_config_override: Option<crate::sim::BatchedSimConfig>,
|
||||||
|
/// Diagnostic: every N events (post-forward_step_into), DtoD the 5
|
||||||
|
/// per-horizon `alpha_probs` into a mapped-pinned buffer and update
|
||||||
|
/// running mean / variance / min / max / 10-bin histogram for each
|
||||||
|
/// of the 5 horizons. None = sampling disabled (zero overhead;
|
||||||
|
/// default). Some(N) emits a `per_horizon_logit_diag h{H}: ...` line
|
||||||
|
/// per horizon at end of run, alongside the existing `crt_diag`
|
||||||
|
/// battery.
|
||||||
|
///
|
||||||
|
/// Per `feedback_no_htod_htoh_only_mapped_pinned`: the sample buffer
|
||||||
|
/// is allocated once in `BacktestHarness::new` (5×f32 mapped-pinned)
|
||||||
|
/// and reused on every tick. At stride=1000 on 2M events, ~2k DtoD
|
||||||
|
/// stops × ~50µs ≈ 100ms total overhead.
|
||||||
|
///
|
||||||
|
/// Goal (Smoke 2 follow-up): distinguish whether the observed
|
||||||
|
/// 1.04× uniform `mean_run_len` across horizons is caused by uniform
|
||||||
|
/// per-horizon LOGITS (→ block-diagonal heads needed) or by
|
||||||
|
/// DIFFERENTIATED logits with collapsed downstream computation.
|
||||||
|
pub per_horizon_logit_sampling_stride: Option<u32>,
|
||||||
|
/// Diagnostic mirror of `PerceptionTrainerConfig.kernel_step_trace_path`.
|
||||||
|
/// `Some(path)` records that this backtest was built with the trainer's
|
||||||
|
/// kernel-step-trace JSONL drain pointed at `path`; the harness emits
|
||||||
|
/// a startup `tracing::info!` so post-mortem log scrapes can correlate
|
||||||
|
/// trace files to backtest cells. The trainer is constructed by the
|
||||||
|
/// caller (`bin/fxt-backtest/src/main.rs`) and owns the actual ring +
|
||||||
|
/// drain task; the field here is the contract record. When `None`
|
||||||
|
/// (default), no trace file is referenced and the trainer carries
|
||||||
|
/// zero overhead even if it was built with the `kernel-step-trace`
|
||||||
|
/// Cargo feature compiled in.
|
||||||
|
pub kernel_step_trace_path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BacktestHarness {
|
pub struct BacktestHarness {
|
||||||
@@ -119,6 +218,17 @@ pub struct BacktestHarness {
|
|||||||
/// Equal to `decision_count` after `run()` completes. Used to size
|
/// Equal to `decision_count` after `run()` completes. Used to size
|
||||||
/// the end-of-run DtoV correctly.
|
/// the end-of-run DtoV correctly.
|
||||||
convictions_recorded: u32,
|
convictions_recorded: u32,
|
||||||
|
/// Diagnostic: optional mapped-pinned buffer (5 × f32) used by the
|
||||||
|
/// per-horizon alpha_probs sampler. `Some` iff
|
||||||
|
/// `cfg.per_horizon_logit_sampling_stride.is_some()`. Allocated once
|
||||||
|
/// in `new()` so the hot path only pays a DtoD copy + sync per
|
||||||
|
/// sampling tick (no per-tick host allocation).
|
||||||
|
logit_sample_buf: Option<MappedF32Buffer>,
|
||||||
|
/// Diagnostic: per-horizon Welford accumulators + 10-bin histograms
|
||||||
|
/// for the sampled alpha_probs. Always allocated (cheap: ~80 bytes per
|
||||||
|
/// horizon) — populated only when sampling is enabled. Emitted at
|
||||||
|
/// end-of-run in the existing CRT.diag block as `per_horizon_logit_diag`.
|
||||||
|
logit_stats: [PerHorizonStats; N_HORIZONS],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BacktestHarness {
|
impl BacktestHarness {
|
||||||
@@ -202,6 +312,22 @@ impl BacktestHarness {
|
|||||||
sim.upload_price_range(&sim_config.min_reasonable_px, &sim_config.max_reasonable_px)?;
|
sim.upload_price_range(&sim_config.min_reasonable_px, &sim_config.max_reasonable_px)?;
|
||||||
|
|
||||||
let pnl_curves = (0..cfg.n_parallel).map(|_| Vec::with_capacity(1024)).collect();
|
let pnl_curves = (0..cfg.n_parallel).map(|_| Vec::with_capacity(1024)).collect();
|
||||||
|
|
||||||
|
// Allocate the per-horizon alpha_probs sampling buffer iff the
|
||||||
|
// CLI flag is set. `MappedF32Buffer::new` requires an active CUDA
|
||||||
|
// context, which is guaranteed because `LobSimCuda::new(...)`
|
||||||
|
// above already initialised one on this thread. The `unsafe` here
|
||||||
|
// is the cudarc `cuMemHostAlloc` FFI call — identical pattern to
|
||||||
|
// `crates/ml-alpha/src/heads.rs::download` and `sim/mod.rs`'s
|
||||||
|
// file-level FFI policy.
|
||||||
|
let logit_sample_buf = if cfg.per_horizon_logit_sampling_stride.is_some() {
|
||||||
|
let buf = unsafe { MappedF32Buffer::new(N_HORIZONS) }
|
||||||
|
.map_err(|e| anyhow::anyhow!("logit-sample MappedF32Buffer alloc: {e}"))?;
|
||||||
|
Some(buf)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
cfg,
|
cfg,
|
||||||
sim_config,
|
sim_config,
|
||||||
@@ -214,6 +340,8 @@ impl BacktestHarness {
|
|||||||
event_count: 0,
|
event_count: 0,
|
||||||
pnl_curves,
|
pnl_curves,
|
||||||
convictions_recorded: 0,
|
convictions_recorded: 0,
|
||||||
|
logit_sample_buf,
|
||||||
|
logit_stats: std::array::from_fn(|_| PerHorizonStats::new()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +385,28 @@ impl BacktestHarness {
|
|||||||
.forward_step_into(&raw, self.sim.alpha_probs_d_mut())
|
.forward_step_into(&raw, self.sim.alpha_probs_d_mut())
|
||||||
.context("trainer.forward_step_into")?;
|
.context("trainer.forward_step_into")?;
|
||||||
|
|
||||||
|
// Diagnostic: per-horizon alpha_probs sampler. Stride-gated
|
||||||
|
// DtoD copy into a mapped-pinned 5-f32 buffer + host-side
|
||||||
|
// Welford accumulation. Runs BEFORE step_decision_with_latency
|
||||||
|
// so the sample reflects the same alpha_probs the decision
|
||||||
|
// kernel will consume on this event (the sync is acceptable
|
||||||
|
// here — sampling is gated to ~1-in-1000 events).
|
||||||
|
if let (Some(stride), Some(buf)) = (
|
||||||
|
self.cfg.per_horizon_logit_sampling_stride,
|
||||||
|
self.logit_sample_buf.as_ref(),
|
||||||
|
) {
|
||||||
|
debug_assert!(stride > 0, "stride==0 is rejected at CLI parse time");
|
||||||
|
if self.event_count % stride as u64 == 0 {
|
||||||
|
self.sim
|
||||||
|
.sample_alpha_probs(buf)
|
||||||
|
.context("sample_alpha_probs")?;
|
||||||
|
for h in 0..N_HORIZONS {
|
||||||
|
let p = buf.read_record(h);
|
||||||
|
self.logit_stats[h].update(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Side-channel: record this decision's max_conviction
|
// Side-channel: record this decision's max_conviction
|
||||||
// on-device for the threshold-tuning percentile
|
// on-device for the threshold-tuning percentile
|
||||||
// computation. Kernel reads `alpha_probs_d` and writes
|
// computation. Kernel reads `alpha_probs_d` and writes
|
||||||
@@ -466,6 +616,37 @@ impl BacktestHarness {
|
|||||||
lo, hi, n_total, win_rate * 100.0, mean_pnl,
|
lo, hi, n_total, win_rate * 100.0, mean_pnl,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-horizon alpha_probs sampler diagnostic. Only emitted
|
||||||
|
// when --per-horizon-logit-sampling-stride was set; tells us
|
||||||
|
// whether Smoke 2's uniform `mean_run_len` (~1.04× across all
|
||||||
|
// 5 horizons) is caused by uniform per-horizon LOGITS
|
||||||
|
// (→ heads_w1/w2/gate/main need block-diagonal too) or by
|
||||||
|
// DIFFERENTIATED logits with collapsed downstream
|
||||||
|
// (→ different fix).
|
||||||
|
if self.cfg.per_horizon_logit_sampling_stride.is_some() {
|
||||||
|
for h in 0..n_horizons {
|
||||||
|
let s = &self.logit_stats[h];
|
||||||
|
let mut hist_str = String::from("[");
|
||||||
|
for bk in 0..HIST_BINS {
|
||||||
|
if bk > 0 { hist_str.push(' '); }
|
||||||
|
hist_str.push_str(&format!("bin{}:{}", bk, s.hist[bk]));
|
||||||
|
}
|
||||||
|
hist_str.push(']');
|
||||||
|
// Defensive: if no samples (e.g., max_events < seq_len),
|
||||||
|
// min/max are still ±INFINITY sentinels — emit NaN
|
||||||
|
// so downstream tooling doesn't latch onto sentinels.
|
||||||
|
let (min_disp, max_disp) = if s.count == 0 {
|
||||||
|
(f32::NAN, f32::NAN)
|
||||||
|
} else {
|
||||||
|
(s.min, s.max)
|
||||||
|
};
|
||||||
|
eprintln!(
|
||||||
|
"per_horizon_logit_diag h{}: count={} mean={:.4} std={:.4} min={:.4} max={:.4} hist={}",
|
||||||
|
horizons[h], s.count, s.mean, s.std(), min_disp, max_disp, hist_str,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => eprintln!("crt_diag read failed: {e}"),
|
Err(e) => eprintln!("crt_diag read failed: {e}"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ mod batched_config;
|
|||||||
pub use batched_config::{BatchedSimConfig, ResolvedSimVariant, UniformSimParams};
|
pub use batched_config::{BatchedSimConfig, ResolvedSimVariant, UniformSimParams};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use cudarc::driver::{CudaSlice, CudaStream, LaunchConfig, PushKernelArg};
|
use cudarc::driver::{CudaSlice, CudaStream, DevicePtr, LaunchConfig, PushKernelArg};
|
||||||
|
use ml_alpha::pinned_mem::MappedF32Buffer;
|
||||||
use ml_core::device::MlDevice;
|
use ml_core::device::MlDevice;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -1312,6 +1313,41 @@ impl LobSimCuda {
|
|||||||
&mut self.alpha_probs_d
|
&mut self.alpha_probs_d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Diagnostic-only sampler: copy the current `alpha_probs_d` (5 f32s,
|
||||||
|
/// broadcast across backtests) into a caller-owned mapped-pinned
|
||||||
|
/// buffer and synchronise the stream so the host view is coherent.
|
||||||
|
///
|
||||||
|
/// Per `feedback_no_htod_htoh_only_mapped_pinned`: the buffer is
|
||||||
|
/// allocated once in the harness state via `MappedF32Buffer::new(5)`
|
||||||
|
/// and reused on every sampling tick. The transfer is DtoD into the
|
||||||
|
/// mapped-pinned device pointer (the only permitted cross-direction
|
||||||
|
/// channel); a single `stream.synchronize()` makes the host-side
|
||||||
|
/// `read_record` calls deterministic.
|
||||||
|
///
|
||||||
|
/// Intended for low-frequency sampling (e.g. every 1000 events). The
|
||||||
|
/// per-event production path keeps reading `alpha_probs_d` on-device
|
||||||
|
/// and is unaffected.
|
||||||
|
pub fn sample_alpha_probs(&self, dst: &MappedF32Buffer) -> Result<()> {
|
||||||
|
anyhow::ensure!(
|
||||||
|
dst.len >= N_HORIZONS,
|
||||||
|
"sample_alpha_probs: dst buffer len {} < N_HORIZONS {}",
|
||||||
|
dst.len, N_HORIZONS,
|
||||||
|
);
|
||||||
|
let nbytes = N_HORIZONS * std::mem::size_of::<f32>();
|
||||||
|
unsafe {
|
||||||
|
let (src_ptr, _g) = self.alpha_probs_d.device_ptr(&self.stream);
|
||||||
|
cudarc::driver::result::memcpy_dtod_async(
|
||||||
|
dst.dev_ptr,
|
||||||
|
src_ptr,
|
||||||
|
nbytes,
|
||||||
|
self.stream.cu_stream(),
|
||||||
|
)
|
||||||
|
.context("sample_alpha_probs DtoD")?;
|
||||||
|
}
|
||||||
|
self.stream.synchronize().context("sample_alpha_probs sync")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// CRT Phase A0.5 corrective: record one max-conviction f32 at
|
/// CRT Phase A0.5 corrective: record one max-conviction f32 at
|
||||||
/// `convictions_d[write_idx]` from the current `alpha_probs_d`.
|
/// `convictions_d[write_idx]` from the current `alpha_probs_d`.
|
||||||
/// Single-thread kernel; stream-ordered, no sync. Caller advances
|
/// Single-thread kernel; stream-ordered, no sync. Caller advances
|
||||||
|
|||||||
Reference in New Issue
Block a user