c4aa71b9dfbc150f4bbd1cbfa4ee91f0cc0dbaee
887 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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, |
||
|
|
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 |
||
|
|
66f7e64d13 |
fix(sp5): D1 rewrite — match production per-bar semantics, fix D4 blocker
The original D1 kernel (commit
|
||
|
|
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 ( |
||
|
|
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 (
|
||
|
|
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
|
||
|
|
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 @ |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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
|
||
|
|
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
|
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
8df28b81c8 |
merge: SP6 Pearl 5 — IQN τ per-branch (replaces SP5 broken upload pattern)
Brings in worktree-agent-acd65ada (commit
|
||
|
|
76d58406bd |
merge: SP6 Pearl 3 — NoisyLinear per-branch σ array
Brings in worktree-agent-a4d8a879 (commit
|
||
|
|
becd488320 |
merge: SP6 Pearl 2 — SAXPY per-branch loss budgets
Brings in worktree-agent-a284432c (commit
|
||
|
|
82ca733255 |
fix(sp5): Layer A bug — add 21 reset_named_state dispatch arms
L40S smoke at SP5 Layer B HEAD (
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
ca6e0007da |
docs(sp6): brainstorm spec — per-branch consumer infrastructure refactor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
3ad5e011b9 |
fix(sp5): Layer B fix-up — close 3 review findings before Layer C
Comprehensive review of Layer B (commit
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
b1ef312a40 |
feat(sp5): Task A5 — Pearl 5 per-branch IQN τ schedule GPU producer (Layer A)
Two new CUDA kernels land as SP5 Layer A additive producers feeding ISV[250..270)
with per-branch IQN quantile-τ schedules derived from Q-distribution skew.
q_skew_kurtosis_update: single-block 4-thread, reads save_q_online[B×13],
two-pass central moments → skew clamped [-3,+3] + ex_kurt clamped [-3,+30];
writes scratch[171..179). EPS_DIV=1e-12 (Invariant 1 anchor). No atomicAdd.
pearl_5_iqn_tau_update: single-block 4-thread, shifts symmetric default τ
{0.05,0.25,0.5,0.75,0.95} by skew×SKEW_SHIFT=0.05, clamps to [0.01,0.99];
writes scratch[179..199). SKEW_SHIFT and envelope are Invariant 1 anchors.
launch_sp5_pearl_5_iqn_tau fires both kernels + 20 apply_pearls_ad calls
(ALPHA_META=1e-3) → ISV[IQN_TAU_BASE=250..270). SP5_SCRATCH_TOTAL 171→199.
StateResetRegistry +1 FoldReset entry (sp5_iqn_tau, ISV[250..270)).
training_loop.rs wired after Pearl 4 with tracing::warn on error.
Two GPU-only unit tests (10+11): zero-skew symmetric default + left-skew
floor clamp. Module docstring updated A1-A5.
No consumer migration — Layer A additive only per spec.
cargo check -p ml --offline clean (11 pre-existing warnings, none new).
cargo test --no-run clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
d4d12abab2 |
docs(sp5): close two minor review findings before A5
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
|
||
|
|
4da6b34d78 |
test(sp5): Task A4 — add grad_cosine_sim_update unit test
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> |
||
|
|
67dd414cb3 |
feat(sp5): Task A4 — Pearl 4 per-group Adam β1/β2/ε
Per-param-group Adam hyperparameters from per-group gradient
direction-stability EMA + L2 norm. 8 SP4 param groups × 3 hparams = 24
ISV slots [226..250).
Two-kernel chain:
grad_cosine_sim_update — per-group cosine_sim of curr vs prev grads
+ L2 norm; writes back grad_curr → grad_prev
for next step's comparison.
pearl_4_adam_hparams_update — β1/β2/ε from cosine + l2_norm.
Structural envelopes (Invariant 1 anchors per spec line 88):
β1 ∈ [0.85, 0.95]; β2 ∈ [0.99, 0.9995]; ε ∈ [1e-10, 1e-6]
ALPHA_META migration (Option A — atomic shared-contract migration per
feedback_no_partial_refactor): apply_pearls_ad_kernel.cu and the
launch_apply_pearls Rust wrapper now take alpha_meta as a parameter.
All 45 existing call sites (SP4 + SP5 producers) pass the prior default
1.0e-3 explicitly via crate::cuda_pipeline::sp4_wiener_ema::ALPHA_META.
Pearl 4's apply_pearls calls pass 5.0e-4 (half the default per spec
line 89) to limit β change rate.
Theoretical caveat: adaptive β breaks Adam's constant-β convergence
proof. Mitigations: structural envelopes + halved ALPHA_META + Pearls
A+D smoothing. Fall-back path defined: revert + ε-only adaptive
variant if Layer C destabilization observed.
New mapped-pinned buffer: grad_prev_buf_per_group [TOTAL_PARAMS] for
cosine-sim previous-step direction storage. Mapped-pinned i32 mirror
of grad_buf layout.
producer_step_scratch_buf grew 131 → 171 (40 new outputs: 8 cosine_sim
+ 8 l2_norm + 24 β1/β2/ε). wiener_state_buf already at 543 (A1 sized
for entire SP5 block).
StateResetRegistry: 4 new FoldReset entries: sp5_adam_beta1,
sp5_adam_beta2, sp5_adam_eps, sp5_grad_prev_buf. All sentinel 0 →
bootstrap to envelope midpoint via Pearls A+D + cosine_sim=0 fall-back
on first step of new fold.
Adam-launcher consumer migration deferred to Layer B.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
71a0275f54 |
test(sp5): Task A3 — assert budget sum-to-1 invariant
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> |
||
|
|
4b2093a627 |
feat(sp5): Task A3 — Pearl 2 per-branch loss budget
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> |
||
|
|
ab3e17f4a2 |
feat(sp5): Task A2 — Pearl 3 per-branch NoisyNet sigma
Per-branch sigma scales with per-branch Q magnitude (v_half from Pearl 1's ATOM_V_HALF, populated in A1). SIGMA_FRACTION adapts via entropy-deficit controller targeting 70% of max action entropy (target_entropy = log(n_actions[b]) * 0.7). 8 ISV slots (NOISY_SIGMA[210..214), SIGMA_FRACTION[214..218)). Reuses BRANCH_ENTROPY (218..222) from Task A1's q_branch_stats_kernel. producer_step_scratch_buf grew 103 -> 111 (8 new outputs). wiener_state_buf already at 543 (A1 sized for entire SP5 block). NoisyLinear consumer migration deferred to Layer B. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
3b4ce05465 |
fix(sp5): Task A1 — pearl_1 clip_rate denominator must be per-branch
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>
|
||
|
|
0b0f3b21e1 |
feat(sp5): Task A1 — Pearl 1 atom-span + Q-stats GPU producers (Layer A)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
f91098e456 |
refactor(sp5): Task A0 quality fix-up — rustdoc + HashSet overlap test
Address two code-quality review items on commit
|
||
|
|
6dcaf1a1c9 |
feat(sp5): Task A0 — define 110 SP5 ISV slot constants
Foundation commit for SP5 per-branch + per-group adaptation layer.
Allocates slot ranges 174..286 with 6 cross-fold-persistent Kelly
slots at 280..286 (NOT in fold-reset registry per spec). Intentional
2-slot gap at 278..280 makes the cross-fold carve-out structurally
visible.
Slot layout:
174..226 Per-branch (Pearls 1, 2, 3 + Q-var shared signal)
226..250 Per-group Adam β1/β2/ε (Pearl 4)
250..270 Per-branch IQN τ schedule (Pearl 5)
270..274 Per-direction trail distance (Pearl 8)
274..278 Per-branch num_atoms (Pearl 1-ext)
280..286 Cross-fold-persistent Kelly (Pearl 6)
LAYOUT_FINGERPRINT_SEED bumped — existing checkpoints fail-fast.
ISV_TOTAL_DIM 173 → 286.
No producer/consumer wired yet. Layer A commits A1-A8 populate slots
in per-pearl commits in dependency order:
Pearl 1 → Pearl 3 → Pearl 2 → Pearl 4 → Pearl 5 → Pearl 6 → Pearl 8 → Pearl 1-ext
Refs: docs/superpowers/specs/2026-05-01-sp5-magnitude-differentiation-and-eval-collapse-design.md (HEAD
|