Diag: surfaces `risk_stack.eval_warmup.{remaining,active,blend,
floor_*,target_*}` so the v9 defensive-warmup window is observable
in diag.jsonl. `remaining` is the counter; `blend` is the
defensive-vs-normal mix coefficient (1.0 = full defensive, 0.0 =
normal); `floor_*` reflect the LIVE override values (read AFTER the
warmup kernel ran). Pre-warmup the kernel is a no-op (remaining=-1),
so v9 train-phase diag is bit-identical to v8.
Cleanup: tightens unreachable_pub items in tests/behavioral/* and
tests/sp5_producer_unit_tests.rs (pub → pub(crate)), removes
unused_mut on 6 sp5 scratch buffers, renames unused `step` loop
counter in alpha_baseline example, and explicitly discards an
intentionally-no-op `Command::assert` in cli_integration_test.
Reduces lint count by ~25; remaining 3 dead_code warnings flag
SP15 Phase 2A behavioral scaffolding (Phase 2B never landed —
deliberate signal, not noise).
Pre-existing pearl per feedback_no_hiding: do NOT suppress these
with #[allow]; the warnings ARE the design call surface.
Fixes the SP7 controller dormancy discovered in smoke-test-8556k:
per-branch budgets stuck at exactly bootstrap constants because the
kernel's cold_start_basis numerically equaled the consumer's bootstrap
fallback, AND the Wiener-α was clamped at ALPHA_FLOOR=1e-4 from step 1.
Architectural change:
- 8 new ISV slots LB_{CQL,C51}_ACTIVE_BASE per (head × branch).
Activation flag is monotonic per fold, FoldReset on boundary.
- Kernel only sets active=1 when grads are populated AND on subsequent
active-state computation; cold-start branch leaves active=0 (fresh
branch) or holds prior budget steady (transient grad gate).
- Consumer dispatches on activation: bootstrap when active<0.5,
controller verbatim when active>=0.5. No more spurious bootstrap
when controller writes legitimate small values.
- Welford-α hybrid (max of 1/max(1,epoch_idx_in_fold) and Wiener-α)
gives full update on first active step, falls off as 1/N until
Wiener takes over with meaningful variance estimates. EPOCH_IDX_INDEX
is the existing per-fold-reset counter (no new tuned constants).
State reset registry: 2 new sp7_lb_*_active FoldReset entries +
matching dispatch arms in reset_named_state. Contract test
(every_fold_and_soft_reset_entry_has_dispatch_arm) gates compile.
GPU unit test sp7_loss_balance_controller_activation_flag_transitions
exercises 3 transitions (cold start → both flags 0; active → both
flags 1 with controller-computed budget != bootstrap; transient grad-
gate → flags hold at 1, prior budget held verbatim). Passes on local
RTX 3050 Ti.
Audit doc: Fix 31 sub-bullet describing the activation-flag fix.
Memory pearl out-of-tree (controller will dispatch separately).
Touched:
crates/ml/src/cuda_pipeline/sp5_isv_slots.rs
crates/ml/src/cuda_pipeline/loss_balance_controller_kernel.cu
crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs
crates/ml/src/trainers/dqn/fused_training.rs
crates/ml/src/trainers/dqn/state_reset_registry.rs
crates/ml/src/trainers/dqn/trainer/training_loop.rs
crates/ml/tests/sp5_producer_unit_tests.rs
docs/dqn-wire-up-audit.md
Cargo check workspace clean. Cargo test ml --lib clean (incl. contract
test + 6 sp5_isv_slots tests). 16 pre-existing failures unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
Two minor code-review nits accumulated across A1-A4 reviews; closing
them in a single docs/comment-only commit before Layer A continues.
1. tests/sp5_producer_unit_tests.rs module docstring (A4 review):
the file header still claimed it covered only A1's two kernels
even though A2/A3/A4 had each appended their tests. Updated header
to enumerate all 9 tests across A1-A4 (+ 1 grad_cosine_sim test
landed in 4da6b34d7) so a reader cold to the file can locate each
producer's coverage without scanning the body.
2. gpu_dqn_trainer.rs:2854 stale field comment `[B, TOTAL_ACTIONS(11)]`
(A1 review): comment was left at 11 since the pre-4-direction
layout but the actual slot count is 13 (4 dir + 3 mag + 3 ord + 3
urg). Updated to `[B, TOTAL_ACTIONS(13)]` with the per-branch
breakdown inline so the field declaration matches the SP5 producer
docstrings (e.g. q_branch_stats_kernel comments at line 277, 282).
Comment-only changes; no behavior change. cargo check + cargo test
--no-run both clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-quality review caught that pearl_4_adam_hparams_kernel had direct
GPU tests but the auxiliary grad_cosine_sim_kernel had none. Tests 7
and 8 launched the hparams kernel with pre-baked cosine inputs,
never exercising the per-group reduction (dot + 2× L2 norm sums) or
the writeback (grad_curr → grad_prev for next step's comparison).
The writeback is load-bearing for cross-step cosine evolution — if
broken, every subsequent step's cosine_sim is computed against a stale
or mis-aligned previous gradient.
Adds Test 9 `grad_cosine_sim_per_group_dot_norm_and_writeback` with
8 analytically-known synthetic group cases:
group 0: curr=prev=e1 → cos=+1, |c|=1
group 1: curr=e1, prev=e2 → cos= 0 (orthogonal)
group 2: curr=e1, prev=-e1 → cos=-1 (antiparallel; raw)
group 3: curr=prev=(3,4,0,0) → cos=+1, |c|=5
group 4: curr=0, prev=e1 → cos= 0, |c|=0 (cold-start curr)
group 5: curr=e1, prev=0 → cos= 0, |c|=1 (Pearl A sentinel)
group 6,7: same as group 0
NO CPU oracle — expected values are unit-vector cosines from the
kernel formula. Writeback verified by reading grad_prev_buf after
the kernel runs and asserting bit-identity with grad_curr_buf for
all 32 elements.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-quality review caught that the two Pearl 2 unit tests verified each
output (c51, iqn, cql, ens) against its analytical expected value within
1% relative tolerance, but did NOT assert the structural invariant
`c51 + iqn + cql + ens ≈ 1.0` that the kernel maintains by construction
(`ens = max(0, 1 - iqn - c51 - cql)`).
A coefficient typo (e.g. BASE_IQN=0.111 instead of 0.11) would produce
individually-plausible per-component values that all still pass the
relative checks while quietly summing to 0.97 or 1.04. The sum check
catches that class of regression.
Adds `assert!((c51+iqn+cql+ens - 1.0).abs() < 1e-4)` inside both
per-branch loops in the flat-regime and sharp-regime tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-branch C51/IQN/CQL/Ens loss budgets driven by per-branch flatness =
var(Q[b]) / (σ[b]² + EPS_DIV). IQN dominates when flat, C51 yields
proportionally. CQL stays gated by existing regime_stability allocator.
Reads Q_VAR_PER_BRANCH (222..226 from Pearl 1's q_branch_stats) AND
NOISY_SIGMA (210..214 from Pearl 3 — must run AFTER both).
20 ISV slots (BUDGET_C51[190..194), BUDGET_IQN[194..198),
BUDGET_CQL[198..202), BUDGET_ENS[202..206), FLATNESS[206..210)).
StateResetRegistry: 5 new FoldReset entries.
producer_step_scratch_buf grew 111 → 131 (20 new outputs).
wiener_state_buf already at 543 (A1 sized for entire SP5 block).
Loss budget consumer migration deferred to Layer B.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code-quality review caught: pearl_1_atom_kernel.cu computed
`clip_rate = atoms_clip_count[b] / (batch_size × 13)` using the
total-actions=13 sum across all branches. The headroom controller's
TARGET_CLIP_RATE=0.01 is calibrated against the per-branch clip event
rate. Using a denominator 3.25–4.3× larger than the per-branch
denominator made the controller systematically under-responsive — at
actual 1% per-branch clipping it would read ~0.23–0.31% and contract
headroom toward the 2.0 floor instead of holding the target.
Currently masked: `atoms_clip_count` is zero in Layer A (Layer B's
atoms_update_kernel migration is what populates it). Without this fix,
Layer B would inherit a silently-wrong formula that converges to the
floor rather than the target rate.
Fix: plumb `action_counts[4]` ({4, 3, 3, 3}) through the kernel
signature and use `batch_size × action_counts[b]` as the per-branch
denominator. Matches q_branch_stats_kernel's existing parameter pattern.
Test #2 launcher updated to allocate action_counts_buf and pass its
dev_ptr through the new kernel parameter slot. Audit doc entry added
per Invariant 7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Test sp5_producer_unit_tests.rs:212 called `scratch_buf.len()` but `len`
is declared as `pub len: usize` on MappedF32Buffer (mapped_pinned.rs:210),
not a method. `cargo test --no-run` failed with E0599; library + cubin
build was unaffected.
One-character fix: `scratch_buf.len()` → `scratch_buf.len`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>