Compare commits
18 Commits
chore/gite
...
sp15-trade
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c34d085e52 | ||
|
|
e9b5f51f1f | ||
|
|
1ade16438a | ||
|
|
5264111b2b | ||
|
|
74b0dd36ae | ||
|
|
001f2c7600 | ||
|
|
a90290bfc2 | ||
|
|
8836302baf | ||
|
|
80a8715cbf | ||
|
|
57b434379c | ||
|
|
7cf07875e7 | ||
|
|
567a0581e4 | ||
|
|
1a52b67d09 | ||
|
|
9ad1955abd | ||
|
|
6702c2e76c | ||
|
|
31c64372ec | ||
|
|
6d8ae93bbe | ||
|
|
7ca2d8b8ae |
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
# .claude/helpers/pre_commit_behavioral_suite.sh
|
||||
#
|
||||
# SP15 Phase 2 — behavioral suite gate. Blocks argo-train.sh if any
|
||||
# behavioral test fails. Suite runs <10 min on dev RTX 3050 Ti.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
echo "Running SP15 behavioral suite (gate for argo-train.sh)..."
|
||||
|
||||
if ! SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 \
|
||||
cargo test -p ml --test behavioral_suite --features cuda \
|
||||
-- --include-ignored --test-threads=1 2>&1 \
|
||||
| tee /tmp/sp15_behavioral_suite.log; then
|
||||
echo "Behavioral suite FAILED. argo-train.sh blocked."
|
||||
echo " See /tmp/sp15_behavioral_suite.log for details."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Behavioral suite PASS — argo-train.sh unblocked."
|
||||
@@ -1 +1 @@
|
||||
{"sessionId":"edd8cbc4-f0a4-4090-9e9a-424c797dee3d","pid":1676949,"procStart":"6609584","acquiredAt":1779464404408}
|
||||
{"sessionId":"33873aa5-4830-4d4e-bffe-dbff7a8f62fe","pid":3788906,"acquiredAt":1776942573722}
|
||||
@@ -1,17 +1,5 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "if echo \"${CLAUDE_TOOL_INPUT:-}\" | grep -qE 'argo-train\\.sh'; then \"$CLAUDE_PROJECT_DIR/.claude/helpers/pre_commit_behavioral_suite.sh\"; fi",
|
||||
"timeout": 600
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -7,9 +7,6 @@ test_data/feature-cache/
|
||||
/data/feature-cache/
|
||||
*.fxcache
|
||||
|
||||
# Tier 1.5 mid-smoke local data (symlinks to test_data/futures-baseline-mbp10/)
|
||||
test_data/futures-baseline-mid/
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -60,10 +57,6 @@ certs/**/*.serial
|
||||
!infra/modules/secrets/
|
||||
!infra/live/production/secrets/
|
||||
!infra/k8s/secrets/
|
||||
# Allow ml-alpha horizon-token kernel sources (no secrets, just the v2 attention path)
|
||||
!crates/ml-alpha/cuda/horizon_token_*
|
||||
!crates/ml-alpha/src/horizon_token_*
|
||||
!crates/ml-alpha/tests/horizon_token_*
|
||||
!infra/k8s/secrets/*.yaml
|
||||
!scripts/deploy-secrets.sh
|
||||
|
||||
@@ -192,8 +185,6 @@ test_data/databento/samples/
|
||||
*.pt
|
||||
*.pth
|
||||
*.bin
|
||||
# Exception: deterministic test fixtures (golden bytes for bit-equivalence)
|
||||
!crates/ml-alpha/tests/fixtures/*.bin
|
||||
# Load test results
|
||||
services/*/load_tests/results/
|
||||
services/*/load_tests/*.json
|
||||
@@ -203,5 +194,3 @@ crates/ml/ml/
|
||||
|
||||
# Foxhunt audit hook dedup state (cleared at SessionStart)
|
||||
.claude/.foxhunt-audit-state
|
||||
/config/ml/alpha_logits_cache.bin
|
||||
data/surfer/
|
||||
|
||||
18
CLAUDE.md
18
CLAUDE.md
@@ -66,24 +66,6 @@ cargo bench --bench database_performance
|
||||
- `SQLX_OFFLINE=true` — compiled SQL queries (no live DB needed for build)
|
||||
- `SCCACHE_BUCKET=foxhunt-sccache` — shared compile cache
|
||||
|
||||
### Tiered local validation (fast dev cycle)
|
||||
|
||||
Three-tier funnel before cluster submit (per `docs/superpowers/specs/2026-06-02-fast-dev-cycle.md`):
|
||||
|
||||
| Tier | Setup | Time | What it validates |
|
||||
|---|---|---|---|
|
||||
| 1 (correctness) | RTX 3050, b=16, 200 steps, 1 file | ~6 sec | Kernel correctness, ISV slots, NaN |
|
||||
| 1.5 (behavior) | RTX 3050, b=128, 2000+500 steps, 2 files | ~10 min | entropy, hold growth, controller stability, early Pearson |
|
||||
| 2 (eval verdict) | L40S, b=1024, 20k+5k, 9 files | ~70 min | eval pnl, eval wr, regime stratification |
|
||||
|
||||
**Tier 1.5 data path**: `test_data/futures-baseline-mid/` (symlinks to existing MBP-10 files for fold-1 train/eval split).
|
||||
|
||||
```bash
|
||||
./scripts/local-mid-smoke.sh # Run Tier 1.5 mid-smoke (~10 min)
|
||||
./scripts/determinism-check.sh # Reproducibility self-test (runs mid-smoke twice, diffs final 5 rows)
|
||||
python3 scripts/tier1_5_verdict.py /tmp/foxhunt-mid-smoke # Behavioral kill verdict
|
||||
```
|
||||
|
||||
## Deployment (Argo Workflows)
|
||||
|
||||
```bash
|
||||
|
||||
106
Cargo.lock
generated
106
Cargo.lock
generated
@@ -399,7 +399,6 @@ dependencies = [
|
||||
"arrow-buffer 56.2.0",
|
||||
"arrow-cast 56.2.0",
|
||||
"arrow-data 56.2.0",
|
||||
"arrow-ipc 56.2.0",
|
||||
"arrow-ord 56.2.0",
|
||||
"arrow-row 56.2.0",
|
||||
"arrow-schema 56.2.0",
|
||||
@@ -1862,20 +1861,6 @@ name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
dependencies = [
|
||||
"bytemuck_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck_derive"
|
||||
version = "1.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@@ -2714,11 +2699,8 @@ dependencies = [
|
||||
name = "cudarc"
|
||||
version = "0.19.3"
|
||||
dependencies = [
|
||||
"float4",
|
||||
"float8",
|
||||
"half",
|
||||
"libloading 0.9.0",
|
||||
"no-std-compat",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3651,21 +3633,6 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
|
||||
|
||||
[[package]]
|
||||
name = "float4"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5939bac0ef2ad7c83a53e4fb889c1d81f007b07061d648cd271071984d86f257"
|
||||
|
||||
[[package]]
|
||||
name = "float8"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2d1f04709a8ac06e8e8042875a3c466cc4832d3c1a18dbcb9dba3c6e83046bc"
|
||||
dependencies = [
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
@@ -4067,31 +4034,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxt-backtest"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"ml-alpha",
|
||||
"ml-backtesting",
|
||||
"ml-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxt-data-audit"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"data",
|
||||
"ml-features",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -6015,7 +5957,6 @@ dependencies = [
|
||||
"lru",
|
||||
"memmap2",
|
||||
"mimalloc",
|
||||
"ml-alpha",
|
||||
"ml-asset-selection",
|
||||
"ml-backtesting",
|
||||
"ml-checkpoint",
|
||||
@@ -6082,33 +6023,6 @@ dependencies = [
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ml-alpha"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx",
|
||||
"arrow 56.2.0",
|
||||
"bincode",
|
||||
"clap",
|
||||
"cudarc",
|
||||
"data",
|
||||
"memmap2",
|
||||
"ml-backtesting",
|
||||
"ml-core",
|
||||
"ml-features",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ml-asset-selection"
|
||||
version = "1.0.0"
|
||||
@@ -6126,24 +6040,11 @@ name = "ml-backtesting"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrow 56.2.0",
|
||||
"arrow-array 56.2.0",
|
||||
"arrow-schema 56.2.0",
|
||||
"bytemuck",
|
||||
"chrono",
|
||||
"csv",
|
||||
"cudarc",
|
||||
"ml-alpha",
|
||||
"ml-core",
|
||||
"parquet",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6337,7 +6238,6 @@ dependencies = [
|
||||
"data",
|
||||
"dbn 0.42.0",
|
||||
"ml-core",
|
||||
"ml-labeling",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"rayon",
|
||||
@@ -6769,12 +6669,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
||||
@@ -114,7 +114,6 @@ members = [
|
||||
"crates/ml-dqn",
|
||||
"crates/ml-ppo",
|
||||
"crates/ml-supervised",
|
||||
"crates/ml-alpha",
|
||||
"crates/ml-data",
|
||||
"crates/ml-hyperopt",
|
||||
"crates/ml-features",
|
||||
@@ -146,8 +145,6 @@ members = [
|
||||
"crates/training_uploader",
|
||||
# CLI binary
|
||||
"bin/fxt",
|
||||
"bin/fxt-backtest",
|
||||
"bin/fxt-data-audit",
|
||||
# Services
|
||||
"services/backtesting_service",
|
||||
"services/broker_gateway_service",
|
||||
@@ -442,7 +439,6 @@ ml-ppo = { path = "crates/ml-ppo" }
|
||||
ml-supervised = { path = "crates/ml-supervised" }
|
||||
ml-hyperopt = { path = "crates/ml-hyperopt" }
|
||||
ml-features = { path = "crates/ml-features" }
|
||||
ml-alpha = { path = "crates/ml-alpha" }
|
||||
ml-labeling = { path = "crates/ml-labeling" }
|
||||
ml-regime = { path = "crates/ml-regime" }
|
||||
ml-regime-detection = { path = "crates/ml-regime-detection" }
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
[package]
|
||||
name = "fxt-backtest"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
publish.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
description = "Real-LOB GPU backtest CLI (run + sweep aggregator)"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Per-step kernel-state JSONL trace. Proxies through to ml-alpha's
|
||||
# `kernel-step-trace` feature so the trainer compiles in the GPU log
|
||||
# ring + drain task. Off by default (zero overhead). Enable with
|
||||
# `cargo build -p fxt-backtest --features kernel-step-trace`.
|
||||
kernel-step-trace = ["ml-alpha/kernel-step-trace"]
|
||||
|
||||
[dependencies]
|
||||
ml-backtesting = { path = "../../crates/ml-backtesting" }
|
||||
ml-alpha = { path = "../../crates/ml-alpha" }
|
||||
ml-core.workspace = true
|
||||
anyhow.workspace = true
|
||||
clap = { workspace = true, features = ["derive", "env"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,631 +0,0 @@
|
||||
//! `fxt-backtest` — Real-LOB GPU backtest CLI.
|
||||
//!
|
||||
//! Two subcommands:
|
||||
//!
|
||||
//! fxt-backtest run --data <mbp10-dir> [--policy-grid <yaml>] ...
|
||||
//! Runs the LOB backtest harness on a directory of MBP-10 dbn files.
|
||||
//! Writes per-cell artifacts under --out/cell_<NNNN>/.
|
||||
//!
|
||||
//! fxt-backtest aggregate <sweep-dir>
|
||||
//! Walks every cell directory under <sweep-dir>, parses summary.json,
|
||||
//! emits aggregate.parquet + pareto_frontier.json at the sweep-dir root.
|
||||
//!
|
||||
//! See docs/superpowers/specs/2026-05-18-real-lob-integration-design.md §7.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use ml_alpha::cfc::trunk::CfcConfig;
|
||||
use ml_alpha::heads::N_HORIZONS;
|
||||
use ml_alpha::trainer::perception::{PerceptionTrainer, PerceptionTrainerConfig};
|
||||
use ml_backtesting::aggregate::{aggregate_sweep_dir, emit_deployability_verdict};
|
||||
use ml_backtesting::harness::{BacktestHarness, BacktestHarnessConfig};
|
||||
use ml_backtesting::policy::Strategy;
|
||||
use ml_core::device::MlDevice;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "fxt-backtest", version, about = "Real-LOB GPU backtest CLI", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Cmd {
|
||||
/// Run a single backtest over an MBP-10 dataset.
|
||||
Run(RunArgs),
|
||||
/// Aggregate per-cell summary.json files in a sweep directory into
|
||||
/// aggregate.parquet + pareto_frontier.json.
|
||||
Aggregate(AggregateArgs),
|
||||
/// Run a parameter sweep: iterate over a grid of `Run` configs,
|
||||
/// writing each cell's artifacts to `<out>/<cell_name>/`, then
|
||||
/// automatically invoke `aggregate` on the resulting directory.
|
||||
/// All cells run sequentially on the same GPU (single-machine);
|
||||
/// for cluster fan-out use `argo-alpha-sweep.sh` (see infra/argo).
|
||||
Sweep(SweepArgs),
|
||||
/// Emit a deployability verdict from a completed sweep. Reads the
|
||||
/// per-cell summary.json files at the realistic (1.0 tick, 200 ms)
|
||||
/// and stress (1.5 tick, 400 ms) anchors against the supplied
|
||||
/// pre-registered threshold + walk-forward window IDs, classifies
|
||||
/// into Pass-robust / Pass-nominal / Fail-inconclusive / Fail /
|
||||
/// Fail-degenerate per spec §3.5, and writes the audit JSON.
|
||||
Verdict(VerdictArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct RunArgs {
|
||||
/// Root directory containing MBP-10 .dbn.zst files (and predecoded sidecars).
|
||||
#[arg(long)]
|
||||
data: PathBuf,
|
||||
/// Directory containing predecoded sidecars; defaults to --data.
|
||||
#[arg(long)]
|
||||
predecoded_dir: Option<PathBuf>,
|
||||
/// Number of parallel backtest cells (one CUDA block per cell).
|
||||
#[arg(long, default_value_t = 1)]
|
||||
n_parallel: usize,
|
||||
/// Optional YAML file listing per-cell Strategy compositions.
|
||||
/// If omitted, every cell gets Strategy::default_for(--max-lots).
|
||||
#[arg(long)]
|
||||
policy_grid: Option<PathBuf>,
|
||||
/// Total submission→fill latency in nanoseconds (IBKR + Scaleway
|
||||
/// baseline = 100_000_000 = 100ms). Propagated to the resting-order
|
||||
/// engine so in-flight orders wait for arrival_ts_ns before crossing.
|
||||
/// 0 = immediate-match path (legacy).
|
||||
#[arg(long, default_value_t = 100_000_000)]
|
||||
latency_ns: u32,
|
||||
/// Target annual volatility in price-unit lot-equivalents
|
||||
/// (see spec §5 IsvKellyState.target_annual_vol).
|
||||
#[arg(long, default_value_t = 50.0)]
|
||||
target_annual_vol_units: f32,
|
||||
/// Trade-rate annualisation factor — non-overlapping convention,
|
||||
/// 825 (K=6000 holding × 250 trading days) per pearl_phase1d4 default.
|
||||
#[arg(long, default_value_t = 825.0)]
|
||||
annualisation_factor: f32,
|
||||
/// Per-leaf max concurrent lots (Strategy::default_for argument).
|
||||
#[arg(long, default_value_t = 5)]
|
||||
max_lots: u16,
|
||||
/// Cap on events processed; 0 = exhaust the input stream.
|
||||
#[arg(long, default_value_t = 0)]
|
||||
max_events: u64,
|
||||
/// Cold-start Kelly fraction floor (see BacktestHarnessConfig +
|
||||
/// pearl_blend_formulas_must_have_permanent_floor). When isv_kelly
|
||||
/// has no closed-trade history, kelly_frac defaults to this floor so
|
||||
/// the first trade can fire and bootstrap state.
|
||||
#[arg(long, default_value_t = 0.20)]
|
||||
kelly_frac_floor: f32,
|
||||
/// Cold-start Sharpe weight floor — applied to recent_sharpe in the
|
||||
/// cross-horizon aggregation. Lets the aggregate produce a non-zero
|
||||
/// signed size before recent_sharpe is populated.
|
||||
#[arg(long, default_value_t = 0.10)]
|
||||
sharpe_weight_floor: f32,
|
||||
/// Random seed for trunk initialisation when --checkpoint is not given.
|
||||
#[arg(long, default_value_t = 0xC0FFEE)]
|
||||
seed: u64,
|
||||
/// Path to a CfcTrunk bincode checkpoint (see ml-alpha
|
||||
/// CfcTrunk::save_checkpoint). When set, overrides --seed and the
|
||||
/// trunk runs with trained weights. Without it the trunk is
|
||||
/// random-initialised — useful for plumbing tests, not real backtests.
|
||||
#[arg(long)]
|
||||
checkpoint: Option<PathBuf>,
|
||||
/// Output directory; per-cell artifacts written to <out>/cell_NNNN/.
|
||||
#[arg(long)]
|
||||
out: PathBuf,
|
||||
/// Enable per-step kernel-state JSONL trace to the given file.
|
||||
///
|
||||
/// Mirrors `alpha_train --kernel-step-trace`. When set, the
|
||||
/// `PerceptionTrainer` constructed by the binary allocates the GPU
|
||||
/// log ring + spawns a drain task that writes one JSONL record per
|
||||
/// kernel emission to this path. Requires the `kernel-step-trace`
|
||||
/// Cargo feature at compile time (proxied through to `ml-alpha`).
|
||||
/// When unset (default), no ring allocated, zero overhead.
|
||||
///
|
||||
/// Example: `--kernel-step-trace /feature-cache/runs/<sha>/step_trace.jsonl`
|
||||
#[arg(long)]
|
||||
kernel_step_trace: Option<PathBuf>,
|
||||
/// Per-horizon alpha_probs sampling stride for ALPHA Smoke 2
|
||||
/// diagnostic. When set to N, every Nth inference call DtoHs the 5
|
||||
/// alpha_probs and accumulates per-horizon mean/stddev/min/max + 10-bin
|
||||
/// histogram (emitted at end of CRT.diag block).
|
||||
#[arg(long)]
|
||||
per_horizon_logit_sampling_stride: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct AggregateArgs {
|
||||
/// Sweep directory containing one subdirectory per cell, each with
|
||||
/// a summary.json file.
|
||||
sweep_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct VerdictArgs {
|
||||
/// Sweep directory containing one subdirectory per cell (named
|
||||
/// `cell_<NNNN>_cost<C>_lat<L>_th<T>_<W>`), each with a summary.json.
|
||||
sweep_dir: PathBuf,
|
||||
/// Pre-registered threshold (from the threshold-tuning sweep on W0).
|
||||
/// Read from `config/ml/v2_prod_thresholds.json` in production usage.
|
||||
#[arg(long)]
|
||||
threshold: f32,
|
||||
/// Walk-forward window IDs, comma-separated. Typical:
|
||||
/// "W1,W2,W3,W4" matching the 4 held-out quarters in
|
||||
/// `config/ml/sweep_deployability.yaml`.
|
||||
#[arg(long, default_value = "W1,W2,W3,W4")]
|
||||
windows: String,
|
||||
/// Training-run commit SHA (recorded in the audit JSON).
|
||||
#[arg(long)]
|
||||
training_sha: String,
|
||||
/// Spec-revision commit SHA (recorded in the audit JSON).
|
||||
#[arg(long)]
|
||||
spec_sha: String,
|
||||
/// Output JSON path. Convention: `deployability_verdict.json` at
|
||||
/// repo root, committed as audit record.
|
||||
#[arg(long)]
|
||||
out: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct SweepArgs {
|
||||
/// Sweep grid YAML file. See SweepGrid struct in main.rs for the
|
||||
/// schema; each cell may override a subset of Run args.
|
||||
#[arg(long)]
|
||||
grid: PathBuf,
|
||||
/// Root output directory for the sweep. One subdirectory created
|
||||
/// per cell, plus aggregate.parquet + pareto_frontier.json at root.
|
||||
#[arg(long)]
|
||||
out: PathBuf,
|
||||
/// Stop the sweep after this many cells (0 = run all). Useful for
|
||||
/// smoke-testing a large grid.
|
||||
#[arg(long, default_value_t = 0)]
|
||||
max_cells: usize,
|
||||
}
|
||||
|
||||
/// Sweep grid YAML schema. The `base` field provides defaults that
|
||||
/// each `cells` entry may override on a per-field basis.
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct SweepGrid {
|
||||
base: SweepBase,
|
||||
cells: Vec<SweepCell>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, Clone)]
|
||||
struct SweepBase {
|
||||
/// Single data file path. Used by the legacy fan-out flow (one Argo
|
||||
/// task per cell). For the P6 batched flow that varies window per
|
||||
/// cell, prefer `data_template` with `{window}` interpolation.
|
||||
#[serde(default)]
|
||||
data: Option<PathBuf>,
|
||||
/// P6: data path template with `{window}` placeholder. When set,
|
||||
/// each cell's `window` field interpolates into this template to
|
||||
/// produce the per-cell data path.
|
||||
/// Example: /mnt/training-data/futures-baseline-mbp10/ES.FUT/ES.FUT_{window}.dbn.zst
|
||||
#[serde(default)]
|
||||
data_template: Option<String>,
|
||||
#[serde(default)]
|
||||
predecoded_dir: Option<PathBuf>,
|
||||
#[serde(default = "default_n_parallel")] n_parallel: usize,
|
||||
#[serde(default = "default_latency_ns")] latency_ns: u32,
|
||||
#[serde(default = "default_target_vol")] target_annual_vol_units: f32,
|
||||
#[serde(default = "default_ann_factor")] annualisation_factor: f32,
|
||||
#[serde(default = "default_max_lots")] max_lots: u16,
|
||||
#[serde(default)] max_events: u64,
|
||||
#[serde(default = "default_seed")] seed: u64,
|
||||
#[serde(default)] checkpoint: Option<PathBuf>,
|
||||
#[serde(default = "default_kelly_frac_floor")] kelly_frac_floor: f32,
|
||||
#[serde(default = "default_sharpe_weight_floor")] sharpe_weight_floor: f32,
|
||||
/// P6: list of sim variants. When non-empty, each cell runs ONE
|
||||
/// harness at n_parallel=variants.len() instead of fanning out one
|
||||
/// harness per cell. Cells become a window axis; variants become the
|
||||
/// (cost, latency, threshold) axis.
|
||||
#[serde(default)] sim_variants: Vec<SimVariant>,
|
||||
/// S1.19: instrument-aware price-range sanitization. Snapshots whose
|
||||
/// top-of-book bid or ask falls outside [min, max] are skipped.
|
||||
/// None = no lower bound (0.0 passed to kernel, which disables the check).
|
||||
#[serde(default)] min_reasonable_px: Option<f32>,
|
||||
/// None = no upper bound (f32::INFINITY passed to kernel).
|
||||
#[serde(default)] max_reasonable_px: Option<f32>,
|
||||
/// CRT.1 C1.3: no-trade band in lots. seed_inflight_limits_batched skips
|
||||
/// seeding when |target_signed − effective| < delta_floor. Default 1.0 =
|
||||
/// 1 lot (ES single-contract sizing).
|
||||
#[serde(default = "default_delta_floor")] delta_floor: f32,
|
||||
/// Optional per-step kernel-state JSONL trace path. Propagated to
|
||||
/// every cell's `PerceptionTrainerConfig.kernel_step_trace_path` +
|
||||
/// `BacktestHarnessConfig.kernel_step_trace_path`. Requires the
|
||||
/// `kernel-step-trace` Cargo feature on the binary at compile time.
|
||||
/// Sweep cells share the same trace file path — useful for single-cell
|
||||
/// diagnostic sweeps; with N>1 cells the trace will interleave records
|
||||
/// from every harness/trainer instance (per-cell isolation must be
|
||||
/// handled by the operator setting distinct paths via a richer schema).
|
||||
#[serde(default)] kernel_step_trace: Option<PathBuf>,
|
||||
/// Per-horizon alpha_probs sampling stride (ALPHA Smoke 2 diagnostic).
|
||||
/// `None` = disabled. When set, every Nth inference call DtoHs the 5
|
||||
/// alpha_probs into a mapped-pinned buffer for per-horizon stats.
|
||||
#[serde(default)] per_horizon_logit_sampling_stride: Option<u32>,
|
||||
}
|
||||
|
||||
/// P6: per-sim-variant overrides for the batched-cell sweep flow.
|
||||
/// `threshold` and `cost_per_lot_per_side` are required (no defaults —
|
||||
/// these are the spec's primary sweep axes); other fields are optional
|
||||
/// overrides on top of the SweepBase scalars.
|
||||
#[derive(Debug, serde::Deserialize, Clone)]
|
||||
struct SimVariant {
|
||||
name: String,
|
||||
threshold: f32,
|
||||
cost_per_lot_per_side: f32,
|
||||
#[serde(default)] latency_ns: Option<u32>,
|
||||
#[serde(default)] target_annual_vol_units: Option<f32>,
|
||||
#[serde(default)] annualisation_factor: Option<f32>,
|
||||
#[serde(default)] max_lots: Option<u16>,
|
||||
#[serde(default)] kelly_frac_floor: Option<f32>,
|
||||
#[serde(default)] sharpe_weight_floor: Option<f32>,
|
||||
/// Follow-up gp74n: max-hold force-close. 0 = disabled (default).
|
||||
/// Existing YAMLs omit this field; serde(default) gives 0.
|
||||
#[serde(default)] max_hold_ns: u64,
|
||||
/// CRT.1 C1.3: per-variant override for the no-trade band. None = use
|
||||
/// SweepBase.delta_floor.
|
||||
#[serde(default)] delta_floor: Option<f32>,
|
||||
}
|
||||
|
||||
fn default_n_parallel() -> usize { 1 }
|
||||
fn default_latency_ns() -> u32 { 100_000_000 }
|
||||
fn default_target_vol() -> f32 { 50.0 }
|
||||
fn default_ann_factor() -> f32 { 825.0 }
|
||||
fn default_max_lots() -> u16 { 5 }
|
||||
fn default_seed() -> u64 { 0xC0FFEE }
|
||||
// Sized so a strong-conviction signal (|p-0.5| ≥ 0.2 ⇒ sig_mag ≥ 0.4)
|
||||
// produces ≥1 lot at cold-start for max_lots=5:
|
||||
// ss = sig_mag * floor * cap_lots → 0.4 * 0.20 * 5 = 0.4 → rounds to 0
|
||||
// ss = sig_mag * floor * cap_lots → 0.5 * 0.20 * 5 = 0.5 → rounds to 1
|
||||
// Weaker signals stay at 0 lots cold-start. Once kelly state bootstraps
|
||||
// (first close), `max(floor, computed_kelly)` keeps the floor as the
|
||||
// minimum trading intensity.
|
||||
fn default_kelly_frac_floor() -> f32 { 0.20 }
|
||||
fn default_sharpe_weight_floor() -> f32 { 0.10 }
|
||||
// CRT.1 C1.3: 1.0 lot — skip seeding when |delta| < 1 lot. Appropriate for
|
||||
// ES single-contract sizing. Per-variant YAML can override.
|
||||
fn default_delta_floor() -> f32 { 1.0 }
|
||||
|
||||
#[derive(Debug, serde::Deserialize, Clone)]
|
||||
struct SweepCell {
|
||||
name: String,
|
||||
/// P6: window identifier (e.g., "2025-Q2") substituted into
|
||||
/// SweepBase.data_template. When set, the cell's data path is
|
||||
/// data_template.replace("{window}", window).
|
||||
#[serde(default)] window: Option<String>,
|
||||
#[serde(default)] latency_ns: Option<u32>,
|
||||
#[serde(default)] target_annual_vol_units: Option<f32>,
|
||||
#[serde(default)] annualisation_factor: Option<f32>,
|
||||
#[serde(default)] max_lots: Option<u16>,
|
||||
#[serde(default)] max_events: Option<u64>,
|
||||
#[serde(default)] seed: Option<u64>,
|
||||
#[serde(default)] checkpoint: Option<PathBuf>,
|
||||
#[serde(default)] kelly_frac_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<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let cli = Cli::parse();
|
||||
match cli.cmd {
|
||||
Cmd::Run(args) => run(args),
|
||||
Cmd::Aggregate(args) => {
|
||||
let out = aggregate_sweep_dir(&args.sweep_dir)?;
|
||||
println!(
|
||||
"aggregated {} cells; {} on Pareto frontier; parquet={}, frontier={}",
|
||||
out.n_cells,
|
||||
out.n_pareto,
|
||||
out.parquet_path.display(),
|
||||
out.frontier_path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Cmd::Sweep(args) => sweep(args),
|
||||
Cmd::Verdict(args) => {
|
||||
let windows: Vec<&str> = args.windows.split(',').map(|s| s.trim()).collect();
|
||||
let verdict = emit_deployability_verdict(
|
||||
&args.sweep_dir,
|
||||
args.threshold,
|
||||
&windows,
|
||||
&args.training_sha,
|
||||
&args.spec_sha,
|
||||
)
|
||||
.context("emit_deployability_verdict")?;
|
||||
let json = serde_json::to_string_pretty(&verdict)
|
||||
.context("serialize deployability_verdict")?;
|
||||
std::fs::write(&args.out, &json)
|
||||
.with_context(|| format!("write {}", args.out.display()))?;
|
||||
println!(
|
||||
"verdict: {:?} (realistic median sharpe = {:.3}, max_dd_pct = {:.3}; stress median sharpe = {:.3}, max_dd_pct = {:.3}) -> {}",
|
||||
verdict.verdict,
|
||||
verdict.realistic.median_sharpe,
|
||||
verdict.realistic.median_max_dd_pct,
|
||||
verdict.stress.median_sharpe,
|
||||
verdict.stress.median_max_dd_pct,
|
||||
args.out.display(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sweep(args: SweepArgs) -> Result<()> {
|
||||
let grid_yaml = std::fs::read_to_string(&args.grid)
|
||||
.with_context(|| format!("read grid yaml {}", args.grid.display()))?;
|
||||
let grid: SweepGrid = serde_yaml::from_str(&grid_yaml).context("parse grid yaml")?;
|
||||
let n_cells = if args.max_cells > 0 {
|
||||
args.max_cells.min(grid.cells.len())
|
||||
} else {
|
||||
grid.cells.len()
|
||||
};
|
||||
anyhow::ensure!(n_cells > 0, "grid has no cells");
|
||||
std::fs::create_dir_all(&args.out)
|
||||
.with_context(|| format!("create sweep out {}", args.out.display()))?;
|
||||
|
||||
// P6: resolve sim variants once. When non-empty, each cell runs ONE
|
||||
// harness at n_parallel=variants.len() with BatchedSimConfig::from_grid.
|
||||
let resolved_variants = if grid.base.sim_variants.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
resolve_sim_variants(&grid.base)
|
||||
};
|
||||
let batched_mode = !resolved_variants.is_empty();
|
||||
|
||||
for (i, cell) in grid.cells.iter().take(n_cells).enumerate() {
|
||||
let cell_out = args.out.join(&cell.name);
|
||||
tracing::info!(
|
||||
cell = cell.name.as_str(),
|
||||
progress = format!("{}/{}", i + 1, n_cells),
|
||||
"running sweep cell"
|
||||
);
|
||||
// Resolve per-cell data path. data_template + cell.window interpolation
|
||||
// takes precedence over scalar `data` when both are present.
|
||||
let cell_data = match (&grid.base.data_template, &cell.window) {
|
||||
(Some(tpl), Some(window)) => PathBuf::from(tpl.replace("{window}", window)),
|
||||
_ => grid.base.data.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!(
|
||||
"sweep base must set either `data` (legacy) or `data_template + cell.window`"
|
||||
))?,
|
||||
};
|
||||
if batched_mode {
|
||||
// P6 batched flow: build BatchedSimConfig from grid, run ONE
|
||||
// harness at n_parallel=variants.len() with variant_names plumbed
|
||||
// through for sim_<name>/ output dirs.
|
||||
run_batched_cell(
|
||||
&resolved_variants,
|
||||
cell,
|
||||
&grid.base,
|
||||
&cell_data,
|
||||
&cell_out,
|
||||
)?;
|
||||
tracing::info!(cell = cell.name.as_str(), "batched cell complete");
|
||||
continue;
|
||||
}
|
||||
let run_args = RunArgs {
|
||||
data: cell_data,
|
||||
predecoded_dir: grid.base.predecoded_dir.clone(),
|
||||
n_parallel: grid.base.n_parallel,
|
||||
policy_grid: None, // sweep cells use the default policy; nested
|
||||
// grid-of-grids is a separate v2 concern.
|
||||
latency_ns: cell.latency_ns.unwrap_or(grid.base.latency_ns),
|
||||
target_annual_vol_units:
|
||||
cell.target_annual_vol_units.unwrap_or(grid.base.target_annual_vol_units),
|
||||
annualisation_factor:
|
||||
cell.annualisation_factor.unwrap_or(grid.base.annualisation_factor),
|
||||
max_lots: cell.max_lots.unwrap_or(grid.base.max_lots),
|
||||
max_events: cell.max_events.unwrap_or(grid.base.max_events),
|
||||
seed: cell.seed.unwrap_or(grid.base.seed),
|
||||
checkpoint: cell.checkpoint.clone().or_else(|| grid.base.checkpoint.clone()),
|
||||
kelly_frac_floor:
|
||||
cell.kelly_frac_floor.unwrap_or(grid.base.kelly_frac_floor),
|
||||
sharpe_weight_floor:
|
||||
cell.sharpe_weight_floor.unwrap_or(grid.base.sharpe_weight_floor),
|
||||
out: cell_out.clone(),
|
||||
kernel_step_trace: grid.base.kernel_step_trace.clone(),
|
||||
per_horizon_logit_sampling_stride: grid.base.per_horizon_logit_sampling_stride,
|
||||
};
|
||||
run(run_args).with_context(|| format!("sweep cell {}", cell.name))?;
|
||||
tracing::info!(cell = cell.name.as_str(), "cell complete");
|
||||
}
|
||||
|
||||
// Auto-aggregate at the end.
|
||||
let out = aggregate_sweep_dir(&args.out)?;
|
||||
println!(
|
||||
"sweep complete: {} cells; {} on Pareto frontier; parquet={}, frontier={}",
|
||||
out.n_cells,
|
||||
out.n_pareto,
|
||||
out.parquet_path.display(),
|
||||
out.frontier_path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// P6: resolve sim_variants from SweepBase, layering per-variant overrides
|
||||
/// over base scalars. Produces a flat Vec<ResolvedSimVariant> suitable for
|
||||
/// BatchedSimConfig::from_grid.
|
||||
fn resolve_sim_variants(base: &SweepBase) -> Vec<ml_backtesting::sim::ResolvedSimVariant> {
|
||||
base.sim_variants.iter().map(|v| ml_backtesting::sim::ResolvedSimVariant {
|
||||
name: v.name.clone(),
|
||||
target_annual_vol_units: v.target_annual_vol_units.unwrap_or(base.target_annual_vol_units),
|
||||
annualisation_factor: v.annualisation_factor.unwrap_or(base.annualisation_factor),
|
||||
max_lots: v.max_lots.unwrap_or(base.max_lots),
|
||||
latency_ns: v.latency_ns.unwrap_or(base.latency_ns),
|
||||
kelly_frac_floor: v.kelly_frac_floor.unwrap_or(base.kelly_frac_floor),
|
||||
sharpe_weight_floor: v.sharpe_weight_floor.unwrap_or(base.sharpe_weight_floor),
|
||||
threshold: v.threshold,
|
||||
cost_per_lot_per_side: v.cost_per_lot_per_side,
|
||||
max_hold_ns: v.max_hold_ns,
|
||||
min_reasonable_px: base.min_reasonable_px.unwrap_or(0.0),
|
||||
max_reasonable_px: base.max_reasonable_px.unwrap_or(f32::INFINITY),
|
||||
delta_floor: v.delta_floor.unwrap_or(base.delta_floor),
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// P6: run ONE cell with n_parallel=variants.len() backtests sharing the
|
||||
/// forward pass. Each backtest gets distinct (cost, latency, threshold,
|
||||
/// ...) from its ResolvedSimVariant. Output dirs are named `sim_<name>`
|
||||
/// per spec §3.3.
|
||||
fn run_batched_cell(
|
||||
variants: &[ml_backtesting::sim::ResolvedSimVariant],
|
||||
cell: &SweepCell,
|
||||
base: &SweepBase,
|
||||
cell_data: &std::path::Path,
|
||||
cell_out: &std::path::Path,
|
||||
) -> Result<()> {
|
||||
use ml_alpha::trainer::perception::{PerceptionTrainer, PerceptionTrainerConfig};
|
||||
use ml_core::device::MlDevice;
|
||||
let n = variants.len();
|
||||
let names: Vec<String> = variants.iter().map(|v| v.name.clone()).collect();
|
||||
let batched = ml_backtesting::sim::BatchedSimConfig::from_grid(variants);
|
||||
|
||||
let dev = MlDevice::cuda(0).context("CUDA device unavailable")?;
|
||||
let seed = cell.seed.unwrap_or(base.seed);
|
||||
let trainer_cfg = PerceptionTrainerConfig {
|
||||
seed,
|
||||
n_batch: 1,
|
||||
kernel_step_trace_path: base.kernel_step_trace.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let checkpoint = cell.checkpoint.clone().or_else(|| base.checkpoint.clone());
|
||||
let trainer = if let Some(ckpt_path) = &checkpoint {
|
||||
PerceptionTrainer::from_checkpoint(&dev, &trainer_cfg, ckpt_path)
|
||||
.with_context(|| format!("load checkpoint {}", ckpt_path.display()))?
|
||||
} else {
|
||||
let mut cfg2 = trainer_cfg.clone();
|
||||
cfg2.seed = seed;
|
||||
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 {
|
||||
data_root: cell_data.to_path_buf(),
|
||||
predecoded_dir: base.predecoded_dir.clone().unwrap_or_else(|| cell_data.to_path_buf()),
|
||||
n_parallel: n,
|
||||
target_annual_vol_units: base.target_annual_vol_units, // overridden by sim_config
|
||||
annualisation_factor: base.annualisation_factor,
|
||||
max_lots: base.max_lots,
|
||||
max_events: cell.max_events.unwrap_or(base.max_events),
|
||||
latency_ns: base.latency_ns,
|
||||
kelly_frac_floor: base.kelly_frac_floor,
|
||||
sharpe_weight_floor: base.sharpe_weight_floor,
|
||||
threshold: 0.0, // overridden by sim_config
|
||||
cost_per_lot_per_side: 0.0, // overridden by sim_config
|
||||
variant_names: Some(names),
|
||||
sim_config_override: Some(batched),
|
||||
strategies: Vec::new(), // batched flow uses default policy per backtest
|
||||
kernel_step_trace_path: base.kernel_step_trace.clone(),
|
||||
per_horizon_logit_sampling_stride: base.per_horizon_logit_sampling_stride,
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(cell_out)
|
||||
.with_context(|| format!("create cell out {}", cell_out.display()))?;
|
||||
let mut harness = BacktestHarness::new(cfg, &dev, trainer).context("BacktestHarness::new (batched)")?;
|
||||
let stats = harness.run().context("BacktestHarness::run (batched)")?;
|
||||
harness.write_artifacts(cell_out).context("write_artifacts (batched)")?;
|
||||
println!(
|
||||
"batched cell {} done: {} events, {} decisions, {} variants → {}",
|
||||
cell.name, stats.events_processed, stats.decisions_taken, n, cell_out.display(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(args: RunArgs) -> Result<()> {
|
||||
// Resolve strategies — either from YAML or default_for.
|
||||
let strategies: Vec<Strategy> = if let Some(grid_path) = &args.policy_grid {
|
||||
let yaml = std::fs::read_to_string(grid_path)
|
||||
.with_context(|| format!("read policy grid {}", grid_path.display()))?;
|
||||
let parsed: Vec<Strategy> =
|
||||
serde_yaml::from_str(&yaml).context("parse policy grid yaml")?;
|
||||
anyhow::ensure!(
|
||||
parsed.len() == args.n_parallel,
|
||||
"policy grid has {} cells, --n-parallel = {}",
|
||||
parsed.len(),
|
||||
args.n_parallel
|
||||
);
|
||||
parsed
|
||||
} else {
|
||||
(0..args.n_parallel)
|
||||
.map(|_| Strategy::default_for(args.max_lots))
|
||||
.collect()
|
||||
};
|
||||
// If the user provided a policy grid YAML, strategies will be flattened
|
||||
// and uploaded to the per-backtest bytecode-program slots in the sim;
|
||||
// otherwise each cell uses the hardcoded default policy
|
||||
// (WeightedByRealizedSharpe across all 5 horizons).
|
||||
let strategy_grid = if args.policy_grid.is_some() {
|
||||
strategies
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let dev = MlDevice::cuda(0).map_err(|e| anyhow::anyhow!("MlDevice::cuda(0): {e}"))?;
|
||||
// X11 inference path: backtester drives forward via PerceptionTrainer
|
||||
// (in inference role). The trainer wraps a CfcTrunk loaded from a
|
||||
// Checkpoint file; forward kernels live on the trainer and read
|
||||
// weights from `self.trunk` (the post-X1-X9 source of truth).
|
||||
let trainer_cfg = PerceptionTrainerConfig {
|
||||
n_batch: 1,
|
||||
seq_len: 32,
|
||||
kernel_step_trace_path: args.kernel_step_trace.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let trainer = if let Some(ckpt_path) = &args.checkpoint {
|
||||
tracing::info!(checkpoint = %ckpt_path.display(), "loading PerceptionTrainer from checkpoint");
|
||||
PerceptionTrainer::from_checkpoint(&dev, &trainer_cfg, ckpt_path)
|
||||
.with_context(|| format!("load checkpoint {}", ckpt_path.display()))?
|
||||
} else {
|
||||
tracing::warn!(
|
||||
seed = args.seed,
|
||||
"no --checkpoint provided; using random-init PerceptionTrainer (backtest results are NOISE)"
|
||||
);
|
||||
let _ = CfcConfig::default; // CfcConfig used transitively via trunk_cfg
|
||||
let _ = N_HORIZONS;
|
||||
let mut cfg2 = trainer_cfg.clone();
|
||||
cfg2.seed = args.seed;
|
||||
PerceptionTrainer::new(&dev, &cfg2)
|
||||
.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 {
|
||||
data_root: args.data.clone(),
|
||||
predecoded_dir: args.predecoded_dir.clone().unwrap_or(args.data),
|
||||
n_parallel: args.n_parallel,
|
||||
target_annual_vol_units: args.target_annual_vol_units,
|
||||
annualisation_factor: args.annualisation_factor,
|
||||
max_lots: args.max_lots,
|
||||
max_events: args.max_events,
|
||||
latency_ns: args.latency_ns,
|
||||
kelly_frac_floor: args.kelly_frac_floor,
|
||||
sharpe_weight_floor: args.sharpe_weight_floor,
|
||||
threshold: 0.0, // P4: default = gate disabled
|
||||
cost_per_lot_per_side: 0.0, // P4: default = frictionless
|
||||
variant_names: None, // P6: legacy single-cell flow uses cell_NNNN naming
|
||||
sim_config_override: None, // P6: legacy single-cell flow uses from_uniform
|
||||
strategies: strategy_grid,
|
||||
kernel_step_trace_path: args.kernel_step_trace.clone(),
|
||||
per_horizon_logit_sampling_stride: args.per_horizon_logit_sampling_stride,
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&args.out)
|
||||
.with_context(|| format!("create out dir {}", args.out.display()))?;
|
||||
|
||||
let mut harness = BacktestHarness::new(cfg, &dev, trainer).context("BacktestHarness::new")?;
|
||||
let stats = harness.run().context("BacktestHarness::run")?;
|
||||
harness.write_artifacts(&args.out).context("write_artifacts")?;
|
||||
|
||||
println!(
|
||||
"done: {} events processed, {} decisions taken; artifacts under {}",
|
||||
stats.events_processed,
|
||||
stats.decisions_taken,
|
||||
args.out.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "fxt-data-audit"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
publish.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
description = "Diagnostic audit of predecoded MBP-10 sidecar files — symbol distribution, price outliers, schema oddities."
|
||||
|
||||
[dependencies]
|
||||
ml-features = { path = "../../crates/ml-features" }
|
||||
data = { path = "../../crates/data" }
|
||||
anyhow.workspace = true
|
||||
@@ -1,157 +0,0 @@
|
||||
//! Diagnostic audit of predecoded MBP-10 sidecar files.
|
||||
//!
|
||||
//! Usage: `fxt-data-audit <source.dbn.zst> <predecoded_dir>`
|
||||
//!
|
||||
//! Loads via `ml_features::predecoded::load_or_predecode_mbp10` and reports:
|
||||
//! * Total snapshots in the file.
|
||||
//! * Symbol distribution (count by `Mbp10Snapshot.symbol`).
|
||||
//! * Per-symbol price stats: min/max/p1/p50/p99 of `bid_px[0]` and `ask_px[0]`
|
||||
//! (raw i64 from the parser; conversion to f64 = /1e9 nanoprice).
|
||||
//! * Outlier counts: bid_px[0]=0, bid_px[0]>1e14 (=100k$ price after /1e9),
|
||||
//! ask_px[0]=0, ask_px[0]>1e14.
|
||||
//! * "Sized but un-priced" anomalies: bid_sz[0]>0 AND bid_px[0]=0 (and ask).
|
||||
//!
|
||||
//! Run on the cluster with the training-data + feature-cache PVCs mounted.
|
||||
|
||||
use anyhow::Result;
|
||||
use data::providers::databento::{dbn_parser::InstrumentFilter, mbp10::BidAskPair};
|
||||
use ml_features::predecoded::load_or_predecode_mbp10;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn percentile(sorted: &[i64], p: f64) -> i64 {
|
||||
if sorted.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let idx = ((sorted.len() as f64) * p).clamp(0.0, sorted.len() as f64 - 1.0) as usize;
|
||||
sorted[idx]
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() < 3 {
|
||||
eprintln!("usage: {} <source.dbn.zst> <predecoded_dir>", args[0]);
|
||||
std::process::exit(2);
|
||||
}
|
||||
let source = PathBuf::from(&args[1]);
|
||||
let predecoded_dir = PathBuf::from(&args[2]);
|
||||
|
||||
eprintln!("loading: {} (predecoded_dir={})", source.display(), predecoded_dir.display());
|
||||
// The audit tool inspects raw multi-instrument sidecars — we want to see
|
||||
// every instrument in the file, so the filter is `All` by design.
|
||||
let snapshots = load_or_predecode_mbp10(&source, &predecoded_dir, InstrumentFilter::All)
|
||||
.map_err(|e| anyhow::anyhow!("load_or_predecode_mbp10: {e}"))?;
|
||||
eprintln!("loaded: {} snapshots", snapshots.len());
|
||||
|
||||
// Symbol distribution.
|
||||
let mut symbol_counts: BTreeMap<String, usize> = BTreeMap::new();
|
||||
for s in &snapshots {
|
||||
*symbol_counts.entry(s.symbol.clone()).or_default() += 1;
|
||||
}
|
||||
println!("=== symbol distribution ===");
|
||||
for (sym, n) in &symbol_counts {
|
||||
println!(" {:>20} : {}", sym, n);
|
||||
}
|
||||
|
||||
// Per-symbol price stats + outliers.
|
||||
println!();
|
||||
println!("=== per-symbol price stats (raw i64, /1e9 nanoprice = display $) ===");
|
||||
println!(
|
||||
"{:>12} | {:>10} | {:>14} {:>14} {:>14} {:>14} {:>14} | {:>14} {:>14} {:>14} {:>14} {:>14}",
|
||||
"symbol", "n", "bid_min", "bid_p1", "bid_p50", "bid_p99", "bid_max",
|
||||
"ask_min", "ask_p1", "ask_p50", "ask_p99", "ask_max"
|
||||
);
|
||||
let mut total_zero_bid = 0usize;
|
||||
let mut total_zero_ask = 0usize;
|
||||
let mut total_huge_bid = 0usize;
|
||||
let mut total_huge_ask = 0usize;
|
||||
let mut total_zero_bid_sized = 0usize;
|
||||
let mut total_zero_ask_sized = 0usize;
|
||||
let huge_threshold: i64 = 100_000_000_000_000; // 1e14 raw → /1e9 = $100k. ES never at $100k.
|
||||
|
||||
for (sym, _) in &symbol_counts {
|
||||
let mut bid_px: Vec<i64> = Vec::new();
|
||||
let mut ask_px: Vec<i64> = Vec::new();
|
||||
let mut zero_bid = 0usize;
|
||||
let mut zero_ask = 0usize;
|
||||
let mut huge_bid = 0usize;
|
||||
let mut huge_ask = 0usize;
|
||||
let mut zero_bid_sized = 0usize;
|
||||
let mut zero_ask_sized = 0usize;
|
||||
for s in &snapshots {
|
||||
if &s.symbol != sym {
|
||||
continue;
|
||||
}
|
||||
if let Some(l0) = s.levels.first() {
|
||||
if l0.bid_px == 0 {
|
||||
zero_bid += 1;
|
||||
if l0.bid_sz > 0 {
|
||||
zero_bid_sized += 1;
|
||||
}
|
||||
} else if l0.bid_px > huge_threshold {
|
||||
huge_bid += 1;
|
||||
}
|
||||
if l0.ask_px == 0 {
|
||||
zero_ask += 1;
|
||||
if l0.ask_sz > 0 {
|
||||
zero_ask_sized += 1;
|
||||
}
|
||||
} else if l0.ask_px > huge_threshold {
|
||||
huge_ask += 1;
|
||||
}
|
||||
bid_px.push(l0.bid_px);
|
||||
ask_px.push(l0.ask_px);
|
||||
}
|
||||
}
|
||||
bid_px.sort_unstable();
|
||||
ask_px.sort_unstable();
|
||||
let bid_min = *bid_px.first().unwrap_or(&0);
|
||||
let bid_max = *bid_px.last().unwrap_or(&0);
|
||||
let ask_min = *ask_px.first().unwrap_or(&0);
|
||||
let ask_max = *ask_px.last().unwrap_or(&0);
|
||||
println!(
|
||||
"{:>12} | {:>10} | {:>14} {:>14} {:>14} {:>14} {:>14} | {:>14} {:>14} {:>14} {:>14} {:>14}",
|
||||
sym, bid_px.len(),
|
||||
bid_min, percentile(&bid_px, 0.01), percentile(&bid_px, 0.50),
|
||||
percentile(&bid_px, 0.99), bid_max,
|
||||
ask_min, percentile(&ask_px, 0.01), percentile(&ask_px, 0.50),
|
||||
percentile(&ask_px, 0.99), ask_max
|
||||
);
|
||||
total_zero_bid += zero_bid;
|
||||
total_zero_ask += zero_ask;
|
||||
total_huge_bid += huge_bid;
|
||||
total_huge_ask += huge_ask;
|
||||
total_zero_bid_sized += zero_bid_sized;
|
||||
total_zero_ask_sized += zero_ask_sized;
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("=== outlier counts (across all symbols) ===");
|
||||
println!(" bid_px[0]=0 : {}", total_zero_bid);
|
||||
println!(" bid_px[0]=0 AND bid_sz[0]>0 : {} (Bug C-b source)", total_zero_bid_sized);
|
||||
println!(" bid_px[0]>1e14 (>$100k) : {}", total_huge_bid);
|
||||
println!(" ask_px[0]=0 : {}", total_zero_ask);
|
||||
println!(" ask_px[0]=0 AND ask_sz[0]>0 : {} (Bug C-b source)", total_zero_ask_sized);
|
||||
println!(" ask_px[0]>1e14 (>$100k) : {}", total_huge_ask);
|
||||
|
||||
// Spot-check the price scale for the first record of the largest-symbol group.
|
||||
println!();
|
||||
println!("=== sanity check: first record per symbol (raw → /1e9 display) ===");
|
||||
let mut seen: BTreeMap<String, bool> = BTreeMap::new();
|
||||
for s in &snapshots {
|
||||
if seen.contains_key(&s.symbol) {
|
||||
continue;
|
||||
}
|
||||
if let Some(l0) = s.levels.first() {
|
||||
let bid_f = BidAskPair::price_to_f64(l0.bid_px);
|
||||
let ask_f = BidAskPair::price_to_f64(l0.ask_px);
|
||||
println!(
|
||||
" {:>20} ts={} bid_px={} (={:.4}) bid_sz={} ask_px={} (={:.4}) ask_sz={}",
|
||||
s.symbol, s.timestamp, l0.bid_px, bid_f, l0.bid_sz, l0.ask_px, ask_f, l0.ask_sz
|
||||
);
|
||||
}
|
||||
seen.insert(s.symbol.clone(), true);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -215,11 +215,10 @@ fn test_tune_status_invalid_uuid() {
|
||||
#[ignore = "Ignored because it tries to launch terminal UI"]
|
||||
fn test_dashboard_command() {
|
||||
let mut cmd = Command::cargo_bin("fxt").unwrap();
|
||||
let _ = cmd.arg("dashboard")
|
||||
cmd.arg("dashboard")
|
||||
.timeout(std::time::Duration::from_secs(2))
|
||||
.assert();
|
||||
// Will timeout or fail trying to connect, but that's expected — assert result
|
||||
// intentionally discarded; this test verifies arg parsing reaches launch.
|
||||
// Will timeout or fail trying to connect, but that's expected
|
||||
}
|
||||
|
||||
/// Test version flag
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"regime_vol_ref_floor": 9.999999960041972e-13
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
{
|
||||
"bins": [
|
||||
{
|
||||
"avg_n_trades": 451.74334716796875,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -10.72819995880127,
|
||||
"n_episodes": 300,
|
||||
"p05": -26.052268981933594,
|
||||
"p50": -10.36817741394043,
|
||||
"p95": 4.74384880065918,
|
||||
"sharpe_annualised": -28.334871292114258,
|
||||
"sharpe_per_episode": -0.9901005029678345,
|
||||
"std_reward": 10.835465431213379,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.11666666716337204
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 451.5833435058594,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -18.23717498779297,
|
||||
"n_episodes": 300,
|
||||
"p05": -32.81657409667969,
|
||||
"p50": -17.798627853393555,
|
||||
"p95": -5.656877517700195,
|
||||
"sharpe_annualised": -55.0507698059082,
|
||||
"sharpe_per_episode": -1.9236295223236084,
|
||||
"std_reward": 9.480607032775879,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.02666666731238365
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 455.510009765625,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -26.240476608276367,
|
||||
"n_episodes": 300,
|
||||
"p05": -41.32231903076172,
|
||||
"p50": -26.38330078125,
|
||||
"p95": -12.855978965759277,
|
||||
"sharpe_annualised": -86.34113311767578,
|
||||
"sharpe_per_episode": -3.017003297805786,
|
||||
"std_reward": 8.697529792785645,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 454.6600036621094,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -41.06214141845703,
|
||||
"n_episodes": 300,
|
||||
"p05": -57.790008544921875,
|
||||
"p50": -40.88361740112305,
|
||||
"p95": -20.842697143554688,
|
||||
"sharpe_annualised": -110.67660522460938,
|
||||
"sharpe_per_episode": -3.8673534393310547,
|
||||
"std_reward": 10.617633819580078,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 459.260009765625,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -74.15150451660156,
|
||||
"n_episodes": 300,
|
||||
"p05": -106.13854217529297,
|
||||
"p50": -72.46161651611328,
|
||||
"p95": -47.33892822265625,
|
||||
"sharpe_annualised": -116.2009048461914,
|
||||
"sharpe_per_episode": -4.060388088226318,
|
||||
"std_reward": 18.26217269897461,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 370.260009765625,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -8.213351249694824,
|
||||
"n_episodes": 300,
|
||||
"p05": -22.143348693847656,
|
||||
"p50": -7.91927433013916,
|
||||
"p95": 5.754604339599609,
|
||||
"sharpe_annualised": -27.00295639038086,
|
||||
"sharpe_per_episode": -0.9435596466064453,
|
||||
"std_reward": 8.704645156860352,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.09666666388511658
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 370.75665283203125,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -15.160398483276367,
|
||||
"n_episodes": 300,
|
||||
"p05": -32.71171569824219,
|
||||
"p50": -14.214750289916992,
|
||||
"p95": -0.866797685623169,
|
||||
"sharpe_annualised": -37.087486267089844,
|
||||
"sharpe_per_episode": -1.2959415912628174,
|
||||
"std_reward": 11.698366165161133,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.046666666865348816
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 369.0333251953125,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -20.403078079223633,
|
||||
"n_episodes": 300,
|
||||
"p05": -34.1339111328125,
|
||||
"p50": -20.087478637695312,
|
||||
"p95": -9.10377311706543,
|
||||
"sharpe_annualised": -75.91487884521484,
|
||||
"sharpe_per_episode": -2.6526806354522705,
|
||||
"std_reward": 7.691493988037109,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.006666666828095913
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 371.84332275390625,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -33.83988571166992,
|
||||
"n_episodes": 300,
|
||||
"p05": -53.0679931640625,
|
||||
"p50": -32.836673736572266,
|
||||
"p95": -19.397377014160156,
|
||||
"sharpe_annualised": -96.48247528076172,
|
||||
"sharpe_per_episode": -3.371370553970337,
|
||||
"std_reward": 10.037426948547363,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 373.9566650390625,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -60.00151824951172,
|
||||
"n_episodes": 300,
|
||||
"p05": -81.8888931274414,
|
||||
"p50": -59.34823226928711,
|
||||
"p95": -42.3861198425293,
|
||||
"sharpe_annualised": -136.0745849609375,
|
||||
"sharpe_per_episode": -4.754830837249756,
|
||||
"std_reward": 12.619065284729004,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 286.7366638183594,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -6.028912544250488,
|
||||
"n_episodes": 300,
|
||||
"p05": -17.442401885986328,
|
||||
"p50": -5.043452262878418,
|
||||
"p95": 3.8487255573272705,
|
||||
"sharpe_annualised": -21.08204460144043,
|
||||
"sharpe_per_episode": -0.7366662621498108,
|
||||
"std_reward": 8.184048652648926,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.1433333307504654
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 297.32000732421875,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -11.952619552612305,
|
||||
"n_episodes": 300,
|
||||
"p05": -27.852296829223633,
|
||||
"p50": -9.96550178527832,
|
||||
"p95": -0.7230279445648193,
|
||||
"sharpe_annualised": -34.2513427734375,
|
||||
"sharpe_per_episode": -1.1968387365341187,
|
||||
"std_reward": 9.986825942993164,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.05000000074505806
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 285.8933410644531,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -16.28207778930664,
|
||||
"n_episodes": 300,
|
||||
"p05": -35.10637283325195,
|
||||
"p50": -14.313798904418945,
|
||||
"p95": -4.262900352478027,
|
||||
"sharpe_annualised": -44.82547378540039,
|
||||
"sharpe_per_episode": -1.566328763961792,
|
||||
"std_reward": 10.395057678222656,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.02666666731238365
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 295.2900085449219,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -25.938884735107422,
|
||||
"n_episodes": 300,
|
||||
"p05": -44.77133560180664,
|
||||
"p50": -24.313446044921875,
|
||||
"p95": -9.273771286010742,
|
||||
"sharpe_annualised": -62.883323669433594,
|
||||
"sharpe_per_episode": -2.1973211765289307,
|
||||
"std_reward": 11.804777145385742,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.006666666828095913
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 280.7466735839844,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -44.50050354003906,
|
||||
"n_episodes": 300,
|
||||
"p05": -66.49164581298828,
|
||||
"p50": -42.04655075073242,
|
||||
"p95": -26.325918197631836,
|
||||
"sharpe_annualised": -99.81108856201172,
|
||||
"sharpe_per_episode": -3.4876816272735596,
|
||||
"std_reward": 12.7593355178833,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 223.94000244140625,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -5.630343914031982,
|
||||
"n_episodes": 300,
|
||||
"p05": -23.06067657470703,
|
||||
"p50": -3.5636019706726074,
|
||||
"p95": 5.983572483062744,
|
||||
"sharpe_annualised": -17.714860916137695,
|
||||
"sharpe_per_episode": -0.6190073490142822,
|
||||
"std_reward": 9.095762252807617,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.1733333319425583
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 224.2066650390625,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -7.762266159057617,
|
||||
"n_episodes": 300,
|
||||
"p05": -23.935901641845703,
|
||||
"p50": -5.957376480102539,
|
||||
"p95": 3.385673999786377,
|
||||
"sharpe_annualised": -26.093162536621094,
|
||||
"sharpe_per_episode": -0.911768913269043,
|
||||
"std_reward": 8.51341438293457,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.09000000357627869
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 227.23333740234375,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -13.033651351928711,
|
||||
"n_episodes": 300,
|
||||
"p05": -32.822052001953125,
|
||||
"p50": -10.511049270629883,
|
||||
"p95": -2.25492525100708,
|
||||
"sharpe_annualised": -38.49271011352539,
|
||||
"sharpe_per_episode": -1.3450441360473633,
|
||||
"std_reward": 9.690128326416016,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.009999999776482582
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 233.4933319091797,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -21.03263282775879,
|
||||
"n_episodes": 300,
|
||||
"p05": -40.14529800415039,
|
||||
"p50": -19.439125061035156,
|
||||
"p95": -8.333877563476562,
|
||||
"sharpe_annualised": -57.79651641845703,
|
||||
"sharpe_per_episode": -2.01957368850708,
|
||||
"std_reward": 10.41439151763916,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.006666666828095913
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 225.48666381835938,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -33.60525131225586,
|
||||
"n_episodes": 300,
|
||||
"p05": -55.04161834716797,
|
||||
"p50": -31.479198455810547,
|
||||
"p95": -15.453825950622559,
|
||||
"sharpe_annualised": -68.2383041381836,
|
||||
"sharpe_per_episode": -2.38443922996521,
|
||||
"std_reward": 14.093565940856934,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 159.84666442871094,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -4.220123291015625,
|
||||
"n_episodes": 300,
|
||||
"p05": -20.078947067260742,
|
||||
"p50": -2.5951507091522217,
|
||||
"p95": 3.9630990028381348,
|
||||
"sharpe_annualised": -17.054563522338867,
|
||||
"sharpe_per_episode": -0.5959346890449524,
|
||||
"std_reward": 7.081520080566406,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.18000000715255737
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 167.7866668701172,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -6.742062091827393,
|
||||
"n_episodes": 300,
|
||||
"p05": -24.98937225341797,
|
||||
"p50": -4.563099384307861,
|
||||
"p95": 2.2458012104034424,
|
||||
"sharpe_annualised": -22.914066314697266,
|
||||
"sharpe_per_episode": -0.8006822466850281,
|
||||
"std_reward": 8.42039680480957,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.12999999523162842
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 165.1233367919922,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -9.334568977355957,
|
||||
"n_episodes": 300,
|
||||
"p05": -27.746673583984375,
|
||||
"p50": -7.128000259399414,
|
||||
"p95": -0.07927465438842773,
|
||||
"sharpe_annualised": -31.278589248657227,
|
||||
"sharpe_per_episode": -1.092962384223938,
|
||||
"std_reward": 8.540613174438477,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.046666666865348816
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 173.01666259765625,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -15.824145317077637,
|
||||
"n_episodes": 300,
|
||||
"p05": -35.53749084472656,
|
||||
"p50": -13.105096817016602,
|
||||
"p95": -4.606574535369873,
|
||||
"sharpe_annualised": -47.958587646484375,
|
||||
"sharpe_per_episode": -1.6758086681365967,
|
||||
"std_reward": 9.442691802978516,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 164.22666931152344,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -24.750625610351562,
|
||||
"n_episodes": 300,
|
||||
"p05": -46.327919006347656,
|
||||
"p50": -22.30762481689453,
|
||||
"p95": -10.495248794555664,
|
||||
"sharpe_annualised": -64.83598327636719,
|
||||
"sharpe_per_episode": -2.265552520751953,
|
||||
"std_reward": 10.924762725830078,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.0033333334140479565
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 136.1999969482422,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -5.492737293243408,
|
||||
"n_episodes": 300,
|
||||
"p05": -20.194347381591797,
|
||||
"p50": -2.940850019454956,
|
||||
"p95": 2.9858238697052,
|
||||
"sharpe_annualised": -19.53951644897461,
|
||||
"sharpe_per_episode": -0.6827659606933594,
|
||||
"std_reward": 8.044831275939941,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.20666666328907013
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 135.9566650390625,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -7.7528533935546875,
|
||||
"n_episodes": 300,
|
||||
"p05": -23.993549346923828,
|
||||
"p50": -5.217850685119629,
|
||||
"p95": 0.9867243766784668,
|
||||
"sharpe_annualised": -26.615497589111328,
|
||||
"sharpe_per_episode": -0.9300207495689392,
|
||||
"std_reward": 8.336215019226074,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.09000000357627869
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 131.84666442871094,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -9.753680229187012,
|
||||
"n_episodes": 300,
|
||||
"p05": -28.568151473999023,
|
||||
"p50": -7.167825222015381,
|
||||
"p95": -0.901602029800415,
|
||||
"sharpe_annualised": -31.260038375854492,
|
||||
"sharpe_per_episode": -1.0923141241073608,
|
||||
"std_reward": 8.929372787475586,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.02666666731238365
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 135.81333923339844,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -13.736656188964844,
|
||||
"n_episodes": 300,
|
||||
"p05": -34.117652893066406,
|
||||
"p50": -10.9698486328125,
|
||||
"p95": -1.744225263595581,
|
||||
"sharpe_annualised": -36.68037033081055,
|
||||
"sharpe_per_episode": -1.2817158699035645,
|
||||
"std_reward": 10.717395782470703,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.013333333656191826
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 136.97999572753906,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -22.093671798706055,
|
||||
"n_episodes": 300,
|
||||
"p05": -45.44099807739258,
|
||||
"p50": -19.13762664794922,
|
||||
"p95": -7.422874450683594,
|
||||
"sharpe_annualised": -53.82737350463867,
|
||||
"sharpe_per_episode": -1.88088059425354,
|
||||
"std_reward": 11.746451377868652,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.0
|
||||
}
|
||||
],
|
||||
"cost_grid": [
|
||||
0.0,
|
||||
0.0625,
|
||||
0.125,
|
||||
0.25,
|
||||
0.5
|
||||
],
|
||||
"horizon": 600,
|
||||
"n_eval_episodes": 300,
|
||||
"n_train_episodes": 1000,
|
||||
"phase": "E.3 Task 23 (2D sweep)",
|
||||
"threshold_grid": [
|
||||
0.0,
|
||||
0.05000000074505806,
|
||||
0.10000000149011612,
|
||||
0.15000000596046448,
|
||||
0.20000000298023224,
|
||||
0.25
|
||||
],
|
||||
"train_cost": 0.0625,
|
||||
"train_frac": 0.800000011920929
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
{
|
||||
"bins": [
|
||||
{
|
||||
"avg_n_trades": 505.7820129394531,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -7.376502513885498,
|
||||
"n_episodes": 500,
|
||||
"p05": -15.246373176574707,
|
||||
"p50": -9.201272964477539,
|
||||
"p95": 6.196549415588379,
|
||||
"sharpe_annualised": -31.529918670654297,
|
||||
"sharpe_per_episode": -1.101744532585144,
|
||||
"std_reward": 6.695292949676514,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.1420000046491623
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 506.8420104980469,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -15.909566879272461,
|
||||
"n_episodes": 500,
|
||||
"p05": -23.928251266479492,
|
||||
"p50": -17.78717803955078,
|
||||
"p95": -3.438425302505493,
|
||||
"sharpe_annualised": -68.6070327758789,
|
||||
"sharpe_per_episode": -2.3973236083984375,
|
||||
"std_reward": 6.636386871337891,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.029999999329447746
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 505.7179870605469,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -23.42654800415039,
|
||||
"n_episodes": 500,
|
||||
"p05": -32.949134826660156,
|
||||
"p50": -24.983301162719727,
|
||||
"p95": -9.647102355957031,
|
||||
"sharpe_annualised": -91.52935028076172,
|
||||
"sharpe_per_episode": -3.1982944011688232,
|
||||
"std_reward": 7.324700355529785,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0020000000949949026
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 507.2300109863281,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -40.23649215698242,
|
||||
"n_episodes": 500,
|
||||
"p05": -50.727237701416016,
|
||||
"p50": -41.87934875488281,
|
||||
"p95": -25.59070587158203,
|
||||
"sharpe_annualised": -140.9852752685547,
|
||||
"sharpe_per_episode": -4.926424026489258,
|
||||
"std_reward": 8.167484283447266,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 506.33599853515625,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -72.27515411376953,
|
||||
"n_episodes": 500,
|
||||
"p05": -89.18425750732422,
|
||||
"p50": -72.6242904663086,
|
||||
"p95": -53.71200180053711,
|
||||
"sharpe_annualised": -197.0381622314453,
|
||||
"sharpe_per_episode": -6.88507080078125,
|
||||
"std_reward": 10.4973726272583,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 411.7900085449219,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -5.731086730957031,
|
||||
"n_episodes": 500,
|
||||
"p05": -13.217626571655273,
|
||||
"p50": -6.432249069213867,
|
||||
"p95": 4.667325973510742,
|
||||
"sharpe_annualised": -28.958410263061523,
|
||||
"sharpe_per_episode": -1.0118887424468994,
|
||||
"std_reward": 5.663751602172852,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.1459999978542328
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 414.65399169921875,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -12.296024322509766,
|
||||
"n_episodes": 500,
|
||||
"p05": -22.073755264282227,
|
||||
"p50": -12.375904083251953,
|
||||
"p95": -2.1291003227233887,
|
||||
"sharpe_annualised": -56.619964599609375,
|
||||
"sharpe_per_episode": -1.978461742401123,
|
||||
"std_reward": 6.214941501617432,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.03999999910593033
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 413.5060119628906,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -19.207530975341797,
|
||||
"n_episodes": 500,
|
||||
"p05": -31.44267463684082,
|
||||
"p50": -19.380374908447266,
|
||||
"p95": -7.792024612426758,
|
||||
"sharpe_annualised": -76.79408264160156,
|
||||
"sharpe_per_episode": -2.6834022998809814,
|
||||
"std_reward": 7.157902240753174,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.00800000037997961
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 411.93798828125,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -32.429752349853516,
|
||||
"n_episodes": 500,
|
||||
"p05": -48.73774337768555,
|
||||
"p50": -32.32264709472656,
|
||||
"p95": -16.66182518005371,
|
||||
"sharpe_annualised": -97.9114761352539,
|
||||
"sharpe_per_episode": -3.4213037490844727,
|
||||
"std_reward": 9.47877025604248,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 415.7279968261719,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -60.21247482299805,
|
||||
"n_episodes": 500,
|
||||
"p05": -83.98477935791016,
|
||||
"p50": -63.01105499267578,
|
||||
"p95": -33.47249984741211,
|
||||
"sharpe_annualised": -111.4959716796875,
|
||||
"sharpe_per_episode": -3.895984649658203,
|
||||
"std_reward": 15.455008506774902,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 307.6679992675781,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -2.764266014099121,
|
||||
"n_episodes": 500,
|
||||
"p05": -11.989750862121582,
|
||||
"p50": -2.776402473449707,
|
||||
"p95": 7.969524383544922,
|
||||
"sharpe_annualised": -13.065699577331543,
|
||||
"sharpe_per_episode": -0.4565524756908417,
|
||||
"std_reward": 6.054651260375977,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.257999986410141
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 313.0580139160156,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -8.023506164550781,
|
||||
"n_episodes": 500,
|
||||
"p05": -20.223373413085938,
|
||||
"p50": -7.3301496505737305,
|
||||
"p95": 2.120025396347046,
|
||||
"sharpe_annualised": -33.08244323730469,
|
||||
"sharpe_per_episode": -1.1559940576553345,
|
||||
"std_reward": 6.940784931182861,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.09000000357627869
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 312.7879943847656,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -13.200392723083496,
|
||||
"n_episodes": 500,
|
||||
"p05": -27.572002410888672,
|
||||
"p50": -11.809425354003906,
|
||||
"p95": -1.6250247955322266,
|
||||
"sharpe_annualised": -45.03028106689453,
|
||||
"sharpe_per_episode": -1.5734853744506836,
|
||||
"std_reward": 8.389269828796387,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.03799999877810478
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 309.92401123046875,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -23.74254035949707,
|
||||
"n_episodes": 500,
|
||||
"p05": -43.5518913269043,
|
||||
"p50": -22.597496032714844,
|
||||
"p95": -7.84235143661499,
|
||||
"sharpe_annualised": -57.187339782714844,
|
||||
"sharpe_per_episode": -1.998287320137024,
|
||||
"std_reward": 11.881443977355957,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.004000000189989805
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 317.7900085449219,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -44.596744537353516,
|
||||
"n_episodes": 500,
|
||||
"p05": -77.12137603759766,
|
||||
"p50": -44.13597869873047,
|
||||
"p95": -14.920825004577637,
|
||||
"sharpe_annualised": -60.16629409790039,
|
||||
"sharpe_per_episode": -2.1023805141448975,
|
||||
"std_reward": 21.212499618530273,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 214.0800018310547,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -0.7483880519866943,
|
||||
"n_episodes": 500,
|
||||
"p05": -8.611875534057617,
|
||||
"p50": -0.9981997013092041,
|
||||
"p95": 7.168798923492432,
|
||||
"sharpe_annualised": -4.30732536315918,
|
||||
"sharpe_per_episode": -0.15051013231277466,
|
||||
"std_reward": 4.972343444824219,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.3479999899864197
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 220.29600524902344,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -4.5742506980896,
|
||||
"n_episodes": 500,
|
||||
"p05": -15.124225616455078,
|
||||
"p50": -3.609375,
|
||||
"p95": 5.420872688293457,
|
||||
"sharpe_annualised": -22.15690803527832,
|
||||
"sharpe_per_episode": -0.7742250561714172,
|
||||
"std_reward": 5.908166408538818,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.16200000047683716
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 206.593994140625,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -7.650266170501709,
|
||||
"n_episodes": 500,
|
||||
"p05": -20.91200065612793,
|
||||
"p50": -5.522923946380615,
|
||||
"p95": 0.7698264122009277,
|
||||
"sharpe_annualised": -31.48302459716797,
|
||||
"sharpe_per_episode": -1.100105881690979,
|
||||
"std_reward": 6.954117774963379,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.08399999886751175
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 207.1020050048828,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -14.568126678466797,
|
||||
"n_episodes": 500,
|
||||
"p05": -31.57849884033203,
|
||||
"p50": -11.627604484558105,
|
||||
"p95": -3.4181251525878906,
|
||||
"sharpe_annualised": -42.31534957885742,
|
||||
"sharpe_per_episode": -1.4786180257797241,
|
||||
"std_reward": 9.852529525756836,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.014000000432133675
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 212.37600708007812,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -29.32847023010254,
|
||||
"n_episodes": 500,
|
||||
"p05": -61.26882553100586,
|
||||
"p50": -24.778127670288086,
|
||||
"p95": -7.954624652862549,
|
||||
"sharpe_annualised": -46.66537857055664,
|
||||
"sharpe_per_episode": -1.6306202411651611,
|
||||
"std_reward": 17.986082077026367,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 133.64199829101562,
|
||||
"cost": 0.0,
|
||||
"mean_reward": 0.8386048078536987,
|
||||
"n_episodes": 500,
|
||||
"p05": -4.833250522613525,
|
||||
"p50": -0.04905200004577637,
|
||||
"p95": 9.4207763671875,
|
||||
"sharpe_annualised": 5.863218307495117,
|
||||
"sharpe_per_episode": 0.20487743616104126,
|
||||
"std_reward": 4.093202114105225,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.492000013589859
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 130.7899932861328,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -1.5558525323867798,
|
||||
"n_episodes": 500,
|
||||
"p05": -8.187274932861328,
|
||||
"p50": -1.694624423980713,
|
||||
"p95": 5.250924110412598,
|
||||
"sharpe_annualised": -10.904839515686035,
|
||||
"sharpe_per_episode": -0.381045937538147,
|
||||
"std_reward": 4.083110332489014,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.2840000092983246
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 132.32000732421875,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -3.9048287868499756,
|
||||
"n_episodes": 500,
|
||||
"p05": -11.645946502685547,
|
||||
"p50": -3.379124402999878,
|
||||
"p95": 2.8128762245178223,
|
||||
"sharpe_annualised": -24.98102569580078,
|
||||
"sharpe_per_episode": -0.8729076981544495,
|
||||
"std_reward": 4.473358154296875,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.15600000321865082
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 131.50799560546875,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -7.8982110023498535,
|
||||
"n_episodes": 500,
|
||||
"p05": -18.36737632751465,
|
||||
"p50": -6.697125434875488,
|
||||
"p95": 0.27234911918640137,
|
||||
"sharpe_annualised": -38.383419036865234,
|
||||
"sharpe_per_episode": -1.341225266456604,
|
||||
"std_reward": 5.888802528381348,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.05999999865889549
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 128.50399780273438,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -17.548030853271484,
|
||||
"n_episodes": 500,
|
||||
"p05": -35.324703216552734,
|
||||
"p50": -15.349874496459961,
|
||||
"p95": -5.963876247406006,
|
||||
"sharpe_annualised": -55.831520080566406,
|
||||
"sharpe_per_episode": -1.9509111642837524,
|
||||
"std_reward": 8.994787216186523,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.0020000000949949026
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 98.94200134277344,
|
||||
"cost": 0.0,
|
||||
"mean_reward": 1.554954171180725,
|
||||
"n_episodes": 500,
|
||||
"p05": -3.022125720977783,
|
||||
"p50": 0.3521251678466797,
|
||||
"p95": 10.139877319335938,
|
||||
"sharpe_annualised": 10.411664009094238,
|
||||
"sharpe_per_episode": 0.36381298303604126,
|
||||
"std_reward": 4.274048328399658,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.5519999861717224
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 98.55400085449219,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -0.1477748453617096,
|
||||
"n_episodes": 500,
|
||||
"p05": -6.051650524139404,
|
||||
"p50": -0.8751249313354492,
|
||||
"p95": 7.868124485015869,
|
||||
"sharpe_annualised": -0.9564568996429443,
|
||||
"sharpe_per_episode": -0.03342130780220032,
|
||||
"std_reward": 4.421575546264648,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.36800000071525574
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 95.23999786376953,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -1.9221543073654175,
|
||||
"n_episodes": 500,
|
||||
"p05": -7.974623680114746,
|
||||
"p50": -2.276874542236328,
|
||||
"p95": 5.843000888824463,
|
||||
"sharpe_annualised": -13.812037467956543,
|
||||
"sharpe_per_episode": -0.482631653547287,
|
||||
"std_reward": 3.9826526641845703,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.20600000023841858
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 97.05400085449219,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -5.653500556945801,
|
||||
"n_episodes": 500,
|
||||
"p05": -13.8267240524292,
|
||||
"p50": -5.270999431610107,
|
||||
"p95": 1.1986992359161377,
|
||||
"sharpe_annualised": -35.398399353027344,
|
||||
"sharpe_per_episode": -1.2369202375411987,
|
||||
"std_reward": 4.570626258850098,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.08399999886751175
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 96.16999816894531,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -12.689785957336426,
|
||||
"n_episodes": 500,
|
||||
"p05": -23.98550033569336,
|
||||
"p50": -11.424251556396484,
|
||||
"p95": -4.219249248504639,
|
||||
"sharpe_annualised": -58.158931732177734,
|
||||
"sharpe_per_episode": -2.0322375297546387,
|
||||
"std_reward": 6.244243621826172,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.006000000052154064
|
||||
}
|
||||
],
|
||||
"c51": true,
|
||||
"c51_n_atoms": 51,
|
||||
"c51_vmax": 10.0,
|
||||
"c51_vmin": -10.0,
|
||||
"cost_grid": [
|
||||
0.0,
|
||||
0.0625,
|
||||
0.125,
|
||||
0.25,
|
||||
0.5
|
||||
],
|
||||
"horizon": 600,
|
||||
"n_allowed_actions": 9,
|
||||
"n_eval_episodes": 500,
|
||||
"n_train_episodes": 1000,
|
||||
"phase": "E.3 Task 23 (2D sweep)",
|
||||
"pruned_actions": false,
|
||||
"threshold_grid": [
|
||||
0.0,
|
||||
0.05000000074505806,
|
||||
0.10000000149011612,
|
||||
0.15000000596046448,
|
||||
0.20000000298023224,
|
||||
0.25
|
||||
],
|
||||
"train_cost": 0.0625,
|
||||
"train_frac": 0.800000011920929
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
{
|
||||
"bins": [
|
||||
{
|
||||
"avg_n_trades": 505.7820129394531,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -7.379087924957275,
|
||||
"n_episodes": 500,
|
||||
"p05": -15.246373176574707,
|
||||
"p50": -9.201272964477539,
|
||||
"p95": 6.196549415588379,
|
||||
"sharpe_annualised": -31.543193817138672,
|
||||
"sharpe_per_episode": -1.1022083759307861,
|
||||
"std_reward": 6.694820880889893,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.1420000046491623
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 506.8420104980469,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -15.913408279418945,
|
||||
"n_episodes": 500,
|
||||
"p05": -23.970996856689453,
|
||||
"p50": -17.78717803955078,
|
||||
"p95": -3.438425302505493,
|
||||
"sharpe_annualised": -68.62244415283203,
|
||||
"sharpe_per_episode": -2.397862195968628,
|
||||
"std_reward": 6.63649845123291,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.029999999329447746
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 505.7179870605469,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -23.430904388427734,
|
||||
"n_episodes": 500,
|
||||
"p05": -32.949134826660156,
|
||||
"p50": -24.983301162719727,
|
||||
"p95": -9.647102355957031,
|
||||
"sharpe_annualised": -91.5419921875,
|
||||
"sharpe_per_episode": -3.1987359523773193,
|
||||
"std_reward": 7.3250508308410645,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0020000000949949026
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 507.2300109863281,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -40.23920440673828,
|
||||
"n_episodes": 500,
|
||||
"p05": -50.727237701416016,
|
||||
"p50": -41.87934875488281,
|
||||
"p95": -25.59070587158203,
|
||||
"sharpe_annualised": -140.99142456054688,
|
||||
"sharpe_per_episode": -4.926639080047607,
|
||||
"std_reward": 8.167678833007812,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 506.33599853515625,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -72.27790832519531,
|
||||
"n_episodes": 500,
|
||||
"p05": -89.18425750732422,
|
||||
"p50": -72.6242904663086,
|
||||
"p95": -53.71200180053711,
|
||||
"sharpe_annualised": -197.06411743164062,
|
||||
"sharpe_per_episode": -6.885977745056152,
|
||||
"std_reward": 10.496389389038086,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 411.7900085449219,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -5.73362922668457,
|
||||
"n_episodes": 500,
|
||||
"p05": -13.241250991821289,
|
||||
"p50": -6.448873996734619,
|
||||
"p95": 4.655198097229004,
|
||||
"sharpe_annualised": -28.97418212890625,
|
||||
"sharpe_per_episode": -1.0124398469924927,
|
||||
"std_reward": 5.663179874420166,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.1459999978542328
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 414.65399169921875,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -12.299840927124023,
|
||||
"n_episodes": 500,
|
||||
"p05": -22.073755264282227,
|
||||
"p50": -12.375904083251953,
|
||||
"p95": -2.1291003227233887,
|
||||
"sharpe_annualised": -56.6534309387207,
|
||||
"sharpe_per_episode": -1.9796310663223267,
|
||||
"std_reward": 6.213198661804199,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.03999999910593033
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 413.5060119628906,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -19.20939064025879,
|
||||
"n_episodes": 500,
|
||||
"p05": -31.44267463684082,
|
||||
"p50": -19.380374908447266,
|
||||
"p95": -7.792024612426758,
|
||||
"sharpe_annualised": -76.8126220703125,
|
||||
"sharpe_per_episode": -2.6840503215789795,
|
||||
"std_reward": 7.156866550445557,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.00800000037997961
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 411.93798828125,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -32.43183517456055,
|
||||
"n_episodes": 500,
|
||||
"p05": -48.73774337768555,
|
||||
"p50": -32.32264709472656,
|
||||
"p95": -16.66182518005371,
|
||||
"sharpe_annualised": -97.9302749633789,
|
||||
"sharpe_per_episode": -3.4219608306884766,
|
||||
"std_reward": 9.477559089660645,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 415.7279968261719,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -60.216548919677734,
|
||||
"n_episodes": 500,
|
||||
"p05": -83.98477935791016,
|
||||
"p50": -63.01105499267578,
|
||||
"p95": -33.47249984741211,
|
||||
"sharpe_annualised": -111.51805877685547,
|
||||
"sharpe_per_episode": -3.896756410598755,
|
||||
"std_reward": 15.452993392944336,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 307.6679992675781,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -2.7668509483337402,
|
||||
"n_episodes": 500,
|
||||
"p05": -11.989750862121582,
|
||||
"p50": -2.776402473449707,
|
||||
"p95": 7.871026992797852,
|
||||
"sharpe_annualised": -13.079078674316406,
|
||||
"sharpe_per_episode": -0.45701998472213745,
|
||||
"std_reward": 6.054113864898682,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.257999986410141
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 313.0580139160156,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -8.025443077087402,
|
||||
"n_episodes": 500,
|
||||
"p05": -20.223373413085938,
|
||||
"p50": -7.3301496505737305,
|
||||
"p95": 2.120025396347046,
|
||||
"sharpe_annualised": -33.09466552734375,
|
||||
"sharpe_per_episode": -1.156421184539795,
|
||||
"std_reward": 6.939896583557129,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.09000000357627869
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 312.7879943847656,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -13.201777458190918,
|
||||
"n_episodes": 500,
|
||||
"p05": -27.572002410888672,
|
||||
"p50": -11.809425354003906,
|
||||
"p95": -1.6250247955322266,
|
||||
"sharpe_annualised": -45.03886795043945,
|
||||
"sharpe_per_episode": -1.573785424232483,
|
||||
"std_reward": 8.3885498046875,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.03799999877810478
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 309.92401123046875,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -23.745981216430664,
|
||||
"n_episodes": 500,
|
||||
"p05": -43.5518913269043,
|
||||
"p50": -22.692001342773438,
|
||||
"p95": -7.84235143661499,
|
||||
"sharpe_annualised": -57.198463439941406,
|
||||
"sharpe_per_episode": -1.998676061630249,
|
||||
"std_reward": 11.880855560302734,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.004000000189989805
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 317.7900085449219,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -44.5992431640625,
|
||||
"n_episodes": 500,
|
||||
"p05": -77.12137603759766,
|
||||
"p50": -44.13597869873047,
|
||||
"p95": -14.920825004577637,
|
||||
"sharpe_annualised": -60.17354202270508,
|
||||
"sharpe_per_episode": -2.1026337146759033,
|
||||
"std_reward": 21.211132049560547,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 214.0800018310547,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -0.7500550150871277,
|
||||
"n_episodes": 500,
|
||||
"p05": -8.611875534057617,
|
||||
"p50": -1.001124382019043,
|
||||
"p95": 7.168798923492432,
|
||||
"sharpe_annualised": -4.3168110847473145,
|
||||
"sharpe_per_episode": -0.1508415937423706,
|
||||
"std_reward": 4.972468376159668,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.3479999899864197
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 220.29600524902344,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -4.576065540313721,
|
||||
"n_episodes": 500,
|
||||
"p05": -15.124225616455078,
|
||||
"p50": -3.609375,
|
||||
"p95": 5.420872688293457,
|
||||
"sharpe_annualised": -22.16614532470703,
|
||||
"sharpe_per_episode": -0.774547815322876,
|
||||
"std_reward": 5.908047676086426,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.16200000047683716
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 206.593994140625,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -7.652109146118164,
|
||||
"n_episodes": 500,
|
||||
"p05": -20.91200065612793,
|
||||
"p50": -5.522923946380615,
|
||||
"p95": 0.7698264122009277,
|
||||
"sharpe_annualised": -31.492902755737305,
|
||||
"sharpe_per_episode": -1.100451111793518,
|
||||
"std_reward": 6.953611373901367,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.08399999886751175
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 207.1020050048828,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -14.569986343383789,
|
||||
"n_episodes": 500,
|
||||
"p05": -31.593997955322266,
|
||||
"p50": -11.627604484558105,
|
||||
"p95": -3.4181251525878906,
|
||||
"sharpe_annualised": -42.324275970458984,
|
||||
"sharpe_per_episode": -1.478929877281189,
|
||||
"std_reward": 9.85170841217041,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.014000000432133675
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 212.37600708007812,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -29.330257415771484,
|
||||
"n_episodes": 500,
|
||||
"p05": -61.26882553100586,
|
||||
"p50": -24.778127670288086,
|
||||
"p95": -7.954624652862549,
|
||||
"sharpe_annualised": -46.668235778808594,
|
||||
"sharpe_per_episode": -1.6307201385498047,
|
||||
"std_reward": 17.98607635498047,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 133.64199829101562,
|
||||
"cost": 0.0,
|
||||
"mean_reward": 0.8351230621337891,
|
||||
"n_episodes": 500,
|
||||
"p05": -4.833250522613525,
|
||||
"p50": -0.04905200004577637,
|
||||
"p95": 9.4207763671875,
|
||||
"sharpe_annualised": 5.84094762802124,
|
||||
"sharpe_per_episode": 0.20409922301769257,
|
||||
"std_reward": 4.091750144958496,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.49000000953674316
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 130.7899932861328,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -1.5587923526763916,
|
||||
"n_episodes": 500,
|
||||
"p05": -8.187274932861328,
|
||||
"p50": -1.694624423980713,
|
||||
"p95": 5.250924110412598,
|
||||
"sharpe_annualised": -10.926456451416016,
|
||||
"sharpe_per_episode": -0.3818012773990631,
|
||||
"std_reward": 4.082732200622559,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.2840000092983246
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 132.32000732421875,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -3.9061996936798096,
|
||||
"n_episodes": 500,
|
||||
"p05": -11.645946502685547,
|
||||
"p50": -3.379124402999878,
|
||||
"p95": 2.765799045562744,
|
||||
"sharpe_annualised": -24.989980697631836,
|
||||
"sharpe_per_episode": -0.8732205629348755,
|
||||
"std_reward": 4.473325252532959,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.15600000321865082
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 131.50799560546875,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -7.899728298187256,
|
||||
"n_episodes": 500,
|
||||
"p05": -18.36737632751465,
|
||||
"p50": -6.697125434875488,
|
||||
"p95": 0.27234911918640137,
|
||||
"sharpe_annualised": -38.39303970336914,
|
||||
"sharpe_per_episode": -1.3415614366531372,
|
||||
"std_reward": 5.888458251953125,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.05999999865889549
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 128.50399780273438,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -17.549320220947266,
|
||||
"n_episodes": 500,
|
||||
"p05": -35.324703216552734,
|
||||
"p50": -15.349874496459961,
|
||||
"p95": -5.963876247406006,
|
||||
"sharpe_annualised": -55.83051681518555,
|
||||
"sharpe_per_episode": -1.9508761167526245,
|
||||
"std_reward": 8.995609283447266,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.0020000000949949026
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 98.94200134277344,
|
||||
"cost": 0.0,
|
||||
"mean_reward": 1.5531558990478516,
|
||||
"n_episodes": 500,
|
||||
"p05": -3.022125720977783,
|
||||
"p50": 0.3488759994506836,
|
||||
"p95": 10.139877319335938,
|
||||
"sharpe_annualised": 10.39977741241455,
|
||||
"sharpe_per_episode": 0.36339762806892395,
|
||||
"std_reward": 4.273984909057617,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.5519999861717224
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 98.55400085449219,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -0.1503140926361084,
|
||||
"n_episodes": 500,
|
||||
"p05": -6.051650524139404,
|
||||
"p50": -0.8751249313354492,
|
||||
"p95": 7.868124485015869,
|
||||
"sharpe_annualised": -0.972659170627594,
|
||||
"sharpe_per_episode": -0.033987462520599365,
|
||||
"std_reward": 4.422633647918701,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.36800000071525574
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 95.23999786376953,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -1.923614263534546,
|
||||
"n_episodes": 500,
|
||||
"p05": -7.974623680114746,
|
||||
"p50": -2.276874542236328,
|
||||
"p95": 5.843000888824463,
|
||||
"sharpe_annualised": -13.825085639953613,
|
||||
"sharpe_per_episode": -0.48308759927749634,
|
||||
"std_reward": 3.9819159507751465,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.20600000023841858
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 97.05400085449219,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -5.656537055969238,
|
||||
"n_episodes": 500,
|
||||
"p05": -13.8267240524292,
|
||||
"p50": -5.272023677825928,
|
||||
"p95": 1.1986992359161377,
|
||||
"sharpe_annualised": -35.41947937011719,
|
||||
"sharpe_per_episode": -1.2376567125320435,
|
||||
"std_reward": 4.57036018371582,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.08399999886751175
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 96.16999816894531,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -12.691283226013184,
|
||||
"n_episodes": 500,
|
||||
"p05": -23.98550033569336,
|
||||
"p50": -11.424251556396484,
|
||||
"p95": -4.219249248504639,
|
||||
"sharpe_annualised": -58.172401428222656,
|
||||
"sharpe_per_episode": -2.032708168029785,
|
||||
"std_reward": 6.243533611297607,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.006000000052154064
|
||||
}
|
||||
],
|
||||
"c51": true,
|
||||
"c51_n_atoms": 51,
|
||||
"c51_vmax": 10.0,
|
||||
"c51_vmin": -10.0,
|
||||
"cost_grid": [
|
||||
0.0,
|
||||
0.0625,
|
||||
0.125,
|
||||
0.25,
|
||||
0.5
|
||||
],
|
||||
"horizon": 600,
|
||||
"n_allowed_actions": 9,
|
||||
"n_eval_episodes": 500,
|
||||
"n_train_episodes": 1000,
|
||||
"phase": "E.3 Task 23 (2D sweep)",
|
||||
"pruned_actions": false,
|
||||
"threshold_grid": [
|
||||
0.0,
|
||||
0.05000000074505806,
|
||||
0.10000000149011612,
|
||||
0.15000000596046448,
|
||||
0.20000000298023224,
|
||||
0.25
|
||||
],
|
||||
"train_cost": 0.0625,
|
||||
"train_frac": 0.800000011920929
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
{
|
||||
"bins": [
|
||||
{
|
||||
"avg_n_trades": 409.4419860839844,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -30.64391326904297,
|
||||
"n_episodes": 500,
|
||||
"p05": -49.61526870727539,
|
||||
"p50": -28.566925048828125,
|
||||
"p95": -18.450275421142578,
|
||||
"sharpe_annualised": -77.86469268798828,
|
||||
"sharpe_per_episode": -2.7208125591278076,
|
||||
"std_reward": 11.262779235839844,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 407.4179992675781,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -45.718017578125,
|
||||
"n_episodes": 500,
|
||||
"p05": -64.58588409423828,
|
||||
"p50": -44.32868576049805,
|
||||
"p95": -31.426769256591797,
|
||||
"sharpe_annualised": -126.78475189208984,
|
||||
"sharpe_per_episode": -4.430217742919922,
|
||||
"std_reward": 10.319586753845215,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 412.2200012207031,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -62.28627014160156,
|
||||
"n_episodes": 500,
|
||||
"p05": -87.70376586914062,
|
||||
"p50": -59.780799865722656,
|
||||
"p95": -41.51686477661133,
|
||||
"sharpe_annualised": -125.80178833007812,
|
||||
"sharpe_per_episode": -4.395870208740234,
|
||||
"std_reward": 14.169269561767578,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 412.1180114746094,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -94.7795181274414,
|
||||
"n_episodes": 500,
|
||||
"p05": -125.90235137939453,
|
||||
"p50": -93.9426498413086,
|
||||
"p95": -67.9682388305664,
|
||||
"sharpe_annualised": -141.44520568847656,
|
||||
"sharpe_per_episode": -4.942495346069336,
|
||||
"std_reward": 19.176450729370117,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 412.614013671875,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -159.3415069580078,
|
||||
"n_episodes": 500,
|
||||
"p05": -202.5274200439453,
|
||||
"p50": -165.60362243652344,
|
||||
"p95": -108.91909790039062,
|
||||
"sharpe_annualised": -150.140380859375,
|
||||
"sharpe_per_episode": -5.246329307556152,
|
||||
"std_reward": 30.371997833251953,
|
||||
"threshold": 0.0,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 346.38800048828125,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -26.021240234375,
|
||||
"n_episodes": 500,
|
||||
"p05": -42.303611755371094,
|
||||
"p50": -23.978374481201172,
|
||||
"p95": -13.484901428222656,
|
||||
"sharpe_annualised": -78.6505355834961,
|
||||
"sharpe_per_episode": -2.748272180557251,
|
||||
"std_reward": 9.468217849731445,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0020000000949949026
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 345.72198486328125,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -39.795684814453125,
|
||||
"n_episodes": 500,
|
||||
"p05": -60.49996566772461,
|
||||
"p50": -36.7501220703125,
|
||||
"p95": -25.743999481201172,
|
||||
"sharpe_annualised": -99.63706970214844,
|
||||
"sharpe_per_episode": -3.4816009998321533,
|
||||
"std_reward": 11.430282592773438,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 343.6199951171875,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -53.896480560302734,
|
||||
"n_episodes": 500,
|
||||
"p05": -81.64607238769531,
|
||||
"p50": -49.21561813354492,
|
||||
"p95": -34.734275817871094,
|
||||
"sharpe_annualised": -98.90637969970703,
|
||||
"sharpe_per_episode": -3.456068515777588,
|
||||
"std_reward": 15.59473705291748,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 342.9639892578125,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -79.68690490722656,
|
||||
"n_episodes": 500,
|
||||
"p05": -113.12445068359375,
|
||||
"p50": -73.63372802734375,
|
||||
"p95": -55.00608825683594,
|
||||
"sharpe_annualised": -116.19927978515625,
|
||||
"sharpe_per_episode": -4.060331344604492,
|
||||
"std_reward": 19.625713348388672,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 346.8240051269531,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -135.82601928710938,
|
||||
"n_episodes": 500,
|
||||
"p05": -184.07630920410156,
|
||||
"p50": -126.77576446533203,
|
||||
"p95": -91.21710968017578,
|
||||
"sharpe_annualised": -118.39791107177734,
|
||||
"sharpe_per_episode": -4.137157917022705,
|
||||
"std_reward": 32.83075714111328,
|
||||
"threshold": 0.05000000074505806,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 283.99200439453125,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -22.65932846069336,
|
||||
"n_episodes": 500,
|
||||
"p05": -43.386817932128906,
|
||||
"p50": -20.47347068786621,
|
||||
"p95": -9.256776809692383,
|
||||
"sharpe_annualised": -60.726375579833984,
|
||||
"sharpe_per_episode": -2.1219513416290283,
|
||||
"std_reward": 10.678532600402832,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 280.16400146484375,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -33.22947311401367,
|
||||
"n_episodes": 500,
|
||||
"p05": -57.53879165649414,
|
||||
"p50": -31.169370651245117,
|
||||
"p95": -16.533370971679688,
|
||||
"sharpe_annualised": -72.24711608886719,
|
||||
"sharpe_per_episode": -2.5245184898376465,
|
||||
"std_reward": 13.162697792053223,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 274.6679992675781,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -42.85700225830078,
|
||||
"n_episodes": 500,
|
||||
"p05": -70.168212890625,
|
||||
"p50": -40.32282257080078,
|
||||
"p95": -22.919273376464844,
|
||||
"sharpe_annualised": -79.69395446777344,
|
||||
"sharpe_per_episode": -2.7847321033477783,
|
||||
"std_reward": 15.38999080657959,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.0020000000949949026
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 274.9219970703125,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -64.72478485107422,
|
||||
"n_episodes": 500,
|
||||
"p05": -101.20083618164062,
|
||||
"p50": -61.02626419067383,
|
||||
"p95": -35.71025085449219,
|
||||
"sharpe_annualised": -84.47412872314453,
|
||||
"sharpe_per_episode": -2.9517648220062256,
|
||||
"std_reward": 21.9274845123291,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 275.6679992675781,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -108.99763488769531,
|
||||
"n_episodes": 500,
|
||||
"p05": -166.28961181640625,
|
||||
"p50": -104.48725128173828,
|
||||
"p95": -57.43674850463867,
|
||||
"sharpe_annualised": -82.64939880371094,
|
||||
"sharpe_per_episode": -2.8880035877227783,
|
||||
"std_reward": 37.74151611328125,
|
||||
"threshold": 0.10000000149011612,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 215.17799377441406,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -17.607177734375,
|
||||
"n_episodes": 500,
|
||||
"p05": -35.043479919433594,
|
||||
"p50": -15.50200080871582,
|
||||
"p95": -5.586098670959473,
|
||||
"sharpe_annualised": -49.608726501464844,
|
||||
"sharpe_per_episode": -1.7334692478179932,
|
||||
"std_reward": 10.157191276550293,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.017999999225139618
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 209.697998046875,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -26.11052131652832,
|
||||
"n_episodes": 500,
|
||||
"p05": -48.8406982421875,
|
||||
"p50": -22.618318557739258,
|
||||
"p95": -10.570127487182617,
|
||||
"sharpe_annualised": -57.65428161621094,
|
||||
"sharpe_per_episode": -2.014603614807129,
|
||||
"std_reward": 12.960623741149902,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.004000000189989805
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 217.76600646972656,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -35.587677001953125,
|
||||
"n_episodes": 500,
|
||||
"p05": -62.99174499511719,
|
||||
"p50": -31.81800079345703,
|
||||
"p95": -16.088119506835938,
|
||||
"sharpe_annualised": -66.77113342285156,
|
||||
"sharpe_per_episode": -2.33317232131958,
|
||||
"std_reward": 15.252914428710938,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.0020000000949949026
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 213.79600524902344,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -51.22513961791992,
|
||||
"n_episodes": 500,
|
||||
"p05": -86.71883392333984,
|
||||
"p50": -47.09474563598633,
|
||||
"p95": -25.441743850708008,
|
||||
"sharpe_annualised": -71.61820220947266,
|
||||
"sharpe_per_episode": -2.502542495727539,
|
||||
"std_reward": 20.469240188598633,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 216.11000061035156,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -85.81346893310547,
|
||||
"n_episodes": 500,
|
||||
"p05": -140.97569274902344,
|
||||
"p50": -81.15174102783203,
|
||||
"p95": -41.56011962890625,
|
||||
"sharpe_annualised": -76.13468933105469,
|
||||
"sharpe_per_episode": -2.6603612899780273,
|
||||
"std_reward": 32.25632095336914,
|
||||
"threshold": 0.15000000596046448,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 161.28199768066406,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -14.621756553649902,
|
||||
"n_episodes": 500,
|
||||
"p05": -34.215675354003906,
|
||||
"p50": -11.45572280883789,
|
||||
"p95": -4.464199542999268,
|
||||
"sharpe_annualised": -42.86232376098633,
|
||||
"sharpe_per_episode": -1.4977308511734009,
|
||||
"std_reward": 9.762606620788574,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.014000000432133675
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 165.6179962158203,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -21.606536865234375,
|
||||
"n_episodes": 500,
|
||||
"p05": -42.06482696533203,
|
||||
"p50": -18.75684356689453,
|
||||
"p95": -8.337549209594727,
|
||||
"sharpe_annualised": -55.583866119384766,
|
||||
"sharpe_per_episode": -1.9422574043273926,
|
||||
"std_reward": 11.124444961547852,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.00800000037997961
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 158.93600463867188,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -26.25715446472168,
|
||||
"n_episodes": 500,
|
||||
"p05": -49.11092758178711,
|
||||
"p50": -23.162250518798828,
|
||||
"p95": -11.844600677490234,
|
||||
"sharpe_annualised": -63.4965705871582,
|
||||
"sharpe_per_episode": -2.218749761581421,
|
||||
"std_reward": 11.834211349487305,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 161.45599365234375,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -39.04631805419922,
|
||||
"n_episodes": 500,
|
||||
"p05": -68.93614959716797,
|
||||
"p50": -34.38017272949219,
|
||||
"p95": -20.195877075195312,
|
||||
"sharpe_annualised": -70.44474029541016,
|
||||
"sharpe_per_episode": -2.461538314819336,
|
||||
"std_reward": 15.862567901611328,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 163.34800720214844,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -63.5859375,
|
||||
"n_episodes": 500,
|
||||
"p05": -102.9427719116211,
|
||||
"p50": -58.92830276489258,
|
||||
"p95": -32.09709548950195,
|
||||
"sharpe_annualised": -75.88227844238281,
|
||||
"sharpe_per_episode": -2.6515414714813232,
|
||||
"std_reward": 23.980743408203125,
|
||||
"threshold": 0.20000000298023224,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 134.08599853515625,
|
||||
"cost": 0.0,
|
||||
"mean_reward": -13.83078670501709,
|
||||
"n_episodes": 500,
|
||||
"p05": -36.201629638671875,
|
||||
"p50": -10.920823097229004,
|
||||
"p95": -2.622623920440674,
|
||||
"sharpe_annualised": -37.462730407714844,
|
||||
"sharpe_per_episode": -1.3090537786483765,
|
||||
"std_reward": 10.565484046936035,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.00800000037997961
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 136.51199340820312,
|
||||
"cost": 0.0625,
|
||||
"mean_reward": -18.66702651977539,
|
||||
"n_episodes": 500,
|
||||
"p05": -40.3795051574707,
|
||||
"p50": -15.633600234985352,
|
||||
"p95": -5.700125217437744,
|
||||
"sharpe_annualised": -46.29603576660156,
|
||||
"sharpe_per_episode": -1.6177144050598145,
|
||||
"std_reward": 11.539135932922363,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.004000000189989805
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 136.00399780273438,
|
||||
"cost": 0.125,
|
||||
"mean_reward": -22.70488739013672,
|
||||
"n_episodes": 500,
|
||||
"p05": -46.12636947631836,
|
||||
"p50": -20.130022048950195,
|
||||
"p95": -8.378849029541016,
|
||||
"sharpe_annualised": -52.185447692871094,
|
||||
"sharpe_per_episode": -1.8235070705413818,
|
||||
"std_reward": 12.45121955871582,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.004000000189989805
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 133.22999572753906,
|
||||
"cost": 0.25,
|
||||
"mean_reward": -31.63664436340332,
|
||||
"n_episodes": 500,
|
||||
"p05": -58.47060012817383,
|
||||
"p50": -29.137378692626953,
|
||||
"p95": -13.295875549316406,
|
||||
"sharpe_annualised": -64.48110961914062,
|
||||
"sharpe_per_episode": -2.253152370452881,
|
||||
"std_reward": 14.041058540344238,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.0
|
||||
},
|
||||
{
|
||||
"avg_n_trades": 133.7779998779297,
|
||||
"cost": 0.5,
|
||||
"mean_reward": -49.41081619262695,
|
||||
"n_episodes": 500,
|
||||
"p05": -84.51933288574219,
|
||||
"p50": -46.30955123901367,
|
||||
"p95": -23.72110366821289,
|
||||
"sharpe_annualised": -71.6323471069336,
|
||||
"sharpe_per_episode": -2.5030367374420166,
|
||||
"std_reward": 19.74034881591797,
|
||||
"threshold": 0.25,
|
||||
"win_rate": 0.0
|
||||
}
|
||||
],
|
||||
"cost_grid": [
|
||||
0.0,
|
||||
0.0625,
|
||||
0.125,
|
||||
0.25,
|
||||
0.5
|
||||
],
|
||||
"horizon": 600,
|
||||
"n_allowed_actions": 4,
|
||||
"n_eval_episodes": 500,
|
||||
"n_train_episodes": 1000,
|
||||
"phase": "E.3 Task 23 (2D sweep)",
|
||||
"pruned_actions": true,
|
||||
"threshold_grid": [
|
||||
0.0,
|
||||
0.05000000074505806,
|
||||
0.10000000149011612,
|
||||
0.15000000596046448,
|
||||
0.20000000298023224,
|
||||
0.25
|
||||
],
|
||||
"train_cost": 0.0625,
|
||||
"train_frac": 0.800000011920929
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
{
|
||||
"action_entropy_ema": 1.9956868886947632,
|
||||
"all_pass": true,
|
||||
"alpha_m": 0.8999999761581421,
|
||||
"early_q_movement_ema": 0.13011053204536438,
|
||||
"eps_end": 0.05000000074505806,
|
||||
"eps_start": 0.5,
|
||||
"gamma": 0.9900000095367432,
|
||||
"grad_clip": 1.0,
|
||||
"horizon": 6000,
|
||||
"kc_log": [
|
||||
{
|
||||
"early_mvmt": 0.014495394192636013,
|
||||
"entropy": 2.02925705909729,
|
||||
"episode": 50,
|
||||
"q_spread": 16.203521728515625,
|
||||
"rvr": 1.0110208988189697
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.021577946841716766,
|
||||
"entropy": 2.0279672145843506,
|
||||
"episode": 100,
|
||||
"q_spread": 19.90399932861328,
|
||||
"rvr": 1.0110208988189697
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.029696958139538765,
|
||||
"entropy": 2.0261473655700684,
|
||||
"episode": 150,
|
||||
"q_spread": 19.78053092956543,
|
||||
"rvr": 1.0108973979949951
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.03799359127879143,
|
||||
"entropy": 2.024191379547119,
|
||||
"episode": 200,
|
||||
"q_spread": 22.229785919189453,
|
||||
"rvr": 1.0106135606765747
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.04607415571808815,
|
||||
"entropy": 2.022186040878296,
|
||||
"episode": 250,
|
||||
"q_spread": 22.845211029052734,
|
||||
"rvr": 1.0104221105575562
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.05395592004060745,
|
||||
"entropy": 2.019918441772461,
|
||||
"episode": 300,
|
||||
"q_spread": 26.798715591430664,
|
||||
"rvr": 1.00983726978302
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.06150386109948158,
|
||||
"entropy": 2.0177736282348633,
|
||||
"episode": 350,
|
||||
"q_spread": 28.840295791625977,
|
||||
"rvr": 1.0095032453536987
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.06863006949424744,
|
||||
"entropy": 2.015869140625,
|
||||
"episode": 400,
|
||||
"q_spread": 28.40835952758789,
|
||||
"rvr": 1.0089091062545776
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.07545721530914307,
|
||||
"entropy": 2.013965129852295,
|
||||
"episode": 450,
|
||||
"q_spread": 23.48478889465332,
|
||||
"rvr": 1.0084400177001953
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0819401815533638,
|
||||
"entropy": 2.0119924545288086,
|
||||
"episode": 500,
|
||||
"q_spread": 23.23484230041504,
|
||||
"rvr": 1.0082913637161255
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0881161019206047,
|
||||
"entropy": 2.010127544403076,
|
||||
"episode": 550,
|
||||
"q_spread": 24.8494873046875,
|
||||
"rvr": 1.0076979398727417
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.09393466264009476,
|
||||
"entropy": 2.0083234310150146,
|
||||
"episode": 600,
|
||||
"q_spread": 22.99201202392578,
|
||||
"rvr": 1.0069901943206787
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.09942196309566498,
|
||||
"entropy": 2.006577253341675,
|
||||
"episode": 650,
|
||||
"q_spread": 32.014129638671875,
|
||||
"rvr": 1.0060724020004272
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.10459540784358978,
|
||||
"entropy": 2.004857063293457,
|
||||
"episode": 700,
|
||||
"q_spread": 26.52780532836914,
|
||||
"rvr": 1.0056869983673096
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.10947921127080917,
|
||||
"entropy": 2.0032100677490234,
|
||||
"episode": 750,
|
||||
"q_spread": 98.947265625,
|
||||
"rvr": 1.005436658859253
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.11409175395965576,
|
||||
"entropy": 2.0016136169433594,
|
||||
"episode": 800,
|
||||
"q_spread": 48.021114349365234,
|
||||
"rvr": 1.0041899681091309
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.11845553666353226,
|
||||
"entropy": 2.0000827312469482,
|
||||
"episode": 850,
|
||||
"q_spread": 35.853179931640625,
|
||||
"rvr": 1.0036035776138306
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.1225731149315834,
|
||||
"entropy": 1.998610019683838,
|
||||
"episode": 900,
|
||||
"q_spread": 34.26390075683594,
|
||||
"rvr": 1.0024551153182983
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.12645159661769867,
|
||||
"entropy": 1.9971414804458618,
|
||||
"episode": 950,
|
||||
"q_spread": 37.47758102416992,
|
||||
"rvr": 1.0019822120666504
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.13011053204536438,
|
||||
"entropy": 1.9956868886947632,
|
||||
"episode": 1000,
|
||||
"q_spread": 29.594701766967773,
|
||||
"rvr": 1.001029133796692
|
||||
}
|
||||
],
|
||||
"lr": 0.00009999999747378752,
|
||||
"n_episodes": 1000,
|
||||
"pass_early": true,
|
||||
"pass_entropy": true,
|
||||
"pass_q_spread": true,
|
||||
"pass_rvr": true,
|
||||
"phase": "E.1 Task 12",
|
||||
"q_init_norm": 2.5735182762145996,
|
||||
"q_spread_ema": 29.594701766967773,
|
||||
"return_vs_random_ema": 1.001029133796692,
|
||||
"reward_scale": 1000.0,
|
||||
"target_update_every": 10,
|
||||
"tau": 0.029999999329447746
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
{
|
||||
"action_entropy_ema": 1.968935489654541,
|
||||
"all_pass": true,
|
||||
"alpha_m": 0.8999999761581421,
|
||||
"early_q_movement_ema": 0.09944231063127518,
|
||||
"eps_end": 0.05000000074505806,
|
||||
"eps_start": 0.5,
|
||||
"gamma": 0.9900000095367432,
|
||||
"grad_clip": 1.0,
|
||||
"horizon": 600,
|
||||
"kc_log": [
|
||||
{
|
||||
"early_mvmt": 0.011208697222173214,
|
||||
"entropy": 2.0113048553466797,
|
||||
"episode": 50,
|
||||
"q_spread": 38.21281433105469,
|
||||
"rvr": 1.0437408685684204
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.016528302803635597,
|
||||
"entropy": 2.0112295150756836,
|
||||
"episode": 100,
|
||||
"q_spread": 27.53325080871582,
|
||||
"rvr": 1.0437407493591309
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.022869938984513283,
|
||||
"entropy": 2.0082004070281982,
|
||||
"episode": 150,
|
||||
"q_spread": 21.13676643371582,
|
||||
"rvr": 1.0437486171722412
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.029317570850253105,
|
||||
"entropy": 2.005980968475342,
|
||||
"episode": 200,
|
||||
"q_spread": 20.710119247436523,
|
||||
"rvr": 1.04374098777771
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0356033593416214,
|
||||
"entropy": 2.0036354064941406,
|
||||
"episode": 250,
|
||||
"q_spread": 19.258197784423828,
|
||||
"rvr": 1.0437567234039307
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.041733257472515106,
|
||||
"entropy": 2.001204252243042,
|
||||
"episode": 300,
|
||||
"q_spread": 26.219839096069336,
|
||||
"rvr": 1.0437005758285522
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.047568317502737045,
|
||||
"entropy": 1.9986319541931152,
|
||||
"episode": 350,
|
||||
"q_spread": 16.797637939453125,
|
||||
"rvr": 1.0436869859695435
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.053121890872716904,
|
||||
"entropy": 1.9962358474731445,
|
||||
"episode": 400,
|
||||
"q_spread": 12.861183166503906,
|
||||
"rvr": 1.0436040163040161
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.05835384875535965,
|
||||
"entropy": 1.993706464767456,
|
||||
"episode": 450,
|
||||
"q_spread": 11.837430000305176,
|
||||
"rvr": 1.043595314025879
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.06333397328853607,
|
||||
"entropy": 1.9912099838256836,
|
||||
"episode": 500,
|
||||
"q_spread": 14.526165008544922,
|
||||
"rvr": 1.0435247421264648
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0680142194032669,
|
||||
"entropy": 1.988599419593811,
|
||||
"episode": 550,
|
||||
"q_spread": 13.646886825561523,
|
||||
"rvr": 1.043499231338501
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.07242720574140549,
|
||||
"entropy": 1.9862360954284668,
|
||||
"episode": 600,
|
||||
"q_spread": 13.742671012878418,
|
||||
"rvr": 1.0434374809265137
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.07657017558813095,
|
||||
"entropy": 1.9838505983352661,
|
||||
"episode": 650,
|
||||
"q_spread": 17.030311584472656,
|
||||
"rvr": 1.0434088706970215
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.08046558499336243,
|
||||
"entropy": 1.9814624786376953,
|
||||
"episode": 700,
|
||||
"q_spread": 19.744157791137695,
|
||||
"rvr": 1.043370008468628
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.08412447571754456,
|
||||
"entropy": 1.9791860580444336,
|
||||
"episode": 750,
|
||||
"q_spread": 16.830333709716797,
|
||||
"rvr": 1.0432881116867065
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.08755383640527725,
|
||||
"entropy": 1.9769866466522217,
|
||||
"episode": 800,
|
||||
"q_spread": 18.11051368713379,
|
||||
"rvr": 1.0432313680648804
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.09078100323677063,
|
||||
"entropy": 1.9748573303222656,
|
||||
"episode": 850,
|
||||
"q_spread": 21.77625846862793,
|
||||
"rvr": 1.0432629585266113
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.09382475167512894,
|
||||
"entropy": 1.972854495048523,
|
||||
"episode": 900,
|
||||
"q_spread": 14.12209415435791,
|
||||
"rvr": 1.0431597232818604
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.09670793265104294,
|
||||
"entropy": 1.9708701372146606,
|
||||
"episode": 950,
|
||||
"q_spread": 12.469049453735352,
|
||||
"rvr": 1.0431069135665894
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.09944231063127518,
|
||||
"entropy": 1.968935489654541,
|
||||
"episode": 1000,
|
||||
"q_spread": 10.922320365905762,
|
||||
"rvr": 1.0431302785873413
|
||||
}
|
||||
],
|
||||
"lr": 0.00009999999747378752,
|
||||
"n_episodes": 1000,
|
||||
"pass_early": true,
|
||||
"pass_entropy": true,
|
||||
"pass_q_spread": true,
|
||||
"pass_rvr": true,
|
||||
"phase": "E.1 Task 12",
|
||||
"q_init_norm": 2.5735182762145996,
|
||||
"q_spread_ema": 10.922320365905762,
|
||||
"return_vs_random_ema": 1.0431302785873413,
|
||||
"reward_scale": 1000.0,
|
||||
"target_update_every": 10,
|
||||
"tau": 0.029999999329447746
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
{
|
||||
"action_entropy_ema": 0.6501350402832031,
|
||||
"all_pass": false,
|
||||
"alpha_m": 0.8999999761581421,
|
||||
"c51": true,
|
||||
"c51_n_atoms": 51,
|
||||
"c51_vmax": 10.0,
|
||||
"c51_vmin": -10.0,
|
||||
"early_q_movement_ema": 0.012797871604561806,
|
||||
"eps_end": 0.05000000074505806,
|
||||
"eps_start": 0.5,
|
||||
"final_stacker_kelly_attenuation": 0.10000000149011612,
|
||||
"final_stacker_threshold": 0.3718600869178772,
|
||||
"final_trade_rate_observed_ema": 0.14652971923351288,
|
||||
"gamma": 0.9900000095367432,
|
||||
"grad_clip": 1.0,
|
||||
"horizon": 600,
|
||||
"kc_log": [
|
||||
{
|
||||
"early_mvmt": 0.0019911762792617083,
|
||||
"entropy": 1.5733345746994019,
|
||||
"episode": 50,
|
||||
"q_spread": 31.75593376159668,
|
||||
"rvr": 1.0442577600479126
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0024081910960376263,
|
||||
"entropy": 1.4243084192276,
|
||||
"episode": 100,
|
||||
"q_spread": 23.955223083496094,
|
||||
"rvr": 1.0445905923843384
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0031225469429045916,
|
||||
"entropy": 1.310657262802124,
|
||||
"episode": 150,
|
||||
"q_spread": 19.488779067993164,
|
||||
"rvr": 1.0448150634765625
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0038882701192051172,
|
||||
"entropy": 1.2122981548309326,
|
||||
"episode": 200,
|
||||
"q_spread": 17.970020294189453,
|
||||
"rvr": 1.0449013710021973
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.004593650344759226,
|
||||
"entropy": 1.132895827293396,
|
||||
"episode": 250,
|
||||
"q_spread": 60.273345947265625,
|
||||
"rvr": 1.0449384450912476
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.005275565665215254,
|
||||
"entropy": 1.06606125831604,
|
||||
"episode": 300,
|
||||
"q_spread": 44.77523422241211,
|
||||
"rvr": 1.0449566841125488
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.005944964475929737,
|
||||
"entropy": 1.0079469680786133,
|
||||
"episode": 350,
|
||||
"q_spread": 35.19332504272461,
|
||||
"rvr": 1.0449742078781128
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0066252113319933414,
|
||||
"entropy": 0.9588310718536377,
|
||||
"episode": 400,
|
||||
"q_spread": 23.109128952026367,
|
||||
"rvr": 1.0449976921081543
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.007311227265745401,
|
||||
"entropy": 0.9154046773910522,
|
||||
"episode": 450,
|
||||
"q_spread": 30.99519920349121,
|
||||
"rvr": 1.045042634010315
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.007989339530467987,
|
||||
"entropy": 0.8766676187515259,
|
||||
"episode": 500,
|
||||
"q_spread": 28.470869064331055,
|
||||
"rvr": 1.0450870990753174
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.008644208312034607,
|
||||
"entropy": 0.8420573472976685,
|
||||
"episode": 550,
|
||||
"q_spread": 20.39789581298828,
|
||||
"rvr": 1.0451370477676392
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.009266316890716553,
|
||||
"entropy": 0.8106650114059448,
|
||||
"episode": 600,
|
||||
"q_spread": 15.78366470336914,
|
||||
"rvr": 1.0452361106872559
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.009855338372290134,
|
||||
"entropy": 0.7826521992683411,
|
||||
"episode": 650,
|
||||
"q_spread": 15.634352684020996,
|
||||
"rvr": 1.0453643798828125
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.010388685390353203,
|
||||
"entropy": 0.7577194571495056,
|
||||
"episode": 700,
|
||||
"q_spread": 13.281824111938477,
|
||||
"rvr": 1.0455033779144287
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.010871796868741512,
|
||||
"entropy": 0.7353291511535645,
|
||||
"episode": 750,
|
||||
"q_spread": 13.777722358703613,
|
||||
"rvr": 1.0455647706985474
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.011310325935482979,
|
||||
"entropy": 0.714767575263977,
|
||||
"episode": 800,
|
||||
"q_spread": 49.5208854675293,
|
||||
"rvr": 1.0456284284591675
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.011722153052687645,
|
||||
"entropy": 0.696082353591919,
|
||||
"episode": 850,
|
||||
"q_spread": 25.91875648498535,
|
||||
"rvr": 1.0456622838974
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.01210756879299879,
|
||||
"entropy": 0.6795051693916321,
|
||||
"episode": 900,
|
||||
"q_spread": 19.041736602783203,
|
||||
"rvr": 1.0456805229187012
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.012464887462556362,
|
||||
"entropy": 0.6642012000083923,
|
||||
"episode": 950,
|
||||
"q_spread": 31.483474731445312,
|
||||
"rvr": 1.0456591844558716
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.012797871604561806,
|
||||
"entropy": 0.6501350402832031,
|
||||
"episode": 1000,
|
||||
"q_spread": 26.823280334472656,
|
||||
"rvr": 1.0456656217575073
|
||||
}
|
||||
],
|
||||
"lr": 0.00009999999747378752,
|
||||
"n_allowed_actions": 9,
|
||||
"n_episodes": 1000,
|
||||
"pass_early": true,
|
||||
"pass_entropy": false,
|
||||
"pass_q_spread": true,
|
||||
"pass_rvr": true,
|
||||
"phase": "E.1 Task 12",
|
||||
"pruned_actions": false,
|
||||
"q_init_norm": 17.471145629882812,
|
||||
"q_spread_ema": 26.823280334472656,
|
||||
"return_vs_random_ema": 1.0456656217575073,
|
||||
"reward_scale": 1000.0,
|
||||
"target_update_every": 10,
|
||||
"tau": 0.029999999329447746,
|
||||
"trade_rate_target": 0.07999999821186066
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
{
|
||||
"action_entropy_ema": 0.6501350402832031,
|
||||
"all_pass": false,
|
||||
"alpha_m": 0.8999999761581421,
|
||||
"c51": true,
|
||||
"c51_n_atoms": 51,
|
||||
"c51_vmax": 10.0,
|
||||
"c51_vmin": -10.0,
|
||||
"early_q_movement_ema": 0.012797871604561806,
|
||||
"eps_end": 0.05000000074505806,
|
||||
"eps_start": 0.5,
|
||||
"final_stacker_kelly_attenuation": 0.10000000149011612,
|
||||
"final_stacker_threshold": 0.3718600869178772,
|
||||
"final_trade_rate_observed_ema": 0.14652971923351288,
|
||||
"gamma": 0.9900000095367432,
|
||||
"grad_clip": 1.0,
|
||||
"horizon": 600,
|
||||
"kc_log": [
|
||||
{
|
||||
"early_mvmt": 0.0019911762792617083,
|
||||
"entropy": 1.5733345746994019,
|
||||
"episode": 50,
|
||||
"q_spread": 31.75593376159668,
|
||||
"rvr": 1.044257402420044
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0024081910960376263,
|
||||
"entropy": 1.4243084192276,
|
||||
"episode": 100,
|
||||
"q_spread": 23.955223083496094,
|
||||
"rvr": 1.0445903539657593
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0031225469429045916,
|
||||
"entropy": 1.310657262802124,
|
||||
"episode": 150,
|
||||
"q_spread": 19.488779067993164,
|
||||
"rvr": 1.0448148250579834
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0038882701192051172,
|
||||
"entropy": 1.2122981548309326,
|
||||
"episode": 200,
|
||||
"q_spread": 17.970020294189453,
|
||||
"rvr": 1.0449012517929077
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.004593650344759226,
|
||||
"entropy": 1.132895827293396,
|
||||
"episode": 250,
|
||||
"q_spread": 60.273643493652344,
|
||||
"rvr": 1.0449382066726685
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.005275565665215254,
|
||||
"entropy": 1.06606125831604,
|
||||
"episode": 300,
|
||||
"q_spread": 44.775360107421875,
|
||||
"rvr": 1.0449564456939697
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.005944964475929737,
|
||||
"entropy": 1.0079469680786133,
|
||||
"episode": 350,
|
||||
"q_spread": 35.19337463378906,
|
||||
"rvr": 1.0449739694595337
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0066252113319933414,
|
||||
"entropy": 0.9588310718536377,
|
||||
"episode": 400,
|
||||
"q_spread": 23.109146118164062,
|
||||
"rvr": 1.0449974536895752
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.007311227265745401,
|
||||
"entropy": 0.9154046773910522,
|
||||
"episode": 450,
|
||||
"q_spread": 30.995208740234375,
|
||||
"rvr": 1.0450423955917358
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.007989339530467987,
|
||||
"entropy": 0.8766676187515259,
|
||||
"episode": 500,
|
||||
"q_spread": 28.47087287902832,
|
||||
"rvr": 1.0450869798660278
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.008644208312034607,
|
||||
"entropy": 0.8420573472976685,
|
||||
"episode": 550,
|
||||
"q_spread": 20.39789390563965,
|
||||
"rvr": 1.0451369285583496
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.009266316890716553,
|
||||
"entropy": 0.8106650114059448,
|
||||
"episode": 600,
|
||||
"q_spread": 15.783662796020508,
|
||||
"rvr": 1.0452359914779663
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.009855338372290134,
|
||||
"entropy": 0.7826521992683411,
|
||||
"episode": 650,
|
||||
"q_spread": 15.634352684020996,
|
||||
"rvr": 1.0453643798828125
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.010388685390353203,
|
||||
"entropy": 0.7577194571495056,
|
||||
"episode": 700,
|
||||
"q_spread": 13.28182315826416,
|
||||
"rvr": 1.0455031394958496
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.010871796868741512,
|
||||
"entropy": 0.7353291511535645,
|
||||
"episode": 750,
|
||||
"q_spread": 13.777722358703613,
|
||||
"rvr": 1.0455645322799683
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.011310325935482979,
|
||||
"entropy": 0.714767575263977,
|
||||
"episode": 800,
|
||||
"q_spread": 49.52091979980469,
|
||||
"rvr": 1.0456281900405884
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.011722153052687645,
|
||||
"entropy": 0.696082353591919,
|
||||
"episode": 850,
|
||||
"q_spread": 25.91876983642578,
|
||||
"rvr": 1.0456620454788208
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.01210756879299879,
|
||||
"entropy": 0.6795051693916321,
|
||||
"episode": 900,
|
||||
"q_spread": 19.0417423248291,
|
||||
"rvr": 1.045680284500122
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.012464887462556362,
|
||||
"entropy": 0.6642012000083923,
|
||||
"episode": 950,
|
||||
"q_spread": 31.48348045349121,
|
||||
"rvr": 1.0456589460372925
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.012797871604561806,
|
||||
"entropy": 0.6501350402832031,
|
||||
"episode": 1000,
|
||||
"q_spread": 26.82328224182129,
|
||||
"rvr": 1.0456655025482178
|
||||
}
|
||||
],
|
||||
"lr": 0.00009999999747378752,
|
||||
"n_allowed_actions": 9,
|
||||
"n_episodes": 1000,
|
||||
"pass_early": true,
|
||||
"pass_entropy": false,
|
||||
"pass_q_spread": true,
|
||||
"pass_rvr": true,
|
||||
"phase": "E.1 Task 12",
|
||||
"pruned_actions": false,
|
||||
"q_init_norm": 17.471145629882812,
|
||||
"q_spread_ema": 26.82328224182129,
|
||||
"return_vs_random_ema": 1.0456655025482178,
|
||||
"reward_scale": 1000.0,
|
||||
"target_update_every": 10,
|
||||
"tau": 0.029999999329447746,
|
||||
"trade_rate_target": 0.07999999821186066
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
{
|
||||
"action_entropy_ema": 1.9570317268371582,
|
||||
"all_pass": true,
|
||||
"alpha_m": 0.8999999761581421,
|
||||
"early_q_movement_ema": 0.12511339783668518,
|
||||
"eps_end": 0.05000000074505806,
|
||||
"eps_start": 0.5,
|
||||
"final_stacker_kelly_attenuation": 0.10000000149011612,
|
||||
"final_stacker_threshold": 0.5,
|
||||
"final_trade_rate_observed_ema": 0.711693525314331,
|
||||
"gamma": 0.9900000095367432,
|
||||
"grad_clip": 1.0,
|
||||
"horizon": 600,
|
||||
"kc_log": [
|
||||
{
|
||||
"early_mvmt": 0.022276392206549644,
|
||||
"entropy": 2.0100932121276855,
|
||||
"episode": 100,
|
||||
"q_spread": 16.85750389099121,
|
||||
"rvr": 1.0427366495132446
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0334387831389904,
|
||||
"entropy": 2.00576114654541,
|
||||
"episode": 200,
|
||||
"q_spread": 18.60127067565918,
|
||||
"rvr": 1.0427367687225342
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.046235110610723495,
|
||||
"entropy": 2.0008304119110107,
|
||||
"episode": 300,
|
||||
"q_spread": 29.530759811401367,
|
||||
"rvr": 1.0427361726760864
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.05913778021931648,
|
||||
"entropy": 1.9954335689544678,
|
||||
"episode": 400,
|
||||
"q_spread": 17.126867294311523,
|
||||
"rvr": 1.0427309274673462
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.07171963155269623,
|
||||
"entropy": 1.9893970489501953,
|
||||
"episode": 500,
|
||||
"q_spread": 17.392959594726562,
|
||||
"rvr": 1.0427260398864746
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.08374495804309845,
|
||||
"entropy": 1.9831516742706299,
|
||||
"episode": 600,
|
||||
"q_spread": 15.180435180664062,
|
||||
"rvr": 1.0427193641662598
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0951075330376625,
|
||||
"entropy": 1.9764565229415894,
|
||||
"episode": 700,
|
||||
"q_spread": 18.398340225219727,
|
||||
"rvr": 1.042712688446045
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.10574351996183395,
|
||||
"entropy": 1.9698795080184937,
|
||||
"episode": 800,
|
||||
"q_spread": 19.092670440673828,
|
||||
"rvr": 1.0426955223083496
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.1157108023762703,
|
||||
"entropy": 1.9635075330734253,
|
||||
"episode": 900,
|
||||
"q_spread": 12.077985763549805,
|
||||
"rvr": 1.0426815748214722
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.12511339783668518,
|
||||
"entropy": 1.9570317268371582,
|
||||
"episode": 1000,
|
||||
"q_spread": 10.57870864868164,
|
||||
"rvr": 1.0426677465438843
|
||||
}
|
||||
],
|
||||
"lr": 0.00009999999747378752,
|
||||
"n_episodes": 1000,
|
||||
"pass_early": true,
|
||||
"pass_entropy": true,
|
||||
"pass_q_spread": true,
|
||||
"pass_rvr": true,
|
||||
"phase": "E.1 Task 12",
|
||||
"q_init_norm": 2.5735182762145996,
|
||||
"q_spread_ema": 10.57870864868164,
|
||||
"return_vs_random_ema": 1.0426677465438843,
|
||||
"reward_scale": 1000.0,
|
||||
"target_update_every": 10,
|
||||
"tau": 0.029999999329447746,
|
||||
"trade_rate_target": 0.07999999821186066
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
{
|
||||
"action_entropy_ema": 0.5963922142982483,
|
||||
"all_pass": false,
|
||||
"alpha_m": 0.8999999761581421,
|
||||
"early_q_movement_ema": 0.06557312607765198,
|
||||
"eps_end": 0.05000000074505806,
|
||||
"eps_start": 0.5,
|
||||
"final_stacker_kelly_attenuation": 0.10000000149011612,
|
||||
"final_stacker_threshold": 0.39542248845100403,
|
||||
"final_trade_rate_observed_ema": 0.0526169054210186,
|
||||
"gamma": 0.9900000095367432,
|
||||
"grad_clip": 1.0,
|
||||
"horizon": 600,
|
||||
"kc_log": [
|
||||
{
|
||||
"early_mvmt": 0.007667880039662123,
|
||||
"entropy": 1.2100406885147095,
|
||||
"episode": 50,
|
||||
"q_spread": 10.2034273147583,
|
||||
"rvr": 1.0386292934417725
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.010766620747745037,
|
||||
"entropy": 1.1296873092651367,
|
||||
"episode": 100,
|
||||
"q_spread": 18.91918182373047,
|
||||
"rvr": 1.0395418405532837
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.014657312072813511,
|
||||
"entropy": 1.0565096139907837,
|
||||
"episode": 150,
|
||||
"q_spread": 15.888318061828613,
|
||||
"rvr": 1.0402482748031616
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.018783867359161377,
|
||||
"entropy": 0.9925659894943237,
|
||||
"episode": 200,
|
||||
"q_spread": 15.430439949035645,
|
||||
"rvr": 1.0408954620361328
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.022793982177972794,
|
||||
"entropy": 0.9379445314407349,
|
||||
"episode": 250,
|
||||
"q_spread": 23.6329402923584,
|
||||
"rvr": 1.0414402484893799
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.026724718511104584,
|
||||
"entropy": 0.8919486403465271,
|
||||
"episode": 300,
|
||||
"q_spread": 17.598243713378906,
|
||||
"rvr": 1.0418473482131958
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.030426282435655594,
|
||||
"entropy": 0.8524131178855896,
|
||||
"episode": 350,
|
||||
"q_spread": 13.011943817138672,
|
||||
"rvr": 1.0422507524490356
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0339919850230217,
|
||||
"entropy": 0.8179648518562317,
|
||||
"episode": 400,
|
||||
"q_spread": 12.7743558883667,
|
||||
"rvr": 1.0425257682800293
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.037394702434539795,
|
||||
"entropy": 0.7875949144363403,
|
||||
"episode": 450,
|
||||
"q_spread": 24.746097564697266,
|
||||
"rvr": 1.0427244901657104
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.04064859449863434,
|
||||
"entropy": 0.7609207630157471,
|
||||
"episode": 500,
|
||||
"q_spread": 23.808012008666992,
|
||||
"rvr": 1.042922019958496
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.04376926273107529,
|
||||
"entropy": 0.737092912197113,
|
||||
"episode": 550,
|
||||
"q_spread": 51.05651092529297,
|
||||
"rvr": 1.043082356452942
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.046768978238105774,
|
||||
"entropy": 0.7154706120491028,
|
||||
"episode": 600,
|
||||
"q_spread": 29.437070846557617,
|
||||
"rvr": 1.0432312488555908
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.049585551023483276,
|
||||
"entropy": 0.6959313750267029,
|
||||
"episode": 650,
|
||||
"q_spread": 37.589622497558594,
|
||||
"rvr": 1.0434421300888062
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.05222097411751747,
|
||||
"entropy": 0.6781342625617981,
|
||||
"episode": 700,
|
||||
"q_spread": 23.144182205200195,
|
||||
"rvr": 1.0434865951538086
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.05473678559064865,
|
||||
"entropy": 0.6617897748947144,
|
||||
"episode": 750,
|
||||
"q_spread": 37.95399856567383,
|
||||
"rvr": 1.0435773134231567
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.05712851136922836,
|
||||
"entropy": 0.6466087698936462,
|
||||
"episode": 800,
|
||||
"q_spread": 27.247364044189453,
|
||||
"rvr": 1.0437748432159424
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.059404753148555756,
|
||||
"entropy": 0.6326004862785339,
|
||||
"episode": 850,
|
||||
"q_spread": 17.723329544067383,
|
||||
"rvr": 1.0438848733901978
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.06157543137669563,
|
||||
"entropy": 0.6195741295814514,
|
||||
"episode": 900,
|
||||
"q_spread": 28.05809211730957,
|
||||
"rvr": 1.0439565181732178
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.0636264905333519,
|
||||
"entropy": 0.6075642704963684,
|
||||
"episode": 950,
|
||||
"q_spread": 21.408233642578125,
|
||||
"rvr": 1.0439708232879639
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.06557312607765198,
|
||||
"entropy": 0.5963922142982483,
|
||||
"episode": 1000,
|
||||
"q_spread": 17.820077896118164,
|
||||
"rvr": 1.043927788734436
|
||||
}
|
||||
],
|
||||
"lr": 0.00009999999747378752,
|
||||
"n_allowed_actions": 4,
|
||||
"n_episodes": 1000,
|
||||
"pass_early": true,
|
||||
"pass_entropy": false,
|
||||
"pass_q_spread": true,
|
||||
"pass_rvr": true,
|
||||
"phase": "E.1 Task 12",
|
||||
"pruned_actions": true,
|
||||
"q_init_norm": 2.5735182762145996,
|
||||
"q_spread_ema": 17.820077896118164,
|
||||
"return_vs_random_ema": 1.043927788734436,
|
||||
"reward_scale": 1000.0,
|
||||
"target_update_every": 10,
|
||||
"tau": 0.029999999329447746,
|
||||
"trade_rate_target": 0.07999999821186066
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
{
|
||||
"action_entropy_ema": 0.6412715315818787,
|
||||
"all_pass": false,
|
||||
"alpha_m": 0.8999999761581421,
|
||||
"c51": true,
|
||||
"c51_n_atoms": 51,
|
||||
"c51_vmax": 10.0,
|
||||
"c51_vmin": -10.0,
|
||||
"early_q_movement_ema": 0.03639886900782585,
|
||||
"eps_end": 0.05000000074505806,
|
||||
"eps_start": 0.5,
|
||||
"final_stacker_kelly_attenuation": 0.10000000149011612,
|
||||
"final_stacker_threshold": 0.3594285249710083,
|
||||
"final_trade_rate_observed_ema": 0.13283130526542664,
|
||||
"gamma": 0.9900000095367432,
|
||||
"grad_clip": 1.0,
|
||||
"horizon": 600,
|
||||
"kc_log": [
|
||||
{
|
||||
"early_mvmt": 0.005454623606055975,
|
||||
"entropy": 1.3495063781738281,
|
||||
"episode": 50,
|
||||
"q_spread": 10.139967918395996,
|
||||
"rvr": 1.0472848415374756
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.007253357674926519,
|
||||
"entropy": 1.2438831329345703,
|
||||
"episode": 100,
|
||||
"q_spread": 19.075225830078125,
|
||||
"rvr": 1.0472843647003174
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.009519391693174839,
|
||||
"entropy": 1.156934142112732,
|
||||
"episode": 150,
|
||||
"q_spread": 18.019407272338867,
|
||||
"rvr": 1.0472824573516846
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.011883598752319813,
|
||||
"entropy": 1.0822319984436035,
|
||||
"episode": 200,
|
||||
"q_spread": 13.705775260925293,
|
||||
"rvr": 1.047277808189392
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.014179909601807594,
|
||||
"entropy": 1.0193876028060913,
|
||||
"episode": 250,
|
||||
"q_spread": 11.369466781616211,
|
||||
"rvr": 1.047271728515625
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.01631350815296173,
|
||||
"entropy": 0.9656475186347961,
|
||||
"episode": 300,
|
||||
"q_spread": 17.306245803833008,
|
||||
"rvr": 1.0472663640975952
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.018330903723835945,
|
||||
"entropy": 0.9194064736366272,
|
||||
"episode": 350,
|
||||
"q_spread": 18.86232566833496,
|
||||
"rvr": 1.0472619533538818
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.020250117406249046,
|
||||
"entropy": 0.8801246881484985,
|
||||
"episode": 400,
|
||||
"q_spread": 11.836480140686035,
|
||||
"rvr": 1.047256350517273
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.022106800228357315,
|
||||
"entropy": 0.8464117646217346,
|
||||
"episode": 450,
|
||||
"q_spread": 27.371524810791016,
|
||||
"rvr": 1.0472533702850342
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.023895522579550743,
|
||||
"entropy": 0.8163612484931946,
|
||||
"episode": 500,
|
||||
"q_spread": 17.102041244506836,
|
||||
"rvr": 1.0472468137741089
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.025634583085775375,
|
||||
"entropy": 0.7898619771003723,
|
||||
"episode": 550,
|
||||
"q_spread": 12.669693946838379,
|
||||
"rvr": 1.0472373962402344
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.027278758585453033,
|
||||
"entropy": 0.7658124566078186,
|
||||
"episode": 600,
|
||||
"q_spread": 15.247847557067871,
|
||||
"rvr": 1.0472314357757568
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.02877088077366352,
|
||||
"entropy": 0.7445480823516846,
|
||||
"episode": 650,
|
||||
"q_spread": 11.973329544067383,
|
||||
"rvr": 1.0472320318222046
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.03013652190566063,
|
||||
"entropy": 0.7258573174476624,
|
||||
"episode": 700,
|
||||
"q_spread": 9.663625717163086,
|
||||
"rvr": 1.047275185585022
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.03140057623386383,
|
||||
"entropy": 0.7090933918952942,
|
||||
"episode": 750,
|
||||
"q_spread": 8.647809982299805,
|
||||
"rvr": 1.0473036766052246
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.032570064067840576,
|
||||
"entropy": 0.6933974027633667,
|
||||
"episode": 800,
|
||||
"q_spread": 8.795377731323242,
|
||||
"rvr": 1.0472261905670166
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.03364194557070732,
|
||||
"entropy": 0.6786754131317139,
|
||||
"episode": 850,
|
||||
"q_spread": 7.188756465911865,
|
||||
"rvr": 1.047145128250122
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.034635137766599655,
|
||||
"entropy": 0.6654810309410095,
|
||||
"episode": 900,
|
||||
"q_spread": 7.986563682556152,
|
||||
"rvr": 1.0470893383026123
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.03555254638195038,
|
||||
"entropy": 0.6529881954193115,
|
||||
"episode": 950,
|
||||
"q_spread": 42684.86328125,
|
||||
"rvr": 1.0470367670059204
|
||||
},
|
||||
{
|
||||
"early_mvmt": 0.03639886900782585,
|
||||
"entropy": 0.6412715315818787,
|
||||
"episode": 1000,
|
||||
"q_spread": 16417.994140625,
|
||||
"rvr": 1.0469965934753418
|
||||
}
|
||||
],
|
||||
"lr": 0.00009999999747378752,
|
||||
"n_allowed_actions": 9,
|
||||
"n_episodes": 1000,
|
||||
"pass_early": true,
|
||||
"pass_entropy": false,
|
||||
"pass_q_spread": true,
|
||||
"pass_rvr": true,
|
||||
"phase": "E.1 Task 12",
|
||||
"pruned_actions": false,
|
||||
"q_init_norm": 17.554412841796875,
|
||||
"q_spread_ema": 16417.994140625,
|
||||
"return_vs_random_ema": 1.0469965934753418,
|
||||
"reward_scale": 1000.0,
|
||||
"target_update_every": 10,
|
||||
"tau": 0.029999999329447746,
|
||||
"trade_rate_target": 0.07999999821186066
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"ask_coeffs": [
|
||||
[
|
||||
0.2075730860233307,
|
||||
-36.41001892089844,
|
||||
0.1882239580154419,
|
||||
0.8074590563774109,
|
||||
0.21634404361248016
|
||||
],
|
||||
[
|
||||
-0.4855740964412689,
|
||||
-36.41001892089844,
|
||||
0.1882239580154419,
|
||||
0.8074590563774109,
|
||||
0.21634404361248016
|
||||
],
|
||||
[
|
||||
-0.891039252281189,
|
||||
-36.41001892089844,
|
||||
0.1882239580154419,
|
||||
0.8074590563774109,
|
||||
0.21634404361248016
|
||||
]
|
||||
],
|
||||
"bid_coeffs": [
|
||||
[
|
||||
-0.23750488460063934,
|
||||
-1.8683799505233765,
|
||||
-0.10426050424575806,
|
||||
-0.006267971359193325,
|
||||
-0.29509592056274414
|
||||
],
|
||||
[
|
||||
-0.9306520819664001,
|
||||
-1.8683799505233765,
|
||||
-0.10426050424575806,
|
||||
-0.006267971359193325,
|
||||
-0.29509592056274414
|
||||
],
|
||||
[
|
||||
-1.336117148399353,
|
||||
-1.8683799505233765,
|
||||
-0.10426050424575806,
|
||||
-0.006267971359193325,
|
||||
-0.29509592056274414
|
||||
]
|
||||
],
|
||||
"empirical_ask_l1_rate": 0.713424,
|
||||
"empirical_bid_l1_rate": 0.049668,
|
||||
"fit_iters": 1000,
|
||||
"fit_lr": 0.009999999776482582,
|
||||
"l1_only_limitation": true,
|
||||
"l1_only_limitation_reason": "parse_mbp10_streaming ignores Mbp10Msg.levels[1..10]; L2/L3 coefficients are L1 with attenuated β_0 (-ln(L+1))",
|
||||
"l2_lambda": 0.009999999776482582,
|
||||
"n_degenerate_skipped": 138941,
|
||||
"n_snapshots": 500000,
|
||||
"n_trades": 5198778,
|
||||
"phase": "E.0 Task 5",
|
||||
"snapshot_interval": 50,
|
||||
"window_seconds": 60.0
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"avg_fills_per_episode": 139.7092,
|
||||
"cost_per_contract": 0.0625,
|
||||
"fill_coeffs_path": "config/ml/alpha_fill_coeffs.json",
|
||||
"horizon": 600,
|
||||
"kill_threshold_mean_plus_2sigma": 4735.716615200314,
|
||||
"mean_reward": -5191.527750268555,
|
||||
"n_episodes": 10000,
|
||||
"n_snapshots_loaded": 500000,
|
||||
"p05_reward": -14256.82421875,
|
||||
"p25_reward": -7252.75,
|
||||
"p50_reward": -2813.9375,
|
||||
"p75_reward": -1786.6875,
|
||||
"p95_reward": -952.8125,
|
||||
"phase": "E.0 Task 7",
|
||||
"seed": 3405691582,
|
||||
"snapshot_interval": 50,
|
||||
"std_reward": 4963.622182734434,
|
||||
"trade_size_contracts": 1
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
# Example sweep grid for `fxt-backtest sweep`.
|
||||
# A1: decision_stride removed. This file previously swept stride={1,2,4,8};
|
||||
# those cell overrides are now no-ops. Repurposed as a threshold sweep example.
|
||||
#
|
||||
# Run:
|
||||
# fxt-backtest sweep --grid config/ml/sweep_decision_stride_example.yaml \
|
||||
# --out results/sweep_example
|
||||
# Produces results/sweep_example/{cell_1,...}/{summary.json,trades.csv,pnl_curve.bin}
|
||||
# plus aggregate.parquet + pareto_frontier.json at the root.
|
||||
|
||||
base:
|
||||
data: test_data/futures-baseline/ES.FUT
|
||||
n_parallel: 1
|
||||
latency_ns: 100000000 # 100 ms IBKR + Scaleway baseline
|
||||
target_annual_vol_units: 50.0
|
||||
annualisation_factor: 825.0
|
||||
max_lots: 5
|
||||
max_events: 0 # exhaust the input stream
|
||||
seed: 12648430
|
||||
# checkpoint: path/to/trained.bin # uncomment to use real weights
|
||||
|
||||
cells:
|
||||
- name: cell_1
|
||||
- name: cell_2
|
||||
- name: cell_3
|
||||
- name: cell_4
|
||||
@@ -1,163 +0,0 @@
|
||||
# P6 deployability sweep — 4 windows × 140 sim_variants (7 cost × 4 latency × 5 threshold).
|
||||
# Generated by scripts/generate_sweep_variants.py — do NOT hand-edit.
|
||||
# Re-run after threshold-tuning to refresh threshold absolute values.
|
||||
|
||||
base:
|
||||
data_template: /mnt/training-data/futures-baseline-mbp10/ES.FUT/ES.FUT_{window}.dbn.zst
|
||||
predecoded_dir: /feature-cache/predecoded
|
||||
n_parallel: 1 # batched flow overrides with variants.len()=140
|
||||
latency_ns: 200000000 # default (overridden per variant)
|
||||
target_annual_vol_units: 50.0
|
||||
annualisation_factor: 825.0
|
||||
max_lots: 5
|
||||
max_events: 0
|
||||
seed: 12648430
|
||||
# __SHA__ replaced by submitter with post-trunk-grows training short-SHA.
|
||||
checkpoint: /feature-cache/alpha-perception-runs/__SHA__/trunk_best_h1000.bin
|
||||
sim_variants:
|
||||
- { name: c0_l0_p60, threshold: 0.4, cost_per_lot_per_side: 0.0625, latency_ns: 100000000 }
|
||||
- { name: c0_l0_p70, threshold: 0.55, cost_per_lot_per_side: 0.0625, latency_ns: 100000000 }
|
||||
- { name: c0_l0_p80, threshold: 0.7, cost_per_lot_per_side: 0.0625, latency_ns: 100000000 }
|
||||
- { name: c0_l0_p90, threshold: 0.85, cost_per_lot_per_side: 0.0625, latency_ns: 100000000 }
|
||||
- { name: c0_l0_p95, threshold: 0.95, cost_per_lot_per_side: 0.0625, latency_ns: 100000000 }
|
||||
- { name: c0_l1_p60, threshold: 0.4, cost_per_lot_per_side: 0.0625, latency_ns: 200000000 }
|
||||
- { name: c0_l1_p70, threshold: 0.55, cost_per_lot_per_side: 0.0625, latency_ns: 200000000 }
|
||||
- { name: c0_l1_p80, threshold: 0.7, cost_per_lot_per_side: 0.0625, latency_ns: 200000000 }
|
||||
- { name: c0_l1_p90, threshold: 0.85, cost_per_lot_per_side: 0.0625, latency_ns: 200000000 }
|
||||
- { name: c0_l1_p95, threshold: 0.95, cost_per_lot_per_side: 0.0625, latency_ns: 200000000 }
|
||||
- { name: c0_l2_p60, threshold: 0.4, cost_per_lot_per_side: 0.0625, latency_ns: 300000000 }
|
||||
- { name: c0_l2_p70, threshold: 0.55, cost_per_lot_per_side: 0.0625, latency_ns: 300000000 }
|
||||
- { name: c0_l2_p80, threshold: 0.7, cost_per_lot_per_side: 0.0625, latency_ns: 300000000 }
|
||||
- { name: c0_l2_p90, threshold: 0.85, cost_per_lot_per_side: 0.0625, latency_ns: 300000000 }
|
||||
- { name: c0_l2_p95, threshold: 0.95, cost_per_lot_per_side: 0.0625, latency_ns: 300000000 }
|
||||
- { name: c0_l3_p60, threshold: 0.4, cost_per_lot_per_side: 0.0625, latency_ns: 400000000 }
|
||||
- { name: c0_l3_p70, threshold: 0.55, cost_per_lot_per_side: 0.0625, latency_ns: 400000000 }
|
||||
- { name: c0_l3_p80, threshold: 0.7, cost_per_lot_per_side: 0.0625, latency_ns: 400000000 }
|
||||
- { name: c0_l3_p90, threshold: 0.85, cost_per_lot_per_side: 0.0625, latency_ns: 400000000 }
|
||||
- { name: c0_l3_p95, threshold: 0.95, cost_per_lot_per_side: 0.0625, latency_ns: 400000000 }
|
||||
- { name: c1_l0_p60, threshold: 0.4, cost_per_lot_per_side: 0.125, latency_ns: 100000000 }
|
||||
- { name: c1_l0_p70, threshold: 0.55, cost_per_lot_per_side: 0.125, latency_ns: 100000000 }
|
||||
- { name: c1_l0_p80, threshold: 0.7, cost_per_lot_per_side: 0.125, latency_ns: 100000000 }
|
||||
- { name: c1_l0_p90, threshold: 0.85, cost_per_lot_per_side: 0.125, latency_ns: 100000000 }
|
||||
- { name: c1_l0_p95, threshold: 0.95, cost_per_lot_per_side: 0.125, latency_ns: 100000000 }
|
||||
- { name: c1_l1_p60, threshold: 0.4, cost_per_lot_per_side: 0.125, latency_ns: 200000000 }
|
||||
- { name: c1_l1_p70, threshold: 0.55, cost_per_lot_per_side: 0.125, latency_ns: 200000000 }
|
||||
- { name: c1_l1_p80, threshold: 0.7, cost_per_lot_per_side: 0.125, latency_ns: 200000000 }
|
||||
- { name: c1_l1_p90, threshold: 0.85, cost_per_lot_per_side: 0.125, latency_ns: 200000000 }
|
||||
- { name: c1_l1_p95, threshold: 0.95, cost_per_lot_per_side: 0.125, latency_ns: 200000000 }
|
||||
- { name: c1_l2_p60, threshold: 0.4, cost_per_lot_per_side: 0.125, latency_ns: 300000000 }
|
||||
- { name: c1_l2_p70, threshold: 0.55, cost_per_lot_per_side: 0.125, latency_ns: 300000000 }
|
||||
- { name: c1_l2_p80, threshold: 0.7, cost_per_lot_per_side: 0.125, latency_ns: 300000000 }
|
||||
- { name: c1_l2_p90, threshold: 0.85, cost_per_lot_per_side: 0.125, latency_ns: 300000000 }
|
||||
- { name: c1_l2_p95, threshold: 0.95, cost_per_lot_per_side: 0.125, latency_ns: 300000000 }
|
||||
- { name: c1_l3_p60, threshold: 0.4, cost_per_lot_per_side: 0.125, latency_ns: 400000000 }
|
||||
- { name: c1_l3_p70, threshold: 0.55, cost_per_lot_per_side: 0.125, latency_ns: 400000000 }
|
||||
- { name: c1_l3_p80, threshold: 0.7, cost_per_lot_per_side: 0.125, latency_ns: 400000000 }
|
||||
- { name: c1_l3_p90, threshold: 0.85, cost_per_lot_per_side: 0.125, latency_ns: 400000000 }
|
||||
- { name: c1_l3_p95, threshold: 0.95, cost_per_lot_per_side: 0.125, latency_ns: 400000000 }
|
||||
- { name: c2_l0_p60, threshold: 0.4, cost_per_lot_per_side: 0.1875, latency_ns: 100000000 }
|
||||
- { name: c2_l0_p70, threshold: 0.55, cost_per_lot_per_side: 0.1875, latency_ns: 100000000 }
|
||||
- { name: c2_l0_p80, threshold: 0.7, cost_per_lot_per_side: 0.1875, latency_ns: 100000000 }
|
||||
- { name: c2_l0_p90, threshold: 0.85, cost_per_lot_per_side: 0.1875, latency_ns: 100000000 }
|
||||
- { name: c2_l0_p95, threshold: 0.95, cost_per_lot_per_side: 0.1875, latency_ns: 100000000 }
|
||||
- { name: c2_l1_p60, threshold: 0.4, cost_per_lot_per_side: 0.1875, latency_ns: 200000000 }
|
||||
- { name: c2_l1_p70, threshold: 0.55, cost_per_lot_per_side: 0.1875, latency_ns: 200000000 }
|
||||
- { name: c2_l1_p80, threshold: 0.7, cost_per_lot_per_side: 0.1875, latency_ns: 200000000 }
|
||||
- { name: c2_l1_p90, threshold: 0.85, cost_per_lot_per_side: 0.1875, latency_ns: 200000000 }
|
||||
- { name: c2_l1_p95, threshold: 0.95, cost_per_lot_per_side: 0.1875, latency_ns: 200000000 }
|
||||
- { name: c2_l2_p60, threshold: 0.4, cost_per_lot_per_side: 0.1875, latency_ns: 300000000 }
|
||||
- { name: c2_l2_p70, threshold: 0.55, cost_per_lot_per_side: 0.1875, latency_ns: 300000000 }
|
||||
- { name: c2_l2_p80, threshold: 0.7, cost_per_lot_per_side: 0.1875, latency_ns: 300000000 }
|
||||
- { name: c2_l2_p90, threshold: 0.85, cost_per_lot_per_side: 0.1875, latency_ns: 300000000 }
|
||||
- { name: c2_l2_p95, threshold: 0.95, cost_per_lot_per_side: 0.1875, latency_ns: 300000000 }
|
||||
- { name: c2_l3_p60, threshold: 0.4, cost_per_lot_per_side: 0.1875, latency_ns: 400000000 }
|
||||
- { name: c2_l3_p70, threshold: 0.55, cost_per_lot_per_side: 0.1875, latency_ns: 400000000 }
|
||||
- { name: c2_l3_p80, threshold: 0.7, cost_per_lot_per_side: 0.1875, latency_ns: 400000000 }
|
||||
- { name: c2_l3_p90, threshold: 0.85, cost_per_lot_per_side: 0.1875, latency_ns: 400000000 }
|
||||
- { name: c2_l3_p95, threshold: 0.95, cost_per_lot_per_side: 0.1875, latency_ns: 400000000 }
|
||||
- { name: c3_l0_p60, threshold: 0.4, cost_per_lot_per_side: 0.25, latency_ns: 100000000 }
|
||||
- { name: c3_l0_p70, threshold: 0.55, cost_per_lot_per_side: 0.25, latency_ns: 100000000 }
|
||||
- { name: c3_l0_p80, threshold: 0.7, cost_per_lot_per_side: 0.25, latency_ns: 100000000 }
|
||||
- { name: c3_l0_p90, threshold: 0.85, cost_per_lot_per_side: 0.25, latency_ns: 100000000 }
|
||||
- { name: c3_l0_p95, threshold: 0.95, cost_per_lot_per_side: 0.25, latency_ns: 100000000 }
|
||||
- { name: c3_l1_p60, threshold: 0.4, cost_per_lot_per_side: 0.25, latency_ns: 200000000 }
|
||||
- { name: c3_l1_p70, threshold: 0.55, cost_per_lot_per_side: 0.25, latency_ns: 200000000 }
|
||||
- { name: c3_l1_p80, threshold: 0.7, cost_per_lot_per_side: 0.25, latency_ns: 200000000 }
|
||||
- { name: c3_l1_p90, threshold: 0.85, cost_per_lot_per_side: 0.25, latency_ns: 200000000 }
|
||||
- { name: c3_l1_p95, threshold: 0.95, cost_per_lot_per_side: 0.25, latency_ns: 200000000 }
|
||||
- { name: c3_l2_p60, threshold: 0.4, cost_per_lot_per_side: 0.25, latency_ns: 300000000 }
|
||||
- { name: c3_l2_p70, threshold: 0.55, cost_per_lot_per_side: 0.25, latency_ns: 300000000 }
|
||||
- { name: c3_l2_p80, threshold: 0.7, cost_per_lot_per_side: 0.25, latency_ns: 300000000 }
|
||||
- { name: c3_l2_p90, threshold: 0.85, cost_per_lot_per_side: 0.25, latency_ns: 300000000 }
|
||||
- { name: c3_l2_p95, threshold: 0.95, cost_per_lot_per_side: 0.25, latency_ns: 300000000 }
|
||||
- { name: c3_l3_p60, threshold: 0.4, cost_per_lot_per_side: 0.25, latency_ns: 400000000 }
|
||||
- { name: c3_l3_p70, threshold: 0.55, cost_per_lot_per_side: 0.25, latency_ns: 400000000 }
|
||||
- { name: c3_l3_p80, threshold: 0.7, cost_per_lot_per_side: 0.25, latency_ns: 400000000 }
|
||||
- { name: c3_l3_p90, threshold: 0.85, cost_per_lot_per_side: 0.25, latency_ns: 400000000 }
|
||||
- { name: c3_l3_p95, threshold: 0.95, cost_per_lot_per_side: 0.25, latency_ns: 400000000 }
|
||||
- { name: c4_l0_p60, threshold: 0.4, cost_per_lot_per_side: 0.375, latency_ns: 100000000 }
|
||||
- { name: c4_l0_p70, threshold: 0.55, cost_per_lot_per_side: 0.375, latency_ns: 100000000 }
|
||||
- { name: c4_l0_p80, threshold: 0.7, cost_per_lot_per_side: 0.375, latency_ns: 100000000 }
|
||||
- { name: c4_l0_p90, threshold: 0.85, cost_per_lot_per_side: 0.375, latency_ns: 100000000 }
|
||||
- { name: c4_l0_p95, threshold: 0.95, cost_per_lot_per_side: 0.375, latency_ns: 100000000 }
|
||||
- { name: c4_l1_p60, threshold: 0.4, cost_per_lot_per_side: 0.375, latency_ns: 200000000 }
|
||||
- { name: c4_l1_p70, threshold: 0.55, cost_per_lot_per_side: 0.375, latency_ns: 200000000 }
|
||||
- { name: c4_l1_p80, threshold: 0.7, cost_per_lot_per_side: 0.375, latency_ns: 200000000 }
|
||||
- { name: c4_l1_p90, threshold: 0.85, cost_per_lot_per_side: 0.375, latency_ns: 200000000 }
|
||||
- { name: c4_l1_p95, threshold: 0.95, cost_per_lot_per_side: 0.375, latency_ns: 200000000 }
|
||||
- { name: c4_l2_p60, threshold: 0.4, cost_per_lot_per_side: 0.375, latency_ns: 300000000 }
|
||||
- { name: c4_l2_p70, threshold: 0.55, cost_per_lot_per_side: 0.375, latency_ns: 300000000 }
|
||||
- { name: c4_l2_p80, threshold: 0.7, cost_per_lot_per_side: 0.375, latency_ns: 300000000 }
|
||||
- { name: c4_l2_p90, threshold: 0.85, cost_per_lot_per_side: 0.375, latency_ns: 300000000 }
|
||||
- { name: c4_l2_p95, threshold: 0.95, cost_per_lot_per_side: 0.375, latency_ns: 300000000 }
|
||||
- { name: c4_l3_p60, threshold: 0.4, cost_per_lot_per_side: 0.375, latency_ns: 400000000 }
|
||||
- { name: c4_l3_p70, threshold: 0.55, cost_per_lot_per_side: 0.375, latency_ns: 400000000 }
|
||||
- { name: c4_l3_p80, threshold: 0.7, cost_per_lot_per_side: 0.375, latency_ns: 400000000 }
|
||||
- { name: c4_l3_p90, threshold: 0.85, cost_per_lot_per_side: 0.375, latency_ns: 400000000 }
|
||||
- { name: c4_l3_p95, threshold: 0.95, cost_per_lot_per_side: 0.375, latency_ns: 400000000 }
|
||||
- { name: c5_l0_p60, threshold: 0.4, cost_per_lot_per_side: 0.5, latency_ns: 100000000 }
|
||||
- { name: c5_l0_p70, threshold: 0.55, cost_per_lot_per_side: 0.5, latency_ns: 100000000 }
|
||||
- { name: c5_l0_p80, threshold: 0.7, cost_per_lot_per_side: 0.5, latency_ns: 100000000 }
|
||||
- { name: c5_l0_p90, threshold: 0.85, cost_per_lot_per_side: 0.5, latency_ns: 100000000 }
|
||||
- { name: c5_l0_p95, threshold: 0.95, cost_per_lot_per_side: 0.5, latency_ns: 100000000 }
|
||||
- { name: c5_l1_p60, threshold: 0.4, cost_per_lot_per_side: 0.5, latency_ns: 200000000 }
|
||||
- { name: c5_l1_p70, threshold: 0.55, cost_per_lot_per_side: 0.5, latency_ns: 200000000 }
|
||||
- { name: c5_l1_p80, threshold: 0.7, cost_per_lot_per_side: 0.5, latency_ns: 200000000 }
|
||||
- { name: c5_l1_p90, threshold: 0.85, cost_per_lot_per_side: 0.5, latency_ns: 200000000 }
|
||||
- { name: c5_l1_p95, threshold: 0.95, cost_per_lot_per_side: 0.5, latency_ns: 200000000 }
|
||||
- { name: c5_l2_p60, threshold: 0.4, cost_per_lot_per_side: 0.5, latency_ns: 300000000 }
|
||||
- { name: c5_l2_p70, threshold: 0.55, cost_per_lot_per_side: 0.5, latency_ns: 300000000 }
|
||||
- { name: c5_l2_p80, threshold: 0.7, cost_per_lot_per_side: 0.5, latency_ns: 300000000 }
|
||||
- { name: c5_l2_p90, threshold: 0.85, cost_per_lot_per_side: 0.5, latency_ns: 300000000 }
|
||||
- { name: c5_l2_p95, threshold: 0.95, cost_per_lot_per_side: 0.5, latency_ns: 300000000 }
|
||||
- { name: c5_l3_p60, threshold: 0.4, cost_per_lot_per_side: 0.5, latency_ns: 400000000 }
|
||||
- { name: c5_l3_p70, threshold: 0.55, cost_per_lot_per_side: 0.5, latency_ns: 400000000 }
|
||||
- { name: c5_l3_p80, threshold: 0.7, cost_per_lot_per_side: 0.5, latency_ns: 400000000 }
|
||||
- { name: c5_l3_p90, threshold: 0.85, cost_per_lot_per_side: 0.5, latency_ns: 400000000 }
|
||||
- { name: c5_l3_p95, threshold: 0.95, cost_per_lot_per_side: 0.5, latency_ns: 400000000 }
|
||||
- { name: c6_l0_p60, threshold: 0.4, cost_per_lot_per_side: 1.0, latency_ns: 100000000 }
|
||||
- { name: c6_l0_p70, threshold: 0.55, cost_per_lot_per_side: 1.0, latency_ns: 100000000 }
|
||||
- { name: c6_l0_p80, threshold: 0.7, cost_per_lot_per_side: 1.0, latency_ns: 100000000 }
|
||||
- { name: c6_l0_p90, threshold: 0.85, cost_per_lot_per_side: 1.0, latency_ns: 100000000 }
|
||||
- { name: c6_l0_p95, threshold: 0.95, cost_per_lot_per_side: 1.0, latency_ns: 100000000 }
|
||||
- { name: c6_l1_p60, threshold: 0.4, cost_per_lot_per_side: 1.0, latency_ns: 200000000 }
|
||||
- { name: c6_l1_p70, threshold: 0.55, cost_per_lot_per_side: 1.0, latency_ns: 200000000 }
|
||||
- { name: c6_l1_p80, threshold: 0.7, cost_per_lot_per_side: 1.0, latency_ns: 200000000 }
|
||||
- { name: c6_l1_p90, threshold: 0.85, cost_per_lot_per_side: 1.0, latency_ns: 200000000 }
|
||||
- { name: c6_l1_p95, threshold: 0.95, cost_per_lot_per_side: 1.0, latency_ns: 200000000 }
|
||||
- { name: c6_l2_p60, threshold: 0.4, cost_per_lot_per_side: 1.0, latency_ns: 300000000 }
|
||||
- { name: c6_l2_p70, threshold: 0.55, cost_per_lot_per_side: 1.0, latency_ns: 300000000 }
|
||||
- { name: c6_l2_p80, threshold: 0.7, cost_per_lot_per_side: 1.0, latency_ns: 300000000 }
|
||||
- { name: c6_l2_p90, threshold: 0.85, cost_per_lot_per_side: 1.0, latency_ns: 300000000 }
|
||||
- { name: c6_l2_p95, threshold: 0.95, cost_per_lot_per_side: 1.0, latency_ns: 300000000 }
|
||||
- { name: c6_l3_p60, threshold: 0.4, cost_per_lot_per_side: 1.0, latency_ns: 400000000 }
|
||||
- { name: c6_l3_p70, threshold: 0.55, cost_per_lot_per_side: 1.0, latency_ns: 400000000 }
|
||||
- { name: c6_l3_p80, threshold: 0.7, cost_per_lot_per_side: 1.0, latency_ns: 400000000 }
|
||||
- { name: c6_l3_p90, threshold: 0.85, cost_per_lot_per_side: 1.0, latency_ns: 400000000 }
|
||||
- { name: c6_l3_p95, threshold: 0.95, cost_per_lot_per_side: 1.0, latency_ns: 400000000 }
|
||||
|
||||
cells:
|
||||
- { name: W1, window: 2025-Q2 }
|
||||
- { name: W2, window: 2025-Q3 }
|
||||
- { name: W3, window: 2025-Q4 }
|
||||
- { name: W4, window: 2026-Q1 }
|
||||
@@ -1,20 +0,0 @@
|
||||
base:
|
||||
data: /mnt/training-data/futures-baseline-mbp10/ES.FUT
|
||||
predecoded_dir: /feature-cache/predecoded
|
||||
n_parallel: 1
|
||||
latency_ns: 200000000
|
||||
target_annual_vol_units: 50.0
|
||||
annualisation_factor: 825.0
|
||||
max_lots: 5
|
||||
max_events: 2000000
|
||||
seed: 12648430
|
||||
checkpoint: /feature-cache/alpha-perception-runs/8b2ac1e57/trunk_best_h1000.bin
|
||||
min_reasonable_px: 1000.0
|
||||
max_reasonable_px: 20000.0
|
||||
delta_floor: 1.0
|
||||
per_horizon_logit_sampling_stride: 1000
|
||||
sim_variants:
|
||||
- { name: t0c1l200_perhoriz_diag, threshold: 0.0, cost_per_lot_per_side: 0.125,
|
||||
latency_ns: 200000000, max_hold_ns: 60000000000 }
|
||||
cells:
|
||||
- { name: perhoriz_diag }
|
||||
@@ -1,40 +0,0 @@
|
||||
# Realistic-anchor single-variant smoke. P6 batched flow.
|
||||
# Purpose: prove infra end-to-end AND get a useful single-cell trading
|
||||
# readout at the deployment anchor:
|
||||
# - 200ms RTT (Scaleway PAR → IBKR realistic)
|
||||
# - 1-tick cost (0.125 price units / lot / side)
|
||||
# - moderate conviction gate (0.30 — passes ~p70-80 of conviction)
|
||||
#
|
||||
# Note: data is a DIRECTORY (loader iterates all files in it, not a
|
||||
# single-file path). data_template + cell.window is NOT yet wired to a
|
||||
# per-quarter loader filter — that's a TODO for the walk-forward
|
||||
# deployability sweep. For the smoke we cap max_events to keep wall
|
||||
# under ~20 min while producing a real ~500k-decision sample.
|
||||
|
||||
base:
|
||||
data: /mnt/training-data/futures-baseline-mbp10/ES.FUT
|
||||
predecoded_dir: /feature-cache/predecoded
|
||||
n_parallel: 1 # overridden by sim_variants.len()
|
||||
# A1: decision_stride removed. Decisions now fire every event (event-rate
|
||||
# CRT). With ~2M events, expect ~2M decisions (minus seq_len warmup).
|
||||
latency_ns: 200000000 # realistic anchor (overridden per variant)
|
||||
target_annual_vol_units: 50.0
|
||||
annualisation_factor: 825.0
|
||||
max_lots: 5
|
||||
max_events: 2000000 # ~2M events, ~10k decisions, ~16 min wall
|
||||
seed: 12648430
|
||||
# dbd500ecf is the post-trunk-grows training checkpoint.
|
||||
checkpoint: /feature-cache/alpha-perception-runs/dbd500ecf/trunk_best_h1000.bin
|
||||
min_reasonable_px: 1000.0 # ES futures: reject sub-$1000 (catches $48-100 outliers + negatives)
|
||||
max_reasonable_px: 20000.0 # ES futures: reject super-$20000 (catches $53k outliers; > ES all-time-high ~$7000)
|
||||
delta_floor: 1.0 # CRT.1 C1.3: skip seeding when |target − effective| < 1 lot
|
||||
sim_variants:
|
||||
# ISV stop controller smoke: t=0, 1-tick cost, 200ms latency.
|
||||
# Validates end-to-end: ISV stop controller (Tasks 2-9) produces
|
||||
# n_trades > 0 without any stopgap. max_hold_ns=60s caps the
|
||||
# long tail observed in smoke gp74n (263985s pathological hold).
|
||||
- { name: t0c1l200_isv_stops, threshold: 0.0, cost_per_lot_per_side: 0.125,
|
||||
latency_ns: 200000000, max_hold_ns: 60000000000 }
|
||||
|
||||
cells:
|
||||
- { name: smoke }
|
||||
@@ -1,35 +0,0 @@
|
||||
# Anti-calibration validation smoke (post D1 signed-EMA, post-front-month-filter).
|
||||
# Loads trunk_best_h1000.bin from alpha-perception-jvv7d Smoke 1 — the FIRST clean
|
||||
# (front-month-filtered) checkpoint. auc_h1000=0.5757 best at epoch 3. The prior
|
||||
# 2efedcd6b checkpoint's 0.7137 AUC was an artifact of multi-instrument $5000 ΔP
|
||||
# contamination (see commit 78a9e0835); jvv7d's 0.5757 is the real learnable
|
||||
# signal at h=1000 on clean ES.FUT front-month data (commit 20aa345a7).
|
||||
#
|
||||
# Primary gate: outcome_by_entry_conv table must NOT show monotonically
|
||||
# anti-calibrated PnL (high conviction should be >= mid conviction PnL).
|
||||
# Prior baseline 8b2ac1e57 showed -15.6 → -145.2 PnL ramp across conviction
|
||||
# deciles — D1's signed-EMA fix should collapse this.
|
||||
#
|
||||
# Secondary diagnostics: CRT.diag per-horizon mean_run_len for h10/h100/
|
||||
# h1000 (informational; differentiation thesis no longer architectural goal).
|
||||
|
||||
base:
|
||||
data: /mnt/training-data/futures-baseline-mbp10/ES.FUT
|
||||
predecoded_dir: /feature-cache/predecoded
|
||||
n_parallel: 1
|
||||
latency_ns: 200000000
|
||||
target_annual_vol_units: 50.0
|
||||
annualisation_factor: 825.0
|
||||
max_lots: 5
|
||||
max_events: 2000000
|
||||
seed: 12648430
|
||||
checkpoint: /feature-cache/alpha-perception-runs/20aa345a7/trunk_best_h1000.bin
|
||||
min_reasonable_px: 1000.0
|
||||
max_reasonable_px: 20000.0
|
||||
delta_floor: 1.0
|
||||
sim_variants:
|
||||
- { name: t0c1l200_perhoriz_cfc, threshold: 0.0, cost_per_lot_per_side: 0.125,
|
||||
latency_ns: 200000000, max_hold_ns: 60000000000 }
|
||||
|
||||
cells:
|
||||
- { name: perhoriz_smoke }
|
||||
@@ -1,36 +0,0 @@
|
||||
# P6 batched flow — threshold pre-registration on W0 only.
|
||||
# Single-cell, 8 sim variants (one per threshold) sharing one forward pass
|
||||
# on the validation window. Selects the production threshold value by
|
||||
# maximising in-sample Sharpe; selected value is persisted to
|
||||
# config/ml/v2_prod_thresholds.json and committed before the deployability
|
||||
# sweep runs.
|
||||
|
||||
base:
|
||||
# P6 batched flow: data_template + cell.window interpolation produces the per-cell data path.
|
||||
data_template: /mnt/training-data/futures-baseline-mbp10/ES.FUT/ES.FUT_{window}.dbn.zst
|
||||
predecoded_dir: /feature-cache/predecoded
|
||||
n_parallel: 1 # legacy default; overridden by variants.len() per cell
|
||||
latency_ns: 200000000 # anchor (Scaleway PAR → IBKR realistic RTT)
|
||||
target_annual_vol_units: 50.0
|
||||
annualisation_factor: 825.0
|
||||
max_lots: 5
|
||||
max_events: 0 # exhaust loader (full quarter)
|
||||
seed: 12648430
|
||||
# __SHA__ is replaced by argo-lob-sweep.sh / the operator at submission time
|
||||
# with the post-trunk-grows training short-SHA (9 chars).
|
||||
checkpoint: /feature-cache/alpha-perception-runs/__SHA__/trunk_best_h1000.bin
|
||||
# 8 thresholds spanning p60-p95 (5pt steps). Cost held at the realistic
|
||||
# anchor (1 tick = 0.125 price units) so threshold-tuning Sharpe reflects
|
||||
# what the deployability sweep will see at that cost.
|
||||
sim_variants:
|
||||
- { name: t60, threshold: 0.60, cost_per_lot_per_side: 0.125 }
|
||||
- { name: t65, threshold: 0.65, cost_per_lot_per_side: 0.125 }
|
||||
- { name: t70, threshold: 0.70, cost_per_lot_per_side: 0.125 }
|
||||
- { name: t75, threshold: 0.75, cost_per_lot_per_side: 0.125 }
|
||||
- { name: t80, threshold: 0.80, cost_per_lot_per_side: 0.125 }
|
||||
- { name: t85, threshold: 0.85, cost_per_lot_per_side: 0.125 }
|
||||
- { name: t90, threshold: 0.90, cost_per_lot_per_side: 0.125 }
|
||||
- { name: t95, threshold: 0.95, cost_per_lot_per_side: 0.125 }
|
||||
|
||||
cells:
|
||||
- { name: W0, window: 2025-Q1 }
|
||||
@@ -75,10 +75,8 @@ c51_alpha_max = [0.3, 0.9]
|
||||
her_ratio = [0.0, 0.8]
|
||||
|
||||
# Composite reward weights
|
||||
# `w_dd` removed (Class A audit-fix Batch 4-A, 2026-05-08) atomically with the
|
||||
# legacy compute_drawdown_penalty path. SP15's λ_dd (ISV slot 420) is the
|
||||
# producer-driven replacement and is not exposed as a hyperopt search dim.
|
||||
w_pnl = [0.0, 1.0]
|
||||
w_dd = [0.0, 5.0]
|
||||
w_idle = [0.0, 0.1]
|
||||
dd_threshold = [0.005, 0.03] # HFT: tight drawdown tolerance (0.5%-3%)
|
||||
loss_aversion = [1.0, 1.0]
|
||||
@@ -123,7 +121,7 @@ max_trace_length = 7
|
||||
hindsight_fraction = 0.1
|
||||
hindsight_lookahead = 10
|
||||
w_pnl = 0.3
|
||||
# `w_dd` removed (Class A audit-fix Batch 4-A, 2026-05-08) atomically with legacy compute_drawdown_penalty.
|
||||
w_dd = 1.0
|
||||
w_idle = 0.01
|
||||
dd_threshold = 0.01
|
||||
loss_aversion = 1.0
|
||||
@@ -157,7 +155,7 @@ td_lambda = 0.9
|
||||
hindsight_fraction = 0.1
|
||||
hindsight_lookahead = 10
|
||||
w_pnl = 0.3
|
||||
# `w_dd` removed (Class A audit-fix Batch 4-A, 2026-05-08) atomically with legacy compute_drawdown_penalty.
|
||||
w_dd = 1.0
|
||||
w_idle = 0.01
|
||||
dd_threshold = 0.01
|
||||
loss_aversion = 1.0
|
||||
|
||||
@@ -122,7 +122,7 @@ hindsight_fraction = 0.1
|
||||
hindsight_lookahead = 10
|
||||
loss_aversion = 1.0
|
||||
q_gap_threshold = 0.1
|
||||
# `w_dd` removed (Class A audit-fix Batch 4-A, 2026-05-08) atomically with legacy compute_drawdown_penalty.
|
||||
w_dd = 1.0
|
||||
dd_threshold = 0.02
|
||||
cea_weight = 0.3
|
||||
b3_size = 3
|
||||
|
||||
@@ -13,11 +13,7 @@
|
||||
|
||||
[training]
|
||||
epochs = 200
|
||||
# batch_size: removed 2026-05-14 — sourced from `GpuProfile.training.batch_size`
|
||||
# (config/gpu/*.toml: H100=8192, L40S=4096, A100=2048, RTX3050=64). Hard-coded
|
||||
# 16384 here was H100-tuned and caused OOM on L40S after the 2026-05-14
|
||||
# argo gpu-pool default flip from H100 → L40S (workflow train-ft8ph: next_states
|
||||
# alloc 4 GiB collision at fold 0 from compounded H100-sized buffers).
|
||||
batch_size = 16384
|
||||
learning_rate = 1e-5
|
||||
gamma = 0.99
|
||||
weight_decay = 0.0001
|
||||
@@ -31,7 +27,7 @@ lr_min = 1e-6
|
||||
max_bars = 0
|
||||
# Data source: "ohlcv" (1-min candles) or "mbp10" (imbalance bars from MBP-10 order book)
|
||||
data_source = "mbp10"
|
||||
imbalance_bar_threshold = 20.0
|
||||
imbalance_bar_threshold = 0.5
|
||||
imbalance_bar_ewma_alpha = 0.1
|
||||
# OFI feature enrichment from MBP-10 + trades data
|
||||
# Paths match Argo PVC mount at /data. CLI --mbp10-data-dir overrides if needed.
|
||||
@@ -56,9 +52,7 @@ q_gap_threshold = 0.05
|
||||
noise_sigma = 0.1
|
||||
|
||||
[replay_buffer]
|
||||
# buffer_size: removed 2026-05-14 — sourced from `GpuProfile.training.buffer_size`
|
||||
# (config/gpu/*.toml: H100=500K, L40S=300K, A100=200K, RTX3050=5K). Hard-coded
|
||||
# 500_000 here was H100-tuned. See [training].batch_size comment.
|
||||
buffer_size = 500000
|
||||
min_replay_size = 1000
|
||||
per_alpha = 0.3 # was 0.6. Lower = more uniform. Dense micro-rewards have tiny TD-errors, high alpha ignores them.
|
||||
per_beta_start = 0.4
|
||||
@@ -70,12 +64,7 @@ patience = 20
|
||||
min_epochs_before_stopping = 80
|
||||
|
||||
[experience]
|
||||
# gpu_n_episodes: removed 2026-05-14 — sourced from `GpuProfile.experience.
|
||||
# gpu_n_episodes` (config/gpu/*.toml: H100=4096, L40S=2048, A100=Some(?),
|
||||
# RTX3050=Some(?)). Hard-coded 4096 here was H100-tuned; on L40S the
|
||||
# `build_next_states_f32` rollout-step alloc = n_episodes × 2 × timesteps ×
|
||||
# state_dim × 4 = 4 GiB collides with the rest of the collector VRAM by the
|
||||
# rollout phase (workflow train-ft8ph repro). See [training].batch_size comment.
|
||||
gpu_n_episodes = 4096
|
||||
initial_capital = 35000.0
|
||||
tx_cost_multiplier = 0.18 # bps: IBKR ES RT = $4.50/contract ($0.85 comm + $1.38 exch + $0.02 reg × 2 sides). At ES=$5100: 0.18 bps × 5100 × 0.0001 = 0.09 pts = $4.50
|
||||
|
||||
@@ -160,7 +149,7 @@ hindsight_fraction = 0.1
|
||||
hindsight_lookahead = 10
|
||||
loss_aversion = 1.0
|
||||
q_gap_threshold = 0.1
|
||||
# `w_dd` removed (Class A audit-fix Batch 4-A, 2026-05-08) atomically with legacy compute_drawdown_penalty.
|
||||
w_dd = 1.0
|
||||
dd_threshold = 0.02
|
||||
cea_weight = 0.3
|
||||
# Dense micro-reward component weights (tunable via hyperopt)
|
||||
|
||||
@@ -140,7 +140,7 @@ hindsight_fraction = 0.1
|
||||
hindsight_lookahead = 10
|
||||
loss_aversion = 1.0
|
||||
q_gap_threshold = 0.1
|
||||
# `w_dd` removed (Class A audit-fix Batch 4-A, 2026-05-08) atomically with legacy compute_drawdown_penalty.
|
||||
w_dd = 1.0
|
||||
dd_threshold = 0.02
|
||||
cea_weight = 0.3
|
||||
b3_size = 3
|
||||
|
||||
@@ -699,19 +699,25 @@ impl DbnParser {
|
||||
if let RecordRefEnum::Mbp10(mbp10) = record_enum {
|
||||
update_count += 1;
|
||||
|
||||
// MBP-10 messages carry the FULL post-update top-10 book
|
||||
// in `mbp10.levels`; `apply_mbp10_record` overlays all 10
|
||||
// levels (incl. L0, the inside quote) and bumps
|
||||
// trade_count on Trade actions. See its doc comment.
|
||||
// MBP-10 messages are single-level updates, not full 10-level snapshots
|
||||
// We need to aggregate them into full order book snapshots
|
||||
let is_bid = mbp10.side == b'B' as i8;
|
||||
let action = OrderBookAction::from(mbp10.action as u8);
|
||||
apply_mbp10_record(
|
||||
&mut current_snapshot,
|
||||
&mbp10.levels,
|
||||
|
||||
// For simplicity, store all updates in level 0
|
||||
// A full implementation would maintain proper level ordering
|
||||
current_snapshot.update_level(
|
||||
0, // Level index
|
||||
action,
|
||||
mbp10.hd.ts_event,
|
||||
mbp10.sequence,
|
||||
mbp10.price,
|
||||
mbp10.size,
|
||||
1, // Order count (not available in MBP-10 single update)
|
||||
is_bid,
|
||||
);
|
||||
|
||||
current_snapshot.timestamp = mbp10.hd.ts_event;
|
||||
current_snapshot.sequence = mbp10.sequence;
|
||||
|
||||
// Create snapshot periodically to reduce memory
|
||||
if update_count % SNAPSHOT_INTERVAL == 0 {
|
||||
snapshots.push(current_snapshot.clone());
|
||||
@@ -761,10 +767,6 @@ impl DbnParser {
|
||||
///
|
||||
/// * `path` - Path to the .dbn or .dbn.zst file
|
||||
/// * `snapshot_interval` - Create a snapshot every N raw updates (e.g. 100)
|
||||
/// * `filter` - Instrument selection strategy. See [`InstrumentFilter`] for variants.
|
||||
/// ES.FUT files from databento are parent-symbol expansions and contain ~14 distinct
|
||||
/// instrument_ids per file (one per contract month). Without filtering, K-window
|
||||
/// labels span contract boundaries and produce $5000+ ΔP artifacts.
|
||||
/// * `callback` - Called with each aggregated snapshot (by reference, not cloned)
|
||||
///
|
||||
/// # Returns
|
||||
@@ -774,36 +776,16 @@ impl DbnParser {
|
||||
&self,
|
||||
path: P,
|
||||
snapshot_interval: usize,
|
||||
filter: InstrumentFilter,
|
||||
mut callback: F,
|
||||
) -> Result<usize>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
F: FnMut(&Mbp10Snapshot),
|
||||
{
|
||||
let path = path.as_ref();
|
||||
|
||||
// FrontMonth is two-pass: pass 1 detects the dominant id and validates
|
||||
// it resolves to an ES contract via SymbolMapping records; pass 2
|
||||
// streams emission. Resolve to Id(winning_id) so the hot loop below
|
||||
// stays branch-free.
|
||||
let resolved_filter = match filter {
|
||||
InstrumentFilter::FrontMonth => {
|
||||
let winning_id = self.detect_front_month_id(path)?;
|
||||
InstrumentFilter::Id(winning_id)
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
|
||||
let id_filter: Option<u32> = match resolved_filter {
|
||||
InstrumentFilter::All => None,
|
||||
InstrumentFilter::Id(id) => Some(id),
|
||||
InstrumentFilter::FrontMonth => unreachable!("resolved above"),
|
||||
};
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
let path = path.as_ref();
|
||||
let file = File::open(path)?;
|
||||
let is_zstd = path.to_string_lossy().ends_with(".dbn.zst");
|
||||
let reader: Box<dyn std::io::Read> = if is_zstd {
|
||||
@@ -828,11 +810,6 @@ impl DbnParser {
|
||||
let mut current_snapshot = Mbp10Snapshot::empty(symbol);
|
||||
let mut update_count: usize = 0;
|
||||
let mut snapshot_count: usize = 0;
|
||||
// Diagnostic counters: how many records matched vs. were skipped by
|
||||
// the instrument_id filter. Logged at end-of-parse so multi-instrument
|
||||
// DBN files are visible in training-job stdout.
|
||||
let mut kept: u64 = 0;
|
||||
let mut skipped: u64 = 0;
|
||||
|
||||
loop {
|
||||
match decoder.decode_record_ref() {
|
||||
@@ -842,32 +819,23 @@ impl DbnParser {
|
||||
})?;
|
||||
|
||||
if let RecordRefEnum::Mbp10(mbp10) = record_enum {
|
||||
if let Some(filter_id) = id_filter {
|
||||
if mbp10.hd.instrument_id != filter_id {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
kept += 1;
|
||||
update_count += 1;
|
||||
|
||||
// The dbn `Mbp10Msg` carries the full 10-level
|
||||
// post-update book in `levels: [BidAskPair; 10]`
|
||||
// (index 0 = inside quote). `apply_mbp10_record` overlays
|
||||
// ALL levels — including L0 — using the existing 1e9
|
||||
// fixed-point scale convention, and bumps trade_count on
|
||||
// Trade actions. Writing the single update event into L0
|
||||
// (the previous behavior) corrupted the inside quote:
|
||||
// ~6.4% crossed books, ~10% wide-L0 spikes.
|
||||
let is_bid = mbp10.side == b'B' as i8;
|
||||
let action = OrderBookAction::from(mbp10.action as u8);
|
||||
apply_mbp10_record(
|
||||
&mut current_snapshot,
|
||||
&mbp10.levels,
|
||||
|
||||
current_snapshot.update_level(
|
||||
0,
|
||||
action,
|
||||
mbp10.hd.ts_event,
|
||||
mbp10.sequence,
|
||||
mbp10.price,
|
||||
mbp10.size,
|
||||
1,
|
||||
is_bid,
|
||||
);
|
||||
|
||||
current_snapshot.timestamp = mbp10.hd.ts_event;
|
||||
current_snapshot.sequence = mbp10.sequence;
|
||||
|
||||
if update_count % snapshot_interval == 0 {
|
||||
callback(¤t_snapshot);
|
||||
snapshot_count += 1;
|
||||
@@ -890,154 +858,8 @@ impl DbnParser {
|
||||
snapshot_count += 1;
|
||||
}
|
||||
|
||||
if let Some(filter_id) = id_filter {
|
||||
tracing::info!(
|
||||
kept,
|
||||
skipped,
|
||||
filter = filter_id,
|
||||
path = %path.display(),
|
||||
"instrument filter applied"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(snapshot_count)
|
||||
}
|
||||
|
||||
/// First pass for [`InstrumentFilter::FrontMonth`]: stream-decodes the DBN
|
||||
/// file once to identify the most-frequent `instrument_id` (the front-month
|
||||
/// contract has the bulk of trading volume) and validates that its
|
||||
/// resolved symbol matches an ES futures contract pattern using
|
||||
/// SymbolMapping records.
|
||||
///
|
||||
/// Returns the winning instrument_id on success. Errors when:
|
||||
/// - The file contains no MBP-10 records.
|
||||
/// - The dominant id cannot be resolved to a symbol via SymbolMapping
|
||||
/// records (suggests a malformed or non-databento DBN file).
|
||||
/// - The resolved symbol does not match the ES contract pattern
|
||||
/// (suggests a calendar spread or non-ES instrument is dominating —
|
||||
/// loud failure so the caller surfaces the data anomaly).
|
||||
fn detect_front_month_id(&self, path: &Path) -> Result<u32> {
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
let file = File::open(path)?;
|
||||
let is_zstd = path.to_string_lossy().ends_with(".dbn.zst");
|
||||
let reader: Box<dyn std::io::Read> = if is_zstd {
|
||||
Box::new(zstd::Decoder::new(BufReader::new(file)).map_err(|e| {
|
||||
DataError::InvalidFormat(format!("Failed to create zstd decoder: {}", e))
|
||||
})?)
|
||||
} else {
|
||||
Box::new(BufReader::new(file))
|
||||
};
|
||||
|
||||
let mut decoder = DbnDecoder::new(reader).map_err(|e| {
|
||||
DataError::InvalidFormat(format!("Failed to create DBN decoder: {}", e))
|
||||
})?;
|
||||
|
||||
let mut counts: HashMap<u32, u64> = HashMap::new();
|
||||
let mut id_to_symbol: HashMap<u32, String> = HashMap::new();
|
||||
|
||||
loop {
|
||||
match decoder.decode_record_ref() {
|
||||
Ok(Some(record)) => {
|
||||
let record_enum = record.as_enum().map_err(|e| {
|
||||
DataError::InvalidFormat(format!("Failed to convert record: {}", e))
|
||||
})?;
|
||||
match record_enum {
|
||||
RecordRefEnum::Mbp10(mbp10) => {
|
||||
*counts.entry(mbp10.hd.instrument_id).or_insert(0) += 1;
|
||||
}
|
||||
RecordRefEnum::SymbolMapping(sym) => {
|
||||
if let Ok(out) = sym.stype_out_symbol() {
|
||||
id_to_symbol
|
||||
.insert(sym.hd.instrument_id, out.to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
return Err(DataError::InvalidFormat(format!(
|
||||
"front-month detect: decode failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (winner_id, winner_count) = counts
|
||||
.iter()
|
||||
.max_by_key(|(_, c)| *c)
|
||||
.map(|(id, c)| (*id, *c))
|
||||
.ok_or_else(|| {
|
||||
DataError::InvalidFormat(format!(
|
||||
"front-month detect: no MBP-10 records in {}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
let total: u64 = counts.values().sum();
|
||||
|
||||
// Soft-validate via SymbolMapping records when present. Databento
|
||||
// historical bulk files often emit mappings only in the metadata
|
||||
// header (date-range form) and skip in-stream SymbolMappingMsg
|
||||
// entirely; in that case we trust the dominant-id (volume leader =
|
||||
// front-month) and proceed. When the in-stream symbol IS present,
|
||||
// we check it against the ES contract regex and warn (not fail) on
|
||||
// mismatch so calendar-spread anomalies surface in logs without
|
||||
// blocking training.
|
||||
let winner_symbol = id_to_symbol.get(&winner_id).cloned();
|
||||
let es_re = regex::Regex::new(r"^ES[FGHJKMNQUVXZ]\d{1,2}$")
|
||||
.expect("static ES futures regex compiles");
|
||||
let symbol_status: &str = match &winner_symbol {
|
||||
Some(sym) if es_re.is_match(sym) => "es-contract",
|
||||
Some(_) => "symbol-mismatch",
|
||||
None => "no-streaming-mapping",
|
||||
};
|
||||
if symbol_status == "symbol-mismatch" {
|
||||
warn!(
|
||||
instrument_id = winner_id,
|
||||
symbol = %winner_symbol.as_deref().unwrap_or(""),
|
||||
path = %path.display(),
|
||||
"front-month dominant id resolves to a non-ES symbol via SymbolMapping; \
|
||||
proceeding anyway (calendar spread or unexpected contract?)"
|
||||
);
|
||||
}
|
||||
|
||||
info!(
|
||||
instrument_id = winner_id,
|
||||
symbol = %winner_symbol.as_deref().unwrap_or("(metadata-only)"),
|
||||
symbol_status,
|
||||
count = winner_count,
|
||||
total = total,
|
||||
distinct_ids = counts.len(),
|
||||
path = %path.display(),
|
||||
"front-month detected"
|
||||
);
|
||||
|
||||
Ok(winner_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Strategy for selecting which `instrument_id` records to keep when streaming
|
||||
/// MBP-10 from a DBN file. ES.FUT parent-expanded files contain ~14 distinct
|
||||
/// contract months in one stream; without filtering, downstream K-window
|
||||
/// labels span contract boundaries and produce ΔP artifacts at the rolls.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InstrumentFilter {
|
||||
/// Emit all MBP-10 records regardless of `instrument_id`. Preserves the
|
||||
/// pre-refactor behavior for callers that need multi-instrument streams
|
||||
/// (e.g., DQN training that doesn't compute multi-step labels).
|
||||
All,
|
||||
/// Emit only records where `record.hd.instrument_id == id`. Fails silently
|
||||
/// for files where the configured id never appears (caller must validate).
|
||||
Id(u32),
|
||||
/// Two-pass: detect the most-frequent `instrument_id` in the file (volume
|
||||
/// leader = front-month for ES.FUT), validate it resolves to a valid ES
|
||||
/// contract via SymbolMapping records, then emit only its records.
|
||||
/// Self-tuning across quarterly contract rolls.
|
||||
FrontMonth,
|
||||
}
|
||||
|
||||
/// Processed message types from DBN parsing
|
||||
@@ -1245,43 +1067,6 @@ impl DbnParserMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Overlay an MBP-10 record's authoritative post-update book onto `snapshot`.
|
||||
///
|
||||
/// MBP-10 messages carry the full top-10 book in `levels` (index 0 = inside
|
||||
/// quote). We copy **all** levels including L0. A prior version wrote only the
|
||||
/// single update event's price into L0 via `update_level(0, …)` and then copied
|
||||
/// `levels[1..]`, which left L0 holding the lone event price (typically one side
|
||||
/// only) instead of the real inside quote → crossed books and wide-L0 spikes.
|
||||
///
|
||||
/// `trade_count` is bumped on Trade-action records, preserving the downstream
|
||||
/// signal (encoder feature `[17] = log1p(trade_count)` and the inter-snapshot
|
||||
/// trade delta in the ml-alpha / ml-features loaders). A trade does not alter
|
||||
/// the book structure itself, so only the counter moves.
|
||||
fn apply_mbp10_record(
|
||||
snapshot: &mut Mbp10Snapshot,
|
||||
levels: &[dbn::BidAskPair],
|
||||
action: OrderBookAction,
|
||||
ts_event: u64,
|
||||
sequence: u32,
|
||||
) {
|
||||
let max_lvl = levels.len().min(snapshot.levels.len());
|
||||
for lvl in 0..max_lvl {
|
||||
snapshot.levels[lvl].bid_px = levels[lvl].bid_px;
|
||||
snapshot.levels[lvl].bid_sz = levels[lvl].bid_sz;
|
||||
snapshot.levels[lvl].bid_ct = levels[lvl].bid_ct;
|
||||
snapshot.levels[lvl].ask_px = levels[lvl].ask_px;
|
||||
snapshot.levels[lvl].ask_sz = levels[lvl].ask_sz;
|
||||
snapshot.levels[lvl].ask_ct = levels[lvl].ask_ct;
|
||||
}
|
||||
|
||||
if action == OrderBookAction::Trade {
|
||||
snapshot.trade_count += 1;
|
||||
}
|
||||
|
||||
snapshot.timestamp = ts_event;
|
||||
snapshot.sequence = sequence;
|
||||
}
|
||||
|
||||
/// Snapshot of DBN parser metrics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DbnParserMetricsSnapshot {
|
||||
@@ -1341,92 +1126,6 @@ mod tests {
|
||||
assert_eq!(parser.get_symbol(999), "UNKNOWN_999");
|
||||
}
|
||||
|
||||
/// Regression: the MBP-10 decoder must populate L0 from the authoritative
|
||||
/// `mbp10.levels[0]` (the inside quote), NOT from the lone update event.
|
||||
/// Writing the event into L0 produced crossed/half-empty inside quotes
|
||||
/// (root cause of ~6.4% crossed books + ~10% wide-L0 spikes).
|
||||
#[test]
|
||||
fn apply_mbp10_record_fills_inside_quote_without_crossing() {
|
||||
use dbn::BidAskPair as DbnBidAskPair;
|
||||
|
||||
// Authoritative post-update book: L0 = normal uncrossed inside quote.
|
||||
let mut levels: [DbnBidAskPair; 10] = std::array::from_fn(|_| DbnBidAskPair {
|
||||
bid_px: 0,
|
||||
ask_px: 0,
|
||||
bid_sz: 0,
|
||||
ask_sz: 0,
|
||||
bid_ct: 0,
|
||||
ask_ct: 0,
|
||||
});
|
||||
levels[0] = DbnBidAskPair {
|
||||
bid_px: 5_000_000_000_000, // 5000.00 (×1e9)
|
||||
ask_px: 5_000_250_000_000, // 5000.25 (×1e9)
|
||||
bid_sz: 10,
|
||||
ask_sz: 12,
|
||||
bid_ct: 1,
|
||||
ask_ct: 1,
|
||||
};
|
||||
levels[1] = DbnBidAskPair {
|
||||
bid_px: 4_999_750_000_000,
|
||||
ask_px: 5_000_500_000_000,
|
||||
bid_sz: 7,
|
||||
ask_sz: 9,
|
||||
bid_ct: 1,
|
||||
ask_ct: 1,
|
||||
};
|
||||
|
||||
let mut snap = Mbp10Snapshot::empty("ES.FUT".to_string());
|
||||
|
||||
// A bid-side ADD event priced ABOVE the real ask — the classic
|
||||
// corruptor. Pre-fix code wrote this into L0 and copied only levels[1..].
|
||||
apply_mbp10_record(
|
||||
&mut snap,
|
||||
&levels,
|
||||
OrderBookAction::Add,
|
||||
1_700_000_000_000_000_000,
|
||||
42,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
snap.levels[0].bid_px, 5_000_000_000_000,
|
||||
"L0 bid must be the real inside bid, not the event price"
|
||||
);
|
||||
assert_eq!(
|
||||
snap.levels[0].ask_px, 5_000_250_000_000,
|
||||
"L0 ask must be the real inside ask (event never set the ask side)"
|
||||
);
|
||||
assert!(
|
||||
snap.levels[0].bid_px < snap.levels[0].ask_px,
|
||||
"inside quote must not be crossed"
|
||||
);
|
||||
assert_eq!(snap.levels[1].ask_px, 5_000_500_000_000, "L1 still copied");
|
||||
assert_eq!(snap.timestamp, 1_700_000_000_000_000_000);
|
||||
assert_eq!(snap.sequence, 42);
|
||||
}
|
||||
|
||||
/// `trade_count` is a live model feature; only Trade-action records bump it.
|
||||
#[test]
|
||||
fn apply_mbp10_record_counts_only_trade_actions() {
|
||||
use dbn::BidAskPair as DbnBidAskPair;
|
||||
|
||||
let levels: [DbnBidAskPair; 10] = std::array::from_fn(|_| DbnBidAskPair {
|
||||
bid_px: 5_000_000_000_000,
|
||||
ask_px: 5_000_250_000_000,
|
||||
bid_sz: 1,
|
||||
ask_sz: 1,
|
||||
bid_ct: 1,
|
||||
ask_ct: 1,
|
||||
});
|
||||
let mut snap = Mbp10Snapshot::empty("ES.FUT".to_string());
|
||||
|
||||
apply_mbp10_record(&mut snap, &levels, OrderBookAction::Add, 1, 1);
|
||||
assert_eq!(snap.trade_count, 0, "non-trade action must not bump count");
|
||||
|
||||
apply_mbp10_record(&mut snap, &levels, OrderBookAction::Trade, 2, 2);
|
||||
apply_mbp10_record(&mut snap, &levels, OrderBookAction::Trade, 3, 3);
|
||||
assert_eq!(snap.trade_count, 2, "Trade actions must increment trade_count");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_price_scaling() {
|
||||
let parser = DbnParser::new().unwrap();
|
||||
|
||||
@@ -27,17 +27,17 @@ pub struct BidAskPair {
|
||||
impl BidAskPair {
|
||||
/// Convert fixed-point price to f64
|
||||
///
|
||||
/// DBN prices are stored as i64 with 1e-9 nanoprice scaling (the DBN standard).
|
||||
/// Prior /1e12 was test-data calibration that under-scaled production prices by
|
||||
/// 1000× (ES at 5500 → 5.5). Confirmed via cluster smoke v74v4 2026-05-20 —
|
||||
/// trade records showed entry_px=5.24 instead of expected ~5240 ES index points.
|
||||
/// DBN prices are stored as i64 with different scaling depending on instrument
|
||||
/// For simplicity, we use the standard 1e-9 scaling used in DBN OHLCV
|
||||
pub fn price_to_f64(fixed: i64) -> f64 {
|
||||
fixed as f64 / 1e9
|
||||
// DBN test data uses 1e12 scaling (150000000000000 = 150.0)
|
||||
// This matches the test expectations
|
||||
fixed as f64 / 1e12
|
||||
}
|
||||
|
||||
/// Convert f64 price to fixed-point
|
||||
pub fn price_from_f64(price: f64) -> i64 {
|
||||
(price * 1e9) as i64
|
||||
(price * 1e12) as i64
|
||||
}
|
||||
|
||||
/// Get bid price as f64
|
||||
@@ -312,7 +312,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_price_conversion() {
|
||||
let fixed = 150_000_000_000i64; // 150.0 at DBN 1e-9 nanoprice scaling
|
||||
let fixed = 150000000000000; // 150.0 * 1e9
|
||||
let price = BidAskPair::price_to_f64(fixed);
|
||||
assert!((price - 150.0).abs() < 0.001);
|
||||
|
||||
@@ -330,10 +330,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_snapshot_vwap() {
|
||||
let levels = vec![BidAskPair {
|
||||
bid_px: 100_000_000_000i64, // 100.0 at DBN 1e-9 nanoprice scaling
|
||||
bid_px: 100000000000000, // 100.0
|
||||
bid_sz: 100,
|
||||
bid_ct: 5,
|
||||
ask_px: 101_000_000_000i64, // 101.0 at DBN 1e-9 nanoprice scaling
|
||||
ask_px: 101000000000000, // 101.0
|
||||
ask_sz: 200,
|
||||
ask_ct: 6,
|
||||
}];
|
||||
|
||||
@@ -73,18 +73,18 @@ async fn test_mbp10_snapshot_creation() -> Result<()> {
|
||||
// Create test BidAskPair data
|
||||
let levels = vec![
|
||||
BidAskPair {
|
||||
bid_px: 150_000_000_000, // 150.000000 (scaled by 1e9)
|
||||
bid_px: 150000000000000, // 150.000000 (scaled by 1e9)
|
||||
bid_sz: 100,
|
||||
bid_ct: 5,
|
||||
ask_px: 150_010_000_000, // 150.010000
|
||||
ask_px: 150010000000000, // 150.010000
|
||||
ask_sz: 120,
|
||||
ask_ct: 6,
|
||||
},
|
||||
BidAskPair {
|
||||
bid_px: 149_990_000_000,
|
||||
bid_px: 149990000000000,
|
||||
bid_sz: 200,
|
||||
bid_ct: 8,
|
||||
ask_px: 150_020_000_000,
|
||||
ask_px: 150020000000000,
|
||||
ask_sz: 180,
|
||||
ask_ct: 7,
|
||||
},
|
||||
@@ -110,10 +110,10 @@ async fn test_mbp10_snapshot_creation() -> Result<()> {
|
||||
#[test]
|
||||
fn test_mbp10_price_conversion() {
|
||||
let pair = BidAskPair {
|
||||
bid_px: 150_000_000_000, // 150.000000 * 1e9
|
||||
bid_px: 150000000000000, // 150.000000 * 1e9
|
||||
bid_sz: 100,
|
||||
bid_ct: 5,
|
||||
ask_px: 150_010_000_000, // 150.010000 * 1e9
|
||||
ask_px: 150010000000000, // 150.010000 * 1e9
|
||||
ask_sz: 120,
|
||||
ask_ct: 6,
|
||||
};
|
||||
@@ -130,18 +130,18 @@ fn test_mbp10_price_conversion() {
|
||||
fn test_mbp10_best_bid_ask() {
|
||||
let levels = vec![
|
||||
BidAskPair {
|
||||
bid_px: 150_000_000_000,
|
||||
bid_px: 150000000000000,
|
||||
bid_sz: 100,
|
||||
bid_ct: 5,
|
||||
ask_px: 150_010_000_000,
|
||||
ask_px: 150010000000000,
|
||||
ask_sz: 120,
|
||||
ask_ct: 6,
|
||||
},
|
||||
BidAskPair {
|
||||
bid_px: 149_990_000_000, // Lower bid
|
||||
bid_px: 149990000000000, // Lower bid
|
||||
bid_sz: 200,
|
||||
bid_ct: 8,
|
||||
ask_px: 150_020_000_000, // Higher ask
|
||||
ask_px: 150020000000000, // Higher ask
|
||||
ask_sz: 180,
|
||||
ask_ct: 7,
|
||||
},
|
||||
@@ -158,10 +158,10 @@ fn test_mbp10_best_bid_ask() {
|
||||
#[test]
|
||||
fn test_mbp10_mid_price() {
|
||||
let levels = vec![BidAskPair {
|
||||
bid_px: 150_000_000_000,
|
||||
bid_px: 150000000000000,
|
||||
bid_sz: 100,
|
||||
bid_ct: 5,
|
||||
ask_px: 150_010_000_000,
|
||||
ask_px: 150010000000000,
|
||||
ask_sz: 120,
|
||||
ask_ct: 6,
|
||||
}];
|
||||
@@ -176,10 +176,10 @@ fn test_mbp10_mid_price() {
|
||||
#[test]
|
||||
fn test_mbp10_spread() {
|
||||
let levels = vec![BidAskPair {
|
||||
bid_px: 150_000_000_000,
|
||||
bid_px: 150000000000000,
|
||||
bid_sz: 100,
|
||||
bid_ct: 5,
|
||||
ask_px: 150_010_000_000,
|
||||
ask_px: 150010000000000,
|
||||
ask_sz: 120,
|
||||
ask_ct: 6,
|
||||
}];
|
||||
@@ -195,18 +195,18 @@ fn test_mbp10_spread() {
|
||||
fn test_mbp10_total_volumes() {
|
||||
let levels = vec![
|
||||
BidAskPair {
|
||||
bid_px: 150_000_000_000,
|
||||
bid_px: 150000000000000,
|
||||
bid_sz: 100,
|
||||
bid_ct: 5,
|
||||
ask_px: 150_010_000_000,
|
||||
ask_px: 150010000000000,
|
||||
ask_sz: 120,
|
||||
ask_ct: 6,
|
||||
},
|
||||
BidAskPair {
|
||||
bid_px: 149_990_000_000,
|
||||
bid_px: 149990000000000,
|
||||
bid_sz: 200,
|
||||
bid_ct: 8,
|
||||
ask_px: 150_020_000_000,
|
||||
ask_px: 150020000000000,
|
||||
ask_sz: 180,
|
||||
ask_ct: 7,
|
||||
},
|
||||
@@ -222,10 +222,10 @@ fn test_mbp10_total_volumes() {
|
||||
#[test]
|
||||
fn test_mbp10_volume_imbalance() {
|
||||
let levels = vec![BidAskPair {
|
||||
bid_px: 150_000_000_000,
|
||||
bid_px: 150000000000000,
|
||||
bid_sz: 200, // More bid volume
|
||||
bid_ct: 5,
|
||||
ask_px: 150_010_000_000,
|
||||
ask_px: 150010000000000,
|
||||
ask_sz: 100,
|
||||
ask_ct: 6,
|
||||
}];
|
||||
@@ -242,26 +242,26 @@ fn test_mbp10_volume_imbalance() {
|
||||
fn test_mbp10_depth() {
|
||||
let levels = vec![
|
||||
BidAskPair {
|
||||
bid_px: 150_000_000_000,
|
||||
bid_px: 150000000000000,
|
||||
bid_sz: 100,
|
||||
bid_ct: 5,
|
||||
ask_px: 150_010_000_000,
|
||||
ask_px: 150010000000000,
|
||||
ask_sz: 120,
|
||||
ask_ct: 6,
|
||||
},
|
||||
BidAskPair {
|
||||
bid_px: 149_990_000_000,
|
||||
bid_px: 149990000000000,
|
||||
bid_sz: 200,
|
||||
bid_ct: 8,
|
||||
ask_px: 150_020_000_000,
|
||||
ask_px: 150020000000000,
|
||||
ask_sz: 180,
|
||||
ask_ct: 7,
|
||||
},
|
||||
BidAskPair {
|
||||
bid_px: 149_980_000_000,
|
||||
bid_px: 149980000000000,
|
||||
bid_sz: 150,
|
||||
bid_ct: 4,
|
||||
ask_px: 150_030_000_000,
|
||||
ask_px: 150030000000000,
|
||||
ask_sz: 160,
|
||||
ask_ct: 5,
|
||||
},
|
||||
@@ -307,7 +307,7 @@ fn test_mbp10_snapshot_aggregation() {
|
||||
snapshot.update_level(
|
||||
0,
|
||||
OrderBookAction::Add,
|
||||
150_000_000_000,
|
||||
150000000000000,
|
||||
100,
|
||||
5,
|
||||
true, // is_bid
|
||||
@@ -317,7 +317,7 @@ fn test_mbp10_snapshot_aggregation() {
|
||||
snapshot.update_level(
|
||||
0,
|
||||
OrderBookAction::Add,
|
||||
150_010_000_000,
|
||||
150010000000000,
|
||||
120,
|
||||
6,
|
||||
false, // is_ask
|
||||
@@ -335,10 +335,10 @@ fn test_mbp10_parsing_performance() {
|
||||
|
||||
// Create 1000 test snapshots
|
||||
let test_level = BidAskPair {
|
||||
bid_px: 150_000_000_000,
|
||||
bid_px: 150000000000000,
|
||||
bid_sz: 100,
|
||||
bid_ct: 5,
|
||||
ask_px: 150_010_000_000,
|
||||
ask_px: 150010000000000,
|
||||
ask_sz: 120,
|
||||
ask_ct: 6,
|
||||
};
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
[package]
|
||||
name = "ml-alpha"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "CfC perception + multi-horizon alpha heads (Phase A foundation for CfC+PPO greenfield)"
|
||||
publish = false
|
||||
|
||||
# Phase A scope: snapshot-level CfC trunk + 5 multi-horizon BCE heads,
|
||||
# pre-compiled CUDA cubins, GPU-resident weights. The hard validation gate
|
||||
# (CfC vs Mamba2 AUC at every horizon) sits at the end of Phase A. Phase B
|
||||
# (PPO) extends this crate; live integration lives in trading_agent_service.
|
||||
|
||||
[features]
|
||||
default = ["cuda"]
|
||||
cuda = []
|
||||
kernel-step-trace = []
|
||||
|
||||
[dependencies]
|
||||
ml-core = { workspace = true }
|
||||
# NOTE: cannot depend on `ml` (would cycle — ml depends on ml-alpha for the
|
||||
# mamba2_block gate reference). MappedF32Buffer is duplicated locally in
|
||||
# `src/pinned_mem.rs`; eventually we should move mapped-pinned to ml-core.
|
||||
ml-features = { workspace = true }
|
||||
data = { workspace = true }
|
||||
cudarc = { version = "0.19", default-features = false, features = ["driver", "cublas", "dynamic-linking", "std", "cuda-version-from-build-system", "f16"] }
|
||||
|
||||
# Standard async + error + logging plumbing
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "fs", "io-util"] }
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
bincode.workspace = true
|
||||
thiserror.workspace = true
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
|
||||
# Arrow IPC for fxcache reading (gate reference path).
|
||||
arrow = { workspace = true, features = ["ipc"] }
|
||||
|
||||
# Deterministic RNG for weight init, shuffling.
|
||||
rand = { workspace = true }
|
||||
rand_chacha = "0.3"
|
||||
|
||||
# Phase A data path: mmap predecoded MBP-10 sidecars.
|
||||
memmap2 = { workspace = true }
|
||||
|
||||
# SPEED-A (2026-05-22): parallel per-file load + label generation in
|
||||
# MultiHorizonLoader::new gives ~4-8x speedup on typical 4-8 core hosts.
|
||||
rayon = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
approx = { workspace = true }
|
||||
# Phase R6: convergence-gate fixtures + alpha_rl_train CLI need
|
||||
# LobSimCuda from ml-backtesting. Dev-dep avoids the cycle —
|
||||
# ml-backtesting → ml-alpha is the production direction; this
|
||||
# dev-only edge only loads when building ml-alpha's own tests/examples.
|
||||
ml-backtesting = { path = "../ml-backtesting" }
|
||||
@@ -1,303 +0,0 @@
|
||||
//! Pre-compile all ml-alpha CUDA kernels into arch-specific cubins.
|
||||
//!
|
||||
//! Per `feedback_no_nvrtc.md`: no runtime kernel compilation.
|
||||
//! Per `pearl_build_rs_rerun_if_env_changed.md`: every `std::env::var`
|
||||
//! is paired with `cargo:rerun-if-env-changed`.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
const KERNELS: &[&str] = &[
|
||||
"mamba2_alpha_kernel", // Mamba2 SSM scan kernel (used by PerceptionTrainer's encoder prefix)
|
||||
"snap_feature_assemble",
|
||||
"cfc_step",
|
||||
"multi_horizon_heads",
|
||||
"projection",
|
||||
"bce_loss_multi_horizon", // Kendall σ-weighted multi-horizon BCE (axis A)
|
||||
"adamw_step",
|
||||
"grad_norm",
|
||||
"horizon_lambda", // ISV-driven per-horizon gradient scaler (EMA + lambda)
|
||||
"layer_norm", // Phase 1: trunk pre-CfC normalisation
|
||||
"variable_selection", // Phase 2D: TFT-style per-feature gating
|
||||
"attention_pool", // Phase 3: single-Q learned content summary at CfC k=0
|
||||
"reduce_axis0", // Phase B: cross-batch param-grad reducer
|
||||
"output_smoothness", // CRT.train: per-horizon adjacent-position prob-jitter penalty
|
||||
"smoothness_lambda_controller", // CRT.train: ISV-driven λ controller anchored on h30 jitter
|
||||
"gpu_log_ring", // GPU diagnostic log ring — tick kernel + log_record helper
|
||||
"bucket_transition_kernels", // Per-horizon CfC Phase 1→2 transition: tau_sort, bucket_assign, bucket_iqr, channels_in_bucket, heads_compact, zero_off_bucket (ALPHA fix 2026-05-21)
|
||||
"cfc_step_per_branch", // Per-horizon CfC Phase 2: fused per-(batch, branch) fwd + bwd over [25,25,25,25,28] buckets
|
||||
"heads_block_diagonal_fwd", // Per-horizon CfC Phase 2: heads w_skip projection with compact ragged storage (640→128 floats)
|
||||
"aux_trunk", // SDD-3 Layer B3: smaller single-bucket CfC trunk (AUX_HIDDEN=64) for outcome-supervision (D-labels)
|
||||
"aux_heads", // SDD-3 Layer B4: per-direction linear regression heads on AuxTrunk output (long + short, N_AUX_HORIZONS each)
|
||||
"aux_loss", // SDD-3 Layer B4: Huber loss + grad for aux trade-outcome regression targets (NaN-masked)
|
||||
"aux_vec_add", // SDD-3 Layer B5: element-wise dst += src for aux→encoder gradient accumulation (lifted stop-grad)
|
||||
"dqn_distributional_q", // RL Phase C: C51 distributional Q-head fwd + Bellman TD bwd for integrated RL trainer
|
||||
"rl_gamma_controller", // RL Phase C: ISV controller emitting γ to ISV[RL_GAMMA_INDEX=400]
|
||||
"rl_target_tau_controller", // RL Phase C: ISV controller emitting τ to ISV[RL_TARGET_TAU_INDEX=401]
|
||||
"ppo_clipped_surrogate", // RL Phase D: PPO clipped-surrogate + entropy bonus + value MSE fwd/bwd
|
||||
"ppo_loss_reduce_b", // F4.1 (2026-05-31): [B] → [1] block tree-reduce mean of per-batch PPO loss + entropy loss (replaces atomicAdd; eliminates l_pi step-count staleness bug — see ppo_clipped_surrogate.cu header)
|
||||
"rl_ppo_clip_controller", // RL Phase D: ISV controller emitting ε to ISV[RL_PPO_CLIP_INDEX=402]
|
||||
"rl_entropy_coef_controller", // RL Phase D: ISV controller emitting entropy bonus weight to ISV[RL_ENTROPY_COEF_INDEX=403]
|
||||
"v_head_fwd_bwd", // RL Phase E.2: scalar V(s) head fwd + MSE bwd (linear layer; per-batch scratch + reduce_axis0)
|
||||
"grad_h_accumulate", // RL Phase E.2: element-wise grad_h_encoder += λ × grad_h_head accumulator (one head at a time, serialised by stream)
|
||||
"bellman_target_projection", // RL Phase E.2-DEFER: C51 categorical projection of Bellman target Z(s_{t+1}, a*) onto the discrete support, reads γ from ISV[400]; replaces host-side build_synthetic_bellman_target stand-in
|
||||
"rl_bellman_target_saturation_reduce", // B-9 (2026-06-01): cross-batch tree-reduce of per-batch saturation tallies from bellman_target_projection / _fused; emits ISV slots 726-729 (top/bot rate + max/min pre-proj)
|
||||
"rl_q_distribution_stats", // B-10 (2026-06-01) G1: Q-distribution informativeness; two entry points `rl_q_distribution_per_batch` (per-block per-action softmax + entropy + E_Q over online Q logits) + `rl_q_distribution_reduce` (cross-batch tree-reduce → ISV slots 730/731/732)
|
||||
"rl_ppo_diagnostic_stats_reduce",// B-10 (2026-06-01) G3+G4: cross-batch tree-reduce of per-batch advantage + ratio + surrogate scratches written by ppo_clipped_surrogate.cu; reads var_pre_norm slot 612 for σ_used; emits ISV slots 735-742
|
||||
"rl_lr_controller", // RL Phase E.2-DEFER: per-head learning-rate ISV emitter — bootstraps ISV[412..417] with 1e-3 (BCE/Q/π/V/aux); replaces hardcoded PHASE_E2_DEFAULT_LR
|
||||
"rl_rollout_steps_controller", // RL Phase E.3b: rollout-buffer-length ISV emitter — emits ISV[RL_N_ROLLOUT_STEPS_INDEX=404] from var(advantage)/|mean A| EMA; bootstraps 2048
|
||||
"rl_per_alpha_controller", // RL Phase E.3b: PER priority-exponent ISV emitter — emits ISV[RL_PER_ALPHA_INDEX=405] from TD-error kurtosis EMA; bootstraps 0.6
|
||||
"rl_reward_scale_controller", // RL Phase R1 (rebuild): reward-standardisation scale ISV emitter — emits ISV[RL_REWARD_SCALE_INDEX=406] from mean |realized_pnl_usd| EMA; bootstraps 1.0
|
||||
"ema_update_on_done", // RL Phase R3: generic done-gated EMA producer (slot-parameterised) for closed-trade-magnitude EMAs (mean_abs_pnl, q_divergence, td_kurtosis)
|
||||
"ema_update_per_step", // RL Phase R3: generic per-step EMA producer (slot-parameterised) for continuous EMAs (kl_pi, entropy_observed, advantage_var_ratio, trade_duration)
|
||||
"compute_advantage_return", // RL Phase R3: element-wise A_t = r + γ(1-done)·V(s_{t+1}) − V(s_t), R_t = r + γ(1-done)·V(s_{t+1}); reads γ from ISV[400]
|
||||
"gae_backward_sweep", // Phase 1B-A (2026-06-02): GAE backward sweep over [B × T] rollout trajectories — A_t = δ_t + γλ·A_{t+1}·non_terminal, returns_t = A_t + V_t; deterministic single-thread-per-batch sequential sweep; replaces compute_advantage_return when wired in Phase 1B-B+
|
||||
"rollout_pack", // Phase 1B-B (2026-06-02): per-step f32→u8 dones packer; writes `dones_f32 [B]` into rollout buffer's `dones_u8_bt [B × T]` at offset `b * T + t_cursor`; single-thread-per-batch, deterministic
|
||||
"rl_action_kernel", // RL Phase R4: Thompson sampler over C51 atoms; one block per batch, N_ACTIONS threads; per-batch xorshift32 PRNG state; replaces host Thompson loop per feedback_cpu_is_read_only
|
||||
"argmax_expected_q", // RL Phase R4: argmax over expected Q per action; Bellman-target argmax (Double-DQN); pairs with rl_action_kernel per pearl_thompson_for_distributional_action_selection
|
||||
"log_pi_at_action", // RL Phase R4: per-batch log π(action_b) via log-softmax + lookup; PPO importance-ratio path
|
||||
"dqn_target_soft_update", // RL Phase R5: element-wise target[i] = (1-τ)·target + τ·current, reads τ from ISV[401]; closes defect #4 (no target-net soft update in flawed branch)
|
||||
"extract_realized_pnl_delta", // RL Phase R6: GPU-pure reward + done extraction from device Pos array; replaces host read_pos loop per feedback_cpu_is_read_only
|
||||
"apply_reward_scale", // RL Phase R6: element-wise rewards *= ISV[RL_REWARD_SCALE_INDEX=406]; closes the F.3b host roundtrip
|
||||
"actions_to_market_targets", // RL Phase R6: 9-action grid → LobSim market_targets[B*2] on device; replaces host submit_market loop per feedback_cpu_is_read_only
|
||||
"abs_copy", // RL Phase R7a: element-wise dst[b] = fabsf(src[b]); feeds |reward| into ema_update_on_done for the MEAN_ABS_PNL_EMA slot
|
||||
"rl_var_over_abs_mean_streaming",// EMA-streaming var/|mean| (folds across STEPS, fixes b_size=1 → ISV[421] feeding rl_rollout_steps
|
||||
"rl_kurtosis_streaming", // EMA-streaming kurtosis M4/M2² (folds across STEPS, fixes b_size=1) → ISV[422] feeding rl_per_alpha
|
||||
"rl_kl_approx_b", // Schulman-style KL = mean(log π_old − log π_new) → kl_pi_ema (ISV[419]) feeding rl_ppo_clip
|
||||
"rl_l2_diff_norm", // ‖W_online − W_target‖₂ → q_divergence_ema (ISV[418]) feeding rl_target_tau
|
||||
"rl_step_counter_update", // per-batch trade-duration counter + done-gated emit → mean_trade_duration_ema (ISV[417]) feeding rl_gamma
|
||||
"rl_l2_norm", // ‖x‖₂ single-buffer reduction → q/pi/v grad-norm EMAs (ISV[424..427]) feeding rl_lr_controller
|
||||
// (entropy_observed_ema, ISV[420], feeds rl_entropy_coef directly via ema_update_per_step's internal mean reduce on entropy_d — no separate kernel needed.)
|
||||
"rl_ppo_ratio_clamp_controller", // RL R9: PPO ratio clamp ceiling at ISV[440], anchored on ε at ISV[402] — bounds catastrophic unclipped-branch surrogate
|
||||
"ppo_log_ratio_abs_max_b", // RL R9 diag: per-batch max|log π_new − log π_old| → ISV[441]; surfaces ratio-clamp activity in diag JSONL
|
||||
"rl_streaming_clamp_init", // RL R9: device-side seeder for streaming-kernel output clamp ceilings (ISV[447], ISV[448]) — no HtoD
|
||||
"rl_isv_write", // RL R9: generic single-slot ISV seeder for tunable design constants — no HtoD
|
||||
"rl_q_pi_agree_b", // RL R9 audit: per-batch fraction (argmax Q == argmax π) → ISV[407] EMA; wires previously-dead slot
|
||||
"rl_pi_action_kernel", // audit Option B: π drives action selection via multinomial sampling from softmax(pi_logits); Q becomes pure critic
|
||||
"rl_reward_clamp_controller", // audit 2026-05-24: adaptive [-LOSS, +WIN] clamp from positive-tail EMA; replaces static [-3, +1] that crushed winning-trade signal in rmgm5
|
||||
"rl_atom_support_update", // audit 2026-05-24 followup: refreshes atom_supports_d from ISV V_MIN/V_MAX so C51 atom span adapts with reward clamp (Q learning was capped at V_MAX=1.0)
|
||||
"rl_kl_reference_grad",
|
||||
"rl_q_pi_distill_grad", // audit 2026-05-24 vj5f6 followup: KL(softmax(E_Q/τ) || π_new) gradient ADDED to pi_grad_logits — couples Q's improved C51 calibration to action selection (was decoupled per Option B)
|
||||
"rl_pi_grad_blend", // Phase 3D-C (2026-06-03): PPO surrogate × Q-distill blend operator (replaces zero-fill before Q-distill +=); restores direct PG signal to π
|
||||
"action_entropy_per_step", // POST-gate action entropy EMA for SAC α/τ co-tuning
|
||||
"rl_q_distill_lambda_controller",// audit 2026-05-24 rljzl followup: adaptive λ_distill via Schulman bounded step on KL_EMA vs target
|
||||
"rl_unit_state_update", // SP20 P1+P5 audit fix: per-unit trade state machine — detects open/close/reverse transitions, sets up unit slot 0 entry+trail
|
||||
"rl_trail_mutate", // SP20 P1+P5 audit fix (a7/a8 dead): TrailTighten/TrailLoosen mutate unit_trail_distance bounded MIN/MAX, symmetric reciprocal adjust
|
||||
"rl_trail_stop_check", // SP20 P1+P5 audit fix: per-unit trail breach check; OVERRIDE action to FlatFromLong/Short on breach (close routes through existing flat plumbing)
|
||||
"rl_frd_fwd", // SP20 P3: Forward-Return-Distribution head fwd — 2-layer MLP [HIDDEN_DIM → FRD_HIDDEN_DIM → FRD_N_HORIZONS × FRD_N_ATOMS]; ReLU hidden cached for bwd; softmax + CE happen in bwd
|
||||
"rl_frd_softmax_ce_grad", // SP20 P3 F.3a: per-(batch, horizon) softmax + CE loss + dL/dlogits; 1 block per (b, h), 21 threads; label = -1 sentinel masks the row
|
||||
"rl_frd_layer2_bwd", // SP20 P3 F.3b: FRD head layer-2 backward — dW2 (per-batch scratch), db2 (per-batch scratch), dhidden (per-batch overwrite); 1 block per batch, 64 threads
|
||||
"rl_frd_layer1_bwd", // SP20 P3 F.3c: FRD head layer-1 backward — dW1 (per-batch scratch), db1 (per-batch scratch), dh_t (per-batch overwrite); applies ReLU mask via cached post-ReLU hidden; 1 block per batch, 128 threads
|
||||
"rl_position_heat_check", // SP20 P6: position heat cap — force-flat when |position_lots| exceeds ISV-driven max; last defense before actions_to_market_targets
|
||||
"rl_confidence_gate", // P8: override opening actions to Hold when C51 distributional Q is insufficiently confident (LCB < threshold)
|
||||
"rl_frd_gate", // P9: override opening actions to Hold when FRD head predicts insufficient favorable probability mass
|
||||
"rl_recent_outcome_update", // P10: per-batch signed outcome EMA for anti-martingale sizing
|
||||
"rl_trade_context_update", // P1: per-batch trade-arc features (time_in_trade, unrealized_R, pos_mag, entry_dist)
|
||||
"rl_multires_features_update", // P0: per-batch multi-resolution streaming features (3 horizons × 4 features)
|
||||
"rl_encoder_context_broadcast", // P2: broadcast per-batch context (16 dims) into encoder input [B,K,56] at cols 40-55
|
||||
"rl_gate_threshold_controller", // adaptive gate thresholds from trade frequency (dones EMA)
|
||||
"rl_reward_shaping", // surfer-philosophy: entry cost + short-hold penalty + hold bonus
|
||||
"rl_min_hold_check", // hard minimum hold time — override close actions to Hold before N steps
|
||||
"rl_asymmetric_trail_decay", // auto-tighten losers, auto-widen winners — structural P&L asymmetry
|
||||
"rl_session_risk_check", // session-level loss limit circuit breaker
|
||||
"rl_iqn_forward", // IQN distributional Q-head: quantile embedding + action-value projection; complementary to C51
|
||||
"rl_iqn_loss", // IQN quantile Huber loss: ρ_τ(δ) = |τ - 1(δ<0)| × Huber(δ, κ=1.0); forward + backward
|
||||
"rl_iqn_backward", // IQN backward through forward pass: grad_output → grad_w_out/b_out/w_embed/b_embed per-batch scratch
|
||||
"rl_dueling_q_forward", // Phase 4 (2026-05-30): Independent dueling Q head forward — V[B] + A[B,N] + composed_Q[B,N] = V + A − mean_a A; parallel to C51/IQN, zero shared state per spec 2026-05-30-phase4-independent-dueling-head-design.md
|
||||
"rl_dueling_q_bellman_target", // Phase 4: argmax over target composed_Q + Bellman target = r + γ^n × (1-done) × max_Q
|
||||
"rl_dueling_q_loss_and_grad", // Phase 4: Huber loss on (target − online_composed_Q[taken]) + grad_composed
|
||||
"rl_dueling_q_decompose_and_bwd", // Phase 4: decompose grad_composed → grad_V + grad_A via mean-subtraction Jacobian + per-batch weight gradients
|
||||
"rl_v_blend", // Phase 4.4 (2026-05-30): elementwise V_used = α V_scalar + (1−α) V_dq, α from ISV
|
||||
"rl_v_blend_alpha_controller", // Phase 4.4: ISV-adaptive Schulman-bounded controller on α from observed |V_dq − V_scalar| / |V_scalar| tracking ratio
|
||||
"rl_advantage_normalize", // Phase 4.5 (2026-05-30): per-batch advantage normalization (A − mean)/std with ε² variance floor — standard PPO practice, self-adaptive (no tuned params)
|
||||
"rl_signal_variance_update", // Adaptive controller floors (2026-05-30): Welford online variance for controller input EMAs; replaces hardcoded NOISE_FLOOR_FRAC constants in 8 controllers — sample_var = M² / (count-1); see spec 2026-05-30-adaptive-controller-floor-design
|
||||
"rl_win_rate_ema_update", // Adaptive risk management (2026-05-30): Layer 4 (Kelly) input — observed win_rate EMA from per-batch trade outcomes
|
||||
"rl_avg_win_loss_ema_update", // Adaptive risk management (2026-05-30): Layer 4 (Kelly) input — avg_win/avg_loss USD EMAs from per-batch closed-trade pnl
|
||||
"rl_inventory_variance_update", // Adaptive risk management (2026-05-30): Layer 3 (inventory β) input — |net_position| variance EMA across batches
|
||||
"rl_cmdp_constraints_check", // Adaptive risk management (2026-05-30): Layer 1 hard gates — session DD + cooldown + consecutive-loss tracking; writes override flags to ISV
|
||||
"rl_iqn_action_tau_controller", // Adaptive risk management (2026-05-30): Layer 2 — adapts IQN action-selection quantile τ from session drawdown signal
|
||||
"rl_inventory_beta_controller", // Adaptive risk management (2026-05-30): Layer 3 — adapts inventory penalty β from observed inventory variance vs reward magnitude
|
||||
"rl_kelly_fraction_controller", // Adaptive risk management (2026-05-30): Layer 4 — half-Kelly position-size multiplier from observed win_rate × R-multiple; warmup-gated
|
||||
"rl_surfer_scaffold_controller", // Reward-policy realignment v5 (2026-06-01): adaptive Phase 5 shaping weight ∈ [0,1] — fades surfer-bias scaffold as agent crosses break-even, re-engages on edge-decay PH alerts
|
||||
"rl_eval_warmup_decay", // v9 (2026-05-31): defensive eval-boundary calibration — overrides Kelly/τ_min/entropy_min/ε_min during warmup, linearly decays back to normal; runs every step
|
||||
"rl_regime_flat_count", // F1.2 (2026-05-31): block-reduce per-account lots[b] → flat_count single int; prereq for regime_observer (F1.3)
|
||||
"rl_regime_observer", // F1.3 (2026-05-31): unified regime state-machine — dead-zone, Welford PnL variance, tail-event, recovery_factor/eps_live
|
||||
"rl_ensemble_action_value", // C51+IQN ensemble: E_ensemble = α×E_C51 + (1-α)×E_IQN; α from ISV[544]
|
||||
"rl_noisy_linear_forward", // NoisyNet: factored noisy linear forward — y = (mu_w + sigma_w ⊙ eps_w) × x + (mu_b + sigma_b ⊙ eps_b); state-dependent exploration for C51/IQN final projection
|
||||
"rl_noisy_linear_backward", // NoisyNet: factored noisy linear backward — grad_mu_w/sigma_w/mu_b/sigma_b per-batch scratch for reduce_axis0
|
||||
"rl_sample_tau", // CUDA graph prereq: device-side xorshift32 tau ~ U(0,1) for IQN; replaces host ChaCha8 + mapped-pinned upload
|
||||
"rl_sample_noise", // CUDA graph prereq: device-side factored noise f(rand) for NoisyLinear; replaces host ChaCha8 + mapped-pinned upload
|
||||
"rl_write_u64", // CUDA graph prereq: single-thread u64 scalar write for device-resident ts_ns (graph-captured kernels read via pointer)
|
||||
"rl_fused_reward_pipeline", // P3: 7→1 fused per-batch reward extraction + shaping + EMA + outcome update
|
||||
"rl_per_push_ring", // GPU PER: coalesced n-step ring write + flush decision (Grid=B, Block=128)
|
||||
"rl_per_push_flush", // GPU PER: coordinated coalesced replay write with prefix-sum slot allocation (Grid=B, Block=128)
|
||||
"rl_per_sample", // GPU PER: stratified proportional sampling via sum-tree walk + gather
|
||||
"rl_per_update_priority", // GPU PER: write |TD|^α to tree leaves + block-wide max reduction
|
||||
"rl_per_tree_rebuild", // GPU PER: bottom-up parallel sum-tree rebuild (no atomics)
|
||||
"rl_hindsight_track", // HER Phase 1: per-step mid-price ring + peak tracking for backward hindsight
|
||||
"rl_hindsight_inject", // HER Phase 2: backward inject — synthetic replay push on done if peak >> actual
|
||||
"rl_hindsight_forward", // HER Phase 3: forward continuation — evaluates closed trades after lookahead
|
||||
"rl_increment_step", // device-resident step counter bump (ISV[548] += 1.0); graph-safe prereq — removes scalar current_step from all downstream kernel args
|
||||
"rl_fused_controllers", // fused kernel: 10 RL ISV controllers in one launch (gamma, tau, ppo_clip, entropy_coef, rollout_steps, per_alpha, reward_scale, ppo_ratio_clamp, gate_threshold, q_distill_lambda) — saves 9 kernel launch overheads (~40-80μs/step)
|
||||
"rl_popart_normalize", // PopArt: Welford-EMA reward normalization (replaces apply_reward_scale)
|
||||
"rl_popart_v_correct", // PopArt: V-head output correction after stats shift
|
||||
"rl_spectral_norm", // Spectral norm: power iteration σ_max estimate + W rescale
|
||||
"rl_spectral_decouple", // Spectral decoupling: L2 penalty on logit magnitudes
|
||||
"rl_q_bias_correction", // Q-bias: EMA of (Q_pred - actual_return) → Bellman correction
|
||||
"rl_per_branch_lr", // Per-branch LR: adaptive per-head learning rate scaling
|
||||
"rl_outcome_fwd", // Outcome aux: linear forward h_t → 3-class logits
|
||||
"rl_outcome_ce", // Outcome aux: softmax CE loss + gradient (masked by sentinel -1)
|
||||
"rl_outcome_label", // Outcome aux: assign labels from reward/done (Profit/Timeout/Loss)
|
||||
"rl_outcome_bwd", // Outcome aux: backward through linear layer → grad_W/b/h_t
|
||||
"rl_outcome_fused", // Outcome aux: fused fwd + CE + bwd — eliminates 2 global round-trips (logits, grad_logits kept in smem)
|
||||
"rl_curriculum_weights", // E8: per-segment difficulty-weighted softmax from Sharpe → PER weights
|
||||
"rl_adversarial_boost", // Adversarial: boost PER priority for negative-reward transitions
|
||||
"rl_outcome_bwd", // Outcome aux: single linear layer backward — dW (per-batch), db (per-batch), dh_t (per-batch); 1 block per batch, 128 threads
|
||||
"snapshot_aos_to_soa", // AoS→SoA scatter: one thread per snapshot reads contiguous Mbp10RawInput, writes into 10 SoA device buffers; replaces host nested loops + 10 DtoD copies
|
||||
"gpu_sample_and_gather", // GPU-resident batch sampler: random file+anchor sampling + AoS→SoA gather from pre-uploaded dataset; eliminates ALL per-step CPU data loading
|
||||
"rl_deterministic_checksum", // Determinism foundation Phase 1 (2026-06-02): provably deterministic sum-of-squares (single-block / single-thread / f64 accumulation) for per-step component checksums in diag; localizes non-determinism source. Spec: docs/superpowers/specs/2026-06-02-determinism-foundation.md §1.1
|
||||
"multi_head_policy_forward", // Phase 2A-A (2026-06-03): K-head policy logit + softmax + mixture combination; per-batch deterministic sequential mixture sum. Spec: docs/superpowers/specs/2026-06-02-multi-head-policy-with-r-multiple.md §R.6
|
||||
"multi_head_policy_gate_forward",// Phase 2A-A (2026-06-03): gating head from raw regime features [B × 6] (parallel channel bypassing VSN/Mamba2). Spec ADDENDUM 2026-06-03 §R.3 Option B
|
||||
"multi_head_policy_backward", // Phase 2A-B (2026-06-03): backward through mixture + per-head softmax + gating softmax. Two kernels: `_backward_pi` and `_backward_gate`. Per-batch grad scratch; caller reduces via reduce_axis0. Spec ADDENDUM 2026-06-03 §R.6
|
||||
"multi_head_policy_aux_prior", // Phase 2A-B (2026-06-03): per-head auxiliary KL prior gradient — additive into grad_pi_logits_k. β read from ISV slot 764. Spec ADDENDUM 2026-06-03 §R.4
|
||||
"multi_head_policy_aggregate_diag", // Phase 2A-D (2026-06-03): device-aggregated gate-and-head diagnostics. Reduces forward outputs (gate_probs, pi_probs_k) over B → 25 ISV slots (gate_probs_mean[K] / gate_argmax_mass[K] / gate_entropy_mean / per_head_entropy_mean[K]) for the multi-head specialization verdict signal. Tree-reduce, no atomicAdd.
|
||||
"rl_gate_lr_multiplier_controller", // Phase 2A-D fix B1.3 (2026-06-03): adaptive gate-LR multiplier controller. Reads gate_entropy_mean (slot 781) via EMA → escalates +0.3 %/step when entropy_ema > 0.85·log(K) (under-learning), decays −5 %/step when < 0.20·log(K) (collapse risk), leaves alone in healthy band. Single-thread launch (1,1,1)/(1,1,1).
|
||||
"rl_band_head_forward", // Phase 4-A (2026-06-03): No-transaction-band head forward — two-stage launch: (1) `rl_band_head_linear_fwd` produces [B × 2] pre-activation band logits via tree-reduce over HIDDEN_DIM; (2) `rl_band_apply_activation` applies asymmetric ±|tanh| × N_max_eff to enforce b_l ≤ 0 ≤ b_u per Davis-Norman optimality. Spec: docs/superpowers/specs/2026-06-03-no-transaction-band-architecture.md §1.1.
|
||||
"rl_band_mask", // Phase 4-A: override actions[b]→Hold when position_lots[b] ∈ [b_l, b_u]. Master-gated at slot 799 (RL_BAND_ENABLED_INDEX). Grid=(B), Block=(1) — mirrors rl_confidence_gate launch shape; runs OUTSIDE graph capture per spec §9.5.
|
||||
"rl_state_action_mask", // Phase 7b F5 (2026-06-05): state-conditional action availability mask. Sets pi_logits[b][a]=-INF for state-illegal actions BEFORE rl_pi_action_kernel samples. Master-gated at slot 823 (RL_F5_STATE_MASK_ENABLED_INDEX, bootstrap 0.0 = OFF). Grid=(B), Block=(1). Spec docs/superpowers/specs/2026-06-04-bellman-target-foundation-reshape.md §3.5.
|
||||
"rl_band_turnover_loss", // Phase 4-A: turnover regularizer (Option b) with sigmoid surrogate for differentiable boundary gradient. Emits per-batch loss + per-batch grad on (b_l, b_u). Phase 4-A wires for OBSERVABILITY only; full encoder backward chain lands in Phase 4-B.
|
||||
"rl_band_frac_aggregate", // Phase 4-B (2026-06-04): per-step `frac_not_masked` reducer (single-block tree-reduce over batch) → writes to RL_BAND_FRAC_NOT_MASKED_OBSERVED_INDEX (slot 812). Consumed by `rl_band_turnover_controller`.
|
||||
"rl_band_turnover_controller", // Phase 4-B: adaptive turnover-target controller. Single-thread launch (1×1×1); first-observation bootstrap for EMA at slot 809 + Schulman-bounded asymmetric adapter for slot 811 from slot 812 input. Spec §3.1 Option (c).
|
||||
"rl_band_head_backward", // Phase 4-B: backward through ±|tanh|×N_max activation + linear projection → per-batch grad_w/grad_b scratch + grad_h_t (OVERWRITE). Caller reduces via reduce_axis0 + folds grad_h_t into encoder via grad_h_accumulate_scaled. Spec §3.3.
|
||||
];
|
||||
|
||||
// Cache bust v31 — five new reduce / derive kernels populate the input
|
||||
// EMAs for the previously-frozen controllers (entropy_coef,
|
||||
// rollout_steps, per_alpha, ppo_clip, target_tau, gamma). Each kernel
|
||||
// is a single-block reduction (tree-reduce, grid-stride, or per-batch
|
||||
// state update). The trainer launches each one immediately after its
|
||||
// source signal is populated:
|
||||
// * `rl_var_over_abs_mean_b` after compute_advantage_return
|
||||
// * `rl_kurtosis_b` after dqn_distributional_q_bwd
|
||||
// * `rl_kl_approx_b` after PPO surrogate forward
|
||||
// * `rl_l2_diff_norm` after target-net soft update
|
||||
// * `rl_step_counter_update` after extract_realized_pnl_delta
|
||||
// The scalar output is then consumed by ema_update_per_step (continuous
|
||||
// EMAs) or ema_update_on_done (done-gated EMAs) at the right ISV slot.
|
||||
// `entropy_observed_ema` reuses ema_update_per_step's built-in
|
||||
// per-batch mean reduce on entropy_d directly.
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
// Track shared headers so .cuh / .h edits trigger rebuilds of every
|
||||
// .cu that #includes them. Without these, an edit to a helper header
|
||||
// leaves a stale cubin.
|
||||
println!("cargo:rerun-if-changed=cuda/gpu_log_ids.h");
|
||||
println!("cargo:rerun-if-changed=cuda/gpu_log_helpers.cuh");
|
||||
|
||||
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_CUDA");
|
||||
if std::env::var("CARGO_FEATURE_CUDA").is_err() {
|
||||
eprintln!(" ml-alpha: cuda feature disabled, skipping kernel build");
|
||||
return;
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-env-changed=CUDA_HOME");
|
||||
println!("cargo:rerun-if-env-changed=CUDA_COMPUTE_CAP");
|
||||
|
||||
let nvcc = match find_nvcc() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
eprintln!(" ml-alpha: nvcc not found, skipping kernel build (set CUDA_HOME or install CUDA toolkit)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let out = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set by cargo"));
|
||||
|
||||
// Detect GPU arch: CUDA_COMPUTE_CAP env > nvidia-smi query > default 86
|
||||
let arch = detect_arch(&nvcc);
|
||||
eprintln!(" ml-alpha: compiling kernels for sm_{arch}");
|
||||
|
||||
for k in KERNELS {
|
||||
let src = PathBuf::from(format!("cuda/{k}.cu"));
|
||||
if !src.exists() {
|
||||
eprintln!(" ml-alpha: skipping {k} — source not yet present");
|
||||
continue;
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", src.display());
|
||||
let cubin = out.join(format!("{k}.cubin"));
|
||||
compile(&nvcc, &src, &cubin, &arch);
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_arch(_nvcc: &Path) -> String {
|
||||
// 1. Explicit env override
|
||||
if let Ok(cap) = std::env::var("CUDA_COMPUTE_CAP") {
|
||||
return cap;
|
||||
}
|
||||
// 2. Query the GPU on this machine
|
||||
if let Ok(output) = Command::new("nvidia-smi")
|
||||
.args(["--query-gpu=compute_cap", "--format=csv,noheader"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let s = String::from_utf8_lossy(&output.stdout);
|
||||
let cap = s.trim().replace('.', "");
|
||||
if !cap.is_empty() {
|
||||
return cap;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. Default to sm_86 (RTX 3050 Ti local dev)
|
||||
"86".to_string()
|
||||
}
|
||||
|
||||
fn compile(nvcc: &Path, src: &Path, cubin: &Path, arch: &str) {
|
||||
let status = Command::new(nvcc)
|
||||
.args([
|
||||
"-cubin",
|
||||
&format!("-arch=sm_{arch}"),
|
||||
"-O3",
|
||||
"--use_fast_math",
|
||||
"--ftz=true",
|
||||
"--fmad=true",
|
||||
"-o",
|
||||
cubin.to_str().unwrap(),
|
||||
src.to_str().unwrap(),
|
||||
])
|
||||
.status()
|
||||
.unwrap_or_else(|e| panic!("nvcc spawn failed for {}: {e}", src.display()));
|
||||
if !status.success() {
|
||||
panic!(
|
||||
"nvcc failed for {} (exit {})",
|
||||
src.display(),
|
||||
status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
eprintln!(
|
||||
" ml-alpha: compiled {} -> {} (sm_{arch})",
|
||||
src.display(),
|
||||
cubin.display()
|
||||
);
|
||||
}
|
||||
|
||||
fn find_nvcc() -> Option<PathBuf> {
|
||||
if let Ok(home) = std::env::var("CUDA_HOME") {
|
||||
let p = PathBuf::from(home).join("bin/nvcc");
|
||||
if p.exists() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
for cand in ["/usr/local/cuda/bin/nvcc", "/usr/bin/nvcc"] {
|
||||
let p = PathBuf::from(cand);
|
||||
if p.exists() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
Command::new("nvcc")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.map(|_| PathBuf::from("nvcc"))
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// abs_copy.cu — element-wise absolute-value copy: dst[b] = fabsf(src[b])
|
||||
// (Phase R7a of the integrated RL trainer rebuild;
|
||||
// see docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
|
||||
//
|
||||
// Feeds the |reward| signal into `ema_update_on_done` for the
|
||||
// `MEAN_ABS_PNL_EMA` slot (ISV[423], input to
|
||||
// rl_reward_scale_controller). The trainer keeps signed `rewards_d`
|
||||
// for the advantage / return / reward-scale-apply pipeline; this
|
||||
// kernel writes the magnitude into a separate `reward_abs_d` scratch
|
||||
// so the EMA producer reads the magnitude without mutating the signed
|
||||
// reward stream.
|
||||
//
|
||||
// Element-wise, trivially parallel. No reduction, no atomics.
|
||||
|
||||
extern "C" __global__ void abs_copy(
|
||||
const float* __restrict__ src,
|
||||
float* __restrict__ dst,
|
||||
int b_size
|
||||
) {
|
||||
const int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= b_size) return;
|
||||
dst[b] = fabsf(src[b]);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// action_entropy_per_step.cu — compute batch-level action entropy from
|
||||
// the actions buffer and update an ISV-resident EMA.
|
||||
//
|
||||
// The confidence gate decouples policy entropy from action entropy:
|
||||
// policy softmax can be high-entropy while the gate forces most actions
|
||||
// to Hold. SAC α/τ co-tuning must respond to ACTION entropy (what the
|
||||
// agent actually does) not policy entropy (what the model predicts).
|
||||
//
|
||||
// Single block, N_ACTIONS threads. Each thread counts its action in
|
||||
// shared memory via tree reduction over the batch, then thread 0
|
||||
// computes H(histogram) and updates the EMA.
|
||||
//
|
||||
// Per feedback_no_atomicadd: uses tree reduction, no atomics.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define N_ACTIONS 11
|
||||
|
||||
extern "C" __global__ void action_entropy_per_step(
|
||||
float* __restrict__ isv, // ISV bus (RW)
|
||||
int slot_index,// ISV slot to write
|
||||
float alpha, // EMA blend α (≥ 0.4)
|
||||
const int* __restrict__ actions, // [b_size] per-batch action indices
|
||||
int b_size
|
||||
) {
|
||||
// Phase 1: each of N_ACTIONS threads counts how many batch elements
|
||||
// chose that action. Single-pass scan over the actions buffer.
|
||||
__shared__ int counts[N_ACTIONS];
|
||||
const int a = threadIdx.x;
|
||||
if (a >= N_ACTIONS) return;
|
||||
|
||||
counts[a] = 0;
|
||||
__syncthreads();
|
||||
|
||||
// Sequential scan — N_ACTIONS threads each check all b_size elements.
|
||||
// At b=1024, N_ACTIONS=11: 1024/11 ≈ 93 iterations per thread.
|
||||
// No atomics needed since each thread owns its histogram bin.
|
||||
for (int b = 0; b < b_size; ++b) {
|
||||
if (actions[b] == a) {
|
||||
counts[a] += 1;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Phase 2: thread 0 computes entropy from the histogram.
|
||||
if (a == 0) {
|
||||
float h = 0.0f;
|
||||
const float inv_b = 1.0f / (float)b_size;
|
||||
for (int i = 0; i < N_ACTIONS; ++i) {
|
||||
if (counts[i] > 0) {
|
||||
float p = (float)counts[i] * inv_b;
|
||||
h -= p * logf(p);
|
||||
}
|
||||
}
|
||||
|
||||
// EMA update with first-observation bootstrap.
|
||||
const float prev = isv[slot_index];
|
||||
if (prev == 0.0f) {
|
||||
if (h != 0.0f) {
|
||||
isv[slot_index] = h;
|
||||
}
|
||||
} else {
|
||||
isv[slot_index] = (1.0f - alpha) * prev + alpha * h;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
// actions_to_market_targets.cu — translate the 11-action discrete grid
|
||||
// to LobSim's `market_targets[B*2] = {side, size}` device buffer.
|
||||
//
|
||||
// Action mapping (must match `crate::rl::common::Action` and
|
||||
// `LobSimCuda::submit_market_immediate`'s expected `{side, size}` pair):
|
||||
//
|
||||
// 0 ShortLarge side=1 (sell), size=2
|
||||
// 1 ShortSmall side=1 (sell), size=1
|
||||
// 2 Hold side=2 (no-op),size=0
|
||||
// 3 FlatFromLong side=1 (sell), size=position_lots (only if pos > 0)
|
||||
// 4 FlatFromShort side=0 (buy), size=|position_lots| (only if pos < 0)
|
||||
// 5 LongSmall side=0 (buy), size=1
|
||||
// 6 LongLarge side=0 (buy), size=2
|
||||
// 7 TrailTighten side=2 (no-op),size=0 — handled by rl_trail_mutate kernel
|
||||
// 8 TrailLoosen side=2 (no-op),size=0 — handled by rl_trail_mutate kernel
|
||||
// 9 HalfFlatLong side=1 (sell), size=oldest_unit_lots (if pyramid>1)
|
||||
// or ceil(|pos|/2) (if pyramid<=1)
|
||||
// 10 HalfFlatShort side=0 (buy), size=oldest_unit_lots (if pyramid>1)
|
||||
// or ceil(|pos|/2) (if pyramid<=1)
|
||||
//
|
||||
// Pyramid semantics for actions 5/6 when already long (and 0/1 when
|
||||
// already short):
|
||||
// - If (mid - last_entry) >= threshold AND count < MAX_UNITS → ADD
|
||||
// - Else if count < MAX_UNITS → REPLACE (fill replaces oldest unit)
|
||||
// - Else (at MAX_UNITS, threshold not met) → no-op
|
||||
//
|
||||
// HalfFlat semantics (a9/a10) when pyramid_units_count > 1:
|
||||
// - Close the oldest active unit (lowest active index, OR
|
||||
// close_unit_index[b] if trail-stop overrode it to a specific slot).
|
||||
// - Emit size = |lots| of that unit.
|
||||
// - When pyramid_units_count <= 1, fall back to ceil(|pos|/2).
|
||||
//
|
||||
// Element-wise, one thread per batch entry. No atomics.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define MAX_UNITS 4
|
||||
#define RL_PYRAMID_THRESHOLD_INDEX 506
|
||||
#define RL_ANTIMARTINGALE_KAPPA_INDEX 509
|
||||
#define RL_ANTIMARTINGALE_MIN_INDEX 510
|
||||
#define RL_ANTIMARTINGALE_MAX_INDEX 511
|
||||
|
||||
// ── Layer 1 (CMDP hard constraints — spec 2026-05-30-adaptive-risk-management) ──
|
||||
// Session-level DD-triggered + cooldown are PER-BATCH (one buffer slot per
|
||||
// independent backtest session). The two ISV slots below carry only
|
||||
// canonical-summary aggregates (worst per-batch DD, max per-batch
|
||||
// cooldown) for diag visibility — gating reads the per-batch arrays.
|
||||
#define RL_MAX_OPEN_UNITS_INDEX 665
|
||||
#define RL_NET_INVENTORY_LIMIT_USD_INDEX 670
|
||||
|
||||
// ── Layer 4 (Kelly fraction sizing — spec 2026-05-30-adaptive-risk-management) ──
|
||||
#define RL_KELLY_FRACTION_INDEX 676
|
||||
|
||||
// Action indices — must match crate::rl::common::Action.
|
||||
#define ACTION_SHORT_LARGE 0
|
||||
#define ACTION_SHORT_SMALL 1
|
||||
#define ACTION_HOLD 2
|
||||
#define ACTION_FLAT_FROM_LONG 3
|
||||
#define ACTION_FLAT_FROM_SHORT 4
|
||||
#define ACTION_LONG_SMALL 5
|
||||
#define ACTION_LONG_LARGE 6
|
||||
#define ACTION_TRAIL_TIGHTEN 7
|
||||
#define ACTION_TRAIL_LOOSEN 8
|
||||
#define ACTION_HALF_FLAT_LONG 9
|
||||
#define ACTION_HALF_FLAT_SHORT 10
|
||||
|
||||
__device__ __forceinline__ int antimartingale_size(
|
||||
int base_size, float outcome_ema_b, const float* isv
|
||||
) {
|
||||
const float kappa = isv[RL_ANTIMARTINGALE_KAPPA_INDEX];
|
||||
const float lo = isv[RL_ANTIMARTINGALE_MIN_INDEX];
|
||||
const float hi = isv[RL_ANTIMARTINGALE_MAX_INDEX];
|
||||
const float scale = fmaxf(lo, fminf(1.0f + kappa * outcome_ema_b, hi));
|
||||
const int sized = (int)(((float)base_size) * scale + 0.5f);
|
||||
return (sized > 0) ? sized : 1;
|
||||
}
|
||||
|
||||
// Returns true if `action` would open or grow a one-sided position
|
||||
// (excludes closes, half-flats, holds, and trail mutations).
|
||||
__device__ __forceinline__ bool is_opening_action(int action) {
|
||||
return (action == ACTION_SHORT_LARGE) || (action == ACTION_SHORT_SMALL)
|
||||
|| (action == ACTION_LONG_SMALL) || (action == ACTION_LONG_LARGE);
|
||||
}
|
||||
|
||||
// Returns true if executing `action` from the current `net_pos_lots`
|
||||
// would INCREASE one-sided exposure (i.e. opens/grows the dominant side).
|
||||
// Closes/reverses/holds never trigger this — only same-side adds.
|
||||
__device__ __forceinline__ bool would_increase_exposure(int action, int net_pos_lots) {
|
||||
const bool longs = (action == ACTION_LONG_SMALL) || (action == ACTION_LONG_LARGE);
|
||||
const bool shorts = (action == ACTION_SHORT_SMALL) || (action == ACTION_SHORT_LARGE);
|
||||
if (longs && net_pos_lots >= 0) return true; // open or pyramid same side
|
||||
if (shorts && net_pos_lots <= 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" __global__ void actions_to_market_targets(
|
||||
const int* __restrict__ actions, // [b_size]
|
||||
const unsigned char* __restrict__ pos_state, // [b_size * pos_bytes]
|
||||
int* __restrict__ market_targets, // [b_size * 2] OUT
|
||||
const float* __restrict__ isv,
|
||||
const float* __restrict__ bid_px, // [BOOK_LEVELS]
|
||||
const float* __restrict__ ask_px, // [BOOK_LEVELS]
|
||||
const float* __restrict__ unit_entry_price, // [b_size * MAX_UNITS]
|
||||
const unsigned char* __restrict__ unit_active, // [b_size * MAX_UNITS]
|
||||
const int* __restrict__ unit_lots, // [b_size * MAX_UNITS]
|
||||
const int* __restrict__ pyramid_units_count, // [b_size]
|
||||
const int* __restrict__ close_unit_index, // [b_size] (-1 = auto oldest)
|
||||
const float* __restrict__ outcome_ema, // [b_size] per-batch
|
||||
// CMDP per-batch state (Layer 1, spec 2026-05-30-adaptive-risk-management).
|
||||
// Each batch element is an independent session; one account hitting
|
||||
// its DD limit or cooldown does not gate the other 1023 sessions.
|
||||
const float* __restrict__ session_dd_triggered_per_batch, // [b_size]
|
||||
const float* __restrict__ cooldown_remaining_per_batch, // [b_size]
|
||||
int b_size,
|
||||
int pos_bytes
|
||||
) {
|
||||
const int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= b_size) return;
|
||||
|
||||
int action = actions[b];
|
||||
const int position_lots = *(const int*)(pos_state + b * pos_bytes);
|
||||
const float outcome_ema_b = outcome_ema[b];
|
||||
|
||||
int side = 2; // default no-op
|
||||
int size = 0;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Layer 1 (CMDP hard constraints — spec 2026-05-30-adaptive-risk-management).
|
||||
// Session-level overrides force the account FLAT regardless of agent's
|
||||
// choice. Per-batch (one independent backtest session per b); a tripped
|
||||
// DD or active cooldown on session `b` does NOT lock out the others.
|
||||
//
|
||||
// Fix E (2026-05-30): emit a *closing* market order when the account
|
||||
// is non-flat — otherwise an existing losing position bleeds m2m for
|
||||
// the entire 500-step cooldown with no exit (trail stops are also
|
||||
// suppressed by this branch). Cluster alpha-rl-rjsjq step 371 showed
|
||||
// worst session_pnl growing monotonically -$3.7k → -$9.3k from
|
||||
// exactly this mechanism.
|
||||
//
|
||||
// Once the position is flat, subsequent cooldown steps emit a true
|
||||
// no-op (side=2, size=0) and the account waits for its recovery clock.
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
const bool dd_triggered = session_dd_triggered_per_batch[b] >= 0.5f;
|
||||
const bool in_cooldown = cooldown_remaining_per_batch[b] > 0.0f;
|
||||
if (dd_triggered || in_cooldown) {
|
||||
if (position_lots > 0) {
|
||||
// long → close via sell
|
||||
market_targets[b * 2 + 0] = 1;
|
||||
market_targets[b * 2 + 1] = position_lots;
|
||||
} else if (position_lots < 0) {
|
||||
// short → close via buy
|
||||
market_targets[b * 2 + 0] = 0;
|
||||
market_targets[b * 2 + 1] = -position_lots;
|
||||
} else {
|
||||
// already flat → true no-op
|
||||
market_targets[b * 2 + 0] = 2;
|
||||
market_targets[b * 2 + 1] = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Max-open-units check (per-batch dynamic count): refuse opens once
|
||||
// pyramid is at MAX. Closes still allowed (handled by remaining logic).
|
||||
{
|
||||
int active_units = 0;
|
||||
const int base_u = b * MAX_UNITS;
|
||||
for (int u = 0; u < MAX_UNITS; ++u) {
|
||||
if (unit_active[base_u + u]) active_units += 1;
|
||||
}
|
||||
const int max_open = (int)isv[RL_MAX_OPEN_UNITS_INDEX];
|
||||
if (active_units >= max_open && is_opening_action(action)) {
|
||||
action = ACTION_HOLD;
|
||||
}
|
||||
}
|
||||
|
||||
// Net-inventory cap (USD): block actions that grow one-sided exposure
|
||||
// beyond the limit. Uses bid/ask mid as the USD multiplier.
|
||||
{
|
||||
const float inv_limit = isv[RL_NET_INVENTORY_LIMIT_USD_INDEX];
|
||||
if (inv_limit > 0.0f) {
|
||||
const float mid = 0.5f * (bid_px[0] + ask_px[0]);
|
||||
const float net_pos_usd = (float)position_lots * mid;
|
||||
if (fabsf(net_pos_usd) > inv_limit && would_increase_exposure(action, position_lots)) {
|
||||
action = ACTION_HOLD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action == 0) {
|
||||
// ShortLarge — when already short, apply pyramid logic.
|
||||
if (position_lots < 0) {
|
||||
const int count = pyramid_units_count[b];
|
||||
const float threshold = isv[RL_PYRAMID_THRESHOLD_INDEX];
|
||||
const float mid = 0.5f * (bid_px[0] + ask_px[0]);
|
||||
float last_entry = 0.0f;
|
||||
const int base = b * MAX_UNITS;
|
||||
for (int u = MAX_UNITS - 1; u >= 0; --u) {
|
||||
if (unit_active[base + u]) {
|
||||
last_entry = unit_entry_price[base + u];
|
||||
break;
|
||||
}
|
||||
}
|
||||
const float dist = last_entry - mid;
|
||||
if (dist >= threshold && count < MAX_UNITS) {
|
||||
side = 1; size = 2;
|
||||
} else if (count < MAX_UNITS) {
|
||||
side = 1; size = 2;
|
||||
}
|
||||
} else {
|
||||
side = 1; size = antimartingale_size(2, outcome_ema_b, isv);
|
||||
}
|
||||
} else if (action == 1) {
|
||||
// ShortSmall — when already short, apply pyramid logic.
|
||||
if (position_lots < 0) {
|
||||
const int count = pyramid_units_count[b];
|
||||
const float threshold = isv[RL_PYRAMID_THRESHOLD_INDEX];
|
||||
const float mid = 0.5f * (bid_px[0] + ask_px[0]);
|
||||
float last_entry = 0.0f;
|
||||
const int base = b * MAX_UNITS;
|
||||
for (int u = MAX_UNITS - 1; u >= 0; --u) {
|
||||
if (unit_active[base + u]) {
|
||||
last_entry = unit_entry_price[base + u];
|
||||
break;
|
||||
}
|
||||
}
|
||||
const float dist = last_entry - mid;
|
||||
if (dist >= threshold && count < MAX_UNITS) {
|
||||
side = 1; size = 1;
|
||||
} else if (count < MAX_UNITS) {
|
||||
side = 1; size = 1;
|
||||
}
|
||||
} else {
|
||||
side = 1; size = antimartingale_size(1, outcome_ema_b, isv);
|
||||
}
|
||||
} else if (action == 2) {
|
||||
// Hold — no-op.
|
||||
} else if (action == 3) {
|
||||
if (position_lots > 0) {
|
||||
side = 1; size = position_lots;
|
||||
}
|
||||
} else if (action == 4) {
|
||||
if (position_lots < 0) {
|
||||
side = 0; size = -position_lots;
|
||||
}
|
||||
} else if (action == 5) {
|
||||
// LongSmall — when already long, apply pyramid logic.
|
||||
if (position_lots > 0) {
|
||||
const int count = pyramid_units_count[b];
|
||||
const float threshold = isv[RL_PYRAMID_THRESHOLD_INDEX];
|
||||
const float mid = 0.5f * (bid_px[0] + ask_px[0]);
|
||||
float last_entry = 0.0f;
|
||||
const int base = b * MAX_UNITS;
|
||||
for (int u = MAX_UNITS - 1; u >= 0; --u) {
|
||||
if (unit_active[base + u]) {
|
||||
last_entry = unit_entry_price[base + u];
|
||||
break;
|
||||
}
|
||||
}
|
||||
const float dist = mid - last_entry;
|
||||
if (dist >= threshold && count < MAX_UNITS) {
|
||||
side = 0; size = 1;
|
||||
} else if (count < MAX_UNITS) {
|
||||
side = 0; size = 1;
|
||||
}
|
||||
} else {
|
||||
side = 0; size = antimartingale_size(1, outcome_ema_b, isv);
|
||||
}
|
||||
} else if (action == 6) {
|
||||
// LongLarge — when already long, apply pyramid logic.
|
||||
if (position_lots > 0) {
|
||||
const int count = pyramid_units_count[b];
|
||||
const float threshold = isv[RL_PYRAMID_THRESHOLD_INDEX];
|
||||
const float mid = 0.5f * (bid_px[0] + ask_px[0]);
|
||||
float last_entry = 0.0f;
|
||||
const int base = b * MAX_UNITS;
|
||||
for (int u = MAX_UNITS - 1; u >= 0; --u) {
|
||||
if (unit_active[base + u]) {
|
||||
last_entry = unit_entry_price[base + u];
|
||||
break;
|
||||
}
|
||||
}
|
||||
const float dist = mid - last_entry;
|
||||
if (dist >= threshold && count < MAX_UNITS) {
|
||||
side = 0; size = 2;
|
||||
} else if (count < MAX_UNITS) {
|
||||
side = 0; size = 2;
|
||||
}
|
||||
} else {
|
||||
side = 0; size = antimartingale_size(2, outcome_ema_b, isv);
|
||||
}
|
||||
} else if (action == 9) {
|
||||
// HalfFlatLong: close oldest unit when pyramided, else ceil(|pos|/2).
|
||||
if (position_lots > 0) {
|
||||
side = 1;
|
||||
const int count = pyramid_units_count[b];
|
||||
if (count > 1) {
|
||||
const int base = b * MAX_UNITS;
|
||||
const int target_u = close_unit_index[b];
|
||||
int close_slot = -1;
|
||||
if (target_u >= 0 && target_u < MAX_UNITS && unit_active[base + target_u]) {
|
||||
close_slot = target_u;
|
||||
} else {
|
||||
for (int u = 0; u < MAX_UNITS; ++u) {
|
||||
if (unit_active[base + u]) { close_slot = u; break; }
|
||||
}
|
||||
}
|
||||
if (close_slot >= 0) {
|
||||
const int slot_lots = unit_lots[base + close_slot];
|
||||
size = (slot_lots > 0) ? slot_lots : -slot_lots;
|
||||
} else {
|
||||
const int half = (position_lots + 1) / 2;
|
||||
size = (half > 0) ? half : 1;
|
||||
}
|
||||
} else {
|
||||
const int half = (position_lots + 1) / 2;
|
||||
size = (half > 0) ? half : 1;
|
||||
}
|
||||
}
|
||||
} else if (action == 10) {
|
||||
// HalfFlatShort: close oldest unit when pyramided, else ceil(|pos|/2).
|
||||
if (position_lots < 0) {
|
||||
side = 0;
|
||||
const int count = pyramid_units_count[b];
|
||||
const int abs_pos = -position_lots;
|
||||
if (count > 1) {
|
||||
const int base = b * MAX_UNITS;
|
||||
const int target_u = close_unit_index[b];
|
||||
int close_slot = -1;
|
||||
if (target_u >= 0 && target_u < MAX_UNITS && unit_active[base + target_u]) {
|
||||
close_slot = target_u;
|
||||
} else {
|
||||
for (int u = 0; u < MAX_UNITS; ++u) {
|
||||
if (unit_active[base + u]) { close_slot = u; break; }
|
||||
}
|
||||
}
|
||||
if (close_slot >= 0) {
|
||||
const int slot_lots = unit_lots[base + close_slot];
|
||||
size = (slot_lots > 0) ? slot_lots : -slot_lots;
|
||||
} else {
|
||||
const int half = (abs_pos + 1) / 2;
|
||||
size = (half > 0) ? half : 1;
|
||||
}
|
||||
} else {
|
||||
const int half = (abs_pos + 1) / 2;
|
||||
size = (half > 0) ? half : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// actions 7, 8 (TrailTighten/Loosen): no fill — handled by
|
||||
// rl_trail_mutate kernel. The side=2 no-op default is correct here.
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Layer 4 (Kelly-fraction sizing — spec 2026-05-30-adaptive-risk-management).
|
||||
// Scales the opening/sizing size by the observed-edge Kelly fraction.
|
||||
// Closes (FlatFrom* + HalfFlat*) and Hold are NOT scaled — risk
|
||||
// management for exiting positions is handled by Layer 1 + trail stops.
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
if (size > 0 && is_opening_action(action)) {
|
||||
const float kelly = isv[RL_KELLY_FRACTION_INDEX];
|
||||
// Bootstrap = 1.0 (full size); negative or zero → no commitment.
|
||||
const float scaled = (float)size * kelly;
|
||||
if (scaled <= 0.0f) {
|
||||
size = 0;
|
||||
side = 2;
|
||||
} else {
|
||||
// Ceil to preserve at least 1-lot when kelly is small-but-positive.
|
||||
int new_size = (int)ceilf(scaled);
|
||||
if (new_size < 1) new_size = 1;
|
||||
size = new_size;
|
||||
}
|
||||
}
|
||||
|
||||
market_targets[b * 2 + 0] = side;
|
||||
market_targets[b * 2 + 1] = size;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// adamw_step.cu — element-wise AdamW update.
|
||||
//
|
||||
// m_t = beta1 * m_{t-1} + (1 - beta1) * g
|
||||
// v_t = beta2 * v_{t-1} + (1 - beta2) * g^2
|
||||
// m_hat = m_t / (1 - beta1^step)
|
||||
// v_hat = v_t / (1 - beta2^step)
|
||||
// theta -= lr * (m_hat / (sqrt(v_hat) + eps) + wd * theta)
|
||||
//
|
||||
// One thread per parameter. Block tree-reduce not needed; this is pure
|
||||
// element-wise. No atomicAdd.
|
||||
//
|
||||
// `step` is a host-supplied i32 holding the current 1-indexed training
|
||||
// step. The Rust launcher increments a host-side counter and passes it
|
||||
// as a kernel argument each launch, eliminating the separate
|
||||
// `adamw_increment_counter` kernel (saves 3407 launches per training
|
||||
// step).
|
||||
|
||||
extern "C" __global__ void adamw_step(
|
||||
float* __restrict__ theta,
|
||||
const float* __restrict__ grad,
|
||||
float* __restrict__ m,
|
||||
float* __restrict__ v,
|
||||
int n_params,
|
||||
float lr,
|
||||
float beta1,
|
||||
float beta2,
|
||||
float eps,
|
||||
float wd,
|
||||
int step
|
||||
) {
|
||||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= n_params) return;
|
||||
|
||||
const float g = grad[i];
|
||||
const float m_n = beta1 * m[i] + (1.0f - beta1) * g;
|
||||
const float v_n = beta2 * v[i] + (1.0f - beta2) * g * g;
|
||||
m[i] = m_n;
|
||||
v[i] = v_n;
|
||||
|
||||
const float bc1 = 1.0f - powf(beta1, (float) step);
|
||||
const float bc2 = 1.0f - powf(beta2, (float) step);
|
||||
const float m_hat = m_n / fmaxf(bc1, 1e-12f);
|
||||
const float v_hat = v_n / fmaxf(bc2, 1e-12f);
|
||||
|
||||
theta[i] -= lr * (m_hat / (sqrtf(v_hat) + eps) + wd * theta[i]);
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
// apply_reward_scale.cu — element-wise reward standardisation +
|
||||
// asymmetric clamp + per-step pre-clamp |max| diagnostic.
|
||||
//
|
||||
// Reads the standardisation scale from `ISV[RL_REWARD_SCALE_INDEX = 406]`
|
||||
// (the rl_reward_scale_controller emits this — see
|
||||
// rl_reward_scale_controller.cu for the controller logic and bounds).
|
||||
//
|
||||
// Phase R6 originally only scaled rewards in-place. This left V
|
||||
// regression catastrophically exposed when the reward_scale controller
|
||||
// hadn't yet adapted to a fresh trade-magnitude regime: a single
|
||||
// closed trade with realised PnL well outside `1 / mean_abs_pnl_ema`'s
|
||||
// current estimate could produce a scaled reward 100s of times the
|
||||
// C51 atom span. The V regression target `returns = reward + γ(1-done)
|
||||
// v_tp1` then equals this large scaled reward on trade-close steps
|
||||
// (done=1), and `(v_pred - returns)²` spikes into the 1e4-1e6 range.
|
||||
// Canonical incident: alpha-rl-xv66n fold0 had 10 trade-close steps
|
||||
// with l_v > 1e4, max 9.46e4.
|
||||
//
|
||||
// FIX (this kernel): clamp the scaled reward to an asymmetric bound
|
||||
// before writing it back. The clamp:
|
||||
//
|
||||
// * Caps loss magnitudes at `REWARD_CLAMP_LOSS = 3.0`
|
||||
// * Caps win magnitudes at `REWARD_CLAMP_WIN = 1.0`
|
||||
//
|
||||
// per `pearl_audit_unboundedness_for_implicit_asymmetry`: preserving
|
||||
// the loss > win asymmetry (loss aversion) so a single fat-tail loss
|
||||
// remains visible while still bounding V regression's target. The
|
||||
// asymmetry also matches typical HFT reward distributions where losses
|
||||
// can be 2-3× larger than wins per close.
|
||||
//
|
||||
// DIAGNOSTIC: writes the pre-clamp max |scaled| over the current batch
|
||||
// to `ISV[RL_MAX_ABS_SCALED_REWARD_PRE_CLAMP_INDEX = 439]`. This lets
|
||||
// the diag JSONL surface how often the clamp is actually firing — when
|
||||
// pre-clamp max stays ≤ REWARD_CLAMP_WIN the clamp is a no-op
|
||||
// (controller has adapted scale correctly); when it rises above that
|
||||
// repeatedly, the reward_scale controller is failing to track typical
|
||||
// trade magnitudes and the clamp is doing load-bearing work.
|
||||
//
|
||||
// Per `pearl_one_unbounded_signal_per_reward`: `rewards[b]` pre-scale
|
||||
// is the single unbounded multiplicand; `isv[RL_REWARD_SCALE_INDEX]`
|
||||
// is bounded by the controller [1e-3, 1e3]. Post-clamp, even
|
||||
// rewards[b] is bounded — the controller's earlier "single unbounded
|
||||
// multiplicand" property is preserved at the source (raw PnL) but the
|
||||
// V/Q-target-facing signal is bounded.
|
||||
//
|
||||
// Per `pearl_no_atomicadd`: single-block reduction, no atomicMax. For
|
||||
// b_size > BLOCK_DIM the kernel does a grid-stride loop with per-thread
|
||||
// local max then a shared-memory tree reduce.
|
||||
|
||||
#define RL_REWARD_SCALE_INDEX 406
|
||||
#define RL_MAX_ABS_SCALED_REWARD_PRE_CLAMP_INDEX 439
|
||||
|
||||
// ISV-driven asymmetric clamp bounds per `feedback_isv_for_adaptive_bounds`.
|
||||
// Win cap default 1.0 matches the C51 atom span (V_MAX=1.0); loss cap
|
||||
// default 3.0 preserves loss-aversion asymmetry (HFT P&L losses are
|
||||
// typically 2-3× larger than wins per close). Seeded at trainer init
|
||||
// by `rl_isv_write`; tunable at runtime by re-seeding. Also adaptive
|
||||
// per-step via `rl_reward_clamp_controller` reading the per-step
|
||||
// positive-scaled max published below.
|
||||
#define RL_REWARD_CLAMP_WIN_INDEX 452
|
||||
// B-7 (2026-06-01): ISV toggle to disable the post-scale clamp entirely.
|
||||
// Default 0 → clamp disabled, popart's standardization + F4 envelope
|
||||
// handle tail magnitudes. Legacy 1 → clamp applied as before.
|
||||
#define RL_REWARD_CLAMP_ENABLED_INDEX 724
|
||||
#define RL_REWARD_CLAMP_LOSS_INDEX 453
|
||||
|
||||
// audit 2026-05-24: per-step max(positive scaled reward, 0) published
|
||||
// here so `rl_reward_clamp_controller` can adapt WIN/LOSS bounds from
|
||||
// the actual positive-tail distribution rather than the static 1.0/3.0
|
||||
// defaults. alpha-rl-rmgm5 evidence: clamp fired on 85% of steps with
|
||||
// p95 pre_clamp = 15.5× the WIN bound, crushing the gradient signal
|
||||
// of profitable trades down to 1.0 while losses (avg -3.84) routinely
|
||||
// exceeded the LOSS bound 3.0. Adaptive clamp lets the winning tail
|
||||
// reach the C51 atom span properly.
|
||||
#define RL_POS_SCALED_REWARD_MAX_INDEX 478
|
||||
|
||||
// wwcsz followup 2026-05-24: also track per-step max(-scaled, 0) — the
|
||||
// loss-side tail — so the RATIO (LOSS/WIN) can become adaptive from
|
||||
// observed loss/win magnitudes rather than being hardcoded 3:1. wwcsz
|
||||
// data showed avg_loss=-$4.36 vs avg_win=+$5.27 (actual ratio 0.83);
|
||||
// the 3:1 built-in bias was over-emphasising loss-aversion vs reality.
|
||||
#define RL_NEG_SCALED_REWARD_MAX_INDEX 489
|
||||
|
||||
// Single block; grid-stride for b_size > BLOCK_DIM. Caller launches
|
||||
// with block_dim = min(b_size, 256), grid_dim = 1, shared bytes =
|
||||
// block_dim * sizeof(float).
|
||||
// Shared-mem layout: caller passes `shared_mem_bytes = 3 * block_dim *
|
||||
// sizeof(float)`. Three parallel reductions:
|
||||
// [0..block_dim) s_abs — max(|scaled|) for diag slot
|
||||
// [block_dim..2*block_dim) s_pos — max(positive scaled, 0)
|
||||
// [2*block_dim..3*block_dim) s_neg — max(-scaled, 0) for adaptive RATIO
|
||||
extern "C" __global__ void apply_reward_scale(
|
||||
float* __restrict__ isv, // ISV bus IN/OUT
|
||||
float* __restrict__ rewards, // [b_size] IN/OUT (scaled+clamped)
|
||||
int b_size
|
||||
) {
|
||||
extern __shared__ float smem[];
|
||||
float* s_abs = smem;
|
||||
float* s_pos = smem + blockDim.x;
|
||||
float* s_neg = smem + 2 * blockDim.x;
|
||||
const int tid = threadIdx.x;
|
||||
const float scale = isv[RL_REWARD_SCALE_INDEX];
|
||||
// ISV-driven clamp bounds (was hardcoded 1.0 / 3.0). Reading per
|
||||
// kernel launch is one extra coalesced load; broadcast across the
|
||||
// block by all threads.
|
||||
const float clamp_win = isv[RL_REWARD_CLAMP_WIN_INDEX];
|
||||
const float clamp_loss = isv[RL_REWARD_CLAMP_LOSS_INDEX];
|
||||
const float clamp_enabled = isv[RL_REWARD_CLAMP_ENABLED_INDEX];
|
||||
|
||||
// Pass 1 — scale, clamp, accumulate per-thread max |scaled|, max
|
||||
// max(positive scaled, 0), AND max(-scaled, 0). Three independent
|
||||
// local maxes tracked in a single grid-stride loop so the kernel's
|
||||
// HBM read pass is shared.
|
||||
float local_abs = 0.0f;
|
||||
float local_pos = 0.0f;
|
||||
float local_neg = 0.0f;
|
||||
for (int b = tid; b < b_size; b += blockDim.x) {
|
||||
const float raw = rewards[b];
|
||||
const float scaled = raw * scale;
|
||||
|
||||
// Track magnitude BEFORE clamping so the diag sees what the
|
||||
// controller failed to bound on its own.
|
||||
const float a = fabsf(scaled);
|
||||
if (a > local_abs) local_abs = a;
|
||||
|
||||
// Track positive tail BEFORE clamping for the adaptive clamp
|
||||
// controller — winning-trade signal that the current static
|
||||
// WIN=1.0 bound clips.
|
||||
const float p = fmaxf(scaled, 0.0f);
|
||||
if (p > local_pos) local_pos = p;
|
||||
|
||||
// Track negative tail magnitude (max(-scaled, 0)) for adaptive
|
||||
// RATIO calculation per wwcsz followup. Separate from |scaled|
|
||||
// because we need positive-only and negative-only EMAs to
|
||||
// compute observed loss/win magnitude ratio.
|
||||
const float n = fmaxf(-scaled, 0.0f);
|
||||
if (n > local_neg) local_neg = n;
|
||||
|
||||
// B-7 (2026-06-01): ISV-gated clamp. Default OFF — popart's
|
||||
// standardization + F4 envelope handle tail magnitudes per
|
||||
// van Hasselt 2016. Clamp truncates tail losses BEFORE popart
|
||||
// sees them, creating a reward-hacking gap (training reward
|
||||
// signal positive while realized eval pnl strongly negative).
|
||||
// Set RL_REWARD_CLAMP_ENABLED_INDEX = 1 to restore legacy.
|
||||
const float clamped = (clamp_enabled > 0.5f)
|
||||
? fmaxf(-clamp_loss, fminf(scaled, clamp_win))
|
||||
: scaled;
|
||||
rewards[b] = clamped;
|
||||
}
|
||||
s_abs[tid] = local_abs;
|
||||
s_pos[tid] = local_pos;
|
||||
s_neg[tid] = local_neg;
|
||||
__syncthreads();
|
||||
|
||||
// Pass 2 — block tree reduce (no atomics per pearl_no_atomicadd).
|
||||
// Three reductions advance together to share the __syncthreads barrier.
|
||||
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) {
|
||||
const float a_a = s_abs[tid];
|
||||
const float a_b = s_abs[tid + stride];
|
||||
s_abs[tid] = a_a > a_b ? a_a : a_b;
|
||||
const float p_a = s_pos[tid];
|
||||
const float p_b = s_pos[tid + stride];
|
||||
s_pos[tid] = p_a > p_b ? p_a : p_b;
|
||||
const float n_a = s_neg[tid];
|
||||
const float n_b = s_neg[tid + stride];
|
||||
s_neg[tid] = n_a > n_b ? n_a : n_b;
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
// Thread 0 publishes per-step maxes to ISV. All are POINT
|
||||
// measurements; the controller maintains the EMA over the positive
|
||||
// and negative signals in its own slots.
|
||||
if (tid == 0) {
|
||||
isv[RL_MAX_ABS_SCALED_REWARD_PRE_CLAMP_INDEX] = s_abs[0];
|
||||
isv[RL_POS_SCALED_REWARD_MAX_INDEX] = s_pos[0];
|
||||
isv[RL_NEG_SCALED_REWARD_MAX_INDEX] = s_neg[0];
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// argmax_expected_q.cu — GPU-resident argmax over expected Q-values
|
||||
// per action (Phase R4 of the integrated RL trainer rebuild;
|
||||
// see docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
|
||||
//
|
||||
// Companion to `rl_action_kernel.cu`. Per
|
||||
// `pearl_thompson_for_distributional_action_selection`: Thompson is
|
||||
// used for ROLLOUT action selection (rl_action_kernel) and argmax over
|
||||
// EXPECTED Q is used for the Bellman-target argmax (this kernel). The
|
||||
// distinction matters for the canonical Double-DQN-style backup:
|
||||
//
|
||||
// target Q = Q_target(s_{t+1}, argmax_a E[Q_online(s_{t+1}, a)])
|
||||
//
|
||||
// Replaces the host-side `argmax_f32(&action_expected)` loop the
|
||||
// flawed Phase F shipped in step_with_lobsim — which violated
|
||||
// `feedback_cpu_is_read_only` by DtoH-copying q_logits and reducing
|
||||
// on CPU.
|
||||
//
|
||||
// Per-batch: softmax over each action's atoms, accumulate
|
||||
// `expected[a] = Σ_i p_i · support[i]`, then argmax over the 9
|
||||
// per-action expected values. Deterministic (no PRNG).
|
||||
//
|
||||
// Per `feedback_no_atomicadd`: thread 0 writes to `next_actions[b]`
|
||||
// after __syncthreads.
|
||||
|
||||
#define N_ACTIONS 11
|
||||
#define Q_N_ATOMS 21
|
||||
|
||||
// One block per batch (grid_dim.x = b_size). N_ACTIONS threads per
|
||||
// block; each thread computes its action's expected Q and writes to
|
||||
// shared mem; thread 0 argmaxes + writes next_actions[b].
|
||||
//
|
||||
// Inputs:
|
||||
// q_logits [b_size, N_ACTIONS, Q_N_ATOMS] row-major
|
||||
// atom_supports [Q_N_ATOMS]
|
||||
// Outputs:
|
||||
// next_actions [b_size] argmax over expected Q per batch
|
||||
extern "C" __global__ void argmax_expected_q(
|
||||
const float* __restrict__ q_logits,
|
||||
const float* __restrict__ atom_supports,
|
||||
int* __restrict__ next_actions,
|
||||
int b_size
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
const int a = threadIdx.x;
|
||||
if (b >= b_size) return;
|
||||
if (a >= N_ACTIONS) return;
|
||||
|
||||
__shared__ float expected_q[N_ACTIONS];
|
||||
|
||||
// Softmax + expected value over this action's atoms.
|
||||
const int row_off = (b * N_ACTIONS + a) * Q_N_ATOMS;
|
||||
float max_l = -INFINITY;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < Q_N_ATOMS; ++i) {
|
||||
const float l = q_logits[row_off + i];
|
||||
if (l > max_l) max_l = l;
|
||||
}
|
||||
float sum_exp = 0.0f;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < Q_N_ATOMS; ++i) {
|
||||
sum_exp += expf(q_logits[row_off + i] - max_l);
|
||||
}
|
||||
if (!isfinite(sum_exp) || sum_exp <= 0.0f) sum_exp = 1.0f;
|
||||
|
||||
float ev = 0.0f;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < Q_N_ATOMS; ++i) {
|
||||
const float p = expf(q_logits[row_off + i] - max_l) / sum_exp;
|
||||
ev += p * atom_supports[i];
|
||||
}
|
||||
expected_q[a] = ev;
|
||||
__syncthreads();
|
||||
|
||||
if (a == 0) {
|
||||
int best_a = 0;
|
||||
float best_v = expected_q[0];
|
||||
#pragma unroll
|
||||
for (int i = 1; i < N_ACTIONS; ++i) {
|
||||
if (expected_q[i] > best_v) {
|
||||
best_v = expected_q[i];
|
||||
best_a = i;
|
||||
}
|
||||
}
|
||||
next_actions[b] = best_a;
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
// attention_pool.cu — Single-head attention pool over Mamba2 K-positions.
|
||||
// Phase 3 (2026-05-17).
|
||||
//
|
||||
// Replaces the CfC's zero-initialised `h_old` at k=0 with a learned
|
||||
// attention-pooled summary over all K LN_b output positions. The K-loop
|
||||
// CfC keeps its recurrent semantics (h_old at k+1 = h_new at k); only
|
||||
// the initial state changes from zero to a content-addressable lookup.
|
||||
//
|
||||
// Forward math (per sample b):
|
||||
// scores[k] = Q · keys[b, k, :] # [K] — dot product over HIDDEN_DIM
|
||||
// attn[k] = softmax_k(scores) # [K]
|
||||
// context[h] = sum_k attn[k] * values[b, k, h] # [HIDDEN_DIM]
|
||||
//
|
||||
// For this attention pool, keys == values == LN_b output [B, K, HIDDEN_DIM].
|
||||
// Single learned param: Q [HIDDEN_DIM].
|
||||
//
|
||||
// Saved for backward:
|
||||
// attn_weights [B, K] (post-softmax)
|
||||
//
|
||||
// Block layout (forward):
|
||||
// grid_dim = (B, 1, 1), block_dim = (HIDDEN_DIM=128, 1, 1).
|
||||
// Thread tid handles dimension h of HIDDEN_DIM.
|
||||
//
|
||||
// Per `feedback_no_atomicadd.md`: block tree-reduce only, no atomics.
|
||||
|
||||
#define ATTN_HIDDEN_DIM 128
|
||||
#define ATTN_BLOCK 128
|
||||
#define ATTN_MAX_K 512 // safety cap on K; smoke uses K=16-64.
|
||||
|
||||
extern "C" __global__ void attention_pool_fwd(
|
||||
const float* __restrict__ Q, // [HIDDEN_DIM]
|
||||
const float* __restrict__ ln_out, // [B, K, HIDDEN_DIM]
|
||||
int n_batch,
|
||||
int k_seq,
|
||||
float* __restrict__ context, // [B, HIDDEN_DIM]
|
||||
float* __restrict__ attn_weights // [B, K] — saved post-softmax
|
||||
) {
|
||||
int b_idx = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (b_idx >= n_batch || tid >= ATTN_BLOCK) return;
|
||||
|
||||
extern __shared__ float smem[];
|
||||
float* s_scores = smem; // [K]
|
||||
float* s_red = smem + k_seq; // [BLOCK] — reduce scratch
|
||||
float* s_context = smem + k_seq + ATTN_BLOCK; // [HIDDEN_DIM] — final output buffer
|
||||
|
||||
// Cache Q in shared mem (broadcast).
|
||||
__shared__ float s_Q[ATTN_HIDDEN_DIM];
|
||||
if (tid < ATTN_HIDDEN_DIM) s_Q[tid] = Q[tid];
|
||||
__syncthreads();
|
||||
|
||||
// Pass 1: scores[k] = Q · ln_out[b, k, :].
|
||||
// Each k is handled sequentially by ALL threads via tree-reduce
|
||||
// over HIDDEN_DIM. K outer loop, threads tile HIDDEN_DIM inner.
|
||||
// For HIDDEN_DIM=128 and ATTN_BLOCK=128 we have one-to-one.
|
||||
const float* ln_b = ln_out + (long long)b_idx * k_seq * ATTN_HIDDEN_DIM;
|
||||
for (int k = 0; k < k_seq; ++k) {
|
||||
const float v = ln_b[k * ATTN_HIDDEN_DIM + tid] * s_Q[tid];
|
||||
s_red[tid] = v;
|
||||
__syncthreads();
|
||||
for (int s = ATTN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) s_red[tid] += s_red[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) s_scores[k] = s_red[0];
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
// Pass 2: softmax over K. Numerically stable: max-subtract + exp + sum.
|
||||
// Block-wide max over s_scores[0..k_seq).
|
||||
float my_max = -INFINITY;
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
const float v = s_scores[k];
|
||||
if (v > my_max) my_max = v;
|
||||
}
|
||||
s_red[tid] = my_max;
|
||||
__syncthreads();
|
||||
for (int s = ATTN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) {
|
||||
const float a = s_red[tid];
|
||||
const float b = s_red[tid + s];
|
||||
s_red[tid] = (a > b) ? a : b;
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
__shared__ float s_max;
|
||||
if (tid == 0) s_max = s_red[0];
|
||||
__syncthreads();
|
||||
|
||||
// exp(score - max) + sum.
|
||||
float my_sum = 0.0f;
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
const float e = expf(s_scores[k] - s_max);
|
||||
s_scores[k] = e; // reuse as exp_shifted
|
||||
my_sum += e;
|
||||
}
|
||||
s_red[tid] = my_sum;
|
||||
__syncthreads();
|
||||
for (int s = ATTN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) s_red[tid] += s_red[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
__shared__ float s_sum;
|
||||
if (tid == 0) s_sum = s_red[0];
|
||||
__syncthreads();
|
||||
|
||||
// Pass 3: attn[k] = exp/sum; save; context[h] = sum_k attn[k] * ln_out[b, k, h].
|
||||
// Save attn weights AND build the context output.
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
const float a = s_scores[k] / s_sum;
|
||||
s_scores[k] = a; // overwrite again — now holds true attn weights
|
||||
attn_weights[(long long)b_idx * k_seq + k] = a;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Accumulate context[h=tid] = sum_k attn[k] * ln_out[b, k, h=tid].
|
||||
if (tid < ATTN_HIDDEN_DIM) {
|
||||
float c = 0.0f;
|
||||
for (int k = 0; k < k_seq; ++k) {
|
||||
c += s_scores[k] * ln_b[k * ATTN_HIDDEN_DIM + tid];
|
||||
}
|
||||
s_context[tid] = c;
|
||||
context[(long long)b_idx * ATTN_HIDDEN_DIM + tid] = c;
|
||||
}
|
||||
}
|
||||
|
||||
// Attention pool backward — chain rule:
|
||||
//
|
||||
// d_attn[k] = sum_h grad_context[h] * values[b, k, h]
|
||||
// + (this is from context = sum_k attn[k] * values[b, k, :])
|
||||
//
|
||||
// d_values[b, k, h] += grad_context[h] * attn[k]
|
||||
// + d_scores[k] * Q[h]
|
||||
//
|
||||
// d_scores via softmax Jacobian:
|
||||
// d_scores[k] = attn[k] * (d_attn[k] - sum_kp attn[kp] * d_attn[kp])
|
||||
//
|
||||
// d_Q[h] += sum_{b, k} d_scores[k] * values[b, k, h]
|
||||
// (here values == ln_out)
|
||||
//
|
||||
// Single-writer discipline:
|
||||
// - d_Q is [HIDDEN_DIM] shared across all (b, k). ONE block per
|
||||
// launch, internal batch loop, tile over HIDDEN_DIM. += into d_Q.
|
||||
// - d_values[b, k, h] is [B, K, HIDDEN_DIM] — one block per launch
|
||||
// iterates over (b, k, h) sequentially; with block_dim = HIDDEN_DIM,
|
||||
// thread h is sole writer of column h for ALL (b, k).
|
||||
|
||||
// Block-per-batch attn_pool bwd (Phase B commit 4).
|
||||
// grid=(n_batch, 1, 1) block=(ATTN_BLOCK, 1, 1)
|
||||
//
|
||||
// Each block handles one batch's HIDDEN_DIM channels. grad_ln_out is
|
||||
// per-batch indexed (already safe), so each block bi writes its
|
||||
// [bi, :, :] slice via += onto whatever value grad_ln_out holds at
|
||||
// kernel launch (the K-loop's contribution to LN_b output grad).
|
||||
//
|
||||
// grad_Q is a single [HIDDEN_DIM] shared across all batches → per-batch
|
||||
// scratch [B, HIDDEN_DIM], reduced after the kernel returns.
|
||||
extern "C" __global__ void attention_pool_bwd(
|
||||
const float* __restrict__ Q, // [HIDDEN_DIM]
|
||||
const float* __restrict__ ln_out, // [B, K, HIDDEN_DIM] (= values)
|
||||
const float* __restrict__ attn_weights, // [B, K] from fwd
|
||||
const float* __restrict__ grad_context, // [B, HIDDEN_DIM]
|
||||
int n_batch,
|
||||
int k_seq,
|
||||
float* __restrict__ grad_Q_scratch, // [B, HIDDEN_DIM] (+=)
|
||||
float* __restrict__ grad_ln_out // [B, K, HIDDEN_DIM] (+= chained with K-loop)
|
||||
) {
|
||||
int bi = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (bi >= n_batch || tid >= ATTN_BLOCK) return;
|
||||
|
||||
extern __shared__ float smem[];
|
||||
float* s_dattn = smem; // [K]
|
||||
float* s_dscores = smem + k_seq; // [K]
|
||||
float* s_red = smem + 2 * k_seq; // [BLOCK]
|
||||
|
||||
__shared__ float s_Q[ATTN_HIDDEN_DIM];
|
||||
__shared__ float s_attn[ATTN_MAX_K];
|
||||
__shared__ float s_grad_ctx[ATTN_HIDDEN_DIM];
|
||||
__shared__ float s_sum_attn_dattn;
|
||||
|
||||
if (tid < ATTN_HIDDEN_DIM) s_Q[tid] = Q[tid];
|
||||
__syncthreads();
|
||||
|
||||
const float* ln_b = ln_out + (long long)bi * k_seq * ATTN_HIDDEN_DIM;
|
||||
float* grad_ln_b = grad_ln_out + (long long)bi * k_seq * ATTN_HIDDEN_DIM;
|
||||
// Cache attn + grad_context for this block's batch.
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
s_attn[k] = attn_weights[(long long)bi * k_seq + k];
|
||||
}
|
||||
if (tid < ATTN_HIDDEN_DIM) {
|
||||
s_grad_ctx[tid] = grad_context[(long long)bi * ATTN_HIDDEN_DIM + tid];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 1: d_attn[k] = sum_h grad_context[h] * values[b, k, h].
|
||||
for (int k = 0; k < k_seq; ++k) {
|
||||
const float v = (tid < ATTN_HIDDEN_DIM)
|
||||
? s_grad_ctx[tid] * ln_b[k * ATTN_HIDDEN_DIM + tid] : 0.0f;
|
||||
s_red[tid] = v;
|
||||
__syncthreads();
|
||||
for (int s = ATTN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) s_red[tid] += s_red[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) s_dattn[k] = s_red[0];
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
// Pass 2: sum_{kp} attn[kp] * d_attn[kp] (softmax Jacobian centring).
|
||||
float my_sum = 0.0f;
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
my_sum += s_attn[k] * s_dattn[k];
|
||||
}
|
||||
s_red[tid] = my_sum;
|
||||
__syncthreads();
|
||||
for (int s = ATTN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) s_red[tid] += s_red[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) s_sum_attn_dattn = s_red[0];
|
||||
__syncthreads();
|
||||
|
||||
// Pass 3: d_scores[k] = attn[k] * (d_attn[k] - s_sum_attn_dattn).
|
||||
for (int k = tid; k < k_seq; k += ATTN_BLOCK) {
|
||||
s_dscores[k] = s_attn[k] * (s_dattn[k] - s_sum_attn_dattn);
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 4: grad_Q_scratch[bi, h] += sum_k d_scores[k] * ln_out[b, k, h].
|
||||
// d_ln_out[b, k, h] += grad_context[h] * attn[k] + d_scores[k] * Q[h].
|
||||
// Thread h owns column h. Loops over k. grad_ln_out += chains the
|
||||
// attn-path gradient on top of the K-loop's contribution.
|
||||
if (tid < ATTN_HIDDEN_DIM) {
|
||||
float dq_local = 0.0f;
|
||||
for (int k = 0; k < k_seq; ++k) {
|
||||
const float v = ln_b[k * ATTN_HIDDEN_DIM + tid];
|
||||
dq_local += s_dscores[k] * v;
|
||||
grad_ln_b[k * ATTN_HIDDEN_DIM + tid] +=
|
||||
s_grad_ctx[tid] * s_attn[k] + s_dscores[k] * s_Q[tid];
|
||||
}
|
||||
grad_Q_scratch[(long long)bi * ATTN_HIDDEN_DIM + tid] += dq_local;
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
// aux_heads.cu — A+B paired linear heads on the AuxTrunk output (CB3).
|
||||
//
|
||||
// Per SDD-3 CB1+CB3: each direction (long, short) emits TWO independent
|
||||
// heads on the shared aux trunk hidden state:
|
||||
//
|
||||
// prof_long_logit [B, N] = h_aux [B, H] @ W_prof_long^T + b_prof_long
|
||||
// size_long_pred [B, N] = h_aux [B, H] @ W_size_long^T + b_size_long
|
||||
// prof_short_logit[B, N] = h_aux [B, H] @ W_prof_short^T + b_prof_short
|
||||
// size_short_pred [B, N] = h_aux [B, H] @ W_size_short^T + b_size_short
|
||||
//
|
||||
// where H = AUX_HIDDEN = 64 and N = N_AUX_HORIZONS = 3. The prof heads
|
||||
// emit RAW LOGITS — sigmoid is fused into the BCE loss kernel in
|
||||
// `aux_loss.cu`. The size heads emit raw linear regression outputs
|
||||
// supervised by NaN-masked Huber (conditional on the matching prof label;
|
||||
// CB1's loader sets `y_size = NaN` whenever `y_prof = 0`, which gives
|
||||
// the conditional-Huber semantics for free).
|
||||
//
|
||||
// Design constraints (per CLAUDE.md + repo memory):
|
||||
// * `feedback_no_atomicadd.md` — no atomicAdd. Each thread is
|
||||
// the sole writer of the slot(s) it touches; cross-batch reduction
|
||||
// of grad_w / grad_b is deferred to the caller (via the existing
|
||||
// `reduce_axis0` infrastructure that `cfc/aux_trunk.rs` already
|
||||
// uses for its own per-batch grad scratch).
|
||||
// * `feedback_no_nvrtc.md` — pre-compiled cubin via build.rs.
|
||||
// * `feedback_nvidia_grade_perf_for_kernels.md` — warp-uniform launch,
|
||||
// no host branches, coalesced loads via cooperative h_aux staging
|
||||
// (one shared-memory pass per block — `pearl_cooperative_staging_eliminates_redundant_reads`).
|
||||
// * `pearl_no_host_branches_in_captured_graph` — kernel takes no flags
|
||||
// that affect control flow.
|
||||
//
|
||||
// Block layout: one block per batch sample, AUX_HIDDEN=64 threads per
|
||||
// block. Forward issues a fused matmul that produces all four outputs
|
||||
// per direction by routing the first N_OUTPUTS_PER_BLOCK threads to
|
||||
// distinct (head, horizon) slots. Backward uses thread role = hidden
|
||||
// unit c so that grad_h_aux (which sums over k across all four heads)
|
||||
// is the sole-writer responsibility of thread c; the parameter-grad
|
||||
// writes (grad_w[k, c], grad_b[k]) then loop k inside each thread c.
|
||||
|
||||
#define AUX_HIDDEN 64
|
||||
#define N_AUX_HORIZONS 3
|
||||
#define N_DIRS 2 // long, short
|
||||
#define N_HEADS_PER_DIR 2 // prof, size
|
||||
#define N_OUTPUTS (N_DIRS * N_HEADS_PER_DIR * N_AUX_HORIZONS) // 12
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// aux_heads_fwd: per-direction × per-head linear projection, batched.
|
||||
//
|
||||
// Launch:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (AUX_HIDDEN = 64, 1, 1)
|
||||
// shared_mem_bytes = AUX_HIDDEN * sizeof(float) (h_aux row stage)
|
||||
//
|
||||
// Outputs are written in one kernel launch:
|
||||
// prof_long_logit [batch, k] = b_prof_long [k] + Σ_c W_prof_long [k, c] * h_aux[batch, c]
|
||||
// size_long_pred [batch, k] = b_size_long [k] + Σ_c W_size_long [k, c] * h_aux[batch, c]
|
||||
// prof_short_logit[batch, k] = b_prof_short[k] + Σ_c W_prof_short[k, c] * h_aux[batch, c]
|
||||
// size_short_pred [batch, k] = b_size_short[k] + Σ_c W_size_short[k, c] * h_aux[batch, c]
|
||||
//
|
||||
// Layout: all four W_* are row-major [N_AUX_HORIZONS, AUX_HIDDEN]; all
|
||||
// four b_* are length N_AUX_HORIZONS.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void aux_heads_fwd(
|
||||
const float* __restrict__ w_prof_long, // [N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
const float* __restrict__ b_prof_long, // [N_AUX_HORIZONS]
|
||||
const float* __restrict__ w_size_long, // [N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
const float* __restrict__ b_size_long, // [N_AUX_HORIZONS]
|
||||
const float* __restrict__ w_prof_short, // [N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
const float* __restrict__ b_prof_short, // [N_AUX_HORIZONS]
|
||||
const float* __restrict__ w_size_short, // [N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
const float* __restrict__ b_size_short, // [N_AUX_HORIZONS]
|
||||
const float* __restrict__ h_aux, // [B × AUX_HIDDEN]
|
||||
int B,
|
||||
float* __restrict__ prof_long_logit, // [B × N_AUX_HORIZONS]
|
||||
float* __restrict__ size_long_pred, // [B × N_AUX_HORIZONS]
|
||||
float* __restrict__ prof_short_logit, // [B × N_AUX_HORIZONS]
|
||||
float* __restrict__ size_short_pred // [B × N_AUX_HORIZONS]
|
||||
) {
|
||||
extern __shared__ float s_h[]; // [AUX_HIDDEN]
|
||||
|
||||
const int batch = blockIdx.x;
|
||||
const int c = threadIdx.x; // 0..AUX_HIDDEN-1
|
||||
|
||||
if (batch >= B) return;
|
||||
|
||||
// Cooperative staging of h_aux[batch, *] into shared mem so every
|
||||
// thread's later dot-product reads from smem rather than re-fetching
|
||||
// the same row N_OUTPUTS times from DRAM.
|
||||
s_h[c] = h_aux[batch * AUX_HIDDEN + c];
|
||||
__syncthreads();
|
||||
|
||||
// Compute the 12 outputs (2 dirs × 2 heads × 3 horizons) via 12
|
||||
// threads. Remaining 52 threads sit idle this phase — block is
|
||||
// intentionally sized to AUX_HIDDEN so the *backward* kernel (which
|
||||
// is the heavier pass) gets full occupancy on h_aux/grad_h_aux.
|
||||
if (c < N_OUTPUTS) {
|
||||
// Output layout: thread index c encodes (dir, head, k) as
|
||||
// dir = c / (N_HEADS_PER_DIR * N_AUX_HORIZONS) ∈ {0,1}
|
||||
// head = (c / N_AUX_HORIZONS) % N_HEADS_PER_DIR ∈ {0=prof,1=size}
|
||||
// k = c % N_AUX_HORIZONS ∈ {0..2}
|
||||
const int dir = c / (N_HEADS_PER_DIR * N_AUX_HORIZONS);
|
||||
const int head = (c / N_AUX_HORIZONS) % N_HEADS_PER_DIR;
|
||||
const int k = c % N_AUX_HORIZONS;
|
||||
|
||||
// Route to the correct (W, b, out) triple. Branch is uniform
|
||||
// across the warp because all four if-branches dispatch on the
|
||||
// same (dir, head) — the warp uniformly executes a single chain.
|
||||
const float* w;
|
||||
const float* bv;
|
||||
float* out;
|
||||
if (dir == 0 && head == 0) {
|
||||
w = w_prof_long; bv = b_prof_long; out = prof_long_logit;
|
||||
} else if (dir == 0 && head == 1) {
|
||||
w = w_size_long; bv = b_size_long; out = size_long_pred;
|
||||
} else if (dir == 1 && head == 0) {
|
||||
w = w_prof_short; bv = b_prof_short; out = prof_short_logit;
|
||||
} else {
|
||||
w = w_size_short; bv = b_size_short; out = size_short_pred;
|
||||
}
|
||||
|
||||
float acc = bv[k];
|
||||
#pragma unroll
|
||||
for (int ci = 0; ci < AUX_HIDDEN; ++ci) {
|
||||
acc += w[k * AUX_HIDDEN + ci] * s_h[ci];
|
||||
}
|
||||
out[batch * N_AUX_HORIZONS + k] = acc;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// aux_heads_bwd: backward through the four per-direction × per-head
|
||||
// linear heads.
|
||||
//
|
||||
// Launch:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (AUX_HIDDEN = 64, 1, 1)
|
||||
// shared_mem_bytes = AUX_HIDDEN * sizeof(float) (h_aux row stage)
|
||||
//
|
||||
// Grad shapes (per-batch scratch; `+=` accumulate semantics — caller
|
||||
// reduces axis 0 via existing `reduce_axis0` infrastructure):
|
||||
// grad_w_prof_long : [B × N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
// grad_b_prof_long : [B × N_AUX_HORIZONS]
|
||||
// grad_w_size_long : [B × N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
// grad_b_size_long : [B × N_AUX_HORIZONS]
|
||||
// grad_w_prof_short : [B × N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
// grad_b_prof_short : [B × N_AUX_HORIZONS]
|
||||
// grad_w_size_short : [B × N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
// grad_b_size_short : [B × N_AUX_HORIZONS]
|
||||
// grad_h_aux : [B × AUX_HIDDEN] — to be added by the
|
||||
// trainer to the aux trunk's grad_h_new (no cross-
|
||||
// batch reduction needed — h_aux is per-batch).
|
||||
//
|
||||
// Inputs (per-element gradients from the loss kernels):
|
||||
// grad_prof_long_logit : [B × N_AUX_HORIZONS] — dL_BCE/dz, sigmoid fused
|
||||
// grad_size_long_pred : [B × N_AUX_HORIZONS] — dL_Huber/dy_size
|
||||
// grad_prof_short_logit : [B × N_AUX_HORIZONS]
|
||||
// grad_size_short_pred : [B × N_AUX_HORIZONS]
|
||||
//
|
||||
// Math (per batch, per head):
|
||||
// grad_w[k, c] = grad_out[k] * h_aux[c]
|
||||
// grad_b[k] = grad_out[k]
|
||||
// grad_h_aux[c] += Σ_k Σ_{head ∈ all four} grad_out[head][k] * W[head][k, c]
|
||||
//
|
||||
// Thread role: c = threadIdx.x is the hidden-unit dim. Each thread c
|
||||
// is the sole writer of:
|
||||
// * grad_w_*_*[batch, k, c] for all k (loops k internally)
|
||||
// * grad_h_aux[batch, c]
|
||||
// And the first N_AUX_HORIZONS threads additionally write grad_b for
|
||||
// each of the four heads.
|
||||
//
|
||||
// No atomicAdd; no cross-thread reduction needed for grad_w / grad_h_aux
|
||||
// because each thread owns disjoint output slots (per
|
||||
// `feedback_no_atomicadd.md`).
|
||||
//
|
||||
// Accumulation semantics (SDD-3 Layer B5 callers): grad_w / grad_b are
|
||||
// `+=` accumulated across the K-loop; grad_h_aux is `=` overwrite
|
||||
// (per-K consumer downstream). The trainer's per-K backward loop launches
|
||||
// this kernel multiple times over the K axis (gradient at each step k
|
||||
// re-enters the head with a different h_aux), and the per-batch param-
|
||||
// grad sum across K is the correct gradient. Caller is responsible for
|
||||
// zeroing the grad_w / grad_b scratches before the first K-iteration
|
||||
// (the trainer does this with the rest of its per-step scratch memsets).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void aux_heads_bwd(
|
||||
const float* __restrict__ w_prof_long, // [N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
const float* __restrict__ w_size_long, // [N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
const float* __restrict__ w_prof_short, // [N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
const float* __restrict__ w_size_short, // [N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
const float* __restrict__ h_aux, // [B × AUX_HIDDEN]
|
||||
const float* __restrict__ grad_prof_long_logit, // [B × N_AUX_HORIZONS]
|
||||
const float* __restrict__ grad_size_long_pred, // [B × N_AUX_HORIZONS]
|
||||
const float* __restrict__ grad_prof_short_logit, // [B × N_AUX_HORIZONS]
|
||||
const float* __restrict__ grad_size_short_pred, // [B × N_AUX_HORIZONS]
|
||||
int B,
|
||||
float* __restrict__ grad_w_prof_long, // [B × N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
float* __restrict__ grad_b_prof_long, // [B × N_AUX_HORIZONS]
|
||||
float* __restrict__ grad_w_size_long, // [B × N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
float* __restrict__ grad_b_size_long, // [B × N_AUX_HORIZONS]
|
||||
float* __restrict__ grad_w_prof_short, // [B × N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
float* __restrict__ grad_b_prof_short, // [B × N_AUX_HORIZONS]
|
||||
float* __restrict__ grad_w_size_short, // [B × N_AUX_HORIZONS × AUX_HIDDEN]
|
||||
float* __restrict__ grad_b_size_short, // [B × N_AUX_HORIZONS]
|
||||
float* __restrict__ grad_h_aux // [B × AUX_HIDDEN]
|
||||
) {
|
||||
extern __shared__ float s_h[]; // [AUX_HIDDEN]
|
||||
|
||||
const int batch = blockIdx.x;
|
||||
const int c = threadIdx.x; // 0..AUX_HIDDEN-1
|
||||
|
||||
if (batch >= B) return;
|
||||
|
||||
// Stage h_aux[batch, *] and the four per-head grad_out vectors. The
|
||||
// grad_out vectors are small (N_AUX_HORIZONS = 3 each) so we keep
|
||||
// them in shared via the first N_AUX_HORIZONS threads.
|
||||
s_h[c] = h_aux[batch * AUX_HIDDEN + c];
|
||||
|
||||
__shared__ float s_gpl[N_AUX_HORIZONS]; // grad_prof_long_logit
|
||||
__shared__ float s_gsl[N_AUX_HORIZONS]; // grad_size_long_pred
|
||||
__shared__ float s_gps[N_AUX_HORIZONS]; // grad_prof_short_logit
|
||||
__shared__ float s_gss[N_AUX_HORIZONS]; // grad_size_short_pred
|
||||
if (c < N_AUX_HORIZONS) {
|
||||
s_gpl[c] = grad_prof_long_logit [batch * N_AUX_HORIZONS + c];
|
||||
s_gsl[c] = grad_size_long_pred [batch * N_AUX_HORIZONS + c];
|
||||
s_gps[c] = grad_prof_short_logit[batch * N_AUX_HORIZONS + c];
|
||||
s_gss[c] = grad_size_short_pred [batch * N_AUX_HORIZONS + c];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// grad_b_*[batch, k] += grad_out_*[batch, k]
|
||||
// Sole writer: thread c == k (for k in [0, N_AUX_HORIZONS)).
|
||||
// `+=` per K-loop iteration sums each step's bias gradient.
|
||||
if (c < N_AUX_HORIZONS) {
|
||||
grad_b_prof_long [batch * N_AUX_HORIZONS + c] += s_gpl[c];
|
||||
grad_b_size_long [batch * N_AUX_HORIZONS + c] += s_gsl[c];
|
||||
grad_b_prof_short[batch * N_AUX_HORIZONS + c] += s_gps[c];
|
||||
grad_b_size_short[batch * N_AUX_HORIZONS + c] += s_gss[c];
|
||||
}
|
||||
|
||||
// For thread c (the hidden-unit dim) — accumulate grad_w[batch, k, c]
|
||||
// for each k (per-step h_aux varies across the K-loop), and write
|
||||
// grad_h_aux[batch, c] (consumed immediately by the per-step
|
||||
// aux_trunk_bwd, so OVERWRITE semantics — the K-loop doesn't read it
|
||||
// across iterations).
|
||||
const float h_c = s_h[c];
|
||||
float gh_acc = 0.0f;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_AUX_HORIZONS; ++k) {
|
||||
const float gpl_k = s_gpl[k];
|
||||
const float gsl_k = s_gsl[k];
|
||||
const float gps_k = s_gps[k];
|
||||
const float gss_k = s_gss[k];
|
||||
|
||||
const long long off =
|
||||
(long long)batch * N_AUX_HORIZONS * AUX_HIDDEN
|
||||
+ (long long)k * AUX_HIDDEN + c;
|
||||
|
||||
// grad_w_*[batch, k, c] += grad_out_*[batch, k] * h_aux[batch, c]
|
||||
grad_w_prof_long [off] += gpl_k * h_c;
|
||||
grad_w_size_long [off] += gsl_k * h_c;
|
||||
grad_w_prof_short[off] += gps_k * h_c;
|
||||
grad_w_size_short[off] += gss_k * h_c;
|
||||
|
||||
// grad_h_aux[batch, c] += Σ over all four heads
|
||||
gh_acc += gpl_k * w_prof_long [k * AUX_HIDDEN + c];
|
||||
gh_acc += gsl_k * w_size_long [k * AUX_HIDDEN + c];
|
||||
gh_acc += gps_k * w_prof_short[k * AUX_HIDDEN + c];
|
||||
gh_acc += gss_k * w_size_short[k * AUX_HIDDEN + c];
|
||||
}
|
||||
grad_h_aux[batch * AUX_HIDDEN + c] = gh_acc;
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
// aux_loss.cu — class-weighted BCE + NaN-masked Huber for the A+B
|
||||
// paired prof-binary + size-regression aux supervision (SDD-3 CB1/CB3/CB4).
|
||||
//
|
||||
// Per CB1's relabel: each (direction, horizon) emits TWO labels:
|
||||
// y_prof ∈ {0, 1} or NaN — did the trade hit profit before stop?
|
||||
// y_size ∈ ℝ or NaN — σ-normalised realised return (only when prof=1)
|
||||
//
|
||||
// Two loss kernels here:
|
||||
//
|
||||
// aux_bce_loss_fwd_bwd — sigmoid+BCE on (prof_logit, y_prof), with
|
||||
// per-horizon `pos_weight` for class balance
|
||||
// (`pos_weight = clamp(n_neg/n_pos, 1.0, 50.0)`
|
||||
// computed once per epoch on the loader side).
|
||||
// NaN in y_prof zeroes the gradient and
|
||||
// skips the loss contribution.
|
||||
//
|
||||
// aux_huber_masked_fwd_bwd — Huber on (y_size_hat, y_size_true). The
|
||||
// loader emits y_size = NaN whenever
|
||||
// y_prof = 0 (or the sample is invalid),
|
||||
// which gives the conditional-Huber
|
||||
// semantics for free: the NaN mask both
|
||||
// zeroes the gradient and skips the loss
|
||||
// contribution.
|
||||
//
|
||||
// Formulation:
|
||||
//
|
||||
// BCE (sigmoid fused, class-weighted):
|
||||
// p = 1 / (1 + exp(-z))
|
||||
// L_BCE = -[pos_weight * y * log(p) + (1-y) * log(1-p)]
|
||||
// dL_BCE/dz = (y > 0.5) ? pos_weight * (p - 1)
|
||||
// : p
|
||||
// (clamp log(·) at log(1e-7) to guard against p ∈ {0, 1} blowup;
|
||||
// `--use_fast_math` already produces approximate sigmoid so the
|
||||
// clamp is a defence-in-depth.)
|
||||
//
|
||||
// Huber (δ = 1.0 default):
|
||||
// r = y_hat - y_true
|
||||
// L_Huber = (|r| <= δ) ? 0.5 * r² : δ * (|r| - 0.5 * δ)
|
||||
// dL/dy_hat = (|r| <= δ) ? r : δ * sign(r)
|
||||
//
|
||||
// NaN-mask discipline: y_true may be NaN at masked positions; we MUST
|
||||
// zero the gradient AND skip the loss accumulator at those slots so
|
||||
// downstream `aux_heads_bwd` propagates zero through the head and the
|
||||
// `aux_trunk_bwd` sees no spurious gradient. The optimiser cannot eat
|
||||
// a NaN scalar — a single masked label without the mask would
|
||||
// poison the entire training step.
|
||||
//
|
||||
// Reduction discipline (per `feedback_no_atomicadd.md`):
|
||||
// * Per-thread strided accumulation into registers.
|
||||
// * Warp-shuffle reduce (`__shfl_xor_sync`) to one float per warp.
|
||||
// * One `__syncthreads` to gather warp partials in shared memory.
|
||||
// * Final warp-0 reduction to total loss / total valid count.
|
||||
// * No atomicAdd anywhere.
|
||||
//
|
||||
// Per-direction split: each kernel processes ONE direction × ONE head's
|
||||
// tensor at a time. The trainer calls each twice (once for long, once
|
||||
// for short) and sums the four scalars into the total aux loss. Keeping
|
||||
// the kernels per-(direction, head) keeps the block layout simple and
|
||||
// lets the per-direction telemetry (mean loss, valid count) emerge
|
||||
// for free.
|
||||
//
|
||||
// Block layout: grid = (1), block = (BLOCK = 256). Single block handles
|
||||
// the full [B × N_AUX_HORIZONS] grid (max ~32 × 3 = 96, easily within
|
||||
// stride reach). Mirrors the pattern in `bce_loss_multi_horizon.cu`.
|
||||
|
||||
#define AUX_LOSS_BLOCK 256
|
||||
#define AUX_LOSS_WARPS (AUX_LOSS_BLOCK / 32) // == 8
|
||||
#define N_AUX_HORIZONS 3
|
||||
|
||||
|
||||
__device__ __forceinline__ float warp_reduce_sum_f32(float v) {
|
||||
#pragma unroll
|
||||
for (int s = 16; s > 0; s >>= 1) {
|
||||
v += __shfl_xor_sync(0xffffffff, v, s);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
__device__ __forceinline__ int warp_reduce_sum_i32(int v) {
|
||||
#pragma unroll
|
||||
for (int s = 16; s > 0; s >>= 1) {
|
||||
v += __shfl_xor_sync(0xffffffff, v, s);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// aux_bce_loss_fwd_bwd:
|
||||
// Fused sigmoid + class-weighted BCE forward + per-element logit
|
||||
// gradient writeback for ONE direction (long or short) × the prof
|
||||
// head. Inputs/outputs are [B × N_AUX_HORIZONS] layout. NaN-masked
|
||||
// positions contribute zero loss and write zero gradient (no
|
||||
// contamination of downstream consumers).
|
||||
//
|
||||
// Inputs:
|
||||
// prof_logit [B × N_AUX_HORIZONS] — RAW logit z (no sigmoid)
|
||||
// y_prof_true [B × N_AUX_HORIZONS] — labels in {0, 1} or NaN
|
||||
// pos_weight [N_AUX_HORIZONS] — per-horizon positive-class
|
||||
// weight (uploaded host-side
|
||||
// from loader epoch stats).
|
||||
// n_total = B × N_AUX_HORIZONS
|
||||
//
|
||||
// Outputs:
|
||||
// loss_out [1] — Σ_valid L_BCE (UNREDUCED;
|
||||
// caller divides by valid_count
|
||||
// if a mean is desired).
|
||||
// grad_prof_logit [B × N_AUX_HORIZONS] — dL_BCE/dz per element.
|
||||
// valid_count_out [1] — #{(b, k) : isfinite(y_prof_true)}.
|
||||
//
|
||||
// `loss_out` is intentionally a SUM, not a mean — caller controls the
|
||||
// reduction policy without re-running the kernel.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void aux_bce_loss_fwd_bwd(
|
||||
const float* __restrict__ prof_logit, // [B × N_AUX_HORIZONS]
|
||||
const float* __restrict__ y_prof_true, // [B × N_AUX_HORIZONS] (NaN-maskable)
|
||||
const float* __restrict__ pos_weight, // [N_AUX_HORIZONS]
|
||||
int n_total, // = B × N_AUX_HORIZONS
|
||||
float* __restrict__ loss_out, // [1] — Σ L_BCE (unreduced)
|
||||
float* __restrict__ grad_prof_logit, // [B × N_AUX_HORIZONS]
|
||||
int* __restrict__ valid_count_out // [1]
|
||||
) {
|
||||
const int tid = threadIdx.x;
|
||||
const int lane = tid & 31;
|
||||
const int warp = tid >> 5;
|
||||
|
||||
// Stage `pos_weight[N_AUX_HORIZONS]` into shared so every strided
|
||||
// iteration reads from smem rather than re-fetching from DRAM.
|
||||
__shared__ float s_pw[N_AUX_HORIZONS];
|
||||
if (tid < N_AUX_HORIZONS) {
|
||||
s_pw[tid] = pos_weight[tid];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
float local_loss = 0.0f;
|
||||
int local_valid = 0;
|
||||
|
||||
for (int i = tid; i < n_total; i += AUX_LOSS_BLOCK) {
|
||||
const float y_t = y_prof_true[i];
|
||||
if (!isfinite(y_t)) {
|
||||
grad_prof_logit[i] = 0.0f;
|
||||
continue;
|
||||
}
|
||||
const int h = i % N_AUX_HORIZONS;
|
||||
const float pw = s_pw[h];
|
||||
const float z = prof_logit[i];
|
||||
// sigmoid(z) = 1/(1+exp(-z)). With --use_fast_math nvcc emits
|
||||
// the fast intrinsic; we clamp the log argument to avoid -inf
|
||||
// when p saturates to 0 or 1.
|
||||
const float p = 1.0f / (1.0f + expf(-z));
|
||||
const float p_c = fmaxf(p, 1e-7f);
|
||||
const float one_m_p = fmaxf(1.0f - p, 1e-7f);
|
||||
|
||||
// Class-weighted BCE: positive class gets weight `pw`, negative
|
||||
// class gets weight 1. Equivalent to scaling the positive-class
|
||||
// term in the standard formula.
|
||||
float L;
|
||||
float grad_z;
|
||||
if (y_t > 0.5f) {
|
||||
L = -pw * logf(p_c);
|
||||
grad_z = pw * (p - 1.0f);
|
||||
} else {
|
||||
L = -logf(one_m_p);
|
||||
grad_z = p;
|
||||
}
|
||||
local_loss += L;
|
||||
local_valid += 1;
|
||||
grad_prof_logit[i] = grad_z;
|
||||
}
|
||||
|
||||
// Warp-shuffle reduce.
|
||||
float warp_loss = warp_reduce_sum_f32(local_loss);
|
||||
int warp_valid = warp_reduce_sum_i32(local_valid);
|
||||
|
||||
__shared__ float s_loss[AUX_LOSS_WARPS];
|
||||
__shared__ int s_valid[AUX_LOSS_WARPS];
|
||||
if (lane == 0) {
|
||||
s_loss[warp] = warp_loss;
|
||||
s_valid[warp] = warp_valid;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (warp == 0) {
|
||||
float v_l = (lane < AUX_LOSS_WARPS) ? s_loss[lane] : 0.0f;
|
||||
int v_v = (lane < AUX_LOSS_WARPS) ? s_valid[lane] : 0;
|
||||
v_l = warp_reduce_sum_f32(v_l);
|
||||
v_v = warp_reduce_sum_i32(v_v);
|
||||
if (lane == 0) {
|
||||
loss_out[0] = v_l;
|
||||
valid_count_out[0] = v_v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// aux_huber_masked_fwd_bwd:
|
||||
// Fused Huber-loss forward + per-element gradient writeback for ONE
|
||||
// direction (long or short) × the size head. NaN-masked positions
|
||||
// contribute zero loss and write zero gradient.
|
||||
//
|
||||
// Conditional-Huber semantics: the loader (CB1) sets `y_size = NaN`
|
||||
// whenever `y_prof = 0` (no profit hit so the realised-return target is
|
||||
// undefined). The NaN-mask path here therefore implements the
|
||||
// conditional-Huber loss `L_Huber if y_prof=1 else 0` without needing a
|
||||
// separate prof-mask buffer — the loader has already encoded the
|
||||
// condition into the label tensor.
|
||||
//
|
||||
// Inputs:
|
||||
// y_size_hat [B × N_AUX_HORIZONS] — raw linear regression output
|
||||
// y_size_true [B × N_AUX_HORIZONS] — σ-normalised return or NaN
|
||||
// delta — Huber transition point (default 1.0)
|
||||
// n_total = B × N_AUX_HORIZONS
|
||||
//
|
||||
// Outputs:
|
||||
// loss_out [1] — Σ_valid L_Huber (unreduced)
|
||||
// grad_y_size_hat [B × N_AUX_HORIZONS] — dL/dy_size_hat per element
|
||||
// valid_count_out [1] — #{(b, k) : isfinite(y_size_true)}.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void aux_huber_masked_fwd_bwd(
|
||||
const float* __restrict__ y_size_hat, // [B × N_AUX_HORIZONS]
|
||||
const float* __restrict__ y_size_true, // [B × N_AUX_HORIZONS] (NaN-maskable)
|
||||
float delta, // Huber transition point
|
||||
int n_total, // = B × N_AUX_HORIZONS
|
||||
float* __restrict__ loss_out, // [1] — Σ L_Huber (unreduced)
|
||||
float* __restrict__ grad_y_size_hat, // [B × N_AUX_HORIZONS]
|
||||
int* __restrict__ valid_count_out // [1]
|
||||
) {
|
||||
const int tid = threadIdx.x;
|
||||
const int lane = tid & 31;
|
||||
const int warp = tid >> 5;
|
||||
|
||||
float local_loss = 0.0f;
|
||||
int local_valid = 0;
|
||||
|
||||
for (int i = tid; i < n_total; i += AUX_LOSS_BLOCK) {
|
||||
const float y_t = y_size_true[i];
|
||||
if (!isfinite(y_t)) {
|
||||
grad_y_size_hat[i] = 0.0f;
|
||||
continue;
|
||||
}
|
||||
const float y_p = y_size_hat[i];
|
||||
const float r = y_p - y_t;
|
||||
const float ar = fabsf(r);
|
||||
|
||||
float L, g;
|
||||
if (ar <= delta) {
|
||||
L = 0.5f * r * r;
|
||||
g = r;
|
||||
} else {
|
||||
L = delta * (ar - 0.5f * delta);
|
||||
g = copysignf(delta, r);
|
||||
}
|
||||
local_loss += L;
|
||||
local_valid += 1;
|
||||
grad_y_size_hat[i] = g;
|
||||
}
|
||||
|
||||
// Warp-shuffle reduce.
|
||||
float warp_loss = warp_reduce_sum_f32(local_loss);
|
||||
int warp_valid = warp_reduce_sum_i32(local_valid);
|
||||
|
||||
__shared__ float s_loss[AUX_LOSS_WARPS];
|
||||
__shared__ int s_valid[AUX_LOSS_WARPS];
|
||||
if (lane == 0) {
|
||||
s_loss[warp] = warp_loss;
|
||||
s_valid[warp] = warp_valid;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (warp == 0) {
|
||||
float v_l = (lane < AUX_LOSS_WARPS) ? s_loss[lane] : 0.0f;
|
||||
int v_v = (lane < AUX_LOSS_WARPS) ? s_valid[lane] : 0;
|
||||
v_l = warp_reduce_sum_f32(v_l);
|
||||
v_v = warp_reduce_sum_i32(v_v);
|
||||
if (lane == 0) {
|
||||
loss_out[0] = v_l;
|
||||
valid_count_out[0] = v_v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
// aux_trunk.cu — single-bucket CfC step (forward + backward) at AUX_HIDDEN=64.
|
||||
//
|
||||
// Per `pearl_separate_aux_trunk_when_shared_starves`: this is the kernel
|
||||
// behind a SECOND, smaller CfC trunk used for outcome-supervision (D-labels).
|
||||
// It runs in parallel with the main per-branch CfC trunk on the same encoder
|
||||
// output, with INDEPENDENT parameters (no shared w_in/w_rec/b/tau) so that
|
||||
// BCE gradient flow on the main trunk does not starve the aux supervision.
|
||||
//
|
||||
// Design contract:
|
||||
// • Single bucket — no per-horizon channel splitting, no
|
||||
// channels_in_bucket lookup, no bucket_dim_k. Aux supervision is
|
||||
// per-K at the head (B4 wiring), not at the τ-state.
|
||||
// • Hidden dim is AUX_HIDDEN=64 (half of main trunk's HIDDEN_DIM=128).
|
||||
// • Algorithm matches `cfc_step_per_branch.cu`: continuous-time CfC
|
||||
// decay update `h_new = h_old·decay + (1-decay)·tanh(pre)`, with
|
||||
// `decay = exp(-dt / max(tau, 1e-6))`.
|
||||
// • Input feature dim is parameterised (`feat_dim` runtime argument)
|
||||
// so a single cubin handles both raw-snap input (FEATURE_DIM=40) and
|
||||
// post-encoder input (HIDDEN_DIM=128). The host wrapper passes the
|
||||
// correct dim; the kernel iterates `feat_dim` in the input MVM.
|
||||
//
|
||||
// Per `feedback_no_atomicadd.md`: no atomicAdd anywhere. Each thread
|
||||
// writes to its own (batch, c) slot in forward, and to its own per-batch
|
||||
// grad slice in backward. Cross-batch reduction is the caller's
|
||||
// responsibility via the existing `reduce_axis0` infrastructure.
|
||||
//
|
||||
// Per `pearl_cooperative_staging_eliminates_redundant_reads`: the per-
|
||||
// batch `x` and `h_old` rows are SHARED across all output channels in a
|
||||
// block. Stage them into shared memory once at block entry so the K-loop
|
||||
// reads from smem instead of issuing AUX_HIDDEN × redundant DRAM reads.
|
||||
//
|
||||
// Per `pearl_no_host_branches_in_captured_graph`: no host branching;
|
||||
// the kernel takes feat_dim as a runtime arg but does not consult any
|
||||
// host-side state during execution.
|
||||
//
|
||||
// Per `feedback_nvidia_grade_perf_for_kernels.md`: warp-uniform branches
|
||||
// (the only conditional is `batch < B`, identical for every thread in a
|
||||
// block since batch == blockIdx.x), no atomicAdd, coalesced loads via
|
||||
// cooperative staging.
|
||||
|
||||
#define AUX_HIDDEN 64
|
||||
#define MAX_FEAT_DIM 128 // upper bound for shared-memory staging; covers
|
||||
// both FEATURE_DIM=40 and HIDDEN_DIM=128 callers
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// aux_trunk_fwd: forward pass.
|
||||
//
|
||||
// Launch:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (AUX_HIDDEN = 64, 1, 1)
|
||||
// shared_mem_bytes = (feat_dim + AUX_HIDDEN) * sizeof(float)
|
||||
//
|
||||
// Per (batch, c) thread computes:
|
||||
// pre = b[c] + Σ_k W_in[c, k] * x[batch, k] + Σ_k W_rec[c, k] * h_old[batch, k]
|
||||
// decay = exp(-dt / max(tau[c], 1e-6))
|
||||
// h_new[batch, c] = h_old[batch, c] * decay + (1 - decay) * tanh(pre)
|
||||
//
|
||||
// W_in is [AUX_HIDDEN × feat_dim], W_rec is [AUX_HIDDEN × AUX_HIDDEN].
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void aux_trunk_fwd(
|
||||
const float* __restrict__ w_in, // [AUX_HIDDEN × feat_dim]
|
||||
const float* __restrict__ w_rec, // [AUX_HIDDEN × AUX_HIDDEN]
|
||||
const float* __restrict__ b, // [AUX_HIDDEN]
|
||||
const float* __restrict__ tau, // [AUX_HIDDEN]
|
||||
const float* __restrict__ x, // [B × feat_dim]
|
||||
const float* __restrict__ h_old, // [B × AUX_HIDDEN]
|
||||
float dt,
|
||||
int B,
|
||||
int feat_dim,
|
||||
float* __restrict__ h_new // [B × AUX_HIDDEN]
|
||||
) {
|
||||
extern __shared__ float smem[];
|
||||
float* x_local = smem; // [feat_dim]
|
||||
float* h_old_local = smem + feat_dim; // [AUX_HIDDEN]
|
||||
|
||||
int batch = blockIdx.x;
|
||||
int c = threadIdx.x;
|
||||
|
||||
if (batch >= B) return;
|
||||
|
||||
// Cooperative staging of per-batch x[batch, *] and h_old[batch, *].
|
||||
// Each thread loads ceil(feat_dim / AUX_HIDDEN) = up to ~2 x slots
|
||||
// and exactly one h_old slot (since blockDim.x == AUX_HIDDEN).
|
||||
for (int i = c; i < feat_dim; i += AUX_HIDDEN) {
|
||||
x_local[i] = x[batch * feat_dim + i];
|
||||
}
|
||||
h_old_local[c] = h_old[batch * AUX_HIDDEN + c];
|
||||
__syncthreads();
|
||||
|
||||
// pre = b[c] + Σ_k W_in[c, k] * x_local[k] + Σ_k W_rec[c, k] * h_old_local[k]
|
||||
float pre = b[c];
|
||||
for (int k = 0; k < feat_dim; ++k) {
|
||||
pre += w_in[c * feat_dim + k] * x_local[k];
|
||||
}
|
||||
for (int k = 0; k < AUX_HIDDEN; ++k) {
|
||||
pre += w_rec[c * AUX_HIDDEN + k] * h_old_local[k];
|
||||
}
|
||||
|
||||
float decay = expf(-dt / fmaxf(tau[c], 1e-6f));
|
||||
h_new[batch * AUX_HIDDEN + c] =
|
||||
h_old_local[c] * decay + (1.0f - decay) * tanhf(pre);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// aux_trunk_bwd: backward pass.
|
||||
//
|
||||
// Launch:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (AUX_HIDDEN = 64, 1, 1)
|
||||
// shared_mem_bytes = (feat_dim + AUX_HIDDEN) * sizeof(float)
|
||||
//
|
||||
// Per-batch grad slices accumulation semantics (SDD-3 Layer B5 callers):
|
||||
// * grad_w_in / grad_w_rec / grad_b / grad_tau — `+=` per K-loop launch.
|
||||
// Multiple K-iterations sum into the same per-batch scratch (each step
|
||||
// has different x/h_old → different contribution), and reduce_axis0
|
||||
// at end of step collapses across B.
|
||||
// * grad_h_old / grad_x — `=` overwrite (consumed immediately by the
|
||||
// caller after each per-step launch: grad_h_old carries into k-1,
|
||||
// grad_x is accumulated into the encoder grad slot or discarded).
|
||||
// Caller must zero the four param-grad scratches at step start (the
|
||||
// trainer does this alongside the rest of its per-step scratch memsets).
|
||||
//
|
||||
// grad shapes:
|
||||
// grad_w_in : [B × AUX_HIDDEN × feat_dim] per-batch scratch
|
||||
// grad_w_rec : [B × AUX_HIDDEN × AUX_HIDDEN] per-batch scratch
|
||||
// grad_b : [B × AUX_HIDDEN] per-batch scratch
|
||||
// grad_tau : [B × AUX_HIDDEN] per-batch scratch
|
||||
// grad_h_old : [B × AUX_HIDDEN] per-batch (decay
|
||||
// contribution only;
|
||||
// cross-channel reduction
|
||||
// deferred to caller if
|
||||
// unrolled K>1)
|
||||
// grad_x : [B × feat_dim] per-batch (reduced
|
||||
// across c via shared-
|
||||
// memory tree-reduce)
|
||||
//
|
||||
// grad_x cross-channel reduction is done inside this kernel via a
|
||||
// block-resident shared-memory tree-reduce (no atomicAdd per
|
||||
// `feedback_no_atomicadd`).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void aux_trunk_bwd(
|
||||
const float* __restrict__ w_in, // [AUX_HIDDEN × feat_dim]
|
||||
const float* __restrict__ w_rec, // [AUX_HIDDEN × AUX_HIDDEN]
|
||||
const float* __restrict__ b, // [AUX_HIDDEN]
|
||||
const float* __restrict__ tau, // [AUX_HIDDEN]
|
||||
const float* __restrict__ x, // [B × feat_dim]
|
||||
const float* __restrict__ h_old, // [B × AUX_HIDDEN]
|
||||
const float* __restrict__ grad_h_new, // [B × AUX_HIDDEN]
|
||||
float dt,
|
||||
int B,
|
||||
int feat_dim,
|
||||
float* __restrict__ grad_w_in, // [B × AUX_HIDDEN × feat_dim]
|
||||
float* __restrict__ grad_w_rec, // [B × AUX_HIDDEN × AUX_HIDDEN]
|
||||
float* __restrict__ grad_b, // [B × AUX_HIDDEN]
|
||||
float* __restrict__ grad_tau, // [B × AUX_HIDDEN]
|
||||
float* __restrict__ grad_h_old, // [B × AUX_HIDDEN]
|
||||
float* __restrict__ grad_x // [B × feat_dim]
|
||||
) {
|
||||
// Shared layout:
|
||||
// [0 .. feat_dim) : x_local
|
||||
// [feat_dim .. feat_dim+AH) : h_old_local
|
||||
// [feat_dim+AH .. feat_dim+AH+AH) : d_pre_smem (per-channel d_pre
|
||||
// for cross-channel grad_x reduce)
|
||||
extern __shared__ float smem[];
|
||||
float* x_local = smem;
|
||||
float* h_old_local = smem + feat_dim;
|
||||
float* d_pre_smem = smem + feat_dim + AUX_HIDDEN;
|
||||
|
||||
int batch = blockIdx.x;
|
||||
int c = threadIdx.x;
|
||||
|
||||
if (batch >= B) return;
|
||||
|
||||
// Cooperative staging — identical pattern to forward.
|
||||
for (int i = c; i < feat_dim; i += AUX_HIDDEN) {
|
||||
x_local[i] = x[batch * feat_dim + i];
|
||||
}
|
||||
h_old_local[c] = h_old[batch * AUX_HIDDEN + c];
|
||||
__syncthreads();
|
||||
|
||||
// Recompute forward pre + tanh to derive d_pre.
|
||||
float pre = b[c];
|
||||
for (int k = 0; k < feat_dim; ++k) {
|
||||
pre += w_in[c * feat_dim + k] * x_local[k];
|
||||
}
|
||||
for (int k = 0; k < AUX_HIDDEN; ++k) {
|
||||
pre += w_rec[c * AUX_HIDDEN + k] * h_old_local[k];
|
||||
}
|
||||
const float tau_eps = 1e-6f;
|
||||
const float tau_raw = tau[c];
|
||||
const float tau_eff = fmaxf(tau_raw, tau_eps);
|
||||
const float decay = expf(-dt / tau_eff);
|
||||
const float s = tanhf(pre);
|
||||
const float dh = grad_h_new[batch * AUX_HIDDEN + c];
|
||||
const float d_pre = dh * (1.0f - decay) * (1.0f - s * s);
|
||||
const float d_decay = dh * (h_old_local[c] - s);
|
||||
|
||||
// Per-batch grad-scratch writes. Each thread (batch, c) is the sole
|
||||
// writer of grad_*[batch, c, ...] — no race, no atomicAdd.
|
||||
// Param-grad scratches use `+=` so the trainer's reverse-K loop sums
|
||||
// contributions across all K positions (each with different x/h_old
|
||||
// through the same params).
|
||||
grad_b[batch * AUX_HIDDEN + c] += d_pre;
|
||||
|
||||
// grad_tau receives 0 when tau hits the clamp floor
|
||||
// (strict d(max)/d(tau) = 0 below tau_eps).
|
||||
const float gate = (tau_raw > tau_eps) ? 1.0f : 0.0f;
|
||||
grad_tau[batch * AUX_HIDDEN + c] +=
|
||||
gate * d_decay * decay * dt / (tau_eff * tau_eff);
|
||||
|
||||
// grad_w_in[batch, c, k] += d_pre * x_local[k]
|
||||
for (int k = 0; k < feat_dim; ++k) {
|
||||
const long long off =
|
||||
(long long)batch * AUX_HIDDEN * feat_dim
|
||||
+ (long long)c * feat_dim + k;
|
||||
grad_w_in[off] += d_pre * x_local[k];
|
||||
}
|
||||
// grad_w_rec[batch, c, k] += d_pre * h_old_local[k]
|
||||
for (int k = 0; k < AUX_HIDDEN; ++k) {
|
||||
const long long off =
|
||||
(long long)batch * AUX_HIDDEN * AUX_HIDDEN
|
||||
+ (long long)c * AUX_HIDDEN + k;
|
||||
grad_w_rec[off] += d_pre * h_old_local[k];
|
||||
}
|
||||
|
||||
// grad_h_old[batch, c] direct decay contribution. OVERWRITE — the
|
||||
// trainer's per-step DtoD copy reads this immediately into the next
|
||||
// K iteration's carry buffer; no cross-K accumulation here.
|
||||
// (Cross-channel term Σ_j d_pre[j] * W_rec[j, c] is left for the
|
||||
// caller to add if BPTT through h_old is needed — for K=1 per-step
|
||||
// aux supervision this direct term is sufficient.)
|
||||
grad_h_old[batch * AUX_HIDDEN + c] = dh * decay;
|
||||
|
||||
// grad_x[batch, k] = Σ_c d_pre[c] * W_in[c, k]
|
||||
// Cross-channel reduction via shared memory: stash d_pre to smem,
|
||||
// then have each thread compute its assigned k-slice by summing
|
||||
// across c. This avoids atomicAdd while keeping the reduction
|
||||
// block-local. (Per pearl_no_atomicadd + the block tree-reduce
|
||||
// pattern used in reduce_axis0.cu.)
|
||||
d_pre_smem[c] = d_pre;
|
||||
__syncthreads();
|
||||
|
||||
// Each of the AUX_HIDDEN threads handles a strided subset of feat_dim
|
||||
// output positions. With feat_dim ≤ MAX_FEAT_DIM = 128 and
|
||||
// AUX_HIDDEN = 64, each thread covers at most 2 k positions.
|
||||
for (int k = c; k < feat_dim; k += AUX_HIDDEN) {
|
||||
float acc = 0.0f;
|
||||
for (int j = 0; j < AUX_HIDDEN; ++j) {
|
||||
acc += d_pre_smem[j] * w_in[j * feat_dim + k];
|
||||
}
|
||||
grad_x[batch * feat_dim + k] = acc;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// SDD-3 Layer B5 — aux→encoder gradient accumulator.
|
||||
//
|
||||
// Single-purpose kernel that performs an element-wise += of a source
|
||||
// vector into a destination vector. Used by `PerceptionTrainer` to fold
|
||||
// the auxiliary trunk's `grad_x` (computed at each K-step of the
|
||||
// reverse-order aux backward) into the main `grad_h_enriched_seq_t_d`
|
||||
// slot when the asymmetric stop-grad is LIFTED (E3 design memo).
|
||||
//
|
||||
// Why a dedicated kernel rather than reusing an existing add primitive:
|
||||
// * The other ml-alpha kernels that mutate `grad_h_enriched_seq` write
|
||||
// OVERWRITE-style (cfc_step_bwd produces the slot's gradient by
|
||||
// itself). Adding aux on top requires an independent += launch.
|
||||
// * Per `feedback_no_atomicadd.md` — this is purely position-local
|
||||
// (one thread per (b, c)) so no atomics; the destination slot is
|
||||
// write-once per launch.
|
||||
//
|
||||
// Buffer contract:
|
||||
// * `dst` — `[n]` destination, accumulated in-place: `dst[i] += src[i]`
|
||||
// * `src` — `[n]` source (read-only)
|
||||
// * `n` — element count
|
||||
//
|
||||
// Launch: grid=(ceil(n/256), 1, 1), block=(256, 1, 1), shared=0.
|
||||
//
|
||||
// Constraints honoured:
|
||||
// * `feedback_no_atomicadd.md` — single-thread-per-element write.
|
||||
// * `pearl_no_host_branches_in_captured_graph` — the captured graph's
|
||||
// scalar `n` is bound at capture time; replay reads the same value.
|
||||
// * `feedback_no_nvrtc.md` — pre-compiled cubin via build.rs.
|
||||
|
||||
extern "C" __global__ void aux_vec_add_inplace(
|
||||
float* __restrict__ dst,
|
||||
const float* __restrict__ src,
|
||||
int n
|
||||
) {
|
||||
const int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= n) return;
|
||||
dst[i] += src[i];
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
// bce_loss_multi_horizon.cu — base-weight-only multi-horizon BCE.
|
||||
//
|
||||
// Single source of truth for the multi-horizon BCE. Loss is the
|
||||
// base-weight-scaled per-horizon mean of unweighted BCE — no Kendall
|
||||
// σ damping (2026-05-18). Per-horizon prioritization is done outside
|
||||
// this kernel by the ISV-driven λ controller scaling the trunk
|
||||
// grad_h contribution in heads_grn_bwd (per `pearl_adam_normalizes_loss_weights`
|
||||
// — affect the gradient, not the loss aggregate).
|
||||
//
|
||||
// Math:
|
||||
//
|
||||
// raw_bce_h = Σ_{i in horizon h, m_i = 1} L_i (unweighted sum)
|
||||
// count_h = #{i in horizon h : m_i = 1}
|
||||
// mean_bce_h = raw_bce_h / count_h
|
||||
// w_h = base_weight_h (typically 1)
|
||||
// total_loss = Σ_h [ w_h * mean_bce_h ]
|
||||
//
|
||||
// Gradients:
|
||||
// d L / d p_i = m_i * (w_h / count_h) * (p − y) / (p (1 − p))
|
||||
// d L / d log_sigma_h = 0 (σ is a sidecar — emitted by
|
||||
// horizon_ema_and_lambda for telemetry,
|
||||
// not used in the loss path)
|
||||
//
|
||||
// Per `feedback_nvidia_grade_perf_for_kernels`:
|
||||
// - Warp-shuffle reduction (`__shfl_xor_sync`), NOT block tree-reduce.
|
||||
// - One `__syncthreads` for the cross-warp aggregate, total = O(1) barriers.
|
||||
// - Inactive lanes contribute 0 via ternary; never divergent shuffles.
|
||||
// - Hard-coded N_HORIZONS_BCE = 3 → per-warp 3-element accumulator stays in registers.
|
||||
//
|
||||
// Block layout: grid = (1), block = (256) = 8 warps. Single block over
|
||||
// the full [n_pos, n_horizons] grid is fine — we never have > 256 ×
|
||||
// (per-thread fan-in) positions in the trainer hot loop (max K × B =
|
||||
// 64 × 8 = 512, well within stride-loop reach).
|
||||
|
||||
#define BCS_N_HORIZONS 3
|
||||
#define BCS_BLOCK 256
|
||||
#define BCS_N_WARPS (BCS_BLOCK / 32) // == 8
|
||||
|
||||
__device__ __forceinline__ float warp_reduce_sum(float v) {
|
||||
#pragma unroll
|
||||
for (int s = 16; s > 0; s >>= 1) {
|
||||
v += __shfl_xor_sync(0xffffffff, v, s);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
__device__ __forceinline__ int warp_reduce_sum_int(int v) {
|
||||
#pragma unroll
|
||||
for (int s = 16; s > 0; s >>= 1) {
|
||||
v += __shfl_xor_sync(0xffffffff, v, s);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
extern "C" __global__ void bce_multi_horizon_forward_backward(
|
||||
const float* __restrict__ probs, // [n_pos * n_horizons]
|
||||
const float* __restrict__ labels, // [n_pos * n_horizons]
|
||||
const float* __restrict__ base_weights, // [n_horizons] (per-horizon λ from ISV; constants OK)
|
||||
const float* __restrict__ log_sigma_h, // [n_horizons] (learnable Kendall σ logarithm)
|
||||
int n_pos,
|
||||
int n_horizons,
|
||||
float* __restrict__ loss_out, // [1] — total Kendall loss
|
||||
float* __restrict__ mean_bce_per_h_out, // [n_horizons] — unweighted per-horizon mean (ISV signal)
|
||||
float* __restrict__ grad_probs, // [n_pos * n_horizons] — ∂L/∂p
|
||||
int* __restrict__ valid_count_out, // [1] — Σ_i m_i (unweighted)
|
||||
float* __restrict__ d_log_sigma_h // [n_horizons] — ∂L/∂log_sigma_h
|
||||
) {
|
||||
const int tid = threadIdx.x;
|
||||
const int lane = tid & 31;
|
||||
const int warp = tid >> 5;
|
||||
const int total = n_pos * n_horizons;
|
||||
|
||||
// Per-thread per-horizon partials in registers.
|
||||
float local_raw[BCS_N_HORIZONS];
|
||||
float local_cnt[BCS_N_HORIZONS];
|
||||
#pragma unroll
|
||||
for (int h = 0; h < BCS_N_HORIZONS; ++h) {
|
||||
local_raw[h] = 0.0f;
|
||||
local_cnt[h] = 0.0f;
|
||||
}
|
||||
int local_valid = 0;
|
||||
|
||||
// Pass 1 — per-thread strided accumulation. Same loop also zeros
|
||||
// grad_probs at masked positions so the second pass can skip those.
|
||||
for (int i = tid; i < total; i += BCS_BLOCK) {
|
||||
const float y = labels[i];
|
||||
if (isnan(y)) {
|
||||
grad_probs[i] = 0.0f;
|
||||
continue;
|
||||
}
|
||||
const int h = i % n_horizons;
|
||||
const float p = fminf(fmaxf(probs[i], 1e-6f), 1.0f - 1e-6f);
|
||||
const float L = -(y * logf(p) + (1.0f - y) * logf(1.0f - p));
|
||||
local_raw[h] += L;
|
||||
local_cnt[h] += 1.0f;
|
||||
local_valid += 1;
|
||||
}
|
||||
|
||||
// Warp-shuffle reduce each of the 3 horizon sums + the per-horizon
|
||||
// count + the global valid count. Each lane in warp gets the full
|
||||
// warp-sum at the end (no need to broadcast manually).
|
||||
float warp_raw[BCS_N_HORIZONS];
|
||||
float warp_cnt[BCS_N_HORIZONS];
|
||||
#pragma unroll
|
||||
for (int h = 0; h < BCS_N_HORIZONS; ++h) {
|
||||
warp_raw[h] = warp_reduce_sum(local_raw[h]);
|
||||
warp_cnt[h] = warp_reduce_sum(local_cnt[h]);
|
||||
}
|
||||
int warp_valid = warp_reduce_sum_int(local_valid);
|
||||
|
||||
// Cross-warp reduce — only lane 0 of each warp writes its partial.
|
||||
__shared__ float s_raw[BCS_N_WARPS * BCS_N_HORIZONS];
|
||||
__shared__ float s_cnt[BCS_N_WARPS * BCS_N_HORIZONS];
|
||||
__shared__ int s_valid[BCS_N_WARPS];
|
||||
if (lane == 0) {
|
||||
#pragma unroll
|
||||
for (int h = 0; h < BCS_N_HORIZONS; ++h) {
|
||||
s_raw[warp * BCS_N_HORIZONS + h] = warp_raw[h];
|
||||
s_cnt[warp * BCS_N_HORIZONS + h] = warp_cnt[h];
|
||||
}
|
||||
s_valid[warp] = warp_valid;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Warp 0 finishes: reduce N_WARPS partials. All 32 lanes participate;
|
||||
// inactive lanes (tid ≥ N_WARPS) contribute 0 via ternary (NOT a
|
||||
// divergent shuffle).
|
||||
__shared__ float mean_bce_h_shared[BCS_N_HORIZONS];
|
||||
__shared__ float w_eff_shared[BCS_N_HORIZONS];
|
||||
__shared__ int valid_count_shared;
|
||||
|
||||
if (warp == 0) {
|
||||
#pragma unroll
|
||||
for (int h = 0; h < BCS_N_HORIZONS; ++h) {
|
||||
float v_raw = (lane < BCS_N_WARPS) ? s_raw[lane * BCS_N_HORIZONS + h] : 0.0f;
|
||||
float v_cnt = (lane < BCS_N_WARPS) ? s_cnt[lane * BCS_N_HORIZONS + h] : 0.0f;
|
||||
v_raw = warp_reduce_sum(v_raw);
|
||||
v_cnt = warp_reduce_sum(v_cnt);
|
||||
if (lane == 0) {
|
||||
const float c_h = v_cnt;
|
||||
const float mean_bce = (c_h > 0.0f) ? v_raw / c_h : 0.0f;
|
||||
mean_bce_h_shared[h] = mean_bce;
|
||||
mean_bce_per_h_out[h] = mean_bce;
|
||||
|
||||
// σ sidecar (2026-05-18): Kendall σ damped every horizon
|
||||
// by ~30% (σ ≈ √mean_bce ≈ 0.84 → w_h ≈ 0.71) and biased
|
||||
// the encoder away from h6000 — empirically -2pp mean_auc
|
||||
// vs Phase 1+2+3 across multiple architectures. The
|
||||
// Kendall framework's assumption ("damp the noisier
|
||||
// task") inverts what we need ("learn the harder
|
||||
// horizon harder"). σ is now emitted by
|
||||
// horizon_ema_and_lambda for telemetry only and does
|
||||
// NOT participate in the BCE forward weighting or
|
||||
// backward gradient. `log_sigma_h` arg is preserved
|
||||
// for kernel-signature stability.
|
||||
const float bw = (base_weights != nullptr) ? base_weights[h] : 1.0f;
|
||||
const float w_h = bw;
|
||||
w_eff_shared[h] = (c_h > 0.0f) ? w_h / c_h : 0.0f;
|
||||
d_log_sigma_h[h] = 0.0f;
|
||||
}
|
||||
}
|
||||
int v_valid = (lane < BCS_N_WARPS) ? s_valid[lane] : 0;
|
||||
v_valid = warp_reduce_sum_int(v_valid);
|
||||
if (lane == 0) {
|
||||
valid_count_shared = v_valid;
|
||||
valid_count_out[0] = v_valid;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Total loss — small serial reduce by thread 0 over 3 horizons.
|
||||
// σ-Kendall regularizer (log σ_h term) intentionally absent — see
|
||||
// sidecar note above. Loss is unweighted per-horizon BCE sum,
|
||||
// scaled only by `base_weights[h]` (typically uniform [1,1,1]).
|
||||
if (tid == 0) {
|
||||
float total_loss = 0.0f;
|
||||
#pragma unroll
|
||||
for (int h = 0; h < BCS_N_HORIZONS; ++h) {
|
||||
const float bw = (base_weights != nullptr) ? base_weights[h] : 1.0f;
|
||||
total_loss += bw * mean_bce_h_shared[h];
|
||||
}
|
||||
loss_out[0] = total_loss;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 2 — per-element grad. Coalesced strided access. w_eff_shared
|
||||
// is read-only at this point, broadcast freely across threads.
|
||||
for (int i = tid; i < total; i += BCS_BLOCK) {
|
||||
const float y = labels[i];
|
||||
if (isnan(y)) {
|
||||
continue; // grad was already zeroed in pass 1
|
||||
}
|
||||
const int h = i % n_horizons;
|
||||
const float p = fminf(fmaxf(probs[i], 1e-6f), 1.0f - 1e-6f);
|
||||
grad_probs[i] = w_eff_shared[h] * (p - y) / (p * (1.0f - p));
|
||||
}
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
// bellman_target_projection.cu — categorical (C51) projection of the
|
||||
// Bellman target Z(s_{t+1}, a*) onto the discrete support [V_MIN, V_MAX]
|
||||
// with Q_N_ATOMS atoms and Δ_z = (V_MAX - V_MIN) / (Q_N_ATOMS - 1) = 0.1.
|
||||
//
|
||||
// Standard C51 target projection (Bellemare et al. 2017):
|
||||
// 1. Compute target atom values: T_z[j] = r + γ × (1 - done) × atom_value[j]
|
||||
// (clamped to [V_MIN, V_MAX])
|
||||
// 2. For each target atom T_z[j], distribute its probability mass
|
||||
// across the two NEAREST support atoms (linear interpolation):
|
||||
// l = floor((T_z[j] - V_MIN) / Δ_z) (clamp to [0, Q_N_ATOMS-1])
|
||||
// u = ceil (...)
|
||||
// target_dist[l] += target_probs[j] × (u - b_frac)
|
||||
// target_dist[u] += target_probs[j] × (b_frac - l)
|
||||
// where b_frac = (T_z - V_MIN) / Δ_z.
|
||||
//
|
||||
// γ is read from isv[RL_GAMMA_INDEX=400] per pearl_controller_anchors_isv_driven.
|
||||
// γ is clamped to [0, 1] defensively — first-observation bootstrap (γ = 0)
|
||||
// is OK (one-step bandit), the controller fills in the real anchor at the
|
||||
// next emit.
|
||||
//
|
||||
// Inputs:
|
||||
// target_logits [B × Q_N_ATOMS] — target-net's atom logits at the
|
||||
// argmax-Q action of s_{t+1}
|
||||
// rewards [B] — per-transition reward r_t
|
||||
// dones [B] — 0/1 done flag (multiplies γ × 0 at
|
||||
// terminal so V(s') is zeroed)
|
||||
// isv [≥ 401] — read γ at index 400
|
||||
// B — batch size
|
||||
//
|
||||
// Outputs:
|
||||
// target_dist [B × Q_N_ATOMS] — projected target distribution
|
||||
// (sums to 1 per batch row by construction)
|
||||
//
|
||||
// Block layout:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (Q_N_ATOMS, 1, 1) — one thread per source/target atom
|
||||
//
|
||||
// Per `feedback_no_atomicadd.md`: no atomicAdd. Cross-thread accumulation
|
||||
// uses shared memory + per-source-atom serial writes bracketed by
|
||||
// __syncthreads — Q_N_ATOMS = 21 inner-loop syncs is negligible (single
|
||||
// block per batch, small block size).
|
||||
|
||||
#define Q_N_ATOMS 21
|
||||
#define N_ACTIONS 11
|
||||
// V_MIN/V_MAX/DELTA_Z are now ISV-driven per audit 2026-05-24 followup
|
||||
// so adaptive reward clamps also lift Q's distributional support.
|
||||
// Slots RL_C51_V_MIN_INDEX/RL_C51_V_MAX_INDEX track the active reward
|
||||
// range via a symmetric slow EWMA (α=0.001, half-life ~700 steps) on
|
||||
// the WIN/LOSS clamp bounds — see `rl_reward_clamp_controller.cu`
|
||||
// Step 5 (lines 72-81 for design rationale, 333-350 for the update).
|
||||
// Floored at [-1, +1] per pearl_c51_v_max_freeze_required_for_surfer
|
||||
// (V_MAX in 100-200 → trend-follower; past 1000 → degraded). The
|
||||
// earlier "ratchet (monotone-grow)" comment here was stale — superseded
|
||||
// by the wwcsz followup 2026-05-24 which replaced the ratchet to focus
|
||||
// atom resolution on the active range. B-9's saturation observability
|
||||
// (slots 726-729) surfaces if the EWMA decay over-shrinks the span.
|
||||
#define RL_C51_V_MAX_INDEX 484
|
||||
#define RL_C51_V_MIN_INDEX 485
|
||||
#define RL_GAMMA_INDEX 400
|
||||
|
||||
// B-9 (2026-06-01): per-batch scratch outputs for the C51 Bellman-target
|
||||
// saturation diagnostic. Each `[B]` array receives one value per block;
|
||||
// the cross-batch reduce kernel `rl_bellman_target_saturation_reduce`
|
||||
// folds them into ISV slots 726-729. Per `feedback_no_atomicadd.md`:
|
||||
// scratch + tree-reduce; no atomicAdd anywhere. The sequential thread-0
|
||||
// reduction below matches the existing softmax max/sum pattern (lines
|
||||
// 157 / 171 / 234 / 248) — avoids the odd-`Q_N_ATOMS=21` tree-reduce
|
||||
// partner-skip bug.
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// dqn_select_action_atoms — extract per-batch atom row at the requested
|
||||
// action index from the full target-net logits tensor.
|
||||
//
|
||||
// in: full_logits [B × N_ACTIONS × Q_N_ATOMS]
|
||||
// actions [B] (0..N_ACTIONS)
|
||||
// out: action_logits[B × Q_N_ATOMS]
|
||||
//
|
||||
// One block per batch element, Q_N_ATOMS threads per block. Each thread
|
||||
// copies one atom of the selected action's row.
|
||||
//
|
||||
// Out-of-range action indices are clamped to 0 — defensive only; the
|
||||
// integrated trainer enforces bound-checking at the loader boundary.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void dqn_select_action_atoms(
|
||||
const float* __restrict__ full_logits, // [B × N_ACTIONS × Q_N_ATOMS]
|
||||
const int* __restrict__ actions, // [B]
|
||||
int B,
|
||||
float* __restrict__ action_logits // [B × Q_N_ATOMS]
|
||||
) {
|
||||
const int batch = blockIdx.x;
|
||||
const int atom = threadIdx.x;
|
||||
if (batch >= B || atom >= Q_N_ATOMS) return;
|
||||
|
||||
int a = actions[batch];
|
||||
if (a < 0) a = 0;
|
||||
if (a >= N_ACTIONS) a = 0;
|
||||
|
||||
const long long src_idx =
|
||||
(long long)batch * N_ACTIONS * Q_N_ATOMS
|
||||
+ (long long)a * Q_N_ATOMS
|
||||
+ (long long)atom;
|
||||
const long long dst_idx = (long long)batch * Q_N_ATOMS + (long long)atom;
|
||||
action_logits[dst_idx] = full_logits[src_idx];
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// bellman_fused_select_project — fused kernel combining
|
||||
// `dqn_select_action_atoms` + `bellman_target_projection` into a
|
||||
// single launch. Eliminates the intermediate `action_logits[B, Q_N_ATOMS]`
|
||||
// global memory round-trip by staging the selected atom row in shared
|
||||
// memory.
|
||||
//
|
||||
// nsys profiling showed the two kernels always launch back-to-back with
|
||||
// identical grid/block dims: Grid=(B, 1, 1), Block=(Q_N_ATOMS, 1, 1).
|
||||
// The intermediate `action_logits[B, Q_N_ATOMS]` exists only to shuttle
|
||||
// data from the first kernel's output to the second kernel's input.
|
||||
// Fusing them saves one global write + one global read of B*Q_N_ATOMS
|
||||
// floats and one kernel launch overhead (~1.2μs avg).
|
||||
//
|
||||
// Inputs:
|
||||
// full_logits [B × N_ACTIONS × Q_N_ATOMS] — target-net atom logits
|
||||
// actions [B] — action indices (0..N_ACTIONS)
|
||||
// rewards [B] — per-transition reward r_t
|
||||
// dones [B] — 0/1 done flag
|
||||
// n_step_gammas [B] — γⁿ per transition
|
||||
// isv [≥ 486] — reads V_MIN/V_MAX/γ
|
||||
// B int — batch size
|
||||
//
|
||||
// Outputs:
|
||||
// target_dist [B × Q_N_ATOMS] — projected target distribution
|
||||
//
|
||||
// Block layout:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (Q_N_ATOMS, 1, 1)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void bellman_fused_select_project(
|
||||
const float* __restrict__ full_logits, // [B × N_ACTIONS × Q_N_ATOMS]
|
||||
const int* __restrict__ actions, // [B]
|
||||
const float* __restrict__ rewards, // [B]
|
||||
const float* __restrict__ dones, // [B]
|
||||
const float* __restrict__ n_step_gammas, // [B]
|
||||
const float* __restrict__ isv, // ≥ 486
|
||||
int B,
|
||||
float* __restrict__ target_dist, // [B × Q_N_ATOMS]
|
||||
// B-9 (2026-06-01): per-batch saturation tally scratch. Each block
|
||||
// (one per batch element) writes one value to each. Consumed by
|
||||
// `rl_bellman_target_saturation_reduce` after this kernel returns.
|
||||
float* __restrict__ sat_top_per_batch, // [B]
|
||||
float* __restrict__ sat_bot_per_batch, // [B]
|
||||
float* __restrict__ max_pre_per_batch, // [B]
|
||||
float* __restrict__ min_pre_per_batch // [B]
|
||||
) {
|
||||
const int batch = blockIdx.x;
|
||||
const int atom = threadIdx.x;
|
||||
if (batch >= B || atom >= Q_N_ATOMS) return;
|
||||
|
||||
// ── Phase 1: select action atoms into shared memory ─────────────
|
||||
__shared__ float s_logits[Q_N_ATOMS];
|
||||
|
||||
int a = actions[batch];
|
||||
if (a < 0) a = 0;
|
||||
if (a >= N_ACTIONS) a = 0;
|
||||
|
||||
const long long src_idx =
|
||||
(long long)batch * N_ACTIONS * Q_N_ATOMS
|
||||
+ (long long)a * Q_N_ATOMS
|
||||
+ (long long)atom;
|
||||
s_logits[atom] = full_logits[src_idx];
|
||||
__syncthreads();
|
||||
|
||||
// ── Phase 2: bellman target projection from shared memory ───────
|
||||
__shared__ float s_softmax[Q_N_ATOMS];
|
||||
__shared__ float s_max;
|
||||
__shared__ float s_sumexp;
|
||||
__shared__ float s_proj[Q_N_ATOMS];
|
||||
|
||||
// Softmax over target logits (numerically-stable max-subtract)
|
||||
if (atom == 0) {
|
||||
float m = s_logits[0];
|
||||
#pragma unroll
|
||||
for (int z = 1; z < Q_N_ATOMS; ++z)
|
||||
m = fmaxf(m, s_logits[z]);
|
||||
s_max = m;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
const float e = expf(s_logits[atom] - s_max);
|
||||
s_softmax[atom] = e;
|
||||
s_proj[atom] = 0.0f;
|
||||
__syncthreads();
|
||||
|
||||
if (atom == 0) {
|
||||
float sum = 0.0f;
|
||||
#pragma unroll
|
||||
for (int z = 0; z < Q_N_ATOMS; ++z) sum += s_softmax[z];
|
||||
s_sumexp = sum;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
const float p = s_softmax[atom] / s_sumexp;
|
||||
|
||||
const float r = rewards[batch];
|
||||
const float gamma_eff = n_step_gammas[batch];
|
||||
|
||||
const float V_MIN_eff = isv[RL_C51_V_MIN_INDEX];
|
||||
const float V_MAX_eff = isv[RL_C51_V_MAX_INDEX];
|
||||
const float DELTA_Z = (V_MAX_eff - V_MIN_eff) / (float)(Q_N_ATOMS - 1);
|
||||
|
||||
const float atom_value = V_MIN_eff + (float)atom * DELTA_Z;
|
||||
const float t_z = r + gamma_eff * atom_value;
|
||||
const float t_z_clamp = fmaxf(V_MIN_eff, fminf(V_MAX_eff, t_z));
|
||||
const float b_frac = (t_z_clamp - V_MIN_eff) / DELTA_Z;
|
||||
const int l = max(0, min(Q_N_ATOMS - 1, (int)floorf(b_frac)));
|
||||
const int u = max(0, min(Q_N_ATOMS - 1, (int)ceilf(b_frac)));
|
||||
const float frac = b_frac - (float)l;
|
||||
|
||||
// B-9 (2026-06-01): per-(b, atom_z) saturation tally over pre-clamp
|
||||
// t_z. Sequential thread-0 reduction over Q_N_ATOMS=21 (matches the
|
||||
// softmax pattern at lines 157 / 171). Odd count rules out a
|
||||
// symmetric tree-reduce.
|
||||
__shared__ float s_t_z[Q_N_ATOMS];
|
||||
s_t_z[atom] = t_z;
|
||||
__syncthreads();
|
||||
if (atom == 0) {
|
||||
float sum_top = 0.0f, sum_bot = 0.0f;
|
||||
float max_tz = s_t_z[0], min_tz = s_t_z[0];
|
||||
#pragma unroll
|
||||
for (int z = 0; z < Q_N_ATOMS; ++z) {
|
||||
const float tz_z = s_t_z[z];
|
||||
sum_top += (tz_z > V_MAX_eff) ? 1.0f : 0.0f;
|
||||
sum_bot += (tz_z < V_MIN_eff) ? 1.0f : 0.0f;
|
||||
max_tz = fmaxf(max_tz, tz_z);
|
||||
min_tz = fminf(min_tz, tz_z);
|
||||
}
|
||||
sat_top_per_batch[batch] = sum_top;
|
||||
sat_bot_per_batch[batch] = sum_bot;
|
||||
max_pre_per_batch[batch] = max_tz;
|
||||
min_pre_per_batch[batch] = min_tz;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Distribute mass — serialised walk per `feedback_no_atomicadd.md`
|
||||
for (int src = 0; src < Q_N_ATOMS; ++src) {
|
||||
__syncthreads();
|
||||
if (atom == src) {
|
||||
if (l == u) {
|
||||
s_proj[l] += p;
|
||||
} else {
|
||||
s_proj[l] += p * (1.0f - frac);
|
||||
s_proj[u] += p * frac;
|
||||
}
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
target_dist[batch * Q_N_ATOMS + atom] = s_proj[atom];
|
||||
}
|
||||
|
||||
|
||||
extern "C" __global__ void bellman_target_projection(
|
||||
const float* __restrict__ target_logits, // [B × Q_N_ATOMS]
|
||||
const float* __restrict__ rewards, // [B] (n-step discounted R_n)
|
||||
const float* __restrict__ dones, // [B]
|
||||
const float* __restrict__ n_step_gammas, // [B] (γⁿ per transition, 0 if done)
|
||||
const float* __restrict__ isv, // ≥ 401
|
||||
int B,
|
||||
float* __restrict__ target_dist, // [B × Q_N_ATOMS]
|
||||
// B-9 (2026-06-01): per-batch saturation tally scratch. Mirrors the
|
||||
// fused variant above (`bellman_fused_select_project`); per
|
||||
// `feedback_no_partial_refactor.md` both entry points must be
|
||||
// instrumented atomically.
|
||||
float* __restrict__ sat_top_per_batch, // [B]
|
||||
float* __restrict__ sat_bot_per_batch, // [B]
|
||||
float* __restrict__ max_pre_per_batch, // [B]
|
||||
float* __restrict__ min_pre_per_batch // [B]
|
||||
) {
|
||||
const int batch = blockIdx.x;
|
||||
const int atom = threadIdx.x;
|
||||
if (batch >= B || atom >= Q_N_ATOMS) return;
|
||||
|
||||
__shared__ float s_softmax[Q_N_ATOMS];
|
||||
__shared__ float s_max;
|
||||
__shared__ float s_sumexp;
|
||||
__shared__ float s_proj[Q_N_ATOMS];
|
||||
|
||||
const int base = batch * Q_N_ATOMS;
|
||||
|
||||
// ── Softmax over target logits (numerically-stable max-subtract) ─
|
||||
if (atom == 0) {
|
||||
float m = target_logits[base];
|
||||
#pragma unroll
|
||||
for (int z = 1; z < Q_N_ATOMS; ++z)
|
||||
m = fmaxf(m, target_logits[base + z]);
|
||||
s_max = m;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
const float e = expf(target_logits[base + atom] - s_max);
|
||||
s_softmax[atom] = e;
|
||||
s_proj[atom] = 0.0f; // zero the projection accumulator
|
||||
__syncthreads();
|
||||
|
||||
if (atom == 0) {
|
||||
float sum = 0.0f;
|
||||
#pragma unroll
|
||||
for (int z = 0; z < Q_N_ATOMS; ++z) sum += s_softmax[z];
|
||||
s_sumexp = sum;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
const float p = s_softmax[atom] / s_sumexp;
|
||||
|
||||
// ── n-step γ: per-transition γⁿ replaces the ISV γ for the
|
||||
// bootstrap discount. Already zero when any done occurred in the
|
||||
// n-step window (set at push time). For n=1, n_step_gamma = γ.
|
||||
const float r = rewards[batch];
|
||||
const float gamma_eff = n_step_gammas[batch];
|
||||
|
||||
// ── Adaptive atom span from ISV (audit follow-up). DELTA_Z is
|
||||
// recomputed every kernel launch — ratchet semantics in the
|
||||
// controller guarantee V_MAX > V_MIN strictly (floored at
|
||||
// [-1, +1] baseline) so this is always well-defined.
|
||||
const float V_MIN_eff = isv[RL_C51_V_MIN_INDEX];
|
||||
const float V_MAX_eff = isv[RL_C51_V_MAX_INDEX];
|
||||
const float DELTA_Z = (V_MAX_eff - V_MIN_eff) / (float)(Q_N_ATOMS - 1);
|
||||
|
||||
// ── Per-source-atom target value + clamp + interpolation indices ─
|
||||
const float atom_value = V_MIN_eff + (float)atom * DELTA_Z;
|
||||
const float t_z = r + gamma_eff * atom_value;
|
||||
const float t_z_clamp = fmaxf(V_MIN_eff, fminf(V_MAX_eff, t_z));
|
||||
const float b_frac = (t_z_clamp - V_MIN_eff) / DELTA_Z;
|
||||
const int l = max(0, min(Q_N_ATOMS - 1, (int)floorf(b_frac)));
|
||||
const int u = max(0, min(Q_N_ATOMS - 1, (int)ceilf(b_frac)));
|
||||
const float frac = b_frac - (float)l;
|
||||
|
||||
// B-9 (2026-06-01): per-(b, atom_z) saturation tally over pre-clamp
|
||||
// t_z. Identical reduction as `bellman_fused_select_project` above
|
||||
// — per `feedback_no_partial_refactor.md`.
|
||||
__shared__ float s_t_z[Q_N_ATOMS];
|
||||
s_t_z[atom] = t_z;
|
||||
__syncthreads();
|
||||
if (atom == 0) {
|
||||
float sum_top = 0.0f, sum_bot = 0.0f;
|
||||
float max_tz = s_t_z[0], min_tz = s_t_z[0];
|
||||
#pragma unroll
|
||||
for (int z = 0; z < Q_N_ATOMS; ++z) {
|
||||
const float tz_z = s_t_z[z];
|
||||
sum_top += (tz_z > V_MAX_eff) ? 1.0f : 0.0f;
|
||||
sum_bot += (tz_z < V_MIN_eff) ? 1.0f : 0.0f;
|
||||
max_tz = fmaxf(max_tz, tz_z);
|
||||
min_tz = fminf(min_tz, tz_z);
|
||||
}
|
||||
sat_top_per_batch[batch] = sum_top;
|
||||
sat_bot_per_batch[batch] = sum_bot;
|
||||
max_pre_per_batch[batch] = max_tz;
|
||||
min_pre_per_batch[batch] = min_tz;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Distribute mass into the two nearest support atoms ───────────
|
||||
//
|
||||
// Each thread holds (l, u, frac, p) for its OWN source atom. We
|
||||
// walk source-atom-index `src` from 0..Q_N_ATOMS and only the
|
||||
// matching thread writes into s_proj[l] / s_proj[u], bracketed by
|
||||
// __syncthreads so writes are serialised. Q_N_ATOMS = 21 inner-loop
|
||||
// syncs per block is negligible and trivially correct (no
|
||||
// cross-thread race, no atomicAdd).
|
||||
for (int src = 0; src < Q_N_ATOMS; ++src) {
|
||||
__syncthreads();
|
||||
if (atom == src) {
|
||||
if (l == u) {
|
||||
s_proj[l] += p;
|
||||
} else {
|
||||
s_proj[l] += p * (1.0f - frac);
|
||||
s_proj[u] += p * frac;
|
||||
}
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
target_dist[base + atom] = s_proj[atom];
|
||||
}
|
||||
@@ -1,545 +0,0 @@
|
||||
// bucket_transition_kernels.cu — Phase 1→2 transition kernels.
|
||||
//
|
||||
// All-on-device per feedback_no_htod_htoh_only_mapped_pinned. No atomicAdd
|
||||
// per feedback_no_atomicadd; reductions use warp-shuffle. Static tercile
|
||||
// boundaries for HIDDEN_DIM=128 with N_HORIZONS=3: [0, 43, 86, 128]
|
||||
// (post-2026-05-22 horizon-rebase to 3 buckets, matches Rust-side
|
||||
// `crates/ml-alpha/src/cfc/bucket_routing.rs::BUCKET_CHANNEL_OFFSET`).
|
||||
//
|
||||
// MAX_BUCKET_DIM=96 matches the Rust-side `MAX_BUCKET_DIM` and is sized
|
||||
// for the upper bound of ISV-driven τ-assignment concentration (~75% of
|
||||
// channels into one bucket under extreme τ distributions).
|
||||
|
||||
#define HIDDEN_DIM 128
|
||||
#define N_HORIZONS 3
|
||||
#define BUCKET_DIM_LAST 42 // last bucket absorbs HIDDEN_DIM - 2*43 = 42
|
||||
#define MAX_BUCKET_DIM 96
|
||||
|
||||
// Static bucket boundaries (could also be __constant__ memory).
|
||||
// Tercile cut points: 43, 86, 128 (sum = HIDDEN_DIM = 128).
|
||||
__device__ const unsigned int BUCKET_OFFSETS[N_HORIZONS + 1] = {0, 43, 86, 128};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// tau_sort_kernel: bitonic-merge sort of cfc.tau values.
|
||||
//
|
||||
// Launch: 1 block × 32 threads, shared_mem_bytes >= HIDDEN_DIM * 8 bytes
|
||||
// (4 for tau key, 4 for original index value).
|
||||
//
|
||||
// Sorts ascending. Output: sorted_indices_d[p] = original channel index
|
||||
// whose tau is the p-th smallest.
|
||||
//
|
||||
// Algorithm: bitonic merge sort over 128 elements, single block, 4 passes
|
||||
// of 32 threads cooperating. Each thread handles 4 elements per pass.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void tau_sort_kernel(
|
||||
const float* __restrict__ tau, // [HIDDEN_DIM]
|
||||
unsigned int* __restrict__ sorted_indices // [HIDDEN_DIM]
|
||||
) {
|
||||
extern __shared__ float smem[];
|
||||
float* keys = smem; // [HIDDEN_DIM]
|
||||
unsigned int* vals = (unsigned int*)(smem + HIDDEN_DIM); // [HIDDEN_DIM]
|
||||
|
||||
int tid = threadIdx.x;
|
||||
// Each thread loads 4 elements (128 / 32 = 4)
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int idx = tid + i * 32;
|
||||
keys[idx] = tau[idx];
|
||||
vals[idx] = (unsigned int)idx;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Bitonic sort over 128 elements.
|
||||
for (int k = 2; k <= HIDDEN_DIM; k <<= 1) {
|
||||
for (int j = k >> 1; j > 0; j >>= 1) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int idx = tid + i * 32;
|
||||
int partner = idx ^ j;
|
||||
if (partner > idx) {
|
||||
bool ascending = ((idx & k) == 0);
|
||||
bool swap = ascending ? (keys[idx] > keys[partner]) : (keys[idx] < keys[partner]);
|
||||
if (swap) {
|
||||
float tk = keys[idx]; keys[idx] = keys[partner]; keys[partner] = tk;
|
||||
unsigned int tv = vals[idx]; vals[idx] = vals[partner]; vals[partner] = tv;
|
||||
}
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
}
|
||||
|
||||
// Write sorted_indices.
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int idx = tid + i * 32;
|
||||
sorted_indices[idx] = vals[idx];
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// bucket_assign_kernel: from sorted_indices, write bucket_id per channel.
|
||||
//
|
||||
// Launch: 1 block × HIDDEN_DIM threads.
|
||||
// Each thread handles one channel. Looks up its rank (position in sorted)
|
||||
// and assigns bucket id via static tercile boundaries (post-2026-05-22
|
||||
// N_HORIZONS=3 rebase: cuts at rank 43 and 86 for [43, 43, 42]).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void bucket_assign_kernel(
|
||||
const unsigned int* __restrict__ sorted_indices, // [HIDDEN_DIM]
|
||||
unsigned char* __restrict__ bucket_id_per_channel // [HIDDEN_DIM]
|
||||
) {
|
||||
int tid = threadIdx.x;
|
||||
if (tid >= HIDDEN_DIM) return;
|
||||
// tid is a sorted-rank position; sorted_indices[tid] is the original channel
|
||||
// at this rank. Assign bucket = tercile of rank.
|
||||
unsigned char bucket = 0;
|
||||
if (tid >= 86) bucket = 2;
|
||||
else if (tid >= 43) bucket = 1;
|
||||
bucket_id_per_channel[sorted_indices[tid]] = bucket;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// bucket_iqr_kernel: per-bucket Q1 and Q3 of tau values.
|
||||
//
|
||||
// Launch: N_HORIZONS blocks × 32 threads. Each block computes Q1, Q3 for
|
||||
// its bucket via warp-shuffle reduction.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void bucket_iqr_kernel(
|
||||
const float* __restrict__ tau, // [HIDDEN_DIM]
|
||||
const unsigned int* __restrict__ sorted_indices, // [HIDDEN_DIM]
|
||||
float* __restrict__ q1_per_bucket, // [N_HORIZONS]
|
||||
float* __restrict__ q3_per_bucket // [N_HORIZONS]
|
||||
) {
|
||||
int bucket = blockIdx.x;
|
||||
int bucket_start = BUCKET_OFFSETS[bucket];
|
||||
int bucket_end = BUCKET_OFFSETS[bucket + 1];
|
||||
int bucket_size = bucket_end - bucket_start;
|
||||
int q1_idx = bucket_start + bucket_size / 4;
|
||||
int q3_idx = bucket_start + (3 * bucket_size) / 4;
|
||||
if (threadIdx.x == 0) {
|
||||
// tau values are already in sorted-rank order via sorted_indices.
|
||||
q1_per_bucket[bucket] = tau[sorted_indices[q1_idx]];
|
||||
q3_per_bucket[bucket] = tau[sorted_indices[q3_idx]];
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// channels_in_bucket_kernel: build the bucket → channel lookup table.
|
||||
//
|
||||
// ALPHA fix (2026-05-21): replaces `tau_reorder_kernel`. Per spec §2.3
|
||||
// the original reorder approach grouped τ values by bucket position, but
|
||||
// downstream kernels (`cfc_step_per_branch_fwd`, `tau_clamp_kernel`,
|
||||
// `heads_w_skip_*_kernel`) all read using ORIGINAL channel indices —
|
||||
// the reorder caused systematic layout mismatches.
|
||||
//
|
||||
// New approach: keep tau_all_d, W_in, W_rec, b, heads_w_skip in their
|
||||
// ORIGINAL channel layout. At transition, materialise a lookup table
|
||||
// `channels_in_bucket[k][i]` = original channel index of the i-th
|
||||
// channel assigned to bucket k. Per-branch kernels iterate
|
||||
// channels_in_bucket[branch][0..bucket_dim_k[branch]] to find the
|
||||
// original channels in each bucket.
|
||||
//
|
||||
// Layout: `channels_in_bucket` is [N_HORIZONS × MAX_BUCKET_DIM] flat
|
||||
// row-major. Sentinel value `0xFFFFFFFF` (UINT32_MAX) marks positions
|
||||
// beyond `bucket_dim_k[k]` (the last bucket has 28 of 28 positions
|
||||
// occupied; earlier buckets have 25 of 28).
|
||||
//
|
||||
// Launch: 1 block × HIDDEN_DIM threads, shared_mem_bytes = N_HORIZONS × 4
|
||||
// for the per-bucket cursors. Single-thread sequential write (tid==0)
|
||||
// keeps the cursor increments race-free without atomicAdd per
|
||||
// feedback_no_atomicadd.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void channels_in_bucket_kernel(
|
||||
const unsigned char* __restrict__ bucket_id_per_channel, // [HIDDEN_DIM]
|
||||
const unsigned int* __restrict__ bucket_dim_k, // [N_HORIZONS]
|
||||
unsigned int* __restrict__ channels_in_bucket // [N_HORIZONS × MAX_BUCKET_DIM] flat
|
||||
) {
|
||||
__shared__ unsigned int cursor[N_HORIZONS];
|
||||
int tid = threadIdx.x;
|
||||
|
||||
// Init per-bucket cursors to 0.
|
||||
if (tid < N_HORIZONS) {
|
||||
cursor[tid] = 0;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Fill all output slots with the sentinel value first (parallel across
|
||||
// tid). For HIDDEN_DIM=128 ≥ N_HORIZONS*MAX_BUCKET_DIM=140 we use a
|
||||
// simple stride loop: each thread covers a few output positions.
|
||||
const int total_slots = N_HORIZONS * MAX_BUCKET_DIM;
|
||||
for (int idx = tid; idx < total_slots; idx += blockDim.x) {
|
||||
channels_in_bucket[idx] = 0xFFFFFFFFu;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Single-thread sequential write: thread 0 walks channels in original
|
||||
// order and appends each to the cursor position of its bucket. This
|
||||
// gives deterministic per-bucket ordering (channels listed in
|
||||
// ascending original-channel-index order). No atomicAdd, no race —
|
||||
// a single writer per feedback_no_atomicadd.
|
||||
if (tid == 0) {
|
||||
for (int c = 0; c < HIDDEN_DIM; ++c) {
|
||||
unsigned char k = bucket_id_per_channel[c];
|
||||
// Safety: bucket_id_per_channel must be in [0, N_HORIZONS).
|
||||
// bucket_assign_kernel guarantees this; the bound check below
|
||||
// is defensive only.
|
||||
if (k < (unsigned char)N_HORIZONS) {
|
||||
unsigned int pos = cursor[k];
|
||||
unsigned int bdim = bucket_dim_k[k];
|
||||
if (pos < bdim && pos < (unsigned int)MAX_BUCKET_DIM) {
|
||||
channels_in_bucket[(unsigned int)k * MAX_BUCKET_DIM + pos] = (unsigned int)c;
|
||||
cursor[k] = pos + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// zero_off_bucket_kernel — block-diagonal projection on
|
||||
// [N_HORIZONS × HIDDEN_DIM] tensors.
|
||||
//
|
||||
// ALPHA fix (2026-05-21): the grad mask alone is insufficient to keep
|
||||
// `heads_w_skip` block-diagonal because Adam's `m` and `v` momentum
|
||||
// buffers carry pre-transition gradient signal across the boundary. The
|
||||
// grad mask zeros NEW gradients at off-bucket positions; historical
|
||||
// momentum continues to push them away from zero each Adam step.
|
||||
//
|
||||
// This kernel projects a generic `[N_HORIZONS × HIDDEN_DIM]` tensor
|
||||
// onto the block-diagonal subspace by writing 0.0 at every (h, c) where
|
||||
// `bucket_id_per_channel[c] != h`. It is launched on:
|
||||
// 1. `heads_w_skip_d` at transition (one-shot, zeros off-bucket weights)
|
||||
// 2. `heads_w_skip` Adam `m` and `v` buffers at transition (one-shot,
|
||||
// zeros off-bucket momentum so post-transition Adam steps cannot
|
||||
// revive off-bucket positions)
|
||||
// 3. `heads_w_skip_d` after every Phase 2 Adam step (belt-and-suspenders;
|
||||
// guarantees off-bucket positions stay exactly zero even under
|
||||
// eps-driven Adam step from zero moments)
|
||||
//
|
||||
// Launch: grid = (N_HORIZONS, 1, 1), block = (HIDDEN_DIM, 1, 1). Same
|
||||
// layout convention as `heads_w_skip_mask_init_kernel`.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void zero_off_bucket_kernel(
|
||||
const unsigned char* __restrict__ bucket_id_per_channel, // [HIDDEN_DIM]
|
||||
float* __restrict__ tensor // [N_HORIZONS × HIDDEN_DIM]
|
||||
) {
|
||||
int h = blockIdx.x;
|
||||
int c = threadIdx.x;
|
||||
if (h >= N_HORIZONS || c >= HIDDEN_DIM) return;
|
||||
int idx = h * HIDDEN_DIM + c;
|
||||
if (bucket_id_per_channel[c] != (unsigned char)h) {
|
||||
tensor[idx] = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// heads_compact_kernel: reorder heads_w_skip into compact ragged layout.
|
||||
//
|
||||
// Original heads_w_skip is [N_HORIZONS × HIDDEN_DIM]. Output is compact
|
||||
// [HIDDEN_DIM] where each horizon h owns BUCKET_DIM[h] contiguous floats
|
||||
// at position bucket_channel_offset[h].
|
||||
//
|
||||
// For each compact-position c in bucket k, read original
|
||||
// heads_w_skip[k * HIDDEN_DIM + channel_that_landed_at_c].
|
||||
//
|
||||
// Launch: 1 block × HIDDEN_DIM threads.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void heads_compact_kernel(
|
||||
const float* __restrict__ w_skip_orig, // [N_HORIZONS × HIDDEN_DIM]
|
||||
const unsigned char* __restrict__ bucket_id_per_channel, // [HIDDEN_DIM]
|
||||
const unsigned int* __restrict__ bucket_channel_offset, // [N_HORIZONS + 1]
|
||||
float* __restrict__ w_skip_compact // [HIDDEN_DIM]
|
||||
) {
|
||||
extern __shared__ unsigned int per_bucket_cursor[]; // [N_HORIZONS + 1]
|
||||
int tid = threadIdx.x;
|
||||
if (tid <= N_HORIZONS) {
|
||||
per_bucket_cursor[tid] = bucket_channel_offset[tid];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
for (int c = 0; c < HIDDEN_DIM; ++c) {
|
||||
if (tid == 0) {
|
||||
unsigned char k = bucket_id_per_channel[c];
|
||||
unsigned int pos = per_bucket_cursor[k];
|
||||
w_skip_compact[pos] = w_skip_orig[(unsigned int)k * HIDDEN_DIM + c];
|
||||
per_bucket_cursor[k] = pos + 1;
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// tau_change_frobenius_kernel: ||tau_t - tau_{t-1}||_F² (scalar output).
|
||||
//
|
||||
// Single block × HIDDEN_DIM threads. Block-tree reduction (no atomicAdd
|
||||
// per feedback_no_atomicadd).
|
||||
//
|
||||
// Per pearl_first_observation_bootstrap: caller's ControllerA bootstraps
|
||||
// the EMA from this scalar on step 1; on subsequent steps the scalar
|
||||
// feeds the Wiener-α update.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void tau_change_frobenius_kernel(
|
||||
const float* __restrict__ tau_t, // [HIDDEN_DIM]
|
||||
const float* __restrict__ tau_t_minus_1, // [HIDDEN_DIM]
|
||||
float* __restrict__ tau_change_out // [1]
|
||||
) {
|
||||
__shared__ float sdata[HIDDEN_DIM];
|
||||
int tid = threadIdx.x;
|
||||
if (tid >= HIDDEN_DIM) return;
|
||||
float diff = tau_t[tid] - tau_t_minus_1[tid];
|
||||
sdata[tid] = diff * diff;
|
||||
__syncthreads();
|
||||
// Block-tree reduction (HIDDEN_DIM=128 is power-of-two; no padding needed).
|
||||
for (int s = HIDDEN_DIM / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) sdata[tid] += sdata[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) *tau_change_out = sdata[0];
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// slack_factor_apply_kernel: convert (Q1, Q3) → IQR_widened in place.
|
||||
//
|
||||
// Per spec §3.2: slack_factor_k = sqrt(Q3_k / Q1_k), ISV-derived from
|
||||
// observed bucket IQR (no hardcoded 1.5× factor).
|
||||
//
|
||||
// Launch: 1 block × N_HORIZONS threads (5 threads, well within a warp).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void slack_factor_apply_kernel(
|
||||
float* __restrict__ iqr_lo, // [N_HORIZONS] in: Q1, out: Q1/slack
|
||||
float* __restrict__ iqr_hi // [N_HORIZONS] in: Q3, out: Q3*slack
|
||||
) {
|
||||
int k = threadIdx.x;
|
||||
if (k >= N_HORIZONS) return;
|
||||
float q1 = iqr_lo[k];
|
||||
float q3 = iqr_hi[k];
|
||||
// Numerical defense: Q1 must be > 0 (taus are log-uniform [0.01, 1000],
|
||||
// all positive by construction).
|
||||
float slack = sqrtf(q3 / fmaxf(q1, 1e-6f));
|
||||
iqr_lo[k] = q1 / slack;
|
||||
iqr_hi[k] = q3 * slack;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// tau_clamp_kernel — Controller B post-Adam projection.
|
||||
//
|
||||
// Per spec §3.2 and pearl_adam_normalizes_loss_weights: hard projection
|
||||
// AFTER each Adam step on the τ slice, bypassing Adam's m/sqrt(v)
|
||||
// normalization that would no-op a loss-weight modulation.
|
||||
//
|
||||
// Per pearl_isv_for_adaptive_bounds: the (lo, hi) inputs are the IQR-
|
||||
// widened bounds from `slack_factor_apply_kernel` (ISV-derived from each
|
||||
// bucket's observed Q3/Q1 ratio at the Phase 1→2 transition).
|
||||
//
|
||||
// Launch: 1 block × HIDDEN_DIM threads (single warp on Ampere+; tiny).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void tau_clamp_kernel(
|
||||
float* __restrict__ tau_all, // [HIDDEN_DIM]
|
||||
const unsigned char* __restrict__ bucket_id_per_channel, // [HIDDEN_DIM]
|
||||
const float* __restrict__ iqr_lo, // [N_HORIZONS]
|
||||
const float* __restrict__ iqr_hi // [N_HORIZONS]
|
||||
) {
|
||||
int c = threadIdx.x;
|
||||
if (c >= HIDDEN_DIM) return;
|
||||
unsigned char k = bucket_id_per_channel[c];
|
||||
float v = tau_all[c];
|
||||
float lo = iqr_lo[k];
|
||||
float hi = iqr_hi[k];
|
||||
tau_all[c] = fminf(fmaxf(v, lo), hi);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// heads_lr_multiplier_scale_kernel — Controller C per-bucket LR multiplier.
|
||||
//
|
||||
// Per spec §3.3 and pearl_adam_normalizes_loss_weights: rescales the
|
||||
// per-step parameter delta (post-Adam − pre-Adam) by a per-horizon LR
|
||||
// multiplier. Bypasses Adam's m/sqrt(v) normalization because the
|
||||
// rescaling is applied to the parameter delta itself, not the loss
|
||||
// weight or the Adam moments.
|
||||
//
|
||||
// Layout: `param` is row-major [N_HORIZONS × per_horizon_stride]. Each
|
||||
// horizon owns a contiguous block of `per_horizon_stride` floats. Element
|
||||
// `i` belongs to horizon `i / per_horizon_stride`.
|
||||
//
|
||||
// Launch: enough threads to cover `total_elements` (one per element).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void heads_lr_multiplier_scale_kernel(
|
||||
float* __restrict__ param, // [N_HORIZONS * per_horizon_stride]
|
||||
const float* __restrict__ param_pre, // pre-Adam snapshot, same shape
|
||||
const float* __restrict__ lr_mult_per_horizon, // [N_HORIZONS]
|
||||
int per_horizon_stride,
|
||||
int total_elements
|
||||
) {
|
||||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= total_elements) return;
|
||||
int horizon = i / per_horizon_stride;
|
||||
if (horizon >= N_HORIZONS) return; // safety bound for stride/total mismatch
|
||||
float delta = param[i] - param_pre[i];
|
||||
float scaled_delta = delta * lr_mult_per_horizon[horizon];
|
||||
param[i] = param_pre[i] + scaled_delta;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// h_mag_per_bucket_kernel — per-bucket mean(|h_state|) for Controller D.
|
||||
//
|
||||
// Per spec §3.4: Controller D's dead-bucket detector compares per-bucket
|
||||
// `h_mag_k = mean(|h_state_branch_k|)` (across the batch × bucket_dim_k
|
||||
// channels) against the first-observation floor with a 1/e decay
|
||||
// threshold. This kernel computes the per-bucket mean-abs reduction on
|
||||
// device; the host reads the [N_HORIZONS] result via a mapped-pinned
|
||||
// shadow each Phase 2 step.
|
||||
//
|
||||
// ALPHA fix (2026-05-21): h_state stays in ORIGINAL channel layout (no
|
||||
// reorder). The bucket → channel lookup goes through
|
||||
// `channels_in_bucket[bucket][i]` populated at transition by
|
||||
// `channels_in_bucket_kernel`.
|
||||
//
|
||||
// Launch: grid = (N_HORIZONS, 1, 1), block = (128, 1, 1). Each block
|
||||
// reduces one bucket. Per `feedback_no_atomicadd`, reduction is
|
||||
// block-tree on shared memory (no atomicAdd).
|
||||
//
|
||||
// Block size = 128 covers MAX_BUCKET_DIM=96 (smallest pow2 ≥ 96).
|
||||
// Previous block_dim=32 silently dropped channels 32..bdim when bdim>32
|
||||
// — caught by `tests/bucket_transition_kernels.rs` (block_dim=43 case).
|
||||
//
|
||||
// Input `h_state` is `[B × HIDDEN_DIM]` (ORIGINAL channel layout). Each
|
||||
// block reads its bucket's `bucket_dim_k[bucket]` channels (via
|
||||
// `channels_in_bucket[bucket][0..bdim]`) per sample (across all B
|
||||
// samples), accumulates |value|, and writes the per-bucket mean.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void h_mag_per_bucket_kernel(
|
||||
const float* __restrict__ h_state, // [B × HIDDEN_DIM]
|
||||
const unsigned int* __restrict__ channels_in_bucket, // [N_HORIZONS × MAX_BUCKET_DIM]
|
||||
const unsigned int* __restrict__ bucket_dim_k, // [N_HORIZONS]
|
||||
int B,
|
||||
float* __restrict__ h_mag_per_bucket // [N_HORIZONS]
|
||||
) {
|
||||
int bucket = blockIdx.x;
|
||||
if (bucket >= N_HORIZONS) return;
|
||||
int bdim = (int)bucket_dim_k[bucket];
|
||||
|
||||
// sdata sized for the 128-lane block reduction.
|
||||
__shared__ float sdata[128];
|
||||
int tid = threadIdx.x;
|
||||
|
||||
// Each thread sums |h| over its bucket-local index (tid), looking up
|
||||
// the ORIGINAL channel via channels_in_bucket. Threads with tid >= bdim
|
||||
// contribute 0. With bdim ∈ [1, MAX_BUCKET_DIM=96] and block_dim=128,
|
||||
// tail lanes [bdim, 128) are always inactive — uniform predicate.
|
||||
float local_sum = 0.0f;
|
||||
if (tid < bdim) {
|
||||
unsigned int c = channels_in_bucket[bucket * MAX_BUCKET_DIM + tid];
|
||||
// Defensive: out-of-range channel index should not contribute.
|
||||
// Predicate is uniform across active lanes (tid < bdim) — all hold
|
||||
// valid entries by construction of channels_in_bucket_kernel.
|
||||
if (c < (unsigned int)HIDDEN_DIM) {
|
||||
for (int b = 0; b < B; ++b) {
|
||||
float v = h_state[b * HIDDEN_DIM + c];
|
||||
local_sum += fabsf(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
sdata[tid] = local_sum;
|
||||
__syncthreads();
|
||||
|
||||
// Block-tree reduction over 128 lanes (multi-warp). No atomicAdd.
|
||||
// Stages: 64→32→16→8→4→2→1. Each stage halves the active lane count.
|
||||
for (int s = 64; s > 0; s >>= 1) {
|
||||
if (tid < s) sdata[tid] += sdata[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
// Lane 0 writes the per-bucket mean. Total elements summed = bdim * B.
|
||||
if (tid == 0) {
|
||||
float denom = (float)(bdim * B);
|
||||
h_mag_per_bucket[bucket] = sdata[0] / fmaxf(denom, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// heads_w_skip_mask_init_kernel — Phase 1→2 transition one-shot.
|
||||
//
|
||||
// Per spec §3.3 (block-diagonal heads) and follow-up to Task 10 Option B:
|
||||
// build the per-channel routing mask AND zero-out off-bucket entries of
|
||||
// `heads_w_skip` in place so that subsequent forward/backward never sees
|
||||
// non-zero cross-bucket weights. Combined with the grad-mask apply kernel
|
||||
// below, this is mathematically equivalent to a compact-only read of
|
||||
// `heads_w_skip` at zero implementation cost for the GRN kernel.
|
||||
//
|
||||
// Layout: `heads_w_skip` is `[N_HORIZONS × HIDDEN_DIM]` row-major
|
||||
// (horizon-major). `bucket_id_per_channel[c]` records which horizon owns
|
||||
// column `c` in the block-diagonal layout. Entry `(h, c)` is in-bucket
|
||||
// iff `bucket_id_per_channel[c] == h`.
|
||||
//
|
||||
// Launch: grid = (N_HORIZONS, 1, 1), block = (HIDDEN_DIM, 1, 1).
|
||||
// One block per horizon × one thread per channel. No reductions, no
|
||||
// shared memory, no atomicAdd — fully data-parallel.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void heads_w_skip_mask_init_kernel(
|
||||
const unsigned char* __restrict__ bucket_id_per_channel, // [HIDDEN_DIM]
|
||||
float* __restrict__ heads_w_skip, // [N_HORIZONS × HIDDEN_DIM]
|
||||
float* __restrict__ mask // [N_HORIZONS × HIDDEN_DIM]
|
||||
) {
|
||||
int h = blockIdx.x;
|
||||
int c = threadIdx.x;
|
||||
if (h >= N_HORIZONS || c >= HIDDEN_DIM) return;
|
||||
int idx = h * HIDDEN_DIM + c;
|
||||
float m = (bucket_id_per_channel[c] == (unsigned char)h) ? 1.0f : 0.0f;
|
||||
mask[idx] = m;
|
||||
heads_w_skip[idx] *= m; // zero off-bucket entries in place
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// heads_w_skip_grad_mask_apply_kernel — per-step Phase 2 gradient mask.
|
||||
//
|
||||
// Per spec §3.3 + follow-up Option B closure: multiply the post-reduction
|
||||
// `grad_heads_w_skip` element-wise by the routing mask BEFORE the Adam
|
||||
// step. Off-bucket positions receive zero gradient, never update, and
|
||||
// (since they started at zero after the init kernel) remain at zero
|
||||
// throughout training. This delivers block-diagonal `heads_w_skip`
|
||||
// behaviour without touching the GRN forward/backward kernels.
|
||||
//
|
||||
// Launch: grid = (N_HORIZONS, 1, 1), block = (HIDDEN_DIM, 1, 1). Same
|
||||
// layout as `heads_w_skip_mask_init_kernel`.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void heads_w_skip_grad_mask_apply_kernel(
|
||||
const float* __restrict__ mask, // [N_HORIZONS × HIDDEN_DIM]
|
||||
float* __restrict__ grad_heads_w_skip // [N_HORIZONS × HIDDEN_DIM]
|
||||
) {
|
||||
int h = blockIdx.x;
|
||||
int c = threadIdx.x;
|
||||
if (h >= N_HORIZONS || c >= HIDDEN_DIM) return;
|
||||
int idx = h * HIDDEN_DIM + c;
|
||||
grad_heads_w_skip[idx] *= mask[idx];
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// bucket_iqr_double_widening_kernel — Controller D Recovery Attempt 1.
|
||||
//
|
||||
// Per spec §3.4: when a bucket's h_mag EMA falls below the first-
|
||||
// observation × 1/e threshold for `dead_window_k` consecutive Phase 2
|
||||
// steps, Recovery Attempt 1 doubles the bucket's slack envelope by
|
||||
// halving its IQR lower bound and doubling the IQR upper bound. This
|
||||
// releases τ values for that bucket from Controller B's hard projection
|
||||
// clip, letting them drift to the wider range that may re-engage the
|
||||
// channels.
|
||||
//
|
||||
// Per `feedback_isv_for_adaptive_bounds`: the factor 2 is the natural
|
||||
// log-doubling step — one full log-spread expansion beyond the bucket's
|
||||
// natural Q3/Q1 ratio (the IQR already encoded one log-step via the
|
||||
// transition's `sqrt(Q3/Q1)` slack widening; doubling captures one more).
|
||||
//
|
||||
// Launch: grid = (1, 1, 1), block = (1, 1, 1). Single-thread, off the
|
||||
// hot path (fires at most a handful of times per training run when a
|
||||
// bucket actually goes dead).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void bucket_iqr_double_widening_kernel(
|
||||
float* __restrict__ iqr_lo, // [N_HORIZONS]
|
||||
float* __restrict__ iqr_hi, // [N_HORIZONS]
|
||||
int bucket_idx
|
||||
) {
|
||||
if (threadIdx.x == 0 && blockIdx.x == 0) {
|
||||
if (bucket_idx >= 0 && bucket_idx < N_HORIZONS) {
|
||||
iqr_lo[bucket_idx] *= 0.5f;
|
||||
iqr_hi[bucket_idx] *= 2.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,364 +0,0 @@
|
||||
// cfc_step.cu — one CfC time step (Hasani 2022 closed-form recurrence)
|
||||
// + batched variants + 3D transpose helper.
|
||||
|
||||
// Transpose a [N1, N2, N3] row-major tensor to [N2, N1, N3]. Used by
|
||||
// the batched PerceptionTrainer to convert Mamba2's [B, K, hidden]
|
||||
// per-step output into the [K, B, hidden] layout that makes per-K
|
||||
// slot access contiguous in the trainer's K loop (and back again for
|
||||
// grad_h_enriched_seq feeding Mamba2's backward).
|
||||
//
|
||||
// Launches one block per (i, j) pair, blockDim.x threads cover N3.
|
||||
extern "C" __global__ void transpose_3d_swap_01(
|
||||
const float* __restrict__ in, // [N1, N2, N3]
|
||||
float* __restrict__ out, // [N2, N1, N3]
|
||||
int N1,
|
||||
int N2,
|
||||
int N3
|
||||
) {
|
||||
int i = blockIdx.x;
|
||||
int j = blockIdx.y;
|
||||
int k = blockIdx.z * blockDim.x + threadIdx.x;
|
||||
if (i >= N1 || j >= N2 || k >= N3) return;
|
||||
out[((long long)j * N1 + i) * N3 + k] = in[((long long)i * N2 + j) * N3 + k];
|
||||
}
|
||||
|
||||
//
|
||||
// pre[i] = sum_k w_in[i,k] * x[k] + sum_k w_rec[i,k] * h_old[k] + b[i]
|
||||
// decay[i] = exp(-dt_s / max(tau[i], 1e-6))
|
||||
// h_new[i] = h_old[i] * decay[i] + (1 - decay[i]) * tanh(pre[i])
|
||||
//
|
||||
// Launched as one block of n_hid threads (or grid of blocks if n_hid > 128).
|
||||
// Each thread computes one hidden unit; reductions are intra-thread (each
|
||||
// thread does its own dot product). No atomicAdd. Per
|
||||
// `feedback_no_atomicadd.md` and the GPU/CPU contract.
|
||||
|
||||
extern "C" __global__ void cfc_step(
|
||||
const float* __restrict__ w_in, // [n_hid, n_in], row-major
|
||||
const float* __restrict__ w_rec, // [n_hid, n_hid], row-major
|
||||
const float* __restrict__ b, // [n_hid]
|
||||
const float* __restrict__ tau, // [n_hid]
|
||||
const float* __restrict__ x, // [n_in]
|
||||
const float* __restrict__ h_old, // [n_hid]
|
||||
float dt_s,
|
||||
int n_in,
|
||||
int n_hid,
|
||||
float* __restrict__ h_new // [n_hid]
|
||||
) {
|
||||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= n_hid) return;
|
||||
|
||||
float pre = b[i];
|
||||
for (int k = 0; k < n_in; ++k) {
|
||||
pre += w_in[i * n_in + k] * x[k];
|
||||
}
|
||||
for (int k = 0; k < n_hid; ++k) {
|
||||
pre += w_rec[i * n_hid + k] * h_old[k];
|
||||
}
|
||||
const float decay = expf(-dt_s / fmaxf(tau[i], 1e-6f));
|
||||
h_new[i] = h_old[i] * decay + (1.0f - decay) * tanhf(pre);
|
||||
}
|
||||
|
||||
// Backward through ONE cfc_step (truncated BPTT, K=1).
|
||||
//
|
||||
// Forward (for reference):
|
||||
// pre[i] = b[i] + sum_k W_in[i,k] x[k] + sum_k W_rec[i,k] h_old[k]
|
||||
// τ_eff = max(tau[i], 1e-6)
|
||||
// decay[i] = exp(-dt / τ_eff)
|
||||
// s[i] = tanh(pre[i])
|
||||
// h_new[i] = h_old[i] * decay + (1 - decay) * s
|
||||
//
|
||||
// Given grad_h_new[i] = dL/dh_new[i], compute:
|
||||
// d_decay[i] = grad_h_new[i] * (h_old[i] - s[i])
|
||||
// d_pre[i] = grad_h_new[i] * (1 - decay) * (1 - s^2)
|
||||
// grad_tau[i] += d_decay[i] * decay[i] * dt / τ_eff² (when tau > eps)
|
||||
// grad_b[i] += d_pre[i]
|
||||
// grad_W_in[i,k] += d_pre[i] * x[k]
|
||||
// grad_W_rec[i,k] += d_pre[i] * h_old[k]
|
||||
// grad_h_old[i] += grad_h_new[i] * decay + sum_j d_pre[j] * W_rec[j, i]
|
||||
//
|
||||
// tau IS trained — grad_tau receives a `+=` accumulation (callers must
|
||||
// pre-zero across the per-position K loop).
|
||||
//
|
||||
// One thread per hidden unit (128 threads in one block). Block tree-
|
||||
// reduce not needed — each thread owns its own grad_W rows and grad_h
|
||||
// component; grad_h_old accumulates via shared `d_pre` reads.
|
||||
|
||||
extern "C" __global__ void cfc_step_backward(
|
||||
const float* __restrict__ w_in, // [n_hid, n_in]
|
||||
const float* __restrict__ w_rec, // [n_hid, n_hid]
|
||||
const float* __restrict__ b,
|
||||
const float* __restrict__ tau,
|
||||
const float* __restrict__ x, // [n_in]
|
||||
const float* __restrict__ h_old, // [n_hid]
|
||||
const float* __restrict__ grad_h_new, // [n_hid]
|
||||
float dt_s,
|
||||
int n_in,
|
||||
int n_hid,
|
||||
float* __restrict__ grad_w_in, // [n_hid, n_in]
|
||||
float* __restrict__ grad_w_rec, // [n_hid, n_hid]
|
||||
float* __restrict__ grad_b, // [n_hid]
|
||||
float* __restrict__ grad_tau, // [n_hid] — accumulated via +=
|
||||
float* __restrict__ grad_h_old, // [n_hid]
|
||||
float* __restrict__ grad_x // [n_in] — set to nullptr to skip
|
||||
) {
|
||||
extern __shared__ float smem[];
|
||||
float* sd_pre = smem; // [n_hid]
|
||||
float* sdecay = sd_pre + n_hid; // [n_hid]
|
||||
|
||||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= n_hid) return;
|
||||
|
||||
// Recompute forward pre + tanh to derive d_pre.
|
||||
float pre = b[i];
|
||||
for (int k = 0; k < n_in; ++k) pre += w_in[i * n_in + k] * x[k];
|
||||
for (int k = 0; k < n_hid; ++k) pre += w_rec[i * n_hid + k] * h_old[k];
|
||||
const float s = tanhf(pre);
|
||||
const float tau_eps = 1e-6f;
|
||||
const float tau_eff = fmaxf(tau[i], tau_eps);
|
||||
const float decay = expf(-dt_s / tau_eff);
|
||||
const float dh = grad_h_new[i];
|
||||
const float d_pre = dh * (1.0f - decay) * (1.0f - s * s);
|
||||
|
||||
sd_pre[i] = d_pre;
|
||||
sdecay[i] = decay;
|
||||
__syncthreads();
|
||||
|
||||
// Parameter-grad writes use `+=`: callers may invoke this kernel
|
||||
// multiple times per training step (per-position supervision) and
|
||||
// need the gradients summed across calls. Callers MUST pre-zero
|
||||
// grad_b / grad_w_in / grad_w_rec / grad_tau at the start of each step.
|
||||
grad_b[i] += d_pre;
|
||||
for (int k = 0; k < n_in; ++k) {
|
||||
grad_w_in[i * n_in + k] += d_pre * x[k];
|
||||
}
|
||||
for (int k = 0; k < n_hid; ++k) {
|
||||
grad_w_rec[i * n_hid + k] += d_pre * h_old[k];
|
||||
}
|
||||
// grad_tau accumulation. Zero contribution if tau is clamped to eps
|
||||
// (max() inactive on this side) — strict d(max)/d(tau) = 0 there.
|
||||
if (tau[i] > tau_eps) {
|
||||
const float d_decay = dh * (h_old[i] - s);
|
||||
grad_tau[i] += d_decay * decay * dt_s / (tau_eff * tau_eff);
|
||||
}
|
||||
|
||||
// grad_h_old[i] = dh * decay + sum_j d_pre[j] * W_rec[j, i]
|
||||
float gh = dh * decay;
|
||||
for (int j = 0; j < n_hid; ++j) {
|
||||
gh += sd_pre[j] * w_rec[j * n_hid + i];
|
||||
}
|
||||
grad_h_old[i] = gh;
|
||||
|
||||
// grad_x[k] = sum_i d_pre[i] * W_in[i, k]. Each thread i contributes to
|
||||
// n_in different output positions — different threads write to different
|
||||
// grad_x slots if we partition by i, but multiple i's contribute to each k.
|
||||
// Avoid atomicAdd: rely on thread 0 to do the n_in × n_hid sum
|
||||
// sequentially (n_in is small — typically 32 or 128, this is one-time per
|
||||
// backward call). Discriminated by nullptr → skip path for back-compat.
|
||||
if (grad_x != nullptr && i == 0) {
|
||||
for (int k = 0; k < n_in; ++k) {
|
||||
float gx = 0.0f;
|
||||
for (int j = 0; j < n_hid; ++j) {
|
||||
gx += sd_pre[j] * w_in[j * n_in + k];
|
||||
}
|
||||
grad_x[k] = gx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Batched variants ────────────────────────────────────────────────
|
||||
//
|
||||
// Phase B (2026-05-17): block-per-batch refactor. Launch contract:
|
||||
// grid=(n_batch, 1, 1), block=(n_hid, 1, 1).
|
||||
// Each block handles ONE batch's n_hid channels in parallel.
|
||||
//
|
||||
// Param-grad writes go through per-batch scratch buffers
|
||||
// (grad_w_in_scratch [B, n_hid, n_in] etc.). Thread (bi, i) is the
|
||||
// sole writer to its slice of each scratch tensor; the K-loop's 64
|
||||
// invocations accumulate via += within the same (bi, i, *) slice.
|
||||
// After the K-loop the caller runs `reduce_axis0` to sum B → final
|
||||
// grad buffer (OVERWRITE semantics). No atomicAdd per
|
||||
// feedback_no_atomicadd.md.
|
||||
//
|
||||
// Shared memory budget per block: sd_pre[n_hid] + sdecay[n_hid] =
|
||||
// 2 * n_hid * 4 bytes. At n_hid=128 → 1 KiB. Far below 48 KiB limit,
|
||||
// no cudaFuncSetAttribute needed.
|
||||
|
||||
// Block-per-batch (Phase B): grid=(n_batch, 1, 1), block=(n_hid, 1, 1).
|
||||
// Each block handles ONE batch's n_hid channels in parallel. Removes
|
||||
// the internal batch loop — frees ~31 SMs per launch at B=32.
|
||||
extern "C" __global__ void cfc_step_batched(
|
||||
const float* __restrict__ w_in, // [n_hid, n_in], row-major
|
||||
const float* __restrict__ w_rec, // [n_hid, n_hid], row-major
|
||||
const float* __restrict__ b, // [n_hid]
|
||||
const float* __restrict__ tau, // [n_hid]
|
||||
const float* __restrict__ x, // [n_batch, n_in]
|
||||
const float* __restrict__ h_old, // [n_batch, n_hid]
|
||||
float dt_s,
|
||||
int n_in,
|
||||
int n_hid,
|
||||
int n_batch,
|
||||
float* __restrict__ h_new // [n_batch, n_hid]
|
||||
) {
|
||||
// Dynamic shared layout (caller supplies (n_in + n_hid) * 4 bytes):
|
||||
// s_x [n_in] — staged x_b row (read by every channel thread)
|
||||
// s_h_old [n_hid] — staged h_old_b row (same)
|
||||
extern __shared__ float smem_fwd[];
|
||||
float* s_x = smem_fwd;
|
||||
float* s_h_old = s_x + n_in;
|
||||
|
||||
int bi = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (bi >= n_batch) return;
|
||||
|
||||
// Cooperative staging: 128× redundant DRAM reads per row → 1× per row
|
||||
// shared across all channel threads. Both rows fit since n_in = n_hid
|
||||
// = HIDDEN.
|
||||
if (tid < n_in) s_x[tid] = x[(long long)bi * n_in + tid];
|
||||
if (tid < n_hid) s_h_old[tid] = h_old[(long long)bi * n_hid + tid];
|
||||
__syncthreads();
|
||||
|
||||
if (tid >= n_hid) return;
|
||||
const int i = tid;
|
||||
const float bias_i = b[i];
|
||||
const float decay = expf(-dt_s / fmaxf(tau[i], 1e-6f));
|
||||
const float one_minus_decay = 1.0f - decay;
|
||||
|
||||
float pre = bias_i;
|
||||
for (int k = 0; k < n_in; ++k) pre += w_in[i * n_in + k] * s_x[k];
|
||||
for (int k = 0; k < n_hid; ++k) pre += w_rec[i * n_hid + k] * s_h_old[k];
|
||||
h_new[(long long)bi * n_hid + i] =
|
||||
s_h_old[i] * decay + one_minus_decay * tanhf(pre);
|
||||
}
|
||||
|
||||
|
||||
// Block-per-batch backward.
|
||||
// grid=(n_batch, 1, 1) block=(n_hid, 1, 1)
|
||||
// shared mem: 2 * n_hid * 4 bytes (sd_pre + sdecay; one row each)
|
||||
//
|
||||
// Param-grad scratch tensors hold per-batch slices. Thread (bi, i) is
|
||||
// sole writer to its slice of each scratch tensor for every K-iteration
|
||||
// the K-loop calls this kernel — no race even with B parallel blocks.
|
||||
// Caller zeroes the scratch buffers once per training step; this kernel
|
||||
// uses += to accumulate across the K-loop's 64 invocations.
|
||||
//
|
||||
// Outputs grad_h_old and grad_x are per-batch indexed; each block bi is
|
||||
// sole writer to its [bi, :] slice (overwrite, single launch).
|
||||
extern "C" __global__ void cfc_step_backward_batched(
|
||||
const float* __restrict__ w_in, // [n_hid, n_in]
|
||||
const float* __restrict__ w_rec, // [n_hid, n_hid]
|
||||
const float* __restrict__ b, // [n_hid]
|
||||
const float* __restrict__ tau, // [n_hid]
|
||||
const float* __restrict__ x, // [n_batch, n_in]
|
||||
const float* __restrict__ h_old, // [n_batch, n_hid]
|
||||
const float* __restrict__ grad_h_new, // [n_batch, n_hid]
|
||||
float dt_s,
|
||||
int n_in,
|
||||
int n_hid,
|
||||
int n_batch,
|
||||
float* __restrict__ grad_w_in_scratch, // [n_batch, n_hid, n_in] accum +=
|
||||
float* __restrict__ grad_w_rec_scratch, // [n_batch, n_hid, n_hid] accum +=
|
||||
float* __restrict__ grad_b_scratch, // [n_batch, n_hid] accum +=
|
||||
float* __restrict__ grad_tau_scratch, // [n_batch, n_hid] accum +=
|
||||
float* __restrict__ grad_h_old, // [n_batch, n_hid] overwrite
|
||||
float* __restrict__ grad_x // [n_batch, n_in] overwrite (nullptr OK)
|
||||
) {
|
||||
extern __shared__ float smem[];
|
||||
// Shared layout — caller passes (2*n_hid + n_in + n_hid) * 4 bytes:
|
||||
// sd_pre [n_hid] — d_pre_b per channel (set in Pass 0, consumed Pass 1..3)
|
||||
// sdecay [n_hid] — reserved for future passes / kept for ABI symmetry
|
||||
// s_x [n_in] — staged x_b row (eliminates 128× redundant DRAM reads)
|
||||
// s_h_old [n_hid] — staged h_old_b row (same)
|
||||
float* sd_pre = smem;
|
||||
float* sdecay = sd_pre + n_hid;
|
||||
float* s_x = sdecay + n_hid;
|
||||
float* s_h_old = s_x + n_in;
|
||||
|
||||
int bi = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
// block_dim is >= max(n_hid, n_in); each thread serves both roles
|
||||
// across passes. Most paths gate on whether (tid < n_hid) / (tid < n_in).
|
||||
if (bi >= n_batch) return;
|
||||
|
||||
// Cooperative staging — coalesced loads, single sync.
|
||||
if (tid < n_in) s_x[tid] = x[(long long)bi * n_in + tid];
|
||||
if (tid < n_hid) s_h_old[tid] = h_old[(long long)bi * n_hid + tid];
|
||||
__syncthreads();
|
||||
|
||||
// Pass 0: compute pre[i], d_pre[i], decay[i]; write per-channel scratch.
|
||||
// Thread tid serves the channel role i = tid. Reads from shared.
|
||||
float dh_b = 0.0f;
|
||||
float decay_i = 0.0f;
|
||||
float s_i = 0.0f;
|
||||
if (tid < n_hid) {
|
||||
const int i = tid;
|
||||
const float bias_i = b[i];
|
||||
const float tau_eps = 1e-6f;
|
||||
const float tau_i_raw = tau[i];
|
||||
const float tau_eff = fmaxf(tau_i_raw, tau_eps);
|
||||
decay_i = expf(-dt_s / tau_eff);
|
||||
sdecay[i] = decay_i;
|
||||
dh_b = grad_h_new[(long long)bi * n_hid + i];
|
||||
|
||||
float pre = bias_i;
|
||||
for (int k = 0; k < n_in; ++k) pre += w_in[i * n_in + k] * s_x[k];
|
||||
for (int k = 0; k < n_hid; ++k) pre += w_rec[i * n_hid + k] * s_h_old[k];
|
||||
s_i = tanhf(pre);
|
||||
const float d_pre_b = dh_b * (1.0f - decay_i) * (1.0f - s_i * s_i);
|
||||
sd_pre[i] = d_pre_b;
|
||||
|
||||
// Per-channel scratch (one float per thread, no coalescing concern).
|
||||
grad_b_scratch[(long long)bi * n_hid + i] += d_pre_b;
|
||||
|
||||
// Branchless tau-grad — gates contribution by (tau > eps) without
|
||||
// warp divergence.
|
||||
const float d_decay_b = dh_b * (s_h_old[i] - s_i);
|
||||
const float gate = (tau_i_raw > tau_eps) ? 1.0f : 0.0f;
|
||||
const float contrib = d_decay_b * decay_i * dt_s / (tau_eff * tau_eff);
|
||||
grad_tau_scratch[(long long)bi * n_hid + i] += gate * contrib;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 1: grad_w_in[bi, i, k] += d_pre[i] * x[k]
|
||||
// grad_w_rec[bi, i, k] += d_pre[i] * h_old[k]
|
||||
// Thread tid serves the column role k = tid. For each i, every warp
|
||||
// thread writes to grad_w_*[i, tid] → COALESCED 128B/transaction.
|
||||
// Prior layout (thread = i) strided cross-thread writes by n_in/n_hid
|
||||
// = 512B, paying 32 separate transactions per warp instruction.
|
||||
if (tid < n_in) {
|
||||
const int k = tid;
|
||||
const float x_k = s_x[k];
|
||||
const float h_old_k = s_h_old[k]; // valid because n_in == n_hid here
|
||||
for (int i = 0; i < n_hid; ++i) {
|
||||
const float d_pre_i = sd_pre[i];
|
||||
grad_w_in_scratch[((long long)bi * n_hid + i) * n_in + k]
|
||||
+= d_pre_i * x_k;
|
||||
grad_w_rec_scratch[((long long)bi * n_hid + i) * n_hid + k]
|
||||
+= d_pre_i * h_old_k;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: grad_h_old[bi, i] = sum_j sd_pre[j] * W_rec[j, i] + dh_b * decay.
|
||||
// Thread tid is back to channel role i = tid; dh_b and decay_i
|
||||
// stayed in registers from Pass 0.
|
||||
if (tid < n_hid) {
|
||||
const int i = tid;
|
||||
float gh = dh_b * decay_i;
|
||||
for (int j = 0; j < n_hid; ++j) {
|
||||
gh += sd_pre[j] * w_rec[j * n_hid + i];
|
||||
}
|
||||
grad_h_old[(long long)bi * n_hid + i] = gh;
|
||||
}
|
||||
|
||||
// Pass 3: grad_x[bi, k] = sum_j sd_pre[j] * W_in[j, k]
|
||||
// Thread tid serves column role k = tid. Sequential along j, sd_pre
|
||||
// in shared. One coalesced write per thread (grad_x layout matches).
|
||||
if (grad_x != nullptr && tid < n_in) {
|
||||
const int k = tid;
|
||||
float gx = 0.0f;
|
||||
for (int j = 0; j < n_hid; ++j) {
|
||||
gx += sd_pre[j] * w_in[j * n_in + k];
|
||||
}
|
||||
grad_x[(long long)bi * n_in + k] = gx;
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
// cfc_step_per_branch.cu — fused per-branch CfC step (forward + backward).
|
||||
//
|
||||
// Per spec §5.4 point 1 (docs/superpowers/specs/2026-05-21-per-horizon-cfc-inference-design.md):
|
||||
// Single fused kernel covering all N_HORIZONS branches × n_batch in one launch.
|
||||
// Grid: (B, N_HORIZONS, 1), block: (MAX_BUCKET_DIM=96, 1, 1) uniform predicate.
|
||||
//
|
||||
// Uniform predicate (`threadIdx.x >= bucket_dim_k[branch]` early-return) handles
|
||||
// uneven bucket sizes [43, 43, 42] without warp divergence — the
|
||||
// comparison is against a per-block constant, so all threads in a warp take
|
||||
// the same branch.
|
||||
//
|
||||
// Per pearl_cooperative_staging_eliminates_redundant_reads: the per-batch
|
||||
// `x` and `h_old` rows are SHARED across all output channels in a block
|
||||
// (every thread reads HIDDEN_DIM elements of both in the K-loop). Stage
|
||||
// them into shared memory once at block entry. W_in / W_rec rows are
|
||||
// per-channel (different per thread c), so they remain direct DRAM reads.
|
||||
//
|
||||
// Per feedback_no_atomicadd.md: no atomicAdd anywhere. Each thread writes
|
||||
// to its own (batch, c) slot in forward, and to its own per-batch grad
|
||||
// slice in backward. Cross-batch reduction is the caller's responsibility
|
||||
// via the existing `reduce_axis0` infrastructure.
|
||||
//
|
||||
// Per pearl_no_host_branches_in_captured_graph: no host branching; all
|
||||
// control flow uses device-resident metadata read from device tensors.
|
||||
//
|
||||
// Per feedback_nvidia_grade_perf_for_kernels.md: warp-uniform branches,
|
||||
// no atomicAdd, coalesced loads via cooperative staging.
|
||||
//
|
||||
// ALPHA fix (2026-05-21): the kernel now indexes into ORIGINAL channel
|
||||
// layout via `channels_in_bucket[branch][tid]` — abandons the prior
|
||||
// bucket-grouped τ layout that mis-aligned with W_in/W_rec/b/tau_all
|
||||
// (which all stayed in original-channel layout). See
|
||||
// `bucket_transition_kernels.cu::channels_in_bucket_kernel` for the
|
||||
// lookup table.
|
||||
|
||||
#define HIDDEN_DIM 128
|
||||
#define N_HORIZONS 3
|
||||
#define MAX_BUCKET_DIM 96
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// cfc_step_per_branch_fwd: forward pass.
|
||||
//
|
||||
// Launch:
|
||||
// grid = (B, N_HORIZONS, 1)
|
||||
// block = (MAX_BUCKET_DIM = 96, 1, 1)
|
||||
// shared_mem_bytes = 2 * HIDDEN_DIM * sizeof(float) = 1024 bytes
|
||||
//
|
||||
// Per (batch, branch, thread-in-bucket) thread computes:
|
||||
// c = channels_in_bucket[branch * MAX_BUCKET_DIM + tid] (original channel index)
|
||||
// pre = b[c] + Σ_k W_in[c, k] * x[batch, k] + Σ_k W_rec[c, k] * h_old[batch, k]
|
||||
// decay = exp(-dt / max(tau_all[c], 1e-6))
|
||||
// h_new[batch, c] = h_old[batch, c] * decay + (1 - decay) * tanh(pre)
|
||||
//
|
||||
// x_local[HIDDEN_DIM] and h_old_local[HIDDEN_DIM] are cooperative-staged
|
||||
// in shared memory; without them every thread re-reads the full row
|
||||
// (HIDDEN_DIM × bucket_dim threads → up to 96 × redundant reads per row).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void cfc_step_per_branch_fwd(
|
||||
const float* __restrict__ w_in, // [HIDDEN_DIM × HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ w_rec, // [HIDDEN_DIM × HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ b, // [HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ tau_all, // [HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ x, // [B × HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ h_old, // [B × HIDDEN_DIM] original channel layout
|
||||
float dt,
|
||||
int B,
|
||||
const unsigned int* __restrict__ channels_in_bucket, // [N_HORIZONS × MAX_BUCKET_DIM]
|
||||
const unsigned int* __restrict__ bucket_dim_k, // [N_HORIZONS]
|
||||
float* __restrict__ h_new // [B × HIDDEN_DIM] original channel layout
|
||||
) {
|
||||
extern __shared__ float smem[];
|
||||
float* x_local = smem; // [HIDDEN_DIM]
|
||||
float* h_old_local = smem + HIDDEN_DIM; // [HIDDEN_DIM]
|
||||
|
||||
int batch = blockIdx.x;
|
||||
int branch = blockIdx.y;
|
||||
int tid = threadIdx.x;
|
||||
|
||||
if (batch >= B) return;
|
||||
|
||||
// Cooperative staging of per-batch x[batch, *] and h_old[batch, *] rows.
|
||||
// All threads (including those that will early-return on the per-branch
|
||||
// predicate) participate in the staging — the row is shared across the
|
||||
// block's output channels, so we need every thread to help load.
|
||||
//
|
||||
// Each thread loads HIDDEN_DIM / blockDim.x = 128 / 96 ≈ 2 elements
|
||||
// (rounded up via the stride loop).
|
||||
for (int i = tid; i < HIDDEN_DIM; i += blockDim.x) {
|
||||
x_local[i] = x[batch * HIDDEN_DIM + i];
|
||||
h_old_local[i] = h_old[batch * HIDDEN_DIM + i];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Uniform per-branch predicate: idle threads in shorter buckets early-return.
|
||||
// The comparison is against a per-block constant (bucket_dim_k[branch]),
|
||||
// so all threads in a warp take the same path → no warp divergence.
|
||||
unsigned int bucket_dim = bucket_dim_k[branch];
|
||||
if ((unsigned int)tid >= bucket_dim) return;
|
||||
|
||||
// Look up the ORIGINAL channel index for this (branch, tid) position.
|
||||
unsigned int c_u = channels_in_bucket[branch * MAX_BUCKET_DIM + tid];
|
||||
// Defensive: sentinel (0xFFFFFFFF) or out-of-range indices must not
|
||||
// write h_new. By construction of channels_in_bucket_kernel, all
|
||||
// tid < bucket_dim_k[branch] entries hold valid channel indices.
|
||||
if (c_u >= (unsigned int)HIDDEN_DIM) return;
|
||||
int c = (int)c_u;
|
||||
|
||||
// pre = b[c] + Σ_k W_in[c, k] * x_local[k] + Σ_k W_rec[c, k] * h_old_local[k]
|
||||
float pre = b[c];
|
||||
for (int k = 0; k < HIDDEN_DIM; ++k) {
|
||||
pre += w_in[c * HIDDEN_DIM + k] * x_local[k];
|
||||
}
|
||||
for (int k = 0; k < HIDDEN_DIM; ++k) {
|
||||
pre += w_rec[c * HIDDEN_DIM + k] * h_old_local[k];
|
||||
}
|
||||
|
||||
float decay = expf(-dt / fmaxf(tau_all[c], 1e-6f));
|
||||
h_new[batch * HIDDEN_DIM + c] =
|
||||
h_old_local[c] * decay + (1.0f - decay) * tanhf(pre);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// cfc_step_per_branch_bwd: backward pass.
|
||||
//
|
||||
// Launch:
|
||||
// grid = (B, N_HORIZONS, 1)
|
||||
// block = (MAX_BUCKET_DIM = 96, 1, 1)
|
||||
// shared_mem_bytes = 2 * HIDDEN_DIM * sizeof(float) = 1024 bytes
|
||||
//
|
||||
// Per-batch grad slices are written by this kernel; cross-batch
|
||||
// reduction is the caller's responsibility (existing `reduce_axis0`
|
||||
// pattern; see crates/ml-alpha/cuda/cfc_step.cu Phase B comment block).
|
||||
//
|
||||
// grad shapes:
|
||||
// grad_w_in : [B × HIDDEN_DIM × HIDDEN_DIM] per-batch scratch (overwrite)
|
||||
// grad_w_rec : [B × HIDDEN_DIM × HIDDEN_DIM] per-batch scratch (overwrite)
|
||||
// grad_b : [B × HIDDEN_DIM] per-batch scratch (overwrite)
|
||||
// grad_tau_all: [B × HIDDEN_DIM] per-batch scratch (overwrite)
|
||||
// grad_h_old : [B × HIDDEN_DIM] per-batch (overwrite)
|
||||
//
|
||||
// Note: this kernel uses OVERWRITE semantics (the caller zeros scratch
|
||||
// per training step). The single-step CfC backward in the existing
|
||||
// `cfc_step_backward_batched` uses += for K-loop accumulation; for
|
||||
// per-branch usage in Phase 2 the K-loop's per-position accumulation is
|
||||
// the caller's concern (matching existing scratch + reduce pattern).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void cfc_step_per_branch_bwd(
|
||||
const float* __restrict__ w_in, // [HIDDEN_DIM × HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ w_rec, // [HIDDEN_DIM × HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ b, // [HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ tau_all, // [HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ x, // [B × HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ h_old, // [B × HIDDEN_DIM] original channel layout
|
||||
const float* __restrict__ grad_h_new, // [B × HIDDEN_DIM] original channel layout
|
||||
float dt,
|
||||
int B,
|
||||
const unsigned int* __restrict__ channels_in_bucket, // [N_HORIZONS × MAX_BUCKET_DIM]
|
||||
const unsigned int* __restrict__ bucket_dim_k, // [N_HORIZONS]
|
||||
float* __restrict__ grad_w_in, // [B × HIDDEN_DIM × HIDDEN_DIM]
|
||||
float* __restrict__ grad_w_rec, // [B × HIDDEN_DIM × HIDDEN_DIM]
|
||||
float* __restrict__ grad_b, // [B × HIDDEN_DIM]
|
||||
float* __restrict__ grad_tau_all, // [B × HIDDEN_DIM]
|
||||
float* __restrict__ grad_h_old // [B × HIDDEN_DIM]
|
||||
) {
|
||||
extern __shared__ float smem[];
|
||||
float* x_local = smem; // [HIDDEN_DIM]
|
||||
float* h_old_local = smem + HIDDEN_DIM; // [HIDDEN_DIM]
|
||||
|
||||
int batch = blockIdx.x;
|
||||
int branch = blockIdx.y;
|
||||
int tid = threadIdx.x;
|
||||
|
||||
if (batch >= B) return;
|
||||
|
||||
// Cooperative staging — same pattern as forward.
|
||||
for (int i = tid; i < HIDDEN_DIM; i += blockDim.x) {
|
||||
x_local[i] = x[batch * HIDDEN_DIM + i];
|
||||
h_old_local[i] = h_old[batch * HIDDEN_DIM + i];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
unsigned int bucket_dim = bucket_dim_k[branch];
|
||||
if ((unsigned int)tid >= bucket_dim) return;
|
||||
|
||||
// Look up the ORIGINAL channel index for this (branch, tid) position.
|
||||
unsigned int c_u = channels_in_bucket[branch * MAX_BUCKET_DIM + tid];
|
||||
if (c_u >= (unsigned int)HIDDEN_DIM) return;
|
||||
int c = (int)c_u;
|
||||
|
||||
// Recompute forward pre + tanh to derive d_pre.
|
||||
float pre = b[c];
|
||||
for (int k = 0; k < HIDDEN_DIM; ++k) {
|
||||
pre += w_in[c * HIDDEN_DIM + k] * x_local[k];
|
||||
}
|
||||
for (int k = 0; k < HIDDEN_DIM; ++k) {
|
||||
pre += w_rec[c * HIDDEN_DIM + k] * h_old_local[k];
|
||||
}
|
||||
const float tau_eps = 1e-6f;
|
||||
const float tau_raw = tau_all[c];
|
||||
const float tau_eff = fmaxf(tau_raw, tau_eps);
|
||||
const float decay = expf(-dt / tau_eff);
|
||||
const float s = tanhf(pre);
|
||||
const float dh = grad_h_new[batch * HIDDEN_DIM + c];
|
||||
const float d_pre = dh * (1.0f - decay) * (1.0f - s * s);
|
||||
const float d_decay = dh * (h_old_local[c] - s);
|
||||
|
||||
// Per-batch grad scratch writes — each thread (batch, branch, c) is the
|
||||
// sole writer of grad_*[batch, c, ...]. No race; no atomicAdd.
|
||||
grad_b[batch * HIDDEN_DIM + c] = d_pre;
|
||||
|
||||
// grad_tau receives 0 when tau is at the clamp floor (strict d(max)/d(tau) = 0).
|
||||
const float gate = (tau_raw > tau_eps) ? 1.0f : 0.0f;
|
||||
grad_tau_all[batch * HIDDEN_DIM + c] =
|
||||
gate * d_decay * decay * dt / (tau_eff * tau_eff);
|
||||
|
||||
// grad_w_in[batch, c, k] = d_pre * x_local[k]
|
||||
// grad_w_rec[batch, c, k] = d_pre * h_old_local[k]
|
||||
//
|
||||
// Layout note: thread serves the (batch, c) role, sweeping k in the inner
|
||||
// loop. Writes along k are coalesced within a thread but strided across
|
||||
// threads (each thread writes its own [c, *] row in a per-(batch) slab).
|
||||
// Per pearl_coalesce_via_thread_role_swap: this is single-thread-writes-
|
||||
// single-row, the warp-coalescing concern is on cross-thread writes to
|
||||
// the SAME row, which we don't do here. No swap needed.
|
||||
for (int k = 0; k < HIDDEN_DIM; ++k) {
|
||||
const long long off =
|
||||
(long long)batch * HIDDEN_DIM * HIDDEN_DIM
|
||||
+ (long long)c * HIDDEN_DIM + k;
|
||||
grad_w_in[off] = d_pre * x_local[k];
|
||||
grad_w_rec[off] = d_pre * h_old_local[k];
|
||||
}
|
||||
|
||||
// grad_h_old[batch, c] receives the direct decay contribution. The cross-
|
||||
// channel term Σ_j d_pre[j] * W_rec[j, c] requires a reduction across
|
||||
// threads in the block — the caller may run a second-pass reduction
|
||||
// kernel when grad_h_old needs to feed backward through a prior CfC
|
||||
// step (currently CfC is K=1 in per-branch Phase 2, so this direct
|
||||
// contribution is the only one).
|
||||
grad_h_old[batch * HIDDEN_DIM + c] = dh * decay;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// compute_advantage_return.cu — done-gated TD advantage for V regression.
|
||||
//
|
||||
// π is trained via target-Q distillation + SAC entropy (separate kernel).
|
||||
// This kernel only computes V regression targets. Done-gated: non-done
|
||||
// advantages are zero (V learns from trade outcomes only).
|
||||
|
||||
#define RL_GAMMA_INDEX 400
|
||||
|
||||
extern "C" __global__ void compute_advantage_return(
|
||||
const float* __restrict__ isv,
|
||||
const float* __restrict__ rewards,
|
||||
const float* __restrict__ dones,
|
||||
const float* __restrict__ v_t,
|
||||
const float* __restrict__ v_tp1,
|
||||
float* __restrict__ returns,
|
||||
float* __restrict__ advantages,
|
||||
int b_size
|
||||
) {
|
||||
const int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= b_size) return;
|
||||
|
||||
const float gamma = isv[RL_GAMMA_INDEX];
|
||||
const float r = rewards[b];
|
||||
const float is_done = (dones[b] > 0.5f);
|
||||
const float vt = v_t[b];
|
||||
const float vtp1 = v_tp1[b];
|
||||
|
||||
// 2026-05-29: branch-gate (not multiplication-gate) the V_tp1 term
|
||||
// and the (ret - vt) advantage. Multiplication-gating fails on IEEE
|
||||
// `0 * Inf = NaN`: if any batch's encoder produces a non-finite V
|
||||
// prediction early in training (random init outliers), the prior
|
||||
// `done * (ret - vt)` multiplication propagated NaN to ALL non-done
|
||||
// batches' advantages, which then broke compute_advantage_rms (sum
|
||||
// of A² → NaN) and PPO (A/RMS → NaN, ratio×A → NaN, l_pi → NaN).
|
||||
// Branch-gating ensures non-finite vtp1/vt are NEVER mixed into a
|
||||
// non-done batch's advantage. Validated by the smoke bisect at
|
||||
// 10d4614fb (deterministic NaN at step 4 with l_v=6.329 stable —
|
||||
// proving V REGRESSION is fine while V FORWARD has at least one
|
||||
// non-finite output that the 0*Inf trick was promoting to NaN
|
||||
// everywhere). Per `pearl_atomicadd_masks_v_instability`.
|
||||
const float ret = is_done ? r : (r + gamma * vtp1);
|
||||
returns[b] = ret;
|
||||
advantages[b] = is_done ? (ret - vt) : 0.0f;
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
// dqn_distributional_q.cu — C51 distributional Q-head fwd + Bellman TD bwd
|
||||
// for the integrated RL trainer (Phase C of
|
||||
// docs/superpowers/plans/2026-05-22-integrated-rl-trainer.md).
|
||||
//
|
||||
// Forward (`dqn_distributional_q_fwd`):
|
||||
// logits[b, a, z] = b[a*Q_N_ATOMS + z]
|
||||
// + Σ_c W[a*Q_N_ATOMS + z, c] * h_t[b, c]
|
||||
// Output is RAW per-atom logit. Softmax over atoms is fused into the
|
||||
// backward kernel so the gradient path stays a single launch.
|
||||
//
|
||||
// Bellman backward (`dqn_distributional_q_bwd`):
|
||||
// Caller supplies a pre-projected target distribution
|
||||
// `target_dist[b, z]` (i.e. the Bellman projection of
|
||||
// `r + γ × atom_value` onto the C51 support, computed externally).
|
||||
// Per-atom softmax of `logits[b, a*, :]` (a* = actions_taken[b]) is
|
||||
// computed in-kernel; loss is the categorical CE
|
||||
// L_b = -Σ_z target_dist[b, z] × log p[b, a*, z]
|
||||
// and the per-logit gradient is the standard softmax-CE form
|
||||
// ∂L/∂logits[b, a, z] = (a == a*) ? (p[b, a*, z] - target_dist[b, z]) : 0.
|
||||
//
|
||||
// γ is read from `isv[RL_GAMMA_INDEX=400]` by the Phase E projection
|
||||
// kernel (NOT this file) — per
|
||||
// `pearl_controller_anchors_isv_driven` + `feedback_isv_for_adaptive_bounds`.
|
||||
// Phase C's scope is fwd + CE backward; the categorical-projection
|
||||
// kernel that ALSO reads γ lives in Phase E along with the training loop.
|
||||
//
|
||||
// Block layout:
|
||||
// Forward: grid = (B, N_ACTIONS, 1); block = (Q_N_ATOMS, 1, 1).
|
||||
// One thread per atom. Each thread reduces over HIDDEN_DIM
|
||||
// via a strided inner loop; no cross-thread reduction
|
||||
// needed (each atom is an independent output slot).
|
||||
// Backward: grid = (B, 1, 1); block = (BWD_BLOCK_SIZE = 256, 1, 1).
|
||||
// One thread per (action, atom) pair (231 live threads,
|
||||
// 25 padding). Softmax uses shared-memory reduce (21
|
||||
// atoms for the taken action). Loss uses a 256-wide
|
||||
// block-level tree-reduce in shared memory. Per-batch CE
|
||||
// is written to `loss_per_batch[batch]` (single writer per
|
||||
// slot); the trainer's scalar diagnostic is produced by a
|
||||
// separate `mean_reduce_b_f32` kernel call. No atomicAdd.
|
||||
//
|
||||
// REDUCTION DISCIPLINE (per `feedback_no_atomicadd.md`):
|
||||
// F4.1 (2026-05-31) removed the prior `atomicAdd(loss_out, ce/B)`
|
||||
// call. The kernel still tree-reduces atoms-per-batch in shared
|
||||
// memory; cross-batch reduction is now external (single block,
|
||||
// `mean_reduce_b_f32` in ppo_loss_reduce_b cubin) so every device
|
||||
// write in the loss path has exactly one writer. The atomicAdd was
|
||||
// structurally fine here because the trainer DID memset `loss_out`
|
||||
// between launches in `dqn_replay_step`, but the same pattern in
|
||||
// `ppo_clipped_surrogate_fwd` lacked that memset and silently
|
||||
// accumulated across steps — see commit log for the
|
||||
// step-count-fingerprint diagnosis. Both fixed identically.
|
||||
|
||||
#define HIDDEN_DIM 128
|
||||
#define N_ACTIONS 11
|
||||
#define Q_N_ATOMS 21
|
||||
|
||||
// V_MIN / V_MAX / DELTA_Z are kept here as documentation only — the
|
||||
// projection step (which would consume them) lives in the Phase E
|
||||
// `dqn_categorical_project` kernel. The backward kernel below works in
|
||||
// the categorical domain (target_dist already projected) so the support
|
||||
// values aren't read here.
|
||||
#define V_MIN (-1.0f)
|
||||
#define V_MAX (1.0f)
|
||||
#define DELTA_Z ((V_MAX - V_MIN) / (Q_N_ATOMS - 1))
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// dqn_distributional_q_fwd:
|
||||
// Per-action × per-atom linear projection of the encoder hidden state.
|
||||
// One block per (batch, action) slot; one thread per atom.
|
||||
//
|
||||
// Inputs:
|
||||
// w [N_ACTIONS × Q_N_ATOMS, HIDDEN_DIM] — row-major weight matrix
|
||||
// b [N_ACTIONS × Q_N_ATOMS] — bias vector
|
||||
// h_t [B × HIDDEN_DIM] — encoder hidden state
|
||||
// B — batch size
|
||||
// Outputs:
|
||||
// logits[B × N_ACTIONS × Q_N_ATOMS] — raw atom logits
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void dqn_distributional_q_fwd(
|
||||
const float* __restrict__ w, // [N_ACTIONS * Q_N_ATOMS, HIDDEN_DIM]
|
||||
const float* __restrict__ b, // [N_ACTIONS * Q_N_ATOMS]
|
||||
const float* __restrict__ h_t, // [B * HIDDEN_DIM]
|
||||
int B,
|
||||
float* __restrict__ logits // [B * N_ACTIONS * Q_N_ATOMS]
|
||||
) {
|
||||
const int batch = blockIdx.x;
|
||||
const int act = blockIdx.y;
|
||||
const int atom = threadIdx.x;
|
||||
if (batch >= B) return;
|
||||
if (act >= N_ACTIONS) return;
|
||||
if (atom >= Q_N_ATOMS) return;
|
||||
|
||||
const int out_row = act * Q_N_ATOMS + atom;
|
||||
float acc = b[out_row];
|
||||
#pragma unroll
|
||||
for (int c = 0; c < HIDDEN_DIM; ++c) {
|
||||
acc += w[out_row * HIDDEN_DIM + c] * h_t[batch * HIDDEN_DIM + c];
|
||||
}
|
||||
logits[batch * (N_ACTIONS * Q_N_ATOMS) + act * Q_N_ATOMS + atom] = acc;
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// dqn_distributional_q_bwd:
|
||||
// Categorical CE backward against a pre-projected Bellman target
|
||||
// distribution. One block per batch; one thread per (action, atom)
|
||||
// pair plus padding threads for occupancy.
|
||||
//
|
||||
// Block layout (occupancy-optimised):
|
||||
// grid = (B, 1, 1)
|
||||
// block = (BWD_BLOCK_SIZE = 256, 1, 1)
|
||||
// Threads 0..N_ACTIONS*Q_N_ATOMS-1 (= 0..230) each own one
|
||||
// `(action, atom)` slot in grad_logits[batch, :]. The 21 threads
|
||||
// whose action == actions_taken[batch] participate in the softmax
|
||||
// reduction via shared memory (they are NOT necessarily contiguous
|
||||
// in warp layout — see virtual-lane mapping below).
|
||||
// Threads 231..255 are padding and exit after the block-level loss
|
||||
// reduction.
|
||||
//
|
||||
// The previous kernel used block=(Q_N_ATOMS=21,1,1) — only 21 threads
|
||||
// per block, wasting ~84% of each SM on L40S (128 CUDA cores/SM).
|
||||
// This version launches 256 threads, achieving full SM occupancy.
|
||||
//
|
||||
// Softmax virtual-lane mapping: the taken action's 21 atoms live at
|
||||
// thread indices `act * Q_N_ATOMS + 0 .. act * Q_N_ATOMS + 20`. These
|
||||
// are NOT guaranteed to be in the same warp (e.g. act=1 → threads
|
||||
// 21..41, which straddle the warp 0/1 boundary at thread 32). The
|
||||
// reduction therefore uses shared memory (not warp shuffle) which
|
||||
// works for any action index.
|
||||
//
|
||||
// Loss accumulation: block-level tree-reduce in shared memory (no
|
||||
// atomicAdd per `feedback_no_atomicadd.md`). Thread 0 of each block
|
||||
// writes the per-batch CE to loss_per_batch[batch] and adds to
|
||||
// loss_out[0] via atomicAdd on a SINGLE scalar (the cross-batch
|
||||
// accumulator — see ATOMIC NOTE in the file header; deferred fix).
|
||||
//
|
||||
// Inputs:
|
||||
// logits [B × N_ACTIONS × Q_N_ATOMS] — raw atom logits from fwd
|
||||
// target_dist [B × Q_N_ATOMS] — Bellman-projected target
|
||||
// distribution for the
|
||||
// action that was taken
|
||||
// actions_taken [B] — integer action index in
|
||||
// [0, N_ACTIONS) per batch
|
||||
// B — batch size
|
||||
// Outputs:
|
||||
// loss_out [1] — Σ_b L_b (cross-batch atomicAdd — see header).
|
||||
// loss_per_batch [B] — per-sample CE for PER priority update.
|
||||
// grad_logits [B × N_ACTIONS × Q_N_ATOMS] — ∂L/∂logits. Non-taken
|
||||
// actions get zero grad.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
#define BWD_BLOCK_SIZE 256
|
||||
#define BWD_K_OUT (N_ACTIONS * Q_N_ATOMS) // 231
|
||||
|
||||
extern "C" __global__ void dqn_distributional_q_bwd(
|
||||
const float* __restrict__ logits, // [B * N_ACTIONS * Q_N_ATOMS]
|
||||
const float* __restrict__ target_dist, // [B * Q_N_ATOMS]
|
||||
const int* __restrict__ actions_taken, // [B]
|
||||
int B,
|
||||
float* __restrict__ loss_per_batch, // [B] — per-sample CE (PER priority update + diagnostic
|
||||
// mean reduction via `mean_reduce_b_f32`).
|
||||
float* __restrict__ grad_logits // [B * N_ACTIONS * Q_N_ATOMS]
|
||||
) {
|
||||
const int batch = blockIdx.x;
|
||||
const int tid = threadIdx.x;
|
||||
if (batch >= B) return;
|
||||
|
||||
// Decompose tid into (action, atom) for the BWD_K_OUT=231 live threads.
|
||||
// Threads tid >= BWD_K_OUT are padding — they participate in the loss
|
||||
// tree-reduce but do not read/write the logits/grad arrays.
|
||||
const int my_action = tid / Q_N_ATOMS; // 0..10 for tid < 231
|
||||
const int my_atom = tid % Q_N_ATOMS; // 0..20 for tid < 231
|
||||
const int is_live = (tid < BWD_K_OUT);
|
||||
|
||||
const int act = actions_taken[batch];
|
||||
|
||||
// ── Invalid action guard ─────────────────────────────────────────
|
||||
// Defensive: zero all grads and loss for this batch sample.
|
||||
if (act < 0 || act >= N_ACTIONS) {
|
||||
if (is_live) {
|
||||
grad_logits[batch * BWD_K_OUT + tid] = 0.0f;
|
||||
}
|
||||
if (tid == 0) {
|
||||
loss_per_batch[batch] = 0.0f;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const int is_taken = is_live && (my_action == act);
|
||||
|
||||
// ── Softmax over the taken action's atoms ────────────────────────
|
||||
// The 21 threads with my_action == act load the raw logits for the
|
||||
// taken action. We use shared memory for the max-reduce and
|
||||
// sum-exp-reduce since the 21 atoms may span two warps depending
|
||||
// on the action index.
|
||||
__shared__ float s_logits[Q_N_ATOMS];
|
||||
__shared__ float s_exp[Q_N_ATOMS];
|
||||
__shared__ float s_max;
|
||||
__shared__ float s_sumexp;
|
||||
|
||||
if (is_taken) {
|
||||
const int base_taken = batch * BWD_K_OUT + act * Q_N_ATOMS;
|
||||
s_logits[my_atom] = logits[base_taken + my_atom];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Thread 0 computes max over Q_N_ATOMS=21 — sequential is cheaper
|
||||
// than a tree-reduce for 21 elements.
|
||||
if (tid == 0) {
|
||||
float m = s_logits[0];
|
||||
#pragma unroll
|
||||
for (int z = 1; z < Q_N_ATOMS; ++z) {
|
||||
m = fmaxf(m, s_logits[z]);
|
||||
}
|
||||
s_max = m;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Per-atom exp, written by the taken-action threads.
|
||||
if (is_taken) {
|
||||
s_exp[my_atom] = expf(s_logits[my_atom] - s_max);
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Thread 0 computes sum-exp.
|
||||
if (tid == 0) {
|
||||
float sum = 0.0f;
|
||||
#pragma unroll
|
||||
for (int z = 0; z < Q_N_ATOMS; ++z) {
|
||||
sum += s_exp[z];
|
||||
}
|
||||
s_sumexp = sum;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Gradient writeback (all BWD_K_OUT=231 live threads) ──────────
|
||||
// Taken-action threads: grad = p[z] - target[z].
|
||||
// Non-taken-action threads: grad = 0.
|
||||
if (is_live) {
|
||||
float grad_val = 0.0f;
|
||||
if (is_taken) {
|
||||
const float p_z = s_exp[my_atom] / s_sumexp;
|
||||
const float t_z = target_dist[batch * Q_N_ATOMS + my_atom];
|
||||
grad_val = p_z - t_z;
|
||||
}
|
||||
grad_logits[batch * BWD_K_OUT + tid] = grad_val;
|
||||
}
|
||||
|
||||
// ── Per-atom CE contribution (taken-action threads only) ─────────
|
||||
// Each of the 21 taken-action threads computes its
|
||||
// `-target[z] * log(p[z])` contribution. Padding and non-taken
|
||||
// threads contribute 0. The block-level tree-reduce below sums
|
||||
// these into a single per-batch CE.
|
||||
float my_ce = 0.0f;
|
||||
if (is_taken) {
|
||||
const float pz = s_exp[my_atom] / s_sumexp;
|
||||
const float pz_c = fmaxf(pz, 1e-7f);
|
||||
const float tz = target_dist[batch * Q_N_ATOMS + my_atom];
|
||||
my_ce = -tz * logf(pz_c);
|
||||
}
|
||||
|
||||
// ── Block-level tree-reduce for loss (no atomicAdd within block) ─
|
||||
// BWD_BLOCK_SIZE=256 = 8 warps. Tree-reduce in shared memory.
|
||||
__shared__ float s_ce[BWD_BLOCK_SIZE];
|
||||
s_ce[tid] = my_ce;
|
||||
__syncthreads();
|
||||
|
||||
#pragma unroll
|
||||
for (int stride = BWD_BLOCK_SIZE / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) {
|
||||
s_ce[tid] += s_ce[tid + stride];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
// F4.1 (2026-05-31): single writer per batch slot — no atomicAdd.
|
||||
// The trainer-side scalar `loss_out` is produced by a separate
|
||||
// `mean_reduce_b_f32` block tree-reduce kernel (ppo_loss_reduce_b
|
||||
// cubin), called immediately after this kernel. Replaces the Phase
|
||||
// C "loud-flagged" atomicAdd that — like the analogous bug in
|
||||
// `ppo_clipped_surrogate_fwd` — accumulated across steps whenever
|
||||
// the caller forgot to memset between launches, producing
|
||||
// step-count-fingerprinted loss inflation. Per
|
||||
// `feedback_no_atomicadd`.
|
||||
if (tid == 0) {
|
||||
loss_per_batch[batch] = s_ce[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// dqn_grad_w_b_h_t (Phase E.2):
|
||||
// Given per-batch grad_logits from `dqn_distributional_q_bwd`, propagate
|
||||
// to weight / bias gradients (per-batch scratch — caller reduces via
|
||||
// reduce_axis0) and to grad_h_t (per-(batch, c) sole writer; OVERWRITE).
|
||||
//
|
||||
// Mathematics (per batch b, hidden unit c):
|
||||
// grad_w_per_batch[b, a*Q_N_ATOMS + z, c]
|
||||
// = grad_logits[b, a, z] × h_t[b, c]
|
||||
// grad_b_per_batch[b, a*Q_N_ATOMS + z]
|
||||
// = grad_logits[b, a, z]
|
||||
// grad_h_t[b, c]
|
||||
// = Σ_{a, z} grad_logits[b, a, z] × w[a*Q_N_ATOMS + z, c]
|
||||
//
|
||||
// Block layout (mirrors aux_heads_bwd):
|
||||
// grid = (B, 1, 1)
|
||||
// block = (HIDDEN_DIM = 128, 1, 1)
|
||||
// shared_mem_bytes = (N_ACTIONS * Q_N_ATOMS) * sizeof(float)
|
||||
// — stages grad_logits row [a*Q_N_ATOMS + z] once
|
||||
//
|
||||
// Thread c is the sole writer of grad_w_per_batch[batch, k, c] (for all k
|
||||
// in [0, N_ACTIONS * Q_N_ATOMS)) and grad_h_t[batch, c]. The first
|
||||
// N_ACTIONS * Q_N_ATOMS threads (= 189 ≤ 128 → spills past block size)
|
||||
// would need the bias write — instead, atom-style threads aren't
|
||||
// available; we use a tile-wise loop where the first 128 threads
|
||||
// cover the first 128 bias slots and a stride-loop covers the
|
||||
// remaining 189 - 128 = 61. Per `feedback_no_atomicadd.md`: each k
|
||||
// has a single thread writing it (one stride loop, single writer per k).
|
||||
//
|
||||
// No atomicAdd. No nvrtc.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void dqn_grad_w_b_h_t(
|
||||
const float* __restrict__ w, // [N_ACTIONS * Q_N_ATOMS, HIDDEN_DIM]
|
||||
const float* __restrict__ h_t, // [B * HIDDEN_DIM]
|
||||
const float* __restrict__ grad_logits, // [B * N_ACTIONS * Q_N_ATOMS]
|
||||
int B,
|
||||
float* __restrict__ grad_w_per_batch, // [B * N_ACTIONS * Q_N_ATOMS * HIDDEN_DIM]
|
||||
float* __restrict__ grad_b_per_batch, // [B * N_ACTIONS * Q_N_ATOMS]
|
||||
float* __restrict__ grad_h_t // [B * HIDDEN_DIM] (OVERWRITE)
|
||||
) {
|
||||
// grad_logits row for this batch: [N_ACTIONS * Q_N_ATOMS] = 189 floats.
|
||||
extern __shared__ float s_gl[]; // [N_ACTIONS * Q_N_ATOMS]
|
||||
|
||||
const int batch = blockIdx.x;
|
||||
const int c = threadIdx.x;
|
||||
if (batch >= B) return;
|
||||
|
||||
const int K_OUT = N_ACTIONS * Q_N_ATOMS; // 189
|
||||
|
||||
// Cooperative stage of grad_logits[batch, :] into shared. Stride-loop
|
||||
// over K_OUT in steps of HIDDEN_DIM (block size).
|
||||
for (int k = c; k < K_OUT; k += HIDDEN_DIM) {
|
||||
s_gl[k] = grad_logits[batch * K_OUT + k];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── grad_b_per_batch ─ stride-loop, sole writer per k ────────────
|
||||
for (int k = c; k < K_OUT; k += HIDDEN_DIM) {
|
||||
grad_b_per_batch[batch * K_OUT + k] = s_gl[k];
|
||||
}
|
||||
|
||||
if (c >= HIDDEN_DIM) return;
|
||||
|
||||
// ── grad_w + grad_h_t (thread role c = hidden unit) ──────────────
|
||||
const float h_bc = h_t[batch * HIDDEN_DIM + c];
|
||||
float gh_acc = 0.0f;
|
||||
for (int k = 0; k < K_OUT; ++k) {
|
||||
const float g_k = s_gl[k];
|
||||
// grad_w[batch, k, c] = g_k × h_bc — sole writer per (batch, k, c).
|
||||
grad_w_per_batch[(long long) batch * K_OUT * HIDDEN_DIM
|
||||
+ (long long) k * HIDDEN_DIM + c]
|
||||
= g_k * h_bc;
|
||||
gh_acc += g_k * w[k * HIDDEN_DIM + c];
|
||||
}
|
||||
// OVERWRITE — caller folds via grad_h_accumulate_scaled.
|
||||
grad_h_t[batch * HIDDEN_DIM + c] = gh_acc;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// dqn_target_soft_update.cu — element-wise Polyak-style target-net
|
||||
// soft update for the C51 Q-head (Phase R5 of the integrated RL
|
||||
// trainer rebuild;
|
||||
// see docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
|
||||
//
|
||||
// Closes defect #4 from the flawed Phase F+G arc: `DqnHead` owned
|
||||
// `w_target_d` / `b_target_d` but NO kernel updated them. The
|
||||
// "target network" was actually the initial random init for the
|
||||
// entire run — no soft Polyak update, no hard sync. Double-DQN's
|
||||
// Bellman target reduced to a static-target backup, which the
|
||||
// `MockLobEnv` toy fixture's horizon=1 reward structure couldn't
|
||||
// expose.
|
||||
//
|
||||
// Per-element formula:
|
||||
//
|
||||
// target[i] = (1 − τ) · target[i] + τ · current[i]
|
||||
//
|
||||
// τ is read from `ISV[RL_TARGET_TAU_INDEX = 401]`. R1 bootstraps the
|
||||
// slot to 0.005; R5 adapts via `rl_target_tau_controller` reading the
|
||||
// Q-divergence EMA from ISV[418] (Phase R3 ema_update_per_step writes
|
||||
// the input).
|
||||
//
|
||||
// Element-wise, trivially parallel — one thread per weight scalar.
|
||||
// No reduction, no atomics (per `feedback_no_atomicadd`). Called twice
|
||||
// per training step: once for the weight tensor, once for the bias
|
||||
// tensor. Both calls share the same τ read since the kernel reads ISV
|
||||
// at each invocation.
|
||||
|
||||
#define RL_TARGET_TAU_INDEX 401
|
||||
|
||||
extern "C" __global__ void dqn_target_soft_update(
|
||||
const float* __restrict__ current,
|
||||
float* __restrict__ target,
|
||||
const float* __restrict__ isv,
|
||||
int n_elements
|
||||
) {
|
||||
const int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= n_elements) return;
|
||||
|
||||
const float tau = isv[RL_TARGET_TAU_INDEX];
|
||||
target[i] = (1.0f - tau) * target[i] + tau * current[i];
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// ema_update_on_done.cu — done-gated EMA producer for ISV-resident
|
||||
// adaptive-control EMAs (Phase R3 of the integrated RL trainer rebuild;
|
||||
// see docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
|
||||
//
|
||||
// Generic over the ISV slot — one kernel serves all the closed-trade
|
||||
// EMAs the 7 RL controllers consume (e.g. mean_abs_pnl_ema → ISV[423]
|
||||
// feeding rl_reward_scale_controller; q_divergence_ema → ISV[418]
|
||||
// feeding rl_target_tau_controller; etc.). The caller passes the slot
|
||||
// index and the Wiener-α (already floored at 0.4 per
|
||||
// `pearl_wiener_alpha_floor_for_nonstationary` host-side).
|
||||
//
|
||||
// Done-gated: only the per-batch entries where `dones[b] >= 0.5`
|
||||
// contribute to the EMA observation. If no batch closed a trade this
|
||||
// step, the kernel does nothing — the ISV slot retains its prior value
|
||||
// rather than blending toward zero on hold steps. This is what
|
||||
// distinguishes "EMA over closed-trade magnitudes" from "EMA over all
|
||||
// steps" (the latter lives in `ema_update_per_step.cu`).
|
||||
//
|
||||
// Bootstrap discipline (per `pearl_first_observation_bootstrap`): if
|
||||
// the ISV slot reads sentinel `0.0`, the first non-zero observation
|
||||
// REPLACES the slot directly — no blend with the cold-start zero. If
|
||||
// `mean_obs` happens to be exactly zero on the first observation
|
||||
// (vanishingly rare for strictly-positive signals like |reward|), the
|
||||
// kernel defers the bootstrap to a later call rather than writing zero
|
||||
// back as a degenerate sentinel that would never be distinguishable
|
||||
// from the cold-start state.
|
||||
//
|
||||
// Per `feedback_no_atomicadd`: shared-memory tree reduction inside a
|
||||
// single block; no atomics. Caller passes `shared_mem_bytes =
|
||||
// 2 * b_size * sizeof(float)` (one slot for the gated-sum, one for the
|
||||
// gate count).
|
||||
|
||||
extern "C" __global__ void ema_update_on_done(
|
||||
float* __restrict__ isv, // ISV bus (≥ slot_index + 1)
|
||||
int slot_index, // which ISV slot to update
|
||||
float alpha, // Wiener-α (host-floored ≥ 0.4)
|
||||
const float* __restrict__ obs, // [b_size] per-batch raw signal
|
||||
const float* __restrict__ dones, // [b_size] 0.0 / 1.0 done flag
|
||||
int b_size
|
||||
) {
|
||||
// Single block, b_size threads. The block layout requires
|
||||
// b_size ≤ blockDim.x; callers (Phase R5) launch with
|
||||
// block_dim = (max(b_size, 1), 1, 1). For b_size = 1 the tree
|
||||
// loop degenerates and the if at the end runs unchanged.
|
||||
extern __shared__ float smem[];
|
||||
float* s_obs_gated = smem; // [b_size]
|
||||
float* s_gate = smem + b_size; // [b_size]
|
||||
|
||||
const int tid = threadIdx.x;
|
||||
if (tid >= b_size) return;
|
||||
|
||||
const float d = dones[tid];
|
||||
const float gate = (d >= 0.5f) ? 1.0f : 0.0f;
|
||||
s_obs_gated[tid] = obs[tid] * gate;
|
||||
s_gate[tid] = gate;
|
||||
__syncthreads();
|
||||
|
||||
// Tree reduction (block-level, no atomicAdd per
|
||||
// feedback_no_atomicadd). Power-of-two stride covers b_size ≤
|
||||
// blockDim.x; bounds-check `tid + stride < b_size` handles
|
||||
// non-power-of-two b_size correctly.
|
||||
for (int stride = 1; stride < b_size; stride *= 2) {
|
||||
if ((tid % (stride * 2)) == 0 && (tid + stride) < b_size) {
|
||||
s_obs_gated[tid] += s_obs_gated[tid + stride];
|
||||
s_gate[tid] += s_gate[tid + stride];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
const float count = s_gate[0];
|
||||
if (count == 0.0f) {
|
||||
// No closed trade this step — preserve the prior EMA.
|
||||
return;
|
||||
}
|
||||
const float mean_obs = s_obs_gated[0] / count;
|
||||
const float prev = isv[slot_index];
|
||||
if (prev == 0.0f) {
|
||||
// First-observation bootstrap per pearl_first_observation_bootstrap.
|
||||
// Defer if mean_obs == 0 so we don't write a sentinel that
|
||||
// would be re-bootstrapped on the next call.
|
||||
if (mean_obs != 0.0f) {
|
||||
isv[slot_index] = mean_obs;
|
||||
}
|
||||
} else {
|
||||
// Wiener-α blend; caller pre-floored α at 0.4 per
|
||||
// pearl_wiener_alpha_floor_for_nonstationary.
|
||||
isv[slot_index] = (1.0f - alpha) * prev + alpha * mean_obs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
// ema_update_per_step.cu — per-step EMA producer for ISV-resident
|
||||
// adaptive-control EMAs that update every step regardless of episode
|
||||
// boundary (Phase R3 of the integrated RL trainer rebuild;
|
||||
// see docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
|
||||
//
|
||||
// Companion to `ema_update_on_done.cu`. This variant has NO done-gate
|
||||
// — it consumes the per-batch raw signal on every step. Used for EMAs
|
||||
// that should update continuously rather than only at trade closes:
|
||||
//
|
||||
// * KL(π_new ‖ π_old) EMA (input to rl_ppo_clip_controller, ISV[419])
|
||||
// * Observed action entropy H(π) EMA (input to rl_entropy_coef_controller,
|
||||
// ISV[420])
|
||||
// * Advantage variance ratio EMA (input to rl_rollout_steps_controller,
|
||||
// ISV[421])
|
||||
//
|
||||
// Generic over the ISV slot — caller passes `slot_index` + Wiener-α.
|
||||
// Bootstrap discipline + reduction strategy are identical to
|
||||
// `ema_update_on_done.cu`; only the gate differs (here, every batch
|
||||
// entry contributes, so the per-batch mean is the unweighted average).
|
||||
//
|
||||
// Per `feedback_no_atomicadd`: shared-memory tree reduction in a
|
||||
// single block; no atomics. Caller passes
|
||||
// `shared_mem_bytes = b_size * sizeof(float)`.
|
||||
|
||||
extern "C" __global__ void ema_update_per_step(
|
||||
float* __restrict__ isv, // ISV bus (≥ slot_index + 1)
|
||||
int slot_index, // which ISV slot to update
|
||||
float alpha, // Wiener-α (host-floored ≥ 0.4)
|
||||
const float* __restrict__ obs, // [b_size] per-batch raw signal
|
||||
int b_size
|
||||
) {
|
||||
extern __shared__ float smem[]; // [b_size]
|
||||
|
||||
const int tid = threadIdx.x;
|
||||
if (tid >= b_size) return;
|
||||
|
||||
smem[tid] = obs[tid];
|
||||
__syncthreads();
|
||||
|
||||
// Tree reduction (block-level, no atomicAdd per
|
||||
// feedback_no_atomicadd).
|
||||
for (int stride = 1; stride < b_size; stride *= 2) {
|
||||
if ((tid % (stride * 2)) == 0 && (tid + stride) < b_size) {
|
||||
smem[tid] += smem[tid + stride];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
const float mean_obs = smem[0] / (float)b_size;
|
||||
const float prev = isv[slot_index];
|
||||
if (prev == 0.0f) {
|
||||
// First-observation bootstrap per pearl_first_observation_bootstrap.
|
||||
// Defer if mean_obs == 0 so we don't write a sentinel that
|
||||
// would be re-bootstrapped on the next call.
|
||||
if (mean_obs != 0.0f) {
|
||||
isv[slot_index] = mean_obs;
|
||||
}
|
||||
} else {
|
||||
// Wiener-α blend; caller pre-floored α at 0.4 per
|
||||
// pearl_wiener_alpha_floor_for_nonstationary.
|
||||
isv[slot_index] = (1.0f - alpha) * prev + alpha * mean_obs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// extract_realized_pnl_delta.cu — GPU-pure reward + done extraction
|
||||
// (Phase R6 of the integrated RL trainer rebuild;
|
||||
// see docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
|
||||
//
|
||||
// Replaces the host-side `read_pos` + scalar-subtract loop the flawed
|
||||
// Phase F shipped in step_with_lobsim's LobSimEnvAdapter. That loop
|
||||
// DtoH-copied the entire Pos struct per batch, computed the delta on
|
||||
// CPU, then HtoD-uploaded actions back — a multi-direction roundtrip
|
||||
// per training step per batch that violated `feedback_cpu_is_read_only`.
|
||||
//
|
||||
// This kernel reads the device-resident `Pos` array (the simulator's
|
||||
// authoritative position state) and writes:
|
||||
// - reward[b] = pos.realized_pnl − prev_realized_pnl[b]
|
||||
// - done[b] = (prev_position_lots[b] != 0 && pos.position_lots == 0) ? 1 : 0
|
||||
// - prev_realized_pnl[b] := pos.realized_pnl (for next-step delta)
|
||||
// - prev_position_lots[b] := pos.position_lots (for next-step done)
|
||||
//
|
||||
// `Pos` struct layout (from crates/ml-backtesting/src/lob/mod.rs::PosFlat,
|
||||
// #[repr(C)], bytemuck::Pod):
|
||||
// offset 0: position_lots: i32 (4 bytes)
|
||||
// offset 4: vwap_entry: f32 (4 bytes)
|
||||
// offset 8: realized_pnl: f32 (4 bytes)
|
||||
// offset 12: peak_equity: f32 (4 bytes)
|
||||
// offset 16: submission_overflow: u32 (4 bytes)
|
||||
// offset 20: open_horizon_mask: u32 (4 bytes)
|
||||
// total: 24 bytes per Pos
|
||||
//
|
||||
// `pos_bytes` is passed in as a kernel arg so this kernel doesn't have
|
||||
// to be rebuilt if PosFlat ever grows fields.
|
||||
//
|
||||
// Element-wise, one thread per batch entry. No atomics, no reductions.
|
||||
// Per `feedback_no_atomicadd` not needed.
|
||||
|
||||
extern "C" __global__ void extract_realized_pnl_delta(
|
||||
const unsigned char* __restrict__ pos_state, // [b_size * pos_bytes]
|
||||
float* __restrict__ prev_realized_pnl, // [b_size] IN/OUT
|
||||
int* __restrict__ prev_position_lots, // [b_size] IN/OUT
|
||||
float* __restrict__ reward_out, // [b_size] OUT
|
||||
float* __restrict__ done_out, // [b_size] OUT
|
||||
int b_size,
|
||||
int pos_bytes
|
||||
) {
|
||||
const int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= b_size) return;
|
||||
|
||||
const unsigned char* p = pos_state + b * pos_bytes;
|
||||
const int current_lots = *(const int*)(p + 0);
|
||||
const float current_realized = *(const float*)(p + 8);
|
||||
|
||||
const float prev_realized = prev_realized_pnl[b];
|
||||
const int prev_lots = prev_position_lots[b];
|
||||
|
||||
reward_out[b] = current_realized - prev_realized;
|
||||
done_out[b] = (prev_lots != 0 && current_lots == 0) ? 1.0f : 0.0f;
|
||||
|
||||
// Update prev_* for the next step's delta computation.
|
||||
prev_realized_pnl[b] = current_realized;
|
||||
prev_position_lots[b] = current_lots;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// gae_backward_sweep.cu — Generalized Advantage Estimation backward sweep.
|
||||
//
|
||||
// Spec: docs/superpowers/specs/2026-06-02-trainer-rollout-buffer-gae.md §1.3
|
||||
// Plan: docs/superpowers/plans/2026-06-02-trainer-rollout-buffer-gae-implementation.md
|
||||
//
|
||||
// Per batch b, sequentially compute (backward over T):
|
||||
// A_T = 0
|
||||
// for t in (T-1)..=0:
|
||||
// non_terminal = 1.0f - (float)dones[b][t]
|
||||
// v_tp1 = (t == T-1) ? v_T_bootstrap[b] : v_t[b][t+1]
|
||||
// δ_t = r[b][t] + γ * v_tp1 * non_terminal - v_t[b][t]
|
||||
// A_t = δ_t + γ * λ * A_next * non_terminal
|
||||
// A_next = A_t * non_terminal // reset GAE accumulator on terminal
|
||||
// returns[b][t] = A_t + v_t[b][t]
|
||||
//
|
||||
// Single-thread-per-batch kernel. Sequential backward sweep guarantees
|
||||
// determinism (no parallel reductions, no inter-thread/block races).
|
||||
// Memory access is strided over T for each thread — coalescing is
|
||||
// suboptimal but functional; the rollout T (≤1024) and B (≤1024) keep
|
||||
// this off the hot path. If profiling later shows it dominating, the
|
||||
// follow-up is a warp-cooperative tiled implementation that preserves
|
||||
// determinism via a single-warp reduction.
|
||||
//
|
||||
// Per `feedback_no_atomicadd`: no atomic ops anywhere.
|
||||
// Per `feedback_cpu_is_read_only`: pure device-side, no host roundtrips.
|
||||
// Per `feedback_no_nvrtc`: pre-compiled cubin (build.rs registers it).
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
extern "C" __global__ void gae_backward_sweep(
|
||||
const float* __restrict__ rewards, // [B × T]
|
||||
const uint8_t* __restrict__ dones, // [B × T]
|
||||
const float* __restrict__ v_t, // [B × T]
|
||||
const float* __restrict__ v_T_bootstrap, // [B] — V(s_T) for trajectory end
|
||||
float* __restrict__ advantages, // [B × T] (output)
|
||||
float* __restrict__ returns, // [B × T] (output)
|
||||
const int B,
|
||||
const int T,
|
||||
const float gamma,
|
||||
const float lambda
|
||||
) {
|
||||
const int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= B) return;
|
||||
|
||||
float a_next = 0.0f;
|
||||
for (int t = T - 1; t >= 0; t--) {
|
||||
const int idx = b * T + t;
|
||||
const float r_t = rewards[idx];
|
||||
const float v_curr = v_t[idx];
|
||||
const float non_terminal = 1.0f - (float)dones[idx];
|
||||
const float v_tp1 = (t == T - 1) ? v_T_bootstrap[b] : v_t[idx + 1];
|
||||
const float delta = r_t + gamma * v_tp1 * non_terminal - v_curr;
|
||||
const float a_t = delta + gamma * lambda * a_next * non_terminal;
|
||||
advantages[idx] = a_t;
|
||||
returns[idx] = a_t + v_curr;
|
||||
a_next = a_t * non_terminal; // reset on done
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// gpu_log_helpers.cuh — header-only device helpers for the GPU log ring.
|
||||
//
|
||||
// Include this from any .cu file that needs to call `log_record()`.
|
||||
// The `gpu_log_tick` kernel itself lives in `gpu_log_ring.cu`.
|
||||
//
|
||||
// Per `feedback_single_source_of_truth_no_duplicates`: this is the ONE
|
||||
// definition of `LogHeader` / `LogRecord` / `LogRing` and `log_record`;
|
||||
// other kernels must include this header instead of duplicating the
|
||||
// struct layouts. The IDs (kernel_id / record_type / magic / slot math
|
||||
// constants) live in `gpu_log_ids.h` and are included here.
|
||||
|
||||
#ifndef GPU_LOG_HELPERS_CUH
|
||||
#define GPU_LOG_HELPERS_CUH
|
||||
|
||||
#include "gpu_log_ids.h"
|
||||
#include <cstdint>
|
||||
|
||||
struct LogHeader {
|
||||
uint32_t magic;
|
||||
uint32_t step;
|
||||
uint8_t kernel_id;
|
||||
uint8_t record_type;
|
||||
uint8_t payload_words;
|
||||
uint8_t reserved;
|
||||
uint32_t _padding;
|
||||
};
|
||||
|
||||
struct LogRecord {
|
||||
LogHeader header;
|
||||
float payload[GPU_LOG_PAYLOAD_F32];
|
||||
};
|
||||
|
||||
struct LogRing {
|
||||
LogRecord records[GPU_LOG_N_SLOTS];
|
||||
};
|
||||
|
||||
// log_record — emit a diagnostic record from a kernel.
|
||||
//
|
||||
// Single-writer per (step, kernel_id, record_type) slot. Thread (0,0) of
|
||||
// block 0 writes; all other threads no-op. Header-last commit ensures
|
||||
// torn-record reads on the host side are detectable via magic mismatch.
|
||||
//
|
||||
// Caller responsibility:
|
||||
// - g_log_ring == nullptr → no-op (logging disabled).
|
||||
// - g_step_counter is the device step counter (incremented by gpu_log_tick).
|
||||
// - payload_words ≤ GPU_LOG_PAYLOAD_F32 (12); kernel must respect this.
|
||||
__device__ __forceinline__ void log_record(
|
||||
LogRing* g_log_ring,
|
||||
const int* g_step_counter,
|
||||
uint8_t kernel_id,
|
||||
uint8_t record_type,
|
||||
const float* payload,
|
||||
int payload_words
|
||||
) {
|
||||
if (g_log_ring == nullptr) return;
|
||||
if (blockIdx.x != 0 || threadIdx.x != 0) return;
|
||||
|
||||
const int step = g_step_counter[0];
|
||||
const int slot = (step * GPU_LOG_N_RECORDS_PER_STEP
|
||||
+ (int)kernel_id * GPU_LOG_N_RT_PER_KERNEL
|
||||
+ (int)record_type) & (GPU_LOG_N_SLOTS - 1);
|
||||
LogRecord* rec = &g_log_ring->records[slot];
|
||||
|
||||
const int n = payload_words < GPU_LOG_PAYLOAD_F32 ? payload_words : GPU_LOG_PAYLOAD_F32;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < GPU_LOG_PAYLOAD_F32; ++i) {
|
||||
rec->payload[i] = (i < n) ? payload[i] : 0.0f;
|
||||
}
|
||||
rec->header.step = (uint32_t)step;
|
||||
rec->header.kernel_id = kernel_id;
|
||||
rec->header.record_type = record_type;
|
||||
rec->header.payload_words = (uint8_t)n;
|
||||
rec->header.reserved = 0;
|
||||
rec->header._padding = 0;
|
||||
|
||||
__threadfence(); // make payload + header fields visible …
|
||||
rec->header.magic = GPU_LOG_MAGIC; // … BEFORE the commit sentinel.
|
||||
}
|
||||
|
||||
#endif // GPU_LOG_HELPERS_CUH
|
||||
@@ -1,39 +0,0 @@
|
||||
// gpu_log_ids.h — single source of truth for log-ring kernel/record IDs.
|
||||
//
|
||||
// Both kernel-side (#include from .cu files) and Rust-side decoder reference
|
||||
// these symbols. Drift between them is checked by `build.rs` (Task 2).
|
||||
//
|
||||
// kernel_id (uint8) — which kernel emitted the record.
|
||||
// record_type (uint8) — sub-type within the kernel (input/state/output/debug).
|
||||
// payload layout — defined per (kernel_id, record_type) in decoder.
|
||||
|
||||
#ifndef GPU_LOG_IDS_H
|
||||
#define GPU_LOG_IDS_H
|
||||
|
||||
#define GPU_LOG_N_KERNELS 8
|
||||
#define GPU_LOG_N_RT_PER_KERNEL 4
|
||||
#define GPU_LOG_N_RECORDS_PER_STEP (GPU_LOG_N_KERNELS * GPU_LOG_N_RT_PER_KERNEL)
|
||||
#define GPU_LOG_N_SLOTS 32768
|
||||
#define GPU_LOG_RECORD_BYTES 64
|
||||
#define GPU_LOG_PAYLOAD_F32 12
|
||||
#define GPU_LOG_MAGIC 0xF0F0A710u
|
||||
|
||||
// Kernel IDs (uint8). Add new kernels at the bottom; never reorder.
|
||||
#define KID_SMOOTHNESS_CONTROLLER 0
|
||||
#define KID_OUTPUT_SMOOTHNESS 1
|
||||
#define KID_BCE_MULTI_HORIZON 2
|
||||
#define KID_HORIZON_LAMBDA 3
|
||||
#define KID_HEADS_GRN_FWD 4
|
||||
#define KID_HEADS_GRN_BWD 5
|
||||
#define KID_CFC_STEP 6
|
||||
#define KID_RESERVED_7 7
|
||||
|
||||
// Record types within a kernel (uint8). Reuse RT_INPUT/STATE/OUTPUT/DEBUG
|
||||
// across kernels — payload layout differs per (kid, rt) pair but the four
|
||||
// roles are conventional.
|
||||
#define RT_INPUT 0
|
||||
#define RT_STATE 1
|
||||
#define RT_OUTPUT 2
|
||||
#define RT_DEBUG 3
|
||||
|
||||
#endif // GPU_LOG_IDS_H
|
||||
@@ -1,26 +0,0 @@
|
||||
// gpu_log_ring.cu — unified GPU diagnostic log ring (tick kernel).
|
||||
//
|
||||
// Provides the `gpu_log_tick` kernel that increments the step counter
|
||||
// once per captured-graph step.
|
||||
//
|
||||
// Struct definitions (`LogHeader`, `LogRecord`, `LogRing`) and the
|
||||
// `log_record` device helper live in `gpu_log_helpers.cuh` so other
|
||||
// kernels can include the helpers without re-compiling them as
|
||||
// separate symbols. Single source of truth — see
|
||||
// `feedback_single_source_of_truth_no_duplicates`.
|
||||
//
|
||||
// Constraints (still apply to anything including the helper header):
|
||||
// - No atomicAdd (slot reservation by deterministic step/kid/rt math).
|
||||
// - No host branches; thread-id gating only.
|
||||
// - Header-last commit (magic field written after payload + other header).
|
||||
// - Mapped-pinned ring; host reads via volatile, no DtoH memcpy.
|
||||
|
||||
#include "gpu_log_helpers.cuh"
|
||||
|
||||
// Tick kernel — 1 thread, 1 block. Increments step counter; runs FIRST in
|
||||
// every captured-graph replay so all subsequent kernels see the new step.
|
||||
extern "C" __global__ void gpu_log_tick(int* step_counter) {
|
||||
if (threadIdx.x == 0 && blockIdx.x == 0) {
|
||||
step_counter[0] = step_counter[0] + 1;
|
||||
}
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
// gpu_sample_and_gather.cu — GPU-resident batch sampler + AoS→SoA scatter
|
||||
//
|
||||
// Replaces the CPU-side `next_sequence_pair` + `build_sequence_at` +
|
||||
// `extend_from_slice` + mapped-pinned memcpy hot path. At b=256, K=32,
|
||||
// the CPU version spent ~16ms/step on 512 build_sequence_at calls with
|
||||
// 7,700 heap allocations while GPU compute was 0.62ms. This kernel
|
||||
// samples B random windows and gathers snapshots directly from a
|
||||
// pre-uploaded GPU-resident dataset — zero CPU work per step.
|
||||
//
|
||||
// Architecture:
|
||||
// * At init, the host pre-converts ALL Mbp10Snapshot → Mbp10RawInput
|
||||
// and uploads as a single flat CudaSlice<u8> (reinterpreted as
|
||||
// Mbp10Raw on device). File offsets + sizes are uploaded as i32
|
||||
// arrays. Labels/regime are uploaded similarly.
|
||||
// * Per step, this kernel runs with Grid=(B,1,1), Block=(K,1,1):
|
||||
// - Thread 0 of each block uses device-side xorshift32 PRNG to
|
||||
// sample file_idx and anchor within that file.
|
||||
// - All K threads gather all_snapshots[file_offset + anchor + k]
|
||||
// and scatter to SoA output buffers (same layout as
|
||||
// snapshot_aos_to_soa.cu).
|
||||
//
|
||||
// Per feedback_no_atomicadd: block-local shared memory for sampling,
|
||||
// no atomics. Per feedback_no_nvrtc: pre-compiled cubin via build.rs.
|
||||
|
||||
#define BOOK_LEVELS 10
|
||||
#define REGIME_DIM 6
|
||||
|
||||
// Must match #[repr(C)] Mbp10RawInput in snap_features.rs (216 bytes).
|
||||
struct __align__(8) Mbp10Raw {
|
||||
float bid_px[BOOK_LEVELS]; // offset 0, 40 bytes
|
||||
float bid_sz[BOOK_LEVELS]; // offset 40, 40 bytes
|
||||
float ask_px[BOOK_LEVELS]; // offset 80, 40 bytes
|
||||
float ask_sz[BOOK_LEVELS]; // offset 120, 40 bytes
|
||||
float prev_mid; // offset 160, 4 bytes
|
||||
float trade_signed_vol; // offset 164, 4 bytes
|
||||
unsigned int trade_count; // offset 168, 4 bytes
|
||||
// 4 bytes padding for u64 alignment
|
||||
unsigned long long ts_ns; // offset 176, 8 bytes
|
||||
unsigned long long prev_ts_ns; // offset 184, 8 bytes
|
||||
float regime[REGIME_DIM]; // offset 192, 24 bytes
|
||||
// total: 216 bytes
|
||||
};
|
||||
|
||||
// Marsaglia xorshift32 — minimal state, sufficient quality for
|
||||
// stochastic sampling. Each batch element carries its own seed to
|
||||
// avoid inter-block contention.
|
||||
__device__ __forceinline__ unsigned int xorshift32(unsigned int s) {
|
||||
s ^= s << 13;
|
||||
s ^= s >> 17;
|
||||
s ^= s << 5;
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── Primary kernel: sample + gather + SoA scatter ────────────────────
|
||||
//
|
||||
// Grid = (B, 1, 1) — one block per batch element
|
||||
// Block = (K, 1, 1) — one thread per sequence position
|
||||
//
|
||||
// Thread 0 samples (file_idx, anchor) via xorshift32. All K threads
|
||||
// then read their assigned snapshot from the flat GPU array and write
|
||||
// to SoA outputs at position [b * K + k].
|
||||
|
||||
extern "C" __global__ void gpu_sample_and_gather(
|
||||
const Mbp10Raw* __restrict__ all_snapshots, // [total_snaps]
|
||||
const int* __restrict__ file_offsets, // [n_files]
|
||||
const int* __restrict__ file_sizes, // [n_files]
|
||||
unsigned int* __restrict__ prng_state, // [B]
|
||||
int n_files,
|
||||
int seq_len,
|
||||
int max_horizon,
|
||||
// SoA outputs (same layout as snapshot_aos_to_soa):
|
||||
float* __restrict__ bid_px_soa, // [B*K * BOOK_LEVELS]
|
||||
float* __restrict__ bid_sz_soa,
|
||||
float* __restrict__ ask_px_soa,
|
||||
float* __restrict__ ask_sz_soa,
|
||||
float* __restrict__ regime_soa, // [B*K * REGIME_DIM]
|
||||
float* __restrict__ prev_mid_soa, // [B*K]
|
||||
float* __restrict__ tsv_soa, // [B*K]
|
||||
int* __restrict__ tc_soa, // [B*K]
|
||||
long long* __restrict__ ts_ns_soa, // [B*K]
|
||||
long long* __restrict__ prev_ts_ns_soa, // [B*K]
|
||||
// Global-memory outputs for the sampled (file_offset, anchor, file_idx)
|
||||
// per batch element. Read by gpu_gather_next, gpu_gather_frd_labels,
|
||||
// and gpu_gather_bce_labels to reuse the same sampling decision.
|
||||
int* __restrict__ out_file_offset, // [B]
|
||||
int* __restrict__ out_anchor, // [B]
|
||||
int* __restrict__ out_file_idx, // [B]
|
||||
int B
|
||||
) {
|
||||
int b = blockIdx.x;
|
||||
int k = threadIdx.x;
|
||||
if (b >= B || k >= seq_len) return;
|
||||
|
||||
// ── Thread 0: sample file + anchor via device-side PRNG ──────────
|
||||
__shared__ int s_file_offset;
|
||||
__shared__ int s_anchor;
|
||||
|
||||
if (k == 0) {
|
||||
unsigned int seed = prng_state[b];
|
||||
// Sample file index (uniform across files).
|
||||
seed = xorshift32(seed);
|
||||
int file_idx = (int)(seed % (unsigned int)n_files);
|
||||
// Sample anchor within file. Must leave room for seq_len +
|
||||
// max_horizon snapshots after the anchor (+ 1 for the s_{t+1}
|
||||
// window gathered by gpu_gather_next).
|
||||
int fsize = file_sizes[file_idx];
|
||||
int usable = fsize - seq_len - max_horizon - 1;
|
||||
seed = xorshift32(seed);
|
||||
int anchor;
|
||||
if (usable > 0) {
|
||||
anchor = (int)(seed % (unsigned int)usable);
|
||||
} else {
|
||||
// Degenerate file too short — clamp to 0 (defensive).
|
||||
anchor = 0;
|
||||
}
|
||||
s_file_offset = file_offsets[file_idx];
|
||||
s_anchor = anchor;
|
||||
// Write to global memory so downstream kernels can read the
|
||||
// same (file_offset, anchor, file_idx) without re-sampling.
|
||||
out_file_offset[b] = s_file_offset;
|
||||
out_anchor[b] = anchor;
|
||||
out_file_idx[b] = file_idx;
|
||||
prng_state[b] = seed;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── All K threads: gather + SoA scatter ──────────────────────────
|
||||
int global_idx = s_file_offset + s_anchor + k;
|
||||
const Mbp10Raw& snap = all_snapshots[global_idx];
|
||||
int n = b * seq_len + k; // output position in [B*K] flat layout
|
||||
|
||||
int base_book = n * BOOK_LEVELS;
|
||||
int base_regime = n * REGIME_DIM;
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < BOOK_LEVELS; i++) {
|
||||
bid_px_soa[base_book + i] = snap.bid_px[i];
|
||||
bid_sz_soa[base_book + i] = snap.bid_sz[i];
|
||||
ask_px_soa[base_book + i] = snap.ask_px[i];
|
||||
ask_sz_soa[base_book + i] = snap.ask_sz[i];
|
||||
}
|
||||
#pragma unroll
|
||||
for (int i = 0; i < REGIME_DIM; i++) {
|
||||
regime_soa[base_regime + i] = snap.regime[i];
|
||||
}
|
||||
prev_mid_soa[n] = snap.prev_mid;
|
||||
tsv_soa[n] = snap.trade_signed_vol;
|
||||
tc_soa[n] = (int)snap.trade_count;
|
||||
ts_ns_soa[n] = (long long)snap.ts_ns;
|
||||
prev_ts_ns_soa[n] = (long long)snap.prev_ts_ns;
|
||||
}
|
||||
|
||||
// ── Label gather kernel ──────────────────────────────────────────────
|
||||
//
|
||||
// Runs AFTER gpu_sample_and_gather. Gathers per-horizon FRD labels at
|
||||
// the anchor position (rightmost K position = newest snapshot in the
|
||||
// window, matching the supervised label semantics).
|
||||
//
|
||||
// Grid = (B, 1, 1)
|
||||
// Block = (FRD_N_HORIZONS, 1, 1) — typically 3 threads
|
||||
//
|
||||
// For RL training, the FRD labels are the primary use case. BCE labels
|
||||
// and outcome labels are handled by the supervised path and are not
|
||||
// needed per-step in the RL loop.
|
||||
|
||||
extern "C" __global__ void gpu_gather_frd_labels(
|
||||
const int* __restrict__ all_frd_labels, // [n_frd_horizons * total_snaps]
|
||||
const int* __restrict__ file_offsets, // [n_files] (same as above)
|
||||
const int* __restrict__ sample_file_offset,// [B] — file_offset chosen by gpu_sample_and_gather
|
||||
const int* __restrict__ sample_anchor, // [B] — anchor chosen by gpu_sample_and_gather
|
||||
int seq_len,
|
||||
int n_frd_horizons,
|
||||
int total_snaps,
|
||||
int* __restrict__ frd_labels_out, // [B * n_frd_horizons]
|
||||
int B
|
||||
) {
|
||||
int b = blockIdx.x;
|
||||
int h = threadIdx.x;
|
||||
if (b >= B || h >= n_frd_horizons) return;
|
||||
|
||||
// The FRD label is at the NEWEST snapshot in the window = anchor + seq_len - 1.
|
||||
int newest_idx = sample_file_offset[b] + sample_anchor[b] + seq_len - 1;
|
||||
int label_idx = h * total_snaps + newest_idx; // row-major [n_frd_horizons, total_snaps]
|
||||
frd_labels_out[b * n_frd_horizons + h] = all_frd_labels[label_idx];
|
||||
}
|
||||
|
||||
// ── Pair gather: s_{t+1} window for Bellman targets ──────────────────
|
||||
//
|
||||
// The RL trainer needs (s_t, s_{t+1}) consecutive windows. This kernel
|
||||
// gathers the SECOND window at anchor+1 into a separate set of SoA
|
||||
// buffers, reusing the same (file_offset, anchor) from the primary
|
||||
// gpu_sample_and_gather call. Avoids a second sampling pass.
|
||||
//
|
||||
// Grid = (B, 1, 1)
|
||||
// Block = (K, 1, 1)
|
||||
|
||||
extern "C" __global__ void gpu_gather_next(
|
||||
const Mbp10Raw* __restrict__ all_snapshots, // [total_snaps]
|
||||
const int* __restrict__ sample_file_offset,// [B]
|
||||
const int* __restrict__ sample_anchor, // [B]
|
||||
int seq_len,
|
||||
// SoA outputs for s_{t+1}:
|
||||
float* __restrict__ bid_px_soa,
|
||||
float* __restrict__ bid_sz_soa,
|
||||
float* __restrict__ ask_px_soa,
|
||||
float* __restrict__ ask_sz_soa,
|
||||
float* __restrict__ regime_soa,
|
||||
float* __restrict__ prev_mid_soa,
|
||||
float* __restrict__ tsv_soa,
|
||||
int* __restrict__ tc_soa,
|
||||
long long* __restrict__ ts_ns_soa,
|
||||
long long* __restrict__ prev_ts_ns_soa,
|
||||
int B
|
||||
) {
|
||||
int b = blockIdx.x;
|
||||
int k = threadIdx.x;
|
||||
if (b >= B || k >= seq_len) return;
|
||||
|
||||
// anchor + 1 for the next-step window.
|
||||
int global_idx = sample_file_offset[b] + sample_anchor[b] + 1 + k;
|
||||
const Mbp10Raw& snap = all_snapshots[global_idx];
|
||||
int n = b * seq_len + k;
|
||||
|
||||
int base_book = n * BOOK_LEVELS;
|
||||
int base_regime = n * REGIME_DIM;
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < BOOK_LEVELS; i++) {
|
||||
bid_px_soa[base_book + i] = snap.bid_px[i];
|
||||
bid_sz_soa[base_book + i] = snap.bid_sz[i];
|
||||
ask_px_soa[base_book + i] = snap.ask_px[i];
|
||||
ask_sz_soa[base_book + i] = snap.ask_sz[i];
|
||||
}
|
||||
#pragma unroll
|
||||
for (int i = 0; i < REGIME_DIM; i++) {
|
||||
regime_soa[base_regime + i] = snap.regime[i];
|
||||
}
|
||||
prev_mid_soa[n] = snap.prev_mid;
|
||||
tsv_soa[n] = snap.trade_signed_vol;
|
||||
tc_soa[n] = (int)snap.trade_count;
|
||||
ts_ns_soa[n] = (long long)snap.ts_ns;
|
||||
prev_ts_ns_soa[n] = (long long)snap.prev_ts_ns;
|
||||
}
|
||||
|
||||
// ── Re-gather s_t: current window at anchor+0 ──────────────────────
|
||||
//
|
||||
// After gpu_sample_and_gather samples an anchor and gpu_gather_next
|
||||
// overwrites the SoA with s_{t+1}, this kernel re-populates the SoA
|
||||
// with the original s_t window. Uses the saved (file_offset, anchor)
|
||||
// from gpu_sample_and_gather — no PRNG re-sampling. Identical to
|
||||
// gpu_gather_next except the global_idx offset is +0 instead of +1.
|
||||
//
|
||||
// Grid = (B, 1, 1)
|
||||
// Block = (K, 1, 1)
|
||||
|
||||
extern "C" __global__ void gpu_gather_current(
|
||||
const Mbp10Raw* __restrict__ all_snapshots,
|
||||
const int* __restrict__ sample_file_offset,
|
||||
const int* __restrict__ sample_anchor,
|
||||
int seq_len,
|
||||
float* __restrict__ bid_px_soa,
|
||||
float* __restrict__ bid_sz_soa,
|
||||
float* __restrict__ ask_px_soa,
|
||||
float* __restrict__ ask_sz_soa,
|
||||
float* __restrict__ regime_soa,
|
||||
float* __restrict__ prev_mid_soa,
|
||||
float* __restrict__ tsv_soa,
|
||||
int* __restrict__ tc_soa,
|
||||
long long* __restrict__ ts_ns_soa,
|
||||
long long* __restrict__ prev_ts_ns_soa,
|
||||
int B
|
||||
) {
|
||||
int b = blockIdx.x;
|
||||
int k = threadIdx.x;
|
||||
if (b >= B || k >= seq_len) return;
|
||||
|
||||
int global_idx = sample_file_offset[b] + sample_anchor[b] + k;
|
||||
const Mbp10Raw& snap = all_snapshots[global_idx];
|
||||
int n = b * seq_len + k;
|
||||
|
||||
int base_book = n * BOOK_LEVELS;
|
||||
int base_regime = n * REGIME_DIM;
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < BOOK_LEVELS; i++) {
|
||||
bid_px_soa[base_book + i] = snap.bid_px[i];
|
||||
bid_sz_soa[base_book + i] = snap.bid_sz[i];
|
||||
ask_px_soa[base_book + i] = snap.ask_px[i];
|
||||
ask_sz_soa[base_book + i] = snap.ask_sz[i];
|
||||
}
|
||||
#pragma unroll
|
||||
for (int i = 0; i < REGIME_DIM; i++) {
|
||||
regime_soa[base_regime + i] = snap.regime[i];
|
||||
}
|
||||
prev_mid_soa[n] = snap.prev_mid;
|
||||
tsv_soa[n] = snap.trade_signed_vol;
|
||||
tc_soa[n] = (int)snap.trade_count;
|
||||
ts_ns_soa[n] = (long long)snap.ts_ns;
|
||||
prev_ts_ns_soa[n] = (long long)snap.prev_ts_ns;
|
||||
}
|
||||
|
||||
// ── BCE / aux label gather kernel ───────────────────────────────────
|
||||
//
|
||||
// Gathers per-horizon float labels at each of the K window positions
|
||||
// (anchor + k, for k in [0, K)) into the output buffer with the
|
||||
// same [K, B, N_H] row-major layout that step_batched's mapped-pinned
|
||||
// staging uses. This unified kernel serves all 6 per-horizon float
|
||||
// label arrays (labels, outcome_prof_{long,short}, outcome_size_{long,
|
||||
// short}, sigma_k) — the caller selects which by passing the
|
||||
// appropriate `all_labels` source pointer from GpuDataset.
|
||||
//
|
||||
// Grid = (B, 1, 1)
|
||||
// Block = (K, 1, 1) where K = seq_len
|
||||
//
|
||||
// The output layout is:
|
||||
// out[k * B * n_horizons + b * n_horizons + h] = all_labels[h * total_snaps + global_idx]
|
||||
// matching the staging fill loop in perception.rs step_batched.
|
||||
|
||||
extern "C" __global__ void gpu_gather_bce_labels(
|
||||
const float* __restrict__ all_labels, // [n_horizons, total_snaps] row-major
|
||||
const int* __restrict__ sample_file_offset, // [B]
|
||||
const int* __restrict__ sample_anchor, // [B]
|
||||
int seq_len,
|
||||
int n_horizons,
|
||||
int total_snaps,
|
||||
float* __restrict__ labels_out, // [K, B, n_horizons] row-major
|
||||
int B
|
||||
) {
|
||||
int b = blockIdx.x;
|
||||
int k = threadIdx.x;
|
||||
if (b >= B || k >= seq_len) return;
|
||||
|
||||
int global_idx = sample_file_offset[b] + sample_anchor[b] + k;
|
||||
int out_base = (k * B + b) * n_horizons;
|
||||
|
||||
for (int h = 0; h < n_horizons; h++) {
|
||||
labels_out[out_base + h] = all_labels[h * total_snaps + global_idx];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-file pos_fraction gather kernel ─────────────────────────────
|
||||
//
|
||||
// Reads the per-file positive-class fraction for batch element 0's
|
||||
// sampled file and writes a single pos_fraction vector. The trainer
|
||||
// uses per-file pos_fraction as a coarse class-balance signal; all
|
||||
// batch elements within a step use the same pw value (batch 0's file).
|
||||
//
|
||||
// Grid = (1, 1, 1)
|
||||
// Block = (2 * N_HORIZONS, 1, 1) — one thread per pos_fraction slot
|
||||
|
||||
extern "C" __global__ void gpu_gather_pos_fraction(
|
||||
const float* __restrict__ all_pos_fraction, // [n_files, 2 * n_horizons]
|
||||
const int* __restrict__ sample_file_idx, // [B]
|
||||
int n_horizons,
|
||||
float* __restrict__ pos_fraction_out, // [2 * n_horizons]
|
||||
int B
|
||||
) {
|
||||
int slot = threadIdx.x;
|
||||
int n_slots = 2 * n_horizons;
|
||||
if (slot >= n_slots) return;
|
||||
|
||||
// Use batch 0's file index as the representative.
|
||||
int file_idx = sample_file_idx[0];
|
||||
pos_fraction_out[slot] = all_pos_fraction[file_idx * n_slots + slot];
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// grad_h_accumulate.cu — element-wise scaled gradient accumulator
|
||||
// (Phase E.2 of the integrated RL trainer).
|
||||
//
|
||||
// Combines per-head encoder-input gradients into the shared encoder
|
||||
// grad slot via scaled +=:
|
||||
//
|
||||
// grad_h_encoder[i] += lambda × grad_h_head[i]
|
||||
//
|
||||
// Used by IntegratedTrainer to fold the Q / π / V heads'
|
||||
// `grad_h_t [B × HIDDEN_DIM]` outputs into the encoder backward
|
||||
// input, on top of the BCE + aux contribution already deposited by
|
||||
// the PerceptionTrainer's own grad chain.
|
||||
//
|
||||
// One thread per element. Each thread is the sole writer of its
|
||||
// destination slot (within a single launch) — no atomicAdd per
|
||||
// `feedback_no_atomicadd.md`. Multiple launches in sequence on the
|
||||
// same destination are safe because each launch reads-modifies-writes
|
||||
// the same slot serially via stream ordering.
|
||||
//
|
||||
// Constraints honoured:
|
||||
// * `feedback_no_atomicadd.md` — sole-writer per (i)
|
||||
// * `feedback_no_nvrtc.md` — pre-compiled cubin via build.rs
|
||||
// * `pearl_no_host_branches_in_captured_graph` — `n` is a captured
|
||||
// scalar; `lambda` is a captured scalar (the trainer reads it from
|
||||
// the host ISV mirror BEFORE the capture region).
|
||||
//
|
||||
// Launch: grid=(ceil(n/256), 1, 1); block=(256, 1, 1); shared=0.
|
||||
|
||||
extern "C" __global__ void grad_h_accumulate_scaled(
|
||||
const float* __restrict__ grad_h_head, // [n] — input head gradient
|
||||
float lambda, // scalar weight (host-passed)
|
||||
int n, // total elements
|
||||
float* __restrict__ grad_h_encoder // [n] — encoder grad accumulator (additive)
|
||||
) {
|
||||
const int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= n) return;
|
||||
grad_h_encoder[i] += lambda * grad_h_head[i];
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// grad_norm.cu — GPU-resident grad-norm + clip-scale computation.
|
||||
//
|
||||
// Replaces the host-side memcpy_dtoh + sum-of-squares loop in
|
||||
// `Mamba2AdamW::step_from_buffers`. The previous path did 9× dtoh per
|
||||
// training step — each a stream sync barrier costing ~1ms+ on the hot
|
||||
// path. This kernel suite keeps the entire computation on GPU.
|
||||
//
|
||||
// Workflow (per training step):
|
||||
// 1. Zero `grad_norm_sq_d` once via `memset_zeros`.
|
||||
// 2. For each of N gradient tensors:
|
||||
// - launch `grad_norm_sq_phase1` → writes block-tree partials to scratch
|
||||
// - launch `grad_norm_sq_phase2` → reduces partials, ACCUMULATES into grad_norm_sq_d
|
||||
// 3. launch `grad_clip_scale` → reads grad_norm_sq_d, writes
|
||||
// `grad_scale_d = min(1, max_norm / sqrt(norm_sq))`
|
||||
// 4. AdamW kernels read `grad_scale_d` as a device pointer (not host scalar).
|
||||
//
|
||||
// All per-block reductions use shared-memory tree reduce per
|
||||
// `feedback_no_atomicadd.md`. Phase 2 uses a single-thread `+=` for
|
||||
// the cross-tensor accumulation, which is race-free because phase-2
|
||||
// launches are sequential in the stream and each launch writes exactly
|
||||
// one float to the accumulator slot.
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#define GRAD_NORM_BLOCK 256
|
||||
|
||||
/// Phase 1: per-block tree-reduce of `x[]*x[]` → block_partials[blockIdx.x].
|
||||
extern "C" __global__ void grad_norm_sq_phase1(
|
||||
const float* __restrict__ x,
|
||||
int n,
|
||||
float* __restrict__ block_partials
|
||||
) {
|
||||
__shared__ float smem[GRAD_NORM_BLOCK];
|
||||
int tid = threadIdx.x;
|
||||
int gid = blockIdx.x * blockDim.x + tid;
|
||||
smem[tid] = (gid < n) ? x[gid] * x[gid] : 0.0f;
|
||||
__syncthreads();
|
||||
for (int s = blockDim.x / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) smem[tid] += smem[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) block_partials[blockIdx.x] = smem[0];
|
||||
}
|
||||
|
||||
/// Phase 2: single-block tree-reduce of `block_partials[0..n_blocks]`
|
||||
/// → accumulates into `accum[0]`. `accumulate=1` adds; `accumulate=0`
|
||||
/// overwrites (use for the first call in a step).
|
||||
extern "C" __global__ void grad_norm_sq_phase2(
|
||||
const float* __restrict__ block_partials,
|
||||
int n_blocks,
|
||||
float* __restrict__ accum,
|
||||
int accumulate
|
||||
) {
|
||||
__shared__ float smem[GRAD_NORM_BLOCK];
|
||||
int tid = threadIdx.x;
|
||||
float v = 0.0f;
|
||||
for (int i = tid; i < n_blocks; i += blockDim.x) {
|
||||
v += block_partials[i];
|
||||
}
|
||||
smem[tid] = v;
|
||||
__syncthreads();
|
||||
for (int s = blockDim.x / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) smem[tid] += smem[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) {
|
||||
float prev = (accumulate != 0) ? accum[0] : 0.0f;
|
||||
accum[0] = prev + smem[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// Final clamp: grad_scale = min(1, max_norm / sqrt(norm_sq + eps)).
|
||||
/// One-thread kernel — writes a single float to `grad_scale_out[0]`.
|
||||
/// AdamW kernels read this device pointer instead of a host scalar.
|
||||
extern "C" __global__ void grad_clip_scale(
|
||||
const float* __restrict__ norm_sq,
|
||||
float max_norm,
|
||||
float* __restrict__ grad_scale_out
|
||||
) {
|
||||
if (threadIdx.x != 0 || blockIdx.x != 0) return;
|
||||
const float eps = 1e-12f;
|
||||
float norm = sqrtf(norm_sq[0] + eps);
|
||||
grad_scale_out[0] = (norm > max_norm) ? (max_norm / norm) : 1.0f;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// heads_block_diagonal_fwd.cu — heads forward with compact ragged w_skip.
|
||||
//
|
||||
// Per spec §5.4 point 2 (docs/superpowers/specs/2026-05-21-per-horizon-cfc-inference-design.md):
|
||||
// heads_w_skip storage is compacted from [N_HORIZONS × HIDDEN_DIM] = 384
|
||||
// floats with off-bucket zeros to a ragged buffer of size HIDDEN_DIM = 128
|
||||
// floats. Each horizon head reads ONLY its bucket's contiguous slice via
|
||||
// heads_w_skip_offset[N_HORIZONS+1].
|
||||
//
|
||||
// Layout (for HIDDEN_DIM=128, BUCKET_DIM_K=[43,43,42]):
|
||||
// w_skip_compact: [43 (head 0) | 43 (head 1) | 42 (head 2)]
|
||||
// heads_w_skip_offset: [0, 43, 86, 128]
|
||||
//
|
||||
// Skip projection for horizon h, batch b:
|
||||
// skip_logit[b, h] = b_skip[h]
|
||||
// + Σ_{c=0..bucket_dim_k[h]} h_state[b, bucket_channel_offset[h] + c]
|
||||
// * w_skip_compact[heads_w_skip_offset[h] + c]
|
||||
//
|
||||
// Launch: grid = (B, N_HORIZONS, 1), block = (MAX_BUCKET_DIM = 96, 1, 1).
|
||||
// Uniform predicate (threadIdx.x < bucket_dim) handles uneven bucket sizes
|
||||
// without warp divergence — the comparison is against a per-block constant,
|
||||
// so all threads in a warp take the same branch.
|
||||
//
|
||||
// Per feedback_no_atomicadd.md: block-tree reduction with warp-shuffle-style
|
||||
// shared-memory pairwise pattern; never atomicAdd.
|
||||
// Per pearl_no_host_branches_in_captured_graph.md: no host branching; all
|
||||
// control flow uses device-resident metadata.
|
||||
// Per feedback_nvidia_grade_perf_for_kernels.md: warp-uniform branches,
|
||||
// block-tree reduction, no atomicAdd.
|
||||
|
||||
#define HIDDEN_DIM 128
|
||||
#define N_HORIZONS 3
|
||||
#define MAX_BUCKET_DIM 96
|
||||
// Power-of-two padding for the block-tree reduction. Lanes
|
||||
// [MAX_BUCKET_DIM .. REDUCE_PAD) are zero-initialized in shared memory so
|
||||
// the halving-stride reduction is correct for the non-power-of-two
|
||||
// MAX_BUCKET_DIM=96 case. Next power of two above 96 is 128.
|
||||
#define REDUCE_PAD 128
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// heads_block_diagonal_fwd: compact ragged w_skip projection.
|
||||
//
|
||||
// Launch:
|
||||
// grid = (B, N_HORIZONS, 1)
|
||||
// block = (MAX_BUCKET_DIM = 96, 1, 1)
|
||||
// shared_mem_bytes = 0 (uses statically-sized __shared__ buffer)
|
||||
//
|
||||
// Output:
|
||||
// skip_logit[batch, horizon] = b_skip[horizon] + Σ_c h_state[batch, bucket_start+c] *
|
||||
// w_skip_compact[w_start+c]
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void heads_block_diagonal_fwd(
|
||||
// Compact ragged w_skip:
|
||||
const float* __restrict__ w_skip_compact, // [HIDDEN_DIM = sum(bucket_dim_k)]
|
||||
const unsigned int* __restrict__ heads_w_skip_offset, // [N_HORIZONS + 1]
|
||||
const unsigned int* __restrict__ bucket_channel_offset, // [N_HORIZONS + 1]
|
||||
const unsigned int* __restrict__ bucket_dim_k, // [N_HORIZONS]
|
||||
const float* __restrict__ b_skip, // [N_HORIZONS]
|
||||
// Inputs
|
||||
const float* __restrict__ h_state, // [B × HIDDEN_DIM]
|
||||
int B,
|
||||
// Outputs
|
||||
float* __restrict__ skip_logit // [B × N_HORIZONS]
|
||||
) {
|
||||
int batch = blockIdx.x;
|
||||
int horizon = blockIdx.y;
|
||||
int tid = threadIdx.x;
|
||||
|
||||
if (batch >= B) return;
|
||||
|
||||
unsigned int bucket_dim = bucket_dim_k[horizon];
|
||||
unsigned int bucket_start = bucket_channel_offset[horizon];
|
||||
unsigned int w_start = heads_w_skip_offset[horizon];
|
||||
|
||||
// Shared accumulator sized to REDUCE_PAD (128, next power of two ≥
|
||||
// MAX_BUCKET_DIM=96). Lanes [bucket_dim .. REDUCE_PAD) hold 0.0 so the
|
||||
// standard power-of-two halving-stride reduction is correct.
|
||||
//
|
||||
// Only blockDim.x = MAX_BUCKET_DIM = 96 threads exist; lanes
|
||||
// 96..127 are written by tid<32 (those threads write BOTH their own slot
|
||||
// and the padding slot at tid+MAX_BUCKET_DIM).
|
||||
__shared__ float sdata[REDUCE_PAD];
|
||||
sdata[tid] = 0.0f;
|
||||
if (tid + MAX_BUCKET_DIM < REDUCE_PAD) {
|
||||
sdata[tid + MAX_BUCKET_DIM] = 0.0f;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if ((unsigned int)tid < bucket_dim) {
|
||||
unsigned int c = bucket_start + (unsigned int)tid;
|
||||
unsigned int w_idx = w_start + (unsigned int)tid;
|
||||
// Defensive: c < HIDDEN_DIM by construction (bucket_channel_offset[N_HORIZONS] == HIDDEN_DIM).
|
||||
sdata[tid] = h_state[batch * HIDDEN_DIM + c] * w_skip_compact[w_idx];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Block-tree reduction over REDUCE_PAD=128 padded lanes (no atomicAdd per
|
||||
// feedback_no_atomicadd.md). Power-of-two halving stride is clean since
|
||||
// padding lanes are zero-initialized.
|
||||
for (int s = REDUCE_PAD / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) {
|
||||
sdata[tid] += sdata[tid + s];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
skip_logit[batch * N_HORIZONS + horizon] = sdata[0] + b_skip[horizon];
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// horizon_lambda.cu — ISV-driven per-horizon gradient scaler AND closed-form Kendall σ.
|
||||
//
|
||||
// Single source of truth for the two per-horizon controllers:
|
||||
//
|
||||
// 1) lambda[h] ∈ [LAMBDA_FLOOR, LAMBDA_CEILING] — multiplier on the
|
||||
// trunk grad_h contribution in heads_grn_bwd. Boosts horizons
|
||||
// that the model is currently failing to learn (per their
|
||||
// unweighted BCE EMA).
|
||||
//
|
||||
// 2) log_sigma_h[h] — Kendall σ for the BCE forward kernel. Computed
|
||||
// in closed form from the same loss_ema signal (Kendall's
|
||||
// gradient equilibrium ⟹ σ_h = sqrt(mean_bce_h)). Replaces
|
||||
// Adam-learned σ, eliminating the m/√v normalization conflict
|
||||
// (pearl_adam_normalizes_loss_weights).
|
||||
//
|
||||
// ISV anchors per `pearl_controller_anchors_isv_driven`: every anchor/
|
||||
// target/cap derives from a tracked signal, never from hardcoded
|
||||
// constants outside the bootstrap epsilons:
|
||||
//
|
||||
// loss_ema[h] — EMA(unweighted BCE per horizon)
|
||||
// z_max_ema — EMA(max |z_h|) across horizons → drives
|
||||
// adaptive Z_SCALE so the OBSERVED-max-z
|
||||
// horizon maps exactly to LAMBDA_CEILING.
|
||||
// With the prior hardcoded Z_SCALE=0.5 the
|
||||
// controller rarely engaged on real data
|
||||
// (max observed lambda ~1.04); adaptive
|
||||
// Z_SCALE uses the full envelope.
|
||||
//
|
||||
// Sentinel bootstrap (`prev <= 0`) per
|
||||
// `pearl_first_observation_bootstrap.md`; permanent floor
|
||||
// (`max(real, floor)`) per `pearl_blend_formulas_must_have_permanent_floor.md`;
|
||||
// asymmetric clamp (floor-only on σ, floor + ceiling on λ) per
|
||||
// `pearl_audit_unboundedness_for_implicit_asymmetry.md`. Z-score
|
||||
// normalization per `pearl_zscore_normalization_for_magnitude_asymmetric_signals.md`.
|
||||
|
||||
#define N_HORIZONS_LAMBDA 3
|
||||
#define ALPHA_FIXED 0.1f
|
||||
#define LAMBDA_FLOOR 1.0f
|
||||
#define LAMBDA_CEILING 2.0f
|
||||
#define LOG_SIGMA_FLOOR (-0.6931472f) // log(0.5) — σ never collapses below 0.5
|
||||
#define Z_MAX_FLOOR 0.1f // guards Z_SCALE_ISV when z_max_ema is tiny
|
||||
|
||||
extern "C" __global__ void horizon_ema_and_lambda(
|
||||
const float* __restrict__ loss_per_horizon, // [3] — current step UNWEIGHTED BCE
|
||||
float* __restrict__ loss_ema, // [3] — EMA state (read + write)
|
||||
float* __restrict__ z_max_ema, // [1] — EMA(max|z|) state (read + write)
|
||||
float* __restrict__ lambda, // [3] — output: λ_h
|
||||
float* __restrict__ log_sigma_h // [3] — output: closed-form Kendall σ
|
||||
) {
|
||||
if (threadIdx.x != 0 || blockIdx.x != 0) return;
|
||||
|
||||
// -------- 1) EMA update on loss_per_horizon, with first-obs bootstrap.
|
||||
float new_ema[N_HORIZONS_LAMBDA];
|
||||
float sum = 0.0f;
|
||||
#pragma unroll
|
||||
for (int h = 0; h < N_HORIZONS_LAMBDA; ++h) {
|
||||
const float cur = loss_per_horizon[h];
|
||||
const float prev = loss_ema[h];
|
||||
new_ema[h] = (prev <= 0.0f) ? cur : (prev + ALPHA_FIXED * (cur - prev));
|
||||
loss_ema[h] = new_ema[h];
|
||||
sum += new_ema[h];
|
||||
}
|
||||
const float mean = sum / (float)N_HORIZONS_LAMBDA;
|
||||
|
||||
// -------- 2) Z-score normalize across horizons.
|
||||
float ssq = 0.0f;
|
||||
#pragma unroll
|
||||
for (int h = 0; h < N_HORIZONS_LAMBDA; ++h) {
|
||||
const float diff = new_ema[h] - mean;
|
||||
ssq += diff * diff;
|
||||
}
|
||||
const float std = sqrtf(ssq / (float)N_HORIZONS_LAMBDA + 1e-12f);
|
||||
const float inv_std = (std > 1e-6f) ? (1.0f / std) : 0.0f;
|
||||
|
||||
float z[N_HORIZONS_LAMBDA];
|
||||
float z_max = 0.0f;
|
||||
#pragma unroll
|
||||
for (int h = 0; h < N_HORIZONS_LAMBDA; ++h) {
|
||||
z[h] = (new_ema[h] - mean) * inv_std;
|
||||
const float az = fabsf(z[h]);
|
||||
if (az > z_max) z_max = az;
|
||||
}
|
||||
|
||||
// -------- 3) z_max_ema: tracks the typical spread, drives adaptive Z_SCALE.
|
||||
// Sentinel = 0 ⇒ first-obs bootstrap. Fixed α matches loss_ema's
|
||||
// smoothing horizon.
|
||||
const float prev_zmax = z_max_ema[0];
|
||||
const float new_zmax = (prev_zmax <= 0.0f)
|
||||
? z_max
|
||||
: (prev_zmax + ALPHA_FIXED * (z_max - prev_zmax));
|
||||
z_max_ema[0] = new_zmax;
|
||||
|
||||
// -------- 4) Adaptive Z_SCALE: maps the historical-max-z to LAMBDA_CEILING.
|
||||
// When z_max_ema is large (spread regime), Z_SCALE_ISV shrinks
|
||||
// to avoid saturating early; when small (tight regime), it
|
||||
// grows to amplify the controller into engagement.
|
||||
const float z_anchor = fmaxf(new_zmax, Z_MAX_FLOOR);
|
||||
const float z_scale_isv = (LAMBDA_CEILING - LAMBDA_FLOOR) / z_anchor;
|
||||
|
||||
// -------- 5) Per-horizon outputs.
|
||||
#pragma unroll
|
||||
for (int h = 0; h < N_HORIZONS_LAMBDA; ++h) {
|
||||
// λ_h: boost-only, asymmetric clamped.
|
||||
const float raw_lambda = 1.0f + z_scale_isv * z[h];
|
||||
lambda[h] = fmaxf(LAMBDA_FLOOR, fminf(LAMBDA_CEILING, raw_lambda));
|
||||
|
||||
// log σ_h: Kendall equilibrium from the same loss_ema signal.
|
||||
// ∂L/∂log σ = 0 ⟹ σ_h² = mean_bce_h ⟹ log σ_h = ½·log(loss_ema_h).
|
||||
// Floor at log(0.5) so σ can't collapse to 0 (which would explode w_h).
|
||||
const float ema_h = fmaxf(new_ema[h], 1e-12f);
|
||||
const float raw_log_sigma = 0.5f * logf(ema_h);
|
||||
log_sigma_h[h] = fmaxf(LOG_SIGMA_FLOOR, raw_log_sigma);
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
// layer_norm.cu — per-row LayerNorm over a [N_rows, HIDDEN] tensor.
|
||||
//
|
||||
// y[r, j] = gain[j] * (x[r, j] - mean_r) / sqrt(var_r + eps) + bias[j]
|
||||
//
|
||||
// Forward saves `mean_r` and `inv_std_r = 1 / sqrt(var_r + eps)` to a
|
||||
// scratch buffer of shape [N_rows, 2] so the backward kernel doesn't
|
||||
// re-derive them. One block per row; block tree-reduce for mean + var.
|
||||
// Per `feedback_no_atomicadd.md`: shared-mem tree-reduce only, no atomics.
|
||||
|
||||
#define LN_HIDDEN 128
|
||||
#define LN_BLOCK 128
|
||||
#define LN_EPS 1e-5f
|
||||
|
||||
extern "C" __global__ void layer_norm_fwd(
|
||||
const float* __restrict__ x, // [N_rows, HIDDEN]
|
||||
const float* __restrict__ gain, // [HIDDEN]
|
||||
const float* __restrict__ bias, // [HIDDEN]
|
||||
int n_rows,
|
||||
float* __restrict__ y, // [N_rows, HIDDEN]
|
||||
float* __restrict__ stats // [N_rows, 2] — col 0=mean, col 1=inv_std
|
||||
) {
|
||||
int r = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (r >= n_rows) return;
|
||||
|
||||
__shared__ float ssum[LN_BLOCK];
|
||||
__shared__ float ssq[LN_BLOCK];
|
||||
__shared__ float s_mean;
|
||||
__shared__ float s_inv_std;
|
||||
|
||||
const float* x_row = x + (long long)r * LN_HIDDEN;
|
||||
float my_sum = 0.0f, my_sq = 0.0f;
|
||||
if (tid < LN_HIDDEN) {
|
||||
float v = x_row[tid];
|
||||
my_sum = v;
|
||||
my_sq = v * v;
|
||||
}
|
||||
ssum[tid] = my_sum;
|
||||
ssq[tid] = my_sq;
|
||||
__syncthreads();
|
||||
|
||||
for (int s = LN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) {
|
||||
ssum[tid] += ssum[tid + s];
|
||||
ssq[tid] += ssq[tid + s];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) {
|
||||
const float n = (float)LN_HIDDEN;
|
||||
const float mean = ssum[0] / n;
|
||||
const float var = ssq[0] / n - mean * mean;
|
||||
s_mean = mean;
|
||||
s_inv_std = rsqrtf(var + LN_EPS);
|
||||
stats[(long long)r * 2 + 0] = mean;
|
||||
stats[(long long)r * 2 + 1] = s_inv_std;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (tid < LN_HIDDEN) {
|
||||
float normalised = (x_row[tid] - s_mean) * s_inv_std;
|
||||
y[(long long)r * LN_HIDDEN + tid] = gain[tid] * normalised + bias[tid];
|
||||
}
|
||||
}
|
||||
|
||||
// Backward: given grad_y[N, HIDDEN] and saved mean+inv_std per row,
|
||||
// produce grad_x[N, HIDDEN] and per-row grad_gain / grad_bias
|
||||
// scratch tensors. A separate reducer (`layer_norm_reduce_param_grads`)
|
||||
// sums the per-row scratch into the canonical [HIDDEN] grad_gain /
|
||||
// grad_bias buffers.
|
||||
//
|
||||
// Per-row writes from this kernel are race-free because each block
|
||||
// owns one row; the cross-row sum needs the reducer (no atomicAdd
|
||||
// per `feedback_no_atomicadd.md`).
|
||||
//
|
||||
// grad_x[r, j] = inv_std * (gain[j] * grad_y[r, j]
|
||||
// - mean(gain[j'] * grad_y[r, j'])
|
||||
// - normalised[r, j] * mean(gain[j'] * grad_y[r, j'] * normalised[r, j']))
|
||||
// grad_gain_per_row[r, j] = grad_y[r, j] * normalised[r, j]
|
||||
// grad_bias_per_row[r, j] = grad_y[r, j]
|
||||
|
||||
extern "C" __global__ void layer_norm_bwd(
|
||||
const float* __restrict__ x, // [N_rows, HIDDEN]
|
||||
const float* __restrict__ gain, // [HIDDEN]
|
||||
const float* __restrict__ stats, // [N_rows, 2] from fwd
|
||||
const float* __restrict__ grad_y, // [N_rows, HIDDEN]
|
||||
int n_rows,
|
||||
float* __restrict__ grad_x, // [N_rows, HIDDEN] (overwrite)
|
||||
float* __restrict__ grad_gain_per_row, // [N_rows, HIDDEN] (overwrite)
|
||||
float* __restrict__ grad_bias_per_row // [N_rows, HIDDEN] (overwrite)
|
||||
) {
|
||||
int r = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (r >= n_rows) return;
|
||||
|
||||
__shared__ float s_a[LN_BLOCK]; // sum of gain[j] * grad_y[r, j]
|
||||
__shared__ float s_b[LN_BLOCK]; // sum of gain[j] * grad_y[r, j] * normalised[r, j]
|
||||
__shared__ float s_mean_a;
|
||||
__shared__ float s_mean_b;
|
||||
|
||||
const float mean = stats[(long long)r * 2 + 0];
|
||||
const float inv_std = stats[(long long)r * 2 + 1];
|
||||
const float gy = (tid < LN_HIDDEN) ? grad_y[(long long)r * LN_HIDDEN + tid] : 0.0f;
|
||||
const float xv = (tid < LN_HIDDEN) ? x[(long long)r * LN_HIDDEN + tid] : 0.0f;
|
||||
const float gn = (tid < LN_HIDDEN) ? gain[tid] : 0.0f;
|
||||
const float normalised = (xv - mean) * inv_std;
|
||||
|
||||
s_a[tid] = gn * gy;
|
||||
s_b[tid] = gn * gy * normalised;
|
||||
__syncthreads();
|
||||
for (int s = LN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) {
|
||||
s_a[tid] += s_a[tid + s];
|
||||
s_b[tid] += s_b[tid + s];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) {
|
||||
const float n = (float)LN_HIDDEN;
|
||||
s_mean_a = s_a[0] / n;
|
||||
s_mean_b = s_b[0] / n;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (tid < LN_HIDDEN) {
|
||||
grad_x[(long long)r * LN_HIDDEN + tid] =
|
||||
inv_std * (gn * gy - s_mean_a - normalised * s_mean_b);
|
||||
grad_gain_per_row[(long long)r * LN_HIDDEN + tid] = gy * normalised;
|
||||
grad_bias_per_row[(long long)r * LN_HIDDEN + tid] = gy;
|
||||
}
|
||||
}
|
||||
|
||||
// Reducer: sum [N_rows, HIDDEN] per-row tensor along axis 0 into
|
||||
// [HIDDEN]. One block per output column; block tree-reduce across rows
|
||||
// (no atomicAdd). Stride-loop within each block so we handle any
|
||||
// N_rows up to ~64K (limited only by block-thread count of LN_BLOCK).
|
||||
extern "C" __global__ void layer_norm_reduce_param_grads(
|
||||
const float* __restrict__ per_row, // [N_rows, HIDDEN]
|
||||
int n_rows,
|
||||
float* __restrict__ out // [HIDDEN] (overwrite, NOT +=)
|
||||
) {
|
||||
int j = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (j >= LN_HIDDEN) return;
|
||||
|
||||
__shared__ float ssum[LN_BLOCK];
|
||||
float my_sum = 0.0f;
|
||||
for (int r = tid; r < n_rows; r += LN_BLOCK) {
|
||||
my_sum += per_row[(long long)r * LN_HIDDEN + j];
|
||||
}
|
||||
ssum[tid] = my_sum;
|
||||
__syncthreads();
|
||||
for (int s = LN_BLOCK / 2; s > 0; s >>= 1) {
|
||||
if (tid < s) ssum[tid] += ssum[tid + s];
|
||||
__syncthreads();
|
||||
}
|
||||
if (tid == 0) {
|
||||
out[j] = ssum[0];
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// log_pi_at_action.cu — GPU-resident per-batch log π(action | s)
|
||||
// computation for the PPO importance-ratio path (Phase R4 of the
|
||||
// integrated RL trainer rebuild;
|
||||
// see docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
|
||||
//
|
||||
// Replaces the host-side `pi_logits[action] − log_sum_exp(pi_logits)`
|
||||
// loop the flawed Phase F shipped in step_with_lobsim — which
|
||||
// violated `feedback_cpu_is_read_only` by DtoH-copying π logits and
|
||||
// computing log-softmax on CPU.
|
||||
//
|
||||
// Per-batch: log_pi[b] = pi_logits[b, actions[b]] − log Σ_a exp(pi_logits[b, a]).
|
||||
// Numerically stabilised via max-shift before exp.
|
||||
//
|
||||
// One thread per batch entry; element-wise. No reduction across
|
||||
// batches, no atomics. N_ACTIONS = 9 fits in a per-thread sequential
|
||||
// loop comfortably.
|
||||
|
||||
#define N_ACTIONS 11
|
||||
|
||||
// Inputs:
|
||||
// pi_logits [b_size, N_ACTIONS] row-major
|
||||
// actions [b_size] index into [0, N_ACTIONS)
|
||||
// Outputs:
|
||||
// log_pi_out [b_size]
|
||||
extern "C" __global__ void log_pi_at_action(
|
||||
const float* __restrict__ pi_logits,
|
||||
const int* __restrict__ actions,
|
||||
float* __restrict__ log_pi_out,
|
||||
int b_size
|
||||
) {
|
||||
const int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= b_size) return;
|
||||
|
||||
const int row_off = b * N_ACTIONS;
|
||||
float max_l = -INFINITY;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < N_ACTIONS; ++i) {
|
||||
const float l = pi_logits[row_off + i];
|
||||
if (l > max_l) max_l = l;
|
||||
}
|
||||
float sum_exp = 0.0f;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < N_ACTIONS; ++i) {
|
||||
sum_exp += expf(pi_logits[row_off + i] - max_l);
|
||||
}
|
||||
// log-sum-exp identity: log Σ exp(x) = max + log Σ exp(x − max)
|
||||
const float log_norm = max_l + logf(sum_exp);
|
||||
|
||||
const int chosen = actions[b];
|
||||
// No bounds check on `chosen` per `feedback_no_quickfixes`: the
|
||||
// upstream Thompson kernel (rl_action_kernel) writes into
|
||||
// [0, N_ACTIONS) by construction. A bounds check here would mask
|
||||
// an upstream contract violation; let it crash visibly.
|
||||
log_pi_out[b] = pi_logits[row_off + chosen] - log_norm;
|
||||
}
|
||||
@@ -1,725 +0,0 @@
|
||||
/* =====================================================================
|
||||
* Mamba2 selective scan — ml-alpha (Phase 1d.1)
|
||||
*
|
||||
* Forward + analytical backward for the SSM scan used by the supervised
|
||||
* snapshot-stream pipeline. Purpose-built for the supervised path — no
|
||||
* ISV adaptive signals, no per-position temporal_weight, no NULL-pointer
|
||||
* branches.
|
||||
*
|
||||
* Layout conventions (row-major):
|
||||
* a_proj : [N, K, state_d] per-step state-update gates (pre-sigmoid)
|
||||
* b_proj : [N, K, state_d] per-step state-input contributions
|
||||
* w_c : [sh2, state_d] output mix weights (per-hidden-channel)
|
||||
* h_s2 : [N, sh2] residual input added to scan output
|
||||
* h_enriched : [N, sh2] OUTPUT of forward: h_s2 + (W_c · final_state)
|
||||
* d_h_enriched : [N, sh2] upstream gradient flowing into h_enriched
|
||||
*
|
||||
* Backward scratch (per-channel, atomicAdd-free):
|
||||
* d_a_per_channel : [N, sh2, K, state_d] each (i, j) thread writes its
|
||||
* unique slot; reduced across j by
|
||||
* `mamba2_alpha_reduce_d_proj`
|
||||
* d_b_per_channel : [N, sh2, K, state_d] symmetric
|
||||
* d_w_c_per_sample: [N, sh2, state_d] reduced across N by
|
||||
* `mamba2_alpha_reduce_d_w_c`
|
||||
*
|
||||
* Backward outputs (after reductions):
|
||||
* d_a_proj : [N, K, state_d] sum over j of d_a_per_channel
|
||||
* d_b_proj : [N, K, state_d] sum over j of d_b_per_channel
|
||||
* d_w_c : [sh2, state_d] sum over N of d_w_c_per_sample
|
||||
* d_h_s2 : [N, sh2] identity passthrough of d_h_enriched
|
||||
*
|
||||
* Kernel constraints (must hold at every launch):
|
||||
* state_d ≤ 32 (register array `float x[32]`)
|
||||
* K ≤ 96 (local-memory array `float x_hist[96 * 32]` = 12 KiB / thread —
|
||||
* spills to per-thread DRAM-backed local memory but L2-cached;
|
||||
* chosen so K ≥ longest practical horizon for tick-level
|
||||
* supervised heads while keeping the backward replay buffer
|
||||
* comfortably below the L2 working-set ceiling)
|
||||
*
|
||||
* Rust constructor enforces both via Mamba2BlockConfig::validate.
|
||||
* ===================================================================== */
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
// state_d capacity: raised from 16 → 32 (per-thread register array
|
||||
// `float x[32]` = 128 bytes; backward replay cache `x_hist[K*32]` up
|
||||
// to 12 KiB/thread of local memory at K=96). L40S/H100 register file
|
||||
// (256 KiB/SM) handles this without occupancy collapse for our block
|
||||
// dim (32-128 threads/block). 32-wide state empirically validated as
|
||||
// the next step up the capacity ladder from 16; further increases
|
||||
// will need a different storage scheme (smem-tiled state).
|
||||
#define MAMBA2_ALPHA_MAX_STATE_D 32
|
||||
#define MAMBA2_ALPHA_MAX_K 96
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Forward scan: one thread per (sample i, hidden-channel j).
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void mamba2_alpha_scan_fwd(
|
||||
const float* __restrict__ a_proj,
|
||||
const float* __restrict__ b_proj,
|
||||
const float* __restrict__ w_c,
|
||||
const float* __restrict__ h_s2,
|
||||
float* __restrict__ h_enriched,
|
||||
int N,
|
||||
int K,
|
||||
int sh2,
|
||||
int state_d
|
||||
) {
|
||||
int i = blockIdx.x;
|
||||
int j = blockIdx.y * blockDim.x + threadIdx.x;
|
||||
if (i >= N || j >= sh2) return;
|
||||
|
||||
float x[MAMBA2_ALPHA_MAX_STATE_D];
|
||||
#pragma unroll
|
||||
for (int s = 0; s < MAMBA2_ALPHA_MAX_STATE_D; s++) x[s] = 0.0f;
|
||||
|
||||
for (int t = 0; t < K; t++) {
|
||||
long long base = ((long long)i * K + t) * state_d;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
float gate = 1.0f / (1.0f + expf(-a_proj[base + s]));
|
||||
x[s] = gate * x[s] + b_proj[base + s];
|
||||
}
|
||||
}
|
||||
|
||||
float ctx = 0.0f;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
ctx += w_c[(long long)j * state_d + s] * x[s];
|
||||
}
|
||||
h_enriched[(long long)i * sh2 + j] = h_s2[(long long)i * sh2 + j] + ctx;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Backward scan: one thread per (sample i, hidden-channel j).
|
||||
*
|
||||
* Each thread writes to its UNIQUE slot in the per-channel scratch buffers
|
||||
* `d_a_per_channel[i, j, t, s]` and `d_b_per_channel[i, j, t, s]`. No
|
||||
* atomicAdd; cross-channel reduction is done by `mamba2_alpha_reduce_d_proj`
|
||||
* below. The `d_w_c_per_sample[i, j, s]` slot is also (i, j)-unique;
|
||||
* reduced across N by `mamba2_alpha_reduce_d_w_c`.
|
||||
*
|
||||
* `d_h_s2[i, j]` is the identity passthrough of `d_h_enriched[i, j]`.
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void mamba2_alpha_scan_bwd(
|
||||
const float* __restrict__ a_proj,
|
||||
const float* __restrict__ b_proj,
|
||||
const float* __restrict__ d_h_enriched,
|
||||
const float* __restrict__ w_c,
|
||||
float* __restrict__ d_a_per_channel, // [N, sh2, K, state_d]
|
||||
float* __restrict__ d_b_per_channel, // [N, sh2, K, state_d]
|
||||
float* __restrict__ d_w_c_per_sample, // [N, sh2, state_d]
|
||||
float* __restrict__ d_h_s2, // [N, sh2]
|
||||
int N,
|
||||
int K,
|
||||
int sh2,
|
||||
int state_d
|
||||
) {
|
||||
int i = blockIdx.x;
|
||||
int j = blockIdx.y * blockDim.x + threadIdx.x;
|
||||
if (i >= N || j >= sh2) return;
|
||||
|
||||
/* Replay the forward state path; cache x[t][s] for the reverse scan. */
|
||||
float x_hist[MAMBA2_ALPHA_MAX_K * MAMBA2_ALPHA_MAX_STATE_D];
|
||||
float x[MAMBA2_ALPHA_MAX_STATE_D];
|
||||
#pragma unroll
|
||||
for (int s = 0; s < MAMBA2_ALPHA_MAX_STATE_D; s++) x[s] = 0.0f;
|
||||
|
||||
for (int t = 0; t < K; t++) {
|
||||
long long base = ((long long)i * K + t) * state_d;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
float gate = 1.0f / (1.0f + expf(-a_proj[base + s]));
|
||||
x[s] = gate * x[s] + b_proj[base + s];
|
||||
x_hist[t * MAMBA2_ALPHA_MAX_STATE_D + s] = x[s];
|
||||
}
|
||||
}
|
||||
|
||||
/* Initial d_state at t = K-1: gradient flowing back through the dot
|
||||
* product `ctx[i,j] = sum_s w_c[j,s] * x[s]` is d_h_ij * w_c[j,s]. */
|
||||
float d_state[MAMBA2_ALPHA_MAX_STATE_D];
|
||||
float d_h_ij = d_h_enriched[(long long)i * sh2 + j];
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
d_state[s] = d_h_ij * w_c[(long long)j * state_d + s];
|
||||
}
|
||||
|
||||
/* W_c gradient contribution from this (i, j):
|
||||
* d_w_c[j, s] += d_h_ij * x_final[s]
|
||||
* Each (i, j) thread writes its UNIQUE slot in d_w_c_per_sample[i, j, s];
|
||||
* reduction across i happens in mamba2_alpha_reduce_d_w_c. */
|
||||
long long w_c_slot = ((long long)i * sh2 + j) * state_d;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
d_w_c_per_sample[w_c_slot + s] =
|
||||
d_h_ij * x_hist[(K - 1) * MAMBA2_ALPHA_MAX_STATE_D + s];
|
||||
}
|
||||
|
||||
/* Reverse scan. At each step t (going K-1 → 0):
|
||||
* d_b[t, s] = d_state[s] (b is added linearly)
|
||||
* d_a[t, s] = d_state[s] * x_prev[s] * σ'(a[t, s]) (a feeds the gate)
|
||||
* d_state[s] = d_state[s] * gate[t, s] (propagate to t-1)
|
||||
*
|
||||
* Per-channel scratch slot for (i, j, t, s):
|
||||
* d_a_per_channel[((i * sh2 + j) * K + t) * state_d + s]
|
||||
*/
|
||||
long long per_chan_base = ((long long)i * sh2 + j) * (long long)K * state_d;
|
||||
for (int t = K - 1; t >= 0; t--) {
|
||||
long long fwd_base = ((long long)i * K + t) * state_d;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
float a_raw = a_proj[fwd_base + s];
|
||||
float gate = 1.0f / (1.0f + expf(-a_raw));
|
||||
float sig_deriv = gate * (1.0f - gate);
|
||||
|
||||
float x_prev = (t == 0) ? 0.0f : x_hist[(t - 1) * MAMBA2_ALPHA_MAX_STATE_D + s];
|
||||
|
||||
long long slot = per_chan_base + (long long)t * state_d + s;
|
||||
d_b_per_channel[slot] = d_state[s];
|
||||
d_a_per_channel[slot] = d_state[s] * x_prev * sig_deriv;
|
||||
|
||||
/* Propagate to t-1. */
|
||||
d_state[s] = d_state[s] * gate;
|
||||
}
|
||||
}
|
||||
|
||||
/* d_h_s2 is identity passthrough (h_s2 added linearly to h_enriched). */
|
||||
d_h_s2[(long long)i * sh2 + j] = d_h_ij;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Reduction kernel: sum d_a_per_channel[N, sh2, K, state_d] across j →
|
||||
* d_a_proj[N, K, state_d].
|
||||
*
|
||||
* Same kernel handles d_b reduction (call twice with different
|
||||
* input/output pointers). Avoids code duplication.
|
||||
*
|
||||
* Grid: (N, K, ceil(state_d / 32)), Block: 32
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void mamba2_alpha_reduce_d_proj(
|
||||
const float* __restrict__ d_per_channel, // [N, sh2, K, state_d]
|
||||
float* __restrict__ d_proj, // [N, K, state_d]
|
||||
int N,
|
||||
int K,
|
||||
int sh2,
|
||||
int state_d
|
||||
) {
|
||||
int i = blockIdx.x;
|
||||
int t = blockIdx.y;
|
||||
int s = blockIdx.z * blockDim.x + threadIdx.x;
|
||||
if (i >= N || t >= K || s >= state_d) return;
|
||||
|
||||
/* Sum over j: d_proj[i, t, s] = sum_j d_per_channel[i, j, t, s] */
|
||||
float sum = 0.0f;
|
||||
for (int j = 0; j < sh2; j++) {
|
||||
long long slot = (((long long)i * sh2 + j) * K + t) * state_d + s;
|
||||
sum += d_per_channel[slot];
|
||||
}
|
||||
d_proj[((long long)i * K + t) * state_d + s] = sum;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Per-step variants — supervise the SSM at EVERY timestep, not just the
|
||||
* final position. Used by ml-alpha PerceptionTrainer.
|
||||
*
|
||||
* Forward writes per-step h_enriched_seq[N, K, sh2]; backward accepts
|
||||
* d_h_enriched_seq[N, K, sh2] and accumulates gradient injections at
|
||||
* every step before propagating the recurrent d_state through the
|
||||
* gate chain. Per-step semantics:
|
||||
*
|
||||
* h_enriched_seq[i, t, j] = h_s2[i, j] + sum_s w_c[j, s] * x[i, t, s]
|
||||
*
|
||||
* d_h_s2[i, j] = sum_t d_h_enriched_seq[i, t, j] (residual added every step)
|
||||
* d_w_c[j, s] = sum_i sum_t d_h_enriched_seq[i, t, j] * x[i, t, s]
|
||||
* d_state[s] at t = (carried d_state from t+1) + sum_j d_h_enriched_seq[i, t, j] * w_c[j, s]
|
||||
*
|
||||
* Backward kernel: thread per (i, j) injects its own j-channel
|
||||
* contribution at each step; the per-channel reductions across j
|
||||
* (mamba2_alpha_reduce_d_proj / _d_w_c) work UNCHANGED — same scratch
|
||||
* shapes, same launch configs.
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void mamba2_alpha_scan_fwd_seq(
|
||||
const float* __restrict__ a_proj, // [N, K, state_d]
|
||||
const float* __restrict__ b_proj, // [N, K, state_d]
|
||||
const float* __restrict__ w_c, // [sh2, state_d]
|
||||
const float* __restrict__ h_s2, // [N, sh2]
|
||||
float* __restrict__ h_enriched_seq, // [N, K, sh2] — written at every step
|
||||
int N,
|
||||
int K,
|
||||
int sh2,
|
||||
int state_d
|
||||
) {
|
||||
int i = blockIdx.x;
|
||||
int j = blockIdx.y * blockDim.x + threadIdx.x;
|
||||
if (i >= N || j >= sh2) return;
|
||||
|
||||
float x[MAMBA2_ALPHA_MAX_STATE_D];
|
||||
#pragma unroll
|
||||
for (int s = 0; s < MAMBA2_ALPHA_MAX_STATE_D; s++) x[s] = 0.0f;
|
||||
|
||||
const float h_s2_ij = h_s2[(long long)i * sh2 + j];
|
||||
|
||||
for (int t = 0; t < K; t++) {
|
||||
long long fwd_base = ((long long)i * K + t) * state_d;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
float gate = 1.0f / (1.0f + expf(-a_proj[fwd_base + s]));
|
||||
x[s] = gate * x[s] + b_proj[fwd_base + s];
|
||||
}
|
||||
float ctx = 0.0f;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
ctx += w_c[(long long)j * state_d + s] * x[s];
|
||||
}
|
||||
h_enriched_seq[(((long long)i * K) + t) * sh2 + j] = h_s2_ij + ctx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* SINGLE-STEP forward scan — CRT Phase A0.5 incremental SSM.
|
||||
*
|
||||
* Same arithmetic as mamba2_alpha_scan_fwd_seq with K=1, but READS the
|
||||
* recurrent SSM register state from `x_state[N, sh2, state_d]` and WRITES
|
||||
* the post-step state back. Lets the caller advance the SSM by one
|
||||
* snapshot at a time across many launches without re-running over a
|
||||
* K-window each time (which is what scan_fwd_seq with K=1 would do —
|
||||
* it'd reset x[] to zero on every call and never advance).
|
||||
*
|
||||
* The dedicated kernel is needed because the existing scan_fwd_seq
|
||||
* unconditionally zero-initialises its register-array state at entry
|
||||
* (line 253-255 in this file). Calling it with K=1 would discard prior
|
||||
* state every launch.
|
||||
*
|
||||
* x_state : [N, sh2, state_d] PERSISTENT SSM register state
|
||||
* (one thread per (i, j) owns
|
||||
* state_d floats); read at start,
|
||||
* written back at end.
|
||||
* a_proj : [N, state_d] per-step gate (single snapshot)
|
||||
* b_proj : [N, state_d] per-step input (single snapshot)
|
||||
* w_c : [sh2, state_d] output mix
|
||||
* h_s2 : [N, sh2] residual (typically zeros)
|
||||
* h_out : [N, sh2] OUTPUT enriched hidden state
|
||||
*
|
||||
* Launch: grid=(N, ceil_div(sh2, blockDim.x), 1) block=(32-128, 1, 1)
|
||||
* — same launch shape as scan_fwd_seq with sh2 channels.
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void mamba2_alpha_scan_fwd_step(
|
||||
float* __restrict__ x_state, // [N, sh2, state_d] in/out
|
||||
const float* __restrict__ a_proj, // [N, state_d]
|
||||
const float* __restrict__ b_proj, // [N, state_d]
|
||||
const float* __restrict__ w_c, // [sh2, state_d]
|
||||
const float* __restrict__ h_s2, // [N, sh2]
|
||||
float* __restrict__ h_out, // [N, sh2]
|
||||
int N,
|
||||
int sh2,
|
||||
int state_d
|
||||
) {
|
||||
int i = blockIdx.x;
|
||||
int j = blockIdx.y * blockDim.x + threadIdx.x;
|
||||
if (i >= N || j >= sh2) return;
|
||||
|
||||
/* Load this (i, j) thread's SSM register state from DRAM. */
|
||||
float x[MAMBA2_ALPHA_MAX_STATE_D];
|
||||
long long state_base = ((long long)i * sh2 + j) * state_d;
|
||||
#pragma unroll
|
||||
for (int s = 0; s < MAMBA2_ALPHA_MAX_STATE_D; s++) x[s] = 0.0f;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
x[s] = x_state[state_base + s];
|
||||
}
|
||||
|
||||
/* Advance by one step: x[s] = sigmoid(a) * x[s] + b. */
|
||||
long long ab_base = (long long)i * state_d;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
float gate = 1.0f / (1.0f + expf(-a_proj[ab_base + s]));
|
||||
x[s] = gate * x[s] + b_proj[ab_base + s];
|
||||
}
|
||||
|
||||
/* Compute output contraction: h_out[i, j] = h_s2[i, j] + sum_s w_c[j, s] * x[s]. */
|
||||
float ctx = 0.0f;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
ctx += w_c[(long long)j * state_d + s] * x[s];
|
||||
}
|
||||
h_out[(long long)i * sh2 + j] = h_s2[(long long)i * sh2 + j] + ctx;
|
||||
|
||||
/* Write post-step state back to DRAM. */
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
x_state[state_base + s] = x[s];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extern "C" __global__ void mamba2_alpha_scan_bwd_seq(
|
||||
const float* __restrict__ a_proj, // [N, K, state_d]
|
||||
const float* __restrict__ b_proj, // [N, K, state_d]
|
||||
const float* __restrict__ d_h_enriched_seq, // [N, K, sh2]
|
||||
const float* __restrict__ w_c, // [sh2, state_d]
|
||||
float* __restrict__ d_a_per_channel, // [N, sh2, K, state_d]
|
||||
float* __restrict__ d_b_per_channel, // [N, sh2, K, state_d]
|
||||
float* __restrict__ d_w_c_per_sample, // [N, sh2, state_d]
|
||||
float* __restrict__ d_h_s2, // [N, sh2]
|
||||
int N,
|
||||
int K,
|
||||
int sh2,
|
||||
int state_d
|
||||
) {
|
||||
int i = blockIdx.x;
|
||||
int j = blockIdx.y * blockDim.x + threadIdx.x;
|
||||
if (i >= N || j >= sh2) return;
|
||||
|
||||
/* Replay forward state, caching x[t][s]. */
|
||||
float x_hist[MAMBA2_ALPHA_MAX_K * MAMBA2_ALPHA_MAX_STATE_D];
|
||||
float x[MAMBA2_ALPHA_MAX_STATE_D];
|
||||
#pragma unroll
|
||||
for (int s = 0; s < MAMBA2_ALPHA_MAX_STATE_D; s++) x[s] = 0.0f;
|
||||
|
||||
for (int t = 0; t < K; t++) {
|
||||
long long fwd_base = ((long long)i * K + t) * state_d;
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
float gate = 1.0f / (1.0f + expf(-a_proj[fwd_base + s]));
|
||||
x[s] = gate * x[s] + b_proj[fwd_base + s];
|
||||
x_hist[t * MAMBA2_ALPHA_MAX_STATE_D + s] = x[s];
|
||||
}
|
||||
}
|
||||
|
||||
/* Zero d_state and d_w_c_per_sample (the latter accumulates across t). */
|
||||
float d_state[MAMBA2_ALPHA_MAX_STATE_D];
|
||||
#pragma unroll
|
||||
for (int s = 0; s < MAMBA2_ALPHA_MAX_STATE_D; s++) d_state[s] = 0.0f;
|
||||
long long w_c_slot = ((long long)i * sh2 + j) * state_d;
|
||||
for (int s = 0; s < state_d; s++) d_w_c_per_sample[w_c_slot + s] = 0.0f;
|
||||
|
||||
float d_h_s2_sum = 0.0f;
|
||||
|
||||
/* Reverse scan: at each step t (going K-1 → 0), inject this step's
|
||||
* gradient into d_state via W_c BEFORE recording d_a/d_b and
|
||||
* propagating through the gate. */
|
||||
long long per_chan_base = ((long long)i * sh2 + j) * (long long)K * state_d;
|
||||
for (int t = K - 1; t >= 0; t--) {
|
||||
long long fwd_base = ((long long)i * K + t) * state_d;
|
||||
float d_h_ij_t = d_h_enriched_seq[(((long long)i * K) + t) * sh2 + j];
|
||||
d_h_s2_sum += d_h_ij_t;
|
||||
|
||||
/* W_c grad contribution at step t: d_w_c[j,s] += d_h_ij_t * x[t,s]. */
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
d_w_c_per_sample[w_c_slot + s] +=
|
||||
d_h_ij_t * x_hist[t * MAMBA2_ALPHA_MAX_STATE_D + s];
|
||||
}
|
||||
/* Inject step-t gradient into d_state via W_c. */
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
d_state[s] += d_h_ij_t * w_c[(long long)j * state_d + s];
|
||||
}
|
||||
/* Record d_b[t,s], d_a[t,s] from the CURRENT (post-injection) d_state. */
|
||||
for (int s = 0; s < state_d; s++) {
|
||||
float a_raw = a_proj[fwd_base + s];
|
||||
float gate = 1.0f / (1.0f + expf(-a_raw));
|
||||
float sig_deriv = gate * (1.0f - gate);
|
||||
float x_prev = (t == 0) ? 0.0f : x_hist[(t - 1) * MAMBA2_ALPHA_MAX_STATE_D + s];
|
||||
|
||||
long long slot = per_chan_base + (long long)t * state_d + s;
|
||||
d_b_per_channel[slot] = d_state[s];
|
||||
d_a_per_channel[slot] = d_state[s] * x_prev * sig_deriv;
|
||||
|
||||
d_state[s] = d_state[s] * gate;
|
||||
}
|
||||
}
|
||||
d_h_s2[(long long)i * sh2 + j] = d_h_s2_sum;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Phase 1d.4 backtest — per-trade PnL kernel.
|
||||
*
|
||||
* For each (threshold τ, sequence n), apply the trading rule:
|
||||
* pred_dev = probs[n] - 0.5
|
||||
* if |pred_dev| > τ:
|
||||
* direction = sign(pred_dev) in {-1, +1}
|
||||
* pnl = direction * (price_kt[n] - price_t[n]) - cost
|
||||
* trade = 1
|
||||
* else:
|
||||
* pnl = 0
|
||||
* trade = 0
|
||||
*
|
||||
* Output is row-major `[T, N]` for both pnl and trade. Allows a single
|
||||
* kernel launch to evaluate the full threshold sweep in one pass.
|
||||
*
|
||||
* Grid: (T, ceil(N / 256)), Block: 256
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void backtest_per_trade_pnl(
|
||||
const float* __restrict__ probs, /* [N] stacker sigmoid output ∈ [0, 1] */
|
||||
const float* __restrict__ prices_t, /* [N] mid-price at sequence end-bar */
|
||||
const float* __restrict__ prices_kt, /* [N] mid-price at end-bar + horizon */
|
||||
const float* __restrict__ thresholds, /* [T] confidence thresholds */
|
||||
float cost, /* round-trip cost in price units */
|
||||
float* __restrict__ pnl_out, /* [T, N] per-trade PnL (0 if no trade) */
|
||||
int* __restrict__ trade_out, /* [T, N] 1 if trade, 0 otherwise */
|
||||
int N,
|
||||
int T
|
||||
) {
|
||||
int t_idx = blockIdx.x;
|
||||
int n_idx = blockIdx.y * blockDim.x + threadIdx.x;
|
||||
if (t_idx >= T || n_idx >= N) return;
|
||||
|
||||
float prob = probs[n_idx];
|
||||
float thresh = thresholds[t_idx];
|
||||
float dev = prob - 0.5f;
|
||||
float abs_d = fabsf(dev);
|
||||
|
||||
long long slot = (long long)t_idx * N + n_idx;
|
||||
if (abs_d > thresh) {
|
||||
float dir = (dev > 0.0f) ? 1.0f : -1.0f;
|
||||
pnl_out[slot] = dir * (prices_kt[n_idx] - prices_t[n_idx]) - cost;
|
||||
trade_out[slot] = 1;
|
||||
} else {
|
||||
pnl_out[slot] = 0.0f;
|
||||
trade_out[slot] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Phase 1d.4 backtest — block tree-reduce one row of [T, N] data.
|
||||
*
|
||||
* Per-threshold reduction: sum across N to produce a [T]-shaped output.
|
||||
* Avoids atomicAdd via the standard block-tree shared-memory reduction.
|
||||
*
|
||||
* Grid: (T, 1), Block: 256
|
||||
* Shared: 256 floats
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void backtest_sum_reduce_f32(
|
||||
const float* __restrict__ input, /* [T, N] */
|
||||
float* __restrict__ output, /* [T] */
|
||||
int N,
|
||||
int T
|
||||
) {
|
||||
__shared__ float sdata[256];
|
||||
int t_idx = blockIdx.x;
|
||||
if (t_idx >= T) return;
|
||||
|
||||
int tid = threadIdx.x;
|
||||
long long base = (long long)t_idx * N;
|
||||
|
||||
/* Each thread accumulates its slice of the N axis. */
|
||||
float local = 0.0f;
|
||||
for (int i = tid; i < N; i += blockDim.x) {
|
||||
local += input[base + i];
|
||||
}
|
||||
sdata[tid] = local;
|
||||
__syncthreads();
|
||||
|
||||
/* Block tree-reduce in shared memory. */
|
||||
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) sdata[tid] += sdata[tid + stride];
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) output[t_idx] = sdata[0];
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Phase 1d.4 backtest — block tree-reduce SQUARED values along N.
|
||||
*
|
||||
* Computes `sum_i(input[t, i]^2)` per threshold t. Used for variance
|
||||
* computation: var = sum_sq / count - mean^2 (Welford-equivalent for
|
||||
* a single pass; n is large so f32 numerical error is acceptable).
|
||||
*
|
||||
* Grid: (T, 1), Block: 256
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void backtest_sum_squared_reduce_f32(
|
||||
const float* __restrict__ input, /* [T, N] */
|
||||
float* __restrict__ output, /* [T] */
|
||||
int N,
|
||||
int T
|
||||
) {
|
||||
__shared__ float sdata[256];
|
||||
int t_idx = blockIdx.x;
|
||||
if (t_idx >= T) return;
|
||||
|
||||
int tid = threadIdx.x;
|
||||
long long base = (long long)t_idx * N;
|
||||
|
||||
float local = 0.0f;
|
||||
for (int i = tid; i < N; i += blockDim.x) {
|
||||
float v = input[base + i];
|
||||
local += v * v;
|
||||
}
|
||||
sdata[tid] = local;
|
||||
__syncthreads();
|
||||
|
||||
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) sdata[tid] += sdata[tid + stride];
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) output[t_idx] = sdata[0];
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Phase 1d.4 backtest — sum reduce for INT data (trade counts).
|
||||
*
|
||||
* Same algorithm as float reduce but operates on int32 input. Counts
|
||||
* the number of trades per threshold.
|
||||
*
|
||||
* Grid: (T, 1), Block: 256
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void backtest_sum_reduce_i32(
|
||||
const int* __restrict__ input, /* [T, N] */
|
||||
int* __restrict__ output, /* [T] */
|
||||
int N,
|
||||
int T
|
||||
) {
|
||||
__shared__ int sdata[256];
|
||||
int t_idx = blockIdx.x;
|
||||
if (t_idx >= T) return;
|
||||
|
||||
int tid = threadIdx.x;
|
||||
long long base = (long long)t_idx * N;
|
||||
|
||||
int local = 0;
|
||||
for (int i = tid; i < N; i += blockDim.x) {
|
||||
local += input[base + i];
|
||||
}
|
||||
sdata[tid] = local;
|
||||
__syncthreads();
|
||||
|
||||
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) sdata[tid] += sdata[tid + stride];
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) output[t_idx] = sdata[0];
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* AdamW step — bias-corrected decoupled-weight-decay update for one
|
||||
* parameter tensor. Identical to ml-core's adamw_update kernel, included
|
||||
* here so ml-alpha's cubin is self-contained (no cross-crate cubin loads).
|
||||
*
|
||||
* grad_scale: 1.0 disables gradient clipping; <1.0 applied as a scale.
|
||||
* t: 1-indexed training step (for bias correction).
|
||||
*
|
||||
* Grid: ceil(n / 256), Block: 256.
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void mamba2_alpha_adamw_step(
|
||||
float* __restrict__ param,
|
||||
const float* __restrict__ grad,
|
||||
float* __restrict__ m,
|
||||
float* __restrict__ v,
|
||||
float lr,
|
||||
float beta1,
|
||||
float beta2,
|
||||
float epsilon,
|
||||
float weight_decay,
|
||||
float grad_scale,
|
||||
int t,
|
||||
int n
|
||||
) {
|
||||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= n) return;
|
||||
|
||||
float g = grad[i] * grad_scale;
|
||||
float p = param[i];
|
||||
|
||||
/* Decoupled weight decay. */
|
||||
p = p * (1.0f - lr * weight_decay);
|
||||
|
||||
/* Moment updates. */
|
||||
float mi = beta1 * m[i] + (1.0f - beta1) * g;
|
||||
float vi = beta2 * v[i] + (1.0f - beta2) * g * g;
|
||||
m[i] = mi;
|
||||
v[i] = vi;
|
||||
|
||||
/* Bias correction. */
|
||||
float bc1 = 1.0f - powf(beta1, (float)t);
|
||||
float bc2 = 1.0f - powf(beta2, (float)t);
|
||||
float m_hat = mi / bc1;
|
||||
float v_hat = vi / bc2;
|
||||
|
||||
/* Parameter update. */
|
||||
p = p - lr * m_hat / (sqrtf(v_hat) + epsilon);
|
||||
param[i] = p;
|
||||
}
|
||||
|
||||
|
||||
/// Variant of `mamba2_alpha_adamw_step` that reads BOTH `grad_scale` and
|
||||
/// `t` (step counter) from DEVICE pointers — companion to the
|
||||
/// GPU-resident grad-clip pipeline in `grad_norm.cu` AND the CUDA Graph
|
||||
/// capture path. Eliminates the host scalars that would otherwise be
|
||||
/// baked into the captured graph (replays would freeze grad_scale to
|
||||
/// its first-step value and step counter would stop advancing → wrong
|
||||
/// bias correction). Use `mamba2_alpha_increment_step_counter` once per
|
||||
/// training step to advance the counter inside the captured region.
|
||||
extern "C" __global__ void mamba2_alpha_adamw_step_devscale(
|
||||
float* __restrict__ param,
|
||||
const float* __restrict__ grad,
|
||||
float* __restrict__ m,
|
||||
float* __restrict__ v,
|
||||
float lr,
|
||||
float beta1,
|
||||
float beta2,
|
||||
float epsilon,
|
||||
float weight_decay,
|
||||
const float* __restrict__ grad_scale_ptr,
|
||||
const int* __restrict__ step_ptr,
|
||||
int n
|
||||
) {
|
||||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= n) return;
|
||||
|
||||
float g = grad[i] * grad_scale_ptr[0];
|
||||
float p = param[i];
|
||||
|
||||
/* Decoupled weight decay. */
|
||||
p = p * (1.0f - lr * weight_decay);
|
||||
|
||||
/* Moment updates. */
|
||||
float mi = beta1 * m[i] + (1.0f - beta1) * g;
|
||||
float vi = beta2 * v[i] + (1.0f - beta2) * g * g;
|
||||
m[i] = mi;
|
||||
v[i] = vi;
|
||||
|
||||
/* Bias correction. */
|
||||
int t = step_ptr[0];
|
||||
float bc1 = 1.0f - powf(beta1, (float)t);
|
||||
float bc2 = 1.0f - powf(beta2, (float)t);
|
||||
float m_hat = mi / bc1;
|
||||
float v_hat = vi / bc2;
|
||||
|
||||
/* Parameter update. */
|
||||
p = p - lr * m_hat / (sqrtf(v_hat) + epsilon);
|
||||
param[i] = p;
|
||||
}
|
||||
|
||||
/// Increment the device-resident step counter for Mamba2 AdamW.
|
||||
/// Single-thread kernel; launch once per training step inside the
|
||||
/// captured graph, AFTER all `mamba2_alpha_adamw_step_devscale` launches
|
||||
/// that read the current step value.
|
||||
extern "C" __global__ void mamba2_alpha_increment_step_counter(int* __restrict__ step_ptr) {
|
||||
if (threadIdx.x == 0 && blockIdx.x == 0) {
|
||||
step_ptr[0] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Reduction kernel: sum d_w_c_per_sample[N, sh2, state_d] across N →
|
||||
* d_w_c[sh2, state_d].
|
||||
*
|
||||
* Grid: (sh2, ceil(state_d / 32)), Block: 32
|
||||
* --------------------------------------------------------------------- */
|
||||
extern "C" __global__ void mamba2_alpha_reduce_d_w_c(
|
||||
const float* __restrict__ d_w_c_per_sample, // [N, sh2, state_d]
|
||||
float* __restrict__ d_w_c, // [sh2, state_d]
|
||||
int N,
|
||||
int sh2,
|
||||
int state_d
|
||||
) {
|
||||
int j = blockIdx.x;
|
||||
int s = blockIdx.y * blockDim.x + threadIdx.x;
|
||||
if (j >= sh2 || s >= state_d) return;
|
||||
|
||||
/* Sum over i: d_w_c[j, s] = sum_i d_w_c_per_sample[i, j, s] */
|
||||
float sum = 0.0f;
|
||||
for (int i = 0; i < N; i++) {
|
||||
sum += d_w_c_per_sample[((long long)i * sh2 + j) * state_d + s];
|
||||
}
|
||||
d_w_c[(long long)j * state_d + s] = sum;
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
// multi_head_policy_aggregate_diag.cu — Phase 2A-D (2026-06-03)
|
||||
//
|
||||
// Device-aggregated gate-and-head diagnostics for the MultiHeadPolicy
|
||||
// path. Reads forward outputs (`gate_probs [B × K]`, `pi_probs_k
|
||||
// [B × K × N_ACTIONS]`) and writes 25 scalars into the ISV bus:
|
||||
//
|
||||
// 765..773 gate_probs_mean[K] — mean over B of gate_probs[b,k]
|
||||
// 773..781 gate_argmax_mass[K] — fraction of B where head k is argmax
|
||||
// 781 gate_entropy_mean — mean over B of −Σ_k p·log p
|
||||
// 782..790 per_head_entropy_mean[K] — mean over B of −Σ_a π_k·log π_k
|
||||
//
|
||||
// Tail entries past runtime K (in the 8-stride arrays) are written 0.0
|
||||
// so the diag JSON schema is constant for K ∈ [1, 8] (matches the
|
||||
// MAX_K_HEADS=8 mirror in `rl/multi_head_policy.rs` and the kernel
|
||||
// `#define MAX_K_HEADS 8` in `multi_head_policy_forward.cu` /
|
||||
// `multi_head_policy_gate_forward.cu`).
|
||||
//
|
||||
// Constraints honoured:
|
||||
// * `feedback_no_atomicadd` — no atomic ops. Each ISV destination cell
|
||||
// has a single writer (thread 0 of the responsible block).
|
||||
// * `feedback_cpu_is_read_only` — pure device kernel.
|
||||
// * `feedback_nvidia_grade_perf_for_kernels` — no host branches in
|
||||
// capture path; deterministic launch shape.
|
||||
// * `pearl_fleet_fraction_not_aggregate` — these are explicit
|
||||
// per-batch-fraction stats (mean over B / argmax-mass over B), not
|
||||
// aggregate scalars hiding per-batch state.
|
||||
//
|
||||
// Determinism: every reduction uses a fixed-order sequential sum within
|
||||
// a single thread. Inputs (`gate_probs`, `pi_probs_k`) are forward
|
||||
// outputs already on the launching stream; aggregator runs on the same
|
||||
// stream so the producer→consumer ordering is single-stream.
|
||||
//
|
||||
// Launch config:
|
||||
// grid = (MAX_K_HEADS + 1, 1, 1) = 9 blocks
|
||||
// block = (BLOCK_THREADS, 1, 1) = 128 threads
|
||||
// smem = 0 (small static shared arrays declared inside)
|
||||
//
|
||||
// Block role assignment:
|
||||
// blockIdx.x = 0..MAX_K_HEADS-1 → per-head stats for head k = blockIdx.x.
|
||||
// Writes gate_probs_mean[k] +
|
||||
// per_head_entropy_mean[k]. Tail
|
||||
// heads (k >= runtime K) write 0.0.
|
||||
// blockIdx.x = MAX_K_HEADS → global stats. Computes
|
||||
// gate_entropy_mean +
|
||||
// gate_argmax_mass[k] for all K
|
||||
// heads. Tail entries write 0.0.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define MAX_K_HEADS 8
|
||||
#define N_ACTIONS 11
|
||||
#define BLOCK_THREADS 128
|
||||
|
||||
extern "C" __global__ void multi_head_policy_aggregate_diag(
|
||||
const float* __restrict__ gate_probs, // [B × K]
|
||||
const float* __restrict__ pi_probs_k, // [B × K × N_ACTIONS]
|
||||
float* __restrict__ isv, // ISV bus
|
||||
int b_size,
|
||||
int k_runtime, // ∈ [1, MAX_K_HEADS]
|
||||
int gate_probs_mean_base, // 765
|
||||
int gate_argmax_mass_base,// 773
|
||||
int gate_entropy_mean_slot,// 781
|
||||
int per_head_entropy_mean_base // 782
|
||||
) {
|
||||
const int k_block = blockIdx.x; // ∈ [0, MAX_K_HEADS]
|
||||
const int tid = threadIdx.x; // ∈ [0, BLOCK_THREADS)
|
||||
const float inv_b = (b_size > 0) ? (1.0f / (float)b_size) : 0.0f;
|
||||
|
||||
// ── Per-head blocks (k_block = 0 .. MAX_K_HEADS-1) ────────────────
|
||||
if (k_block < MAX_K_HEADS) {
|
||||
// Tail heads past runtime K — sole writer (thread 0) clears its
|
||||
// two ISV destinations to 0.0 and exits.
|
||||
if (k_block >= k_runtime) {
|
||||
if (tid == 0) {
|
||||
isv[gate_probs_mean_base + k_block] = 0.0f;
|
||||
isv[per_head_entropy_mean_base + k_block] = 0.0f;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Stat 1: gate_probs_mean[k_block] ─────────────────────────
|
||||
// Each thread sums a strided subset of batches; tree-reduce in
|
||||
// shared memory.
|
||||
__shared__ float s_gpm[BLOCK_THREADS];
|
||||
__shared__ float s_phe[BLOCK_THREADS];
|
||||
float my_gpm = 0.0f;
|
||||
float my_phe = 0.0f;
|
||||
|
||||
for (int b = tid; b < b_size; b += BLOCK_THREADS) {
|
||||
// gate_probs is [B × K] row-major over (b, k).
|
||||
const float p = gate_probs[b * k_runtime + k_block];
|
||||
my_gpm += p;
|
||||
|
||||
// ── Stat 2: per-head entropy ─────────────────────────────
|
||||
// pi_probs_k is [B × K × N_ACTIONS] row-major over
|
||||
// (b, k, a). Per-head per-batch entropy:
|
||||
// H_k(b) = -Σ_a π_k[b,a] · log(π_k[b,a])
|
||||
// Guard against log(0); the forward kernel writes a softmax
|
||||
// output so π_k[b,a] > 0 (modulo fp32 underflow), but be
|
||||
// defensive — entries < 1e-12 are clamped to a 0 contribution
|
||||
// (matches the standard entropy convention 0·log(0) = 0).
|
||||
const int row = (b * k_runtime + k_block) * N_ACTIONS;
|
||||
float h = 0.0f;
|
||||
for (int a = 0; a < N_ACTIONS; ++a) {
|
||||
const float pa = pi_probs_k[row + a];
|
||||
if (pa > 1e-12f) {
|
||||
h -= pa * logf(pa);
|
||||
}
|
||||
}
|
||||
my_phe += h;
|
||||
}
|
||||
s_gpm[tid] = my_gpm;
|
||||
s_phe[tid] = my_phe;
|
||||
__syncthreads();
|
||||
|
||||
// Tree-reduce in shared memory. Fixed-order sequential pairwise
|
||||
// sum — deterministic across launches (no warp shuffles, no
|
||||
// atomic adds).
|
||||
for (int stride = BLOCK_THREADS / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) {
|
||||
s_gpm[tid] += s_gpm[tid + stride];
|
||||
s_phe[tid] += s_phe[tid + stride];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
isv[gate_probs_mean_base + k_block] = s_gpm[0] * inv_b;
|
||||
isv[per_head_entropy_mean_base + k_block] = s_phe[0] * inv_b;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Global block (k_block == MAX_K_HEADS) ─────────────────────────
|
||||
// Computes gate_entropy_mean (scalar) and gate_argmax_mass[K] (array).
|
||||
//
|
||||
// The argmax tally must avoid atomicAdd. Each thread maintains a
|
||||
// private per-head counter in registers (small array indexed by
|
||||
// 0..MAX_K_HEADS-1) while iterating its stride of batches. After
|
||||
// the per-thread loop, tree-reduce each counter across threads via
|
||||
// shared memory.
|
||||
|
||||
__shared__ float s_ge[BLOCK_THREADS]; // gate entropy partial
|
||||
__shared__ float s_am[MAX_K_HEADS][BLOCK_THREADS]; // argmax counts per (k, tid)
|
||||
|
||||
int my_am[MAX_K_HEADS];
|
||||
float my_ge = 0.0f;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < MAX_K_HEADS; ++k) my_am[k] = 0;
|
||||
|
||||
for (int b = tid; b < b_size; b += BLOCK_THREADS) {
|
||||
// Walk this batch row once: argmax + entropy in one pass.
|
||||
int argmax_k = 0;
|
||||
float max_p = gate_probs[b * k_runtime + 0];
|
||||
float h = 0.0f;
|
||||
if (max_p > 1e-12f) h -= max_p * logf(max_p);
|
||||
for (int k = 1; k < k_runtime; ++k) {
|
||||
const float p = gate_probs[b * k_runtime + k];
|
||||
// Ties broken by lowest index — `>` not `>=`. Deterministic.
|
||||
if (p > max_p) {
|
||||
max_p = p;
|
||||
argmax_k = k;
|
||||
}
|
||||
if (p > 1e-12f) h -= p * logf(p);
|
||||
}
|
||||
my_am[argmax_k] += 1;
|
||||
my_ge += h;
|
||||
}
|
||||
|
||||
// Scatter per-thread counters into shared memory for tree reduction.
|
||||
s_ge[tid] = my_ge;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < MAX_K_HEADS; ++k) {
|
||||
s_am[k][tid] = (float)my_am[k];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
for (int stride = BLOCK_THREADS / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) {
|
||||
s_ge[tid] += s_ge[tid + stride];
|
||||
#pragma unroll
|
||||
for (int k = 0; k < MAX_K_HEADS; ++k) {
|
||||
s_am[k][tid] += s_am[k][tid + stride];
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
// gate_entropy_mean — scalar.
|
||||
isv[gate_entropy_mean_slot] = s_ge[0] * inv_b;
|
||||
|
||||
// gate_argmax_mass[k] — fraction over B. Tail entries (k >= K)
|
||||
// are 0 by construction (my_am[k>=K] is never incremented).
|
||||
#pragma unroll
|
||||
for (int k = 0; k < MAX_K_HEADS; ++k) {
|
||||
isv[gate_argmax_mass_base + k] = s_am[k][0] * inv_b;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// multi_head_policy_aux_prior.cu — Per-head auxiliary KL prior gradient.
|
||||
//
|
||||
// Spec: docs/superpowers/specs/2026-06-02-multi-head-policy-with-r-multiple.md
|
||||
// ADDENDUM 2026-06-03 §R.4 (per-head aux KL priors)
|
||||
// Plan: docs/superpowers/plans/2026-06-03-multi-head-policy-implementation.md (Phase 2A-B)
|
||||
//
|
||||
// For each head k, we add an auxiliary KL regularizer that anchors the
|
||||
// head distribution to a fixed prior:
|
||||
//
|
||||
// L_aux_k = β · KL(π_k || prior_k)
|
||||
// = β · Σ_a π_k(a) · log( π_k(a) / prior_k(a) )
|
||||
//
|
||||
// The grad of L_aux on the head's pre-softmax logits is the standard
|
||||
// softmax-Jacobian form (parameterization-invariant):
|
||||
//
|
||||
// ∂L_aux_k / ∂pi_logits_k[a] = β · π_k(a) · ( log(π_k(a)/prior_k(a)) − KL_k )
|
||||
//
|
||||
// This kernel ADDS that contribution to `grad_pi_logits_k` (which the
|
||||
// backward pi kernel populated with the Q-distill chain rule).
|
||||
//
|
||||
// β is read from ISV slot `RL_POLICY_AUX_PRIOR_BETA_INDEX = 764`.
|
||||
//
|
||||
// ── Block layout ────────────────────────────────────────────────────────
|
||||
// grid = (B, K, 1)
|
||||
// block = (N_ACTIONS = 11, 1, 1)
|
||||
//
|
||||
// One block per (batch, head). N_ACTIONS threads cooperate via shared
|
||||
// memory to compute KL_k[b] then write the additive grad per action.
|
||||
//
|
||||
// Per feedback_no_atomicadd: sole-writer per (b, k, a) cell within this
|
||||
// kernel; `grad_pi_logits_k` is read-modify-write but additive into a
|
||||
// slot that was just exclusively written by the preceding pi backward
|
||||
// kernel on the same stream — sequential stream ordering keeps the RMW
|
||||
// safe (no races).
|
||||
//
|
||||
// Per feedback_no_nvrtc: pre-compiled cubin via build.rs.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define N_ACTIONS 11
|
||||
#define RL_POLICY_AUX_PRIOR_BETA_INDEX 764
|
||||
#define KL_PROB_EPS 1e-12f // log domain stability
|
||||
|
||||
extern "C" __global__ void multi_head_policy_aux_prior(
|
||||
const float* __restrict__ pi_probs_k, // [B × K × N_ACTIONS]
|
||||
const float* __restrict__ priors, // [K × N_ACTIONS]
|
||||
const float* __restrict__ isv, // ISV bus — slot 764 holds β
|
||||
const int B,
|
||||
const int K,
|
||||
float* __restrict__ grad_pi_logits_k // [B × K × N_ACTIONS] RW (+=)
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
const int k = blockIdx.y;
|
||||
if (b >= B || k >= K) return;
|
||||
|
||||
const int a = threadIdx.x;
|
||||
if (a >= N_ACTIONS) return;
|
||||
|
||||
const float beta = isv[RL_POLICY_AUX_PRIOR_BETA_INDEX];
|
||||
// Zero-β short circuit — leaves grad_pi_logits_k untouched.
|
||||
if (beta == 0.0f) return;
|
||||
|
||||
// ── Stage probs + per-action log(π/prior) ─────────────────────────
|
||||
__shared__ float s_pi[N_ACTIONS];
|
||||
__shared__ float s_log_ratio[N_ACTIONS];
|
||||
__shared__ float s_kl;
|
||||
|
||||
const int pk_idx = (b * K + k) * N_ACTIONS + a;
|
||||
const float pi_a = pi_probs_k[pk_idx];
|
||||
const float prior_a = priors[k * N_ACTIONS + a];
|
||||
|
||||
s_pi[a] = pi_a;
|
||||
// log(π_a / prior_a) with both args clamped against fp32 underflow.
|
||||
s_log_ratio[a] = logf(fmaxf(pi_a, KL_PROB_EPS) / fmaxf(prior_a, KL_PROB_EPS));
|
||||
__syncthreads();
|
||||
|
||||
// ── KL_k[b] = Σ_a π_a · log(π_a / prior_a) ────────────────────────
|
||||
// Single-thread reduction over N_ACTIONS=11 for deterministic order.
|
||||
if (a == 0) {
|
||||
float kl = 0.0f;
|
||||
for (int aa = 0; aa < N_ACTIONS; ++aa) {
|
||||
kl += s_pi[aa] * s_log_ratio[aa];
|
||||
}
|
||||
s_kl = kl;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── grad += β · π_a · (log_ratio − KL) ────────────────────────────
|
||||
const float g_aux = beta * s_pi[a] * (s_log_ratio[a] - s_kl);
|
||||
grad_pi_logits_k[pk_idx] += g_aux;
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
// multi_head_policy_backward.cu — Backward pass for the K-head policy mixture.
|
||||
//
|
||||
// Spec: docs/superpowers/specs/2026-06-02-multi-head-policy-with-r-multiple.md
|
||||
// ADDENDUM 2026-06-03 §R.4 (per-head aux KL priors), §R.6 (gradient flow)
|
||||
// Plan: docs/superpowers/plans/2026-06-03-multi-head-policy-implementation.md (Phase 2A-B)
|
||||
//
|
||||
// ── Forward recap (multi_head_policy_forward.cu) ────────────────────────
|
||||
// pi_logits_k[b,k,a] = W_heads[k,a,:] · h_t[b,:] + b_heads[k,a]
|
||||
// pi_probs_k[b,k,a] = softmax_a(pi_logits_k[b,k,:])
|
||||
// pi_probs[b,a] = Σ_k gate_probs[b,k] · pi_probs_k[b,k,a]
|
||||
// pi_logits[b,a] = log(pi_probs[b,a] + ε)
|
||||
//
|
||||
// ── Backward chain implemented by this file (two kernels) ───────────────
|
||||
//
|
||||
// (1) `multi_head_policy_backward_pi` — distributes incoming `grad_pi_logits`
|
||||
// through the mixture and per-head softmax. Outputs:
|
||||
// a. grad_pi_logits_k [B × K × N_ACTIONS] (for aux prior + diag)
|
||||
// b. grad_gate_probs [B × K] (consumed by kernel 2)
|
||||
// c. grad_W_heads_pb [B × K × N_ACTIONS × HIDDEN_DIM]
|
||||
// d. grad_b_heads_pb [B × K × N_ACTIONS]
|
||||
// e. grad_h_t_scratch [B × HIDDEN_DIM] (OVERWRITE; caller folds
|
||||
// via grad_h_accumulate_scaled)
|
||||
//
|
||||
// (2) `multi_head_policy_backward_gate` — distributes `grad_gate_probs`
|
||||
// through the gating softmax. Outputs:
|
||||
// a. grad_gate_logits [B × K] (diag)
|
||||
// b. grad_W_gate_pb [B × K × REGIME_DIM]
|
||||
// c. grad_b_gate_pb [B × K]
|
||||
// d. grad_regime_h [B × REGIME_DIM] (computed; NOT used
|
||||
// upstream in Phase 2A-B)
|
||||
//
|
||||
// ── Math (kernel 1) ─────────────────────────────────────────────────────
|
||||
// grad_pi_probs[b,a] = grad_pi_logits[b,a] / (pi_probs[b,a] + ε)
|
||||
// grad_pi_probs_k[b,k,a] = gate_probs[b,k] · grad_pi_probs[b,a]
|
||||
// grad_gate_probs[b,k] = Σ_a pi_probs_k[b,k,a] · grad_pi_probs[b,a]
|
||||
//
|
||||
// (per-head softmax-Jacobian; standard form):
|
||||
// grad_pi_logits_k[b,k,a] = pi_probs_k[b,k,a]
|
||||
// · ( grad_pi_probs_k[b,k,a]
|
||||
// − Σ_{a'} pi_probs_k[b,k,a'] · grad_pi_probs_k[b,k,a'] )
|
||||
//
|
||||
// ── Math (kernel 2) ─────────────────────────────────────────────────────
|
||||
// grad_gate_logits[b,k] = gate_probs[b,k]
|
||||
// · ( grad_gate_probs[b,k]
|
||||
// − Σ_{k'} gate_probs[b,k'] · grad_gate_probs[b,k'] )
|
||||
//
|
||||
// grad_W_gate_pb[b,k,r] = grad_gate_logits[b,k] · regime_h[b,r]
|
||||
// grad_b_gate_pb[b,k] = grad_gate_logits[b,k]
|
||||
// grad_regime_h[b,r] = Σ_k W_gate[k,r] · grad_gate_logits[b,k]
|
||||
//
|
||||
// ── Per-batch grad scratch convention ───────────────────────────────────
|
||||
// Caller reduces `_pb` buffers via `reduce_axis0`:
|
||||
// grad_W_heads_pb [B × K·N·H] → grad_W_heads [K·N·H]
|
||||
// grad_b_heads_pb [B × K·N] → grad_b_heads [K·N]
|
||||
// grad_W_gate_pb [B × K·R] → grad_W_gate [K·R]
|
||||
// grad_b_gate_pb [B × K] → grad_b_gate [K]
|
||||
//
|
||||
// Per feedback_no_atomicadd: NO atomicAdd. Each output cell has exactly one
|
||||
// writer per launch. Per-batch scratch + reduce_axis0 is the standard
|
||||
// foxhunt pattern (see ppo_clipped_surrogate.cu::ppo_grad_w_b_h_t).
|
||||
//
|
||||
// Per feedback_no_nvrtc: pre-compiled cubin via build.rs.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define HIDDEN_DIM 128
|
||||
#define N_ACTIONS 11
|
||||
#define REGIME_DIM 6
|
||||
#define MAX_K_HEADS 8 // Compile-time max; runtime K from ISV
|
||||
#define LOG_PROB_EPS 1e-12f // mirrors forward kernel's `+1e-12` guard
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Kernel 1: backward through the mixture + per-head softmax.
|
||||
//
|
||||
// Block layout:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (HIDDEN_DIM = 128, 1, 1)
|
||||
// shared = MAX_K_HEADS*N_ACTIONS*3 + MAX_K_HEADS + N_ACTIONS ≈ 304 floats
|
||||
//
|
||||
// Thread c handles one hidden-dim index. Threads 0..N_ACTIONS-1 first
|
||||
// reconstruct pi_probs[b,:] and the softmax-Jacobian — only threads with
|
||||
// `c < N_ACTIONS` participate in the small reductions. All HIDDEN_DIM
|
||||
// threads participate in the per-batch weight-grad emit.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void multi_head_policy_backward_pi(
|
||||
const float* __restrict__ grad_pi_logits, // [B × N_ACTIONS] incoming grad on log-mixture
|
||||
const float* __restrict__ h_t, // [B × HIDDEN_DIM]
|
||||
const float* __restrict__ W_heads, // [K × N_ACTIONS × HIDDEN_DIM]
|
||||
const float* __restrict__ gate_probs, // [B × K] forward output
|
||||
const float* __restrict__ pi_probs_k, // [B × K × N_ACTIONS] forward output
|
||||
const int B,
|
||||
const int K,
|
||||
float* __restrict__ grad_pi_logits_k, // [B × K × N_ACTIONS] OUT (for aux + diag)
|
||||
float* __restrict__ grad_gate_probs, // [B × K] OUT (→ kernel 2)
|
||||
float* __restrict__ grad_W_heads_pb, // [B × K × N_ACTIONS × HIDDEN_DIM]
|
||||
float* __restrict__ grad_b_heads_pb, // [B × K × N_ACTIONS]
|
||||
float* __restrict__ grad_h_t_scratch // [B × HIDDEN_DIM] (OVERWRITE)
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
if (b >= B) return;
|
||||
const int c = threadIdx.x; // hidden-dim index, 0..HIDDEN_DIM-1
|
||||
|
||||
// ── Shared state (per-batch) ──────────────────────────────────────
|
||||
__shared__ float s_pi_probs[N_ACTIONS]; // mixture probs
|
||||
__shared__ float s_grad_pi_probs[N_ACTIONS]; // dL/dp[a]
|
||||
__shared__ float s_gate_probs[MAX_K_HEADS]; // forward gate
|
||||
__shared__ float s_pi_probs_k[MAX_K_HEADS * N_ACTIONS]; // forward per-head probs
|
||||
__shared__ float s_grad_pi_probs_k[MAX_K_HEADS * N_ACTIONS]; // step 2 intermediate
|
||||
__shared__ float s_grad_pi_logits_k[MAX_K_HEADS * N_ACTIONS]; // final per-head logit grad
|
||||
__shared__ float s_dot_k[MAX_K_HEADS]; // softmax-Jacobian scalar per head
|
||||
|
||||
// ── Stage forward outputs (small, per-batch) into shared mem ───────
|
||||
if (c < K) {
|
||||
s_gate_probs[c] = gate_probs[b * K + c];
|
||||
}
|
||||
if (c < N_ACTIONS) {
|
||||
const int gpi_idx = b * N_ACTIONS + c;
|
||||
s_grad_pi_probs[c] = 0.0f; // accumulator, populated below
|
||||
// Stage pi_probs_k[b, :, c] across all heads.
|
||||
for (int k = 0; k < K; ++k) {
|
||||
s_pi_probs_k[k * N_ACTIONS + c] =
|
||||
pi_probs_k[(b * K + k) * N_ACTIONS + c];
|
||||
}
|
||||
// Reconstruct pi_probs[b, c] = Σ_k gate · pi_probs_k.
|
||||
// Sequential summation order matches the forward kernel for
|
||||
// determinism.
|
||||
float p = 0.0f;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < K; ++k) {
|
||||
p += gate_probs[b * K + k] * pi_probs_k[(b * K + k) * N_ACTIONS + c];
|
||||
}
|
||||
s_pi_probs[c] = p;
|
||||
// dL/dp[a] = dL/dlog(p+ε) × 1/(p+ε)
|
||||
const float gl = grad_pi_logits[gpi_idx];
|
||||
s_grad_pi_probs[c] = gl / (s_pi_probs[c] + LOG_PROB_EPS);
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Step 2: grad_pi_probs_k + grad_gate_probs ─────────────────────
|
||||
// grad_pi_probs_k[b,k,a] = gate_probs[b,k] · grad_pi_probs[b,a]
|
||||
// grad_gate_probs[b,k] = Σ_a pi_probs_k[b,k,a] · grad_pi_probs[b,a]
|
||||
if (c < N_ACTIONS) {
|
||||
for (int k = 0; k < K; ++k) {
|
||||
s_grad_pi_probs_k[k * N_ACTIONS + c] =
|
||||
s_gate_probs[k] * s_grad_pi_probs[c];
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Thread k computes Σ_a pi_probs_k[k,a] · grad_pi_probs[a] for its head.
|
||||
if (c < K) {
|
||||
float gp = 0.0f;
|
||||
#pragma unroll
|
||||
for (int a = 0; a < N_ACTIONS; ++a) {
|
||||
gp += s_pi_probs_k[c * N_ACTIONS + a] * s_grad_pi_probs[a];
|
||||
}
|
||||
grad_gate_probs[b * K + c] = gp;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Step 3: per-head softmax-Jacobian → grad_pi_logits_k ──────────
|
||||
// dot_k[b,k] = Σ_{a'} pi_probs_k[k,a'] · grad_pi_probs_k[k,a']
|
||||
if (c < K) {
|
||||
float dot = 0.0f;
|
||||
#pragma unroll
|
||||
for (int a = 0; a < N_ACTIONS; ++a) {
|
||||
dot += s_pi_probs_k[c * N_ACTIONS + a]
|
||||
* s_grad_pi_probs_k[c * N_ACTIONS + a];
|
||||
}
|
||||
s_dot_k[c] = dot;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (c < N_ACTIONS) {
|
||||
for (int k = 0; k < K; ++k) {
|
||||
const int kak = k * N_ACTIONS + c;
|
||||
const float gl = s_pi_probs_k[kak]
|
||||
* ( s_grad_pi_probs_k[kak] - s_dot_k[k] );
|
||||
s_grad_pi_logits_k[kak] = gl;
|
||||
// Persist to global memory for aux prior kernel + diagnostics.
|
||||
grad_pi_logits_k[(b * K + k) * N_ACTIONS + c] = gl;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Step 4: per-batch weight grads + grad_h_t ─────────────────────
|
||||
// Each thread c is the sole writer of:
|
||||
// grad_W_heads_pb[b, k, a, c] for all (k, a)
|
||||
// grad_h_t_scratch[b, c]
|
||||
// Threads c<K * N_ACTIONS additionally write grad_b_heads_pb[b, k, a].
|
||||
if (c < HIDDEN_DIM) {
|
||||
const float h_bc = h_t[b * HIDDEN_DIM + c];
|
||||
float gh_acc = 0.0f;
|
||||
for (int k = 0; k < K; ++k) {
|
||||
for (int a = 0; a < N_ACTIONS; ++a) {
|
||||
const int kak = k * N_ACTIONS + a;
|
||||
const float g = s_grad_pi_logits_k[kak];
|
||||
// grad_W_heads_pb[b, k, a, c] = g * h[b, c]
|
||||
grad_W_heads_pb[(long long)((b * K + k) * N_ACTIONS + a) * HIDDEN_DIM + c]
|
||||
= g * h_bc;
|
||||
// Accumulate grad_h_t[b, c] = Σ_{k,a} g · W_heads[k, a, c]
|
||||
gh_acc += g * W_heads[(long long)((k * N_ACTIONS) + a) * HIDDEN_DIM + c];
|
||||
}
|
||||
}
|
||||
// OVERWRITE — caller folds via grad_h_accumulate_scaled.
|
||||
grad_h_t_scratch[b * HIDDEN_DIM + c] = gh_acc;
|
||||
}
|
||||
|
||||
// Bias grads — sole-writer per (b, k, a). Use the first K*N_ACTIONS
|
||||
// threads to cover the per-(k, a) slots in one pass.
|
||||
if (c < K * N_ACTIONS) {
|
||||
const int k = c / N_ACTIONS;
|
||||
const int a = c % N_ACTIONS;
|
||||
grad_b_heads_pb[(b * K + k) * N_ACTIONS + a] =
|
||||
s_grad_pi_logits_k[k * N_ACTIONS + a];
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Kernel 2: backward through the gating softmax.
|
||||
//
|
||||
// Block layout:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (max(K, REGIME_DIM) = 8, 1, 1)
|
||||
//
|
||||
// Tiny block — K≤8 and REGIME_DIM=6. Thread layout is per-head for the
|
||||
// softmax-Jacobian reduction, per-regime-dim for the regime-grad output.
|
||||
// We use the same threads for both phases since both dimensions are ≤8.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void multi_head_policy_backward_gate(
|
||||
const float* __restrict__ grad_gate_probs, // [B × K] from kernel 1
|
||||
const float* __restrict__ gate_probs, // [B × K] forward output
|
||||
const float* __restrict__ regime_h, // [B × REGIME_DIM]
|
||||
const float* __restrict__ W_gate, // [K × REGIME_DIM]
|
||||
const int B,
|
||||
const int K,
|
||||
float* __restrict__ grad_gate_logits,// [B × K] OUT (for diag)
|
||||
float* __restrict__ grad_W_gate_pb, // [B × K × REGIME_DIM]
|
||||
float* __restrict__ grad_b_gate_pb, // [B × K]
|
||||
float* __restrict__ grad_regime_h // [B × REGIME_DIM] (computed; unused upstream)
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
if (b >= B) return;
|
||||
const int tid = threadIdx.x;
|
||||
|
||||
__shared__ float s_gate_probs[MAX_K_HEADS];
|
||||
__shared__ float s_grad_gate_probs[MAX_K_HEADS];
|
||||
__shared__ float s_grad_gate_logits[MAX_K_HEADS];
|
||||
__shared__ float s_dot; // Σ_{k'} gate · grad_gate_probs
|
||||
|
||||
// ── Stage forward + incoming grad ─────────────────────────────────
|
||||
if (tid < K) {
|
||||
s_gate_probs[tid] = gate_probs[b * K + tid];
|
||||
s_grad_gate_probs[tid] = grad_gate_probs[b * K + tid];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Softmax-Jacobian reduction (single thread for determinism) ────
|
||||
if (tid == 0) {
|
||||
float dot = 0.0f;
|
||||
for (int k = 0; k < K; ++k) {
|
||||
dot += s_gate_probs[k] * s_grad_gate_probs[k];
|
||||
}
|
||||
s_dot = dot;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── grad_gate_logits[b, k] = p · (dp − dot) ───────────────────────
|
||||
if (tid < K) {
|
||||
const float gl = s_gate_probs[tid] * (s_grad_gate_probs[tid] - s_dot);
|
||||
s_grad_gate_logits[tid] = gl;
|
||||
grad_gate_logits[b * K + tid] = gl;
|
||||
grad_b_gate_pb[b * K + tid] = gl;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── grad_W_gate_pb[b, k, r] = grad_gate_logits[k] · regime_h[r] ───
|
||||
// Cover all (k, r) slots — K*REGIME_DIM ≤ 48 — using a single loop
|
||||
// per active thread. Thread `tid` handles its column across all k.
|
||||
if (tid < REGIME_DIM) {
|
||||
const float r_b = regime_h[b * REGIME_DIM + tid];
|
||||
for (int k = 0; k < K; ++k) {
|
||||
grad_W_gate_pb[((b * K + k) * REGIME_DIM) + tid] =
|
||||
s_grad_gate_logits[k] * r_b;
|
||||
}
|
||||
}
|
||||
|
||||
// ── grad_regime_h[b, r] = Σ_k W_gate[k, r] · grad_gate_logits[k] ──
|
||||
// Computed but NOT propagated upstream in Phase 2A-B (regime_h is
|
||||
// loader-precomputed in foxhunt). Allocate + write the buffer so a
|
||||
// future phase can route it if needed.
|
||||
if (tid < REGIME_DIM) {
|
||||
float gr = 0.0f;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < K; ++k) {
|
||||
gr += W_gate[k * REGIME_DIM + tid] * s_grad_gate_logits[k];
|
||||
}
|
||||
grad_regime_h[b * REGIME_DIM + tid] = gr;
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// multi_head_policy_forward.cu — K-head policy logit + mixture combination.
|
||||
//
|
||||
// Spec: docs/superpowers/specs/2026-06-02-multi-head-policy-with-r-multiple.md
|
||||
// ADDENDUM 2026-06-03 §R.6
|
||||
// Plan: docs/superpowers/plans/2026-06-03-multi-head-policy-implementation.md (Phase 2A-A)
|
||||
//
|
||||
// Per-(batch, head) computes pi_logits_k = W_k @ h_t + b_k (HIDDEN_DIM=128 → N_ACTIONS=11).
|
||||
// Then per-batch computes the mixture:
|
||||
// pi_probs_k[b, k, a] = softmax_a(pi_logits_k[b, k, :])
|
||||
// pi_probs[b, a] = Σ_k gate_probs[b, k] · pi_probs_k[b, k, a]
|
||||
// pi_logits[b, a] = log(pi_probs[b, a] + 1e-12)
|
||||
//
|
||||
// Deterministic by construction: per-batch sequential mixture sum (no parallel
|
||||
// reductions across batches; the only reduction is over K=3 heads with fixed
|
||||
// summation order). Sole-writer per output cell.
|
||||
//
|
||||
// Block layout:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (N_ACTIONS = 11, 1, 1)
|
||||
// One block per batch. N_ACTIONS threads. Each thread owns one action across
|
||||
// all K heads; loops K times for per-head matmul + softmax + mixture combine.
|
||||
//
|
||||
// Per feedback_no_atomicadd: no atomic ops.
|
||||
// Per feedback_no_nvrtc: pre-compiled cubin via build.rs.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define HIDDEN_DIM 128
|
||||
#define N_ACTIONS 11
|
||||
#define MAX_K_HEADS 8 // Compile-time max; runtime K from ISV
|
||||
|
||||
extern "C" __global__ void multi_head_policy_forward(
|
||||
const float* __restrict__ h_t, // [B × HIDDEN_DIM]
|
||||
const float* __restrict__ W_heads, // [K × N_ACTIONS × HIDDEN_DIM]
|
||||
const float* __restrict__ b_heads, // [K × N_ACTIONS]
|
||||
const float* __restrict__ gate_probs, // [B × K] — already softmax'd
|
||||
float* __restrict__ pi_logits_k, // [B × K × N_ACTIONS] (output, for backward)
|
||||
float* __restrict__ pi_probs_k, // [B × K × N_ACTIONS] (output, for backward)
|
||||
float* __restrict__ pi_logits, // [B × N_ACTIONS] (output, mixture)
|
||||
const int B,
|
||||
const int K
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
if (b >= B) return;
|
||||
|
||||
const int tid = threadIdx.x; // 0..N_ACTIONS-1
|
||||
|
||||
__shared__ float s_pi_logits_k[MAX_K_HEADS * N_ACTIONS];
|
||||
__shared__ float s_pi_probs_k[MAX_K_HEADS * N_ACTIONS];
|
||||
__shared__ float s_pi_probs[N_ACTIONS];
|
||||
__shared__ float s_max_k[MAX_K_HEADS]; // for softmax stability
|
||||
__shared__ float s_sum_k[MAX_K_HEADS];
|
||||
|
||||
// ── Step 1: per-head linear projection ───────────────────────────────
|
||||
// pi_logits_k[b, k, a] = Σ_j W_heads[k, a, j] × h_t[b, j] + b_heads[k, a]
|
||||
// One thread per action; loops over k inside thread.
|
||||
if (tid < N_ACTIONS) {
|
||||
for (int k = 0; k < K; ++k) {
|
||||
float gl = b_heads[k * N_ACTIONS + tid];
|
||||
#pragma unroll
|
||||
for (int j = 0; j < HIDDEN_DIM; ++j) {
|
||||
gl += W_heads[(k * N_ACTIONS + tid) * HIDDEN_DIM + j]
|
||||
* h_t[b * HIDDEN_DIM + j];
|
||||
}
|
||||
s_pi_logits_k[k * N_ACTIONS + tid] = gl;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Step 2: per-head softmax (max-subtract for stability) ────────────
|
||||
// Threads 0..K-1 compute per-head max over the N_ACTIONS logits.
|
||||
if (tid < K) {
|
||||
float mx = s_pi_logits_k[tid * N_ACTIONS];
|
||||
#pragma unroll
|
||||
for (int a = 1; a < N_ACTIONS; ++a) {
|
||||
float v = s_pi_logits_k[tid * N_ACTIONS + a];
|
||||
mx = fmaxf(mx, v);
|
||||
}
|
||||
s_max_k[tid] = mx;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Each action-thread exponentiates its slot for every head.
|
||||
if (tid < N_ACTIONS) {
|
||||
for (int k = 0; k < K; ++k) {
|
||||
float v = expf(s_pi_logits_k[k * N_ACTIONS + tid] - s_max_k[k]);
|
||||
s_pi_probs_k[k * N_ACTIONS + tid] = v;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Threads 0..K-1 reduce per-head sum (denominator).
|
||||
if (tid < K) {
|
||||
float sm = 0.0f;
|
||||
#pragma unroll
|
||||
for (int a = 0; a < N_ACTIONS; ++a) {
|
||||
sm += s_pi_probs_k[tid * N_ACTIONS + a];
|
||||
}
|
||||
s_sum_k[tid] = sm;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Normalize.
|
||||
if (tid < N_ACTIONS) {
|
||||
for (int k = 0; k < K; ++k) {
|
||||
s_pi_probs_k[k * N_ACTIONS + tid] /= s_sum_k[k];
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Step 3: mixture ──────────────────────────────────────────────────
|
||||
// pi_probs[b, a] = Σ_k gate_probs[b, k] · pi_probs_k[k, a]
|
||||
// Sequential summation order over K (deterministic).
|
||||
if (tid < N_ACTIONS) {
|
||||
float p = 0.0f;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < K; ++k) {
|
||||
p += gate_probs[b * K + k] * s_pi_probs_k[k * N_ACTIONS + tid];
|
||||
}
|
||||
s_pi_probs[tid] = p;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Step 4: write outputs ────────────────────────────────────────────
|
||||
// pi_logits[b, a] = log(pi_probs[b, a] + 1e-12) for downstream softmax callers.
|
||||
// Also stash per-head logits + probs for the Phase 2A-B backward kernel.
|
||||
if (tid < N_ACTIONS) {
|
||||
pi_logits[b * N_ACTIONS + tid] = logf(s_pi_probs[tid] + 1e-12f);
|
||||
for (int k = 0; k < K; ++k) {
|
||||
pi_logits_k[(b * K + k) * N_ACTIONS + tid] =
|
||||
s_pi_logits_k[k * N_ACTIONS + tid];
|
||||
pi_probs_k[(b * K + k) * N_ACTIONS + tid] =
|
||||
s_pi_probs_k[k * N_ACTIONS + tid];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// multi_head_policy_gate_forward.cu — gating head from raw regime features.
|
||||
//
|
||||
// Spec: docs/superpowers/specs/2026-06-02-multi-head-policy-with-r-multiple.md
|
||||
// ADDENDUM 2026-06-03 §R.3 Option B (regime-direct gating, bypass VSN)
|
||||
// Plan: docs/superpowers/plans/2026-06-03-multi-head-policy-implementation.md (Phase 2A-A)
|
||||
//
|
||||
// gate_logits[b, k] = Σ_j W_gate[k, j] × regime_h[b, j] + b_gate[k]
|
||||
// gate_probs[b, k] = softmax_k(gate_logits[b])
|
||||
//
|
||||
// Reads regime_h DIRECTLY (parallel channel — does NOT go through VSN/Mamba2).
|
||||
// This is the load-bearing architectural choice motivated by the empirical
|
||||
// regime-attenuation finding (pearl_local_smoke_noise_floor_and_regime_concentration):
|
||||
// VSN's softmax-over-40 gating structurally limits any single encoder-input
|
||||
// feature to ~1/40 of attention budget, suppressing vol-axis routing. The gate
|
||||
// reads regime[0..6] without that bottleneck.
|
||||
//
|
||||
// Block layout:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (K, 1, 1) — typically K=3, ≤ MAX_K_HEADS=8
|
||||
// One block per batch. K threads cooperate via shared-mem reductions.
|
||||
//
|
||||
// Per feedback_no_atomicadd: no atomic ops.
|
||||
// Per feedback_no_nvrtc: pre-compiled cubin via build.rs.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define REGIME_DIM 6
|
||||
#define MAX_K_HEADS 8
|
||||
|
||||
extern "C" __global__ void multi_head_policy_gate_forward(
|
||||
const float* __restrict__ regime_h, // [B × REGIME_DIM=6]
|
||||
const float* __restrict__ W_gate, // [K × REGIME_DIM]
|
||||
const float* __restrict__ b_gate, // [K]
|
||||
float* __restrict__ gate_logits, // [B × K] (output, for backward)
|
||||
float* __restrict__ gate_probs, // [B × K] (output, post-softmax)
|
||||
const int B,
|
||||
const int K
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
if (b >= B) return;
|
||||
|
||||
const int tid = threadIdx.x;
|
||||
__shared__ float s_logits[MAX_K_HEADS]; // pre-softmax logits
|
||||
__shared__ float s_exp[MAX_K_HEADS]; // exp(logit - max) for softmax
|
||||
__shared__ float s_max;
|
||||
__shared__ float s_sum;
|
||||
|
||||
// ── Step 1: linear projection ────────────────────────────────────────
|
||||
// gate_logits[b, k] = Σ_j W_gate[k, j] × regime_h[b, j] + b_gate[k]
|
||||
// One thread per head k.
|
||||
if (tid < K) {
|
||||
float gl = b_gate[tid];
|
||||
#pragma unroll
|
||||
for (int j = 0; j < REGIME_DIM; ++j) {
|
||||
gl += W_gate[tid * REGIME_DIM + j] * regime_h[b * REGIME_DIM + j];
|
||||
}
|
||||
s_logits[tid] = gl;
|
||||
// Persist pre-softmax logits to gmem for the Phase 2A-B backward kernel.
|
||||
gate_logits[b * K + tid] = gl;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Step 2: softmax (max-subtract for stability) ─────────────────────
|
||||
if (tid == 0) {
|
||||
float mx = s_logits[0];
|
||||
for (int k = 1; k < K; ++k) {
|
||||
mx = fmaxf(mx, s_logits[k]);
|
||||
}
|
||||
s_max = mx;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (tid < K) {
|
||||
s_exp[tid] = expf(s_logits[tid] - s_max);
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (tid == 0) {
|
||||
float sm = 0.0f;
|
||||
for (int k = 0; k < K; ++k) {
|
||||
sm += s_exp[k];
|
||||
}
|
||||
s_sum = sm;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (tid < K) {
|
||||
gate_probs[b * K + tid] = s_exp[tid] / s_sum;
|
||||
}
|
||||
}
|
||||
@@ -1,818 +0,0 @@
|
||||
// multi_horizon_heads.cu
|
||||
//
|
||||
// 128-dim hidden -> N_HORIZONS sigmoid logits, one per horizon
|
||||
// (post-2026-05-22 rebase: 3 horizons {10, 100, 1000} snapshots forward).
|
||||
// Single-block kernel, N_HORIZONS threads (one per head). Each thread
|
||||
// computes its own dot product against the 128-dim hidden vector + bias,
|
||||
// then sigmoid.
|
||||
|
||||
// File-wide layout constants (must agree with
|
||||
// `crates/ml-alpha/src/heads.rs::{N_HORIZONS, HIDDEN_DIM, HEAD_MID_DIM}`).
|
||||
#define N_HORIZONS_H 3
|
||||
#define HIDDEN_H 128
|
||||
#define HEAD_MID_H 64
|
||||
#define GELU_C0 0.7978845608f // sqrt(2/pi)
|
||||
#define GELU_C1 0.044715f
|
||||
|
||||
extern "C" __global__ void multi_horizon_heads(
|
||||
const float* __restrict__ w, // [N_HORIZONS, 128]
|
||||
const float* __restrict__ b, // [N_HORIZONS]
|
||||
const float* __restrict__ h, // [128]
|
||||
float* __restrict__ probs // [N_HORIZONS]
|
||||
) {
|
||||
int k = threadIdx.x;
|
||||
if (k >= N_HORIZONS_H) return;
|
||||
float z = b[k];
|
||||
for (int i = 0; i < 128; ++i) {
|
||||
z += w[k * 128 + i] * h[i];
|
||||
}
|
||||
probs[k] = 1.0f / (1.0f + expf(-z));
|
||||
}
|
||||
|
||||
// Backward through multi_horizon_heads.
|
||||
//
|
||||
// Given:
|
||||
// grad_probs[N_HORIZONS] = dL / dp
|
||||
// probs[N_HORIZONS] = forward output (sigmoid)
|
||||
// h[128] = forward input
|
||||
// Computes:
|
||||
// grad_w[N_HORIZONS, 128] = dL / dW = d_z[k] * h[i]
|
||||
// grad_b[N_HORIZONS] = dL / db = d_z[k]
|
||||
// grad_h[128] = dL / dh = sum_k d_z[k] * W[k, i]
|
||||
// where d_z[k] = grad_probs[k] * p[k] * (1 - p[k]).
|
||||
//
|
||||
// Block tree-reduce for the grad_h sum (no atomicAdd). One thread per
|
||||
// hidden unit (128 threads); thread 0 also writes grad_b.
|
||||
|
||||
extern "C" __global__ void multi_horizon_heads_backward(
|
||||
const float* __restrict__ w, // [N_HORIZONS, 128]
|
||||
const float* __restrict__ probs, // [N_HORIZONS]
|
||||
const float* __restrict__ h, // [128]
|
||||
const float* __restrict__ grad_probs, // [N_HORIZONS]
|
||||
const float* __restrict__ grad_h_carry, // [128] — optional carry (nullptr OK)
|
||||
float* __restrict__ grad_w, // [N_HORIZONS, 128] (+=)
|
||||
float* __restrict__ grad_b, // [N_HORIZONS] (+=)
|
||||
float* __restrict__ grad_h // [128] overwrite-with-carry
|
||||
) {
|
||||
__shared__ float d_z[N_HORIZONS_H];
|
||||
|
||||
int tid = threadIdx.x;
|
||||
if (tid < N_HORIZONS_H) {
|
||||
const float p = probs[tid];
|
||||
d_z[tid] = grad_probs[tid] * p * (1.0f - p);
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Parameter-grad writes use `+=`: per-position supervision invokes
|
||||
// this kernel K times per training step and needs the gradients
|
||||
// accumulated. Callers MUST pre-zero grad_w / grad_b at step start.
|
||||
if (tid < N_HORIZONS_H) {
|
||||
grad_b[tid] += d_z[tid];
|
||||
}
|
||||
// grad_w[k, i] += d_z[k] * h[i]
|
||||
// grid stride: each thread covers one i for k in 0..N_HORIZONS.
|
||||
if (tid < 128) {
|
||||
const float h_i = h[tid];
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
grad_w[k * 128 + tid] += d_z[k] * h_i;
|
||||
}
|
||||
// grad_h[i] = sum_k d_z[k] * W[k, i] + carry[i]
|
||||
// The optional carry is the grad_h_old emitted by the cfc_step_bwd
|
||||
// of position k+1 — when the CfC is RECURRENT across positions,
|
||||
// h_new_k flows into h_old_{k+1}, so grad_h_new at position k must
|
||||
// sum the heads-side gradient with the carry from the upstream step.
|
||||
float acc = 0.0f;
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
acc += d_z[k] * w[k * 128 + tid];
|
||||
}
|
||||
const float carry = (grad_h_carry != nullptr) ? grad_h_carry[tid] : 0.0f;
|
||||
grad_h[tid] = acc + carry;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─── Batched variants ────────────────────────────────────────────────
|
||||
// Process N_BATCH samples per launch. Layout: h[B, 128], probs[B, N_HORIZONS].
|
||||
// Thread tid handles either head k (tid < N_HORIZONS) or hidden unit i
|
||||
// (tid < 128). Block dim = 128, one block per launch — each thread loops
|
||||
// over b internally. Param grads (grad_w, grad_b) shared across batch and
|
||||
// accumulated via `+=` (no race: thread tid is sole writer to its row).
|
||||
|
||||
extern "C" __global__ void multi_horizon_heads_batched(
|
||||
const float* __restrict__ w, // [N_HORIZONS, 128]
|
||||
const float* __restrict__ b, // [N_HORIZONS]
|
||||
const float* __restrict__ h, // [n_batch, 128]
|
||||
int n_batch,
|
||||
float* __restrict__ probs // [n_batch, N_HORIZONS]
|
||||
) {
|
||||
int k = threadIdx.x;
|
||||
if (k >= N_HORIZONS_H) return;
|
||||
const float bias_k = b[k];
|
||||
for (int bi = 0; bi < n_batch; ++bi) {
|
||||
const float* h_b = h + (long long)bi * 128;
|
||||
float z = bias_k;
|
||||
for (int i = 0; i < 128; ++i) z += w[k * 128 + i] * h_b[i];
|
||||
probs[(long long)bi * N_HORIZONS_H + k] = 1.0f / (1.0f + expf(-z));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extern "C" __global__ void multi_horizon_heads_backward_batched(
|
||||
const float* __restrict__ w, // [N_HORIZONS, 128]
|
||||
const float* __restrict__ probs, // [n_batch, N_HORIZONS]
|
||||
const float* __restrict__ h, // [n_batch, 128]
|
||||
const float* __restrict__ grad_probs, // [n_batch, N_HORIZONS]
|
||||
const float* __restrict__ grad_h_carry, // [n_batch, 128] (nullptr OK)
|
||||
const float* __restrict__ lambda, // [N_HORIZONS] — per-horizon trunk-grad scaler
|
||||
int n_batch,
|
||||
float* __restrict__ grad_w, // [N_HORIZONS, 128] accum +=
|
||||
float* __restrict__ grad_b, // [N_HORIZONS] accum +=
|
||||
float* __restrict__ grad_h // [n_batch, 128] overwrite + carry
|
||||
) {
|
||||
// Shared mem: d_z[B, N_HORIZONS]. At B=32: 32 * 3 = 96 floats = 384 bytes.
|
||||
extern __shared__ float sd_z[]; // size = n_batch * N_HORIZONS_H
|
||||
|
||||
int tid = threadIdx.x;
|
||||
|
||||
// Pass 1: per-(bi, k) compute d_z and stash in shared.
|
||||
if (tid < N_HORIZONS_H) {
|
||||
for (int bi = 0; bi < n_batch; ++bi) {
|
||||
const float p = probs[(long long)bi * N_HORIZONS_H + tid];
|
||||
const float dp = grad_probs[(long long)bi * N_HORIZONS_H + tid];
|
||||
sd_z[(long long)bi * N_HORIZONS_H + tid] = dp * p * (1.0f - p);
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Per-horizon trunk-gradient scaler, broadcast via shared mem so
|
||||
// every thread reads the same N_HORIZONS floats without repeated global loads.
|
||||
__shared__ float s_lambda[N_HORIZONS_H];
|
||||
if (tid < N_HORIZONS_H) {
|
||||
// Sentinel: a fully zero lambda buffer (initial state, before
|
||||
// the EMA kernel has run even once) is treated as "scaler = 1"
|
||||
// so the kernel reduces to its pre-ISV behavior. After step 1
|
||||
// every entry is clamped to [0.5, 2.0] by horizon_lambda.cu
|
||||
// and the > 0 check is always true.
|
||||
const float l = lambda[tid];
|
||||
s_lambda[tid] = (l > 0.0f) ? l : 1.0f;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// grad_b[k] += sum_b d_z[b, k]. Thread k (k < N_HORIZONS) is sole writer.
|
||||
// NOTE: lambda does NOT scale the head's own bias gradient — only
|
||||
// the trunk gradient. We want the per-horizon head to keep learning
|
||||
// its own bias normally; lambda only biases how much the horizon's
|
||||
// error signal LEAKS into the shared trunk.
|
||||
if (tid < N_HORIZONS_H) {
|
||||
float acc = 0.0f;
|
||||
for (int bi = 0; bi < n_batch; ++bi) {
|
||||
acc += sd_z[(long long)bi * N_HORIZONS_H + tid];
|
||||
}
|
||||
grad_b[tid] += acc;
|
||||
}
|
||||
|
||||
// grad_w[k, i] += sum_b d_z[b, k] * h[b, i]. Same rationale as
|
||||
// grad_b: lambda does NOT scale the head's own weight gradient.
|
||||
if (tid < 128) {
|
||||
// grad_h: sum_k LAMBDA[k] * d_z[b, k] * W[k, i] for each b,
|
||||
// plus optional carry. This is the only path where lambda is
|
||||
// applied — per `pearl_adam_normalizes_loss_weights.md` the
|
||||
// effective lever for horizon-aware learning is the trunk
|
||||
// gradient, not the head loss weight.
|
||||
for (int bi = 0; bi < n_batch; ++bi) {
|
||||
const float h_bi = h[(long long)bi * 128 + tid];
|
||||
// Accumulate W-row grads via += into global (thread tid sole writer of column tid).
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
grad_w[k * 128 + tid] += sd_z[(long long)bi * N_HORIZONS_H + k] * h_bi;
|
||||
}
|
||||
float acc = 0.0f;
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
acc += s_lambda[k] * sd_z[(long long)bi * N_HORIZONS_H + k] * w[k * 128 + tid];
|
||||
}
|
||||
const float carry = (grad_h_carry != nullptr)
|
||||
? grad_h_carry[(long long)bi * 128 + tid] : 0.0f;
|
||||
grad_h[(long long)bi * 128 + tid] = acc + carry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Phase 1: 2-layer MLP heads — per-horizon [HIDDEN=128 → HEAD_MID=64]
|
||||
// GELU hidden → [HEAD_MID=64 → 1] sigmoid output.
|
||||
//
|
||||
// Forward layout:
|
||||
// w1 [N_HORIZONS, HEAD_MID, HIDDEN] per-horizon first-layer weights
|
||||
// b1 [N_HORIZONS, HEAD_MID] per-horizon first-layer biases
|
||||
// w2 [N_HORIZONS, HEAD_MID] per-horizon output weights (scalar per horizon)
|
||||
// b2 [N_HORIZONS] per-horizon output bias
|
||||
// h [n_batch, HIDDEN] trunk output (LayerNorm-normalised)
|
||||
// probs [n_batch, N_HORIZONS] sigmoid output
|
||||
// z1_out [n_batch, N_HORIZONS, HEAD_MID] saved pre-GELU for backward
|
||||
// a1_out [n_batch, N_HORIZONS, HEAD_MID] saved post-GELU for backward
|
||||
//
|
||||
// GELU(x) ≈ 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
|
||||
//
|
||||
// Launch: grid=(n_batch, 1, 1), block=(HEAD_MID, 1, 1).
|
||||
// Each block handles ONE sample and loops over the N_HORIZONS horizons.
|
||||
|
||||
__device__ __forceinline__ float gelu_act(float x) {
|
||||
const float u = GELU_C0 * (x + GELU_C1 * x * x * x);
|
||||
return 0.5f * x * (1.0f + tanhf(u));
|
||||
}
|
||||
|
||||
extern "C" __global__ void multi_horizon_heads_2layer_fwd_batched(
|
||||
const float* __restrict__ w1, // [N_HORIZONS, HEAD_MID, HIDDEN]
|
||||
const float* __restrict__ b1, // [N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ w2, // [N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ b2, // [N_HORIZONS]
|
||||
const float* __restrict__ h, // [n_batch, HIDDEN]
|
||||
int n_batch,
|
||||
float* __restrict__ probs, // [n_batch, N_HORIZONS]
|
||||
float* __restrict__ z1_out, // [n_batch, N_HORIZONS, HEAD_MID]
|
||||
float* __restrict__ a1_out // [n_batch, N_HORIZONS, HEAD_MID]
|
||||
) {
|
||||
int b_idx = blockIdx.x;
|
||||
int m = threadIdx.x;
|
||||
if (b_idx >= n_batch || m >= HEAD_MID_H) return;
|
||||
|
||||
const float* h_row = h + (long long)b_idx * HIDDEN_H;
|
||||
|
||||
// Shared per-horizon post-GELU activations [N_HORIZONS_H * 64], indexed as [k*64 + m].
|
||||
__shared__ float s_a1[N_HORIZONS_H * HEAD_MID_H];
|
||||
|
||||
// Pass 1: per-horizon hidden unit. Thread `m` computes the m-th
|
||||
// hidden unit for each of the N_HORIZONS horizons sequentially.
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
float z = b1[k * HEAD_MID_H + m];
|
||||
for (int i = 0; i < HIDDEN_H; ++i) {
|
||||
z += w1[((long long)(k * HEAD_MID_H) + m) * HIDDEN_H + i] * h_row[i];
|
||||
}
|
||||
const float a = gelu_act(z);
|
||||
s_a1[k * HEAD_MID_H + m] = a;
|
||||
z1_out[((long long)b_idx * N_HORIZONS_H + k) * HEAD_MID_H + m] = z;
|
||||
a1_out[((long long)b_idx * N_HORIZONS_H + k) * HEAD_MID_H + m] = a;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 2: per-horizon output logit. Threads m=0..(N_HORIZONS-1) each
|
||||
// handle one horizon by computing the dot product over a1[k, :] and W2[k, :].
|
||||
if (m < N_HORIZONS_H) {
|
||||
const int k = m;
|
||||
float z2 = b2[k];
|
||||
#pragma unroll
|
||||
for (int u = 0; u < HEAD_MID_H; ++u) {
|
||||
z2 += w2[k * HEAD_MID_H + u] * s_a1[k * HEAD_MID_H + u];
|
||||
}
|
||||
probs[(long long)b_idx * N_HORIZONS_H + k] = 1.0f / (1.0f + expf(-z2));
|
||||
}
|
||||
}
|
||||
|
||||
// 2-layer heads backward — chain rule through sigmoid → linear →
|
||||
// GELU → linear → trunk. ISV `lambda[N_HORIZONS]` scales the trunk-grad
|
||||
// contribution per horizon (per `pearl_adam_normalizes_loss_weights.md`
|
||||
// the effective lever is the trunk gradient, not loss weight).
|
||||
//
|
||||
// Chain rule:
|
||||
// d_z2[k] = grad_probs[k] * p * (1 - p)
|
||||
// d_a1[k, m] = d_z2[k] * w2[k, m]
|
||||
// d_z1[k, m] = d_a1[k, m] * gelu_prime(z1[k, m])
|
||||
// grad_w2[k, m] += d_z2[k] * a1[k, m]
|
||||
// grad_b2[k] += d_z2[k]
|
||||
// grad_w1[k, m, i] += d_z1[k, m] * h[i]
|
||||
// grad_b1[k, m] += d_z1[k, m]
|
||||
// grad_h[i] = sum_{k, m} lambda[k] * d_z1[k, m] * w1[k, m, i] + carry[i]
|
||||
//
|
||||
// Single-writer discipline: thread tid (< HIDDEN) is the sole writer
|
||||
// of grad_h[bi, tid] and grad_w1[k, m, tid] (across all k, m). Both
|
||||
// the per-block sample loop and the per-k loop are serial within one
|
||||
// thread; no cross-block races since each block handles ONE sample.
|
||||
// Same for grad_w2[k, m] / grad_b2[k] / grad_b1[k, m]: each is written
|
||||
// by exactly one thread per block (m for grad_w2 / grad_b1 dims, m<N_HORIZONS
|
||||
// for grad_b2). grad_w1 / grad_b1 / grad_w2 / grad_b2 accumulate via
|
||||
// += across the n_batch samples (multiple blocks); single-writer per
|
||||
// (block, output) holds within a block but blocks race on the +=.
|
||||
//
|
||||
// Resolution: launch ONE block per launch (not one per sample) and
|
||||
// iterate over n_batch internally; this matches the existing
|
||||
// single-layer kernel pattern. Block dim = HEAD_MID = 64; threads
|
||||
// also tile over HIDDEN for grad_h / grad_w1 columns (HEAD_MID=64 <
|
||||
// HIDDEN=128 so we serialise across 2 HIDDEN-column groups).
|
||||
//
|
||||
// Layout for the per-sample loop:
|
||||
// - threads 0..63 each own one (m) hidden-unit dimension.
|
||||
// - grad_b2[k]: thread m==k for k in 0..N_HORIZONS (within m<N_HORIZONS group).
|
||||
// - grad_w2[k, m]: thread m writes column m for all k = serialised inside thread.
|
||||
// - grad_b1[k, m]: thread m writes (k, m) for all k = serialised inside thread.
|
||||
// - grad_w1[k, m, i] += d_z1[k, m] * h[bi, i]: the i dimension is
|
||||
// HIDDEN=128; thread m needs to write to column i for ALL i.
|
||||
// Each thread loops over i; threads contend on the same row (k, m)
|
||||
// but write to DIFFERENT columns i — no race.
|
||||
// - grad_h[bi, i]: thread responsible for column i. We tile i over
|
||||
// the 64 threads in 2 strides (i = m, m+64).
|
||||
|
||||
__device__ __forceinline__ float gelu_prime(float x) {
|
||||
const float u = GELU_C0 * (x + GELU_C1 * x * x * x);
|
||||
const float t = tanhf(u);
|
||||
const float dut = GELU_C0 * (1.0f + 3.0f * GELU_C1 * x * x);
|
||||
return 0.5f * (1.0f + t) + 0.5f * x * (1.0f - t * t) * dut;
|
||||
}
|
||||
|
||||
extern "C" __global__ void multi_horizon_heads_2layer_bwd_batched(
|
||||
const float* __restrict__ w1, // [N_HORIZONS, HEAD_MID, HIDDEN]
|
||||
const float* __restrict__ w2, // [N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ probs, // [n_batch, N_HORIZONS]
|
||||
const float* __restrict__ grad_probs, // [n_batch, N_HORIZONS]
|
||||
const float* __restrict__ z1, // [n_batch, N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ a1, // [n_batch, N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ h, // [n_batch, HIDDEN]
|
||||
const float* __restrict__ grad_h_carry, // [n_batch, HIDDEN] (nullptr OK)
|
||||
const float* __restrict__ lambda, // [N_HORIZONS]
|
||||
int n_batch,
|
||||
float* __restrict__ grad_w1, // [N_HORIZONS, HEAD_MID, HIDDEN] (+=)
|
||||
float* __restrict__ grad_b1, // [N_HORIZONS, HEAD_MID] (+=)
|
||||
float* __restrict__ grad_w2, // [N_HORIZONS, HEAD_MID] (+=)
|
||||
float* __restrict__ grad_b2, // [N_HORIZONS] (+=)
|
||||
float* __restrict__ grad_h // [n_batch, HIDDEN] (overwrite + carry)
|
||||
) {
|
||||
int tid = threadIdx.x;
|
||||
if (tid >= HEAD_MID_H) return;
|
||||
|
||||
// Broadcast lambda[N_HORIZONS] via shared mem (N_HORIZONS floats).
|
||||
__shared__ float s_lambda[N_HORIZONS_H];
|
||||
if (tid < N_HORIZONS_H) {
|
||||
const float l = lambda[tid];
|
||||
s_lambda[tid] = (l > 0.0f) ? l : 1.0f; // sentinel: zero buffer → 1.0
|
||||
}
|
||||
|
||||
// Shared mem for d_z1[k, m] and d_z2[k] per sample.
|
||||
__shared__ float s_d_z1[N_HORIZONS_H * HEAD_MID_H];
|
||||
__shared__ float s_d_z2[N_HORIZONS_H];
|
||||
__syncthreads();
|
||||
|
||||
for (int bi = 0; bi < n_batch; ++bi) {
|
||||
// Pass 1: compute d_z2[k] for k=0..(N_HORIZONS-1) (only first N_HORIZONS threads).
|
||||
if (tid < N_HORIZONS_H) {
|
||||
const int k = tid;
|
||||
const float p = probs[(long long)bi * N_HORIZONS_H + k];
|
||||
const float dp = grad_probs[(long long)bi * N_HORIZONS_H + k];
|
||||
s_d_z2[k] = dp * p * (1.0f - p);
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 2: per-(k, m) compute d_a1 and d_z1, stash in shared.
|
||||
// Also accumulate grad_w2[k, m] += d_z2[k] * a1[bi, k, m] and
|
||||
// grad_b1[k, m] += d_z1[k, m]. Thread m is sole writer.
|
||||
const int m = tid;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
const float d_z2 = s_d_z2[k];
|
||||
const float w2_km = w2[k * HEAD_MID_H + m];
|
||||
const float d_a1 = d_z2 * w2_km;
|
||||
const float z1_km = z1[((long long)bi * N_HORIZONS_H + k) * HEAD_MID_H + m];
|
||||
const float a1_km = a1[((long long)bi * N_HORIZONS_H + k) * HEAD_MID_H + m];
|
||||
const float d_z1_km = d_a1 * gelu_prime(z1_km);
|
||||
s_d_z1[k * HEAD_MID_H + m] = d_z1_km;
|
||||
|
||||
// grad_w2[k, m] += d_z2[k] * a1[k, m]
|
||||
grad_w2[k * HEAD_MID_H + m] += d_z2 * a1_km;
|
||||
// grad_b1[k, m] += d_z1[k, m]
|
||||
grad_b1[k * HEAD_MID_H + m] += d_z1_km;
|
||||
}
|
||||
// grad_b2[k] (only first N_HORIZONS threads).
|
||||
if (tid < N_HORIZONS_H) {
|
||||
grad_b2[tid] += s_d_z2[tid];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 3: grad_w1[k, m, i] += d_z1[k, m] * h[bi, i]
|
||||
// and grad_h[bi, i] = sum_{k, m} lambda[k] * d_z1[k, m] * w1[k, m, i] + carry[i]
|
||||
//
|
||||
// Thread m owns row (k, m) of grad_w1 for ALL k — loops over
|
||||
// HIDDEN cols i serially. Thread m also computes grad_h[bi, i]
|
||||
// for i = m and i = m + HEAD_MID (since HIDDEN = 2 * HEAD_MID).
|
||||
//
|
||||
// Two passes for grad_h tiling: i = tid (0..63), then i = tid + 64 (64..127).
|
||||
// Thread for column i is the sole writer.
|
||||
const float* h_row = h + (long long)bi * HIDDEN_H;
|
||||
|
||||
// grad_w1: thread m writes (k, m, i) for all (k, i). Loop over i.
|
||||
for (int i = 0; i < HIDDEN_H; ++i) {
|
||||
const float h_bi = h_row[i];
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
grad_w1[((long long)(k * HEAD_MID_H) + m) * HIDDEN_H + i] +=
|
||||
s_d_z1[k * HEAD_MID_H + m] * h_bi;
|
||||
}
|
||||
}
|
||||
|
||||
// grad_h[bi, i] — tile i over 2 strides of HEAD_MID.
|
||||
// Thread tid handles columns i = tid and i = tid + HEAD_MID.
|
||||
#pragma unroll
|
||||
for (int tile = 0; tile < HIDDEN_H / HEAD_MID_H; ++tile) {
|
||||
const int i = tile * HEAD_MID_H + tid;
|
||||
float acc = 0.0f;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
#pragma unroll
|
||||
for (int mm = 0; mm < HEAD_MID_H; ++mm) {
|
||||
acc += s_lambda[k]
|
||||
* s_d_z1[k * HEAD_MID_H + mm]
|
||||
* w1[((long long)(k * HEAD_MID_H) + mm) * HIDDEN_H + i];
|
||||
}
|
||||
}
|
||||
const float carry = (grad_h_carry != nullptr)
|
||||
? grad_h_carry[(long long)bi * HIDDEN_H + i] : 0.0f;
|
||||
grad_h[(long long)bi * HIDDEN_H + i] = acc + carry;
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// TFT Gated Residual Network (GRN) heads — Phase 1.7 (2026-05-17)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Per-horizon GRN structure (Lim et al. 2021 §3.3, adapted to scalar
|
||||
// output per horizon):
|
||||
//
|
||||
// eta_2[k, m_out] = GELU(W1[k, m_out, i] @ h[i] + b1[k, m_out]) # [HIDDEN] → [HEAD_MID]
|
||||
// eta_1[k, m_out] = W2[k, m_out, m_in] @ eta_2[k, m_in] + b2[...] # [HEAD_MID] → [HEAD_MID]
|
||||
// gate_lin[k] = W_gate[k, m] @ eta_1[k, m] + b_gate[k] # [HEAD_MID] → scalar
|
||||
// main[k] = W_main[k, m] @ eta_1[k, m] + b_main[k]
|
||||
// skip[k] = W_skip[k, i] @ h[i] + b_skip[k] # [HIDDEN] → scalar
|
||||
// logit[k] = skip[k] + sigmoid(gate_lin[k]) * main[k]
|
||||
// p[k] = sigmoid(logit[k])
|
||||
//
|
||||
// Naming convention follows the existing 2-layer kernel: w1 = first
|
||||
// linear (HIDDEN → HEAD_MID, GELU-activated), w2 = second linear
|
||||
// (HEAD_MID → HEAD_MID, no activation; produces eta_1).
|
||||
//
|
||||
// Thread layout (block_dim = HEAD_MID = 64):
|
||||
// m = threadIdx.x -- owns "output_row" dim of (k, m_out=m) for w1/w2/eta_1/eta_2
|
||||
// for skip / gate / main scalars: thread tid<N_HORIZONS handles horizon k=tid
|
||||
|
||||
extern "C" __global__ void multi_horizon_heads_grn_fwd_batched(
|
||||
const float* __restrict__ w1, // [N_HORIZONS, HEAD_MID, HIDDEN]
|
||||
const float* __restrict__ b1, // [N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ w2, // [N_HORIZONS, HEAD_MID_out, HEAD_MID_in]
|
||||
const float* __restrict__ b2, // [N_HORIZONS, HEAD_MID_out]
|
||||
const float* __restrict__ w_gate, // [N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ b_gate, // [N_HORIZONS]
|
||||
const float* __restrict__ w_main, // [N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ b_main, // [N_HORIZONS]
|
||||
const float* __restrict__ w_skip, // [N_HORIZONS, HIDDEN]
|
||||
const float* __restrict__ b_skip, // [N_HORIZONS]
|
||||
const float* __restrict__ h, // [B, HIDDEN]
|
||||
int n_batch,
|
||||
float* __restrict__ probs, // [B, N_HORIZONS]
|
||||
float* __restrict__ z1_out, // [B, N_HORIZONS, HEAD_MID] — pre-GELU z1 (= W1 @ h + b1)
|
||||
float* __restrict__ a1_out, // [B, N_HORIZONS, HEAD_MID] — post-GELU eta_2
|
||||
float* __restrict__ z2_out, // [B, N_HORIZONS, HEAD_MID] — eta_1 (= W2 @ eta_2 + b2)
|
||||
float* __restrict__ gate_logit_out, // [B, N_HORIZONS] — pre-sigmoid gate scalar
|
||||
float* __restrict__ main_out, // [B, N_HORIZONS]
|
||||
float* __restrict__ logit_out // [B, N_HORIZONS] — pre-final-sigmoid logit (skip + sigma(gate)*main)
|
||||
) {
|
||||
int b_idx = blockIdx.x;
|
||||
int m = threadIdx.x;
|
||||
if (b_idx >= n_batch || m >= HEAD_MID_H) return;
|
||||
|
||||
const float* h_row = h + (long long)b_idx * HIDDEN_H;
|
||||
|
||||
__shared__ float s_a1[N_HORIZONS_H * HEAD_MID_H]; // post-GELU eta_2
|
||||
__shared__ float s_z2[N_HORIZONS_H * HEAD_MID_H]; // eta_1
|
||||
// Stage h_row in shared — Pass 1 reuses h[i] across all 64 threads,
|
||||
// Pass 3 (skip path) reads it again. Eliminates ~8800 redundant
|
||||
// DRAM reads per block.
|
||||
__shared__ float s_h[HIDDEN_H];
|
||||
#pragma unroll
|
||||
for (int t = 0; t < HIDDEN_H / HEAD_MID_H; ++t) {
|
||||
s_h[t * HEAD_MID_H + m] = h_row[t * HEAD_MID_H + m];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 1: z1[k, m] = W1[k, m, :] @ h + b1[k, m]; a1 = GELU(z1).
|
||||
// Thread m owns row m (output dim) for all k. Sequential over HIDDEN.
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
float z = b1[k * HEAD_MID_H + m];
|
||||
for (int i = 0; i < HIDDEN_H; ++i) {
|
||||
z += w1[((long long)(k * HEAD_MID_H) + m) * HIDDEN_H + i] * s_h[i];
|
||||
}
|
||||
const float a = gelu_act(z);
|
||||
s_a1[k * HEAD_MID_H + m] = a;
|
||||
z1_out[((long long)b_idx * N_HORIZONS_H + k) * HEAD_MID_H + m] = z;
|
||||
a1_out[((long long)b_idx * N_HORIZONS_H + k) * HEAD_MID_H + m] = a;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 2: z2[k, m] = W2[k, m_out=m, m_in=n] @ a1[k, n] + b2[k, m].
|
||||
// Pure linear — no activation. Thread m owns output row m for all k.
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
float z = b2[k * HEAD_MID_H + m];
|
||||
for (int n = 0; n < HEAD_MID_H; ++n) {
|
||||
z += w2[((long long)(k * HEAD_MID_H) + m) * HEAD_MID_H + n]
|
||||
* s_a1[k * HEAD_MID_H + n];
|
||||
}
|
||||
s_z2[k * HEAD_MID_H + m] = z;
|
||||
z2_out[((long long)b_idx * N_HORIZONS_H + k) * HEAD_MID_H + m] = z;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 3: per-horizon scalars. Threads 0..(N_HORIZONS-1) each handle one horizon.
|
||||
// gate_lin[k] = W_gate[k, :] @ z2[k, :] + b_gate[k]
|
||||
// main[k] = W_main[k, :] @ z2[k, :] + b_main[k]
|
||||
// skip[k] = W_skip[k, :] @ h + b_skip[k]
|
||||
// logit[k] = skip[k] + sigmoid(gate_lin[k]) * main[k]
|
||||
// p[k] = sigmoid(logit[k])
|
||||
if (m < N_HORIZONS_H) {
|
||||
const int k = m;
|
||||
float gl = b_gate[k];
|
||||
float mv = b_main[k];
|
||||
#pragma unroll
|
||||
for (int n = 0; n < HEAD_MID_H; ++n) {
|
||||
const float z2_kn = s_z2[k * HEAD_MID_H + n];
|
||||
gl += w_gate[k * HEAD_MID_H + n] * z2_kn;
|
||||
mv += w_main[k * HEAD_MID_H + n] * z2_kn;
|
||||
}
|
||||
float sk = b_skip[k];
|
||||
for (int i = 0; i < HIDDEN_H; ++i) {
|
||||
sk += w_skip[k * HIDDEN_H + i] * s_h[i];
|
||||
}
|
||||
const float sigmoid_gl = 1.0f / (1.0f + expf(-gl));
|
||||
const float logit = sk + sigmoid_gl * mv;
|
||||
const float p = 1.0f / (1.0f + expf(-logit));
|
||||
|
||||
gate_logit_out[(long long)b_idx * N_HORIZONS_H + k] = gl;
|
||||
main_out[(long long)b_idx * N_HORIZONS_H + k] = mv;
|
||||
logit_out[(long long)b_idx * N_HORIZONS_H + k] = logit;
|
||||
probs[(long long)b_idx * N_HORIZONS_H + k] = p;
|
||||
}
|
||||
}
|
||||
|
||||
// GRN backward — chain rule:
|
||||
//
|
||||
// d_logit = grad_probs * p * (1 - p)
|
||||
// d_skip = d_logit
|
||||
// d_main = d_logit * sigma(gate_lin)
|
||||
// d_gate_a = d_logit * main
|
||||
// d_gate_lin = d_gate_a * sigma(gate_lin) * (1 - sigma(gate_lin))
|
||||
//
|
||||
// Per-horizon scalar grads:
|
||||
// grad_w_skip[k, i] += d_skip[k] * h[i]
|
||||
// grad_b_skip[k] += d_skip[k]
|
||||
// grad_w_main[k, m] += d_main[k] * eta_1[k, m]
|
||||
// grad_b_main[k] += d_main[k]
|
||||
// grad_w_gate[k, m] += d_gate_lin[k] * eta_1[k, m]
|
||||
// grad_b_gate[k] += d_gate_lin[k]
|
||||
//
|
||||
// Through eta_1:
|
||||
// d_eta_1[k, m] = d_main * w_main[k, m] + d_gate_lin * w_gate[k, m]
|
||||
// grad_w2[k, m_out, m_in] += d_eta_1[k, m_out] * eta_2[k, m_in]
|
||||
// grad_b2[k, m_out] += d_eta_1[k, m_out]
|
||||
// d_eta_2[k, m_in] = sum_{m_out} d_eta_1[k, m_out] * w2[k, m_out, m_in]
|
||||
//
|
||||
// Through GELU + W1:
|
||||
// d_z1[k, m] = d_eta_2[k, m] * gelu_prime(z1[k, m])
|
||||
// grad_w1[k, m, i] += d_z1[k, m] * h[i]
|
||||
// grad_b1[k, m] += d_z1[k, m]
|
||||
//
|
||||
// Trunk gradient (lambda-scaled, sums two paths):
|
||||
// grad_h[i] = sum_k lambda[k] * (
|
||||
// d_skip[k] * w_skip[k, i] (skip path)
|
||||
// + sum_m d_z1[k, m] * w1[k, m, i] (main path through W2→W1)
|
||||
// ) + carry[i]
|
||||
//
|
||||
// Single-writer discipline:
|
||||
// Thread m (= m_out for w2, m for w1) owns row m of grad_w2 (writes
|
||||
// cols m_in for all m_in) and row m of grad_w1 (writes cols i for
|
||||
// all i). Thread m also owns col m of d_eta_2 (sums over m_out).
|
||||
// Thread tid (with tid < N_HORIZONS) owns horizon-scalar grads.
|
||||
// Trunk grad_h tiles i over 2 strides of HEAD_MID.
|
||||
|
||||
// Block-per-batch GRN bwd (Phase B).
|
||||
// grid=(n_batch, 1, 1) block=(HEAD_MID, 1, 1)
|
||||
//
|
||||
// Param-grad scratch tensors hold per-batch slices [B, ...]. Thread
|
||||
// (bi, m) is sole writer to its slice for every K-iteration; the
|
||||
// K-loop's 64 invocations accumulate via += within (bi, ..., m).
|
||||
// Caller zeroes scratch once per training step; reduce_axis0 collapses
|
||||
// B → final grad after the K-loop.
|
||||
//
|
||||
// grad_h is per-batch indexed; block bi is sole writer to its
|
||||
// [bi, :] slice (overwrite + carry).
|
||||
extern "C" __global__ void multi_horizon_heads_grn_bwd_batched(
|
||||
const float* __restrict__ w1, // [N_HORIZONS, HEAD_MID, HIDDEN]
|
||||
const float* __restrict__ w2, // [N_HORIZONS, HEAD_MID_out, HEAD_MID_in]
|
||||
const float* __restrict__ w_gate, // [N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ w_main, // [N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ w_skip, // [N_HORIZONS, HIDDEN]
|
||||
const float* __restrict__ probs, // [B, N_HORIZONS]
|
||||
const float* __restrict__ grad_probs, // [B, N_HORIZONS]
|
||||
const float* __restrict__ z1, // [B, N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ a1, // [B, N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ z2, // [B, N_HORIZONS, HEAD_MID]
|
||||
const float* __restrict__ gate_logit, // [B, N_HORIZONS]
|
||||
const float* __restrict__ main_val, // [B, N_HORIZONS]
|
||||
const float* __restrict__ h, // [B, HIDDEN]
|
||||
const float* __restrict__ grad_h_carry, // [B, HIDDEN] (nullptr OK)
|
||||
const float* __restrict__ lambda, // [N_HORIZONS]
|
||||
int n_batch,
|
||||
float* __restrict__ grad_w1_scratch, // [B, N_HORIZONS, HEAD_MID, HIDDEN] (+=)
|
||||
float* __restrict__ grad_b1_scratch, // [B, N_HORIZONS, HEAD_MID] (+=)
|
||||
float* __restrict__ grad_w2_scratch, // [B, N_HORIZONS, HEAD_MID, HEAD_MID] (+=)
|
||||
float* __restrict__ grad_b2_scratch, // [B, N_HORIZONS, HEAD_MID] (+=)
|
||||
float* __restrict__ grad_w_gate_scratch, // [B, N_HORIZONS, HEAD_MID] (+=)
|
||||
float* __restrict__ grad_b_gate_scratch, // [B, N_HORIZONS] (+=)
|
||||
float* __restrict__ grad_w_main_scratch, // [B, N_HORIZONS, HEAD_MID] (+=)
|
||||
float* __restrict__ grad_b_main_scratch, // [B, N_HORIZONS] (+=)
|
||||
float* __restrict__ grad_w_skip_scratch, // [B, N_HORIZONS, HIDDEN] (+=)
|
||||
float* __restrict__ grad_b_skip_scratch, // [B, N_HORIZONS] (+=)
|
||||
float* __restrict__ grad_h // [B, HIDDEN] (overwrite + carry)
|
||||
) {
|
||||
int bi = blockIdx.x;
|
||||
int tid = threadIdx.x;
|
||||
if (bi >= n_batch || tid >= HIDDEN_H) return;
|
||||
|
||||
__shared__ float s_lambda[N_HORIZONS_H];
|
||||
__shared__ float s_d_skip[N_HORIZONS_H]; // = d_logit (skip-path grad)
|
||||
__shared__ float s_d_main[N_HORIZONS_H];
|
||||
__shared__ float s_d_gate_lin[N_HORIZONS_H];
|
||||
__shared__ float s_d_eta1[N_HORIZONS_H * HEAD_MID_H];
|
||||
__shared__ float s_d_eta2[N_HORIZONS_H * HEAD_MID_H];
|
||||
__shared__ float s_d_z1[N_HORIZONS_H * HEAD_MID_H];
|
||||
// Cooperatively-staged inputs for warm cache hits across passes.
|
||||
// s_h: reused per (k, m) by every thread in Pass 5 + once per i in Pass 6
|
||||
// s_a1: reused 64× per thread in Pass 3 inner loop
|
||||
__shared__ float s_h[HIDDEN_H];
|
||||
__shared__ float s_a1[N_HORIZONS_H * HEAD_MID_H];
|
||||
|
||||
// Pass 2/3/4 partition over m ∈ [0, HEAD_MID); only first HEAD_MID threads work.
|
||||
// Pass 5/6 partition over i ∈ [0, HIDDEN); all 128 threads work.
|
||||
const int m = tid; // valid iff tid < HEAD_MID_H
|
||||
const bool is_m_thread = (tid < HEAD_MID_H);
|
||||
const float* h_row = h + (long long)bi * HIDDEN_H;
|
||||
|
||||
// Per-batch base offsets into scratch tensors.
|
||||
const long long bi_5 = (long long)bi * N_HORIZONS_H;
|
||||
const long long bi_5_m = bi_5 * HEAD_MID_H;
|
||||
const long long bi_5_h = bi_5 * HIDDEN_H;
|
||||
|
||||
// Cooperative staging — 128 threads load h (1 each), a1 needs 320 slots
|
||||
// covered by HEAD_MID threads × N_HORIZONS_H, then sync.
|
||||
if (tid < N_HORIZONS_H) {
|
||||
const float l = lambda[tid];
|
||||
s_lambda[tid] = (l > 0.0f) ? l : 1.0f; // sentinel: zero buffer → 1.0
|
||||
}
|
||||
s_h[tid] = h_row[tid]; // tid ∈ [0, HIDDEN) — one float each
|
||||
if (is_m_thread) {
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
s_a1[k * HEAD_MID_H + m] =
|
||||
a1[((long long)bi * N_HORIZONS_H + k) * HEAD_MID_H + m];
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 1: per-horizon scalar grads. Threads 0..(N_HORIZONS-1) only.
|
||||
if (tid < N_HORIZONS_H) {
|
||||
const int k = tid;
|
||||
const float p = probs[(long long)bi * N_HORIZONS_H + k];
|
||||
const float dp = grad_probs[(long long)bi * N_HORIZONS_H + k];
|
||||
const float d_logit = dp * p * (1.0f - p);
|
||||
|
||||
const float gl = gate_logit[(long long)bi * N_HORIZONS_H + k];
|
||||
const float sigmoid_gl = 1.0f / (1.0f + expf(-gl));
|
||||
const float mv = main_val[(long long)bi * N_HORIZONS_H + k];
|
||||
|
||||
const float d_skip = d_logit;
|
||||
const float d_main = d_logit * sigmoid_gl;
|
||||
const float d_gate_a = d_logit * mv;
|
||||
const float d_gate_lin = d_gate_a * sigmoid_gl * (1.0f - sigmoid_gl);
|
||||
|
||||
s_d_skip[k] = d_skip;
|
||||
s_d_main[k] = d_main;
|
||||
s_d_gate_lin[k] = d_gate_lin;
|
||||
|
||||
// Per-batch scratch writes for horizon-scalar biases.
|
||||
grad_b_skip_scratch[bi_5 + k] += d_skip;
|
||||
grad_b_main_scratch[bi_5 + k] += d_main;
|
||||
grad_b_gate_scratch[bi_5 + k] += d_gate_lin;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 2: per-(k, m_out=m) compute d_eta_1[k, m] + grad_w_{main,gate}.
|
||||
// First HEAD_MID threads only; threads 64..127 idle this pass.
|
||||
if (is_m_thread) {
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
const float d_main_k = s_d_main[k];
|
||||
const float d_gate_lin_k = s_d_gate_lin[k];
|
||||
const float w_main_km = w_main[k * HEAD_MID_H + m];
|
||||
const float w_gate_km = w_gate[k * HEAD_MID_H + m];
|
||||
const float z2_km = z2[((long long)bi * N_HORIZONS_H + k) * HEAD_MID_H + m];
|
||||
|
||||
const float d_eta1_km = d_main_k * w_main_km + d_gate_lin_k * w_gate_km;
|
||||
s_d_eta1[k * HEAD_MID_H + m] = d_eta1_km;
|
||||
|
||||
grad_w_main_scratch[bi_5_m + (long long)k * HEAD_MID_H + m] += d_main_k * z2_km;
|
||||
grad_w_gate_scratch[bi_5_m + (long long)k * HEAD_MID_H + m] += d_gate_lin_k * z2_km;
|
||||
|
||||
// grad_b2[k, m] += d_eta_1[k, m] — thread m sole writer of row m.
|
||||
grad_b2_scratch[bi_5_m + (long long)k * HEAD_MID_H + m] += d_eta1_km;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 3: through W2. Thread role: tid = n = m_in (column).
|
||||
// grad_w2[k, m_out, m_in=tid] += d_eta_1[k, m_out] * eta_2[k, m_in=tid]
|
||||
// d_eta_2[k, m_in=tid] = sum_{m_out} d_eta_1[k, m_out] * W2[k, m_out, m_in=tid]
|
||||
// Cross-thread writes/reads at fixed m_out → consecutive in the innermost
|
||||
// dim → COALESCED. Prior layout (thread = m_out) strided by HEAD_MID = 256B.
|
||||
// a1[k, tid] is read from s_a1 — broadcast-free (one shared load per
|
||||
// thread per k).
|
||||
if (is_m_thread) {
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
const float a1_kn = s_a1[k * HEAD_MID_H + tid];
|
||||
float d_eta2_kn = 0.0f;
|
||||
#pragma unroll 8
|
||||
for (int m_out = 0; m_out < HEAD_MID_H; ++m_out) {
|
||||
const float d_eta1_kmout = s_d_eta1[k * HEAD_MID_H + m_out];
|
||||
// grad_w2 row major: stride 1 in `tid` (the m_in/n dim).
|
||||
grad_w2_scratch[bi_5_m * HEAD_MID_H
|
||||
+ ((long long)k * HEAD_MID_H + m_out) * HEAD_MID_H + tid]
|
||||
+= d_eta1_kmout * a1_kn;
|
||||
// w2 row major: same stride 1 in `tid`.
|
||||
d_eta2_kn += d_eta1_kmout
|
||||
* w2[((long long)(k * HEAD_MID_H) + m_out) * HEAD_MID_H + tid];
|
||||
}
|
||||
s_d_eta2[k * HEAD_MID_H + tid] = d_eta2_kn;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 4: through GELU. First HEAD_MID threads only.
|
||||
// d_z1[k, m] = d_eta_2[k, m] * gelu_prime(z1[k, m])
|
||||
// grad_b1[k, m] += d_z1[k, m]
|
||||
if (is_m_thread) {
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
const float d_eta2_km = s_d_eta2[k * HEAD_MID_H + m];
|
||||
const float z1_km = z1[((long long)bi * N_HORIZONS_H + k) * HEAD_MID_H + m];
|
||||
const float d_z1_km = d_eta2_km * gelu_prime(z1_km);
|
||||
s_d_z1[k * HEAD_MID_H + m] = d_z1_km;
|
||||
grad_b1_scratch[bi_5_m + (long long)k * HEAD_MID_H + m] += d_z1_km;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Pass 5: grad_w1[k, m, i] += d_z1[k, m] * h[i] (per-batch scratch).
|
||||
// Partitioned over i: thread tid handles output column i = tid.
|
||||
// Cross-thread writes within a warp differ by 1 in the innermost dim
|
||||
// → COALESCED 128-byte transactions per warp (~4× DRAM efficiency
|
||||
// vs the prior thread-per-m layout which strided by HIDDEN_H = 512B).
|
||||
// h_tid pinned in register (constant per thread for the entire pass).
|
||||
{
|
||||
const float h_tid = s_h[tid];
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
for (int mm = 0; mm < HEAD_MID_H; ++mm) {
|
||||
const float d_z1_km = s_d_z1[k * HEAD_MID_H + mm];
|
||||
grad_w1_scratch[bi_5_m * HIDDEN_H
|
||||
+ ((long long)k * HEAD_MID_H + mm) * HIDDEN_H + tid]
|
||||
+= d_z1_km * h_tid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 6: trunk grad_w_skip + grad_h. One thread per output column i.
|
||||
// grad_w_skip[k, i] += d_skip[k] * h[i] (per-batch scratch)
|
||||
// grad_h[bi, i] = sum_k lambda[k] * (
|
||||
// d_skip[k] * w_skip[k, i] # skip path
|
||||
// + sum_m d_z1[k, m] * w1[k, m, i] # main path
|
||||
// ) + carry[i]
|
||||
// With block_dim == HIDDEN_H, no tile loop — thread tid IS the column i.
|
||||
{
|
||||
const int i = tid;
|
||||
const float h_bi_i = s_h[i];
|
||||
float acc_grad_h = 0.0f;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < N_HORIZONS_H; ++k) {
|
||||
grad_w_skip_scratch[bi_5_h + (long long)k * HIDDEN_H + i]
|
||||
+= s_d_skip[k] * h_bi_i;
|
||||
|
||||
float contrib = s_d_skip[k] * w_skip[k * HIDDEN_H + i];
|
||||
#pragma unroll
|
||||
for (int mm = 0; mm < HEAD_MID_H; ++mm) {
|
||||
contrib += s_d_z1[k * HEAD_MID_H + mm]
|
||||
* w1[((long long)(k * HEAD_MID_H) + mm) * HIDDEN_H + i];
|
||||
}
|
||||
acc_grad_h += s_lambda[k] * contrib;
|
||||
}
|
||||
const float carry = (grad_h_carry != nullptr)
|
||||
? grad_h_carry[(long long)bi * HIDDEN_H + i] : 0.0f;
|
||||
grad_h[(long long)bi * HIDDEN_H + i] = acc_grad_h + carry;
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
// output_smoothness.cu — per-horizon temporal smoothness regularizer.
|
||||
//
|
||||
// Penalises (p[k] - p[k-1])² for adjacent positions within each sequence,
|
||||
// weighted per-horizon by λ[h]. λ[h] scales with horizon length so slow
|
||||
// horizons (h6000) are forced to change much more slowly than fast ones
|
||||
// (h30). Forward loss is reported per-horizon (raw, pre-λ) for telemetry
|
||||
// + lumped (λ-weighted) total.
|
||||
//
|
||||
// Per `feedback_nvidia_grade_perf_for_kernels.md`:
|
||||
// - Warp-shuffle reduction; no atomicAdd.
|
||||
// - Single block over the [K, B, N_HORIZONS] grid; stride loop per
|
||||
// thread covers all elements.
|
||||
// - No host branches, no divergent shuffles (inactive lanes contribute
|
||||
// 0 via ternary).
|
||||
// - N_HORIZONS horizon accumulators kept in registers (BCS-style).
|
||||
//
|
||||
// Per `feedback_no_atomicadd.md`: single-block tree-reduce (no atomics).
|
||||
// Per `pearl_one_unbounded_signal_per_reward.md`: smoothness contributes
|
||||
// a bounded (≤ 1 per pair after sigmoid) additive term — no
|
||||
// unboundedness concerns.
|
||||
//
|
||||
// Post-2026-05-22 horizon-rebase: OS_N_HORIZONS = 3 (matches Rust-side
|
||||
// `crates/ml-alpha/src/heads.rs::N_HORIZONS`).
|
||||
|
||||
#define OS_N_HORIZONS 3
|
||||
#define OS_BLOCK 256
|
||||
#define OS_N_WARPS (OS_BLOCK / 32) // 8
|
||||
|
||||
__device__ __forceinline__ float os_warp_reduce_sum(float v) {
|
||||
#pragma unroll
|
||||
for (int s = 16; s > 0; s >>= 1) {
|
||||
v += __shfl_xor_sync(0xffffffff, v, s);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Smoothness forward + backward.
|
||||
//
|
||||
// Inputs:
|
||||
// probs [K * B * N_HORIZONS] row-major, index = k*B*N_HORIZONS + b*N_HORIZONS + h
|
||||
// lambda_per_h [N_HORIZONS] λ[h] per horizon (already includes base × ratio)
|
||||
// K, B (scalars)
|
||||
//
|
||||
// Outputs:
|
||||
// loss_out [1] Σ_h λ[h] · raw_h — total loss
|
||||
// loss_raw_per_h [N_HORIZONS] raw mean-sq-diff per horizon (no λ) — telemetry
|
||||
// grad_probs [K*B*N_HORIZONS] ACCUMULATED (+=) into existing BCE grad
|
||||
//
|
||||
// The gradient at position j for the same (b, h):
|
||||
// grad += (2 λ[h] / N_pairs) · Δ(j, b, h)
|
||||
//
|
||||
// where Δ has the 3-branch boundary structure documented in the plan.
|
||||
//
|
||||
// Single block, OS_BLOCK threads. Each thread strides over the K*B*N_HORIZONS
|
||||
// flat range computing its own Δ(j, b, h) for the gradient pass.
|
||||
// Forward loss is accumulated per-horizon in registers, then warp/block
|
||||
// reduced; only thread 0 writes the N_HORIZONS raw outputs + the λ-weighted total.
|
||||
extern "C" __global__ void output_smoothness_loss_and_grad(
|
||||
const float* __restrict__ probs, // [K * B * N_HORIZONS]
|
||||
const float* __restrict__ lambda_per_h, // [N_HORIZONS]
|
||||
int K,
|
||||
int B,
|
||||
float* __restrict__ loss_out, // [1]
|
||||
float* __restrict__ loss_raw_per_h, // [N_HORIZONS]
|
||||
float* __restrict__ grad_probs // [K * B * N_HORIZONS] (+=)
|
||||
) {
|
||||
const int tid = threadIdx.x;
|
||||
const int lane = tid & 31;
|
||||
const int warp = tid >> 5;
|
||||
const int total = K * B * OS_N_HORIZONS;
|
||||
const int stride_k = B * OS_N_HORIZONS; // stride between k and k+1 at fixed (b,h); B*N_HORIZONS
|
||||
const int n_pairs = (K - 1) * B;
|
||||
// N_pairs == 0 when K==1; in that case we still need to zero outputs.
|
||||
const float inv_n_pairs = (n_pairs > 0) ? (1.0f / (float)n_pairs) : 0.0f;
|
||||
|
||||
// ── Forward: per-thread per-horizon raw accumulator ──
|
||||
// Anchor each (p[k]-p[k-1])² pair on its RIGHT element (k ≥ 1) so
|
||||
// each pair is counted exactly once across all threads.
|
||||
float local_raw[OS_N_HORIZONS];
|
||||
#pragma unroll
|
||||
for (int h = 0; h < OS_N_HORIZONS; ++h) local_raw[h] = 0.0f;
|
||||
|
||||
for (int i = tid; i < total; i += OS_BLOCK) {
|
||||
const int h = i % OS_N_HORIZONS;
|
||||
// Decompose i into (k, b, h). k = i / (B*5); b = (i / 5) % B.
|
||||
const int k = i / stride_k;
|
||||
// Only positions k ≥ 1 anchor a (p[k]-p[k-1])² pair.
|
||||
if (k >= 1) {
|
||||
const float p_cur = probs[i];
|
||||
const float p_prev = probs[i - stride_k];
|
||||
const float d = p_cur - p_prev;
|
||||
local_raw[h] += d * d;
|
||||
}
|
||||
}
|
||||
|
||||
// Warp reduce per-horizon partial sums.
|
||||
float warp_raw[OS_N_HORIZONS];
|
||||
#pragma unroll
|
||||
for (int h = 0; h < OS_N_HORIZONS; ++h) {
|
||||
warp_raw[h] = os_warp_reduce_sum(local_raw[h]);
|
||||
}
|
||||
|
||||
__shared__ float s_raw[OS_N_WARPS * OS_N_HORIZONS];
|
||||
if (lane == 0) {
|
||||
#pragma unroll
|
||||
for (int h = 0; h < OS_N_HORIZONS; ++h) {
|
||||
s_raw[warp * OS_N_HORIZONS + h] = warp_raw[h];
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Warp 0 finishes the cross-warp reduce.
|
||||
__shared__ float s_lambda[OS_N_HORIZONS];
|
||||
if (warp == 0) {
|
||||
#pragma unroll
|
||||
for (int h = 0; h < OS_N_HORIZONS; ++h) {
|
||||
float v = (lane < OS_N_WARPS) ? s_raw[lane * OS_N_HORIZONS + h] : 0.0f;
|
||||
v = os_warp_reduce_sum(v);
|
||||
if (lane == 0) {
|
||||
const float raw_h = v * inv_n_pairs;
|
||||
loss_raw_per_h[h] = raw_h;
|
||||
s_lambda[h] = lambda_per_h[h];
|
||||
// s_raw repurposed below as λ-weighted scratch.
|
||||
s_raw[h] = s_lambda[h] * raw_h;
|
||||
}
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// Thread 0 sums the N_HORIZONS λ-weighted terms into loss_out.
|
||||
if (tid == 0) {
|
||||
float total_loss = 0.0f;
|
||||
#pragma unroll
|
||||
for (int h = 0; h < OS_N_HORIZONS; ++h) total_loss += s_raw[h];
|
||||
loss_out[0] = total_loss;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Backward: per-thread compute Δ(j, b, h) and accumulate into grad_probs ──
|
||||
// Same stride loop. Three boundary cases:
|
||||
// j = 0 : Δ = p[0] − p[1]
|
||||
// j = K−1 : Δ = p[K−1] − p[K−2]
|
||||
// interior : Δ = 2·p[j] − p[j−1] − p[j+1]
|
||||
// Scale: factor = 2 · λ[h] / N_pairs.
|
||||
//
|
||||
// s_lambda is now populated for all threads.
|
||||
for (int i = tid; i < total; i += OS_BLOCK) {
|
||||
const int h = i % OS_N_HORIZONS;
|
||||
const int k = i / stride_k;
|
||||
const float lam = s_lambda[h];
|
||||
const float factor = 2.0f * lam * inv_n_pairs;
|
||||
const float p_j = probs[i];
|
||||
const bool has_left = (k >= 1);
|
||||
const bool has_right = (k <= K - 2);
|
||||
// Branchless boundary: at k=0 we read self for the "left" slot
|
||||
// and at k=K-1 we read self for the "right" slot; both indices
|
||||
// stay in-bounds and the lm/rm multipliers below zero the
|
||||
// spurious contribution. Every lane executes both loads — no
|
||||
// thread divergence on boundaries.
|
||||
const int idx_left = has_left ? (i - stride_k) : i;
|
||||
const int idx_right = has_right ? (i + stride_k) : i;
|
||||
const float p_jm1 = probs[idx_left];
|
||||
const float p_jp1 = probs[idx_right];
|
||||
// Δ(j, b, h):
|
||||
// left contribution = (p[j] - p[j-1]) if has_left else 0
|
||||
// right contribution = -(p[j+1] - p[j]) if has_right else 0
|
||||
// Sum = 2·p[j] − p[j-1] − p[j+1] for interior;
|
||||
// p[j] − p[j+1] for j==0 (has_left=F, has_right=T)
|
||||
// p[j] − p[j-1] for j==K-1 (has_left=T, has_right=F)
|
||||
// Use float multiplies to encode has_left/has_right (1.0 / 0.0).
|
||||
const float lm = has_left ? 1.0f : 0.0f;
|
||||
const float rm = has_right ? 1.0f : 0.0f;
|
||||
const float delta = (lm + rm) * p_j - lm * p_jm1 - rm * p_jp1;
|
||||
grad_probs[i] += factor * delta;
|
||||
}
|
||||
}
|
||||
@@ -1,502 +0,0 @@
|
||||
// ppo_clipped_surrogate.cu — PPO actor loss (clipped surrogate) + entropy bonus.
|
||||
//
|
||||
// PHASE D of the integrated RL trainer
|
||||
// (docs/superpowers/plans/2026-05-22-integrated-rl-trainer.md).
|
||||
//
|
||||
// Forward (`ppo_clipped_surrogate_fwd`):
|
||||
// r_t = exp(log π_new(a|s) - log π_old(a|s))
|
||||
// surr1 = r_t × A_t
|
||||
// surr2 = clip(r_t, 1-ε, 1+ε) × A_t
|
||||
// L_π = -min(surr1, surr2) (negative because we minimise)
|
||||
// L_entropy = -coef × H(π_new) (encourage exploration)
|
||||
//
|
||||
// V LOSS CANONICALIZATION (Phase E.2-DEFER 2026-05-22):
|
||||
// The dedicated V-head kernels in `v_head_fwd_bwd.cu` are the SINGLE
|
||||
// SOURCE OF TRUTH for the value-MSE loss + V-head gradients. The PPO
|
||||
// surrogate kernel previously emitted a redundant `loss_v` accumulator
|
||||
// computed from `r_target` / `v_pred` inputs that the integrated trainer
|
||||
// never consumed. Per `feedback_single_source_of_truth_no_duplicates` +
|
||||
// `feedback_no_partial_refactor` the redundant inputs + computation are
|
||||
// DELETED here, not flagged-off; the only V signal flows through the
|
||||
// V-head kernels.
|
||||
//
|
||||
// ε is read from isv[RL_PPO_CLIP_INDEX=402].
|
||||
// coef is read from isv[RL_ENTROPY_COEF_INDEX=403].
|
||||
// Per pearl_controller_anchors_isv_driven: both are ISV-driven, not const.
|
||||
//
|
||||
// Backward (`ppo_clipped_surrogate_bwd`):
|
||||
// dL_π/dlogit : computed via the standard policy-gradient identity
|
||||
// combined with the clip mask (gradient of the surrogate
|
||||
// is zero in the clipped region).
|
||||
//
|
||||
// REDUCTION DISCIPLINE (per `feedback_no_atomicadd.md`):
|
||||
// Each block (= one batch element) is the SOLE writer of its own slot
|
||||
// in two per-batch buffers — `loss_pi_per_b[batch]` and
|
||||
// `loss_entropy_per_b[batch]`. No atomicAdd. The scalar diagnostics
|
||||
// the trainer reads (mean over batch) are produced by a separate
|
||||
// `ppo_loss_reduce_b` block tree-reduce kernel that the trainer calls
|
||||
// immediately after this kernel. This eliminates the staleness bug
|
||||
// that inflated `last_pi_loss` by N steps (the old mapped-pinned
|
||||
// scalar was never zeroed between forwards, so an atomicAdd from
|
||||
// every step kept accumulating; local 1k × ~9 ≈ 9080 and cluster
|
||||
// 20k × ~1200 ≈ 24M were both bit-exact step-count fingerprints of
|
||||
// that accumulator — see commit log for the diagnosis trail).
|
||||
//
|
||||
// Block layout:
|
||||
// Forward: grid = (B, 1, 1); block = (N_ACTIONS, 1, 1).
|
||||
// One block per batch element, one thread per action.
|
||||
// Shared-mem softmax (max + sumexp) computed by thread 0
|
||||
// then broadcast. Thread 0 also writes the per-batch loss
|
||||
// slots (the surrogate only depends on the taken action,
|
||||
// so no parallelism is wasted there).
|
||||
// Backward: same layout. Each thread writes its own logit's gradient.
|
||||
|
||||
#define HIDDEN_DIM 128
|
||||
#define N_ACTIONS 11
|
||||
#define RL_PPO_CLIP_INDEX 402
|
||||
#define RL_ENTROPY_COEF_INDEX 403
|
||||
// ISV slot carrying the adaptive importance-ratio clamp ceiling
|
||||
// produced by rl_ppo_ratio_clamp_controller. Floor is reciprocal
|
||||
// (clamp window is symmetric in log space: [1/ratio_max, ratio_max]).
|
||||
#define RL_PPO_RATIO_CLAMP_MAX_INDEX 440
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ppo_policy_logits_fwd (Phase E.2):
|
||||
// Standalone linear forward producing per-batch logits from h_t. The
|
||||
// surrogate kernel takes these logits as input; previously the
|
||||
// integrated trainer had no path to materialise them outside of
|
||||
// the surrogate compute.
|
||||
//
|
||||
// logits[b, a] = b_pi[a] + Σ_c W_pi[a, c] × h_t[b, c]
|
||||
//
|
||||
// Block layout:
|
||||
// grid = (B, N_ACTIONS, 1)
|
||||
// block = (HIDDEN_DIM, 1, 1)
|
||||
// shared_mem_bytes = 0 (each thread independently strides; thread 0
|
||||
// performs the per-output dot product. The
|
||||
// block over HIDDEN_DIM is sized for backward
|
||||
// compatibility with grad_w_b_h_t's reuse of
|
||||
// the same h_t layout in shared.)
|
||||
//
|
||||
// Single output slot per block (b, a) → thread 0 reduces inline. Other
|
||||
// threads idle (block size mirrors backward's HIDDEN_DIM for caller
|
||||
// uniformity; perf is non-critical since N_ACTIONS=9 means 9×B blocks).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void ppo_policy_logits_fwd(
|
||||
const float* __restrict__ w, // [N_ACTIONS, HIDDEN_DIM]
|
||||
const float* __restrict__ b, // [N_ACTIONS]
|
||||
const float* __restrict__ h_t, // [B * HIDDEN_DIM]
|
||||
int B,
|
||||
float* __restrict__ logits // [B * N_ACTIONS]
|
||||
) {
|
||||
const int batch = blockIdx.x;
|
||||
const int act = blockIdx.y;
|
||||
const int c = threadIdx.x;
|
||||
if (batch >= B) return;
|
||||
if (act >= N_ACTIONS) return;
|
||||
if (c != 0) return; // single-writer per output slot
|
||||
|
||||
float acc = b[act];
|
||||
#pragma unroll
|
||||
for (int i = 0; i < HIDDEN_DIM; ++i) {
|
||||
acc += w[act * HIDDEN_DIM + i] * h_t[batch * HIDDEN_DIM + i];
|
||||
}
|
||||
logits[batch * N_ACTIONS + act] = acc;
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ppo_clipped_surrogate_fwd:
|
||||
// Computes softmax → log π_new(a_taken|s) → ratio → clipped surrogate +
|
||||
// entropy bonus. Writes per-batch log π / entropy diagnostics plus two
|
||||
// scalar loss accumulators (policy + entropy).
|
||||
//
|
||||
// The value-MSE loss + V gradient now live exclusively in the dedicated
|
||||
// v_head_fwd_bwd kernels — this kernel no longer touches V_pred / R_target
|
||||
// (greenfield removal 2026-05-22, see header).
|
||||
//
|
||||
// Inputs:
|
||||
// logits [B × N_ACTIONS] — new-policy raw logits from PolicyHead
|
||||
// log_pi_old [B] — log π at the taken action when the
|
||||
// transition was recorded (rollout buf)
|
||||
// actions [B] — taken action index in [0, N_ACTIONS)
|
||||
// advantages [B] — A_t = Q(s_t, a_t) - V(s_t)
|
||||
// (RolloutBuffer::advantages)
|
||||
// isv [≥ 404] — reads ε at 402, coef at 403
|
||||
// B — batch size
|
||||
// Outputs:
|
||||
// pi_log_prob [B] — log π_new(a_taken|s) for diagnostics
|
||||
// and Phase E KL EMA
|
||||
// entropy [B] — H(π_new) per batch sample
|
||||
// loss_pi_per_b [B] — per-batch L_π (single writer per slot,
|
||||
// NO atomicAdd). Reduced to scalar mean
|
||||
// by `ppo_loss_reduce_b`.
|
||||
// loss_entropy_per_b [B] — per-batch L_entropy, same discipline.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void ppo_clipped_surrogate_fwd(
|
||||
const float* __restrict__ logits, // [B * N_ACTIONS]
|
||||
const float* __restrict__ log_pi_old, // [B]
|
||||
const int* __restrict__ actions, // [B]
|
||||
const float* __restrict__ advantages, // [B]
|
||||
const float* __restrict__ isv, // [>= 404]
|
||||
int B,
|
||||
float* __restrict__ pi_log_prob, // [B]
|
||||
float* __restrict__ entropy, // [B]
|
||||
float* __restrict__ loss_pi_per_b, // [B]
|
||||
float* __restrict__ loss_entropy_per_b, // [B]
|
||||
// B-10 (2026-06-01) G3+G4: per-batch observability scratches.
|
||||
// Pure observability; written ALONGSIDE existing loss_pi_per_b — does
|
||||
// NOT perturb the loss reduce path (`ppo_loss_reduce_b.cu`). Consumed
|
||||
// by the dedicated reducer `rl_ppo_diagnostic_stats_reduce.cu` which
|
||||
// folds these [B] arrays into ISV slots 735-742. σ_used (= sqrt(var_
|
||||
// pre_norm) from slot 612) is constant per step → reducer computes
|
||||
// it once and reconstructs |A_unnorm| statistics from |A_norm| stats
|
||||
// (no per-batch unnorm scratch needed). The kernel's existing math
|
||||
// (softmax, ratio clamp, surr1/surr2 → l_pi) is byte-identical to
|
||||
// pre-B-10; new writes happen in the same `act == 0` branch.
|
||||
float* __restrict__ ppo_a_norm_pb, // [B] |advantages[batch]|
|
||||
float* __restrict__ ppo_ratio_dev_pb, // [B] |ratio − 1|
|
||||
float* __restrict__ ppo_ratio_clipped_pb, // [B] {0.0, 1.0} outside [1-ε, 1+ε]
|
||||
float* __restrict__ ppo_surrogate_pb // [B] clipped surrogate (no entropy bonus)
|
||||
) {
|
||||
const int batch = blockIdx.x;
|
||||
const int act = threadIdx.x;
|
||||
if (batch >= B) return;
|
||||
if (act >= N_ACTIONS) return;
|
||||
|
||||
const int base = batch * N_ACTIONS;
|
||||
|
||||
// ── Softmax (numerically stable max-subtract) ────────────────────
|
||||
__shared__ float s_logits[N_ACTIONS];
|
||||
__shared__ float s_softmax[N_ACTIONS];
|
||||
__shared__ float s_max;
|
||||
__shared__ float s_sumexp;
|
||||
|
||||
s_logits[act] = logits[base + act];
|
||||
__syncthreads();
|
||||
|
||||
if (act == 0) {
|
||||
float m = s_logits[0];
|
||||
#pragma unroll
|
||||
for (int i = 1; i < N_ACTIONS; ++i) m = fmaxf(m, s_logits[i]);
|
||||
s_max = m;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
s_softmax[act] = expf(s_logits[act] - s_max);
|
||||
__syncthreads();
|
||||
|
||||
if (act == 0) {
|
||||
float sum = 0.0f;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < N_ACTIONS; ++i) sum += s_softmax[i];
|
||||
s_sumexp = sum;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
// ── Entropy H(π) = -Σ p log p (thread 0 only; N_ACTIONS=9 is small) ──
|
||||
if (act == 0) {
|
||||
float h = 0.0f;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < N_ACTIONS; ++i) {
|
||||
const float pi = s_softmax[i] / s_sumexp;
|
||||
h -= pi * logf(fmaxf(pi, 1e-7f));
|
||||
}
|
||||
entropy[batch] = h;
|
||||
|
||||
// ── Surrogate + entropy loss on the TAKEN action ──
|
||||
const int a_taken = actions[batch];
|
||||
// Defensive: silently skip invalid actions. Phase E enforces
|
||||
// bound-checking at the loader boundary; this is the floor.
|
||||
if (a_taken >= 0 && a_taken < N_ACTIONS) {
|
||||
const float log_p = logf(fmaxf(s_softmax[a_taken] / s_sumexp, 1e-7f));
|
||||
pi_log_prob[batch] = log_p;
|
||||
|
||||
// Importance ratio with ISV-driven magnitude clamp.
|
||||
//
|
||||
// PPO's clip(r, 1-ε, 1+ε) bounds the LOSS when surr2 is the
|
||||
// active min — i.e. A>0,r>1+ε and A<0,r<1-ε. The
|
||||
// unclipped branch IS active when A<0,r>1+ε (surr1=A·r is
|
||||
// more negative than surr2=A·(1+ε) so min=surr1) and when
|
||||
// A>0,r<1-ε. In the first case `r` can blow up — we've
|
||||
// seen r reach 1e10 within a few steps of policy drift,
|
||||
// producing l_pi=-A·r=O(1e10) spikes that contaminate
|
||||
// the loss-balance controller and the LR controller's
|
||||
// plateau detection.
|
||||
//
|
||||
// Per `feedback_isv_for_adaptive_bounds` and
|
||||
// `pearl_controller_anchors_isv_driven`: the clamp bound
|
||||
// lives in ISV[RL_PPO_RATIO_CLAMP_MAX_INDEX = 440], emitted
|
||||
// by `rl_ppo_ratio_clamp_controller` which derives it from
|
||||
// the (already KL-adaptive) PPO clip ε at ISV[402]. The
|
||||
// controller widens the clamp when ε widens (rare but
|
||||
// larger excursions are expected) and tightens when ε
|
||||
// tightens (small excursions should be rare anomalies).
|
||||
//
|
||||
// Backward (ppo_clipped_surrogate_bwd) gates pg_grad
|
||||
// inside [1-ε, 1+ε] anyway so this clamp is forward-only —
|
||||
// gradients are bounded already; this bounds the reported
|
||||
// loss value to keep downstream controllers sane.
|
||||
const float ratio_raw = expf(log_p - log_pi_old[batch]);
|
||||
const float ratio_max = isv[RL_PPO_RATIO_CLAMP_MAX_INDEX];
|
||||
const float ratio_min = 1.0f / ratio_max;
|
||||
const float ratio = fmaxf(ratio_min,
|
||||
fminf(ratio_raw, ratio_max));
|
||||
const float A = advantages[batch];
|
||||
const float eps = isv[RL_PPO_CLIP_INDEX];
|
||||
const float surr1 = ratio * A;
|
||||
const float surr2 = fminf(fmaxf(ratio, 1.0f - eps),
|
||||
1.0f + eps) * A;
|
||||
const float l_pi = -fminf(surr1, surr2);
|
||||
|
||||
const float coef = isv[RL_ENTROPY_COEF_INDEX];
|
||||
const float l_ent = -coef * h;
|
||||
|
||||
// Sole writer of per-batch loss slots. No atomicAdd — the
|
||||
// [B] → [1] mean reduction lives in `ppo_loss_reduce_b`.
|
||||
loss_pi_per_b[batch] = l_pi;
|
||||
loss_entropy_per_b[batch] = l_ent;
|
||||
|
||||
// B-10 G3+G4: per-batch observability writes.
|
||||
// surrogate (no entropy bonus) = -l_pi (l_pi negates min in line
|
||||
// above; the canonical surrogate value Schulman PPO reports is
|
||||
// mean of min(ratio*A, clip*A), so we re-construct it here).
|
||||
// Magnitudes intentional: max/mean reductions need positive values.
|
||||
ppo_a_norm_pb[batch] = fabsf(A);
|
||||
ppo_ratio_dev_pb[batch] = fabsf(ratio - 1.0f);
|
||||
// Clipped flag: ratio outside [1-ε, 1+ε] band. We use the
|
||||
// pre-magnitude-clamp `ratio_raw` so it captures excursions
|
||||
// the rl_ppo_ratio_clamp_controller's hard clamp would also
|
||||
// catch; consistent with how ppo_log_ratio_abs_max_b reports.
|
||||
const bool clipped = (ratio_raw < 1.0f - eps) ||
|
||||
(ratio_raw > 1.0f + eps);
|
||||
ppo_ratio_clipped_pb[batch] = clipped ? 1.0f : 0.0f;
|
||||
// Clipped surrogate (Schulman canonical): min(surr1, surr2).
|
||||
// l_pi above = -min(surr1, surr2); negate back to the natural
|
||||
// surrogate value so positive surrogate = positive expected
|
||||
// policy-improvement direction.
|
||||
ppo_surrogate_pb[batch] = -l_pi;
|
||||
} else {
|
||||
// Invalid action; zero per-batch loss slots + diagnostic
|
||||
// (caller's reducer treats these slots like any other).
|
||||
pi_log_prob[batch] = 0.0f;
|
||||
loss_pi_per_b[batch] = 0.0f;
|
||||
loss_entropy_per_b[batch] = 0.0f;
|
||||
// B-10: zero observability scratches consistently.
|
||||
ppo_a_norm_pb[batch] = 0.0f;
|
||||
ppo_ratio_dev_pb[batch] = 0.0f;
|
||||
ppo_ratio_clipped_pb[batch] = 0.0f;
|
||||
ppo_surrogate_pb[batch] = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ppo_clipped_surrogate_bwd:
|
||||
// Per-logit gradient of the clipped surrogate. One block per batch,
|
||||
// one thread per action.
|
||||
//
|
||||
// Gradient form (matches OpenAI/Schulman's PPO derivation):
|
||||
// inside the clip band (1-ε ≤ r_t ≤ 1+ε):
|
||||
// dL_π/dlogit_a = -A × (p_a - 1[a == a_taken]) × ratio
|
||||
// outside the clip band:
|
||||
// dL_π/dlogit_a = 0 (surrogate is constant in r_t there)
|
||||
//
|
||||
// Inputs:
|
||||
// logits [B × N_ACTIONS] — raw logits from fwd
|
||||
// log_pi_old [B]
|
||||
// actions [B]
|
||||
// advantages [B]
|
||||
// isv — ε at 402
|
||||
// B — batch size
|
||||
// Outputs:
|
||||
// grad_logits [B × N_ACTIONS] — ∂L_π/∂logits. Non-taken actions get
|
||||
// the cross-entropy form gradient, not
|
||||
// zero (that's the standard softmax-PG
|
||||
// identity).
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void ppo_clipped_surrogate_bwd(
|
||||
const float* __restrict__ logits, // [B * N_ACTIONS]
|
||||
const float* __restrict__ log_pi_old, // [B]
|
||||
const int* __restrict__ actions, // [B]
|
||||
const float* __restrict__ advantages, // [B]
|
||||
const float* __restrict__ isv, // ε at 402
|
||||
int B,
|
||||
float* __restrict__ grad_logits // [B * N_ACTIONS]
|
||||
) {
|
||||
const int batch = blockIdx.x;
|
||||
const int act = threadIdx.x;
|
||||
if (batch >= B) return;
|
||||
if (act >= N_ACTIONS) return;
|
||||
|
||||
const int base = batch * N_ACTIONS;
|
||||
|
||||
// ── Softmax (same staging as fwd) ────────────────────────────────
|
||||
__shared__ float s_logits[N_ACTIONS];
|
||||
__shared__ float s_softmax[N_ACTIONS];
|
||||
__shared__ float s_max;
|
||||
__shared__ float s_sumexp;
|
||||
|
||||
s_logits[act] = logits[base + act];
|
||||
__syncthreads();
|
||||
|
||||
if (act == 0) {
|
||||
float m = s_logits[0];
|
||||
#pragma unroll
|
||||
for (int i = 1; i < N_ACTIONS; ++i) m = fmaxf(m, s_logits[i]);
|
||||
s_max = m;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
s_softmax[act] = expf(s_logits[act] - s_max);
|
||||
__syncthreads();
|
||||
|
||||
if (act == 0) {
|
||||
float sum = 0.0f;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < N_ACTIONS; ++i) sum += s_softmax[i];
|
||||
s_sumexp = sum;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
const float p_a = s_softmax[act] / s_sumexp;
|
||||
|
||||
const int a_taken = actions[batch];
|
||||
if (a_taken < 0 || a_taken >= N_ACTIONS) {
|
||||
// Defensive: zero grad on invalid action. Phase E enforces at
|
||||
// loader boundary; this is the floor.
|
||||
grad_logits[base + act] = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
const float log_p = logf(fmaxf(s_softmax[a_taken] / s_sumexp, 1e-7f));
|
||||
// Match the forward's ratio clamp from ISV[RL_PPO_RATIO_CLAMP_MAX_INDEX].
|
||||
// For inside_clip the clamp is a no-op (since the controller floors
|
||||
// at well above 1+ε); for outside_clip the gradient is zero anyway
|
||||
// — but keeping fwd/bwd algebraically consistent prevents
|
||||
// future-edit drift.
|
||||
const float ratio_raw = expf(log_p - log_pi_old[batch]);
|
||||
const float ratio_max = isv[RL_PPO_RATIO_CLAMP_MAX_INDEX];
|
||||
const float ratio_min = 1.0f / ratio_max;
|
||||
const float ratio = fmaxf(ratio_min, fminf(ratio_raw, ratio_max));
|
||||
const float A = advantages[batch];
|
||||
const float eps = isv[RL_PPO_CLIP_INDEX];
|
||||
|
||||
// Clip-band membership: gradient of min(surr1, surr2) is the standard
|
||||
// policy-gradient identity inside [1-ε, 1+ε] (where surr1 is unclipped)
|
||||
// and zero outside (where the clipped branch is the active min and is
|
||||
// locally constant in r_t).
|
||||
//
|
||||
// Phase D simplification: we use the inside-band gradient when the
|
||||
// ratio sits inside the clip range, zero outside. The exact OpenAI
|
||||
// form distinguishes by sign of A as well (one side of the clip is
|
||||
// active for A > 0, the other for A < 0); Phase E may refine this
|
||||
// edge handling but for the toy-bandit smoke and initial integration
|
||||
// tests the binary inside/outside split converges identically.
|
||||
float pg_grad = 0.0f;
|
||||
const bool inside_clip = (ratio >= 1.0f - eps) && (ratio <= 1.0f + eps);
|
||||
if (inside_clip) {
|
||||
// dL/dlogit_a = -A × (p_a - 1[a == a_taken]) × ratio
|
||||
const float indicator = (act == a_taken) ? 1.0f : 0.0f;
|
||||
pg_grad = -A * (p_a - indicator) * ratio;
|
||||
}
|
||||
|
||||
// Entropy gradient: ∂H/∂logit_a = -(1 + log π(a)) × π(a) + π(a) × Σ_j π(j)(1 + log π(j))
|
||||
// Simplified through softmax identity: ∂(-H)/∂logit_a = π(a) × (1 + log π(a)) - π(a) × (1 + H + log_sum_exp)
|
||||
// Which reduces to: ∂(-H)/∂logit_a = π(a) × (log π(a) + 1 + H)...
|
||||
// Actually the standard form: ∂H/∂logit_a = -p_a × (1 + log p_a) + p_a × Σ_j p_j(1 + log p_j)
|
||||
// Since we want to MAXIMIZE entropy (minimize -H), gradient of -(-coef*H) = coef * ∂H/∂logit_a
|
||||
// = coef * (-p_a * (1 + log p_a) + p_a * mean_term)
|
||||
// Simplest correct form via softmax: ∂(-H)/∂logit_a = p_a * (log p_a + 1 + H) where H is the entropy
|
||||
// So ∂(coef*H)/∂logit_a = -coef * p_a * (log(p_a) + 1 + H)
|
||||
//
|
||||
// This fires on ALL batch elements (not done-gated), preventing
|
||||
// entropy collapse even when PPO advantages are masked to zero
|
||||
// on non-done steps. Implements the surfer philosophy: Hold is
|
||||
// the default, trading is the exception.
|
||||
const float ent_coef = isv[RL_ENTROPY_COEF_INDEX];
|
||||
float ent_grad = 0.0f;
|
||||
if (ent_coef > 0.0f) {
|
||||
// Compute entropy H for this batch element (thread 0 already did in fwd)
|
||||
__shared__ float s_entropy;
|
||||
if (act == 0) {
|
||||
float h = 0.0f;
|
||||
for (int i = 0; i < N_ACTIONS; ++i) {
|
||||
float pi = s_softmax[i] / s_sumexp;
|
||||
h -= pi * logf(fmaxf(pi, 1e-7f));
|
||||
}
|
||||
s_entropy = h;
|
||||
}
|
||||
__syncthreads();
|
||||
// ∂(ent_coef * H)/∂logit_a: push toward uniform
|
||||
const float log_pa = logf(fmaxf(p_a, 1e-7f));
|
||||
ent_grad = -ent_coef * p_a * (log_pa + 1.0f + s_entropy);
|
||||
}
|
||||
|
||||
grad_logits[base + act] = (pg_grad + ent_grad) / (float)B;
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ppo_grad_w_b_h_t (Phase E.2):
|
||||
// Given per-batch grad_logits [B × N_ACTIONS] from
|
||||
// `ppo_clipped_surrogate_bwd`, propagate to weight / bias gradients
|
||||
// (per-batch scratch — caller reduces via reduce_axis0) and to grad_h_t
|
||||
// (per-(batch, c) sole writer; OVERWRITE).
|
||||
//
|
||||
// Mathematics (per batch b, action a, hidden unit c):
|
||||
// grad_w_per_batch[b, a, c] = grad_logits[b, a] × h_t[b, c]
|
||||
// grad_b_per_batch[b, a] = grad_logits[b, a]
|
||||
// grad_h_t[b, c] = Σ_a grad_logits[b, a] × w[a, c]
|
||||
//
|
||||
// Block layout (mirrors aux_heads_bwd):
|
||||
// grid = (B, 1, 1)
|
||||
// block = (HIDDEN_DIM = 128, 1, 1)
|
||||
// shared_mem_bytes = N_ACTIONS * sizeof(float) (grad_logits stage)
|
||||
//
|
||||
// Thread c is the sole writer of grad_w_per_batch[batch, a, c] for all a,
|
||||
// and of grad_h_t[batch, c]. The first N_ACTIONS threads additionally
|
||||
// write grad_b_per_batch[batch, a]. Per `feedback_no_atomicadd.md`: no
|
||||
// atomics; each output slot has exactly one writer.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void ppo_grad_w_b_h_t(
|
||||
const float* __restrict__ w, // [N_ACTIONS, HIDDEN_DIM]
|
||||
const float* __restrict__ h_t, // [B * HIDDEN_DIM]
|
||||
const float* __restrict__ grad_logits, // [B * N_ACTIONS]
|
||||
int B,
|
||||
float* __restrict__ grad_w_per_batch, // [B * N_ACTIONS * HIDDEN_DIM]
|
||||
float* __restrict__ grad_b_per_batch, // [B * N_ACTIONS]
|
||||
float* __restrict__ grad_h_t // [B * HIDDEN_DIM] (OVERWRITE)
|
||||
) {
|
||||
__shared__ float s_gl[N_ACTIONS];
|
||||
|
||||
const int batch = blockIdx.x;
|
||||
const int c = threadIdx.x;
|
||||
if (batch >= B) return;
|
||||
|
||||
// Stage grad_logits[batch, :] (9 floats) — first N_ACTIONS threads.
|
||||
if (c < N_ACTIONS) {
|
||||
s_gl[c] = grad_logits[batch * N_ACTIONS + c];
|
||||
// Sole writer of grad_b_per_batch[batch, c].
|
||||
grad_b_per_batch[batch * N_ACTIONS + c] = s_gl[c];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (c >= HIDDEN_DIM) return;
|
||||
|
||||
// grad_w + grad_h_t — thread role c = hidden unit.
|
||||
const float h_bc = h_t[batch * HIDDEN_DIM + c];
|
||||
float gh_acc = 0.0f;
|
||||
#pragma unroll
|
||||
for (int a = 0; a < N_ACTIONS; ++a) {
|
||||
const float g_a = s_gl[a];
|
||||
grad_w_per_batch[(long long) batch * N_ACTIONS * HIDDEN_DIM
|
||||
+ (long long) a * HIDDEN_DIM + c]
|
||||
= g_a * h_bc;
|
||||
gh_acc += g_a * w[a * HIDDEN_DIM + c];
|
||||
}
|
||||
// OVERWRITE — caller folds via grad_h_accumulate_scaled.
|
||||
grad_h_t[batch * HIDDEN_DIM + c] = gh_acc;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// ppo_log_ratio_abs_max_b.cu — per-batch max |log π_new − log π_old|
|
||||
// reduced to a single scalar written to
|
||||
// ISV[RL_PPO_LOG_RATIO_ABS_MAX_INDEX = 441].
|
||||
//
|
||||
// Diagnostic-only kernel. Tells the diag JSONL what the largest raw
|
||||
// log-ratio in this batch was BEFORE the kernel-side clamp in
|
||||
// ppo_clipped_surrogate.cu applied. Useful for:
|
||||
//
|
||||
// * Confirming the ratio clamp is firing only when log_ratio_abs_max
|
||||
// exceeds log(isv[RL_PPO_RATIO_CLAMP_MAX_INDEX]) — same comparison
|
||||
// as scaled_pre_clamp_max vs reward clamp bound.
|
||||
// * Surfacing the actual size of policy excursions step-to-step so
|
||||
// we can spot whether the rl_ppo_clip_controller is keeping KL
|
||||
// reasonable.
|
||||
// * Cross-checking the rl_kl_approx_b mean-KL estimator against the
|
||||
// PER-BATCH MAX |log ratio| — KL EMA can stay small while one
|
||||
// outlier batch entry has a huge log ratio (the outlier the clamp
|
||||
// catches).
|
||||
//
|
||||
// Same tree-reduce shape as rl_kl_approx_b and rl_l2_norm: single
|
||||
// block, grid-stride loop into per-thread local max, then in-block
|
||||
// reduction. No atomics per `pearl_no_atomicadd`.
|
||||
|
||||
#define RL_PPO_LOG_RATIO_ABS_MAX_INDEX 441
|
||||
|
||||
extern "C" __global__ void ppo_log_ratio_abs_max_b(
|
||||
const float* __restrict__ log_pi_old, // [b_size]
|
||||
const float* __restrict__ log_pi_new, // [b_size]
|
||||
float* __restrict__ isv, // [≥ 442]
|
||||
int b_size
|
||||
) {
|
||||
extern __shared__ float s[];
|
||||
const int tid = threadIdx.x;
|
||||
|
||||
// Per-thread: scan strided over b_size, accumulate local max |diff|.
|
||||
float local_max = 0.0f;
|
||||
for (int b = tid; b < b_size; b += blockDim.x) {
|
||||
const float diff = log_pi_new[b] - log_pi_old[b];
|
||||
const float a = fabsf(diff);
|
||||
if (a > local_max) local_max = a;
|
||||
}
|
||||
s[tid] = local_max;
|
||||
__syncthreads();
|
||||
|
||||
// Tree reduce max — block dim must be power of 2; caller enforces.
|
||||
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) {
|
||||
const float a = s[tid];
|
||||
const float b = s[tid + stride];
|
||||
s[tid] = a > b ? a : b;
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
isv[RL_PPO_LOG_RATIO_ABS_MAX_INDEX] = s[0];
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// ppo_loss_reduce_b.cu — [B] → [1] mean reducer for the two PPO loss
|
||||
// scalars produced by `ppo_clipped_surrogate_fwd`.
|
||||
//
|
||||
// Replaces the prior `atomicAdd(loss_pi, l_pi/B)` pattern. Per
|
||||
// `feedback_no_atomicadd.md`: block tree-reduce in shared memory only,
|
||||
// no atomics. The forward kernel writes per-batch values to
|
||||
// `loss_pi_per_b[B]` and `loss_entropy_per_b[B]` (single writer per
|
||||
// slot), then this kernel reduces each to a scalar mean.
|
||||
//
|
||||
// Computes:
|
||||
// loss_pi_out[0] = (1/B) * Σ_b loss_pi_per_b[b]
|
||||
// loss_entropy_out[0] = (1/B) * Σ_b loss_entropy_per_b[b]
|
||||
//
|
||||
// Block layout:
|
||||
// grid = (1, 1, 1)
|
||||
// block = (BLOCK_X, 1, 1) (BLOCK_X = 256; power of 2 required)
|
||||
// shared = 2 * BLOCK_X * sizeof(float)
|
||||
//
|
||||
// Caller passes B = batch size; the kernel grid-strides if B > BLOCK_X
|
||||
// (so a single launch supports any cluster batch size).
|
||||
|
||||
#define BLOCK_X 256
|
||||
|
||||
extern "C" __global__ void ppo_loss_reduce_b(
|
||||
const float* __restrict__ loss_pi_per_b, // [B]
|
||||
const float* __restrict__ loss_entropy_per_b, // [B]
|
||||
int B,
|
||||
float* __restrict__ loss_pi_out, // [1] OVERWRITE
|
||||
float* __restrict__ loss_entropy_out // [1] OVERWRITE
|
||||
) {
|
||||
__shared__ float s_pi[BLOCK_X];
|
||||
__shared__ float s_ent[BLOCK_X];
|
||||
|
||||
const int tid = threadIdx.x;
|
||||
|
||||
// Per-thread grid-stride sum over the [B] axis.
|
||||
float local_pi = 0.0f;
|
||||
float local_ent = 0.0f;
|
||||
for (int b = tid; b < B; b += BLOCK_X) {
|
||||
local_pi += loss_pi_per_b[b];
|
||||
local_ent += loss_entropy_per_b[b];
|
||||
}
|
||||
s_pi[tid] = local_pi;
|
||||
s_ent[tid] = local_ent;
|
||||
__syncthreads();
|
||||
|
||||
// Block tree-reduce in shared memory. BLOCK_X must be a power of 2.
|
||||
for (int stride = BLOCK_X / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) {
|
||||
s_pi[tid] += s_pi[tid + stride];
|
||||
s_ent[tid] += s_ent[tid + stride];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
// Single-writer store of the batch-mean diagnostics.
|
||||
if (tid == 0) {
|
||||
const float b_inv = 1.0f / (float)((B > 0) ? B : 1);
|
||||
loss_pi_out[0] = s_pi[0] * b_inv;
|
||||
loss_entropy_out[0] = s_ent[0] * b_inv;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// mean_reduce_b_f32 — single-input variant of the same pattern. Used by
|
||||
// `dqn_distributional_q_bwd` whose per-batch CE values live in
|
||||
// `loss_per_batch[B]` (single-writer already); this kernel produces
|
||||
// the scalar mean for the trainer's diagnostic readout, replacing the
|
||||
// prior `atomicAdd(loss_out, ce / B)` (Phase C "loud-flagged deferral"
|
||||
// — see dqn_distributional_q.cu header).
|
||||
//
|
||||
// Block layout: identical to `ppo_loss_reduce_b`.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
extern "C" __global__ void mean_reduce_b_f32(
|
||||
const float* __restrict__ values_per_b, // [B]
|
||||
int B,
|
||||
float* __restrict__ mean_out // [1] OVERWRITE
|
||||
) {
|
||||
__shared__ float s_val[BLOCK_X];
|
||||
|
||||
const int tid = threadIdx.x;
|
||||
|
||||
float local = 0.0f;
|
||||
for (int b = tid; b < B; b += BLOCK_X) {
|
||||
local += values_per_b[b];
|
||||
}
|
||||
s_val[tid] = local;
|
||||
__syncthreads();
|
||||
|
||||
for (int stride = BLOCK_X / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) {
|
||||
s_val[tid] += s_val[tid + stride];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
const float b_inv = 1.0f / (float)((B > 0) ? B : 1);
|
||||
mean_out[0] = s_val[0] * b_inv;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// projection.cu — h_128 -> z_8 with affine + layer-norm.
|
||||
//
|
||||
// pre[j] = sum_i w[j, i] * h[i] + b[j] for j in 0..8
|
||||
// mean = (1/8) * sum_j pre[j]
|
||||
// var = (1/8) * sum_j (pre[j] - mean)^2
|
||||
// out[j] = ln_gain[j] * (pre[j] - mean) * rsqrt(var + 1e-6) + ln_bias[j]
|
||||
//
|
||||
// Single block, 8 threads (one per output unit). Mean and variance
|
||||
// computed by a single thread (block-coordinated) to keep the kernel
|
||||
// simple — the cost is negligible for 8 outputs. No atomicAdd anywhere.
|
||||
|
||||
extern "C" __global__ void projection_kernel(
|
||||
const float* __restrict__ w, // [8, 128]
|
||||
const float* __restrict__ b, // [8]
|
||||
const float* __restrict__ ln_gain, // [8]
|
||||
const float* __restrict__ ln_bias, // [8]
|
||||
const float* __restrict__ h, // [128]
|
||||
float* __restrict__ out // [8]
|
||||
) {
|
||||
__shared__ float pre[8];
|
||||
__shared__ float mean_var[2]; // [mean, var]
|
||||
|
||||
int j = threadIdx.x;
|
||||
if (j < 8) {
|
||||
float v = b[j];
|
||||
for (int i = 0; i < 128; ++i) {
|
||||
v += w[j * 128 + i] * h[i];
|
||||
}
|
||||
pre[j] = v;
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (j == 0) {
|
||||
float s = 0.0f, ss = 0.0f;
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
s += pre[k];
|
||||
ss += pre[k] * pre[k];
|
||||
}
|
||||
const float mean = s * 0.125f; // /8
|
||||
const float var = ss * 0.125f - mean * mean;
|
||||
mean_var[0] = mean;
|
||||
mean_var[1] = fmaxf(var, 1e-6f);
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
if (j < 8) {
|
||||
const float mean = mean_var[0];
|
||||
const float invstd = rsqrtf(mean_var[1]);
|
||||
out[j] = ln_gain[j] * (pre[j] - mean) * invstd + ln_bias[j];
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// reduce_axis0.cu — sum [B, N] → [N] along the leading axis.
|
||||
//
|
||||
// Layout: each block reduces TILE_J=32 contiguous columns of `out`,
|
||||
// using a 32×8 thread grid. The (tx) dimension covers the column tile
|
||||
// (output index `j`), the (ty) dimension covers a B-stride loop.
|
||||
//
|
||||
// DRAM access pattern:
|
||||
// thread (tx, ty) reads per_batch[(b_base + ty*step + s) * n_tail + (j_base + tx)]
|
||||
// for s = 0..(n_batch/TILE_B), where (tx varies, ty fixed) walks a warp.
|
||||
// → 32 consecutive `j` per warp → COALESCED 128-byte transactions.
|
||||
//
|
||||
// vs the prior block-per-column layout where adjacent threads strided
|
||||
// the input by `n_tail` (often >40K floats = 160KB stride), forcing
|
||||
// one cache line per thread and wasting 8× HBM bandwidth.
|
||||
//
|
||||
// Block reduce: per-thread partial sums land in s[ty][tx]; the first
|
||||
// warp (ty=0) sums the 8 partials per column and writes `out[j]`.
|
||||
// No atomicAdd, no warp-shuffle across warps; block-tree-reduce only.
|
||||
//
|
||||
// Launch contract:
|
||||
// grid_dim = (ceil(n_tail / 32), 1, 1)
|
||||
// block_dim = (32, 8, 1)
|
||||
// shared = 0 (compile-time static)
|
||||
|
||||
#define REDAX0_TILE_J 32
|
||||
#define REDAX0_TILE_B 8
|
||||
#define REDAX0_BLOCK (REDAX0_TILE_J * REDAX0_TILE_B) // 256 threads/block
|
||||
|
||||
extern "C" __global__ __launch_bounds__(REDAX0_BLOCK, 4)
|
||||
void reduce_axis0(
|
||||
const float* __restrict__ per_batch, // [B, N]
|
||||
int n_batch,
|
||||
int n_tail,
|
||||
float* __restrict__ out // [N] — OVERWRITE
|
||||
) {
|
||||
const int tx = threadIdx.x; // column-tile lane
|
||||
const int ty = threadIdx.y; // batch-tile lane
|
||||
const int j = (int)blockIdx.x * REDAX0_TILE_J + tx; // output column
|
||||
const bool active = (j < n_tail);
|
||||
|
||||
float my_sum = 0.0f;
|
||||
if (active) {
|
||||
// Stride-loop along B in chunks of TILE_B. Within a warp (fixed ty,
|
||||
// varying tx) we step `j` by +1 → coalesced 32-float load per warp.
|
||||
for (int bi = ty; bi < n_batch; bi += REDAX0_TILE_B) {
|
||||
my_sum += per_batch[(long long)bi * n_tail + j];
|
||||
}
|
||||
}
|
||||
|
||||
// +1 pad eliminates 32-way shared-mem bank conflict on the ty reduce.
|
||||
__shared__ float s[REDAX0_TILE_B][REDAX0_TILE_J + 1];
|
||||
s[ty][tx] = my_sum;
|
||||
__syncthreads();
|
||||
|
||||
// First warp sums the 8 per-batch-tile partials for its column.
|
||||
if (ty == 0 && active) {
|
||||
float total = 0.0f;
|
||||
#pragma unroll
|
||||
for (int t = 0; t < REDAX0_TILE_B; ++t) {
|
||||
total += s[t][tx];
|
||||
}
|
||||
out[j] = total;
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
// rl_action_kernel.cu — GPU-resident Thompson sampler over the C51
|
||||
// distributional Q-head output (Phase R4 of the integrated RL trainer
|
||||
// rebuild; see
|
||||
// docs/superpowers/plans/2026-05-23-integrated-rl-trainer-rebuild.md).
|
||||
//
|
||||
// Replaces the host-side Thompson loop the flawed Phase F shipped in
|
||||
// step_with_lobsim (which violated `feedback_cpu_is_read_only` by
|
||||
// DtoH-copying q_logits + sampling on CPU + HtoD-copying actions back
|
||||
// to device).
|
||||
//
|
||||
// Per `pearl_thompson_for_distributional_action_selection`: Thompson
|
||||
// is the canonical rollout selector under C51. For each batch + each
|
||||
// action, sample ONE atom from that action's categorical (softmax
|
||||
// over atom logits) and use the sampled atom's support value as the
|
||||
// "sampled return"; pick the action with the highest sampled return.
|
||||
// The companion `argmax_expected_q.cu` kernel does the Bellman-target
|
||||
// argmax (over expected Q, not sampled).
|
||||
//
|
||||
// PRNG: per-batch xorshift32 state in `prng_state_d` (allocated +
|
||||
// host-seeded once at trainer init per
|
||||
// `pearl_scoped_init_seed_for_reproducibility`). To avoid the
|
||||
// inter-thread race that a shared per-batch state would create, each
|
||||
// per-action thread XORs the action index (via golden-ratio constant)
|
||||
// into a thread-local copy of the per-batch state, advances locally
|
||||
// for the atom sample, and only thread 0 writes the advanced per-batch
|
||||
// state back. Reproducibility holds: identical (per-batch seed,
|
||||
// blockDim, action grid, q_logits) → identical actions.
|
||||
//
|
||||
// Per `feedback_no_atomicadd`: thread 0 writes to `actions[b]` after
|
||||
// __syncthreads. Per `pearl_no_host_branches_in_captured_graph`: no
|
||||
// host branches; the only branches inside the kernel are device-side
|
||||
// data-dependent (CDF walk, argmax).
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define N_ACTIONS 11
|
||||
#define Q_N_ATOMS 21
|
||||
|
||||
__device__ static uint32_t xorshift32(uint32_t* state) {
|
||||
uint32_t x = *state;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
*state = x;
|
||||
return x;
|
||||
}
|
||||
|
||||
// One block per batch (grid_dim.x = b_size). N_ACTIONS threads per
|
||||
// block; each thread handles one action's atom sample, writes its
|
||||
// sampled return to shared mem, then thread 0 argmaxes + writes
|
||||
// actions[b] + advances the per-batch PRNG state.
|
||||
//
|
||||
// Inputs:
|
||||
// q_logits [b_size, N_ACTIONS, Q_N_ATOMS] row-major
|
||||
// atom_supports [Q_N_ATOMS] e.g. linspace(Q_V_MIN, Q_V_MAX, Q_N_ATOMS)
|
||||
// prng_state [b_size] per-batch xorshift32 state (mutated in place)
|
||||
// Outputs:
|
||||
// actions [b_size] Thompson-sampled action index per batch
|
||||
extern "C" __global__ void rl_action_kernel(
|
||||
const float* __restrict__ q_logits,
|
||||
const float* __restrict__ atom_supports,
|
||||
uint32_t* __restrict__ prng_state,
|
||||
int* __restrict__ actions,
|
||||
int b_size
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
const int a = threadIdx.x;
|
||||
if (b >= b_size) return;
|
||||
if (a >= N_ACTIONS) return;
|
||||
|
||||
__shared__ float sampled_returns[N_ACTIONS];
|
||||
|
||||
// Derive thread-local PRNG state from the per-batch seed.
|
||||
// Golden-ratio constant decorrelates per-thread streams.
|
||||
const uint32_t base = prng_state[b];
|
||||
uint32_t local_state = base ^ ((uint32_t)a * 0x9E3779B1u);
|
||||
// A few warmup advances dispel correlations from the cheap mixing.
|
||||
#pragma unroll
|
||||
for (int w = 0; w < 4; ++w) xorshift32(&local_state);
|
||||
|
||||
// Softmax over this action's atoms (numerically stabilised).
|
||||
const int row_off = (b * N_ACTIONS + a) * Q_N_ATOMS;
|
||||
float max_l = -INFINITY;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < Q_N_ATOMS; ++i) {
|
||||
const float l = q_logits[row_off + i];
|
||||
if (l > max_l) max_l = l;
|
||||
}
|
||||
float sum_exp = 0.0f;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < Q_N_ATOMS; ++i) {
|
||||
sum_exp += expf(q_logits[row_off + i] - max_l);
|
||||
}
|
||||
// Guard against numerical degeneracy (all-NaN or all-inf logits).
|
||||
if (!isfinite(sum_exp) || sum_exp <= 0.0f) sum_exp = 1.0f;
|
||||
|
||||
// Categorical CDF walk against u ∈ [0, 1). Walk-to-end fallback
|
||||
// catches the case where cumulative-sum rounding edges below 1.0.
|
||||
const uint32_t r = xorshift32(&local_state);
|
||||
const float u = (float)r / 4294967296.0f;
|
||||
float cum = 0.0f;
|
||||
int chosen = Q_N_ATOMS - 1;
|
||||
for (int i = 0; i < Q_N_ATOMS; ++i) {
|
||||
cum += expf(q_logits[row_off + i] - max_l) / sum_exp;
|
||||
if (cum >= u) {
|
||||
chosen = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sampled_returns[a] = atom_supports[chosen];
|
||||
__syncthreads();
|
||||
|
||||
// Thread 0: argmax over per-action sampled returns; write action;
|
||||
// advance per-batch PRNG state for next call.
|
||||
if (a == 0) {
|
||||
int best_a = 0;
|
||||
float best_v = sampled_returns[0];
|
||||
#pragma unroll
|
||||
for (int i = 1; i < N_ACTIONS; ++i) {
|
||||
if (sampled_returns[i] > best_v) {
|
||||
best_v = sampled_returns[i];
|
||||
best_a = i;
|
||||
}
|
||||
}
|
||||
actions[b] = best_a;
|
||||
|
||||
uint32_t adv = base;
|
||||
xorshift32(&adv);
|
||||
prng_state[b] = adv;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// rl_advantage_normalize.cu — Phase 4.5 per-batch advantage normalization (2026-05-30).
|
||||
//
|
||||
// Standard PPO practice (Schulman et al. 2017): normalize per-batch
|
||||
// advantages before the PPO surrogate computation:
|
||||
//
|
||||
// mean = (1/B) Σ_b advantage[b]
|
||||
// var = (1/B) Σ_b (advantage[b] − mean)²
|
||||
// std = sqrt(var + ε²)
|
||||
// advantage_norm[b] = (advantage[b] − mean) / std (in-place)
|
||||
//
|
||||
// Why: PPO surrogate = ratio × A. Without normalization, |A| can vary
|
||||
// 1000× across batches → l_pi magnitude unstable → Adam's per-parameter
|
||||
// scaling still functional, but gradient direction confidence drops
|
||||
// when advantage magnitudes are wildly inconsistent.
|
||||
//
|
||||
// In Phase 4.3, V_dq baseline produced advantages with ~100× more
|
||||
// variance than Plan A v2's V_scalar baseline → l_pi grew to 1.6e9
|
||||
// vs Plan A v2's 3.6e5. Adam internally normalizes, but the policy
|
||||
// updates have higher variance per step → pnl trajectory chops.
|
||||
//
|
||||
// Per pearl_adaptive_not_tuned: this normalization is self-adaptive
|
||||
// (uses observed per-batch statistics, no tuned hyperparameters).
|
||||
// Per pearl_blend_formulas_must_have_permanent_floor: ε² floor on
|
||||
// variance prevents div-by-zero when all advantages are identical.
|
||||
//
|
||||
// Block layout: grid=(1, 1, 1), block=(BLOCK_X=1024, 1, 1). Single
|
||||
// block does parallel reduction for mean → variance → normalize.
|
||||
// Suitable for batch sizes up to B=1024 (current production scale).
|
||||
|
||||
#define BLOCK_X 1024
|
||||
#define ADVANTAGE_VAR_FLOOR 1e-6f
|
||||
|
||||
// Adaptive controller floors (spec 2026-05-30-adaptive-controller-floor-design):
|
||||
// emit raw per-batch advantage variance to ISV[612] BEFORE the in-place
|
||||
// normalize. `rl_rollout_steps_controller` consumes this as its driving
|
||||
// signal — post-normalization variance is definitionally ~ε² floor under
|
||||
// Phase 4.5 and useless as a controller input. Pre-norm variance is the
|
||||
// real signal of "how much advantage spread the policy is producing".
|
||||
#define RL_ADV_VAR_PRE_NORM_INDEX 612
|
||||
|
||||
extern "C" __global__ void rl_advantage_normalize(
|
||||
float* __restrict__ advantage, // [B] in-place
|
||||
float* __restrict__ isv, // ISV bus — only ADV_VAR_PRE_NORM written
|
||||
int B
|
||||
) {
|
||||
const int tid = threadIdx.x;
|
||||
if (tid >= BLOCK_X) return;
|
||||
|
||||
// ── Pass 1: compute mean ──
|
||||
__shared__ float s_sum[BLOCK_X];
|
||||
float sum_partial = 0.0f;
|
||||
for (int b = tid; b < B; b += BLOCK_X) {
|
||||
sum_partial += advantage[b];
|
||||
}
|
||||
s_sum[tid] = sum_partial;
|
||||
__syncthreads();
|
||||
|
||||
for (int stride = BLOCK_X / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) s_sum[tid] += s_sum[tid + stride];
|
||||
__syncthreads();
|
||||
}
|
||||
__shared__ float s_mean;
|
||||
if (tid == 0) s_mean = s_sum[0] / (float)B;
|
||||
__syncthreads();
|
||||
const float mean = s_mean;
|
||||
|
||||
// ── Pass 2: compute variance ──
|
||||
__shared__ float s_var[BLOCK_X];
|
||||
float var_partial = 0.0f;
|
||||
for (int b = tid; b < B; b += BLOCK_X) {
|
||||
const float d = advantage[b] - mean;
|
||||
var_partial += d * d;
|
||||
}
|
||||
s_var[tid] = var_partial;
|
||||
__syncthreads();
|
||||
|
||||
for (int stride = BLOCK_X / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) s_var[tid] += s_var[tid + stride];
|
||||
__syncthreads();
|
||||
}
|
||||
__shared__ float s_inv_std;
|
||||
if (tid == 0) {
|
||||
const float var = s_var[0] / (float)B;
|
||||
// Emit raw pre-normalization variance for rl_rollout_steps_controller.
|
||||
// Done BEFORE we add the ε² floor — the controller wants the true
|
||||
// signal, not the numerical-stability-adjusted divisor.
|
||||
isv[RL_ADV_VAR_PRE_NORM_INDEX] = var;
|
||||
s_inv_std = rsqrtf(var + ADVANTAGE_VAR_FLOOR); // 1/sqrt(var + ε²)
|
||||
}
|
||||
__syncthreads();
|
||||
const float inv_std = s_inv_std;
|
||||
|
||||
// ── Pass 3: normalize in-place ──
|
||||
for (int b = tid; b < B; b += BLOCK_X) {
|
||||
advantage[b] = (advantage[b] - mean) * inv_std;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Adversarial regime injection: boost PER priority on negative-reward
|
||||
// transitions to increase replay sampling of losing experiences.
|
||||
//
|
||||
// Self-regulating: as the agent improves, fewer negative-reward steps
|
||||
// are generated, so fewer boosts are applied. The boost magnitude is
|
||||
// ISV-driven (slot 607) so it adapts with the training regime.
|
||||
//
|
||||
// Grid=(1), Block=(min(B, 256)). Runs once per step after experience
|
||||
// collection, before the next PER prefix scan.
|
||||
//
|
||||
// ISV slot layout (literal indices; canonical names in Rust-side
|
||||
// adversarial_isv_slots module):
|
||||
// 606 RL_ADVERSARIAL_DD_THRESHOLD (reserved, unused in V1)
|
||||
// 607 RL_ADVERSARIAL_BOOST boost multiplier (default 1.5)
|
||||
// 608 RL_MAX_DD_EMA (reserved, unused in V1)
|
||||
|
||||
extern "C" __global__ void rl_adversarial_boost(
|
||||
const float* __restrict__ rewards, // [B] current step rewards
|
||||
float* __restrict__ priorities_pa, // [capacity] flat PER sampling weights
|
||||
const float* __restrict__ isv, // ISV bus (read boost factor)
|
||||
int write_cursor, // host-side write position (post-insert)
|
||||
int b_size, // batch size (number of transitions just inserted)
|
||||
int capacity // replay buffer capacity
|
||||
)
|
||||
{
|
||||
int b = threadIdx.x;
|
||||
if (b >= b_size) return;
|
||||
|
||||
// Read boost factor from ISV[607]. Cold-start sentinel: if ISV is
|
||||
// null or the slot is zero/negative, use 1.0 (no boost = no-op).
|
||||
float boost = 1.0f;
|
||||
if (isv != nullptr) {
|
||||
float raw = isv[607];
|
||||
// Sane range: boost in [1.0, 5.0]. Below 1.0 = no boost.
|
||||
// Above 5.0 = cap to prevent priority explosion.
|
||||
boost = fmaxf(1.0f, fminf(5.0f, raw));
|
||||
}
|
||||
|
||||
// Only boost negative-reward transitions.
|
||||
if (rewards[b] >= 0.0f) return;
|
||||
|
||||
// Map batch index b to the replay buffer entry that was just written.
|
||||
// The caller passes write_cursor AFTER the insert, so the b_size
|
||||
// entries occupy [write_cursor - b_size .. write_cursor) mod capacity.
|
||||
// Entry for batch element b:
|
||||
int entry = (write_cursor - b_size + b) % capacity;
|
||||
// Handle negative modulo (C/CUDA % can return negative for negative dividend).
|
||||
if (entry < 0) entry += capacity;
|
||||
|
||||
// Multiply the PER sampling weight by the boost factor.
|
||||
// This makes negative-reward transitions more likely to be sampled
|
||||
// in the next PER proportional-sampling pass.
|
||||
//
|
||||
// Each thread writes to a unique entry (the b→entry mapping is
|
||||
// injective within the batch), so no synchronization needed.
|
||||
float old_pa = priorities_pa[entry];
|
||||
priorities_pa[entry] = old_pa * boost;
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// rl_asymmetric_trail_decay.cu — structural asymmetric trail management.
|
||||
//
|
||||
// Runs EVERY STEP for each active unit. Automatically adjusts trail
|
||||
// distance based on unrealized P&L direction:
|
||||
//
|
||||
// LOSING (mid moved against entry):
|
||||
// trail *= loss_decay_rate per step (default 0.995)
|
||||
// → halves in ~139 steps (~35 seconds)
|
||||
// "Cut losers fast" — the longer a trade stays underwater,
|
||||
// the tighter the stop gets, accelerating the exit.
|
||||
//
|
||||
// WINNING (unrealized > initial_r):
|
||||
// trail = max(trail, unrealized_profit × win_trail_factor)
|
||||
// → trail ratchets up with profit, never below current level
|
||||
// "Let winners run" — the stop tracks 50% of open profit,
|
||||
// locking in gains while giving room for continuation.
|
||||
//
|
||||
// NEUTRAL (between 0 and initial_r):
|
||||
// trail unchanged — in the "proving zone" where the trade
|
||||
// hasn't yet earned the right to a wider stop.
|
||||
//
|
||||
// This produces asymmetric P&L without the agent needing to LEARN
|
||||
// when to tighten vs loosen — it's a structural edge baked into
|
||||
// the mechanics. The agent's a7/a8 trail actions provide additional
|
||||
// fine-tuning on top.
|
||||
//
|
||||
// Runs BEFORE rl_trail_stop_check so the updated trail distances
|
||||
// are immediately used for breach detection.
|
||||
//
|
||||
// Per `feedback_no_atomicadd`: per-batch per-unit element-wise.
|
||||
// Per `feedback_cpu_is_read_only`: pure device-side.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define MAX_UNITS 4
|
||||
#define RL_ASYM_LOSS_DECAY_RATE_INDEX 537
|
||||
#define RL_ASYM_WIN_TRAIL_FACTOR_INDEX 538
|
||||
#define RL_ASYM_WIN_THRESHOLD_INDEX 546
|
||||
#define RL_TRAIL_MIN_INDEX 494
|
||||
|
||||
extern "C" __global__ void rl_asymmetric_trail_decay(
|
||||
float* __restrict__ unit_trail_distance, // [B × MAX_UNITS] IN/OUT
|
||||
const unsigned char* __restrict__ unit_active, // [B × MAX_UNITS]
|
||||
const float* __restrict__ unit_entry_price, // [B × MAX_UNITS]
|
||||
const float* __restrict__ unit_initial_r, // [B × MAX_UNITS]
|
||||
const int* __restrict__ unit_lots, // [B × MAX_UNITS]
|
||||
const float* __restrict__ bid_px, // [BOOK_LEVELS]
|
||||
const float* __restrict__ ask_px, // [BOOK_LEVELS]
|
||||
const float* __restrict__ isv,
|
||||
int b_size
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
const int u = threadIdx.x;
|
||||
if (b >= b_size || u >= MAX_UNITS) return;
|
||||
|
||||
const int idx = b * MAX_UNITS + u;
|
||||
if (unit_active[idx] == 0) return;
|
||||
|
||||
const int lots = unit_lots[idx];
|
||||
if (lots == 0) return;
|
||||
|
||||
const float entry = unit_entry_price[idx];
|
||||
const float trail = unit_trail_distance[idx];
|
||||
const float init_r = unit_initial_r[idx];
|
||||
const float mid = 0.5f * (bid_px[0] + ask_px[0]);
|
||||
|
||||
const float loss_decay = isv[RL_ASYM_LOSS_DECAY_RATE_INDEX];
|
||||
const float win_factor = isv[RL_ASYM_WIN_TRAIL_FACTOR_INDEX];
|
||||
const float win_threshold = isv[RL_ASYM_WIN_THRESHOLD_INDEX];
|
||||
const float trail_min = isv[RL_TRAIL_MIN_INDEX];
|
||||
|
||||
// Unrealized P&L in price units (positive = profitable).
|
||||
const float direction = (lots > 0) ? 1.0f : -1.0f;
|
||||
const float unrealized = direction * (mid - entry);
|
||||
|
||||
float new_trail = trail;
|
||||
|
||||
if (unrealized < 0.0f) {
|
||||
// LOSING: tighten every step.
|
||||
new_trail = trail * loss_decay;
|
||||
} else if (unrealized > init_r * win_threshold) {
|
||||
// WINNING beyond initial R: ratchet trail to track profit.
|
||||
// Trail = max(current_trail, profit × win_factor).
|
||||
// Never shrinks on winners — only grows.
|
||||
const float profit_trail = unrealized * win_factor;
|
||||
if (profit_trail > new_trail) {
|
||||
new_trail = profit_trail;
|
||||
}
|
||||
}
|
||||
// NEUTRAL (0 to init_r): trail unchanged — proving zone.
|
||||
|
||||
// Floor at trail_min.
|
||||
new_trail = fmaxf(new_trail, trail_min);
|
||||
|
||||
unit_trail_distance[idx] = new_trail;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// rl_atom_support_update.cu — refresh `atom_supports_d` from the
|
||||
// ISV-driven [V_MIN, V_MAX] span (audit 2026-05-24 second follow-up).
|
||||
//
|
||||
// Companion to the C51 atom-span EWMA in rl_reward_clamp_controller
|
||||
// (α=0.001, ~700-step half-life — wwcsz followup 2026-05-24 replaced
|
||||
// the original ratchet to focus atom resolution on the ACTIVE range).
|
||||
// `atom_supports_d` is the device buffer of 21 float values that the
|
||||
// non-projection C51 kernels (`argmax_expected_q`, `rl_action_kernel`,
|
||||
// `dqn_distributional_q`) read instead of recomputing the per-atom
|
||||
// values themselves. When V_MIN/V_MAX adapt, this buffer must be
|
||||
// rewritten or those kernels see a stale span.
|
||||
//
|
||||
// One block, Q_N_ATOMS threads. Each thread writes one atom value:
|
||||
// atom_supports[i] = V_MIN + i * (V_MAX - V_MIN) / (N_ATOMS - 1)
|
||||
//
|
||||
// Per `feedback_no_atomicadd`: no atomics. Per `feedback_cpu_is_read_only`:
|
||||
// pure device write. Per `feedback_no_htod_htoh_only_mapped_pinned`: no
|
||||
// host transfer — values come from ISV slots, written from the trainer
|
||||
// per-step launch right after the reward clamp controller refreshes
|
||||
// V_MIN/V_MAX.
|
||||
|
||||
#define Q_N_ATOMS 21
|
||||
#define RL_C51_V_MAX_INDEX 484
|
||||
#define RL_C51_V_MIN_INDEX 485
|
||||
|
||||
extern "C" __global__ void rl_atom_support_update(
|
||||
const float* __restrict__ isv, // ≥ RL_C51_V_MAX_INDEX + 1
|
||||
float* __restrict__ atom_supports // [Q_N_ATOMS]
|
||||
) {
|
||||
const int tid = threadIdx.x;
|
||||
if (tid >= Q_N_ATOMS) return;
|
||||
|
||||
const float v_min = isv[RL_C51_V_MIN_INDEX];
|
||||
const float v_max = isv[RL_C51_V_MAX_INDEX];
|
||||
const float delta = (v_max - v_min) / (float)(Q_N_ATOMS - 1);
|
||||
|
||||
atom_supports[tid] = v_min + (float)tid * delta;
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
// rl_avg_win_loss_ema_update.cu — Layer 4 (Kelly) input EMA producer.
|
||||
//
|
||||
// Tracks observed per-trade avg_win and avg_loss magnitudes in USD as
|
||||
// EMAs. Kelly fraction uses R-multiple = avg_win / avg_loss together
|
||||
// with win_rate to derive the safe Kelly bet size.
|
||||
// Spec: docs/superpowers/specs/2026-05-30-adaptive-risk-management-design.md
|
||||
//
|
||||
// Inputs:
|
||||
// rewards[b] f32 — per-batch realized pnl delta (shaped) this step
|
||||
// dones[b] f32 — 1.0 if a trade closed this step, else 0.0
|
||||
//
|
||||
// Outcome derived inline: win = (done & reward > 0); loss = (done & reward < 0).
|
||||
//
|
||||
// Outputs:
|
||||
// ISV[RL_AVG_WIN_USD_EMA_INDEX = 678] (positive USD)
|
||||
// ISV[RL_AVG_LOSS_USD_EMA_INDEX = 679] (positive USD, magnitude)
|
||||
//
|
||||
// Bootstrap semantics per `pearl_first_observation_bootstrap`:
|
||||
// sentinel = 0.0; first non-zero closed-trade step replaces directly.
|
||||
//
|
||||
// Per `feedback_no_atomicadd`: single-thread single-block (sums sequentially).
|
||||
// Per `feedback_cpu_is_read_only`: pure device kernel.
|
||||
// Per `feedback_isv_for_adaptive_bounds`: EMA-α structural smoothing
|
||||
// parameter (same as ema_update_per_step convention).
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define RL_AVG_WIN_USD_EMA_INDEX 678
|
||||
#define RL_AVG_LOSS_USD_EMA_INDEX 679
|
||||
#define EMA_ALPHA_FAST 0.05f
|
||||
// B-6 ISV-driven adaptive asymmetric Wiener-α (2026-06-01 spec).
|
||||
// α_slow_eff = α_slow_min + (α_fast − α_slow_min) × trust_eff
|
||||
// trust_eff = trust(n) × stability(CV)
|
||||
// trust(n) = min(1, cum_dones / n_full_threshold) [Phase 1]
|
||||
// stability(CV) = exp(-CV × cv_gain) [Phase 2]
|
||||
// Slot 721 α_slow_min, 722 n_full_threshold, 723 cv_gain.
|
||||
// Welford triplet for reward_magnitude provides CV (Phase 2).
|
||||
#define RL_EMA_ALPHA_SLOW_MIN_INDEX 721
|
||||
#define RL_EMA_TRUST_FULL_THRESHOLD_INDEX 722
|
||||
#define RL_EMA_CV_GAIN_INDEX 723
|
||||
#define RL_CUMULATIVE_DONES_INDEX 660
|
||||
#define RL_REWARD_MAG_VAR_COUNT_INDEX 615
|
||||
#define RL_REWARD_MAG_VAR_MEAN_INDEX 616
|
||||
#define RL_REWARD_MAG_VAR_M2_INDEX 617
|
||||
|
||||
extern "C" __global__ void rl_avg_win_loss_ema_update(
|
||||
float* __restrict__ isv,
|
||||
const float* __restrict__ rewards, // [b_size] shaped pnl
|
||||
const float* __restrict__ dones, // [b_size] 1.0 = close
|
||||
int b_size
|
||||
) {
|
||||
if (threadIdx.x != 0 || threadIdx.y != 0 || threadIdx.z != 0) return;
|
||||
if (blockIdx.x != 0 || blockIdx.y != 0 || blockIdx.z != 0) return;
|
||||
|
||||
// Sum per-trade win/loss magnitudes across the batch.
|
||||
float sum_win = 0.0f;
|
||||
float sum_loss = 0.0f;
|
||||
int n_win = 0;
|
||||
int n_loss = 0;
|
||||
for (int b = 0; b < b_size; ++b) {
|
||||
if (dones[b] < 0.5f) continue;
|
||||
const float p = rewards[b];
|
||||
if (p > 0.0f) {
|
||||
sum_win += p;
|
||||
n_win += 1;
|
||||
} else if (p < 0.0f) {
|
||||
sum_loss += fabsf(p);
|
||||
n_loss += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// B-6 ISV-driven adaptive asymmetric Wiener-α (Bayesian shrinkage).
|
||||
// α_slow_eff blends α_slow_min → α_fast as data confidence grows.
|
||||
const float a_slow_min = isv[RL_EMA_ALPHA_SLOW_MIN_INDEX];
|
||||
const float a_fast = EMA_ALPHA_FAST;
|
||||
const float n_trades = isv[RL_CUMULATIVE_DONES_INDEX];
|
||||
const float n_full = isv[RL_EMA_TRUST_FULL_THRESHOLD_INDEX];
|
||||
const float cv_gain = isv[RL_EMA_CV_GAIN_INDEX];
|
||||
|
||||
// Phase 1: data-quantity trust (resets at fold boundary via reset_session_state)
|
||||
float trust = (n_full > 0.0f) ? fminf(1.0f, n_trades / n_full) : 1.0f;
|
||||
|
||||
// Phase 2: signal-volatility gain (set cv_gain=0 to disable)
|
||||
if (cv_gain > 0.0f) {
|
||||
const float wf_count = isv[RL_REWARD_MAG_VAR_COUNT_INDEX];
|
||||
if (wf_count > 1.0f) {
|
||||
const float wf_m2 = isv[RL_REWARD_MAG_VAR_M2_INDEX];
|
||||
const float wf_mean = isv[RL_REWARD_MAG_VAR_MEAN_INDEX];
|
||||
const float wf_var = wf_m2 / (wf_count - 1.0f);
|
||||
const float cv = (wf_mean > 1e-6f) ? sqrtf(wf_var) / wf_mean : 0.0f;
|
||||
const float stability = expf(-cv * cv_gain);
|
||||
trust *= stability;
|
||||
}
|
||||
}
|
||||
|
||||
const float a_slow_eff = a_slow_min + (a_fast - a_slow_min) * trust;
|
||||
|
||||
if (n_win > 0) {
|
||||
const float step_avg = sum_win / (float)n_win;
|
||||
const float prev = isv[RL_AVG_WIN_USD_EMA_INDEX];
|
||||
// avg_win: slow-up (skeptical of wins), fast-down (correct quickly)
|
||||
const float alpha = (step_avg > prev) ? a_slow_eff : a_fast;
|
||||
isv[RL_AVG_WIN_USD_EMA_INDEX] = (1.0f - alpha) * prev + alpha * step_avg;
|
||||
}
|
||||
|
||||
if (n_loss > 0) {
|
||||
const float step_avg = sum_loss / (float)n_loss;
|
||||
const float prev = isv[RL_AVG_LOSS_USD_EMA_INDEX];
|
||||
// avg_loss: fast-up (admit losses, safety), slow-down (slow forget)
|
||||
const float alpha = (step_avg > prev) ? a_fast : a_slow_eff;
|
||||
isv[RL_AVG_LOSS_USD_EMA_INDEX] = (1.0f - alpha) * prev + alpha * step_avg;
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// rl_band_frac_aggregate.cu — Phase 4-B per-step `frac_not_masked` reducer.
|
||||
//
|
||||
// Single-block tree-reduce over batch. Reads:
|
||||
// * band_out [B × 2] — (b_l, b_u) post-activation, lots units
|
||||
// * pos_state [B × P] — i32 position lots at offset 0
|
||||
// * isv — read-only (gate slot)
|
||||
//
|
||||
// Computes the per-batch boolean `position ∈ [b_l, b_u]` and reduces
|
||||
// `(1 - in_band)` (= "this batch is FREE to trade this step") to a single
|
||||
// scalar `frac_not_masked ∈ [0, 1]`, written to
|
||||
// `RL_BAND_FRAC_NOT_MASKED_OBSERVED_INDEX` (slot 812).
|
||||
//
|
||||
// Spec: docs/superpowers/specs/2026-06-03-no-transaction-band-architecture.md
|
||||
// §5.2 + §5.3 (fleet-fraction discipline per pearl_fleet_fraction_not_aggregate).
|
||||
//
|
||||
// Disciplines:
|
||||
// * `feedback_no_atomicadd` — tree-reduce in shared memory, single-thread
|
||||
// writes the final scalar to ISV.
|
||||
// * `pearl_determinism_achieved` — deterministic shared-mem reduction with
|
||||
// `__syncthreads()` barriers; no PRNG.
|
||||
// * `feedback_no_nvrtc` — pre-compiled cubin via build.rs.
|
||||
//
|
||||
// Launch:
|
||||
// grid = (1, 1, 1)
|
||||
// block = (block_dim, 1, 1) — power-of-two, ≥ b_size when possible
|
||||
// shared_mem_bytes = block_dim × sizeof(float)
|
||||
//
|
||||
// The host caller picks `block_dim = next_power_of_two(b_size).min(1024)`
|
||||
// — matches `rl_q_pi_agree_b`'s launch shape so the reduction is fully
|
||||
// captured in a single warp-aligned tree-reduce.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define BAND_OUT 2
|
||||
#define RL_BAND_ENABLED_INDEX 799
|
||||
#define RL_BAND_FRAC_NOT_MASKED_OBSERVED_INDEX 812
|
||||
|
||||
extern "C" __global__ void rl_band_frac_aggregate(
|
||||
const float* __restrict__ band_outputs, // [B × 2]
|
||||
const unsigned char* __restrict__ pos_state, // [B × pos_bytes]
|
||||
float* __restrict__ isv,
|
||||
int b_size,
|
||||
int pos_bytes
|
||||
) {
|
||||
extern __shared__ float s_partial[]; // [block_dim]
|
||||
|
||||
const int tid = threadIdx.x;
|
||||
const int bdim = blockDim.x;
|
||||
|
||||
// Master gate: if band disabled, write sentinel 0.0 and return — the
|
||||
// controller's master-gate guard will skip the EMA update.
|
||||
const float enabled = isv[RL_BAND_ENABLED_INDEX];
|
||||
if (enabled <= 0.5f) {
|
||||
if (tid == 0) {
|
||||
isv[RL_BAND_FRAC_NOT_MASKED_OBSERVED_INDEX] = 0.0f;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Per-thread accumulation: walk over batch indices in strides of bdim.
|
||||
float local = 0.0f;
|
||||
for (int b = tid; b < b_size; b += bdim) {
|
||||
const int position_lots =
|
||||
*reinterpret_cast<const int*>(pos_state + b * pos_bytes);
|
||||
const float pos_f = (float)position_lots;
|
||||
const float b_l = band_outputs[b * BAND_OUT + 0];
|
||||
const float b_u = band_outputs[b * BAND_OUT + 1];
|
||||
const float in_band = (pos_f >= b_l && pos_f <= b_u) ? 1.0f : 0.0f;
|
||||
local += (1.0f - in_band);
|
||||
}
|
||||
s_partial[tid] = local;
|
||||
__syncthreads();
|
||||
|
||||
// Tree-reduce.
|
||||
for (int stride = bdim / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) {
|
||||
s_partial[tid] += s_partial[tid + stride];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
const float inv_b = 1.0f / (float)b_size;
|
||||
isv[RL_BAND_FRAC_NOT_MASKED_OBSERVED_INDEX] = s_partial[0] * inv_b;
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// rl_band_head_backward.cu — Phase 4-B band-head backward chain.
|
||||
//
|
||||
// Spec: docs/superpowers/specs/2026-06-03-no-transaction-band-architecture.md
|
||||
// §3.3 (backward chain into encoder).
|
||||
//
|
||||
// Forward chain (recap, see `rl_band_head_forward.cu`):
|
||||
// band_pre[b, j] = b_band[j] + Σ_c W_band[j, c] · h_t[b, c]
|
||||
// b_l = -|tanh(band_pre[b, 0])| × N_max_eff
|
||||
// b_u = +|tanh(band_pre[b, 1])| × N_max_eff
|
||||
//
|
||||
// Backward chain (this file):
|
||||
// Given dL/d(b_l) = grad_band[b, 0] and dL/d(b_u) = grad_band[b, 1] from
|
||||
// the turnover-loss kernel:
|
||||
//
|
||||
// dL/d(band_pre[b, 0]) = grad_band[b, 0] · d(b_l)/d(pre)
|
||||
// = grad_band[b, 0] · (-sign(t_l)) · (1 - t_l²) · N_max
|
||||
// = grad_band[b, 0] · (-sign(t_l)) · sech²(pre_l) · N_max
|
||||
// dL/d(band_pre[b, 1]) = grad_band[b, 1] · d(b_u)/d(pre)
|
||||
// = grad_band[b, 1] · (+sign(t_u)) · sech²(pre_u) · N_max
|
||||
//
|
||||
// Then the linear backward:
|
||||
// dL/d(W_band[j, c]) per batch = dL/d(pre[b, j]) · h_t[b, c]
|
||||
// dL/d(b_band[j]) per batch = dL/d(pre[b, j])
|
||||
// dL/d(h_t[b, c]) = Σ_j dL/d(pre[b, j]) · W_band[j, c]
|
||||
//
|
||||
// Outputs (per-batch scratch; caller reduces via `reduce_axis0`):
|
||||
// grad_w_per_batch [B × BAND_OUT × HIDDEN_DIM]
|
||||
// grad_b_per_batch [B × BAND_OUT]
|
||||
// grad_h_t [B × HIDDEN_DIM] (OVERWRITE — caller folds into encoder
|
||||
// grad via `grad_h_accumulate_scaled`)
|
||||
//
|
||||
// Disciplines:
|
||||
// * `feedback_no_atomicadd` — per-batch scratch + reduce_axis0
|
||||
// * `feedback_no_nvrtc` — pre-compiled cubin via build.rs
|
||||
// * `pearl_determinism_achieved` — sole-writer per (b, j, c); no PRNG
|
||||
// * `pearl_no_host_branches_in_captured_graph` — only kernel args, no
|
||||
// host scalars in control flow
|
||||
//
|
||||
// Launch:
|
||||
// grid = (B, 1, 1)
|
||||
// block = (HIDDEN_DIM, 1, 1)
|
||||
// shared_mem_bytes = BAND_OUT × sizeof(float) (s_dpre[2] staging)
|
||||
//
|
||||
// Per-block: thread c writes its own grad_w[b, 0, c], grad_w[b, 1, c],
|
||||
// grad_h_t[b, c]. Thread 0 additionally writes grad_b[b, 0] and grad_b[b, 1].
|
||||
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
|
||||
#define HIDDEN_DIM 128
|
||||
#define BAND_OUT 2
|
||||
#define RL_BAND_ENABLED_INDEX 799
|
||||
#define RL_HEAT_CAP_MAX_LOTS_INDEX 504
|
||||
|
||||
extern "C" __global__ void rl_band_head_backward(
|
||||
const float* __restrict__ w_band, // [BAND_OUT × HIDDEN_DIM]
|
||||
const float* __restrict__ band_pre, // [B × BAND_OUT]
|
||||
const float* __restrict__ h_t, // [B × HIDDEN_DIM]
|
||||
const float* __restrict__ grad_band, // [B × BAND_OUT]
|
||||
const float* __restrict__ isv,
|
||||
int b_size,
|
||||
float* __restrict__ grad_w_per_batch, // [B × BAND_OUT × HIDDEN_DIM]
|
||||
float* __restrict__ grad_b_per_batch, // [B × BAND_OUT]
|
||||
float* __restrict__ grad_h_t // [B × HIDDEN_DIM] (OVERWRITE)
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
const int c = threadIdx.x;
|
||||
if (b >= b_size || c >= HIDDEN_DIM) return;
|
||||
|
||||
__shared__ float s_dpre[BAND_OUT];
|
||||
|
||||
// Master gate: write zeros and return so the caller's reduce_axis0 +
|
||||
// accumulate_grad_h chain produces no encoder-grad contribution. The
|
||||
// host-side branch in the trainer ALSO short-circuits the launch when
|
||||
// disabled — this device-side guard is defense-in-depth so the kernel
|
||||
// is safe to launch unconditionally during graph capture.
|
||||
const float enabled = isv[RL_BAND_ENABLED_INDEX];
|
||||
if (enabled <= 0.5f) {
|
||||
grad_w_per_batch[(b * BAND_OUT + 0) * HIDDEN_DIM + c] = 0.0f;
|
||||
grad_w_per_batch[(b * BAND_OUT + 1) * HIDDEN_DIM + c] = 0.0f;
|
||||
grad_h_t[b * HIDDEN_DIM + c] = 0.0f;
|
||||
if (c == 0) {
|
||||
grad_b_per_batch[b * BAND_OUT + 0] = 0.0f;
|
||||
grad_b_per_batch[b * BAND_OUT + 1] = 0.0f;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Stage 1: thread 0 computes the two activation-derivative scalars ─
|
||||
if (c == 0) {
|
||||
const float n_max_eff = isv[RL_HEAT_CAP_MAX_LOTS_INDEX];
|
||||
|
||||
const float pre_l = band_pre[b * BAND_OUT + 0];
|
||||
const float pre_u = band_pre[b * BAND_OUT + 1];
|
||||
const float t_l = tanhf(pre_l);
|
||||
const float t_u = tanhf(pre_u);
|
||||
// sech²(x) = 1 − tanh²(x)
|
||||
const float sech2_l = 1.0f - t_l * t_l;
|
||||
const float sech2_u = 1.0f - t_u * t_u;
|
||||
// d(b_l)/d(pre_l) = -sign(t_l) · sech²(pre_l) · N_max
|
||||
// d(b_u)/d(pre_u) = +sign(t_u) · sech²(pre_u) · N_max
|
||||
// sign(0) = 0 — at pre = 0 the gradient is zero (the activation
|
||||
// is non-differentiable at the cusp |tanh| → 0). Treat as zero;
|
||||
// upstream signs propagate cleanly.
|
||||
const float sign_l = (t_l > 0.0f) ? 1.0f : ((t_l < 0.0f) ? -1.0f : 0.0f);
|
||||
const float sign_u = (t_u > 0.0f) ? 1.0f : ((t_u < 0.0f) ? -1.0f : 0.0f);
|
||||
|
||||
const float g_b_l = grad_band[b * BAND_OUT + 0];
|
||||
const float g_b_u = grad_band[b * BAND_OUT + 1];
|
||||
// dL/d(pre_l) = g_b_l · (-sign_l · sech2_l · N_max)
|
||||
// dL/d(pre_u) = g_b_u · (+sign_u · sech2_u · N_max)
|
||||
s_dpre[0] = g_b_l * (-sign_l) * sech2_l * n_max_eff;
|
||||
s_dpre[1] = g_b_u * (+sign_u) * sech2_u * n_max_eff;
|
||||
|
||||
// grad_b_band per batch is just dL/d(pre[b, j]).
|
||||
grad_b_per_batch[b * BAND_OUT + 0] = s_dpre[0];
|
||||
grad_b_per_batch[b * BAND_OUT + 1] = s_dpre[1];
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
const float dpre_l = s_dpre[0];
|
||||
const float dpre_u = s_dpre[1];
|
||||
|
||||
// ── Stage 2: per-(b, c) writes ───────────────────────────────────
|
||||
// grad_w_band[j, c] per batch = dpre[j] · h_t[b, c]
|
||||
const float h_bc = h_t[b * HIDDEN_DIM + c];
|
||||
grad_w_per_batch[(b * BAND_OUT + 0) * HIDDEN_DIM + c] = dpre_l * h_bc;
|
||||
grad_w_per_batch[(b * BAND_OUT + 1) * HIDDEN_DIM + c] = dpre_u * h_bc;
|
||||
|
||||
// grad_h_t[b, c] = dpre_l · W_band[0, c] + dpre_u · W_band[1, c]
|
||||
// OVERWRITE — caller's `grad_h_accumulate_scaled` folds into encoder
|
||||
// grad via additive +=.
|
||||
const float w_lc = w_band[0 * HIDDEN_DIM + c];
|
||||
const float w_uc = w_band[1 * HIDDEN_DIM + c];
|
||||
grad_h_t[b * HIDDEN_DIM + c] = dpre_l * w_lc + dpre_u * w_uc;
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// rl_band_head_forward.cu — Phase 4-A band-head forward.
|
||||
//
|
||||
// Two entry points:
|
||||
// * `rl_band_head_linear_fwd` — small linear projection
|
||||
// band_pre[b, j] = b_band[j] + Σ_c W_band[j, c] · h_t[b, c]
|
||||
// for j ∈ {0 (b_l), 1 (b_u)}. Grid = (B, 2, 1), Block = (HIDDEN_DIM, 1, 1)
|
||||
// (matches the `ppo_policy_logits_fwd` launch shape for parity with
|
||||
// `PolicyHead::forward_logits`).
|
||||
// * `rl_band_apply_activation` — asymmetric ±|tanh| activation that
|
||||
// enforces `b_l ≤ 0 ≤ b_u` by construction:
|
||||
// b_l = -|tanh(band_pre[b, 0])| × N_max_eff
|
||||
// b_u = +|tanh(band_pre[b, 1])| × N_max_eff
|
||||
// where `N_max_eff` is read from `RL_HEAT_CAP_MAX_LOTS_INDEX`. Grid =
|
||||
// (ceil(B/32), 1, 1), Block = (32, 1, 1); single-thread-per-batch
|
||||
// element, deterministic.
|
||||
//
|
||||
// No PRNG, no atomicAdd, no shared mem. Pure read-deterministic;
|
||||
// graph-capturable. The forward is independent of the master gate at slot
|
||||
// 799 — the trainer skips the launch when the band is disabled.
|
||||
//
|
||||
// Per `pearl_determinism_achieved` discipline; per spec §1.1 / §1.3.
|
||||
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
|
||||
#define HIDDEN_DIM 128
|
||||
#define BAND_OUT 2
|
||||
#define RL_HEAT_CAP_MAX_LOTS_INDEX 504
|
||||
|
||||
// ── Linear forward: band_pre[b, j] = b_band[j] + Σ_c W_band[j, c] · h_t[b, c]
|
||||
//
|
||||
// Block-reduce over HIDDEN_DIM threads via shared memory. Matches the
|
||||
// existing `ppo_policy_logits_fwd` reduction pattern; deterministic
|
||||
// tree-reduce (no atomics) so output is bit-equal across runs.
|
||||
extern "C" __global__ void rl_band_head_linear_fwd(
|
||||
const float* __restrict__ w_band, // [BAND_OUT × HIDDEN_DIM]
|
||||
const float* __restrict__ b_band, // [BAND_OUT]
|
||||
const float* __restrict__ h_t, // [B × HIDDEN_DIM]
|
||||
int b_size,
|
||||
float* __restrict__ band_pre // [B × BAND_OUT]
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
const int j = blockIdx.y;
|
||||
if (b >= b_size || j >= BAND_OUT) return;
|
||||
|
||||
const int tid = threadIdx.x;
|
||||
|
||||
__shared__ float partial[HIDDEN_DIM];
|
||||
const float* w_row = w_band + j * HIDDEN_DIM;
|
||||
const float* h_row = h_t + b * HIDDEN_DIM;
|
||||
|
||||
partial[tid] = w_row[tid] * h_row[tid];
|
||||
__syncthreads();
|
||||
|
||||
// Tree-reduce over HIDDEN_DIM (assumed power-of-two = 128).
|
||||
for (int stride = HIDDEN_DIM / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) {
|
||||
partial[tid] += partial[tid + stride];
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
band_pre[b * BAND_OUT + j] = partial[0] + b_band[j];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Asymmetric ±|tanh| activation, scaled by N_max_eff.
|
||||
//
|
||||
// Output layout (per spec §1.1):
|
||||
// band_out[b, 0] = -|tanh(band_pre[b, 0])| × N_max_eff (≤ 0)
|
||||
// band_out[b, 1] = +|tanh(band_pre[b, 1])| × N_max_eff (≥ 0)
|
||||
//
|
||||
// Guarantees `b_l ≤ 0 ≤ b_u` for every batch element by construction —
|
||||
// Davis-Norman optimality theorem requires position 0 (flat) to lie inside
|
||||
// the no-transaction region; this activation enforces it.
|
||||
extern "C" __global__ void rl_band_apply_activation(
|
||||
const float* __restrict__ band_pre, // [B × BAND_OUT]
|
||||
const float* __restrict__ isv,
|
||||
int b_size,
|
||||
float* __restrict__ band_out // [B × BAND_OUT]
|
||||
) {
|
||||
const int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= b_size) return;
|
||||
|
||||
const float n_max_eff = isv[RL_HEAT_CAP_MAX_LOTS_INDEX];
|
||||
|
||||
const float pre_l = band_pre[b * BAND_OUT + 0];
|
||||
const float pre_u = band_pre[b * BAND_OUT + 1];
|
||||
|
||||
const float t_l = tanhf(pre_l);
|
||||
const float t_u = tanhf(pre_u);
|
||||
|
||||
band_out[b * BAND_OUT + 0] = -fabsf(t_l) * n_max_eff;
|
||||
band_out[b * BAND_OUT + 1] = +fabsf(t_u) * n_max_eff;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// rl_band_mask.cu — Phase 4-A no-transaction-band action override.
|
||||
//
|
||||
// When the current position lies inside the learned band `[b_l, b_u]`,
|
||||
// force `actions[b] = Hold` regardless of the sampled / argmax action.
|
||||
// This is the architectural default of the Davis-Norman (1990) no-trade
|
||||
// region (extended to deep RL by Imaki-Imajo-Ito 2021, arXiv:2103.01775).
|
||||
//
|
||||
// Layout matches `rl_confidence_gate.cu`:
|
||||
// * Grid=(B, 1, 1), Block=(1, 1, 1).
|
||||
// * One block per batch, single thread — pos_state read + 2-float band
|
||||
// read + scalar compare; no reduction.
|
||||
// * Master gate at `RL_BAND_ENABLED_INDEX` (slot 799). When ≤ 0.5f the
|
||||
// kernel returns immediately, preserving Phase 3D bit-equality.
|
||||
// * No atomicAdd; no shared mem; no PRNG; pure device-side, fully
|
||||
// graph-capturable BUT launched OUTSIDE graph capture per spec §9.5
|
||||
// so the host can branch on the master gate value without runtime
|
||||
// graph rebuild.
|
||||
//
|
||||
// Per `feedback_no_atomicadd`, `feedback_cpu_is_read_only`,
|
||||
// `pearl_fleet_fraction_not_aggregate`.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define ACTION_HOLD 2
|
||||
#define RL_BAND_ENABLED_INDEX 799
|
||||
#define RL_BAND_MAX_MASK_FRAC_INDEX 813
|
||||
|
||||
extern "C" __global__ void rl_band_mask(
|
||||
int* __restrict__ actions, // [B] IN/OUT
|
||||
const float* __restrict__ band_outputs, // [B × 2] (b_l, b_u)
|
||||
const unsigned char* __restrict__ pos_state, // [B × pos_bytes]
|
||||
const float* __restrict__ isv,
|
||||
int b_size,
|
||||
int pos_bytes
|
||||
) {
|
||||
const int b = blockIdx.x;
|
||||
if (b >= b_size) return;
|
||||
|
||||
// Master gate — bootstrap default is OFF (slot 799 = 0.0). When the
|
||||
// operator flips it to 1.0 the band defaults engage.
|
||||
const float enabled = isv[RL_BAND_ENABLED_INDEX];
|
||||
if (enabled <= 0.5f) return;
|
||||
|
||||
// Phase 4-A2 exploration bypass — guarantee at least
|
||||
// `(1 − max_mask) · B` batches are NEVER masked, so positions vary
|
||||
// and the sigmoid surrogate gradient on (b_l, b_u) can flow into the
|
||||
// band head. Without this the band collapses to the dead-signal trap
|
||||
// observed at 552d91bf4 (Phase 4-B smoke): all positions stuck at 0,
|
||||
// deep-in-band sigmoid argument, ~zero gradient, band stays wide.
|
||||
//
|
||||
// Bypass is DETERMINISTIC (lowest-index batches bypass) — random
|
||||
// sampling would break `pearl_determinism_achieved` bit-equality.
|
||||
// Spec §2.2 (kernel) + §9.1 Mitigation 2 (justification).
|
||||
const float max_mask = isv[RL_BAND_MAX_MASK_FRAC_INDEX];
|
||||
const int min_explore = (int)((float)b_size * (1.0f - max_mask));
|
||||
if (b < min_explore) return;
|
||||
|
||||
// Position layout: pos_state[b * pos_bytes + 0..4] = position_lots:i32
|
||||
// (canonical foxhunt offset; see PosFlat at
|
||||
// crates/ml-backtesting/src/lob/mod.rs and the rl_confidence_gate
|
||||
// reader at line 72).
|
||||
const int position_lots =
|
||||
*reinterpret_cast<const int*>(pos_state + b * pos_bytes);
|
||||
|
||||
// Phase 4-A3 (2026-06-04): Davis-Norman position-zero exception.
|
||||
// The Davis-Norman (1990) no-transaction band is a theorem about
|
||||
// MANAGING an EXISTING hedge position — it tells the agent NOT to
|
||||
// micro-adjust within the band. It says nothing about whether to
|
||||
// open a position from flat.
|
||||
//
|
||||
// The `±|tanh|` activation in `rl_band_head_forward.cu` guarantees
|
||||
// `b_l ≤ 0 ≤ b_u` (the Davis-Norman invariant). Combined with the
|
||||
// foxhunt invariant that agents start FLAT (position = 0), every
|
||||
// single flat-batch would be masked to Hold — agents never open,
|
||||
// positions never move, the sigmoid surrogate on (b_l, b_u) sits
|
||||
// deep-in-band where its derivative ≈ 0, and the band loss has no
|
||||
// gradient signal. Verified empirically at 1c23ff368 (Phase 4-A2):
|
||||
// action_hist collapsed to bimodal [0,0,109,0,0,0,0,0,19,0,0] from
|
||||
// step 100 onward, total_trades = 8 over 2000 steps.
|
||||
//
|
||||
// Flat positions get free choice — opens are NOT band-constrained.
|
||||
if (position_lots == 0) return;
|
||||
|
||||
const float b_l = band_outputs[b * 2 + 0];
|
||||
const float b_u = band_outputs[b * 2 + 1];
|
||||
|
||||
const float pos_f = (float)position_lots;
|
||||
if (pos_f >= b_l && pos_f <= b_u) {
|
||||
actions[b] = ACTION_HOLD;
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
// rl_band_turnover_controller.cu — Phase 4-B adaptive turnover-target
|
||||
// controller.
|
||||
//
|
||||
// Spec: docs/superpowers/specs/2026-06-03-no-transaction-band-architecture.md
|
||||
// §3.1 Option (c) + §3.4 (Schulman-bounded adaptive controller).
|
||||
//
|
||||
// Reads (from ISV):
|
||||
// * RL_BAND_ENABLED_INDEX (799) — master gate
|
||||
// * RL_BAND_FRAC_NOT_MASKED_OBSERVED_INDEX (812) — per-step raw signal
|
||||
// written by
|
||||
// `rl_band_frac_aggregate.cu`
|
||||
// * RL_BAND_TURNOVER_EMA_INDEX (809) — EMA state (owned)
|
||||
// * RL_BAND_CONTROLLER_BOOT_DONE_INDEX (810) — bootstrap latch (owned)
|
||||
// * RL_BAND_TURNOVER_TARGET_ADAPTIVE_INDEX (811) — output (owned)
|
||||
//
|
||||
// Writes (to ISV):
|
||||
// * 809 — updated EMA (first-observation bootstrap or Wiener-α blend)
|
||||
// * 810 — latched 1.0 on first observation
|
||||
// * 811 — adaptive target, clamped to [TARGET_MIN, TARGET_MAX]
|
||||
//
|
||||
// Control law (per the Phase 4-B dispatch & spec §3.1 Option c):
|
||||
// Healthy frac_not_masked target ≈ 0.4 (40% of batches free to trade,
|
||||
// 60% inside band). Asymmetric Schulman-bounded adapter:
|
||||
//
|
||||
// if frac_not_masked_ema > OVER_THRESHOLD (too much trading):
|
||||
// target *= TIGHTEN_RATE (push narrower band → fewer trades)
|
||||
// elif frac_not_masked_ema < UNDER_THRESHOLD (band saturated):
|
||||
// target *= LOOSEN_RATE (push wider band → more trading)
|
||||
// else:
|
||||
// healthy band — leave alone.
|
||||
// clamp(target, TARGET_MIN, TARGET_MAX).
|
||||
//
|
||||
// Per `pearl_bootstrap_must_respect_clamp_range`: bootstrap 0.05 ∈
|
||||
// [TARGET_MIN=0.01, TARGET_MAX=0.20]; controller never snaps to a clamp
|
||||
// boundary on its first emission.
|
||||
//
|
||||
// Per `pearl_dead_signal_resurrection_discipline`: the band's output gates
|
||||
// its own input (mask → fewer trades → narrower band drift → mask). The
|
||||
// asymmetric LOOSEN escape rate is faster than the TIGHTEN rate so the
|
||||
// controller can recover when frac_not_masked drops below 0.2.
|
||||
//
|
||||
// Per `feedback_no_atomicadd`: single-thread launch (1×1×1), no atomics.
|
||||
// Per `feedback_no_nvrtc`: pre-compiled cubin via build.rs.
|
||||
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
|
||||
#define RL_BAND_ENABLED_INDEX 799
|
||||
#define RL_BAND_TURNOVER_EMA_INDEX 809
|
||||
#define RL_BAND_CONTROLLER_BOOT_DONE_INDEX 810
|
||||
#define RL_BAND_TURNOVER_TARGET_ADAPTIVE_INDEX 811
|
||||
#define RL_BAND_FRAC_NOT_MASKED_OBSERVED_INDEX 812
|
||||
|
||||
// Tuning constants (all dimensionless).
|
||||
#define BAND_CTRL_EMA_ALPHA 0.02f // Wiener-α — ~50-step horizon
|
||||
#define BAND_CTRL_OVER_THRESHOLD 0.60f // frac_not_masked above → too much trading
|
||||
#define BAND_CTRL_UNDER_THRESHOLD 0.20f // frac_not_masked below → band saturated
|
||||
#define BAND_CTRL_TIGHTEN_RATE 0.97f // −3 %/step (slow narrow)
|
||||
#define BAND_CTRL_LOOSEN_RATE 1.05f // +5 %/step (faster escape per resurrection discipline)
|
||||
#define BAND_CTRL_TARGET_MIN 0.01f // never below 1 % of batches trading
|
||||
#define BAND_CTRL_TARGET_MAX 0.20f // never above 20 % — keeps band materially active
|
||||
|
||||
extern "C" __global__ void rl_band_turnover_controller(float* isv) {
|
||||
// Single-thread kernel — launched (1,1,1)/(1,1,1).
|
||||
if (threadIdx.x != 0 || blockIdx.x != 0) return;
|
||||
|
||||
// Master gate: when the band is disabled, leave all owned slots
|
||||
// untouched so Phase 3D bit-equality is preserved.
|
||||
const float enabled = isv[RL_BAND_ENABLED_INDEX];
|
||||
if (enabled <= 0.5f) return;
|
||||
|
||||
const float frac_now = isv[RL_BAND_FRAC_NOT_MASKED_OBSERVED_INDEX];
|
||||
const float boot_done = isv[RL_BAND_CONTROLLER_BOOT_DONE_INDEX];
|
||||
|
||||
// ── EMA update (first-observation bootstrap pattern) ─────────────
|
||||
float ema;
|
||||
if (boot_done < 0.5f) {
|
||||
// Replace EMA with current observation; latch flag. Necessary
|
||||
// because slot 809 sentinel-zeroes at trainer init and blending
|
||||
// zero with the first real measurement (likely ≈ 0.5 at warm
|
||||
// band-start) would lag the controller by ~50 steps before it
|
||||
// sees a representative value.
|
||||
ema = frac_now;
|
||||
isv[RL_BAND_CONTROLLER_BOOT_DONE_INDEX] = 1.0f;
|
||||
} else {
|
||||
ema = (1.0f - BAND_CTRL_EMA_ALPHA) * isv[RL_BAND_TURNOVER_EMA_INDEX]
|
||||
+ BAND_CTRL_EMA_ALPHA * frac_now;
|
||||
}
|
||||
isv[RL_BAND_TURNOVER_EMA_INDEX] = ema;
|
||||
|
||||
// ── Target update — asymmetric Schulman-bounded adapter ──────────
|
||||
float target = isv[RL_BAND_TURNOVER_TARGET_ADAPTIVE_INDEX];
|
||||
if (ema > BAND_CTRL_OVER_THRESHOLD) {
|
||||
// Trading too much → tighten band (smaller target → smaller
|
||||
// frac_not_masked goal → narrower bands chase the goal).
|
||||
target *= BAND_CTRL_TIGHTEN_RATE;
|
||||
} else if (ema < BAND_CTRL_UNDER_THRESHOLD) {
|
||||
// Band saturated → loosen (escape with faster rate per
|
||||
// resurrection discipline).
|
||||
target *= BAND_CTRL_LOOSEN_RATE;
|
||||
}
|
||||
// Healthy band — leave target alone.
|
||||
|
||||
// Clamp; bootstrap 0.05 sits strictly inside [0.01, 0.20].
|
||||
target = fmaxf(BAND_CTRL_TARGET_MIN,
|
||||
fminf(target, BAND_CTRL_TARGET_MAX));
|
||||
isv[RL_BAND_TURNOVER_TARGET_ADAPTIVE_INDEX] = target;
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
// rl_band_turnover_loss.cu — Phase 4-A turnover regularizer (Option b).
|
||||
//
|
||||
// Per spec §3.1 Option (b) + §3.2:
|
||||
// * Soft turnover proxy per batch:
|
||||
// m_soft[b] = sigmoid(s · (pos − b_l)) · sigmoid(s · (b_u − pos))
|
||||
// where `s` is `RL_BAND_GRAD_SHARPNESS_INDEX` (slot 804). `m_soft[b]`
|
||||
// ≈ 1.0 when position is in band (would-be-Hold), ≈ 0.0 outside.
|
||||
// `not_masked[b] = 1 − m_soft[b]` is the differentiable surrogate for
|
||||
// "this batch was free to trade this step".
|
||||
// * Mean across batch: `turnover_t = mean_b (1 − m_soft[b])`.
|
||||
// * Loss: `L = λ · (turnover_t − target)²` (same scalar for every batch
|
||||
// element; gradient is per-batch via the chain rule below).
|
||||
//
|
||||
// In Phase 4-A this kernel emits the per-batch loss scalar and the per-
|
||||
// batch gradient on `(b_l, b_u)` for future wiring. The trainer integration
|
||||
// (step 4) launches the kernel for OBSERVABILITY only — the gradient is
|
||||
// written to a scratch buffer that is not yet folded into the encoder grad
|
||||
// path. Adaptive controller + full backward chain are Phase 4-B / 4-C.
|
||||
//
|
||||
// Gradient derivation (per spec §3.2):
|
||||
// diff = turnover − target
|
||||
// d(turnover)/db_l = +(1/B) · σ' · s (looser lower bound → fewer in band → higher turnover)
|
||||
// d(turnover)/db_u = −(1/B) · σ' · s (looser upper bound → more in band → lower turnover)
|
||||
// where σ' is sigmoid derivative evaluated at the boundary surrogate.
|
||||
//
|
||||
// `RL_BAND_LOSS_WEIGHT_INDEX` (802) multiplies the loss. `RL_BAND_ENABLED_INDEX`
|
||||
// (799) gates the entire write — when ≤ 0.5 the kernel writes zeros so the
|
||||
// per-batch loss reducer sees no contribution.
|
||||
//
|
||||
// Phase 4-B (2026-06-04): the turnover target is now read from the
|
||||
// adaptive controller's output at `RL_BAND_TURNOVER_TARGET_ADAPTIVE_INDEX`
|
||||
// (811) instead of the static slot 803. The controller updates 811 every
|
||||
// step based on the EMA of `frac_not_masked`. Slot 803 remains in the ISV
|
||||
// (Phase 4-A clamp anchor for the controller) but is no longer the loss
|
||||
// kernel's input. See spec §3.1 Option (c) and `rl_band_turnover_controller.cu`.
|
||||
//
|
||||
// Per `feedback_no_atomicadd` (single-writer per slot), `pearl_determinism_achieved`
|
||||
// (no PRNG, single-thread-per-batch, no shared reductions across blocks).
|
||||
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
|
||||
#define BAND_OUT 2
|
||||
#define RL_BAND_ENABLED_INDEX 799
|
||||
#define RL_BAND_LOSS_WEIGHT_INDEX 802
|
||||
#define RL_BAND_GRAD_SHARPNESS_INDEX 804
|
||||
#define RL_BAND_TURNOVER_TARGET_ADAPTIVE_INDEX 811
|
||||
|
||||
// Numerically-stable sigmoid (clamps argument to avoid expf overflow).
|
||||
static __device__ __forceinline__ float stable_sigmoid(float x) {
|
||||
if (x >= 0.0f) {
|
||||
const float e = expf(-fminf(x, 40.0f));
|
||||
return 1.0f / (1.0f + e);
|
||||
} else {
|
||||
const float e = expf(fmaxf(x, -40.0f));
|
||||
return e / (1.0f + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Per-batch soft-mask + per-batch loss/grad scratch.
|
||||
//
|
||||
// Inputs:
|
||||
// band_outputs [B × 2] — (b_l, b_u) post-activation, in lots units.
|
||||
// pos_state [B × pos_bytes] — i32 position lots at offset 0.
|
||||
// isv [..] — read-only ISV slots.
|
||||
// turnover_t scalar — current per-step soft turnover (mean across B
|
||||
// of (1 - m_soft)), pre-computed by a separate
|
||||
// reduce kernel OR passed as 0 for Phase 4-A
|
||||
// OBSERVABILITY-ONLY (gradient still meaningful
|
||||
// via diff signal).
|
||||
//
|
||||
// Outputs:
|
||||
// m_soft_per_b [B] — sigmoid surrogate "in band" mass ∈ [0, 1].
|
||||
// loss_per_b [B] — per-batch loss contribution (same scalar).
|
||||
// grad_band_per_b [B × 2] — per-batch grad on (b_l, b_u).
|
||||
//
|
||||
// Grid = (ceil(B/32), 1, 1), Block = (32, 1, 1).
|
||||
extern "C" __global__ void rl_band_turnover_loss(
|
||||
const float* __restrict__ band_outputs, // [B × 2]
|
||||
const unsigned char* __restrict__ pos_state, // [B × pos_bytes]
|
||||
const float* __restrict__ isv,
|
||||
float turnover_t, // host-supplied scalar
|
||||
int b_size,
|
||||
int pos_bytes,
|
||||
float* __restrict__ m_soft_per_b, // [B]
|
||||
float* __restrict__ loss_per_b, // [B]
|
||||
float* __restrict__ grad_band_per_b // [B × 2]
|
||||
) {
|
||||
const int b = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (b >= b_size) return;
|
||||
|
||||
const float enabled = isv[RL_BAND_ENABLED_INDEX];
|
||||
if (enabled <= 0.5f) {
|
||||
m_soft_per_b[b] = 0.0f;
|
||||
loss_per_b[b] = 0.0f;
|
||||
grad_band_per_b[b * BAND_OUT + 0] = 0.0f;
|
||||
grad_band_per_b[b * BAND_OUT + 1] = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
const int position_lots =
|
||||
*reinterpret_cast<const int*>(pos_state + b * pos_bytes);
|
||||
const float pos_f = (float)position_lots;
|
||||
|
||||
const float b_l = band_outputs[b * BAND_OUT + 0];
|
||||
const float b_u = band_outputs[b * BAND_OUT + 1];
|
||||
|
||||
const float sharpness = isv[RL_BAND_GRAD_SHARPNESS_INDEX];
|
||||
// Phase 4-B: read the adaptive controller's target instead of the
|
||||
// static slot 803. The controller's bootstrap matches the static
|
||||
// default so first-step behavior is identical to Phase 4-A.
|
||||
const float target = isv[RL_BAND_TURNOVER_TARGET_ADAPTIVE_INDEX];
|
||||
const float loss_w = isv[RL_BAND_LOSS_WEIGHT_INDEX];
|
||||
|
||||
// Sigmoid surrogate near each boundary. `m_lower` ≈ 1 when pos ≥ b_l;
|
||||
// `m_upper` ≈ 1 when pos ≤ b_u; their product is the soft "in band".
|
||||
const float arg_l = sharpness * (pos_f - b_l);
|
||||
const float arg_u = sharpness * (b_u - pos_f);
|
||||
const float s_l = stable_sigmoid(arg_l); // ∂m/∂b_l requires −σ_l(1−σ_l)·s
|
||||
const float s_u = stable_sigmoid(arg_u); // ∂m/∂b_u requires +σ_u(1−σ_u)·s
|
||||
const float m_soft = s_l * s_u;
|
||||
m_soft_per_b[b] = m_soft;
|
||||
|
||||
// Loss: L = (λ × (turnover − target)²) / B, distributed evenly across
|
||||
// batches for reduction purposes. Same value per batch element.
|
||||
const float diff = turnover_t - target;
|
||||
const float inv_b = 1.0f / (float)b_size;
|
||||
loss_per_b[b] = loss_w * diff * diff * inv_b;
|
||||
|
||||
// d(turnover)/d(b_l) = -(1/B) · d(m_soft)/d(b_l)
|
||||
// = -(1/B) · s_u · d(s_l)/d(b_l)
|
||||
// = -(1/B) · s_u · (-sharpness · s_l · (1 − s_l))
|
||||
// = +(1/B) · sharpness · s_l · (1 − s_l) · s_u
|
||||
// d(turnover)/d(b_u) similarly with sign flipped.
|
||||
const float ds_l_db_l = -sharpness * s_l * (1.0f - s_l);
|
||||
const float ds_u_db_u = +sharpness * s_u * (1.0f - s_u);
|
||||
|
||||
// dL/db_l = 2 · λ · diff · d(turnover)/db_l = -2λ·diff·s_u·ds_l_db_l/B
|
||||
// dL/db_u = -2λ·diff·s_l·ds_u_db_u/B
|
||||
grad_band_per_b[b * BAND_OUT + 0] =
|
||||
-2.0f * loss_w * diff * s_u * ds_l_db_l * inv_b;
|
||||
grad_band_per_b[b * BAND_OUT + 1] =
|
||||
-2.0f * loss_w * diff * s_l * ds_u_db_u * inv_b;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// rl_bellman_target_saturation_reduce.cu — B-9 cross-batch tree-reduce of
|
||||
// per-batch saturation tallies produced by `bellman_target_projection` and
|
||||
// `bellman_fused_select_project`. Writes per-step rates + extremes to ISV.
|
||||
//
|
||||
// Inputs (consumed AFTER one of the two bellman variants has run):
|
||||
// sat_top_per_batch [B] — per-block count of (atom_z) where t_z > V_MAX_eff
|
||||
// sat_bot_per_batch [B] — per-block count of (atom_z) where t_z < V_MIN_eff
|
||||
// max_pre_per_batch [B] — per-block max(t_z) pre-clamp
|
||||
// min_pre_per_batch [B] — per-block min(t_z) pre-clamp
|
||||
//
|
||||
// Outputs (single ISV write each, by thread 0):
|
||||
// ISV[RL_Q_TARGET_TOP_SATURATION_RATE_INDEX = 726] Σ(top) / (B × Q_N_ATOMS)
|
||||
// ISV[RL_Q_TARGET_BOT_SATURATION_RATE_INDEX = 727] Σ(bot) / (B × Q_N_ATOMS)
|
||||
// ISV[RL_Q_TARGET_MAX_PRE_PROJ_INDEX = 728] max over batches
|
||||
// ISV[RL_Q_TARGET_MIN_PRE_PROJ_INDEX = 729] min over batches
|
||||
//
|
||||
// Per `feedback_no_atomicadd.md`: single-block kernel, shared-mem
|
||||
// tree-reduce. Caller launches with grid_dim=(1,1,1), block_dim=(N,1,1)
|
||||
// where N is a power of 2 with N <= max(1, B/2) and N <= 256, and
|
||||
// shared bytes = 4 * N * sizeof(float).
|
||||
//
|
||||
// Block size rationale: each thread iterates B/N elements via grid-stride;
|
||||
// the tree-reduce after the strided gather is over N threads.
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#define RL_Q_TARGET_TOP_SATURATION_RATE_INDEX 726
|
||||
#define RL_Q_TARGET_BOT_SATURATION_RATE_INDEX 727
|
||||
#define RL_Q_TARGET_MAX_PRE_PROJ_INDEX 728
|
||||
#define RL_Q_TARGET_MIN_PRE_PROJ_INDEX 729
|
||||
|
||||
#define Q_N_ATOMS 21
|
||||
|
||||
extern "C" __global__ void rl_bellman_target_saturation_reduce(
|
||||
float* __restrict__ isv,
|
||||
const float* __restrict__ sat_top_per_batch,
|
||||
const float* __restrict__ sat_bot_per_batch,
|
||||
const float* __restrict__ max_pre_per_batch,
|
||||
const float* __restrict__ min_pre_per_batch,
|
||||
int B
|
||||
) {
|
||||
extern __shared__ float smem[];
|
||||
float* s_top = smem;
|
||||
float* s_bot = smem + blockDim.x;
|
||||
float* s_max = smem + 2 * blockDim.x;
|
||||
float* s_min = smem + 3 * blockDim.x;
|
||||
const int tid = threadIdx.x;
|
||||
|
||||
// Grid-stride gather. Padded identity values for max/min reductions
|
||||
// (+/- infinity) ensure threads with no work don't pollute the
|
||||
// result.
|
||||
float l_top = 0.0f, l_bot = 0.0f;
|
||||
float l_max = -INFINITY, l_min = INFINITY;
|
||||
for (int b = tid; b < B; b += blockDim.x) {
|
||||
l_top += sat_top_per_batch[b];
|
||||
l_bot += sat_bot_per_batch[b];
|
||||
l_max = fmaxf(l_max, max_pre_per_batch[b]);
|
||||
l_min = fminf(l_min, min_pre_per_batch[b]);
|
||||
}
|
||||
s_top[tid] = l_top;
|
||||
s_bot[tid] = l_bot;
|
||||
s_max[tid] = l_max;
|
||||
s_min[tid] = l_min;
|
||||
__syncthreads();
|
||||
|
||||
// Power-of-2 tree reduce — blockDim.x guaranteed power of 2 by caller.
|
||||
for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
|
||||
if (tid < stride) {
|
||||
s_top[tid] += s_top[tid + stride];
|
||||
s_bot[tid] += s_bot[tid + stride];
|
||||
s_max[tid] = fmaxf(s_max[tid], s_max[tid + stride]);
|
||||
s_min[tid] = fminf(s_min[tid], s_min[tid + stride]);
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
const float denom = (float)(B * Q_N_ATOMS);
|
||||
isv[RL_Q_TARGET_TOP_SATURATION_RATE_INDEX] = s_top[0] / denom;
|
||||
isv[RL_Q_TARGET_BOT_SATURATION_RATE_INDEX] = s_bot[0] / denom;
|
||||
isv[RL_Q_TARGET_MAX_PRE_PROJ_INDEX] = s_max[0];
|
||||
isv[RL_Q_TARGET_MIN_PRE_PROJ_INDEX] = s_min[0];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user