ebbd28437ad153914929164fd24ef16d928f77ed
311 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
db874b1841 |
feat(foxhuntq): Phase 1c snapshot-resolution alpha + leakage fix + variable-dim fxcache
Three things landing atomically because they're load-bearing for each other: 1. **Trend-scanning leakage fix** — trend_scanning.rs was emitting OLS slope+t-stat over a *forward* window [t, t+L]. With the Phase 1a label = sign(price[t+60] − price[t]), the forward feature window overlaps the label window, contaminating it. Purged walk-forward only sterilizes forward-looking *labels* that cross the train/val split, not forward-looking *features* that peek inside the same horizon the label measures. The leak inflated MLP accuracy from 0.49 (legacy 74-dim baseline) to 0.75 — vanished to 0.50 after switching to a trailing window. Bounded the perfect-fit t-stat sentinel from ±1e6 → ±20 (p<1e-30 is already meaningless); eliminated the 16k corruption-cap drops. 2. **Variable-dim alpha column** — fxcache schema now carries the alpha-feature width via metadata (`alpha_feature_dim`), not a compile-time constant. Same on-disk format hosts the 134-dim bar-level stack OR the 81-dim snapshot stack. Reader + auto-detect honor the metadata-declared dim; downstream MLP auto-sizes `in_dim`. Single schema, no forks. 3. **Snapshot pipeline (Phase 1c falsification)** — `snapshot_pipeline.rs`: 81-dim per-MBP10-snapshot extractor reusing 10 snapshot-native alpha blocks + 6 new snapshot-specific features (time-since-trade, time-since-snap, event-rate, spread-bps, L1-imbalance, microprice-mid drift). `precompute_features` gets `--row-unit snapshot` flag; emits one fxcache row per LOB update (1.97M rows from MBP-10 data vs 206K for bar mode). **Smoke verdict on real data** (ES.FUT, 1.97M snapshots, 384K val): - Bar-level honest alpha: accuracy=0.5005, AUC=0.5043 (no signal) - **Snapshot-level alpha**: accuracy=0.5241, AUC=0.6849 (real signal, 384K val) - GBM corroboration: accuracy=0.5401 (non-linear partitioning sees more) - Horizon decay: alpha peaks at K=20-50 snapshots (~5-25ms), gone by K=500 - Regime-conditional: spread-Q4 quintile hits 0.752 accuracy on 76k samples Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
71eab9a253 |
fix(tests): gpu_backtest_validation action constants — Long100 is 72, not 4
Two `gpu_backtest_validation` tests were failing with bit-identical
deterministic values for 2+ months: `test_always_long_on_downtrend`
(expected negative PnL, got +0.00023627281) and
`test_multiple_windows_produce_results` (uptrend < downtrend instead
of > ).
Root cause: SP21 Phase 8.5 (2026-05-12) wired the factored 4-3-3-3
action decoder into the eval (`backtest_state_gather` + env_step),
but the test's hardcoded `constant_action_model(4, ...)` integer
literal wasn't migrated. Pre-Phase-8.5 the eval used a flat
4-action enum where `4` reportedly meant Long100; the factored
decoder now interprets `4` as:
decode_direction_4b(4, b1=3, b2=3, b3=3) = 4 / 27 = 0 = DIR_SHORT
decode_magnitude_4b(4, b1=3, b2=3, b3=3) = (4/9) % 3 = 0 = MAG_QUARTER
So the test was running Short-Quarter (-0.25 position) on the trend
fixtures. On random-walk synthetic prices with drift ±0.001 vs σ=0.01
noise per bar, the 24-step eval window has S/N ≈ 0.49 — specific
seeds can produce net-against-drift trajectories, making the actual
short-quarter PnL small but deterministic, with sign flipped relative
to test intent.
Fix: change `constant_action_model(4, ...)` → `constant_action_model(72, ...)`
in the 2 failing tests. Action 72 = dir=LONG (2) * 27 + mag=FULL (2)
* 9 + 0 + 0 — the actual "Long100" under 4-3-3-3 factoring. Both
tests now pass; no regressions on the 4 previously-passing tests.
Verification
────────────
- gpu_backtest_validation pre-fix: 4 passed, 2 failed
- gpu_backtest_validation post-fix: 6 passed, 0 failed
Out of scope for this commit (follow-up audit needed)
─────────────────────────────────────────────────────
Three other tests in the same file have the same stale `4` constant
with misleading "Always Long100" comments, but currently pass
incidentally:
- `test_always_long_on_uptrend` (line 218): asserts `total_pnl > 0`.
Currently passes BY ACCIDENT — action=4 (Short-Quarter) on seed-42's
net-down 24-bar trajectory produces +PnL, satisfying the assertion
for the wrong reason. Fixing to action=72 alone would break this
test (true Long100 on seed-42's net-down trajectory is negative);
the test needs BOTH the action fix AND a seed/window change so the
"uptrend" trajectory actually trends up over the eval window
(e.g., 250-bar window or drift=0.01).
- `test_extended_metrics_populated` (line 465) and
`test_active_model_records_trades` (line 524): assertions are
direction-agnostic (VaR/CVaR/Calmar/Omega NaN-check + CVaR≤VaR;
total_trades > 0 + win_rate range), so they pass legitimately
under whatever-direction action=4 produces. The "Long100" comments
are misleading but the tests are correctly covering their stated
behavior.
A separate audit-and-fix pass should address all three at once:
either correct the action constants + adjust seed/window to ensure
each test's named trajectory direction is statistically reliable,
or introduce a named constant (e.g., LONG100_ACTION) and helper to
prevent the same drift recurring.
Refs
────
- SP21 T2.2 Phase 8.5 commit
|
||
|
|
9f2d0fffb5 |
fix(sp21): T2.2 Phase 8.7 — default ISV buffer in evaluator (atomic)
v8 smoke (train-96wfk, commit
|
||
|
|
ea12c172e6 |
test(sp21): T2.2 Phase 1.5 — kernel-direct GPU oracle for per-trade predicted_q
Promotes the Phase 2.5 follow-up from the plan to a load-bearing test.
Three #[ignore = "requires GPU"] tests exercise backtest_env_step_batch
directly with controlled inputs:
1. predicted_q_populated_on_close_with_real_q_values — open Long Full
at step 0 with q_values_per_window[w=0, a=Long_Full]=2.5, hold for
two bars, close Flat at step 3. Asserts the per-trade tape's
predicted_q[0] ≈ 2.5 (entry_q captured at open, persisted in
entry_q_state across holds, snapshotted before close emit).
2. predicted_q_stays_zero_when_q_values_is_null — same scenario with
q_values_per_window=NULL (single-step evaluate() semantics). The
kernel's open-time write is gated on the NULL guard, so
entry_q_state[w] stays at buffer-init 0.0 and the tape's
predicted_q is 0.0. Proves the NULL-tolerance contract is
kernel-enforced, not just launcher convention.
3. no_trades_no_predicted_q_emission — all-Flat action sequence
produces zero trades. Path-coverage check that entry_q's open
guard fires only on actual opens / reverses.
Test design:
- Kernel-direct (loads ENV_CUBIN via include_bytes!), no eval-pipeline
scaffolding (no QValueProvider, no cuBLAS forward, no chunked state
gather). Avoids the heavy mock infrastructure the plan flagged.
- Flat-market 4-bar single-window window: every OHLC=100, zero costs,
initial_capital=100k. Isolates the entry_q signal from P&L noise
so the assert-predicted_q is exact under IEEE-754 (kernel writes
the Q-value verbatim with no math).
- NULL fallback for isv_signals/conviction/exploration_scale matches
the existing single-step launcher pattern; FEATURE_DIM=4 (<41) ⇒
compute_regime_trail_scales takes the fixed-width fallback (no
feature deref needed).
Verification:
- SQLX_OFFLINE=true cargo test -p ml --test sp21_per_trade_predicted_q_test
--features cuda -- --ignored --nocapture
- 3/3 pass on RTX 3050 Ti (sm_86).
- All 4 prior SP20 GPU oracle suites still pass (25 tests).
- Total: 28 GPU oracle tests, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4d4cd996db |
feat(sp21): T3.3 ISV defrost — hold_reward_ema (atomic Phase 3.2 wireup)
Closes the Phase 3.2 forward-reference loop in the SP20 aggregator.
Previously `out_inputs->per_bar_hold_reward = 0.0f` was hardcoded; the
per-bar Hold opportunity-cost producer existed
(experience_kernels.cu:3823 — `per_bar_opp_cost = -aux_conf × cost_scale`)
and wrote to `hold_baseline_buffer` and `r_micro` directly, but never
reached HOLD_REWARD_EMA. Result: HOLD_REWARD_EMA frozen at sentinel 0.0
across all observed epochs; the SP20 reward centering loop
(`r_micro += per_bar_opp_cost - HOLD_REWARD_EMA`) stayed uncentered,
biasing the policy's reward signal away from zero-mean.
T3.3 wireup mirrors the T2.2 alpha refactor: a per-env scratch buffer
that the producer writes at every step (alongside the existing
`hold_baseline_buffer` write), and the aggregator reads at the same
step. Sums opp_cost over Hold-direction envs only; emits the mean as
`per_bar_hold_reward`. The HOLD_REWARD_EMA's gate (`hold_fraction > 0.5f`
from T3.2) preserves the strict-majority semantic from before.
Atomic across producer site, kernel signature, aggregator, launcher,
collector, and tests (per feedback_no_partial_refactor):
- experience_kernels.cu — new `float* per_bar_opp_cost_per_env`
kernel arg, NULL-tolerant write at line ~3823.
- sp20_aggregate_inputs_kernel.cu — new arg, 6th shmem stripe
(`sh_opp_cost_sum`), per-thread Hold-gated accumulation, tree
reduction extended to 6 stripes, output `per_bar_hold_reward =
opp_cost_sum / hold_count` when hold_count > 0 else 0.
- sp20_aggregate_inputs.rs — launcher signature, dynamic_shmem_bytes
5→6 stripes, doc table, internal test.
- gpu_experience_collector.rs — new `per_bar_opp_cost_per_env:
CudaSlice<f32>` field, alloc, struct construction, kernel-arg
pass at both experience_env_step and sp20_aggregate_inputs
launches (raw_ptr).
- sp20_aggregate_inputs_test.rs — helper renamed
`run_kernel_with_is_win_and_opp_cost`, 4 existing sites pass
NULL, 2 NEW oracle tests verifying real producer + NULL fallback.
- sp20_phase1_4_wireup_test.rs — NULL fallback at the wireup site;
HOLD_REWARD_EMA-stays-at-zero assertion remains valid.
Verification:
- cargo check -p ml --tests: passes (warnings only)
- cargo test -p ml --test sp20_aggregate_inputs_test --features cuda
-- --ignored: 12/12 GPU oracle tests pass on RTX 3050 Ti, including
both new T3.3 tests (per_bar_hold_reward_means_over_hold_envs_only,
null_per_bar_opp_cost_emits_zero).
- cargo test -p ml --test sp20_phase1_4_wireup_test --features cuda
-- --ignored: 2/2 pass under the new NULL-tolerant contract.
Plan reference: docs/plans/2026-05-10-sp21-train-eval-coherence-isv-defrost.md
Tier 3 status: T3.1 ✓, T3.2 ✓, T3.3 ✓ (this commit), T3.4 withdrawn,
T3.5 cascade-pending, T3.6 withdrawn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
1790a31b66 |
feat(sp21): T3.1+T3.2 ISV defrost — wr_ema + hold_pct_ema (atomic)
SP21 Tier-3 foundation: defrost two production-frozen ISV slots that
pinned at 0 across every observed training epoch (d7bj7, xmd6b). Both
bugs in the same aggregator kernel, same structural shape: per-step
binary majority-vote indicators feeding fractional EMAs.
T3.1 — WR_EMA defrost (sp20_aggregate_inputs_kernel.cu):
- was: is_win_out = (2 * wins_count >= closed_count) ? 1 : 0
binary "majority won this step" indicator
- now: win_fraction_out = (float)wins_count / (float)closed_count
fractional in [0, 1]
- With actual val WR≈0.46, the binary signal was 0 most of the time,
pinning WR_EMA at 0 across 30+ epochs in xmd6b. EMA now converges
to population win rate.
T3.2 — HOLD_PCT_EMA defrost (same kernel, same pattern):
- was: action_is_hold_out = (hold_count * 2 > n_envs) ? 1 : 0
strict-majority indicator
- now: hold_fraction_out = (float)hold_count / (float)n_envs
- Cascade: HOLD_COST_SCALE controller compared hold_pct_ema=0 to
tgt±0.05, always saw < lower, ramped × 0.95 → clamped at floor
0.01 (the hold_cost_scale=0.0100 observation in d7bj7 logs).
T3.5 expected to cascade-fix in next training run.
- HOLD_REWARD_EMA gate preserves strict-majority semantic via
hold_fraction > 0.5f test in the consumer kernel.
Atomic across struct fields, kernel logic, Rust mirror, byte
serialization, doc tables, and 4 test files (per
feedback_no_partial_refactor):
- sp20_aggregate_inputs_kernel.cu (struct + 2 computations)
- sp20_emas_compute_kernel.cu (struct + reader + gate)
- sp20_aggregate_inputs.rs (doc table)
- sp20_emas_compute.rs (Rust struct + serialize + 3 tests)
- sp20_aggregate_inputs_test.rs (reader sig + 4 test assertions)
- sp20_emas_compute_test.rs (5 struct literal updates)
- sp20_phase1_4_wireup_test.rs (HOLD_PCT_EMA expected 0.625)
Verification:
- cargo check -p ml --tests: passes (warnings only)
- cargo test -p ml --lib sp20_emas: 6/6 unit tests pass
Pearl candidate: binary-majority aggregator over a fractional
underlying signal cannot serve as input to a fractional-target EMA.
The previous fix (commit
|
||
|
|
dd9c70df98 |
feat(sp20): Phase 3 Task 3.4 — replay buffer schema for per-bar aux_conf
Lands the producer→ring→consumer schema plumbing for per-bar aux_conf
(the K=2 peak-softmax-above-uniform confidence value the Phase 5
Aux→Q gate consumes at the Bellman target). Atomic across all struct
boundaries per `feedback_no_partial_refactor`.
Data flow (TWO struct boundaries, not one — plan errata Gap 11):
ExperienceCollector → GpuExperienceBatch → insert_batch()
(.aux_conf field) (8th arg)
→ ring → sample() → GpuBatchPtrs → Trainer
(.aux_conf_ptr) (Phase 5 consumer)
Phase 5 lands the consumer (Aux→Q gate kernel at the Bellman target);
this commit is the producer-side schema plumbing only.
Spec §4.4 Phase 5 forward-reference contract:
At each replay batch step:
aux_conf = ptrs.aux_conf_ptr[k] # SAMPLED bar
threshold = ISV[AUX_CONF_THRESHOLD_INDEX] # [0.01, 0.20]
temp = ISV[AUX_GATE_TEMP_INDEX]
gate = sigmoid((aux_conf - threshold) / temp) # ∈ [0, 1]
Q_target = gate × Q_full + (1 - gate) × mean_a Q(s, a)
Producer (experience_env_step):
- 1 new kernel arg `aux_conf_per_sample: float* [N×L×cf_mult]`.
- Per-bar write at kernel entry: `aux_conf_per_sample[out_off] =
sp20_compute_per_bar_aux_conf_k2(aux_logits + i*K)`.
- CF slot write: `aux_conf_per_sample[cf_off] =
aux_conf_per_sample[out_off]` (CF state shares the same env state
so shares the same aux signal).
- NULL-tolerant: defaults to 0.0f (K=2 uniform-prior sentinel).
GpuExperienceCollector:
- +`aux_conf_per_sample: CudaSlice<f32>` field.
- alloc with `total_output * cf_mult` for both on-policy + CF.
- Threads as kernel arg of experience_env_step.
- Clones into `GpuExperienceBatch.aux_conf` at end of
collect_experiences_gpu (size `total = base_total × 2`).
GpuExperienceBatch: +`aux_conf: CudaSlice<f32>` field.
GpuReplayBuffer (crates/ml-dqn):
- +`GpuBatchPtrs.aux_conf_ptr: u64`.
- +ring storage `aux_conf: CudaSlice<f32>` [capacity].
- +sample buffer `sample_aux_conf: CudaSlice<f32>` [max_batch_size].
- +`trainer_aux_conf_ptr: u64` for direct path.
- +`set_trainer_aux_conf_ptr` setter + `trainer_aux_conf_ptr`
accessor (mirrors `set_isv_signals_ptr` pattern).
- `insert_batch` adds 8th arg `aux_conf_in: &CudaSlice<f32>` —
scatters via `scatter_insert_f32` (reuses the rewards/dones
scatter kernel).
- `sample_proportional` adds gather: direct-to-trainer if
`trainer_aux_conf_ptr != 0`, else fallback into
`sample_aux_conf`. Both paths populate `GpuBatchPtrs.aux_conf_ptr`.
Atomic caller updates (insert_batch arg from 7 to 8):
- training_loop.rs:2522 (production)
- gpu_residency.rs:77 (smoke)
- performance.rs:144 (smoke)
- training_stability.rs:154+201 (smoke ×2)
- gpu_per_integration_test.rs:128 (integration)
- sp15_phase1_oracle_tests.rs:2170+2189+2356 (oracle ×3)
- 3 inline tests in gpu_replay_buffer.rs
Tests:
- test_aux_conf_schema_round_trip (GPU): inserts 8 transitions with
distinct aux_conf [0.0, 0.05, ..., 0.35]; samples 1; asserts the
gathered slot value matches one of the inserted values
(round-trip integrity invariant).
- test_aux_conf_trainer_ptr_wiring_contract (CPU): asserts
set_trainer_aux_conf_ptr / trainer_aux_conf_ptr setter+accessor
behavior matches the SP15 Phase 3.5.5.b mirror pattern.
Verification:
SQLX_OFFLINE=true cargo check -p ml --tests --features cuda # green
SQLX_OFFLINE=true cargo check -p ml-dqn --tests --features cuda # green
SQLX_OFFLINE=true cargo test -p ml --lib sp20 # 21/21 pass
SQLX_OFFLINE=true cargo test -p ml-dqn test_aux_conf_trainer_ptr_wiring_contract # CPU pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
06dbb78ffc |
feat(sp20): Phase 3 Task 3.2 — Hold opportunity-cost dual emission (Component 2)
Lands the per-bar Hold opportunity-cost dual emission at
experience_env_step's per-bar branches (positioned-non-event ~3650 +
flat-non-event ~3737), atomically with the trade-close consumer
wiring at the segment_complete branch (~3289 — replaces the Phase 2
Task 2.2 forward-reference placeholder `hold_baseline = 0.0f`) per
`feedback_no_partial_refactor`.
Spec §4.2 dual-emission contract:
per_bar_opp_cost = -aux_conf × cost_scale
Path 1 (Hold-only): r_micro/r_opp_cost += per_bar_opp_cost
- ISV[HOLD_REWARD_EMA]
(centered for Q-target stability)
Path 2 (always): hold_baseline_buffer[env, t % 30] = per_bar_opp_cost
(uncentered, consumed at trade close)
At trade close:
hold_baseline = sp20_sum_hold_baseline_over_trade(buf, env, 30,
current_t,
segment_hold_time)
alpha = R_event - hold_baseline # replaces Phase 2 placeholder
The summation walks backwards `min(30, segment_hold_time)` slots from
`(current_t - 1) % 30` in env i's row of the per-env circular buffer.
The trade-close bar's segment_complete branch does NOT write to the
buffer, so the most recent per-bar write is at current_t - 1.
Layout decision (plan errata Gap 9): per-env stride
`[N_envs × HOLD_BASELINE_BUFFER_SIZE = 30]` row-major. The kernel is
per-env-parallel; a single global ring would interleave bars across
envs and break trade-attribution semantic.
Trade-open tracking decision (plan errata Gap 10): NO new state slot
needed. The existing `segment_hold_time` (= saved_hold_time captured
before PS_HOLD_TIME reset at experience_kernels.cu:2618) plus
current_t suffice — walk backwards in the buffer.
Replaces SP18 D-leg `compute_sp18_hold_opportunity_cost` calls at
experience_kernels.cu:3672 and :3783. The device function in
trade_physics.cuh:655 is RETAINED — still called by
sp18_hold_opp_test_kernel.cu (oracle test surface). Per
`feedback_no_stubs`: only production callers migrate; the helper
stays for the test surface.
New device helpers (sp20_hold_baseline.cuh):
- sp20_compute_per_bar_aux_conf_k2(logits) — bit-identical to the
formula in sp20_stats_compute_kernel.cu Pass A.
- sp20_sum_hold_baseline_over_trade(buf, env, size, current_t,
segment_hold_time) — walks backwards with modulo wrap-around.
New buffer + reset infrastructure:
- GpuExperienceCollector.hold_baseline_buffer field
- HOLD_BASELINE_BUFFER_SIZE = 30 constant (re-exported)
- StateResetRegistry FoldReset entry + invariant test
- reset_named_state dispatch arm
Six new kernel args appended to experience_env_step signature:
aux_logits_per_env, hold_baseline_buffer, hold_buffer_size, aux_k,
hold_cost_scale_idx, hold_reward_ema_idx. All NULL-tolerant.
HOLD_REWARD_EMA forward-reference: ISV[516] is updated by
sp20_emas_compute_kernel reading per_bar_hold_reward from
sp20_aggregate_inputs_kernel which currently emits 0.0f as a Phase
3.2 forward-ref placeholder (line 292). The parallel `sp20-phase-2-fix`
agent owns wiring the real producer; Task 3.2's centering math
references the ISV slot (not a hardcoded 0), so when the parallel
branch lands, EMA starts updating and centering becomes load-bearing
automatically. No additional Task 3.2 work needed post-merge.
Tests (sp20_hold_baseline_test.rs, 4 GPU oracle tests):
1. aux_conf_k2_bounds_and_fixed_points — bounds, uniform/saturated/
spec-warmup/spec-confident/symmetry fixed points.
2. sum_hold_baseline_over_trade_indexing — basic indexing,
hold_time>size clamp, defensive guards, modulo wrap-around.
3. sum_hold_baseline_per_env_stride — env-stride correctness across
row-major buffer.
4. dual_emission_hold_vs_non_hold — full Path 1 + Path 2 contract,
mirrors plan §3.2 reference fixture.
Verification:
SQLX_OFFLINE=true cargo check -p ml --tests --features cuda # green
SQLX_OFFLINE=true cargo test -p ml --lib sp20 # 21/21 pass
SQLX_OFFLINE=true cargo build -p ml # cubin compile
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
13ce783853 |
test(sp20): post-fix runtime diagnostic — read-back accessors + producer test scaffold
Adds infrastructure for verifying the SP20 Phase 2 Task 2.2-fix
(`is_win_per_env`) producer side at runtime. The consumer-side oracle
test passes; the producer-side wire-up has not been independently
verified end-to-end. Production HEALTH_DIAG observed `wr_ema=0.0000`
across all training epochs even after `64bbbe418` landed, with
`alpha_ema` evolving normally — strongly suggesting either (a) all
profitable trade closes had `hold_time == 0` so the producer write
didn't fire, or (b) the producer write fired but `(segment_return >
0.0f)` evaluated to false in the production data.
Code-inspection audit (this commit, summarized in audit-doc):
- Verified 81 kernel args (launcher) match 81 kernel signature args
for `experience_env_step`. `is_win_per_env` at position 81;
`alpha_per_env` at position 78. cudarc `PushKernelArg<&mut
CudaSlice<T>>` impl pushes `&arg.cu_device_ptr` (stable address
for the lifetime of the chained statement).
- Verified buffer allocation at construction (alloc_zeros), pub(crate)
field, FoldReset entry + dispatch arm, kernel-arg pass at both
experience_env_step and sp20_aggregate_inputs launch sites.
- Verified producer site — both alpha + is_win writes inside the SAME
`if (segment_complete && segment_hold_time > 0.0f)` block at
experience_kernels.cu:3196, both NULL-guarded, race-free.
- Verified consumer site — aggregate kernel reads `is_win_per_env[env]`
+ `alpha_per_env[env]` side-by-side inside `if (tc != 0)`.
- Verified stream ordering — both kernels run on `self.stream` in the
same `for t in 0..timesteps` loop; experience kernel at line 6069,
aggregate at 6577. Stream-implicit producer→consumer ordering.
**No code-level bug surfaced by inspection.** The `is_win_per_env`
write is structurally identical to the `alpha_per_env` write 14
lines earlier in the same gating block.
Files modified:
- `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs` —
`read_is_win_per_env_for_test()` and `read_alpha_per_env_for_test()`
pub fns. Mirror `read_epoch_dsr_state()` pattern: dtoh memcpy via
the collector's stream, returns Vec<i32> / Vec<f32>. Cost-free in
production (only called from tests) and benign for invariants.
- `crates/ml/tests/sp20_is_win_producer_test.rs` — production-path
producer test scaffold. Builds 6-float-per-bar synthetic OHLCV
matching `experience_env_step`'s tgt[0..6] contract, runs
`collect_experiences_gpu`, reads back both buffers, asserts the
three structural invariants:
1. alpha_per_env has at least one non-zero slot (precondition —
the synthetic upward-trend data must produce ≥ 1 trade close).
2. is_win_per_env has at least one slot == 1 (regression-pin for
the Task 2.2-fix producer wire-up).
3. Co-occurrence: every env where alpha > 0 must have is_win == 1
(R_event > 0 ⇒ segment_return > 0 ⇒ is_win = 1; structural
guarantee from the kernel's two writes at the same gate).
Currently `#[ignore]` because the collector's cuBLAS forward chain
requires a real `trainer_params_ptr` (NUM_WEIGHT_TENSORS=163
weights). Activation requires either trainer-params plumbing in the
test scaffold OR moving the test inline as `#[cfg(test)]` in
gpu_experience_collector.rs.
- `docs/dqn-wire-up-audit.md` — audit-doc entry per Invariant 7.
Documents the inspection scope, the unverified producer-side
pre-conditions, and the open-issue framing for the wr_ema=0.0000
discrepancy.
Verification:
- SQLX_OFFLINE=true cargo check -p ml --tests passes
- 9 state_reset_registry tests pass (incl.
sp20_is_win_per_env_registered_fold_reset)
- The new test file compiles cleanly under `--features cuda`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
5a29c37cd8 |
test(sp20): failing test pinning WR_EMA bug — wins predicate must use segment P&L not per-bar step_ret
Plumbs new is_win_per_env i32 device-buffer arg through the
sp20_aggregate_inputs kernel signature and Rust launcher, NULL-tolerant
to preserve the legacy `step_ret_per_env > 0` predicate for existing
oracle-test scaffolds. Production caller passes 0 (NULL) in this
commit; commit 2 wires the per-env producer + collector buffer + reset
entry atomically with the kernel-body change that honors the arg.
Adds two new GPU oracle tests in sp20_aggregate_inputs_test:
- wr_ema_uses_segment_pnl_via_is_win_per_env_predicate (FAILS without
fix): step_ret all negative (tx-cost dominated close bars — the
realistic production case), is_win_per_env = [1,1,1,0]. With the
legacy predicate wins_count = 0 ⇒ is_win = 0 (the production bug).
With the fix wins_count = 3 ⇒ is_win = 1.
- null_is_win_per_env_falls_back_to_legacy_step_ret_predicate: pins
the NULL-tolerance contract so existing oracle tests + Phase 1.4
wireup test scaffolds keep working unchanged.
Bug root cause: WR_EMA pinned at 0 across all training epochs in
production (HEALTH_DIAG observed `wr_ema=0.0000` while `alpha_ema`
evolved with real values). The aggregation kernel's win predicate was
`step_ret_per_env[env] > 0.0f` at the close bar, which is the per-bar
mark-to-market `(new_value - prev_equity) / prev_equity`, NOT the
trade's segment-level outcome. At close bars `step_ret = position *
(close_t - close_{t-1}) - tx_cost`; the per-bar tick is small while
tx_cost is fixed → `step_ret < 0` is dominant across closing bars
even for trades that closed profitably overall. Result: wins_count =
0 always, is_win = 0 always, WR_EMA pinned at 0, LOSS_CAP pinned at
-1.0 (cold-start cap, never ramping to -2.0 per spec §4.1).
Fix arrives in the next commit: experience_kernels.cu segment_complete
branch writes `is_win_per_env[i] = (segment_return > 0.0f) ? 1 : 0`
(the segment-level realized-return signed-flag), passed through to
the aggregation kernel which uses the new per-env i32 buffer in place
of the per-bar `sr > 0` predicate when wired.
Per `feedback_no_partial_refactor` the kernel signature change + all
callers (production + 2 test files) migrate atomically in this commit;
the kernel-body behavioral change + producer wire-up + reset registry
+ audit-doc entry land atomically in the immediately following commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3f9b34030c |
feat(sp20): Phase 2 Task 2.2 — SP12 v3 reward block → 4-quadrant + alpha plumbing (atomic)
Lands the SP20 4-quadrant reward at the experience_env_step::
segment_complete site, atomically with per-env alpha plumbing through
the SP20 aggregation kernel per `feedback_no_partial_refactor`:
* `experience_env_step.cu` — replace SP12 v3 inlined block (vol-norm
calc + base_reward + SP18 D-leg trade-close call + asymmetric cap +
min-hold penalty + r_popart/r_trail cascade, ~260 LoC) with the SP20
4-quadrant reward (~80 LoC) computing:
R_event = sp20_compute_event_reward(segment_return,
label_at_open_per_env[i],
ISV[LOSS_CAP_INDEX])
alpha = R_event - 0.0 (Phase 3.2 hold_baseline placeholder)
r_used = alpha - ISV[ALPHA_EMA_INDEX] (advantage centering)
alpha_per_env[i] = alpha (consumed by sp20_aggregate)
r_popart / r_trail = r_used (SP11 component split preserved)
Adds 3 new kernel args at the end (preserves Task 2.0 args):
float* alpha_per_env, int loss_cap_idx, int alpha_ema_idx
Deletes 3 SP12 v3 args (min_hold_target, min_hold_penalty_max,
min_hold_temperature) per `feedback_no_hiding` "wire up or delete".
* `sp20_aggregate_inputs_kernel.cu` — add `alpha_per_env` input arg
(NULL-tolerant), 5th shmem stripe (sh_alpha_sum, was 4 stripes),
per-thread close-gated accumulation, 5-way tree-reduce loop body,
output write `alpha = mean / closed_count`. Replaces the Phase 1.4
hardcoded `out_inputs->alpha = 0.0f` placeholder atomically.
* `sp20_aggregate_inputs.rs` launcher — +1 arg `alpha_per_env_dev`
(passes 0/NULL for tests). Shmem byte-count: 4 stripes → 5 stripes.
* `gpu_experience_collector.rs` — new `alpha_per_env: CudaSlice<f32>`
field [alloc_episodes], allocated next to `label_at_open_per_env`,
threaded into env_step launch + sp20_aggregate launch. Removes the
3 obsolete config.min_hold_* `.arg(...)` calls from env_step launch.
* `state_reset_registry.rs` — `alpha_per_env` FoldReset entry +
invariant test `sp20_alpha_per_env_registered_fold_reset`.
* `training_loop.rs::reset_named_state` — `alpha_per_env` dispatch arm
via `stream.memset_zeros` (mirrors `label_at_open_per_env` pattern).
* Pin-test updates per spec (atomic with the contract change):
- `sp20_aggregate_inputs_test::alpha_and_per_bar_hold_reward_are_
phase_2_and_3_2_placeholders` →
`null_alpha_producer_keeps_phase_1_4_placeholder_contract` (asserts
NULL alpha producer ⇒ alpha=0.0 backward-compat).
- `sp20_phase1_4_wireup_test::alpha_and_hold_reward_emas_stay_at_
zero_phase_2_3_2_placeholders` →
`..._with_null_producers` (same NULL-tolerance pin).
- +2 NEW tests in `sp20_aggregate_inputs_test`:
* `alpha_mean_over_closed_envs_phase_2_contract` — 4 envs (3
close, 1 doesn't), asserts close-gated mean (env 3's alpha=99
ignored).
* `alpha_zero_when_no_close_phase_2_contract` — no-close steps
emit alpha=0.0 regardless of stale alpha_per_env content.
* `dqn-wire-up-audit.md` — Task 2.2 entry documents the deleted SP12 v3
block, the per-env alpha plumbing, the retained device functions
per `feedback_no_stubs`, and the deferred Task 2.4 cleanup of the
orphaned min_hold_temperature_update_kernel producer chain.
Per `pearl_event_driven_reward_density_alignment`: per-bar SP18 D-leg
sites at experience_kernels.cu:3722 / :3833 are RETAINED pending
Phase 3.2 Component 2 replacement. Per `feedback_no_stubs`:
compute_sp18_hold_opportunity_cost / compute_asymmetric_capped_pnl /
compute_min_hold_penalty device functions are RETAINED (still called
by per-bar SP18 sites + sp12_reward_math_test_kernel test wrapper).
Per spec §4.1: alpha_ema centering (`R_used = alpha - alpha_ema`) is
load-bearing for cold-start learning at WR=46% with raw EV ≈ -0.06,
NOT just Q-target stability. Documented in the kernel comment block.
Verification:
SQLX_OFFLINE=true cargo build -p ml # cubin compile
SQLX_OFFLINE=true cargo check -p ml --tests --features cuda # tests compile
SQLX_OFFLINE=true cargo test -p ml --lib sp20 # 20/20 lib tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
eaaab152fc |
feat(sp20): Phase 2 Task 2.1 — sp20_compute_event_reward + 8 GPU oracle tests
Lands the SP20 Component 1 reward math as a header-only `__device__
__forceinline__` function in a new `sp20_reward.cuh` header, plus the
GPU oracle test scaffold for the 4-quadrant truth table per spec §4.1.
* `sp20_reward.cuh` — `sp20_compute_event_reward(close_pnl,
label_at_open_sign, loss_cap)` function. Returns bounded scalar
`R_event ∈ [loss_cap, +1.0]`. 4-quadrant table:
+1, +1 → +1.0 (right reason, right outcome)
+1, -1 → +0.5 (wrong reason, right outcome)
-1, -1 → -0.5 (right reason, wrong outcome)
-1, +1 → loss_cap (wrong reason, wrong outcome — ISV-driven)
0, * → 0.0 (no information — close_pnl == 0)
Sentinel `label_at_open_sign == 0` (Task 2.0 contract) maps to
dir_match=false ⇒ wrong-reason quadrant. Documented as the safer
default in the header comment.
* `sp20_event_reward_test_kernel.cu` — standalone single-thread test
wrapper, mirrors the `sp12_reward_math_test_kernel.cu` /
`thompson_test_kernel.cu` pattern. Outputs to a mapped-pinned [1]
f32 with `__threadfence_system()` for PCIe-visible coherence.
* `tests/sp20_event_reward_test.rs` — 8 GPU oracle tests
(`#[ignore = "requires GPU"]`-gated):
1. quadrant_right_reason_right_outcome
2. quadrant_wrong_reason_right_outcome
3. quadrant_right_reason_wrong_outcome
4. quadrant_wrong_reason_wrong_outcome_loss_cap_at_minus_1
5. quadrant_wrong_reason_wrong_outcome_loss_cap_at_minus_2
6. zero_pnl_returns_zero
7. sentinel_label_winning_trade_lands_shoulder (Task 2.0 contract)
8. sentinel_label_losing_trade_lands_loss_cap (Task 2.0 contract)
* `experience_kernels.cu` — `#include "sp20_reward.cuh"` so Task 2.2's
trade-close site replacement has the function in scope.
* `build.rs` — `sp20_event_reward_test_kernel.cu` cubin entry.
No production callers in this commit. Task 2.2 atomically:
- replaces the SP12 v3 reward block at experience_kernels.cu:3216-3380
with the SP20 4-quadrant reward;
- adds the `is_close` consumer of `label_at_open_per_env[i]` (Task
2.0's per-env scratch);
- threads per-env alpha through the SP20 aggregation kernel,
making `alpha_ema` non-zero post-trade-close (replacing the
Phase 1.4 0.0 forward-reference placeholder).
Per `feedback_no_partial_refactor.md` the device function ships
BEFORE the production caller so Task 2.1's GPU oracle tests can
exercise the math in isolation; the partial refactor that breaks
the no-partial-refactor rule is "feature behind the function with
no caller", which this commit's tests resolve in scope.
Per `feedback_no_cpu_test_fallbacks.md` GPU oracle only — no CPU
reference impl. Per `feedback_no_htod_htoh_only_mapped_pinned.md`
all CPU↔GPU buffers are mapped-pinned (`MappedF32Buffer`).
Verification:
SQLX_OFFLINE=true cargo check -p ml --tests --features cuda # clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
235e838422 |
feat(sp20): Phase 1.4 (Path C) — kernel-arg refactor + aggregation kernel + wire-up
Atomic Phase 1.4 commit per `feedback_no_partial_refactor`. Wires the
SP20 fused-producer chain (Stats → Aggregate → EMAs → Controllers) into
GpuExperienceCollector's per-rollout-step path with one new aggregation
kernel + Phase 1.2 EMA kernel-signature refactor + production wire-up
+ tests + audit/spec/plan amendments, all in one commit.
## Path C decision rationale
The Phase 1.2 EMA kernel originally took 9 scalar value-args. The Phase
1.4 wire-up site (per-rollout-step in GpuExperienceCollector) needs to
feed per-env GPU-resident trade signals (trade_close_per_sample,
step_ret_per_sample, hold_at_exit_per_sample, packed factored
actions_out) into the kernel — forbidden via host sync
(`feedback_cpu_is_read_only`) and via memcpy_dtoh/htod
(`feedback_no_htod_htoh_only_mapped_pinned`). Path C refactors the EMA
kernel to take a device struct pointer (`SP20EmaInputs* ema_inputs`) +
adds an aggregation kernel that writes the struct on the GPU. Phase 1.2
had zero production consumers yet, so the sig change + Phase 1.4 wire-up
land atomically.
## What this commit contains
1. **EMA kernel signature refactor** (Phase 1.2 → Path C):
`sp20_emas_compute_kernel.cu` swaps 9 scalar args for a single
`const SP20EmaInputs* __restrict__ ema_inputs` device pointer.
Math semantics bit-identical. Rust `EmaInputs` mirrored as
`#[repr(C)]` byte-for-byte; new `pack_inputs_into_f32_view` helper
for tests + collectors that fill the struct via a mapped-pinned
f32-aliased buffer.
2. **New aggregation kernel** (`sp20_aggregate_inputs_kernel.cu`):
Per-env arrays (sliced to current rollout step) + sp20_stats outputs
(p50/std) + aux_dir_acc_reduce_kernel output [0] → SP20EmaInputs
struct. Block tree-reduce (4 stripes × bdim × 4 bytes shmem); no
atomicAdd. Aggregation rules per spec §4.5:
- is_close = OR over envs
- is_win = (≥ 0.5 of closed envs were wins)
- trade_duration = round(mean(hold_at_exit) over closed envs)
- action_is_hold = strict majority (count*2 > n_envs) HOLD
- alpha = 0.0 (Phase 2 forward ref — reward kernel)
- per_bar_hold_reward = 0.0 (Phase 3.2 forward ref — Hold-cost dual)
- aux_logits_p50/std/dir_acc forwarded from upstream
3. **Production wire-up in GpuExperienceCollector**:
4 kernel handles + 4 mapped-pinned buffers (struct fields, alloc,
init); per-rollout-step launch sequence after env_step (step 5c):
`Stats → Aggregate → EMAs → Controllers`. All on the same stream;
stream-implicit producer→consumer ordering. Gated on
`isv_signals_dev_ptr != 0 && trainer_params_ptr != 0` (matches the
existing SP14-β EGF chain pattern).
4. **Fold-boundary reset**:
3 new RegistryEntry records (sp20_ema_inputs_buf,
sp20_emas_internal_buf, sp20_emas_obs_count_buf) + 13 new dispatch
arms in training_loop.rs (10 SP20 ISV slot resets + 3 buffer resets).
Closes the pre-existing `every_fold_and_soft_reset_entry_has_dispatch_arm`
regression (was failing on fresh check after the SP20 ISV slot
registrations landed in commit
|
||
|
|
e96f7fcdd1 |
fix(sp20): replay buffer records magnitude INTENT (Bellman consistency)
Parallel to the 2026-05-08 Class C direction fix but on the **magnitude axis**.
Class C correctly chose REALIZED for direction because env-gated trades (capital
floor / trail / broker cap → Flat) mean no trade happens — recording as Flat is
honest. For magnitude, Kelly cap clamping does NOT gate the trade — the trade
still happens, just at a smaller size. `actual_mag_core` is the bucketed
magnitude derived in trade_physics.cuh:1099-1117 from |position|/max_position;
when the policy intends Full and Kelly clamps to 0.35× max (bucket → Quarter),
recording Quarter loses the policy's intent. Q(s, mag=Full) never receives the
reward gradient from Full-intent trades that actually executed.
One-line swap inside the Class C encoding block at experience_kernels.cu:2689-
2697: `actual_mag_core * b2_size * b3_size` → `mag_idx * b2_size * b3_size`.
`mag_idx` is the local intent magnitude already decoded at line ~2460 from the
original action_idx BEFORE any Kelly/CVaR/Q-gap clamping. Direction axis keeps
Class C semantics (`actual_dir_core` → gated trades record as Flat). Order/
urgency remain intent passthrough.
`actual_mag_core` remains consumed downstream by Task 2.X per-magnitude trade-
lifecycle instrumentation at the seg_mag_bin site below — direction-branch Q
learns from realized; magnitude-branch Q learns from intent. The two axes have
different env-enforcement semantics so they take different actions at the
replay-write site.
Verification: new CPU oracle test crates/ml/tests/sp20_magnitude_intent_record_
test.rs pins the encoding contract with 4 invariants (Full→Quarter scenario,
all-pairs sweep, order/urgency passthrough, direction Class C unchanged). Per
pearl_tests_must_prove_not_lock_observations the test asserts invariants
("recorded mag == intent mag, regardless of realized") not observed values, so
it cannot become a bug-lock if the kernel's compute path changes — only if the
encoding contract itself is broken. All 4 tests pass; cargo check clean.
Audit-doc entry added to docs/dqn-wire-up-audit.md as the SP20 magnitude intent
fix, parallel to the Class C direction fix above with the asymmetry between
the two axes spelled out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4e21c38b85 |
fixup(sp20): Phase 1.1 K=3 → K=2 retarget (production aux head is K=2)
The Phase 1.1 sp20_stats_compute kernel (
|
||
|
|
5ded1cb4b9 |
feat(sp20): Phase 1.3 sp20_controllers_compute kernel
Component 5 / Kernel 2 of the SP20 fused-producer chain — the
deterministic derivation step. Reads the EMAs Phase 1.2's
sp20_emas_compute wrote (4 ISV + 4 internal scratch slots) and
derives 6 controller outputs into ISV slots:
- LOSS_CAP (510) = -1 - clamp((wr-0.50)/0.05, 0, 1)
- HOLD_COST_SCALE (513) two-sided ramp around TARGET_HOLD_PCT ±0.05
- TARGET_HOLD_PCT (514) = clamp(0.8 - aux_p50_ema*1.5, 0.1, 0.8)
- N_STEP (517) = clamp(round(trade_dur_ema), 1, 30) (f32)
- AUX_CONF_THRESHOLD (518) = clamp(aux_dir_acc_ema-0.50, 0.01, 0.20)
- AUX_GATE_TEMP (519) = max(aux_conf_std_ema, 0.01)
TARGET_HOLD_PCT computed before HOLD_COST_SCALE because the
HOLD_COST_SCALE controller reads tgt; implemented as a local f32
written to ISV then re-used for the controller (no redundant ISV
read-back). HOLD_COST_SCALE is the only output that READS its own
previous ISV value (in-place update); deadband path writes the
unchanged previous value verbatim per feedback_no_stubs.
Single-block, single-thread kernel — captureable in the per-step
CUDA Graph per pearl_no_host_branches_in_captured_graph. ISV +
internal pointers are mapped-pinned device pointers (existing
buffers from Phase 1.2); kernel emits __threadfence_system() for
PCIe-visible coherence.
Pearls + invariants honoured:
- pearl_controller_anchors_isv_driven: every output's anchor /
target / cap derives from EMAs; only spec-frozen ramp / clamp
parameters are compile-time constants.
- feedback_isv_for_adaptive_bounds: these 6 outputs ARE the
adaptive bounds.
- feedback_no_atomicadd, feedback_no_cpu_compute_strict,
feedback_no_htod_htoh_only_mapped_pinned, feedback_no_stubs.
- feedback_no_partial_refactor: kernel + launcher + tests + build
entry land atomically; production wire-up lands in Phase 1.4.
- pearl_tests_must_prove_not_lock_observations: 7 GPU oracle
tests assert clamp boundaries, formula correctness, and
bidirectional ramp behavior — NOT specific observed values.
Tests (all #[ignore = "requires GPU"], pass on RTX 3050 Ti sm_86):
1. loss_cap_ramp_boundaries — 4 wr_ema sweeps incl. clamps.
2. n_step_bounds — round + clamp [1, 30].
3. aux_conf_threshold_bounds — clamp [0.01, 0.20].
4. aux_gate_temp_floor — max(std, 0.01).
5. target_hold_pct_inverse_relation — 0.8 − p50*1.5 with clamps.
6. hold_cost_scale_two_sided_ramp — up/down/deadband + clamps.
7. all_six_outputs_written_in_one_launch — guards missed writes.
Plus 3 launcher unit tests (slot range, slot uniqueness, ISV input
slot range).
Verification:
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check -p ml --features cuda
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml \
--test sp20_controllers_compute_test --features cuda \
-- --ignored --nocapture
→ 7/7 GPU oracle tests pass; 3/3 launcher unit tests pass.
Phase 1.4 will land the production caller atomically alongside
Kernel 1 + Kernel 3 launches on the same stream in order
Stats → EMAs → Controllers per the SP20 design.
|
||
|
|
71aade18cf |
feat(sp20): Phase 1.2 sp20_emas_compute kernel
Component 5 / Kernel 1 of the SP20 design — central state-tracker
that updates 8 Wiener-α EMAs per training step:
- 4 ISV slots: ALPHA_EMA (511), WR_EMA (512),
HOLD_PCT_EMA (515), HOLD_REWARD_EMA (516)
- 4 internal: trade_duration_ema, aux_conf_p50_ema,
aux_conf_std_ema, aux_dir_acc_ema (private
scratch consumed by Phase 1.3 controllers)
Per-EMA i32 observation counters (mapped-pinned obs_count[8])
handle the pearl_first_observation_bootstrap sentinel transition
correctly even when 0.0 is a legitimate observation (e.g.,
first-loss WR=0). count==0 ⇒ replace, count>0 ⇒ Wiener-blend at
α = 0.4 (WIENER_ALPHA_FLOOR per
pearl_wiener_alpha_floor_for_nonstationary).
Phase 1.2 lands kernel + launcher + 5 tests (4 GPU oracle, 1
floor lock) + build entry + audit doc atomically per
feedback_no_partial_refactor. Production wire-up (Phase 1.4)
deferred — kernel is dead code until then.
Verified on RTX 3050 Ti (sm_86):
- 4 GPU oracle tests pass: first_observation_replaces_sentinel,
wiener_alpha_converges_to_long_run_mean,
hold_reward_ema_gated_on_hold_bars,
per_step_emas_fire_unconditionally
- 4 launcher unit tests pass (constants + struct sanity)
- 1 floor-lock test pass (WIENER_ALPHA_FLOOR == 0.4)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
de922c6a4a |
feat(sp20): Phase 1.1 sp20_stats_compute kernel
Component 5 / Kernel 3 of the SP20 fused-producer chain. Single-block BLOCK=256 kernel reads `aux_logits [B, 3]` (the SP14-C aux head's 3-class direction logits) and emits `[aux_conf_p50, aux_conf_std]` into a `MappedF32Buffer<2>`, where the per-row signal is `aux_conf[i] = max_c softmax(logits[i, *])[c] - 1/3`. p50 uses the inlined `sp4_histogram_p99` pattern (per-warp tile binning + cumulative-from-bottom, no atomicAdd per `feedback_no_atomicadd`); std uses two block tree-reductions sharing one shmem tile sequentially. One fused kernel streams `aux_logits` once for both stats per `pearl_fused_per_group_statistics_oracle`. Phase 1.4 wires the production launch site atomically with the rest of the SP20 reward chain per `feedback_no_partial_refactor`. This commit lands kernel + Rust launcher + GPU oracle tests + build entry + audit-doc entry together so the kernel is independently verifiable on RTX 3050 Ti (sm_86) and L40S (sm_89) before the EMA + controller producers (Phase 1.2 + 1.3) reference its outputs. Tests verify: - uniform logits → aux_conf = 0 → [p50, std] = [0, 0] - varied confidence (logit ramp 0 → 3) → matches CPU oracle - heterogeneous half-hot half-uniform → matches CPU oracle - empty batch → degenerate-guard writes [0, 0] All 4 GPU oracle tests + 4 launcher unit tests pass on RTX 3050 Ti. Test data uses per-row variance to avoid the `pearl_sp4_histogram_warp_tile_undercount` lockstep-uniform trap (concentrated values within one bin_width race the per-warp non-atomic increments). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ef5e745a13 |
feat(sp20): scaffold 7 behavioral test stubs (Phase 0)
All 7 tests marked #[ignore] until corresponding Phase 6 task implements. Tests gate L40S deployment per spec §4.6. - sp20_pure_trend (Phase 6.1) - sp20_pure_noise (Phase 6.2) - sp20_confidence_correlation (Phase 6.3) - sp20_asymmetry_no_game (Phase 6.4) - sp20_n_step_distribution (Phase 6.5) - sp20_per_regime_wr (Phase 6.6) - sp20_regime_transition (Phase 6.7) |
||
|
|
d39005c6f4 |
feat(sp19 commit b): producer-side multi-horizon reward blend at fxcache write time
Path (B) Commit B — load-bearing semantic change for the SP19 multi-
horizon reward augmentation. At fxcache-write time, blend 1-bar / 5-bar
/ 30-bar log-returns into `tgt[1]` (`preproc_next`) using equal-thirds
weights with `1/sqrt(N)` vol-scale correction:
blended = (1/3) * r_1
+ (1/3) * r_5 / sqrt(5)
+ (1/3) * r_30 / sqrt(30)
The vol-scale correction applies INSIDE the blend (before weighting) so
each horizon's log-return is at unit-volatility-equivalent under
random-walk assumption — drops naturally into the existing `tgt[1]`
preprocessing pipeline (consumed in `experience_kernels.cu:1556` and
`cuda_pipeline/mod.rs:508`) without double-scaling.
Producer trim: valid range shrinks by `LOOKAHEAD_HORIZON_MAX = 30`
bars since `r_30` needs `close[i + WARMUP + 30]`. Walk-forward fold
construction is dataset-relative, so trimming at producer time shifts
every fold's tail by 30 bars — one-time cost per fxcache regen.
Both producer call sites (`precompute_features.rs` for the
feature-precompute binary, `data_loading.rs` for the DBN-fallback
in-process path) change atomically per `feedback_no_partial_refactor`.
Kernel consumers are unchanged — only `tgt[1]`'s value composition
differs from the consumer's perspective.
ISV slots [507..510) (allocated in Commit A) are NOT consumed at
producer time — `precompute_features` runs BEFORE training so the
ISV bus isn't initialised when the blend happens. Producer hardcodes
1/3 each via `SP19_HORIZON_BLEND_WEIGHTS`. Future Path (A) refactor
would bump TARGET_DIM to carry per-horizon log-returns and read
ISV at training-step time; for the empirical hypothesis test
("does multi-horizon blend lift WR?") equal-thirds is sufficient.
fxcache schema invalidation: `FXCACHE_VERSION` 8 → 9. Existing
`.fxcache` files (1-bar-only `preproc_next`) fail validation at load
and trigger Argo's ensure-fxcache regen step. First L40S run after
this commit takes ~10-15 min longer for the regen — one-time cost,
expected.
Touches:
- crates/ml/examples/precompute_features.rs: SP19_HORIZON_BLEND_WEIGHTS
constant + LOOKAHEAD_HORIZON_MAX trim + blend in tgt[] writer +
early-bail-out check on minimum dataset size.
- crates/ml/src/trainers/dqn/data_loading.rs: same blend +
trim in DBN-fallback path; `last sample targets itself` block
removed (the trimmed range guarantees feature-vector and target
lengths match without a sentinel last row).
- crates/ml/src/fxcache.rs: FXCACHE_VERSION 8 → 9 + v9 docstring entry.
- crates/ml/tests/multi_horizon_reward_blend_test.rs (NEW): CPU-only
oracle behavioural test — 4 cases including known returns, trim
contract, zero-close short-circuit, constant-price blend.
- docs/dqn-wire-up-audit.md: Concerns subsection appended to the
SP19 Commit B entry already drafted in Commit A (pre-existing
test_fxcache_empty + test_dqn_checkpoint_round_trip flakiness
documented for Invariant 7).
Verification:
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace clean
cargo test -p ml --test multi_horizon_reward_blend_test 4/4 pass
cargo test -p ml --test fxcache_roundtrip_test test_fxcache_f32_roundtrip passes (TARGET_DIM unchanged)
cargo test -p ml --lib ... 13 baseline failures (pre-existing); zero new regressions
bash scripts/audit_sp18_consumers.sh --check exit 0
Pre-existing test_fxcache_empty failure: hand-crafts a 64-byte header
but the actual header is 72 bytes; reader bails early on
"failed to fill whole buffer" instead of "bar_count=0". Test setup
bug, NOT a regression. Pre-Commit B failure count: 13. Post-Commit B
failure count: 13 (the test_dqn_checkpoint_round_trip test is flaky
and toggles independently of this change — verified by stashing and
re-running 3× pre-Commit B).
Atomic-refactor invariant satisfied: Commit A (slot reservations) +
Commit B (producer-side blend) land on the same branch with no L40S
dispatch between. Per task instruction the branch stays unpushed
pending user review.
DBN-fallback path: applied identically in `data_loading.rs:497-528`.
Both producers use the same `SP19_HORIZON_BLEND_WEIGHTS` constant and
the same `LOOKAHEAD_HORIZON_MAX = 30` trim.
Vol-scale correction site: applied inside the blend (before weighting)
in BOTH producers. The existing `tgt[1]` preprocessing pipeline does
NOT double-scale — `experience_kernels.cu:1556` and
`cuda_pipeline/mod.rs:508` consume `tgt[1]` as a unit-scale log-return
exactly as before the blend was added; the blended value drops in
without further scaling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e140392f86 |
feat(audit): per-regime val WR instrumentation (T/R/V buckets)
Adds 6 output slots to compute_backtest_metrics_kernel — per-bucket
(win_rate, trade_count) for the {Trending, Ranging, Volatile} regime
split — so the val backtest surfaces whether the long-running ~46%
aggregate WR hides regime-conditional edge. Trades are bucketed at
trade-OPEN by feature[40] (ADX-norm) per the structural thresholds
(T:ADX>0.4, R:ADX<0.2, V:otherwise), mirroring
gpu_walk_forward.rs::classify_regime_from_features. Block tree-reduce
only per feedback_no_atomicadd. Observability-only emission via new
HEALTH_DIAG[N]: val_regime [wr_T=... n_T=... wr_R=... n_R=... wr_V=...
n_V=...] line; thresholds remain kernel constants per
feedback_isv_for_adaptive_bounds (no controller consumer yet).
Implementation atomic (kernel + launcher + WindowMetrics + HEALTH_DIAG
emit + 3 GPU oracle tests + audit doc):
- backtest_metrics_kernel.cu: per-thread per-regime trade counters,
2-slot boundary buffer extension carrying open-bar regime through
block stitch, output stride 13 → 19, shmem 5 → 11 reduction tiles
- gpu_backtest_evaluator.rs: WindowMetrics +6 fields, metrics_buf
size 13 → 19, launcher passes features_buf + feature_dim, consume
populates per-regime fields
- metrics.rs: val_regime HEALTH_DIAG line in consume_validation_loss
- regime_wr_oracle_tests.rs (NEW): 1 CPU sanity + 3 GPU oracle tests
(bit-exact match ε=1e-5 vs CPU oracle on stratified 30/40/30 batch)
Validation: cargo check --workspace clean; 17/17 gpu_backtest_evaluator
unit tests pass; 1+3/4 regime WR tests pass on local RTX 3050 Ti
(1.89s); audit_sp18_consumers.sh --check exit 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
834eaec4bd |
feat(sp18 v2 P4.T1+T2): B-leg next_states hoist + compute_q_next_target_bootstrap skeleton (additive dead code)
Phase 4 lands the additive infrastructure for the B-leg target-Q DDQN
bootstrap that Phase 5 will swap into td_lambda_kernel's q_next argument
at gpu_experience_collector.rs:~4313 (post-Task-4.1 hoist).
Tasks landed:
- **Task 4.1**: build_next_states_f32 invocation hoisted to BEFORE
the TD(λ) launch block. Pure ordering change — verified via the
SP18 audit grep flagging 0 post-TD(λ) consumers of next_states.
- **Task 4.2**: compute_q_next_target_bootstrap skeleton method on
GpuExperienceCollector with full plan-aligned signature
(next_states, target_params_ptr, online_params_ptr, batch_size →
Result<CudaSlice<f32>, MLError>). Body is a hard-error early-return
per feedback_no_stubs — no production caller in Phase 4 (the
caller is wired by Phase 5 Task 5.1, replacing the self-bootstrap
clone). Returning Err satisfies Invariant 9 (no deferred-work
markers); any accidental pre-Phase-5 call hard-errors with a
clear pointer to the open sub-tasks.
Tasks 4.3 (online forward + DDQN per-branch argmax), 4.4 (target
forward + compute_expected_q gather), 4.5 (replace error early-return
with full implementation + GPU oracle test gates) are deferred to a
follow-up subagent dispatch per their plan-defined per-task TDD cycle
scope. Each requires substantial new infrastructure on the collector
(target-net trunk forward chain duplicating gpu_dqn_trainer.rs Pass 2's
VSN+BN+OFI+trunk+branch-head sequence).
Tests:
- crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs (NEW):
3 introspection tests (no GPU required):
* compute_q_next_target_bootstrap_method_exists
* next_states_built_before_td_lambda (Task 4.1 ordering invariant)
* td_lambda_still_consumes_self_bootstrap_q_next_in_phase4
(Phase 4→Phase 5 atomic-refactor guard)
Atomic-refactor invariant (HARD — feedback_no_partial_refactor):
NO L40S DISPATCH between this Phase 4 close-out commit and the
Phase 5 Task 5.1 commit that replaces the q_next clone with
self.compute_q_next_target_bootstrap(&next_states, ...).
Verification:
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace → clean
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 \
cargo test -p ml --test sp18_td_lambda_q_next_oracle_tests
→ 3/3 pass
bash scripts/audit_sp18_consumers.sh --check → exit 0
Files:
crates/ml/src/cuda_pipeline/gpu_experience_collector.rs (Task 4.1+4.2)
crates/ml/tests/sp18_td_lambda_q_next_oracle_tests.rs (NEW; 3 tests)
docs/sp18-wireup-audit.md (Phase 4 close-out section + fingerprint)
docs/dqn-wire-up-audit.md (Invariant 7 entry)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
5ea5aa9b8e |
feat(sp18 v2 P3.T1-T5): adaptive HOLD_REWARD_POS/NEG_CAP producer kernel
Lifts the Phase 2 caps from sentinel-driven cold-start (5.0/-10.0
fixed) to p99(|step_ret|) over Long/Short trade closes × 1.5 safety
factor with Wiener-optimal alpha blend. The Phase 2 consumer
(compute_sp18_hold_opportunity_cost) sees producer-driven slots
[483]/[484] from epoch 1 onward; sentinel branch in the consumer
remains as the cold-start fallback path (zero-trade-close epoch ⇒
producer early-returns ⇒ slots stay at sentinel ⇒ consumer falls
through to the +5/-10 macro defaults — bit-identical to pre-Phase-3).
Mirrors SP14 P0-A reward_cap_update_kernel structural template with
three differences: (1) filter (is_close && step_ret != 0) — both
winners AND losers (the consumer needs the magnitude scale of *all*
Long/Short closes); (2) Welford-derived Wiener-α (slots [487..493))
replaces fixed α=0.01, with floor at WELFORD_ALPHA_MIN=0.4 per
pearl_wiener_alpha_floor_for_nonstationary (the policy-realised
distribution is intrinsically non-stationary as the policy adapts);
(3) bounds [0.5, 50.0] (vs. position-side [1.0, 50.0]).
Atomic single-commit per feedback_no_partial_refactor:
- crates/ml/src/cuda_pipeline/hold_reward_cap_update_kernel.cu (NEW)
- crates/ml/build.rs cubin manifest entry
- HoldRewardCapUpdateOps in gpu_aux_trunk.rs (new struct + impl)
- HOLD_REWARD_CAP_UPDATE_CUBIN static + struct field +
launch_hold_reward_cap_update method + constructor instantiation +
field-init in gpu_dqn_trainer.rs (5 sites)
- Per-epoch boundary launch in training_loop.rs right AFTER
launch_reward_cap_update (shared step_ret/trade_close source buffers,
independent ISV slot pairs)
- HEALTH_DIAG[N]: hold_reward_cap [pos={:.4} neg={:.4} fire_rate={:.4}]
- 3 GPU oracle tests (T5 producer-drives-slots, Pearl-A REPLACE,
no-closes preserves-isv) — all pass on local RTX 3050 Ti
- Phase 3 close-out sections in docs/sp18-wireup-audit.md and
docs/dqn-wire-up-audit.md
Pearls applied: feedback_no_atomicadd, pearl_first_observation_bootstrap,
pearl_wiener_optimal_adaptive_alpha, pearl_wiener_alpha_floor_for_nonstationary,
pearl_no_host_branches_in_captured_graph, pearl_symmetric_clamp_audit,
pearl_audit_unboundedness_for_implicit_asymmetry (NEG = -2 × POS at
producer time, single source of truth), feedback_isv_for_adaptive_bounds,
pearl_fused_per_group_statistics_oracle.
Validation: cargo check --workspace clean; 3 GPU oracle tests pass on
local RTX 3050 Ti; scripts/audit_sp18_consumers.sh --check exits 0
(no fingerprint drift in tracked sections).
Plan: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md
Phase 4-5 (B-leg target-net forward + q_next replacement) follows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e877e8aefe |
chore(sp18): archaeology cleanup — strip post-deletion markers + ISV_TOTAL_DIM giant docstring
Pure-comment follow-up to SP18 Phase 1 atomic deletion ( |
||
|
|
8da8e2e584 |
feat(sp18 v2 P2.T1-T5): structural Hold opportunity-cost device fn + 3-site migration (INTERIM STATE LIFTED)
Atomic introduction of `compute_sp18_hold_opportunity_cost` device function
in `trade_physics.cuh` + migration of the 3 placeholder-commented per-bar
Hold sites in `experience_kernels.cu` + CPU/GPU oracle tests + test-wrapper
cubin manifest entry — single atomic commit per `feedback_no_partial_refactor`.
Closes the SP18 INTERIM STATE introduced by Phase 1 (
|
||
|
|
3c318953a7 |
feat(sp18 v2 P1.T3-T6): atomic delete SP13/SP16 hold_cost_scale chain
Deletes the entire reactive Hold-cost controller chain from SP13 P0a / SP16
P2 / SP16 T3 atomically — replaced by the SP18 D-leg structural opportunity-
cost reward landing in Phase 2.
Per DD7(c): observation chain preserved (slots 380, 382). Slot 380 becomes
constructor-init-only diagnostic at HOLD_COST_BASE=0.005, never updated.
18 consumer sites deleted (8 from plan checklist + 10 from A1-A10 audit
expansion):
Production code:
- 3 reward subtraction sites (replaced with Phase 2 placeholder comments)
- hold_cost_scale_update_kernel.cu (file deleted)
- HoldCostScaleUpdateOps struct in gpu_aux_trunk.rs (~120 lines)
- launch_hold_cost_scale_update in gpu_dqn_trainer.rs (forwarder)
- Host controller block in training_loop.rs (~60 lines)
- Slot constants [461..468) in sp14_isv_slots.rs
- HOLD_COST_CONTROLLER_GAIN/FLOOR/CEIL constants in sp13_isv_slots.rs
- state_layout.cuh mirror constants for [461..468)
- build.rs cubin manifest entry
- 7 fold-reset registry entries + 7 dispatch arms
Tests:
- sp14_oracle_tests.rs lines 2173-2926 (11 tests + helpers, ~750 lines)
— these include_bytes! the deleted cubin and cannot survive
- lock_sp18_v2_pp4_retired_chain test (deleted, no inverse-contract
replacement — locking a deletion is pointless ceremony per user call)
Layout fingerprint: range [461..468) is RESERVED gap (SP14-C.1 pattern;
ISV_TOTAL_DIM stays at 507 for checkpoint compatibility). Layout fingerprint
seed updated atomically: HOLD_COST_SCALE + HCS_* slots replaced with
RESERVED_GAP_461_TO_468=SP18_P1_RETIRED marker.
INTERIM STATE: 3 reward sites are now missing the per-bar Hold cost. This
is forbidden to L40S-dispatch until Phase 2 lands the
compute_sp18_hold_opportunity_cost device fn that replaces the deleted
subtraction. The audit doc records this hold.
Validation: cargo check --workspace clean; cargo test -p ml --lib passes
(state_reset_registry::every_fold_and_soft_reset_entry_has_dispatch_arm,
sp16_t3_wiener_welford_slot_layout_locked, sp18_combined_slot_layout_locked,
sp18_shrink_perturb_slot_layout_locked, sp18_fold_reset_entries_present —
all OK). Audit fingerprint: per-bar Hold-cost subtraction sites count
13 → 0 (smoking gun for complete deletion).
Plan: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md
Audit: docs/sp18-wireup-audit.md (full A1-A10 expansion + post-deletion
fingerprint)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0b737ec30a |
feat(sp18-v2): ISV-adaptive shrink-and-perturb at fold transition
Replaces the hardcoded `α=0.8 / σ=0.01` constants in `reset_for_fold`'s
`shrink_and_perturb` call with ISV-driven adaptive bounds per
`feedback_isv_for_adaptive_bounds` and `feedback_adaptive_not_tuned`.
The driving signal is the relative weight drift `||current - best||₂ /
||best||₂` already computed each epoch by the SP18 v2 weight-drift
diagnostic kernel (commit
|
||
|
|
aab13a83f2 |
feat(sp18-v2): weight-drift HEALTH_DIAG diagnostic
Add per-epoch HEALTH_DIAG line measuring how far current online params
have drifted from the best-Sharpe checkpoint. The fold-transition
restore-best fix preserves peak weights across folds, but within-fold
edge-decay still happens (Adam keeps stepping after peak Sharpe). The
drift diagnostic surfaces the trajectory so operators can correlate
Sharpe-peak decay with parameter movement.
HEALTH_DIAG line:
HEALTH_DIAG[N]: weight_drift [norm_l2={:.6} relative={:.6} branch_max={:.6}]
Where:
- norm_l2 = ||params_flat - best_params||₂ (absolute L2)
- relative = norm_l2 / max(||best_params||₂, EPS) (relative drift)
- branch_max — currently mirrors `relative` (single-scalar form per spec).
Per-branch breakdown is a deferred follow-up: branch heads are
protected from S&P (skip_start..skip_end), so trunk drift dominates
the L2 in any case.
Cold-start: when best_params_snapshot is None (first fold or any fold
without a Sharpe improvement yet), launcher emits [0.0, 0.0] directly
(kernel skipped, mapped-pinned host slice written from CPU).
Distinguishable from "snapshot matches current" via best_epoch elsewhere.
Kernel (weight_drift_diag_kernel.cu): single block × 256 threads. Two
block tree-reductions sharing one shmem tile sequentially:
Pass 1: ||params - best||₂² over (params - best)
Pass 2: ||best||₂² over best
Thread 0 finalizes sqrt + EPS-floored ratio + writes via
__threadfence_system() for PCIe-visible coherence.
Per feedback_no_atomicadd — block tree-reduce only.
Per feedback_no_htod_htoh_only_mapped_pinned — output is
MappedF32Buffer<2>; cold-start bypass uses host_slice_mut.
Wire-up (atomic):
- crates/ml/build.rs: cubin manifest entry
- crates/ml/src/cuda_pipeline/weight_drift_diag_kernel.cu (NEW)
- crates/ml/src/trainers/dqn/fused_training.rs: field + constructor
+ launch_weight_drift_diag() + read_weight_drift_diag()
- crates/ml/src/trainers/dqn/trainer/mod.rs:
DQNTrainer::read_weight_drift_diag() public wrapper (CPU-only path
emits zeros)
- crates/ml/src/trainers/dqn/trainer/training_loop.rs: per-epoch
HEALTH_DIAG emit adjacent to SP17 dueling block
GPU oracle test (crates/ml/tests/sp18_weight_drift_test.rs, NEW):
4 cases, all #[ignore = "requires GPU"]:
1. weight_drift_matches_cpu_oracle_4096 — N=4096, ε=1e-5
2. weight_drift_is_zero_when_params_equals_best
3. weight_drift_eps_floor_when_best_is_zero (catches NaN/Inf edge)
4. weight_drift_n_zero_emits_zeros (degenerate guard)
All 4 pass on local RTX 3050 Ti.
Pre-commit Invariant 7: docs/dqn-wire-up-audit.md updated with kernel
algorithm, wire-up table, cross-pearl invariants, and oracle-test
inventory.
Per:
- feedback_no_atomicadd (block tree-reduce in drift kernel)
- feedback_no_htod_htoh_only_mapped_pinned (MappedF32Buffer<2>)
- feedback_wire_everything_up (kernel + launcher + emit + test atomic)
- pearl_no_host_branches_in_captured_graph (cold-path, outside graph)
- feedback_no_partial_refactor (single atomic commit)
|
||
|
|
3e9cefdbd9 |
fix(sp18-v2): restore-best precedes shrink-and-perturb at fold transition
`train_walk_forward` called `reset_for_fold().await?` directly at the fold boundary, which applies `shrink_and_perturb(α=0.8, σ=0.01)` to whatever `params_flat` holds. At end-of-fold, `params_flat` carries the LATEST-EPOCH decayed weights, NOT the best-Sharpe snapshot saved mid-fold. Fold N+1 then started from `0.8 × decayed + noise` and carried the within-fold edge-decay forward. Insert `restore_best_gpu_params()` BEFORE `reset_for_fold` so S&P operates on the best-Sharpe snapshot. Cold-start guard swallows the "no snapshot saved" error on the first fold (or any fold without a Sharpe improvement) — matches pre-fix behavior on cold start, no regression. State interaction with `reset_for_fold`: restore-best writes `params_flat` ONLY (online weights); reset_for_fold then runs S&P on the restored online, hard-syncs target ← restored online, and resets Adam state on every optimizer. Non-conflicting. `best_params_snapshot` is constructor-init `None` on FusedTrainingCtx and is NEVER cleared at fold boundary. The trainer's `self.best_sharpe` IS reset to NEG_INFINITY in `reset_for_fold` so the first improvement in fold N+1 will overwrite the snapshot. Until then, the snapshot holds whichever fold most recently saved a peak — intended training-scoped behavior. Strict within-fold semantics flagged as a follow-up consideration. Behavioral test (CPU oracle) pins the math contract: - shrink_and_perturb(α, σ) on X = α × X + (1−α) × N(0, σ) - with-fix vs without-fix differ by α × (W_best − W_curr) - cold-start (best == current) is bit-identical between orderings GPU-level kernel coverage stays in compile_training_kernels smoke and the L40S 30-epoch validation gating SP18 Phase 1. Pre-commit Invariant 7: docs/dqn-wire-up-audit.md updated with full rationale, state-interaction analysis, lifecycle notes, and the preserved cross-pearl invariants. Per: - feedback_no_partial_refactor (call-ordering + test + audit-doc atomic) - feedback_no_legacy_aliases (reuses existing API unchanged) - feedback_wire_everything_up (restore_best_gpu_params gains second cold-path consumer) - pearl_no_host_branches_in_captured_graph (runs outside graph capture) |
||
|
|
27f8e332da |
plan(sp18 v2): Phase 0 Task 0.2 — B-leg V_SHARE trajectory + TD-error magnitude diagnostic
Adds the SP18 v2 Phase 0 B-leg observability scaffold per the plan's
Task 0.2 spec:
- New kernel `td_error_mag_ema_kernel.cu` (single-block × 256 threads):
reads `td_errors_buf [B]` (already populated by C51/MSE loss for PER
priority recomputation, post-train-step), block tree-reduces
`mean(|td_errors[b]|)` (no atomicAdd), and EMA-blends into
`ISV[TD_ERROR_MAG_EMA_INDEX=493]` via Pearl-A first-observation
bootstrap (sentinel 0.0 → REPLACE) + fixed α=`WELFORD_ALPHA_MIN=0.4`
per `pearl_wiener_alpha_floor_for_nonstationary`. The TDB_* Welford
accumulators in slots [498..504) are RESERVED for the Phase 4
q_next_target Wiener-α chain — not used in Phase 0.
- Cubin manifest entry in `crates/ml/build.rs` + `TD_ERROR_MAG_EMA_
CUBIN` re-export in `gpu_dqn_trainer.rs`.
- `sp18_td_error_mag_ema_kernel` field on `GpuDqnTrainer` + cubin load
on the trainer's stream + `launch_sp18_td_error_mag_ema_update()`
cold-path launcher + `read_sp18_td_error_mag_ema()` convenience
wrapper.
- New `sp18_v_share_history: [[f32; 4]; 5]` field on `DQNTrainer` —
fixed-size ring buffer of the last 5 epochs of per-branch V_SHARE
EMA readings (slots [478..482) per SP17 Phase 3.2). Initialised to
`[[NaN; 4]; 5]`; epochs 0–3 emit `nan` as the slope and skip the
ISV write; epoch 4 onward computes `(EMA[now] - EMA[now-4]) / 4`
per branch and writes the dir-branch slope to
`ISV[V_SHARE_TREND_DIAG_INDEX=496]`.
- Two new HEALTH_DIAG lines in `training_loop.rs` at the per-epoch
boundary (right after the SP18 reward_decomp line):
HEALTH_DIAG[N]: v_share_traj [dir_slope=X mag_slope=Y ord_slope=Z urg_slope=W]
HEALTH_DIAG[N]: td_error_pre [magnitude_ema=X]
V_SHARE slope is host-side computation against the ring buffer
(`(now - now_m4) / 4` per branch). TD-error magnitude is post-blend
ISV slot 493 read (producer fires inside `read_sp18_td_error_mag_
ema()`). Pre-fix baseline for the B-DD9 ratio gate
(`avg(|TD-error|) ratio post-fix / pre-fix ∈ [0.5, 5.0]`).
- New GPU oracle tests in `crates/ml/tests/sp18_hold_reward_oracle_
tests.rs`:
* `td_error_mag_ema_pearl_a_bootstrap` — synthetic td_errors with
closed-form mean(|td|)=1.125; pre-populate slot at sentinel;
assert post-launch slot equals the mean (Pearl-A direct-replace).
* `td_error_mag_ema_blend_post_bootstrap` — synthetic td_errors
with mean(|td|)=0.5; pre-populate slot at non-sentinel 1.0;
assert blend equals `(1 - 0.4) × 1.0 + 0.4 × 0.5 = 0.8`.
Pure observability — no production-path consumer in this commit. No
reward changes, no Bellman target changes, no kernel modifications to
the action-selection or training paths. Per `feedback_no_partial_
refactor` the kernel + cubin manifest + buffer + launcher + ring
buffer + HEALTH_DIAG emit + GPU oracle tests all land atomically.
Verification:
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace clean.
All 4 GPU oracle tests pass on RTX 3050 Ti (2.09s):
reward_decomp_per_action_gpu_oracle, reward_decomp_empty_bin,
td_error_mag_ema_pearl_a_bootstrap, td_error_mag_ema_blend_post_bootstrap.
Existing slot lock + state_reset_registry tests still pass.
Plan: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md
§ Phase 0 Task 0.2.
Audit: docs/dqn-wire-up-audit.md § "SP18 v2 Phase 0 Task 0.2".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
15b50ac38f |
plan(sp18 v2): Phase 0 Task 0.1 — D-leg per-action reward decomposition diagnostic
Adds the SP18 v2 Phase 0 D-leg observability scaffold per the plan's
"Phase 0 — diagnostic emit (NO functional change)" task:
- New kernel `reward_decomp_diag_kernel.cu`: block tree-reduce
(4 blocks × 256 threads, one block per direction-axis bin) reading
`reward_components_per_sample [N×6]` + `actions_out [N]` and emitting
5 per-bin stats (mean r_micro / mean r_opp_cost / mean r_popart /
mean |reward| / fire_rate) into a 20-float row-major output. Bin
order: Hold(0)→Long(1)→Short(2)→Flat(3); col order: micro→opp→
popart→abs→fire. Empty-bin guard emits 0.0 (NOT NaN) per the
consumer-side KILL CRITERION arithmetic contract.
- Cubin manifest entry in `crates/ml/build.rs` + `REWARD_DECOMP_DIAG_
CUBIN` re-export in `gpu_dqn_trainer.rs`.
- 20-float `MappedF32Buffer sp18_reward_decomp_diag_buf` field on
`GpuDqnTrainer` + accessor pair (`sp18_reward_decomp_diag_dev_ptr`
for the writer-side launcher; `read_sp18_reward_decomp_diag` for the
HEALTH_DIAG reader). Buffer is constructor-zeroed so cold-start
HEALTH_DIAG emits a deterministic zero block.
- `sp18_reward_decomp_diag_kernel` field on `GpuExperienceCollector` +
cubin load on the collector's stream + `launch_sp18_reward_decomp_
diag(n, b1, b2, b3, out_dev_ptr)` launcher. Wired in
`training_loop.rs` at the per-step boundary, BEFORE
`launch_reward_component_ema_inplace` (which `memset_zeros` the
source buffer after consuming it) per `pearl_canary_input_freshness_
launch_order`.
- New per-epoch HEALTH_DIAG line emit at the existing per-epoch
boundary (after the SP17 dueling line):
HEALTH_DIAG[N]: reward_decomp [hold(micro=X opp=Y popart=Z abs=W
fire=F) long(...) short(...) flat(...)]
Reads the mapped-pinned 20-float diag buffer directly via the
collector→trainer host_ptr — no DtoH copy.
- New `crates/ml/tests/sp18_hold_reward_oracle_tests.rs`:
* `reward_decomp_per_action_cpu_oracle` (CPU oracle pinning the
per-bin reduction math against a 4-sample synthetic batch).
* `reward_decomp_per_action_gpu_oracle` (GPU oracle, ignored unless
`--ignored`; asserts kernel matches CPU oracle bit-for-bit within
1e-6 f32 budget).
* `reward_decomp_empty_bin_emits_zero_not_nan` (empty-bin contract
guard).
Pure observability — no production-path consumer in this commit. No
reward changes, no Bellman target changes, no kernel modifications to
the action-selection or training paths. Per
`feedback_no_partial_refactor` the kernel + cubin manifest + buffer +
launcher + production wire-up + HEALTH_DIAG emit + GPU oracle test all
land atomically.
Verification:
SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check --workspace clean.
CPU oracle test passes; GPU oracle + empty-bin guard both pass on
RTX 3050 Ti (2.13s).
Plan: docs/superpowers/plans/2026-05-08-sp18-reward-shape-hold-attractor.md
§ Phase 0 Task 0.1.
Audit: docs/dqn-wire-up-audit.md § "SP18 v2 Phase 0 Task 0.1".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b6b17d46bb |
feat(sp17-3.2): V_share + advantage_clip_bound producers + extended emit
Phase 3.2 lands the remaining two SP17 dueling-Q diagnostic producers
atomically with kernel + launcher + Rust wrapper + extended HEALTH_DIAG
emit + GPU oracle tests per `feedback_wire_everything_up`.
V_share[d] = |E[V]| / (|E[V]| + |E[A_centered, picked]|)
where picked = argmax_a Σ_z A_raw[i, a, z] (max-Q semantic — tractable
per-batch without depending on actions_history_buf which is collector-
time state stale relative to the cuBLAS forward at HEALTH_DIAG cadence).
Pearl-A bootstrap (sentinel 0.5) + α=WELFORD_ALPHA_MIN=0.4 + bilateral
[0, 1] clamp per `pearl_symmetric_clamp_audit`. 4 blocks × 256 threads.
advantage_clip_bound = p99(|A_centered|) × ADVANTAGE_CLIP_SAFETY_FACTOR=1.5
via sp4_histogram_p99 (block tree-reduce + per-warp tile binning, NO
atomicAdd per `pearl_fused_per_group_statistics_oracle`). EMA α=0.01
slow per-fold + bilateral clamp [0.1, 100.0] per
`pearl_symmetric_clamp_audit`. Pearl-A bootstrap (sentinel 1.0).
Single block × 256 threads + flat |A_centered| scratch buffer
(mapped-pinned, sized to B × Σ_d b_d × NA).
Observability-only — the actual clipping wire-up is Phase 5 follow-up.
The Phase 1 mean-zero contract (commits eabcf8d52..6f53d676f) makes
A_centered a meaningful signal; this commit observes it.
Extended HEALTH_DIAG line:
HEALTH_DIAG[N]: dueling [v_share=(d=X m=Y o=Z u=W)]
[a_var=(d=A m=B o=C u=D)] [clip=K]
GPU oracle tests on RTX 3050 Ti (all pass, 13/13 SP17 tests):
- v_share_per_branch_matches_closed_form: synthetic V=2.0 + linear A
per branch; closed-form V_share = 2/(2 + |K_d × (n_d-1)/2|);
ε=1e-4. Pearl-A bootstrap REPLACES on first launch.
- advantage_clip_bound_tracks_p99_safety: synthetic A with action-
dominant + per-(i,z) jitter (the jitter is REQUIRED — pathologically
lockstep values undercount in sp4_histogram_p99's non-atomic warp
tile binning per the kernel's documented "1/(256×32) loss for
uniformly distributed signals" qualifier; concentrated values violate
the assumption. Real |A_centered| in production is continuous, so
this is a test-data-only effect.) ε=0.20 (jitter + linear histogram
quantization).
Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md
Phase 3.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
1e70cd5e59 |
feat(sp17-3.1): A_var_ema per-branch producer + HEALTH_DIAG emit
Phase 3 of SP17 dueling-Q identifiability — first of three diagnostic
producers landed atomically with kernel + launcher + Rust wrapper +
HEALTH_DIAG emit + GPU oracle test per `feedback_wire_everything_up`.
Per branch d ∈ {dir, mag, ord, urg}:
Var_d = (1/(B × n_d × NA)) Σ_{i, a, z} (A[i, a, z] − mean_a A[*, z])²
Block tree-reduce (no atomicAdd, `feedback_no_atomicadd`); 4 blocks ×
256 threads. Pearl-A first-observation bootstrap (sentinel 0.0 →
REPLACE on first launch); steady-state α = WELFORD_ALPHA_MIN=0.4 per
`pearl_wiener_alpha_floor_for_nonstationary` — the structural-control
floor preserves catch-up bandwidth without storing 24 Welford
accumulator slots for a cold-path-cadence diagnostic.
Cold-path emit: single launch per HEALTH_DIAG cadence (epoch boundary)
right after `v_a_means`. New line:
HEALTH_DIAG[N]: dueling [a_var=(d=X m=Y o=Z u=W)]
The line will be extended with V_share + advantage_clip_bound in
Phase 3.2, then finalised in Phase 3.3.
GPU oracle test on RTX 3050 Ti: synthetic A constructed so each branch
d has a closed-form Var(A_centered); kernel readback matches expected
value within ε=1e-4 (f32 rounding budget for ~8×n×51 accumulator
length).
Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md
Phase 3.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
079a06e485 |
test(sp17): asymmetric A → deterministic argmax under MIN_TEMP=0.5
Verifies that when one direction's centered advantage dominates
strongly enough, action selection picks it deterministically across
all N=1000 Philox-keyed runs.
Plan specifies "Thompson temp = 0.0 → pure argmax E[Q]", but the
kernel floors thompson_temp at MIN_TEMP=0.5 per
pearl_blend_formulas_must_have_permanent_floor — pure τ=0 isn't
ISV-accessible. With τ=0.5 the blend is q_eff = 0.5·E[Q] +
0.5·q_sample; deterministic argmax across all τ=0.5 draws requires
the E[Q] gap to exceed atom_span/2.
Construction: NA=3 atoms [-0.1, 0, +0.1] (atom_span=0.2). A_dir
puts +300 at z=2 for Long, -100 at z=2 for others (per-atom mean
zero ⇒ centering preserves shape). Long centered E[Q] ≈ +0.1, others
≈ -0.05 (gap 0.15 > 0.1). q_sample[Long] = +0.1 with prob ≈ 1
(softmax(300) ≈ delta at z=2); q_sample[other] ∈ {-0.1, 0} (zero
prob for +0.1). q_eff[Long] = 0.1; q_eff[other] ≤ -0.025. Long
strictly wins all draws.
If the centering breaks (Long no longer dominant under centered E[Q])
or τ blends a non-Long sample over Long, this test fires.
Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f31a6b7ff0 |
test(sp17): symmetric A ⇒ identical per-direction E[Q]
Verifies that under uniform A (== 0 across all action × atom slots),
every direction has identical centered E[Q] regardless of V.
The plan's original wording probes this through Thompson selector
("uniform action distribution"), but the kernel's first-wins-strict-
`>` argmax over four i.i.d. Thompson samples produces a structurally
non-uniform distribution under symmetric A even with correct centering
(closed-form earlier-bias predicts ≈[44%, 26%, 18%, 11%] across
Short/Hold/Long/Flat from the tie statistics). The Thompson distribution
is V-dependent through tie statistics — NOT a centering regression.
Restated as the structural pre-Thompson property: with A=0 and
V arbitrary, centered logit = V + 0 is identical across all directions
⇒ per-direction E[Q] identical to ε=1e-5. The Thompson selector reads
these centered logits; if A=0 produced non-zero per-direction E[Q]
spread, *that* would be the centering regression — exactly what this
test catches.
Probed via compute_expected_q (reads back per-action E[Q] directly,
no Thompson noise as red herring). V-non-uniform sanity check confirms
the kernel reads V (non-zero E[Q] when V ≠ 0).
Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c2dd6917e0 |
test(sp17): A-centered invariance under V shift
Verifies the architectural contract: a uniform additive shift to V across atoms cannot leak into the post-centering distribution. The plan specifies reading A_centered directly, but A_centered is a register-local quantity inside compute_expected_q; this test restates the property as the equivalent behavioral assertion that adding a uniform constant to V leaves every per-action E[Q] identical (softmax translation invariance). A regression that accidentally reduced over (V + A) instead of A alone would shift the per-atom mean by V_SHIFT and corrupt the centered logits; the per-action E[Q] would diverge by O(1), failing the ε=1e-4 assertion. Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
10fafe8e60 |
test(sp17): V-invariance behavioral test
Verifies the architectural contract: V depends only on state, not on the specific advantage tensor. Two A tensors with same per-atom mean produce different post-centering shapes ⇒ different per-action E[Q] (centering is shape-sensitive), but the V-only diagnostic readout is identical across the two runs (V cannot leak in via the per-atom mean reduction). This is the structural property that makes dueling work — without it, the model conflates "state value" with "action value" and gradient updates corrupt V via raw-A noise. Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ffa6fda868 |
feat(sp17): centered A in barrier/ib_gradient_direction (Commit D)
User design call DD11: migrate the two aux-CQL gradient kernels in
c51_loss_kernel.cu to the SP17 mean-zero advantage contract. The
pre-SP17 comment "skip advantage-mean centering — small epsilon vs
correctness simplicity" at barrier_gradient_direction:1212 is REMOVED;
its empirical-without-verification rationale doesn't hold under the
SP17 contract that compute_expected_q + c51_loss + c51_grad +
mag_concat_qdir + Thompson + quantile_q_select already enforce.
barrier_gradient_direction:
- Per-thread `a_mean_per_atom[NUM_ATOMS_MAX]` reduction over b0_size
direction actions.
- Forward Q-value computation reads `v_row[z] + (adv_a[z] - mean[z])`.
- Backward gradient recompute uses the same centered logits in the
per-action softmax probability accumulation. The Jacobian's symmetric
-1/b0_size per-atom offset cancels in the barrier's relative-push
gradient direction (max up, 2nd down — both targets see same offset).
- Stale "skip centering" comment deleted; replaced with SP17 explanation.
ib_gradient_direction:
- Same per-atom mean reduction at function entry.
- Forward Q computation + backward dq_dlogit recompute both flow through
centered probabilities. Variance var_q is invariant under common
per-atom shifts (math: shift cancels in (Q(a) - mean_q)^2), so var_q
numerics are bit-equivalent — but the gradient flows through the
centered probability `p(z|a)` for consistency with c51_grad backward.
NUM_ATOMS_MAX=128 ceiling guard added to both kernels (mirror of
experience_kernels.cu); early-exit `if (num_atoms > NUM_ATOMS_MAX) return`
matches the pattern already used by these kernels for unrelated
zero-op guards.
GPU oracle test (RTX 3050 Ti, 6/6 PASS):
barrier_gradient_direction_uses_centered_advantage — A=0, V=0 ⇒
centered logits all zero ⇒ uniform softmax ⇒ E[Q]=0 across actions ⇒
q_gap=0 ⇒ barrier fires at min_req=0.05 ⇒ asserts total |grad| > 1e-6.
Regression detector: any centering breakage produces non-finite or
zero gradients ⇒ test fails loudly. ib_gradient_direction shares the
identical per-atom mean reduction pattern so the same test covers
both kernels structurally.
Verification:
cargo check --workspace → clean
cargo test sp17_dueling_oracle_tests --features cuda
-- --ignored → 6/6 PASS
⚠ INTERIM STATE: c51_loss_batched + c51_grad_kernel already-centered
sites still need Commit E annotation pass to mark the existing Jacobian
+ per-d=1 magnitude-std as SP17-compliant.
Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3107edb8f7 |
feat(sp17): Thompson V-wire-in (Commit C)
User design call DD10 option (b): Thompson direction selector now reads
softmax(V[z] + (A[a, z] - mean_a A[*, z])) instead of pre-SP17
softmax(A[a, z]). Without V wired in, action selection responded to a
DIFFERENT distribution than compute_expected_q's E[Q] (the Bellman-target
ranking) — the very pathology SP17 is fixing at the action layer.
Architectural change closes the contract gap atomically across:
Kernel signature (`experience_action_select`):
- Added `const float* __restrict__ v_logits_dir` after `b_logits_dir`.
NULL is invalid (no fallback) — kernel hard-requires V.
- New per-thread `a_mean_per_atom_dir[THOMPSON_MAX_ATOMS]` reduction
computes mean A across the 4 direction actions per atom z (sample-local
register, no atomicAdd).
- Pass 1 (E[Q] for temperature blend + conviction): builds per-action
`combined_logits_d[z] = V[z] + (A[a,z] - mean_a A[*,z])` and feeds
`softmax_c51_inline` instead of raw `b_logits_dir + d * n_atoms`.
- Pass 2 (Thompson sample): same combined-logits rebuild per direction.
- `softmax_c51_inline` device helper UNCHANGED — keeping centering at
caller maintains a narrower contract; thompson_test_kernel and other
potential callers stay unaffected.
- THOMPSON_MAX_ATOMS=128 ceiling preserved; __trap() on overflow.
QValueProvider trait extension:
- `compute_q_and_b_logits_to` now takes `v_logits_out_ptr: u64` and
DtoD-copies `on_v_logits_buf` per sub-iteration (atomic per
feedback_no_partial_refactor — every consumer migrates in lockstep).
Trainer + evaluator wire-up:
- `gpu_dqn_trainer.rs::on_v_logits_buf_ptr() -> u64` (new pub fn, mirror
of existing `on_b_logits_buf_ptr`).
- `gpu_backtest_evaluator.rs::chunked_v_logits_buf` field allocated
[chunk_n * NA + 32*3] (cuBLAS tail-safety pad).
- Both call sites (collector + evaluator) pass v_logits arg in launch.
GPU oracle test (RTX 3050 Ti, 5/5 PASS):
thompson_direction_select_reads_v_logits — runs production cubin
twice on identical A logits with V=[0,0,0] vs V=[10,0,0]; asserts
q_gap_v_dominant < 50% of q_gap_v_zero. If V is being IGNORED
(regression), both runs produce IDENTICAL q_gaps and the test fails
with a clear message. Uses MappedF32Buffer / MappedI32Buffer per
feedback_no_htod_htoh_only_mapped_pinned.
Note on the plan's "raw argmax = Hold but centered argmax = Long" test:
Mathematical analysis shows softmax-with-constant-shift preserves
action ordering (mean subtraction adds the same per-atom constant to
every action's logits), so the plan's specific assertion isn't
algebraically constructable with simple A/V. The replacement test
(V-dependence of q_gap) is more sensitive — it fails on the actual
regression case (V ignored ⇒ identical q_gaps) the plan was trying
to detect.
Verification:
cargo check --workspace → clean
cargo test sp17_dueling_oracle_tests --features cuda
-- --ignored → 5/5 PASS
⚠ INTERIM STATE: aux-CQL barrier_gradient_direction +
ib_gradient_direction still read raw advantage. Commit D closes them;
Commit E annotates the pre-SP17 c51_loss/c51_grad already-centered sites.
Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4802879494 |
feat(sp17): centered A in mag_concat_qdir (Commit B)
Migrates the magnitude-branch's direction-Q conditioning input to the
SP17 mean-zero contract. mag_concat_qdir builds a per-(sample, action)
softmax over V[z] + A_dir[a, z] across THREE inner passes (max,
sum_exp, E[Q]); all three now read `dir_logits_b[..] - a_mean_per_atom[z]`.
Without this migration the magnitude trunk-input would see raw E[Q_dir]
while c51_loss/c51_grad backward consumes centered A — mixed contract
ruled out by `feedback_no_partial_refactor`.
The Plan-4 q_rms adaptive scale (ISV[96]) is preserved unchanged;
centering changes the per-action E[Q_dir] values that feed it but the
scale formula itself is structurally orthogonal. Backward into the
direction logits flows through c51_grad (already mean-zero) so no bw
change is needed in this kernel.
GPU oracle test: B=1, SH2=2, NA=3, B0=4 with the same synthetic where
mean_a = [0.75, 0, 0.75] is non-zero. Asserts:
(a) h_s2[0..SH2] copied verbatim into concat prefix
(b) tail slots = centered_E[Q_dir] × (1.0 / q_rms) to ε=1e-5
(c) tail[0] < 0 / tail[3] > 0 sign asymmetry (regression detector)
Verification (RTX 3050 Ti):
cargo check --workspace → clean
cargo test sp17_dueling_oracle_tests --features cuda
-- --ignored → 4/4 PASS
⚠ INTERIM STATE: Thompson direction-select + aux-CQL barrier/ib still
read raw advantage. Commits C-D close them; Commit E annotates the two
already-centered c51_loss/c51_grad pre-SP17 sites.
Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
9b6e94854f |
feat(sp17): centered A in quantile_q_select (Commit A)
Migrates `quantile_q_select` (uncertainty-driven action selection from
C51 atom CDFs) to the SP17 mean-zero advantage contract. The kernel
builds a per-(sample, branch, action) softmax over `V[z] + A[a, z]`
across THREE inner passes (max, sum_exp, CDF); all three now read
`adv_a[z] - a_mean_per_atom[z]`.
Plan flagged this as a hidden 6th consumer. Task 1.4/1.5 as authored
covered only c51_loss + c51_grad + mag_concat_qdir + Thompson; the
post-Task-1.2 audit found `quantile_q_select` reads raw advantage at
lines 5704/5709/5720 across all 4 branches and was missed entirely.
Sample-local register reduction (`float a_mean_per_atom[NUM_ATOMS_MAX]`,
NA_MAX=128 mirroring compute_expected_q); no atomicAdd, no shared mem,
no cross-thread sync per `feedback_no_atomicadd`. Device-side __trap()
on num_atoms overflow per `feedback_no_quickfixes`.
GPU oracle test: N=1, NA=3, B0=4 with V=[0,0,0] and A_raw chosen so the
per-atom mean is [0.75, 0, 0.75]. Test runs the production cubin with
iqn_readiness=0 and util_ema=1.0 and asserts each per-action q90 matches
the CPU oracle to ε=1e-5. Two structural-asymmetry assertions fail
loudly if centering regresses (action 0 q90 ≤ 0, action 3 q90 ≥ 0).
Mapped-pinned per `feedback_no_htod_htoh_only_mapped_pinned`.
Verification (RTX 3050 Ti):
cargo check --workspace → clean
cargo test -p ml --test sp17_dueling_oracle_tests --features cuda
-- --ignored → 3/3 PASS
⚠ INTERIM STATE: mag_concat_qdir + Thompson + aux-CQL barrier/ib still
read raw advantage. Commits B-D close them; Commit E annotates the two
already-centered c51_loss/c51_grad pre-SP17 sites. No L40S dispatch
escapes feat/sp17-dueling until every consumer is migrated per
`feedback_no_partial_refactor`.
Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
eabcf8d529 |
feat(sp17): mean-zero identifiability in compute_expected_q
Inserts per-atom mean-A reduction inside the per-branch outer loop: Q[a, z] = V[z] + (A[a, z] - mean_a A[*, z]) The post-centering atom-level identifiability holds: Σ_a A_centered[a, z] = 0 for every (sample, branch, atom). Per Wang et al. 2016 Dueling Networks: mean-zero is smoother and more stable than max-zero. Atom-level subtraction (not expectation-level) preserves distributional shape per action. Sample-local register reduction; no atomicAdd, no shared memory, no cross-thread sync. NUM_ATOMS_MAX=128 mirrors existing THOMPSON_MAX_ATOMS; device __trap() on overflow. ⚠ INTERIM STATE: c51_loss_kernel + c51_grad_kernel + mag_concat_qdir still read RAW (un-centered) advantage logits. Phase 1 Tasks 1.3-1.5 migrate them in lockstep within this branch BEFORE any L40S dispatch. A partially-migrated state is forbidden per feedback_no_partial_refactor. Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7b13324bcb |
test(sp17): CPU oracle for mean-zero centered Q computation
Locks the mean-zero identifiability math contract independent of GPU implementation. Subsequent Phase 1 GPU oracle tests compare kernel output against this contract bit-for-bit. Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cc4746b48d |
plan(sp17): HEALTH_DIAG v_a_means baseline (PRE-CENTERING)
Adds per-epoch readout of the V/A breakdown so we can observe the pre-SP17 baseline distribution before the identifiability projection lands. Also instruments the kill criterion for Phase 0: HEALTH_DIAG[N]: v_a_means [v=X a_dir=Y a_mag=... a_ord=... a_urg=...] If train-multi-seed-b5gmp's Q(Flat) over-attribution is V-driven, V should be elevated (~0.4+) while a_dir is small. If V is balanced and A is doing the work, dueling cannot help and we abort SP17. Block-tree-reduce kernel \`v_a_means_diag_kernel\` (no atomicAdd per \`feedback_no_atomicadd\`); MappedF32Buffer per \`feedback_no_htod_htoh_only_mapped_pinned\`. Plan: docs/superpowers/plans/2026-05-08-sp17-dueling-q-network.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
25ff1d4195 |
fix(sp16): floor Wiener-α at 0.4 for non-stationary control loops
Wiener-optimal α minimizes MSE for stationary stochastic signals.
The min_hold_temp / hold_cost_scale loops track a target driven by
the adapting policy itself — non-stationary by construction.
Empirical evidence: train-multi-seed-b5gmp sha
|
||
|
|
641aa0dfde |
fix(sp16-t3): Wiener-optimal adaptive α per pearl — hold_cost_scale + min_hold_temperature
Per train-multi-seed-hjzss validation: SP16 T1+T2 chain was structurally
landed but BEHAVIORALLY INERT in 5-epoch smoke (bit-identical to pfh9n
baseline through epoch 3). Root cause: hardcoded `alpha = 0.05f` in both
producer kernels violates feedback_isv_for_adaptive_bounds AND prevents
convergence in short runs (~60 epochs needed from cold start).
Fix per pearl_wiener_optimal_adaptive_alpha:
α = diff_var / (diff_var + sample_var + ε)
Where sample_var = running variance of target signal (Welford accumulator)
and diff_var = running variance of consecutive one-step differences.
Cold-start: target jumps 1.0 → 6.4 → 7.0 → high diff_var → α ≈ 0.6+
→ near-bootstrap responsiveness in epochs 1-3
Steady-state: signal stabilizes → diff_var drops → α decays naturally
→ smoothing emerges without hardcoded constant
Adds 12 new ISV slots (6 per producer):
- HCS_TARGET_MEAN/M2, HCS_DIFF_MEAN/M2, HCS_PREV_TARGET, HCS_SAMPLE_COUNT
- MHT_TARGET_MEAN/M2, MHT_DIFF_MEAN/M2, MHT_PREV_TARGET, MHT_SAMPLE_COUNT
ISV_TOTAL_DIM 462 → 474.
Both kernels migrated atomically. Pearl-A bootstrap preserved (sentinel
on prev_blended triggers REPLACE; cold-start α=1.0 when N<3 samples).
Defensive bounds [WELFORD_ALPHA_MIN=0.01, WELFORD_ALPHA_MAX=0.95] on the
Wiener-derived α to guard against denormal/underflow corner cases.
HEALTH_DIAG[N] emit extended with `alpha=...` and `sample_count=...` for
direct trajectory observation in validation smoke.
Behavioral tests verify:
- α high during signal jumps (>0.3 at epoch 3 post-cold-start)
- α low in steady state (mean tail α<0.4 under converging signal)
- Pearl-A bootstrap fires on first observation (Welford state advances
regardless of REPLACE branch)
- α stays within [WELFORD_ALPHA_MIN, WELFORD_ALPHA_MAX] over 50 epochs
(post-cold-start; cold-start α=1.0 by design)
- No 0.05f hardcoded literal remains in blend math (regression-locked
via host-only string scan)
5 GPU + host tests pass: sp16_phase3_alpha_high_during_signal_jump,
alpha_low_in_steady_state, pearl_a_bootstrap_first_obs,
alpha_naturally_bounded, no_hardcoded_alpha. sp14 + sp15 oracle suites
unchanged (34 GPU tests + 4 host tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
1a3bcf97b8 |
feat(sp16-p2): adaptive Hold cost scale via ISV[461]
Per train-multi-seed-pfh9n post-mortem: observed_hold_rate climbed 0.25 → 0.52 across training while cost penalty (~0.006) was 100× smaller than per-bar reward magnitudes (popart=0.97, cf=0.65). Hold action was effectively free, allowing Q(Hold) to dominate via structural low-variance bias. Fix: scale Hold cost adaptively. ISV[HOLD_COST_SCALE_INDEX=461] tracks: scale = clamp(1.0 + 24.0 × max(0, observed - target) / max(target, 0.01), 1.0, 25.0) Effective cost at 100% overrun (observed=2× target): 0.006 × 25 = 0.15, competitive with per-bar reward magnitudes (~0.01-0.1). At/below target: scale = 1.0 (no extra penalty). Pearl-A bootstrap + Welford slow EMA (α=0.05). Mirrors T1's MIN_HOLD_TEMPERATURE pattern (same input signals: ISV[382] observed, ISV[381] target). Producer: hold_cost_scale_update_kernel.cu — single-thread cold-path, per-epoch boundary, AFTER MIN_HOLD_TEMPERATURE in training_loop.rs. Consumer migration (atomic per feedback_no_partial_refactor): 3 sites in experience_kernels.cu — segment_complete branch (line ~3089), per-bar positioned-Hold branch (line ~3553), per-bar flat-Hold branch (line ~3617). Cold-start fallback: scale=1.0 when slot ≤ 0 or out-of-bounds (bit-identical pre-Phase-2 cost magnitude). ISV_TOTAL_DIM: 461 → 462. Behavioral tests (5/5 PASS on RTX 3050): - sp16_phase2_hold_cost_scale_climbs_with_overrun - sp16_phase2_hold_cost_scale_at_target_is_one - sp16_phase2_hold_cost_scale_under_target_is_one - sp16_phase2_hold_cost_scale_bounds_clamp - sp16_phase2_hold_cost_scale_pearl_a_bootstrap Regression: SP14 oracle suite 30/30 PASS, SP15 phase 1 oracle suite 36/36 PASS. Instrumentation: HEALTH_DIAG[N]: hold_cost_scale_diag obs/tgt/norm/scale. Per feedback_isv_for_adaptive_bounds + feedback_no_partial_refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2c469af2f7 |
fixup(sp16-p1): correct stale build.rs comment + remove tautological test assertion
Code-quality review of
|
||
|
|
0426ce8887 |
fix(sp16-p1): MIN_HOLD_TEMPERATURE signal swap to hold-rate overrun + slot 330 non-bug closure
Per train-multi-seed-pfh9n post-mortem follow-up: slot 460 stuck at 50 in Fold 1 was NOT a launch-lifecycle bug. Producer fires per-epoch but kernel had early-return guard on AUX_DIR_ACC_SHORT_EMA (slot 373) at sentinel 0.5. Slot 373 reset on fold boundary; aux dir-acc EMA either didn't fire or settled within ε of 0.5 → kernel kept early-returning. Fix: drop slot 373 dependency entirely. Drive temperature from observed hold-rate vs target overrun: overrun = max(0, observed_hold_rate - target_hold_rate) overrun_norm = clamp(overrun / max(target, 0.01), 0, 1) new_temp = TEMP_MIN + (TEMP_MAX - TEMP_MIN) × overrun_norm blended_temp = Welford EMA α=0.05 with Pearl-A bootstrap When over-holding: temp HIGH → exit ramp permissive (matches design intent). When at/under target: temp LOW → exit penalty strict. Survives fold reset: hold-rate measurement starts fresh with real data immediately, no chained-input-sentinel masking. Slot 330 (KELLY_WARMUP_FLOOR) investigated and confirmed NON-BUG: producer behaves correctly per pearl_kelly_cap_signal_driven_floors cross-fold- persistence. floor=0 post-warmup is correct steady state. Behavioral tests: - sp16_phase1_min_hold_temp_climbs_with_hold_overrun - sp16_phase1_min_hold_temp_strict_when_at_target - sp16_phase1_min_hold_temp_strict_when_under_target - sp16_phase1_min_hold_temp_no_longer_reads_slot_373 Instrumentation: HEALTH_DIAG[N]: min_hold_temp_diag obs/target/overrun/temp. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fb24614f07 |
feat(sp16-p0): Q-by-action HEALTH_DIAG diagnostic for Hold-bias observation
Per train-multi-seed-pfh9n post-mortem: structural-Q-bias hypothesis (Adam's m/sqrt(v) prefers low-variance Hold over noisy direction Q-targets) needs direct per-action Q observations to verify or refute. WR plateaued at ~0.46 across both folds while Hold% climbed 15% → 52% and Q-mean climbed 0.19 → 0.41 — but per-action Q was unobservable. Adds HEALTH_DIAG emit each epoch: HEALTH_DIAG[N]: q_by_action [hold=X long=Y short=Z flat=W] Reads via host-side averaging of `q_out_buf [B, total_actions]` direction-branch columns [0..b0=4]. No new kernel needed — q_out_buf is already populated row-major by compute_expected_q. Modeled directly on the existing Task 0.3 magnitude-bucket diagnostic (same dtoh-and- average cold path). Action-index ordering canonical, see state_layout.cuh:123-126: DIR_SHORT=0, DIR_HOLD=1, DIR_LONG=2, DIR_FLAT=3. Emit slot order is [hold, long, short, flat] (Hold first because the hypothesis is about Hold's ascent dominating direction Q-magnitudes). New surface: - gpu_dqn_trainer.rs: q_dir_means_cached field + update_q_dir_means_cached method + q_direction_action_means accessor + free static helper compute_q_dir_means_from_host_buf (factored for testability). - fused_training.rs: FusedTrainingCtx wrappers parallel to q_mag pair. - training_loop.rs: emit block adjacent to q_var_per_branch. Behavioral test: sp16_phase0_q_by_action_diagnostic_reads_four_action_means seeds known per-column constants, asserts canonical-dir-idx → emit-slot mapping at 1e-3 tolerance, and includes sentinel-leak guard (non-direction columns at 99.0 fail any wrong index→slot mapping with margin >12). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |