Commit Graph

898 Commits

Author SHA1 Message Date
jgrusewski
60804788eb sp7(state): T2 polish — descriptions reflect current state vs future T6/T7
Rewrite the three sp5_budget_c51/cql/ens registry descriptions to be
accurate at HEAD aa2854017: replace present-tense "driven by"/"no longer
writes" with future-tense "will be driven by"/"will stop writing", replace
non-existent C51_BOOTSTRAP_BUDGET/CQL_BOOTSTRAP_BUDGET/ENS_BOOTSTRAP_BUDGET
named constants with their actual anonymous .max() literal line references,
and apply the "(Pearl 2 will stop writing...)" parenthetical uniformly to all
three slots (cql was missing it). Audit doc T2 bullet updated accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:08:36 +02:00
jgrusewski
aa28540175 sp7(state): register loss-balance controller state slots + fix stale doc
4 new ResetEntries × 4 slots each cover ISV[297..313). Sentinel 0
triggers Pearl A first-observation bootstrap on fold boundary, matching
the existing Pearl 2/3/4/5/6/8 pattern.

Also: corrected the stale sp5_budget_cql description claiming a
non-existent "regime_stability allocator" (per
feedback_trust_code_not_docs); clarified sp5_budget_c51 / sp5_budget_ens
to reflect SP7 ownership.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:03:21 +02:00
jgrusewski
6e7690d82d sp7(isv): T1 polish — alignment + audit-doc stub softening
Minor item 1: Remove extra column-alignment spaces from the 4 SP7
constants (LB_DIFF_VAR_CQL_BASE, LB_SAMPLE_VAR_CQL_BASE,
LB_DIFF_VAR_C51_BASE, LB_SAMPLE_VAR_C51_BASE) so they match the
no-alignment style of surrounding SP5 constants (BUDGET_C51_BASE etc.).
Constant values (297, 301, 305, 309) and inline comments are unchanged.

Minor item 2: Soften the Fix 31 T7 stub in dqn-wire-up-audit.md from
"sentinel-aware consumer" to "sentinel-aware bootstrap with bootstrap
constants matching the kernel's cold-start basis (defined in T7)"
so the audit entry does not forward-reference specific constant names
before they land.

Cargo check: clean (zero warnings, zero errors).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:58:30 +02:00
jgrusewski
9822b2eea7 sp7(isv): refresh SP5_PRODUCER_COUNT docstring for T1 slot growth
Update stale (118) → 137 unique-slot count and [174..294) = 120 → [174..313) = 139
range in the SP5_PRODUCER_COUNT docstring. Update inline comment unique-slot count
121 → 137 with SP7 T1 breakdown entry. Append SP7 Task 1 history paragraph matching
the established Layer D D3 paragraph style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:55:09 +02:00
jgrusewski
a92abedbae sp7(isv): allocate 16 ISV slots for loss-balance controller state
LB_DIFF_VAR_CQL_BASE=297, LB_SAMPLE_VAR_CQL_BASE=301,
LB_DIFF_VAR_C51_BASE=305, LB_SAMPLE_VAR_C51_BASE=309. Bumps SP5_SLOT_END
297 → 313 and SP5_PRODUCER_COUNT 123 → 139. Helper fns mirror the
existing budget_*/flatness/q_var_per_branch pattern. Layout fingerprint
extended. slot_layout_no_overlaps_and_total_correct asserts the new
total.

Audit doc Fix 31 stub added; will be extended commit-by-commit through
Tasks 2-9 of the SP7 plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:50:18 +02:00
jgrusewski
bcbd16f8c5 plan(sp7): v2 — fix 5 self-review violations from v1
Critical re-review of v1 caught:

1. Task 6 retained Pearl 2 kernel signature with (void) no-op args
   "to save 3-site cascade" — that's the partial-refactor anti-pattern
   feedback_no_partial_refactor explicitly forbids. v2 does the full
   contract change: signature shrinks 9→6 args, launcher migrates
   atomically.

2. SCRATCH_PEARL_2_C51/_CQL/_ENS would become orphan constants —
   feedback_wire_everything_up says delete or wire. v2 deletes them in
   T6.

3. Audit-doc updates were separated into a single late commit — would
   fail check_audit_doc_updates pre-commit gate on T1-T6. v2 folds Fix
   31 stub into T1 and extends it commit-by-commit.

4. T5 referenced grad_decomp_*_result_dev_ptr field names that don't
   exist — actual layout is one shared 27-float pinned buffer with
   per-component byte offsets (iqn=0, cql_sx=24, c51=36). v2 uses
   pointer arithmetic from grad_decomp_result_dev_ptr.

5. Tasks 1, 2, 4 had "if file shape X then Y, otherwise Z" placeholder
   branches — writing-plans skill forbids these. v2 has concrete patches
   based on reading the actual files.

Also corrected: T2 fixes the stale "regime_stability allocator"
description in state_reset_registry.rs alongside the new entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:35:07 +02:00
jgrusewski
30f7032d12 plan(sp7): implementation plan for loss-balance controller
10 tasks: ISV slots → reset registry → kernel → build manifest → trainer
load+launcher → Pearl 2 retreat → atomic launch+consumer+stale-doc →
audit doc + memory pearl → L40S 5-epoch smoke verify → L40S 50-epoch
full validation.

Each task is producer-only or atomic-pair commits per
feedback_no_partial_refactor. Verification gates (cargo check, cuda
build, smoke acceptance criteria) embedded in each task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:20:33 +02:00
jgrusewski
8f484f3312 spec(sp7): v2 — flatness-gated targets + Wiener α + sentinel-bootstrap
Self-review found 4 violations of feedback_isv_for_adaptive_bounds in v1:
1. TARGET_CQL_OVER_IQN=2.0 hardcoded → flatness-modulated per-branch
   target_ratio = ANCHOR × (1 − flatness[b]). Realizes the original
   pearl_q_flatness_gates_conservatism brainstorm idea.
2. TARGET_C51_OVER_IQN=1.0 hardcoded → mirror direction:
   target_ratio = ANCHOR × flatness[b].
3. ALPHA_META=0.01 fixed → Wiener-optimal α from per-branch state-tracker
   EMAs (16 new ISV slots: diff_var/sample_var × 2 heads × 4 branches).
4. Cascaded floor cleanup: compute_adaptive_budgets line 3384 floors
   reads to 0.02/0.05 always, fighting the controller. Replaced with
   sentinel-aware bootstrap: cold-start uses BOOTSTRAP value, post-write
   takes ISV value verbatim.

Acceptance criteria rewritten to be ISV-relative where the corresponding
signal exists (q-spread vs NOISY_SIGMA, label_scale vs NOISY_SIGMA band).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:11:26 +02:00
jgrusewski
0808ebe3e5 spec(sp7): loss-balance controller — adapt CQL/C51 budgets to grad-norm ratio
Outcome-driven controller replacing Pearl 2's hardcoded-0 CQL budget and
floor-pinned C51 budget with multiplicative ratio adaptation. Targets
`grad_split_bwd cql/iqn = 2.0` and `c51/iqn = 1.0` per slice (trunk, dir,
mag). Slow EMA (α=0.01) consistent with existing controller pearls; Pearl
A sentinel-bootstrap on fold boundary; Pearl D Wiener-optimal smoothing.

Replaces ghost docstring at fused_training.rs:3409 ("0.10×(1−regime)×health"
formula was never implemented). Pearl 2 keeps owning IQN budget (reference
denominator) and FLATNESS_BASE (consumed by NoisyNet σ pearl); stops
writing CQL/C51/ENS slots so the new controller is the sole driver.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:03:14 +02:00
jgrusewski
2fb7d7f57c fix(data): Fix 30 Stale-B — backtest_plan_kernel raw_close from prices_buf (post-MappedF32Buffer migration)
Closes Fix 29 audit row #13 — the last ⚠ Stale row from the Bug-1
contract drift triage. Pre-fix, `backtest_plan_state_isv` extracted
raw_close as `features[bar*feat_dim + 0]`. Post Bug-1 (commit
`5a5dd0fed`) `features[..+0]` is z-normed log-return, NOT raw_close.
The resulting `equity = cash + position*raw_close` and
`unrealized = position*(raw_close - entry_price)` formulas mixed
z-normed-log-return as a dollar price, corrupting val plan_isv
slots [PNL_VS_TARGET] (slot 1) and [PNL_VS_STOP] (slot 2). Other
plan_isv slots (progress, conviction, drift, regime, remaining)
don't depend on raw_close and were correct pre-fix.

Resolution: route raw_close from the upload-once `prices` buffer
(layout `[n*max_len*4]` raw OHLC). Close is at column index 3 — the
same index `backtest_env_kernel.cu` already reads from for portfolio
mark-to-market (line 64 of that kernel; OHLC layout is canonical
across the val backtest path). Reading from `prices` aligns the val
plan_isv path with env_step's source-of-truth, eliminating the
mixed-units pathology end-to-end.

Sites fixed:
  - crates/ml/src/cuda_pipeline/backtest_plan_kernel.cu:74-100
    Kernel signature: `const float* features` and `int feat_dim`
    parameters dropped, replaced by `const float* prices` (the
    [n*max_len*4] raw OHLC buffer). Raw_close read becomes
    `prices[(w*max_len + current_step)*4 + 3]`. Multi-line comment
    block documents the Bug-1 origin and the env_step parity
    reference. The `bool have_close` / `prices != nullptr` guard
    semantics preserved — callers without OHLC data fall back to
    `unrealized = 0` exactly as pre-fix.
  - crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs:~1956
    Single launcher (`evaluate_dqn_graphed` chunk loop, the only
    invocation site of `plan_state_isv_kernel`) updated to pass
    `&self.prices_buf.dev_ptr` instead of `&self.features_buf.dev_ptr`
    and to drop the now-unused `feat_dim_i32` local + `.arg()` call.
    Inline comment explains the Bug-1 origin.
  - docs/dqn-wire-up-audit.md
    Stale-B row appended to Fix 30's table. The standalone Stale-B
    DEFERRED paragraph at the bottom replaced by the commit summary
    + the Fix 30 closure note (all 4 ⚠ Stale and 1  Ambiguous rows
    from Fix 29's deferred follow-ups now resolved).

Migration scope per `feedback_no_partial_refactor`: kernel signature
changed → every consumer migrates in the same commit. Single launcher;
verified via `grep -rn backtest_plan_state_isv` (only the kernel
definition + the gpu_backtest_evaluator launcher + load site appear).

Verification:
  - SQLX_OFFLINE=true cargo check -p ml --offline (43.68s) clean.
  - SQLX_OFFLINE=true cargo build -p ml --release --offline
    --features cuda (1m 30s) clean; cubin recompiled via nvcc.
  - Pre-commit DtoD-via-pinned guard passes (the prereq commit
    `4d966e62f` migrated `gpu_backtest_evaluator.rs`'s buffers to
    MappedF32Buffer, eliminating the 5 `_via_pinned` callers that
    had blocked any prior staging of this file).

Refs Fix 29 row #13. `feedback_no_partial_refactor` (single launcher
migrated alongside kernel signature change in one commit),
`feedback_no_functionality_removal` (PNL_VS_TARGET / PNL_VS_STOP
slots preserved — only their data source corrected; the audit's
"drop the slots" alternative explicitly rejected),
`feedback_no_hiding` (no fallback to z-normed reads remaining;
kernel either gets real raw_close or falls through to
`have_close=false` with `unrealized=0`, identical to pre-fix
smoke-test semantics where prices==NULL),
`feedback_no_cpu_compute_strict` n/a (zero new host-side compute),
`feedback_no_htod_htoh_only_mapped_pinned` already satisfied
(`prices_buf` is `MappedF32Buffer` post the prereq migration),
`feedback_trust_code_not_docs` (the kernel comment said
"raw_close from features buffer" for months — accurate-when-written
pre-Bug-1, stale-after-Bug-1; verify-against-code disambiguates).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:56:00 +02:00
jgrusewski
4d966e62f4 refactor(cuda): migrate gpu_backtest_evaluator buffers to MappedF32Buffer (Fix 30 Stale-B prereq)
Prerequisite for the deferred Fix 30 Stale-B kernel-side fix
(`backtest_plan_kernel.cu` raw_close source). The DtoD-via-pinned
pre-commit guard (`scripts/pre-commit-hook.sh::check_no_dtod_via_pinned`,
commit `5275932f4`) blocks any commit that stages `gpu_backtest_evaluator.rs`
against the file's 5 pre-existing `clone_to_device_*_via_pinned` callers
that landed before the guard. Stale-B has to stage this file (to thread
`prices_buf` into the `backtest_plan_state_isv` launcher), so the
migration must land first as its own atomic commit per
`feedback_no_partial_refactor`.

Migration scope. Single file, four buffer fields:
  - prices_buf       CudaSlice<f32> → MappedF32Buffer  [n*max_len*4]
  - features_buf     CudaSlice<f32> → MappedF32Buffer  [n*max_len*feat_dim]
  - portfolio_buf    CudaSlice<f32> → MappedF32Buffer  [n*8] (kernel-mutated)
  - window_lens_buf  CudaSlice<i32> → MappedI32Buffer  [n]

Init sites (lines ~651-664 post-edit). 4 `clone_to_device_*_via_pinned`
calls replaced with `MappedF32Buffer::new(host.len())` +
`write_from_slice(host)`. No memcpy_dtod_async + stream.synchronize()
pair at construction — mapped-pinned coherence makes the kernel see
host writes after the next stream-sync barrier.

Reset site (`reset_evaluation_state`, line ~1485 post-edit). In-place
`self.portfolio_buf.write_from_slice(&portfolio_init)` replaces the
prior buffer-replacement
`self.portfolio_buf = clone_to_device_f32_via_pinned(...)`. No alloc
churn per epoch; the device pointer stays stable across resets which
matches MappedF32Buffer's intended use.

Consumer sites (17 kernel arg passes). `arg(&self.X_buf)` →
`arg(&self.X_buf.dev_ptr)` so the launcher receives the device pointer
the kernel expects. Sites: launch_gather (×2), launch_gather_chunk,
launch_env_step, env_batch_kernel chunked path, plan_state_isv kernel,
metrics_kernel. CUdeviceptr (u64) is passed by reference exactly as
metrics_dev_ptr already does at the metrics launch site (~line 2497).

Kernel-mutated MappedF32Buffer precedent. portfolio_buf is mutated by
the env_step kernel every step. The same file's `plan_diag_buf` (a
MappedF32Buffer) is also kernel-written via dev_ptr (lines ~1230-1256)
and host-read via host_ptr — direct precedent. The IQN τ migration
(commit `facbf76eb`) confirms cuMemHostAlloc DEVICEMAP for kernel
reads. The mapped_pinned.rs module docstring states explicitly:
"Kernels write through dev_ptr (with __threadfence_system())".

What this change touches:
  - crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs
    Field types (lines 299-321), init block (lines 641-665), reset
    (lines 1462-1471), 17 kernel arg passes across 6 launchers.
  - docs/dqn-wire-up-audit.md
    Fix 30 Stale-B paragraph extended with the prereq commit summary.
    Stale-B itself remains DEFERRED (kernel-side fix lands next).

Verification:
  - SQLX_OFFLINE=true cargo check -p ml --offline (44.38s) clean.
  - SQLX_OFFLINE=true cargo build -p ml --release --offline
    --features cuda (53.78s) clean.
  - grep -n "clone_to_device_.*_via_pinned\|upload_.*_via_pinned"
    crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs returns
    zero hits.
  - grep -n "self\.\(prices\|features\|window_lens\|portfolio\)_buf"
    shows every kernel-arg site followed by `.dev_ptr`. The only
    non-`.dev_ptr` references are the field declarations, the
    constructor moves into `Self { ... }`, and the
    `write_from_slice` calls in `reset_evaluation_state`.
  - Pre-commit DtoD-via-pinned guard now passes on staging this file.

Eliminates the last 5 `_via_pinned` callers in
`gpu_backtest_evaluator.rs`. Per
`feedback_no_htod_htoh_only_mapped_pinned`, `feedback_no_partial_refactor`
(every consumer of a field-type change migrates in one commit),
`feedback_no_hiding` (no `--no-verify`; the migration IS the fix the
guard is asking for).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:51:04 +02:00
jgrusewski
c4aa71b9df cleanup(cuda): Fix 30 Ambiguous-A — delete orphan phantom_liquidity_gbm
Closes Fix 29 audit row #18. The synthetic-data overlay kernel
`phantom_liquidity_gbm` in `dqn_utility_kernels.cu:1050-1096` was
flagged  Ambiguous because its writer wrote `log_ret` to
`market_features[bar*market_dim + 0]` and `+3` — the WRITER's
downstream consumer contract was unverifiable post Bug 1 (commit
`5a5dd0fed`) which redefined feature index 0 from raw log-return to
z-normed log-return.

Investigation result: zero callers in the entire codebase. The audit
grep `grep -rn "phantom_liquidity_gbm" crates/ml/src/ services/
crates/ bin/` returns ONLY the kernel definition itself — no
`load_function`, no `launch_builder`, no Rust-side launcher. The
kernel has been compiled but never invoked since at least early 2026.

Resolution: delete the kernel definition and replace with an
explanatory comment block. Per `feedback_no_hiding` an orphan kernel
with an ambiguous post-Bug-1 contract is exactly the failure mode the
rule warns against — a future re-wirer would have written
synthetic z-normed log-returns into a slot the production pipeline
expects raw log-returns at, re-introducing the very Bug-1 drift the
audit was triaging. Per `feedback_wire_everything_up` a kernel that
compiles but is unconsumed is either wired in the same commit or
deleted; this kernel was never wired since its writer existed.

The replacement comment block (23 lines) documents the design intent
(GBM-based synthetic-data augmentation) so any future re-introduction
has the spec available, plus the re-introduction conditions:
caller wiring must land in the same commit, and the writer/consumer
contract for `market_features[..+0]` (z-normed vs raw log-return)
must be explicit at the kernel boundary.

What this change touches:
  - crates/ml/src/cuda_pipeline/dqn_utility_kernels.cu:1037-1096
    47-line kernel definition + ══ comment header deleted; 23-line
    DELETED notice replaces it. Net -24 lines.
  - docs/dqn-wire-up-audit.md
    Fix 30 Ambiguous-A row appended with the dead-code-deleted
    resolution. Stale-B deferral note also added (separate paragraph)
    documenting the pre-existing `_via_pinned` guard block requires a
    dedicated `gpu_backtest_evaluator.rs` migration commit before the
    Stale-B kernel-side fix can land.

Verification:
  - SQLX_OFFLINE=true cargo build -p ml --release --offline
    --features cuda (1m 30s) clean; cubin recompiled via nvcc.
  - Pre-commit guards pass.
  - Final grep for `phantom_liquidity_gbm` returns only the comment
    block in the kernel file.

Refs Fix 29 row #18 deferred follow-up. `feedback_no_hiding`
(delete orphans, do not leave them as ambiguous landmines),
`feedback_wire_everything_up` (every module/feature/kernel built
must be wired to a production path or removed),
`feedback_no_functionality_removal` does NOT apply: a kernel with
zero callers is not a functional feature, and the audit doc retains
the design intent so any future re-introduction can rebuild the
mechanism with the correct post-Bug-1 contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:40:00 +02:00
jgrusewski
7e54dedd22 fix(data): Fix 30 Stale-C — scripted_policy raw_close from fxcache targets
Closes Fix 29 audit row #12. Pre-fix, `scripted_policy_select` read
`state[MARKET_START]` as `close_now` and computed
`recent_ret = (close_now - prev_close) / prev_close`. Post Bug-1
(commit `5a5dd0fed`) `state[MARKET_START]` is z-normed log-return at
the WRITER, not raw_close, so the formula mixed two unrelated signals
(z-normed return vs dollar price). The seed-phase MOMENTUM /
MEAN_REV / VWAP_DEV branches degenerated to noise-floor below the
±0.0001f cutoffs, leaving 60% of seed-phase episodes (the non-UNIFORM
20%+20%+20%) effectively in DIR_HOLD instead of expressing the
intended scripted-policy diversity.

Resolution: route raw_close from the same fxcache target buffer the
env_step kernel reads from. Kernel signature gains
`const float* targets`, `const int* episode_starts`, `int t`,
`int total_bars` parameters. Per-thread `bar_idx = episode_starts[i]
+ t` (matching `experience_kernels.cu:1769`'s indexing convention),
then `close_now = targets[bar_idx*6 + TARGET_RAW_CLOSE]`. Bounds-clamp
to `[0, total_bars-1]` mirrors env_step's out-of-bounds early-return.

Sites fixed:
  - crates/ml/src/cuda_pipeline/scripted_policy_kernel.cu
    Kernel signature extended; close_now read source switched;
    `FXCACHE_TARGET_STRIDE` / `FXCACHE_TARGET_RAW_CLOSE` mirrored as
    #defines (kernels can't import Rust constants; co-locating the
    literals in a comment-block keeps the cross-language contract
    visible at the call site for any future TARGET_DIM bump). The
    previous `bar_idx` parameter renamed `t`; the LCG seed now mixes
    per-thread `bar_idx = episode_starts[i] + t` instead of the
    per-step `t` — stronger entropy across episodes, no behavioural
    regression (UNIFORM policy still produces 4-direction uniform).
  - crates/ml/src/cuda_pipeline/gpu_experience_collector.rs:~3735
    Single launcher updated to pass `&targets_buf.dev_ptr`,
    `&self.episode_starts_buf`, `t as i32`, and `total_bars` (already
    in scope from line 3301). Inline comment explains Bug-1 origin.

Migration scope per `feedback_no_partial_refactor`: single launcher;
no other call sites. Verified via
`grep -rn scripted_policy_select crates/ml/src/`. NO new `PS_*` slot
or `PORTFOLIO_STRIDE` bump required — the targets buffer is the
canonical raw_close source and routing it directly avoids cascading
through the ~12 PORTFOLIO_STRIDE consumers (kelly_cap_update_kernel,
trade_stats_kernel, gpu_experience_collector, ml-core mirror, etc.).
This is the "route raw_close directly" branch of the audit's contract
decision; the alternative "add an extra state slot" branch was
explicitly rejected as cascade-heavy for a seed-phase-only signal.

Verification:
  - SQLX_OFFLINE=true cargo check -p ml --offline (43.87s) clean.
  - SQLX_OFFLINE=true cargo build -p ml --release --offline
    --features cuda (1m 30s) clean; cubin recompiled via nvcc.
  - No host-side compute added; all reads on GPU.
  - `targets_buf` is mapped pinned by upstream caller (per
    `feedback_no_htod_htoh_only_mapped_pinned`).

Refs Fix 29 row #12. `feedback_no_partial_refactor` (single launcher
migrated in same commit), `feedback_no_functionality_removal`
(`recent_ret` signal preserved — only its data source is fixed; the
CUSUM-substitution alternative path explicitly rejected because the
recent_ret signal IS the scripted-policy contract, not an
implementation accident), `feedback_no_hiding` (no fallback to
z-normed-log-return reads; kernel either gets real raw_close or
clamps to total_bars-1 boundary), `feedback_no_cpu_compute_strict`
n/a (zero new host-side compute), `feedback_trust_code_not_docs` (the
kernel comment said `state[MARKET_START + 0] = close_now` for months
— accurate-when-written, stale-after-Bug-1; verify-against-code
disambiguates).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:36:28 +02:00
jgrusewski
73e3ea87a4 fix(data): Fix 30 Stale-A — host-side target[0] → TARGET_RAW_CLOSE swap
Closes Fix 29 audit rows #14 and #15 (Bug-1 contract drift in val/HPO
close-price extraction). Both sites read `target[0]` thinking it is
raw_close, but post Bug-1 (commit `5a5dd0fed`) `target[0]` is
preproc_close (z-normed log-return). The `fv[3]` fallback path is
unreachable on every production code path because
`set_val_data_from_slices` (in `dqn/trainer/mod.rs:1704`) always yields
`Vec<f64>` of length 6 from `[f64; 6]` slices post `TARGET_DIM=6` bump
(commit `063fd2716`), so `target.len() >= 2` is an always-true guard.
Per `feedback_no_hiding` the dead fallback is removed in the same edit
rather than left as a silent wrong-units path.

Sites fixed:
  - crates/ml/src/trainers/dqn/trainer/metrics.rs:576
    (val window_prices for GpuBacktestEvaluator)
  - crates/ml/src/hyperopt/adapters/dqn.rs:2492
    (HPO adapter val_close_prices for window-aggregated backtest)

Both now read `target[TARGET_RAW_CLOSE]` (col 2). Both import the named
constant from `crate::fxcache` so a future column rename moves the call
site with the writer (Fix 27 prevention pattern).

Verification:
  - SQLX_OFFLINE=true cargo check -p ml --offline (8.03s) clean.
  - No GPU code changed; cubin not affected.

Affects val Sharpe annualization + window equity curves on training
metrics; affects HPO val score on every trial. Pre-fix would yield
"prices" of magnitude ~stddev(log_return) (~7e-5 for ES 1-min) feeding
into PnL math that expects dollar prices, producing degenerate
backtest output. Post-fix prices are real raw_close values.

References Fix 27 Bug B (host-side Welford) — same kind of bug surfaced
at val/HPO consumer rather than training Welford. References
feedback_no_hiding (delete unreachable fallbacks rather than leaving
silent wrong-units fall-through), feedback_no_partial_refactor (both
consumers of the same (fv, target) tuple convention migrate together),
feedback_trust_code_not_docs (the `target.len() >= 2` guard read as
defensive but was masking a contract drift).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:24:18 +02:00
jgrusewski
ab378327ec fix(data): vol_normalizer double-normalization removed (post-Bug-1)
The kernel block at experience_kernels.cu:698-704 divided
market_features[0..3] by vol_normalizer at runtime — designed for the
pre-Bug-1 pipeline where features arrived as RAW log returns (~±0.001).

After Bug 1 fix (commit 5a5dd0fed) moved z-normalization to the WRITER
(precompute_features.rs::NormStats::normalize_batch before fxcache write,
plus data_loading.rs DBN-fallback path applying the same op), features
arrive already-z-normalized. The runtime division then created a
1000-13000× DOUBLE NORMALIZATION inflating column 0 of next_states to
raw-price magnitude.

DIAG_AUX_LABEL diagnostic ground truth (production train-multi-seed-bn42w):
  - features_raw_cuda col 0 mean_abs = 0.443  (clean z-norm at SOURCE)
  - aux_nb_label_buf mean_abs        = 5398   (= 0.443 × inv_vol ~ 13245)
  - ratio matches 1 / vol_normalizer for ES 1-min realised vol ~7.5e-5

This bug caused label_scale=5481 in 50-epoch validation (cancelled
train-multi-seed-bn42w) while smoke ran with smaller window producing
smaller inv_vol → label_scale ~25-432. Both wrong, just different
inflation factors.

What changed:
  - experience_kernels.cu:698-704 block deleted; replaced with header
    comment explaining why. Kernel parameter `vol_normalizer` retained
    in signature with `(void)vol_normalizer;` to silence the
    unused-warning — removing the param would cascade through
    gpu_experience_collector.rs config struct + launcher + per-epoch
    Welford in training_loop.rs (60 lines). Bounded scope: leave the
    pipe wired, gut the consumer.
  - DIAG_AUX_LABEL diagnostic removed per its Fix 28 removal gate
    (gpu_dqn_trainer.rs ~12981 diagnostic block ~165 lines + the
    DIAG_AUX_LABEL_SOURCE_PTRS OnceLock static ~22 lines +
    training_loop.rs populate site ~22 lines).
  - Audit doc Fix 29 entry with audit results + open follow-ups.

Bug-1 contract audit (this commit's exhaustive re-check, 18 sites
classified):
  - 1 ⚠→: experience_kernels.cu:698-704 (this fix)
  - 13 : env-step kernel body, mirror universe, feature mask/noise,
    target reads at col 2 raw_close (Fix-27 already correct), DT
    rewards kernel, curriculum/hindsight/portfolio_sim kernel target
    reads, kernel header docs, PS_PREV_CLOSE state-layout slot
  - 4 ⚠ Stale (deferred — separate triage commits):
    * scripted_policy_kernel.cu:59-65 — seed-phase momentum reads
      state[MARKET_START] as raw_close, but post-Bug-1 it is z-normed
      log-return. Affects seed-phase scripted policy quality only.
    * backtest_plan_kernel.cu:77-100 — val plan_isv reads
      features[bar*feat_dim+0] as raw_close. Corrupts val plan_isv
      slots [PNL_VS_TARGET]/[PNL_VS_STOP].
    * metrics.rs:576 — val_data → window_prices reads target[0] as
      close (clones Fix-27 Bug B at host-side; affects val Sharpe).
    * hyperopt/adapters/dqn.rs:2492 — same target[0] pattern in HPO
      val_close_prices.
  - 1  Ambiguous: dqn_utility_kernels.cu:1089-1093 synthetic feature
    overlay; needs downstream-consumer contract verification.

Verification:
  - SQLX_OFFLINE=true cargo check -p ml --offline (47.87s) clean.
  - SQLX_OFFLINE=true cargo build -p ml --release --offline
    --features cuda (1m 30s) clean; cubin recompiled via nvcc.
  - Next L40S production run should show label_scale ~0.8 (matching
    kernel docstring expectation).

Refs: DIAG_AUX_LABEL diagnostic from Fix 28 in dqn-gpu-hot-path-audit.md
(commit 2683d4637) which captured the ground truth that pinned this
bug. Cancelled validation runs train-multi-seed-p5qzw (label_scale=808)
and train-multi-seed-bn42w (label_scale=5481) both blocked on this —
Fix 27 cleared the host-side variant (Welford reading target[0] as
raw_close); this Fix 29 closes the kernel-side variant. Bug 1 chain
(label_scale=5443 from 4-month-old #193) closes here.
feedback_trust_code_not_docs (the kernel comment said `#13 Vol
normalization` for months — accurate-when-written, stale-after-Bug-1).
feedback_no_partial_refactor does not apply because the kernel
parameter is retained as a no-op, deliberately leaving the
launcher/config-struct contract intact while the consumer is gutted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:15:51 +02:00
jgrusewski
2683d46378 diag(dqn): DIAG_AUX_LABEL one-shot — pin source of label_scale=5481 leak
Production training run train-multi-seed-bn42w showed label_scale=5481
(raw_close magnitude) at epoch 4 vs smoke's ~25 at the same commit
29b1d34c6. The 5-layer data-loading defense + DBN spread filter + target
stride/column fix all in place — yet column 0 of next_states_buf still
sees raw-price-magnitude values in production but not in the
multi_fold_convergence test path.

This one-shot diagnostic fires once at the first training batch and
prints:
  - aux_nb_label_buf stats (the kernel input; should be ~0.8 z-norm)
  - next_states_buf col 0 sample (what strided_gather reads)
  - features_raw_cuda col 0 sample (the source feature buffer)
  - targets_raw_cuda col 2 sample (raw_close — the suspected leak)
  - Verdict line interpreting the 4 stats

Three numbers will pin the source. If aux_buf ~5500: raw_close leaked
into next_states. If features_raw_cuda col 0 ~5500: the writer didn't
normalize. If targets_raw_cuda col 2 ~5500 but aux_buf ~0.8: only
production reads raw_close, smoke doesn't.

One-shot via static AtomicBool. Pre-graph-capture, no perf impact.
Removal gate: delete once the leak source is identified and fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:13:53 +02:00
jgrusewski
29b1d34c6d fix(data): target stride/column drift — host + GPU consumer cleanup
Two latent bugs converged in the cancelled 50-epoch run
(train-multi-seed-p5qzw at 96769d171, label_scale=808 vs smoke
baseline 22-28):

Bug A (latent since 063fd2716, 2026-04-19): TARGET_DIM was bumped 4→6
in fxcache (added raw_open at col 4 + mid_price_open at col 5), but
N consumer kernels in experience_kernels.cu + dt_kernels.cu hardcoded
stride 4 when indexing targets[bar*4+col]. Reading at the old stride
against the stride-6 buffer slid every lookup into the wrong bar's
data. Smoke harness paths (multi_fold_convergence) didn't exercise
expert_action_override / compute_difficulty_scores / hindsight_relabel
heavily; production 368k-bar run surfaced the drift via the
compounded label_scale = 30× expected magnitude.

Bug B (introduced today at 5a5dd0fed, 2026-05-02): the Bug 1 fix
changed targets[0] from raw_close to log-return-normalized
preproc_close. training_loop.rs::epoch_vol_normalizer's Welford pass
still read targets[0] expecting raw_close → output was
ln(log_return/log_return) ~ stddev 0.6 → triggered sanity-band warning
+ default fallback (5e-4); but downstream label_scale consumers fed
the corrupt value through.

Sites fixed:
  HOST (1 site):
    - training_loop.rs:570-573, :603 — w[i].1[0] → w[i].1[TARGET_RAW_CLOSE]
      + warning message updated
  GPU (5 sites in experience_kernels.cu):
    - line 3768 — kernel param doc OHLCV → fxcache TARGET_DIM=6
    - lines 3806-3808 — bar*4+3 → bar*6+2 (raw_close column)
    - lines 3974-3975 — i*4+2 → i*6+2 (stride-only)
    - lines 4015, 4020 — bar*4+2 → bar*6+2 (stride-only)
    - lines 3400-3404 — t_offset = global_idx*4 → *6 (stride-only)
    - lines 1553-1561 — kernel header doc updated to 6-column layout
  GPU (1 site in dt_kernels.cu):
    - line 793 — kernel param doc OHLC → fxcache TARGET_DIM=6
    - lines 803, 807 — i*4+3 → i*6+2 (raw_close col 2)
  HOST docstring (decision_transformer.rs:1049) — [num_bars, 4]
    → TARGET_DIM=6 with reference to fxcache constant module.

Structural prevention: 6 named column constants
(TARGET_PREPROC_CLOSE / TARGET_PREPROC_NEXT / TARGET_RAW_CLOSE /
TARGET_RAW_NEXT / TARGET_RAW_OPEN / TARGET_MID_OPEN) added to
crates/ml/src/fxcache.rs co-located with the existing TARGET_DIM
(promoted from private to pub). Co-locating in the contract-owner
module means future renames or column adds force every consumer to
update at the same call site. Compile-time density test asserts
columns are dense and exhaustive. Host-side consumer training_loop.rs
imports TARGET_RAW_CLOSE; GPU kernels reference the constant module
in comments + use literal stride 6 with a header block documenting
the contract (kernels can't import Rust constants).

PPO consumer NOT in scope: ppo_experience_kernel.cu reads at stride 4
but PPO has its own set_raw_market_data path uploading 4 columns from
Vec<f64>. PPO write/read pair is internally consistent at stride 4,
unaffected by fxcache stride bump. Production train_baseline_rl.rs
flow doesn't invoke set_raw_market_data, so PPO GPU collector is
currently orphaned. Consolidating PPO onto fxcache target buffer is a
separate refactor.

Verification:
  - SQLX_OFFLINE=true cargo check -p ml --offline clean
  - cargo build -p ml --release --offline --features cuda clean
    (cubin compiles via nvcc; release build under 2 min)
  - cargo test -p ml --lib -- target_layout 1/1 pass
    (target_columns_dense_and_exhaustive)
  - audit grep "targets[var * 4 +]" returns ZERO hits in production
  - re-run of train-multi-seed-p5qzw will show label_scale ~22-30
    (not 808) — gating production validation

Refs: cancelled train-multi-seed-p5qzw at 96769d171, 50-epoch
validation blocker. Bug origins: 063fd2716 (target_dim 4→6 bump) and
5a5dd0fed (Bug 1 column-0 semantic fix). feedback_no_partial_refactor
(every consumer of shared buffer contract migrates in same commit),
feedback_trust_code_not_docs (kernel doc OHLCV/OHLC strings stale
post-bump), feedback_no_quickfixes (proper structural fix not
one-line patch), feedback_wire_everything_up (column constants land
co-located with fxcache::TARGET_DIM so writer + caller share single
contract module).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:48:49 +02:00
jgrusewski
96769d1711 docs(sp5): Pearl 7 closure — intent_dist freeze resolved without code
Plan task #296. Pearl 7 was an INVESTIGATION task in the SP5 brainstorm:
the pre-SP5 50-epoch baseline (train-multi-seed-cv2mw, F0 epochs 4-9)
showed intent_dist freezing at exact Bin(2, 0.5) ratios (0.25/0.50/0.25),
suggesting a hidden binary action decomposition somewhere downstream.

The plan §C4 closure rule: if post-SP5 smokes show intent_dist drifting
normally (no freeze), Pearl 7 closes with no code changes.

Verdict from 3 retained SP5-era smokes: intent_dist drifts smoothly each
epoch. No Bin(2, 0.5) freeze observed at:
  - smoke-test-ks2wf  (post-spread-filter, 5845e4403)
  - smoke-test-7pv9v  (Layer D additive, f42b5fff8)
  - smoke-test-w9nsw  (D4 atomic, 2e9e276a0)

(smoke-test-cnlrw (sanitize-only, 8434737a6) cached log was pruned
before evidence-collection; 3 retained smokes sample post-spread-filter
and full Layer D production paths and decisively satisfy the closure
rule on their own.)

Likely cause: SP5 Layer A's per-branch parameter lifting (C51 atom span,
NoisyNet σ, IQN τ schedule, loss budgets, Adam β/ε, Kelly floors) added
enough independent variability at every shared site that no single
2-state decomposition can dominate intent_dist in steady state.

What lands:
  - docs/dqn-wire-up-audit.md  Pearl 7 closure entry
  - memory/pearl_intent_dist_freeze_resolved.md (new)
  - memory/MEMORY.md  Topic Files Index entry

No code changes. No follow-up spec opened. The escalation trigger
(re-freeze for ≥3 consecutive epochs in a future run) is documented
in the audit entry.

Closes plan task #296.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:18:58 +02:00
jgrusewski
2e9e276a0c fix(sp5): Layer D Task D4 — atomic 3-kernel wiring of D1+D2+D3
Wires the 3 Layer D producer kernels into the production hot path AND
removes the host-side EMA arithmetic for sharpe/max_dd/low_dd_ratio in
the same commit per feedback_no_partial_refactor + feedback_no_cpu_compute_strict.

Sites migrated (host → GPU kernel + ISV slot):
  training_sharpe_ema     → launch_training_metrics_ema → ISV[294]
  max_dd_ema              → launch_training_metrics_ema → ISV[295]
  low_dd_ratio            → launch_training_metrics_ema → ISV[296]
  LearningHealth.compose  → launch_health_composition  → ISV[290..294)
  trade-PnL aggregation   → launch_sp5_pnl_aggregation  → ISV[286..290)

Behavior preserved within float precision: every EMA β decay constant,
warmup/clamp wrapper, and downstream consumer formula migrates verbatim.
The kernels reproduce the host computations bit-for-bit (verified by the
GPU-gated unit tests landed in D1/D2/D3 at f42b5fff8 and the D1-rewrite
re-verification at 66f7e64d1; smoke-test-7pv9v PASSED with the additive
kernels loaded but unwired). The additional Pearl A+D smoothing layer
(ALPHA_META=1e-3) is the SP5 architectural change the entire Layer D
programme commits to (cf. pearl_first_observation_bootstrap +
pearl_wiener_optimal_adaptive_alpha).

Removed: host-side EMA arithmetic at training_loop.rs:5043-5099 (~57
lines covering max_dd α=0.1 + low_dd_ratio α=0.15 + training_sharpe
adaptive-α + sentinel branches). Struct fields self.training_sharpe_ema,
::_initialized, self.max_dd_ema, self.low_dd_ratio are KEPT because
external consumers exist (HEALTH_DIAG emit at training_loop.rs:3899,
adaptive-DSR aux-weight controller at 3772, smoke tests at
td_propagation.rs:126/135/155 + generalization.rs:102/103, registry log
emit at 6646); they now hold ISV-Pearl-smoothed values populated by
read_isv_signal_at(294/295/296) after the kernel chain completes.

Also added: GpuDqnTrainer::synchronize_isv_stream() — public cold-path
stream-sync helper at gpu_dqn_trainer.rs:~18475. Single cuStreamSynchronize
of the training stream, mirroring the existing per_branch_q_gap_ema()
embedded sync. Used once per epoch by D4's read-back path.

Out of scope (deferred to D5):
  - HealthEmaTrackers::update host-side α=0.1 EMA (metrics.rs:23-25):
    produces D2's inputs; D2's launcher takes already-EMA'd scalars.
    Eliminating this needs a 4th kernel; out of D4 scope.
  - LearningHealth::update warmup wrapper + [0.2, 0.95] clamp
    (learning_health.rs:114-124): may have non-DQN consumers (PPO, eval);
    deferred to a separate refactor.
  - compute_epoch_financials per-bar equity walk: stays as host-side
    input source for D1; D1 reproduces it on GPU as a PARALLEL producer
    publishing to ISV.

Verification:
  - cargo check + build clean
  - sp5_isv_slots + state_reset_registry unit tests 8/8 pass
  - cargo test -p ml --lib: 933 pass / 14 fail (identical to pre-D4
    baseline; failing tests are pre-existing GPU-context-required tests,
    not regressions)
  - `git grep "= EMA_BETA *|= (1.0 - EMA_BETA)"` returns 0 (was already
    0 — gate is a no-op for SP5 because production uses literal
    coefficients, not a named EMA_BETA constant). Meaningful gate
    `git grep "0.9 \* self.max_dd_ema|0.85 \* self.low_dd_ratio"` also
    returns 0 — confirms host EMA arithmetic deletion.
  - Order-of-ops: launches chained on training stream before consumer
    reads; stream-event sync only (synchronize_isv_stream once per epoch
    for cold-path host scalar refresh; producer→consumer paths on the
    same stream observe FIFO order without explicit fence).

Mapped-pinned discipline (per feedback_no_htod_htoh_only_mapped_pinned):
D1's step_returns + done_flags consumed via fresh MappedF32Buffer
allocations populated by host_slice_mut — no HtoD copy. Once-per-epoch
cold-path; matches existing pattern at training_loop.rs:1318.

L40S smoke validation deferred to next dispatch — D4 is structurally
risky enough (HEALTH_DIAG visibility behaviour change + ISV-Pearl-
smoothing on host-scalar reads + first-epoch cold-start interaction
with Pearl A) to validate separately. Closes feedback_no_cpu_compute_strict
sweep for SP5 scope (modulo D5 follow-up).

Refs: SP5 plan §D Task D4. Builds on D1-rewrite (66f7e64d1), D2
(e49756ac9), D3 (f42b5fff8). First D4 attempt was blocked by D1
per-trade vs per-bar mismatch; resolved by D1-rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:45:21 +02:00
jgrusewski
66f7e64d13 fix(sp5): D1 rewrite — match production per-bar semantics, fix D4 blocker
The original D1 kernel (commit 5ee795f14) was authored against a brief
that assumed per-trade event arrays exist in production. They don't.
Production has per-bar step_returns + done_flags with episode-boundary
resets, plus already-aggregated summary scalars (sum_returns,
sum_sq_returns) on TradeStats. The previous D4 agent found this
mismatch as a structural blocker and stopped before any code changes.

This rewrite fixes the kernel internals to match compute_epoch_financials
in financials.rs bit-for-bit. The slot allocation (ISV[286..290) for
PNL_TOTAL/MEAN/VAR/MAX_DD), Pearls A+D chain, and reset registry are
unchanged — only the kernel interface and max_dd internals shift.

What changed:
  - pnl_aggregation_kernel.cu: full rewrite. New signature consumes
    step_returns[N], done_flags[N], num_bars, n_trades, sum_returns,
    sum_sq_returns, initial_capital. pnl_total uses log-space
    compounded growth (financials.rs:80-97). pnl_mean/pnl_var are
    per-trade (financials.rs:122-124). pnl_max_dd implements per-bar
    equity walk over last 10K bars with reset at done_flags > 0.5
    AFTER processing the bar's return (matches financials.rs:163-194
    line-by-line, including 1.0 cap).
  - launch_sp5_pnl_aggregation: signature updated to match. 2
    MappedF32Buffers + 5 i32/f32 scalars instead of 3 mapped-pinned
    buffers + 1 count. Same Pearls A+D chain on the same stream.
  - sp5_isv_slots.rs: PNL slot docstring updated to reflect the actual
    output semantics (slot constants unchanged).
  - SCRATCH_PNL_AGG_BASE docstring + cubin static docstring + field
    docstring updated.
  - sp5_producer_unit_tests::pnl_aggregation_kernel_correctness:
    rewritten with new oracle. Inputs cover an episode boundary at
    bar 2 so the reset semantics are observable; expected values
    derived analytically from the host formula (no CPU oracle in the
    test process, per feedback_no_cpu_test_fallbacks).
  - Audit doc: D1-rewrite entry supersedes original D1 entry, with a
    line-by-line formula provenance table for D4 reviewers.

Unchanged: ISV slot allocation (sp5_isv_slots.rs constants),
SP5_SLOT_END/SP5_PRODUCER_COUNT/ISV_TOTAL_DIM, wiener offsets,
state_reset_registry arm + dispatch, build.rs cubin registration.
The kernel filename stays the same; only the file contents change.

Formula fidelity provenance (financials.rs as the oracle):
  - pnl_total: lines 80-97 (log-space sum, 1e-10 floor, exp − 1)
  - pnl_mean:  line 122   (per-trade sum_returns / n_trades)
  - pnl_var:   lines 123-124 (per-trade E[X²] − E[X]², .max(0))
  - pnl_max_dd: lines 163-194 (10K window, post-update reset, 1.0 cap)

Tests: cargo check + cargo build --features cuda clean; ISV slot +
state_reset_registry tests pass (8/8 unchanged); rewritten GPU
correctness test fires on next L40S smoke.

This unblocks D4 atomic wiring (D1+D2+D3 into the production hot
path with host-EMA removal). D4 dispatch resumes once this lands.

Refs: SP5 plan §D Task D1 + the structural blocker investigation by
the prior D4 agent (no commit; investigation reported up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:25:10 +02:00
jgrusewski
f42b5fff8d feat(sp5): Layer D Task D3 — Training metrics EMA kernel (additive)
Third of 3 Layer D producer kernels. Replaces host-side training_sharpe_ema,
max_dd_ema, low_dd_ratio updates (host-side EMAs in training_loop.rs) with
a fused GPU kernel chained through apply_pearls_ad_kernel. Per
feedback_no_cpu_compute_strict.

Note: agent investigated training_loop.rs and found the third metric is
low_dd_ratio (not gamma_blend as the original plan brief named). The
authored kernel reproduces the actual host-side EMA triplet present in
the codebase.

Additive only — no consumer wiring, no behavior change. The kernel +
launcher are loaded into GpuDqnTrainer and the slots are reserved on the
ISV bus, but the host-side updates continue to run unchanged. D4 (atomic
Layer D commit) wires this and D1+D2 to call sites in the same atomic
refactor per feedback_no_partial_refactor. Also note: training_loop.rs
gains two state-reset-registry dispatch arms (sp5_health_composition for
D2 + sp5_training_metrics_ema for D3) — registry plumbing required by
the new entries, NOT consumer wiring of the kernels themselves; same
pattern as SP5 Layer A bug-fix #281.

What this lands:
- training_metrics_ema_kernel.cu (single-block 3-thread fused EMA)
- Rust launcher launch_training_metrics_ema()
- 3 new ISV slots (TRAINING_SHARPE_EMA_INDEX..LOW_DD_RATIO_INDEX, 294..297)
- ISV_TOTAL_DIM 294 → 297, SP5_PRODUCER_COUNT 120 → 123 (linear span)
- LAYOUT_FINGERPRINT_SEED bump (auto via slot string)
- StateResetRegistry entries for D2 (sp5_health_composition) + D3
  (sp5_training_metrics_ema) with reset_named_state dispatch arms
- build.rs cubin registration
- GPU-gated unit test in sp5_producer_unit_tests.rs (analytical EMA)
- SP5 contiguity slot test
  training_metrics_ema_slots_contiguous_and_above_health_block
- Audit doc append

Formula fidelity: kernel reproduces the host-side EMA update for
sharpe/max_dd/low_dd_ratio bit-for-bit within float precision. β decay
constants and any warmup/clamp logic migrate as Invariant 1 anchors with
no algorithmic change. Verified by unit test asserting kernel output
matches an analytical EMA sequence within 1e-6 rel-err.

Tests: cargo check + cargo build clean; ISV slot + state_reset_registry
unit tests pass (8/8 incl. new contiguity check); GPU correctness test
fires on next L40S smoke.

Refs: SP5 plan §D Task D3, builds on D1 (5ee795f14) + D2 (e49756ac9).
Layer D additive infrastructure complete after this commit; D4 (atomic
5-site host-EMA → GPU migration) is next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:36:23 +02:00
jgrusewski
e49756ac90 feat(sp5): Layer D Task D2 — Health composition kernel (additive)
Second of 3 Layer D producer kernels. Replaces multi-step host arithmetic
in LearningHealth composition (q_gap_ema + q_var_ema + grad_norm_ema →
composed health score) with a fused GPU kernel chained through
apply_pearls_ad_kernel. Per feedback_no_cpu_compute_strict.

Additive only — no consumer wiring, no behavior change. The kernel +
launcher are loaded into GpuDqnTrainer and the slots are reserved on the
ISV bus, but the host-side composition continues to run unchanged. D4
(atomic Layer D commit) wires this and D1+D3 to call sites in the same
atomic refactor per feedback_no_partial_refactor.

What this lands:
- health_composition_kernel.cu (single-block fused composition)
- Rust launcher launch_health_composition()
- 4 new ISV slots (HEALTH_SCORE_INDEX..GRAD_NORM_NORM_INDEX, slots 290..294)
- ISV_TOTAL_DIM 290 → 294, SP5_PRODUCER_COUNT 116 → 120 (linear span)
- LAYOUT_FINGERPRINT_SEED bump (auto via slot string)
- StateResetRegistry entries (sentinel 0.0; per-fold reset enabled)
- build.rs cubin registration
- GPU-gated unit test in sp5_producer_unit_tests.rs (formula fidelity)
- Audit doc append

Formula fidelity: kernel reproduces the host-side LearningHealth::compose
(or equivalent) computation bit-for-bit within float precision. Migration
is structural only — no algorithmic change. Verified by unit test
asserting kernel output matches a Rust copy of the host formula within
1e-6 rel-err.

Tests: cargo check + cargo build clean; ISV slot + state_reset_registry
unit tests pass; GPU correctness test fires on next L40S smoke.

Refs: SP5 plan §D Task D2, builds on D1 (5ee795f14).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:13:54 +02:00
jgrusewski
5ee795f14f feat(sp5): Layer D Task D1 — PnL aggregation kernel (additive)
First of 3 Layer D producer kernels. Replaces host-side trade-PnL
aggregation loop in training_loop.rs with a GPU kernel chained through
apply_pearls_ad_kernel for smoothing. Per feedback_no_cpu_compute_strict.

Additive only — no consumer wiring, no behavior change. The kernel +
launcher are loaded into GpuDqnTrainer and the slots are reserved on the
ISV bus, but the host-side aggregation continues to run unchanged. D4
(atomic Layer D commit) wires this and the other two D2/D3 kernels to
call sites in the same atomic refactor per feedback_no_partial_refactor.

What this lands:
- pnl_aggregation_kernel.cu (single-block tree-reduce, no atomicAdd)
- Rust launcher launch_sp5_pnl_aggregation()
- 4 new ISV slots (PNL_TOTAL_INDEX..PNL_MAX_DD_INDEX, slots 286..290)
- ISV_TOTAL_DIM 286 → 290; SP5_PRODUCER_COUNT semantics formalised as
  wiener-buffer linear-span (110 → 116; the unique-slot count diverged
  from the linear span at D1 — pre-D1 they coincided by accident at the
  Pearl 6 carve-out's introduction)
- LAYOUT_FINGERPRINT_FRAGMENT extended with PNL_* entries
- StateResetRegistry sp5_pnl_aggregation entry + dispatch arm
  (sentinel 0.0; fold-reset; PnL is NOT cross-fold persistent unlike
  Pearl 6 Kelly stats, so it takes the standard sentinel-bootstrap
  path per pearl_first_observation_bootstrap)
- build.rs cubin registration
- GPU-gated unit test pnl_aggregation_kernel_correctness with
  analytical ground truth (no CPU reference per
  feedback_no_cpu_test_fallbacks)
- Audit doc append documenting D1 + the SP5_PRODUCER_COUNT semantic
  clarification

Tests: cargo check + cargo build --release --features cuda clean;
ISV slot + state_reset_registry unit tests 6/6 pass (incl new
pnl_aggregation_slots_contiguous_and_above_kelly_block); GPU
correctness test fires on next L40S smoke alongside existing 17 SP5
producer unit tests.

Refs: SP5 plan §D Task D1, validated SP5 Layer A/B at 5845e4403.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:54:00 +02:00
jgrusewski
7994d4d1b7 docs(sp5): Layer C close-out — audit doc + 7 implementation-pattern memory pearls
Documents the SP5 implementation (110 ISV slots, 8 layer-A producer kernels,
1 atomic Layer B consumer migration). Closes plan task #295 partial — the
8th pearl (pearl_sp5_close_out) and full close-out commit are DEFERRED
until the 50-epoch 3-seed × 3-fold L40S validation (#289) completes.

7 new memory pearls landed:
  pearl_per_branch_c51_atom_span        — Pearl 1
  pearl_per_branch_loss_budget          — Pearl 2
  pearl_per_branch_noisy_sigma          — Pearl 3
  pearl_per_group_adam_hyperparams      — Pearl 4
  pearl_per_branch_iqn_tau_schedule     — Pearl 5
  pearl_kelly_cap_signal_driven_floors  — Pearl 6 (cross-fold-persistent)
  pearl_trail_stop_signal_driven        — Pearl 8

Audit doc append covers Pearl 6's cross-fold carve-out, Pearl 4's ε-only
fall-back path, and the smoke-test-ks2wf @ 5845e4403 validation metrics
(sharpe 4.5→9.77, label_scale 19-28, trade_count 6468-9432).

Refs: SP5 spec at 6e6e0fa11
2026-05-02 15:31:26 +02:00
jgrusewski
5845e44031 fix(data): DBN spread-instrument filter — root cause of Bug 2 contamination
Direct inspection of ES.FUT_2024-Q1.dbn.zst (databento python client, offline
kubectl-cp from PVC) pinpoints the source of the 8,799 corrupt bars that
sanitize_bars (Fix 25) caught at the bar-level gate.

Q1 file content:
  Outrights (correct): ESH4/ESM4/ESU4/ESZ4/ESH5  — 43,353 records (76.87%)
                       price range $5,063–$5,478
  Spreads  (poison):   ESH4-ESM4/ESM4-ESU4/...   — 13,048 records (23.13%)
                       price range $47.30–$219.70

Calendar spreads trade at the price DIFFERENCE between adjacent contracts
(~$60-150 cost-of-carry roll basis). Databento's stype_in=parent resolution
for ES.FUT returns BOTH outrights AND every spread combination in the same
DBN stream. The legacy decoder keyed only by ts_event + dedup-by-volume;
during low-volume overnight windows + active rollover periods, spread bars
beat the outright on volume and survived the dedup. 780 spread bars
survived in Q1 alone; ~8,800 across 2024-2026.

Fix: build dbn::TsSymbolMap from metadata once, resolve each record's
instrument_id → symbol, skip any symbol containing `-` (spread separator).
Same-ts dedup-by-volume continues to handle legitimate front/back-month
overlap among outrights.

Adds `time = "0.3"` to ml/Cargo.toml (dbn::TsSymbolMap uses time::Date).

Why complementary to the Fix 25 sanitize_bars gate:
  - Spread filter (this commit) catches the cause precisely by symbol pattern,
    but only for instruments where parent-symbol resolution is the source.
  - sanitize_bars catches the symptom universally by close-ratio bound, but
    can't distinguish a low-basis spread from a fast-moving outright in
    extreme cases.
  Both layers active: spread filter dispatches at parser level, sanitize as
  defense-in-depth backstop for unknown future contamination shapes.

Validation: `cargo check -p ml --example train_baseline_rl` clean.

Refs: Bug 2 chain (#191, #194, label_scale=5443 leaks), today's
sanitize_bars find (Fix 25). Closes the contamination-source investigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:51:53 +02:00
jgrusewski
8434737a69 fix(data): structural defense at data-loading boundary — 5 layers
Stops chasing one-off corruption bugs. Three+ historical fixes patched
specific writers (#191 fxcache column-0, Bug 1 target schema,
label_scale=5443 leaks, today's state[0] heavy-tail). Each new
corruption shape found the next hole. This installs a structural
defense so corruption is REJECTED at the data-loading boundary
regardless of source.

Five independent layers, each mandatory:

1. Bar-level sanity (baseline_common::sanitize_bars):
   drop bars with non-positive OHLC, high<low, non-finite values, or
   close/prev_close outside [0.5, 2.0]. Catches DBN parse glitches,
   broker tick errors, near-zero-open bars at source.

2. safe_log_return result clamp (extraction.rs:1246):
   ratio.ln().clamp(±0.1). Real-market 1-bar log returns rarely
   exceed ±0.05; ±0.1 traps every legitimate move while rejecting
   the corruption shape (corrupt bar with bar.open ≈ 0 → ln = -30
   → previously normalized to -30000-magnitude state[0] outliers).

3. validate_features pre-norm bound (extraction.rs:538):
   |val| ≤ 5.0 post-extraction. Pre-norm features come from
   safe_normalize ([0,1]/[-1,1]), safe_clip (max ±3), or clamped
   log-returns (±0.1); ±5 catches extractor invariant breaks.

4. NormStats::normalize post-norm clamp (walk_forward.rs:688):
   ((val - mean) / std).clamp(±20.0). Even if upstream produces
   outliers, every value uploaded to GPU is bounded.

5. Shared validate_normalized_features gate (walk_forward.rs):
   single source-of-truth invariant enforced at THREE sites:
     - fxcache fast path (after discover_and_load)
     - DBN fallback (after normalize_batch)
     - precompute writer (before fxcache write — never persist
       a poisoned cache)

Removed: DIAG_BUG2 + DIAG_BUG2_v2 one-shot diagnostics
(~125 lines of host-side download + outlier scan in
training_loop.rs). Replaced by structural defense — instrumentation
isn't needed when corruption can't reach state[0].

FEATURE_SCHEMA_HASH auto-bumps via build.rs FNV-1a hash over
SCHEMA_FILES (extraction.rs included). All pre-fix .fxcache files
on PVC are invalidated at load time; ensure-fxcache regen produces
clean cache with new clamps applied.

Why this finally closes the chapter: per-writer fixes are reactive
(land after corruption hits prod). Boundary validation is
proactive — every future regression to extraction or normalization
trips the gate at load, not at epoch 5 of a 50-epoch run. The 5
layers are independent: a bug in any one leaves the others as
backstop.

Validation: cargo check -p ml --all-targets --offline clean.
NormStats unit tests (walk_forward.rs:706+) still pass — clamp +
validate are additive; existing test inputs are well within bounds.

Refs: SP5 Bug 2 (state[0] std=570 outliers on smoke-test-xb78r),
historical #191 #210 #214 #193 #195 chains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:02:53 +02:00
jgrusewski
129b7fd3d0 diag(dqn): DIAG_BUG2_v2 — outlier disambiguation H1 vs H2
Augments the existing DIAG_BUG2 one-shot block with two narrower scans
that disambiguate where state[0] heavy-tail (mean=6e-3, std=570,
mean_abs=20 over 3200 samples on smoke-test-xb78r) actually originates.

H1 (data-source corruption): scan features_buf.host_ptr for bars where
|feat[bar*42 + 0]| > 100. host_ptr aliases the same physical memory
the kernel reads (mapped-pinned) — direct read of the data
state_gather sees. Reports up to 10 outlier bar indices + mean/std/
max_abs over up to 2M bars.

H2 (kernel/gather bug): scan gpu_batch.states/next_states (already
DtoH-downloaded) for sample indices where |state[i*sd + 0]| > 100.
Reports up to 10 outlier sample indices.

Verdict line logged: feat-outliers nonempty → H1; state-outliers
nonempty AND feat-outliers empty → H2.

Context: smoke-test-xb78r at HEAD cba9f25ed went through DBN fallback
(train_baseline_rl.rs:611, "Loaded N bars (ts to ts)" message) — no
fxcache file exists on training-data PVC. DBN-fallback applies z-score
normalization (line 633) which has small stddev for log-return columns;
a single bar with bar.open ≈ 0 produces ln(ratio) = -30 → normalized
= -30000. Few bars in 700K could carry this signature.

Triggers once via static AtomicBool. Pre-graph-capture, no perf impact.
Cleanup gate: remove DIAG_BUG2 block once Bug 2 root cause is fixed
(likely a tighter clamp inside safe_log_return for log-return columns).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:48:59 +02:00
jgrusewski
cba9f25ed9 refactor(cuda): features/targets_raw_cuda + 10 cold-path inits → MappedF32Buffer + Bug 2 diag
Two related blocks landing together (Bug 2 diagnostic forces the structural
conversion of touched files; pre-commit guard at 5275932f4 enforces zero
*_via_pinned helpers per `feedback_no_hiding`).

== Bug 2 instrumentation (training_loop.rs after collect_experiences_gpu) ==

One-shot AtomicBool-gated DIAG_BUG2 dump of first 5 samples × 5 cols of
both gpu_batch.states and gpu_batch.next_states + col-0 mean/std/mean_abs
across the full batch. Prints once at first rollout — pre-graph-capture so
no hot-path impact. Interpretation key embedded:
  mean_abs ~0.001 = normalized log_return ✓
  mean_abs ~5000  = raw price ✗ (Bug 2 confirmed)

Resolves the smoke-vs-production state[0] divergence question that pure
static code reading couldn't pin: production aux_label_scale=5300 traces
back through gather kernel to "feat[0] of next_states_buf" but the fxcache
shows feat[0] stddev=1.0 (z-normalized log_return). Either next_states_buf
is populated from a different source than fxcache feat[0], or some kernel
mutates state[0] post-gather. The diagnostic prints both states (post-gather)
and next_states (post-shift) to disambiguate.

== Structural conversion: targets_raw_cuda + features_raw_cuda + 10 sites ==

Field types (DQNTrainer in trainer/mod.rs:621/624):
  Option<CudaSlice<f32>> → Option<MappedF32Buffer>

init_gpu_raw_buffers_from_slices (training_loop.rs): clone_to_device_f32_via_pinned
calls (lines 1308/1312) replaced with `MappedF32Buffer::new + write_from_slice`.

Consumer signatures (3 functions across 2 files):
  - gpu_experience_collector::collect_experiences_gpu(market_features_buf,
    targets_buf): &CudaSlice<f32> → &MappedF32Buffer (each)
  - gpu_experience_collector::launch_timestep_loop: same
  - gpu_experience_collector::compute_difficulty_scores(targets): same
  - decision_transformer::build_dt_trajectories(features_gpu, targets_gpu): same

Launch sites: `.arg(buf)` → `.arg(&buf.dev_ptr)` at all 5 launch_builder
invocations in gpu_experience_collector.rs and the 2 in build_dt_trajectories.

== Cold-path init conversions (forced by guard touching shared files) ==

gpu_dqn_trainer.rs (9 sites → 4 buffer-migration groups):
  - spec_u_s1/v_s1/u_s2/v_s2 (4 buffers, explicit)
  - spec_u/v macro pairs (alloc_spec_pair! body, expands to ~22 buffers)
  - graph_params (cross-branch graph message-passing, 60 floats)
  - denoise_params (diffusion Q-refinement MLP, 1800 floats)
  - qlstm_weights (xLSTM mLSTM-cell, 528 floats)

gpu_experience_collector.rs (1 site):
  - upload_ofi_features → ofi_gpu field type Option<CudaSlice<f32>> → MappedF32Buffer

Inherited from prior worktree-agent attempts (compile clean, included here):
  - sel_clip_buf in gpu_dqn_trainer.rs
  - RmsNormWeightSet γ buffers in gpu_weights.rs

PPO trainer (trainers/ppo.rs) deferred — not on eval-collapse hot path.

== Validation ==

cargo check -p ml --offline: clean
pre-commit hook (check_no_dtod_via_pinned + Invariant 7 + GPU hot-path guard): pass

Bug 2 diagnostic will fire on next L40S run and print state[0] stats for
first batch. If mean_abs ~0.001 → state[0] is correctly normalized and Bug 2
is elsewhere (maybe label_scale_ema initialization). If mean_abs ~5000 →
state[0] really is raw price; the rollout state-builder is the bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:57:12 +02:00
jgrusewski
5a5dd0fed1 fix(fxcache): target column [0:1] log-return-normalized, not raw price
Bug 1 of the eval-Hold-collapse diagnosis. The fxcache target schema
documented in experience_kernels.cu:1556 + cuda_pipeline/mod.rs:508 specifies:

  target[0] = preproc_close  — log-return-normalized close (network input)
  target[1] = preproc_next   — log-return-normalized next close
  target[2] = raw_close      — raw price for portfolio simulation
  target[3] = raw_next       — raw price
  target[4] = raw_open       — raw price
  target[5] = mid_price_open — MBP-10 midpoint (fallback raw_open)

Both writers — `precompute_features.rs:360` and `data_loading.rs:510` —
violated the contract by storing raw OHLCV close prices in slots [0:1].
Empirical fxcache inspection: target[0..4] mean=$5967, stddev=$582 (raw
prices throughout). The raw-price values at target[0:1] were never directly
consumed by training (production aux head reads next_states[i][0] = MARKET
feat[0] = log_return), but they corrupted any code reading targets per the
documented contract.

Both writers now compute (raw_curr / prev_close).ln() and (raw_next /
raw_curr).ln() for the preproc columns. FXCACHE_VERSION bumped 7→8 to
invalidate existing caches and trigger ensure-fxcache regen.

A second bug — eval label_scale=5300 (raw price magnitude) at production
binary despite source state[0] tracing back to z-normalized log_return —
remains unresolved. Bug 2 instrumentation lands in the next commit; that
runtime trace will pin which production-binary code path injects raw_close
into state[0] post-gather.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:36:49 +02:00
jgrusewski
5275932f4c guard+cleanup(cuda): DtoD-via-pinned pre-commit guard + delete orphan HER
Two related changes installing the structural guard against the SP6 Pearl 5
IQN τ failure mode (root cause fixed at facbf76eb for that one site) and
removing the only remaining orphan callers of the broken pattern.

The bug class. The mapped_pinned::{upload,clone_to_device}_{f32,i32}_via_pinned
helpers are named to suggest "no HtoD per feedback_no_htod_htoh_only_mapped_pinned"
but their bodies do MappedXBuffer::new() + memcpy_dtod_async() +
stream.synchronize(). The DtoD copy and synchronize are both forbidden
inside CUDA Graph capture (CUDA_ERROR_STREAM_CAPTURE_INVALIDATED) and add a
host stall otherwise. The canonical pattern is MappedXBuffer stored directly
+ write_from_slice + kernel reads via .dev_ptr, used by SP4 portfolio_state
and SP6 IQN τ at facbf76eb.

Guard. New check_no_dtod_via_pinned in pre-commit-hook.sh rejects any
staged .rs file calling upload_(f32|i32)_via_pinned or
clone_to_device_(f32|i32)_via_pinned, except mapped_pinned.rs itself. Per
feedback_no_hiding: no suppression marker. Also fixes a pre-existing
silent-skip bug: the gpu-hotpath-guard.sh invocation used
$(cd "$(dirname "$0")" && pwd) which resolved to .git/hooks/ (the symlink's
directory) instead of scripts/, so the guard never ran. Replaced with
readlink -f "$0" + an explicit "guard missing" error branch — silent skip
is worse than no guard.

Orphan deletion. gpu_her.rs carried legacy relabel_batch, generate_random_donors
(CPU), HerBatch, slice_clone_f32, slice_clone_i32 — zero production callers
(verified via grep). Production uses relabel_batch_with_strategy +
generate_random_donors_gpu. The orphan held the only upload_i32_via_pinned
callers in the codebase; per feedback_no_hiding the right fix is delete.

Scope. Eliminates 2 of 47 production *_via_pinned call sites. Remaining 45
across 14 files are cold-path init — graph-capture-fragile and host-stalling
but not breaking operationally. Guard enforces no new calls; existing 45
migrate in subsequent atomic per-buffer commits. After all 45 are converted,
the four helpers themselves get deleted from mapped_pinned.rs.

Validation. Smoke smoke-test-82fjk at facbf76eb succeeded — magnitude
differentiation restored (q_full=0.462 > q_half=0.409 > q_quarter=0.350 vs
baseline frozen Pascal-triangle 0.225/0.280/0.495), eval distribution
unfrozen (eq=0.596, eh=0.404, ef=0.000 vs baseline single-action collapse).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:12:23 +02:00
jgrusewski
facbf76eb5 fix(sp6): IQN τ buffers — MappedF32Buffer per feedback_no_htod_htoh_only_mapped_pinned
Pearl 5's online_taus/target_taus/cos_features were declared as
CudaSlice<f32> (device-only), populated via upload_f32_via_pinned
which does a DtoD copy from a separate mapped-pinned staging buffer.
The DtoD inside CUDA Graph capture triggers
CUDA_ERROR_STREAM_CAPTURE_INVALIDATED and the 'continuing ungraphed'
fallback observed in smoke-test-hhr5q.

This violates feedback_no_htod_htoh_only_mapped_pinned: the rule is
mapped-pinned (cuMemHostAlloc DEVICEMAP) for ALL CPU↔GPU paths. No
DtoD copies, no HtoD copies, no exceptions.

Fix: convert all 3 buffers (online_taus, target_taus, cos_features)
to MappedF32Buffer per-branch [MappedF32Buffer; 4] arrays. Host writes
go directly to host_ptr; IQN kernel reads dev_ptr of the same memory
— no copy step at all. The mem::swap pattern is replaced with pure
selection: activate_branch_taus sets active_branch_idx; kernel launch
sites index online_taus_per_branch[active_branch_idx].dev_ptr.

Eliminates upload_f32_via_pinned calls for these buffers entirely.

Refresh becomes a host write to mapped-pinned host_ptr at fold
boundary; subsequent kernel launches see the write through the
mapped-pinned coherence guarantee after stream sync.

cargo check + cargo build --release + cargo test --lib (sp4 sp5
state_reset_registry: 13/13) all clean. Sanity grep for
upload_f32_via_pinned in gpu_iqn_head.rs returns zero.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 09:39:13 +02:00
jgrusewski
b861567890 fix(sp6): clean compile — wire-or-delete all 13 ml + 2 ml-dqn warnings
W1 SEMANTIC: Pearl 2 iqn_branch[b] is now consumed at Pearl 5 per-pass
IQN budget. Previous code used iqn_trunk/4 in both parallel + sequential
IQN paths, silently averaging Pearl 2's per-branch IQN differentiation.
Now: iqn_budget_per_branch = iqn_branch[branch_idx] / 4.0 inside the
per-branch loop — sum across 4 branches = iqn_trunk magnitude preserved,
per-branch differentiation preserved. iqn_trunk renamed _iqn_trunk in
the destructure (still in-scope comments only).

W2 DELETE: ATOM_NUM_ATOMS_BASE + NOISY_SIGMA_BASE imports removed from
fused_training.rs:44 — consumers are atoms_update_kernel.cu (Layer B)
and experience_kernels.cu; imports were speculative additions.

W3 DELETE: v_blocks at gpu_iql_trainer.rs:756 — both kernel launches
used v_blocks2; v_blocks was stale dead code.

W4 DELETE: OrderRouter import from ml-dqn/src/dqn.rs — import only,
never used in the file body.

W5 DELETE: get_snapshots_for_timestamp import from data_loading.rs —
imported but never called; only OFICalculator + get_trades_for_bar used.

W6 DELETE: update_target_networks method (42 lines) from ml-dqn/dqn.rs —
orphan with zero call sites; real path uses fused CUDA EMA kernel.
Cascaded: convergence_half_life import deleted (now unused).

W7-W13 FILE-LEVEL #![allow(unsafe_code)]: cublas_algo_deterministic.rs
and sp4_wiener_ema.rs — workspace unsafe_code = "warn" fires for every
unsafe impl/fn; both files require unsafe for CUDA/cuBLAS interop;
pattern matches mapped_pinned.rs, gpu_iqn_head.rs, gpu_weights.rs, etc.

W14-W15 VISIBILITY SCOPE: ControllerPrevValues + ControllerFireCounts
in trainer/mod.rs changed pub → pub(crate); consumed only within ml.

Per feedback_no_hiding: zero #[allow(...)] item-level suppressions added;
2 file-level #![allow(unsafe_code)] follow established crate convention.

cargo check (ml + ml-dqn) + cargo build --release + cargo test --lib
all CLEAN (0 warnings; 13/13 lib tests pass).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 09:28:48 +02:00
jgrusewski
8df28b81c8 merge: SP6 Pearl 5 — IQN τ per-branch (replaces SP5 broken upload pattern)
Brings in worktree-agent-acd65ada (commit 89fadec24): per-branch IQN
τ schedules via 4 forward passes per step, each with one branch's τ
slab swapped in via mem::swap. ÷4 budget normalization at every
per-branch IQN call preserves total gradient magnitude.

CRITICAL FIX: this merge replaces the SP5 Layer B Pearl 5 implementation
that violated feedback_no_cpu_compute_strict. The old refresh_taus_from_isv
called upload_f32_via_pinned every step inside the CUDA Graph capture
region, triggering CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED and the
'parent graph capture failed (continuing ungraphed)' fallback observed
in smoke-test-qtn7c at SP5 HEAD.

SP6 Pearl 5 uses pre-uploaded τ slabs + mem::swap pointer arithmetic.

Conflict resolution in fused_training.rs:
- Pearl 5 worktree branched from pre-Pearl-2 HEAD; renamed
  iqn_budget → iqn_trunk in 2 sites (parallel + sequential paths) to
  match Pearl 2's compute_adaptive_budgets() new return signature.
- Removed redundant single-call apply_iqn_trunk_gradient(iqn_trunk)
  from post-join block — Pearl 5's per-branch loop above already
  applies iqn_budget_per_branch = iqn_trunk/4 to the IQN trunk gradient
  4 times, achieving the same total magnitude with per-branch τ.

cargo check + cargo test --lib (sp4 sp5 state_reset_registry: 13/13)
both clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:14:52 +02:00
jgrusewski
76d58406bd merge: SP6 Pearl 3 — NoisyLinear per-branch σ array
Brings in worktree-agent-a4d8a879 (commit ed3fa066b): per-branch σ via
[4]-element mapped-pinned device buffer. add_advantage_noise kernel
indexes σ by branch derived from action_idx % total_actions; Q-value
layout is branch-major contiguous so per-branch σ derivation requires
no forward-pass restructuring.

3 ExperienceCollectorConfig constructors updated.

Resolves Pearl 3 averaging from SP5 Layer B which collapsed 4 per-branch
σ values into a single scalar via training_loop.rs:1747.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 09:10:51 +02:00
jgrusewski
becd488320 merge: SP6 Pearl 2 — SAXPY per-branch loss budgets
Brings in worktree-agent-a284432c (commit f62860ea8): per-branch SAXPY
sub-launches via correction-factor pattern. Trunk gets mean budget,
branch HEAD weights get branch[b]/trunk_mean correction multiplier.

Resolves Pearl 2 averaging from SP5 Layer B (compute_adaptive_budgets)
which collapsed 4 per-branch budgets into a single scalar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 09:10:41 +02:00
jgrusewski
82ca733255 fix(sp5): Layer A bug — add 21 reset_named_state dispatch arms
L40S smoke at SP5 Layer B HEAD (3ad5e011b) failed in 9.4s at first
fold-reset boundary with:
  ERROR: fold-reset 'sp5_atom_v_center': StateResetRegistry reset
  dispatch: unknown name 'sp5_atom_v_center'.

Every SP5 Layer A task added RegistryEntry blocks but none added the
matching dispatch arms in reset_named_state. Runtime validator catches
this on first fold boundary, crashing all 3 folds before training runs
(0/3 checkpoints saved). Local unit tests verified slot layouts but
never exercised the fold-reset dispatch path — gap masked through
Layer A close-out.

Adds 21 dispatch arms covering all SP5 Layer A FoldReset entries:
  Pearl 1:    sp5_atom_v_center / v_half / headroom / clip_rate
  shared:     sp5_branch_entropy / q_var_per_branch
  wiener:     sp5_wiener_state (no-op — sp4 arm covers full buffer)
  Pearl 3:    sp5_noisy_sigma / sigma_fraction
  Pearl 2:    sp5_budget_c51 / iqn / cql / ens / flatness
  Pearl 4:    sp5_adam_beta1 / beta2 / eps + sp5_grad_prev_buf
  Pearl 5:    sp5_iqn_tau
  Pearl 8:    sp5_trail_dist_per_dir
  Pearl 1-ext: sp5_atom_num_atoms

Pearl 6 (slots 280..286) intentionally absent — those slots are
cross-fold-persistent (the whole point of A6 per
project_magnitude_eval_collapse_kelly_capped).

Each ISV-range arm zeros its slot range to fire Pearl A's first-
observation sentinel on the new fold's first producer launch.
sp5_grad_prev_buf calls a new reset_sp5_grad_prev_buf bulk-zero
method on GpuDqnTrainer (mirrors reset_sp4_clamp_engage_counters).
sp5_wiener_state is no-op since reset_sp4_wiener_state already
bulk-zeros the full wiener_state_buf [0..543) which includes the SP5
producer block at offsets [213..543).

cargo check + cargo test --lib (sp4+sp5+state_reset_registry: 13/13
pass) both clean. Smoke re-deployment pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 08:54:48 +02:00
jgrusewski
f62860ea82 feat(sp6): Pearl 2 — SAXPY per-branch loss budgets (correction-factor pattern)
SP6 sub-project 1 (Pearl 2): converts the 4 SAXPY launchers from a single
scalar budget (mean of 4 ISV branch slots) to per-branch differentiated
scaling via the correction-factor pattern.

Problem: SP5 Layer B read ISV[190..210) per-branch budget slots but
collapsed them to a scalar via sum/4.0, then passed the single scalar to
apply_c51_budget_scale / apply_cql_saxpy / apply_iqn_trunk_gradient.
Branch HEAD parameters received the same budget as trunk, defeating
per-branch differentiation.

Fix: compute_adaptive_budgets() now returns ([f32;4], [f32;4], [f32;4],
[f32;4], f32, f32, f32, f32) — four per-branch arrays + four trunk-mean
scalars. The trunk mean (D3 decision) is used for the full-buffer trunk/value
SAXPY call (preserving SP5 Layer B behavior for shared params). Branch HEAD
parameter slices receive a correction sub-launch:

  correction = branch_budget[b] / trunk_mean    (skip if |correction-1| <= 1e-6)

After both launches, branch HEAD slice is effectively scaled by
branch_budget[b], trunk/value is scaled by trunk_mean. No double-scaling.

New helpers added to GpuDqnTrainer:
  apply_c51_budget_scale_branch(branch_idx, correction): scale_f32_ungraphed
    on branch-slice [f32 elements], offset via padded_byte_offset.
  apply_cql_saxpy_branch(branch_idx, correction): saxpy_f32_aux on
    branch-slice of both grad_buf and cql_grad_scratch.

IQN trunk gradient: uses iqn_trunk (mean of 4 branch IQN budgets) — IQN
backward flows through trunk only; per-branch IQN routing is beyond SP6 scope.

HEALTH_DIAG: three new per-epoch info! lines emit per-branch c51/iqn/cql
budget arrays (dir/mag/ord/urg) after the intent_dist line.

State: per-branch budget arrays cached on GpuDqnTrainer
(last_*_budget_per_branch: [f32;4]) and on DqnTrainer
(last_*_budget_per_branch: Option<[f32;4]>) for diagnostics.

docs/isv-slots.md: updated Pearl 2 slot rows to reflect SP6 consumer wiring.

Verification: cargo check + release build clean (13 warnings, pre-existing).
13 sp5+sp4+state_reset_registry lib tests pass. sp5_producer_unit_tests
--no-run clean. Sanity grep for old sum/4.0 averaging pattern: empty.

Files changed (6): gpu_dqn_trainer.rs, fused_training.rs, constructor.rs,
mod.rs, training_loop.rs, docs/isv-slots.md. No Pearl 3 or Pearl 5 files touched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 02:08:54 +02:00
jgrusewski
89fadec24a feat(sp6): Pearl 5 — IQN τ per-branch schedule (4 forward passes, ÷4 budget normalization)
GpuIqnHead gains 12 new CudaSlice<f32> buffers (online_taus_branch[4],
target_taus_branch[4], cos_features_branch[4]) allocated at construction time
via alloc_f32. Each slab is [B,N] for taus and [D,N] for cos_features — same
sizes as the existing main buffers.

refresh_taus_for_branch(branch_idx, tau5): uploads one branch's 5-quantile
τ schedule from ISV[IQN_TAU_BASE + b*5 .. +5] to per-branch slabs with cold-start
floor (FIXED_TAUS[q] when ISV slot is zero). No cross-branch averaging.

activate_branch_taus(b) / deactivate_branch_taus(b): symmetric mem::swap helpers
install/restore one branch's slab into self.online_taus/target_taus/cos_features
for a per-branch IQN forward pass. activate→deactivate(b) is its own inverse.

fused_training.rs:
- Tau refresh block calls refresh_taus_for_branch(b, tau5) for all 4 branches,
  then refresh_taus_from_isv for the averaged main buffer (CVaR backward compat).
- grad_decomp_snapshot_iqn() moved BEFORE the parallel/sequential fork so the
  snapshot is taken before any of the 4 per-branch apply_iqn_trunk_gradient calls.
- Parallel path: 4 sequential IQN passes on iqn_stream; after each pass, event
  sync to main stream, apply_iqn_trunk_gradient(iqn_budget/4), re-fork so next
  pass starts after main has consumed d_h_s2_buf. iqn_done_event recorded after
  all 4 passes.
- Sequential path: 4 sequential IQN passes on main stream; apply_iqn_trunk_gradient
  (iqn_budget/4) inline after each pass while d_h_s2_buf holds that branch's result.
- Post-join: single apply_iqn_trunk_gradient removed (now inline); target_ema_update
  and PER loss cast remain.

÷4 normalization: both apply_iqn_trunk_gradient call sites use iqn_budget_per_branch
= iqn_budget / 4.0_f32 so 4 × (budget/4) = budget total — matching SP5 Layer B
gradient magnitude contract exactly.

docs/isv-slots.md: add SP6 Pearl 5 consumer wiring section under the SP5 table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 02:08:45 +02:00
jgrusewski
ed3fa066b9 feat(sp6): Pearl 3 — NoisyLinear per-branch σ array
Replace ExperienceCollectorConfig.noise_sigma: f32 with
noise_sigma_per_branch: [f32; 4] (branch order: dir/mag/ord/urg).

add_advantage_noise kernel (experience_kernels.cu) now takes
const float* noise_sigma[4] + b0/b1/b2/b3 branch-size params.
Each thread derives its branch_idx from action offset using cumulative
branch size offsets; applies that branch's sigma. Sigma=0 fast-exits
with no PRNG work.

GpuExperienceCollector gains noise_sigma_dev: MappedF32Buffer[4]
(mapped-pinned, zero HtoD copy per feedback_no_htod). CPU writes the
4 sigma values via write_from_slice before each kernel launch; kernel
reads via dev_ptr.

training_loop.rs reads ISV[NOISY_SIGMA_BASE..+4] = ISV[210..214)
directly — one slot per branch — instead of averaging all 4 into a
scalar. Cold-start floor 0.01 is Invariant 1 (numerical stability).
Falls back to [hyperparams.noise_sigma; 4] when fused_ctx unavailable.

Default::default() supplies [0.1; 4]. mod.rs test (line 895) uses
Default::default() unchanged — no explicit field to update.

docs/isv-slots.md updated to reflect SP6 Pearl 3 consumer wired.

Files changed: 4 (experience_kernels.cu, gpu_experience_collector.rs,
training_loop.rs, docs/isv-slots.md). No Pearl 2 or Pearl 5 files touched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 02:05:42 +02:00
jgrusewski
6b01b70c3e docs(sp6): implementation plan — 3-sub-project parallel refactor (Pearls 2, 3, 5)
Three independent sub-projects (one per Pearl), each an atomic commit,
each running in its own git worktree (sp6-pearl-2/3/5). Pearl 2 expands
compute_adaptive_budgets() to per-branch [f32;4] arrays + trunk-mean
scalars, dispatches branch-correction SAXPY sub-launches. Pearl 3
replaces noise_sigma: f32 with noise_sigma_per_branch: [f32;4] in
ExperienceCollectorConfig and extends add_advantage_noise to per-branch
lookup. Pearl 5 adds 4-branch tau buffer triplets to GpuIqnHead, runs
4 IQN forward passes per step (Approach B), each contributing
iqn_trunk/4 to prevent 4x gradient accumulation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 01:47:07 +02:00
jgrusewski
ca6e0007da docs(sp6): brainstorm spec — per-branch consumer infrastructure refactor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 01:36:48 +02:00
jgrusewski
3ad5e011b9 fix(sp5): Layer B fix-up — close 3 review findings before Layer C
Comprehensive review of Layer B (commit 99367b9c6) caught one critical
and two important findings. All three fix in one atomic commit.

Critical: IQL Adam groups 4+5 (IqlHigh, IqlLow) missed the Pearl 4
migration. gpu_iql_trainer.rs::train_value_step still read
self.config.beta1=0.9, self.config.beta2=0.999 at runtime. ISV[230]
(adam_beta1(4)) and ISV[231] (adam_beta1(5)) were populated by the
Layer A producer but unconsumed — partial refactor of the 8-group
Pearl 4 contract. Fix mirrors the Curiosity pattern: 3 new
iql_beta1/beta2/epsilon parameters threaded through the IQL Adam call,
2 ISV reads at each of the 2 train_value_step call sites in
fused_training.rs.

Important: consumer clamp envelopes were wider than the SP4 producer
range (0.5..0.9999 vs producer's 0.85..0.95 for β1, etc.). At
cold-start with ISV=0, this produced β1=0.5 (more aggressive momentum
than the SP4-anchored 0.85 floor). Tightened all Pearl 4 consumer
clamps to match the producer envelope: β1∈[0.85,0.95], β2∈[0.99,0.9995],
ε∈[1e-10,1e-6].

Important: gpu_curiosity_trainer.rs:66-68 left dead ADAM_BETA1/BETA2/
EPS constants after the Pearl 4 migration. Removed.

8/8 Pearl 4 groups now ISV-driven (DqnTrunk, Value, Branches, IQN,
IqlHigh, IqlLow, Attn, Curiosity). cargo check + cargo build + lib
tests all clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 01:27:40 +02:00
jgrusewski
99367b9c60 feat(sp5): Layer B — atomic consumer migration to ISV-driven adaptive signals
SP5 Layer B: all consumers of ISV slots 174..286 migrated in one atomic
commit per feedback_no_partial_refactor.

Pearl 1 (C51 atom span, done prior session):
  atoms_update_kernel.cu: reads v_center/v_half from ISV[174+b]/ISV[178+b]

Pearl 1-ext (per-branch num_atoms):
  atoms_update_kernel.cu: reads ISV[274+b], rounds to valid {16,32,64},
  uses eff_na for softmax/cumsum; output stride still global num_atoms

Pearl 2 (per-branch loss budgets):
  fused_training.rs: compute_adaptive_budgets() reads ISV[190..194/198/202]
  averaged over 4 branches; Invariant-1 floors 0.05/0.05/0.02/0.02

Pearl 3 (per-branch NoisyNet sigma):
  training_loop.rs: ExperienceCollectorConfig.noise_sigma reads
  mean(ISV[210..214]) with 0.01 floor; falls back to hyperparams.noise_sigma
  when fused_ctx absent

Pearl 4 (per-group Adam beta1/beta2/epsilon):
  gpu_dqn_trainer.rs: per-group reads inside launch_adam_update loop via
  ISV[226+g], ISV[234+g], ISV[242+g]
  gpu_iqn_head.rs: iqn_beta1/beta2/epsilon threaded into
  execute_training_pipeline + train_iqn_step_gpu
  gpu_attention.rs: attn_beta1/beta2/epsilon threaded into adam_step
  gpu_tlob.rs: same as attention (shares ParamGroup::Attn=6)
  fused_training.rs: 4 call sites read ISV[226+g..242+g] for IQN+Attn+TLOB
  gpu_curiosity_trainer.rs: cur_beta1/beta2/epsilon params added to
  launch_adam_step + train_on_collector_buffers
  gpu_experience_collector.rs: train_curiosity_gpu threads cur_beta1/2/eps
  training_loop.rs: reads ISV[233/241/249] for Curiosity=7 at call site

Pearl 5 (per-branch IQN tau schedule):
  gpu_iqn_head.rs: new refresh_taus_from_isv() averages 4 branches per
  quantile, applies FIXED_TAUS cold-start floor, re-uploads online_taus/
  target_taus/cos_features in-place via upload_f32_via_pinned
  fused_training.rs: reads ISV[250..270) before IQN prepare_buffers

Pearl 6 (Kelly cap) + Pearl 8 (trail distance) done in prior session:
  trade_physics.cuh, experience_kernels.cu, backtest_env_kernel.cu

Audit doc: dqn-wire-up-audit.md SP5 Layer B entry added.

All 930 passing lib tests still pass; 14 pre-existing failures unchanged.
Compile: SQLX_OFFLINE=true cargo check -p ml --offline clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 01:11:30 +02:00
jgrusewski
19e5cde91e docs(sp5): Layer A close-out — replace stale scratch-buf comment
Code-quality review of A8 caught that gpu_dqn_trainer.rs:15154 still
read `// to SP5_SCRATCH_TOTAL (103) = 71 + 16 (q_branch_stats) + 16
(pearl_1_atom)` — pre-existing from A1 that wasn't updated as the
constant grew through A2-A8 (103 → 111 → 131 → 171 → 199 → 203 → 207).
The runtime allocation is correct (uses the constant, not the literal),
so this was readability-only.

Replaced the 2-line stale comment with the complete scratch-buffer
layout map covering all SP5 Layer A producer outputs:

  [0..71)    SP4 producers
  [71..87)   q_branch_stats        (Pearl 1 / A1)
  [87..103)  pearl_1_atom_update   (Pearl 1 / A1)
  [103..107) pearl_3_sigma σ       (Pearl 3 / A2)
  [107..111) pearl_3_sigma SF      (Pearl 3 / A2)
  [111..131) pearl_2_budget        (Pearl 2 / A3)
  [131..147) pearl_4 cosine + l2   (auxiliary, A4)
  [147..171) pearl_4 β1/β2/ε       (Pearl 4 / A4)
  [171..199) pearl_5 skew/kurt+τ   (Pearl 5 / A5)
  Pearl 6 (A6) writes directly to ISV — no scratch.
  [199..203) pearl_8 trail_dist    (Pearl 8 / A7)
  [203..207) pearl_1-ext num_atoms (Pearl 1-ext / A8)

Closes the last residual A1-vintage comment debt; gives Layer B
implementers a self-contained map of the scratch contract.

Comment-only change. cargo check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:26:43 +02:00
jgrusewski
2e84fd35d4 feat(sp5): Task A8 — Pearl 1-ext per-branch num_atoms — Layer A complete
Final SP5 Layer A producer. Per-branch C51 num_atoms derived from
per-branch ATOM_V_HALF (Pearl 1, Task A1). Threshold cascade:
  v_half < 0.1  → 64 atoms (narrow Q, high resolution)
  v_half < 1.0  → 32 atoms (moderate)
  v_half ≥ 1.0  → 16 atoms (wide Q, modest resolution)

4 ISV slots [ATOM_NUM_ATOMS_BASE=274..278). Pearls A+D smooths the
discrete output during transitions; Layer B's atoms_update consumer
rounds to nearest valid count.

producer_step_scratch_buf grew 203 → 207. wiener_state_buf already
at 543 (sized at A1 for entire SP5 block).

StateResetRegistry: 1 new FoldReset entry (sp5_atom_num_atoms).

atoms_update consumer migration deferred to Layer B.

LAYER A COMPLETE. 8 producers + 3 auxiliary kernels (q_branch_stats,
grad_cosine_sim, q_skew_kurtosis) populating 110 ISV slots [174..286)
with 4 cross-fold-persistent slots carved out (Kelly).

Refs: SP5 spec 6e6e0fa11 line 1020

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:36 +02:00
jgrusewski
b4e5fc9891 fix(sp5): Task A7 — close two minor review findings
Combined spec-quality review caught two minor issues in the Pearl 8
commit. Both are mechanical fixes; no behavior change.

1. training_loop.rs:3640 used `let _ = std::mem::ManuallyDrop::new(guard);`
   to keep the cudarc StreamGuard alive past the inner let-binding
   block — the pattern leaks the guard's borrow bookkeeping for the
   lifetime of the function rather than dropping it after the kernel
   launch. Replaced with the idiomatic
     let (feat_dev_ptr, _guard) = features_buf.device_ptr(stream_ref);
   at the same scope as the launch, matching the existing convention
   for SP4 producer wire-ups elsewhere in this file. The
   _guard binding lets cudarc's borrow drop naturally at end-of-block
   after the kernel launch returns.

2. The audit doc's description of test 14 incorrectly claimed
   atr_norm was chosen so atr_abs=10.0 and Short/Long=20.0. The
   actual test uses atr_norm=0.5 → log_atr=1.0 → atr_abs=e^1≈2.7183
   → Short/Long≈5.4366. Updated the audit doc to match the actual
   test values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:06:40 +02:00
jgrusewski
fd1426a398 feat(sp5): Task A7 — Pearl 8 per-direction trail-stop distance
Per-direction trail-stop distance derived from the current bar's ATR.
Short[270] and Long[272] receive 2× denormalized ATR (industry-standard
2×ATR trail); Hold[271] and Flat[273] receive EPS_CLAMP_FLOOR=1.0
(no trail-stop fires for those directions; floor prevents zero).

4 ISV slots [TRAIL_DIST_PER_DIR_BASE=270..274). ATR is read from
features[bar_idx × market_dim + 9] (ATR_NORM column) and denormalized
via the canonical fxcache scheme:
  atr_abs = max(0.01, exp(atr_norm × 16 − 7))
Constants 16, 7, 0.01 are Invariant 1 anchors from the existing
fxcache normalization in experience_kernels.cu:2701.

Layer A simplification: Short and Long share the same current-bar ATR
value. The spec implied direction-conditional ATR via per-trade-event
aggregation, but that requires infrastructure not yet in place. The
4-slot ISV layout preserves the consumer contract so a future Layer C
extension can populate truly direction-specific values without
breaking Layer B's check_trailing_stop reads.

producer_step_scratch_buf grew 199 → 203 (1 new constant
SCRATCH_PEARL_8_TRAIL=199). wiener_state_buf stays at 543 (sized at
A1 for entire SP5 block).

StateResetRegistry: 1 new FoldReset entry (sp5_trail_dist_per_dir).
Pearl A sentinel 0 → bootstrap on fold boundary's first launch.

check_trailing_stop consumer migration deferred to Layer B.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 23:59:22 +02:00
jgrusewski
f40ccc16a7 fix(sp5): Task A6 — close two minor review findings
Combined spec/quality review caught two minor issues in the Pearl 6
commit. Both are mechanical fixes; no behavior change.

1. Test 12 (pearl_6_kelly_within_fold_ewma_blend) was missing an
   explicit assertion for slot 282 (TRADE_VAR_SMOOTH_IDX). The test
   setup initialized tvar_i32 and the launcher passed it through the
   kernel's parameter slot, but no assert! ever fired against the
   resulting ISV value. With n_envs=1, the kernel's
   `(kelly_count > 1) ? variance : 0.0f` branch returns 0 (no
   cross-env variance possible with 1 env), so EWMA blend yields
   0.99 × 0.5 + 0.01 × 0.0 = 0.495. Added the missing assertion to
   close the within-fold coverage gap for s==2.

2. pearl_6_kelly_kernel.cu:136 doc comment said the slot computes
   "standard deviation of per-env Kelly fractions" but the code
   actually computes `ksum_sq / kelly_count` — i.e. the variance
   (second moment), not the standard deviation. The slot name
   TRADE_VAR_SMOOTH_INDEX correctly indicates variance; the comment
   was wrong. Updated comment to match: 'variance of per-env Kelly
   fractions' with explicit note that this is the second moment,
   NOT std-dev (no sqrtf applied).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:31:37 +02:00
jgrusewski
5b4cdec4ff feat(sp5): Task A6 — Pearl 6 cross-fold-persistent Kelly cap signals
Adds `pearl_6_kelly_kernel.cu` (single-block 6-thread kernel reading
portfolio_state directly) to populate ISV[280..286) with Bayesian-prior
Kelly fraction, conviction identity, trade variance, cumulative sample
count (max-semantics), win-rate and loss-rate EMAs. EWMA α=0.01 is an
Invariant 1 structural anchor for cross-fold inertia.

Key design departure from A1-A5: ISV[280..286) are intentionally EXEMPT
from the StateResetRegistry and from apply_pearls/Pearls A+D. portfolio_state
has WindowReset lifecycle (resets at EVERY window boundary, not just fold),
making cross-fold persistence essential. The max()-semantics for sample_count
(s==3) ensure the cumulative count never decreases across any boundary.
Wiener offsets [525..543) that naive formula would produce are intentionally
unused; in-kernel EWMA replaces external wiener bootstrap.

Verification: cargo check -p ml --offline clean (11 pre-existing warnings,
no new). sp5_producer_unit_tests --no-run clean. Two GPU tests (12, 13)
validate EWMA blend and cross-fold persistence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 23:22:06 +02:00
jgrusewski
1aaa1cb0bc docs(sp5): Task A5 — correct pearl_5_iqn_tau header comment direction
Code-quality review caught that the kernel header described the τ
shift as 'toward the dense tail' but the actual math
(`t = default + skew × SKEW_SHIFT`) shifts τ in the SAME direction
as the skew sign — i.e. toward the LONG (sparse) tail, not the dense
region.

For a left-skewed distribution (long left tail, mode + dense mass on
the right), skew < 0 → τ shifts down toward 0 → IQN samples more of
the LEFT tail (sparse). The intent is risk-aware quantile coverage:
when there's tail mass that the symmetric 5-tuple under-represents,
lean the grid toward that tail. This matches risk-aware IQN where
financial risk modeling cares about downside coverage.

The math is correct and faithful to the plan's formula; only the
prose description was reversed. Replaced the one-liner with a 7-line
explanation that names the actual semantic clearly ('leaning the
quantile grid INTO the asymmetry direction').

Comment-only change. Cubin rebuild clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:59:46 +02:00