Commit Graph

488 Commits

Author SHA1 Message Date
jgrusewski
746b8b6754 docs(tlob): feature-diff doc for TLOB 51 vs OFI 20 — Phase A
Compares the 51-feature TLOBFeatureExtractor (crates/ml-supervised/src/tlob/
features.rs + mbp10_feature_extractor.rs) against Foxhunt's 20-slot OFI
vector persisted in .fxcache.

Findings: of TLOB's 51 slots, 23 are placeholders (sine waves / hardcoded
constants), 12 duplicate existing OFI or 42-dim market features, and only
~10 carry genuinely novel information. Meanwhile the existing
MicrostructureState struct in ml-features already computes 10 features
(realized variance, Hawkes intensity, weighted book pressure, spread
dynamics, aggression ratio, queue-depletion asymmetry, order-count flux,
intra-bar momentum, regime score, OFI trajectory) that are dropped before
reaching fxcache — these dominate the TLOB novel candidates and require
zero new math.

Phase B plan (drafted inline): bump OFI_DIM 20→28, bump FXCACHE_VERSION
4→5, prioritize persisting the 10 Foxhunt-internal MicrostructureState
features before any TLOB-derived additions, sweep all hard-coded 18/20
constants across CUDA kernels + gpu_dqn_trainer.rs, and rely on the
ensure-fxcache Argo step for cache regeneration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:12:01 +02:00
jgrusewski
b8cd4e1d94 diag+docs(dqn): trunk-slice grad decomposition + stale IQN-trunk doc fix
Extends grad_decomp_kernel to snapshot the trunk tensor slice (tensors
0..4 = w_s1, b_s1, w_s2, b_s2) in addition to the existing direction +
magnitude branch slices (8..12 / 12..16). Adds a new HEALTH_DIAG group:

  grad_trunk [iqn=<abs> ens=<abs> c51=<abs> cql=<abs> distill=<abs>
              rec=<abs> pred=<abs> cql_sx=<abs> c51_bs=<abs>]

Prior grad_split_bwd / grad_split_aux groups report mag_norm / dir_norm
ratios per loss component, computed over branch-head tensors only. That
measurement range structurally reports 0.0000 for any loss component
that writes exclusively to the trunk — IQN and Ens in particular. This
caused the persistent misdiagnosis that IQN-to-trunk was not wired; the
prior scoping in /tmp/foxhunt_research/iqn-to-trunk-wiring-scoping.md
confirmed the wiring is live (apply_iqn_trunk_gradient at
gpu_dqn_trainer.rs:4882) and that the zero reading was a blind spot in
the measurement pipeline.

Smoke confirms the diagnostic: after iqn_readiness ramps up (late
epochs), grad_trunk reports iqn=100..381 (real trunk SAXPY amplitude),
ens=0.07..3.57, c51=2.46..8.91 (value-head dueling path contributes
through trunk), while cql/cql_sx/distill/rec/pred stay near-zero — a
clean diagnostic baseline.

Also fixes stale documentation at dual-distributional-c51-iqn-design.md
that claimed "IQN trains in isolation — its gradients don't flow back
to the shared trunk": reworded to reflect current wired state with
file:function citation and explicit iqn_readiness gating note. Updated
the "What Changes" table ("IQN training") and "Implementation Order"
(Phase 1 marked DONE) with the same citation.

Changes:
  - grad_decomp_kernel.cu: per-component result slot 2 → 3 floats
    (mag_norm, dir_norm, trunk_norm); extra __shared__ sum_trunk +
    tree reduction; new grad_trunk_start/trunk_len kernel args.
  - gpu_dqn_trainer.rs: pinned result buffer 18 → 27 floats; snapshot
    now does two copy_f32 passes (trunk → dst[0..trunk_len), branch →
    dst[trunk_len..]); per-component slot offsets 0/2/… → 0/3/…;
    grad_component_norms_trunk cached field + accessor; compute trunk
    range from padded_byte_offset(&param_sizes, 0..4).
  - fused_training.rs: grad_trunk_norms_by_component() + per-component
    grad_trunk_*_abs() accessors.
  - training_loop.rs: HEALTH_DIAG emits new grad_trunk group ordered
    [iqn ens c51 cql distill rec pred cql_sx c51_bs]; extended doc
    comment explaining the three groups' roles.
  - design spec (Problem #1 + What Changes row + Implementation Order):
    stale "IQN trains in isolation" replaced by current wired-state
    description, cites gpu_dqn_trainer.rs:4882 and readiness ramp at
    gpu_dqn_trainer.rs:4228-4243.

Pure diagnostic — no training dynamics change, no atomicAdd, no tuning
knobs, no TF32 changes. Smokes unaffected (magnitude_distribution H10
regression pre-exists on HEAD 810b3c570; 4 other smokes pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 22:03:20 +02:00
jgrusewski
a9a51e8fa0 cleanup+fix(reward): Task 2.4 R6 relocation + Task 2.5 Bug #6 docstring
Task 2.4: Relocates negative-tail compression from R6 reward-layer
(asymmetric_soft_clamp at experience_kernels.cu:78-81) to C51 Bellman
target smoothing (c51_loss_kernel.cu::block_bellman_project_f).
Functionality preserved — same invariant, better location. Upper +10
cap kept inline as fminf(reward, 10.0f) for numerical safety.

Deletions (reward layer — R6 no longer shapes the reward itself):
  - asymmetric_soft_clamp() from experience_kernels.cu:78-81 (no callers)
  - Reward-layer clamp replaced with fminf(reward, 10.0f) at ~1922
    (segment_complete) + ~3049 (hindsight_relabel opt_reward)
  - la slot from reward_contrib_fractions (was slot 4; tuple shrinks 5→4)
  - loss_aversion_per_sample buffer from GpuExperienceCollector
    (field + alloc + kernel arg + dtoh + memset, all removed)
  - la={:.3} field from HEALTH_DIAG reward_contrib format string
  - loss_aversion assertion from reward_component_audit smoke test
  - loss_aversion comment reference in raw_returns comment block

Additions (gradient layer — R6 invariant moves here):
  - Huber-style `if t_z < 0 { t_z = -10*(1-exp(t_z/10)); }` in
    c51_loss_kernel.cu::block_bellman_project_f BEFORE v_min/v_max clamp
  - Inline kernel comment documenting the relocation rationale
  - Track 2 triage doc updated: R6 verdict DELETE → DELETED / RELOCATED
    with landed-relocation notes (both call sites + C51 Bellman edit)

Task 2.5 Bug #6: Stale `patience_mult` docstring at
experience_kernels.cu:1144 referenced the defunct R7 V8 reward (deleted
in Task 0.8). Rewrote the reward-shape docstring to reflect current
post-V7 / Task 0.8 reality (sparse = 2.0 * vol_normalized_return, capped
inline) and notes the R6 relocation. Per feedback_trust_code_not_docs.md.

Per feedback_no_functionality_removal.md: R6's invariant is RELOCATED,
not deleted. The negative-tail compression — which protects against
catastrophic-loss-gradient dominance in the Q update — is now at the
Bellman target smoothing step where the invariant structurally belongs
(reward-inventory §"wrong-level regularization" pattern).

Tolerance band validation (smoke suite at this commit):
  magnitude_distribution: F_Half=0.150 F_Full=0.237 (≥0.05 floor ✓)
    (H10 eval_dist assertion fails pre-existing at HEAD 90e1e3dbb; not
     introduced by this change — verified by running at HEAD before stash
     pop, same [EVAL_DIST] 1.000/0.000/0.000 collapse.)
  reward_component_audit: cf_flip=0.584 trail=0.304 (cf_flip≥0.1 ✓, PASS)
  controller_activity: [CTRL_FIRE] anti_lr=0.000 tau=0.000 gamma=0.000
    clip=0.400 cql=0.000 cost=0.000 (PASS)
  exploration_coverage: entropy @ep5=0.988 @ep20=0.985 (PASS)
  multi_fold_convergence: Best Sharpe 81.54/38.82/84.18 (≥20 floor ✓)
    best_val_metric 0.043/0.024/0.049 (baseline was 0.028/0.018/0.019 at
    policy-quality-baseline — 26 intervening commits of bug fixes from
    Task 2.5 bugs #1–#7 would account for persistent drift; within
    run-to-run variance of HEAD-pre-change)
2026-04-22 16:56:36 +02:00
jgrusewski
f9286e938d docs(policy-quality): Task 2.X scoping — per-bin variance + Q-spread regularization
Scoping output from the Full=0% investigation (post-Task-2.2 smoke showed
eval_dist [eq=0.580 eh=0.420 ef=0.000] — Quarter+Half recovered,
Full still 0%).

Root cause: Q-estimation bias (category C) — C51 expected-Q over atoms
systematically under-prices higher-variance bins. Full has 4x Quarter's
PnL variance per experience_kernels.cu risk scaling. Bias is already
documented at experience_kernels.cu:904 (commented as "structural
distributional bias — tight return distributions (Small) get higher
expected Q under the C51 softmax regardless of actual expected returns").

Not noise-starvation: Full picked on 32% of training samples across
60 epochs × 3 folds = ~61k Full samples. Ample data.

Proposed Task 2.X = Rank 1 + Rank 2 combined (~90 LOC):
  Rank 1: per-bin variance weighting in C51 branch loss — amplify
          Full's gradient by sqrt(var_f / mean_var)
  Rank 2: Q-spread regularization — lambda * ReLU(target_spread -
          Q_spread)^2 penalty preventing degenerate fixpoint

Alternative fixes ranked but not recommended:
  Rank 3 per-magnitude reward shaping (~80 LOC) — only if data-disfavor
         is confirmed at L40S
  Rank 4 magnitude curriculum (~20 LOC) — rejected (noise-starvation ruled out)
  Rank 5 state-vector enrichment (~150 LOC) — premature

Prerequisite: ~40 LOC of per-magnitude win-rate + realized-variance
instrumentation to confirm category C vs A at L40S scale.

L40S gating recommendation was:
  ef >= 0.15 at L40S → Task 2.X not needed (smoke artefact)
  0.05 <= ef < 0.15 → Task 2.X scoped but optional
  ef < 0.05 → Task 2.X required

Per feedback_fix_aggressively.md (landed this session): we ship Task 2.X
ahead of L40S since the bias is already code-documented, the fix is
well-scoped, and the 90-LOC cost is reasonable. L40S validation (Task
2.8) will verify the fixed state end-to-end instead of the un-fixed one.

No deletions anywhere per feedback_no_functionality_removal.md.
2026-04-22 11:43:17 +02:00
jgrusewski
7b74290dd0 docs(dqn): R5 micro-reward — documented intentional disable (Phase 2 Task 2.3)
Per feedback_no_functionality_removal.md: R5 was originally scoped as
DELETE in the Phase 2 plan and the Track 2 triage because
reward_contrib[3] = 0.000 across 60 / 60 smoke epochs and
dqn-smoketest.toml sets micro_reward_scale = 0.0. Re-examination during
Phase 2 Task 2.3 rejected the DELETE path:

- dqn-production.toml already sets micro_reward_scale = 0.1, so R5 is
  load-bearing in production, not dead code. The 0.0 value in smoke is
  deliberate test-isolation (td_propagation / magnitude_distribution /
  reward_component_audit all want the sparse-reward TD path isolated).
- The state-vector OFI block at state[SL_OFI_START..SL_OFI_START+SL_OFI_DIM)
  = [42..62) provides representation features for the encoder (policy
  side). R5 is a per-bar reward gradient on the critic (critic side).
  Different mechanisms — production deploys both together.
- R5 also reads PREV_MID (retrospective hold quality), which is NOT in
  the state vector. That signal exists only in the kernel branch.

Changes — pure documentation, no behavior change:
- experience_kernels.cu: ~30-line comment block at the R5 wiring site
  (~L1915) documenting the parameter-not-flag status, production vs
  smoke values, why state-vector OFI is complementary not redundant,
  and the feedback_no_functionality_removal.md seal.
- experience_kernels.cu: fix stale kernel-signature comment that claimed
  OFI was at state[66..74). Correct range is [42..62) per state_layout.cuh.
- config.rs: extend DQNHyperparameters::micro_reward_scale docstring and
  add a comment at the Default impl pointing back at the kernel site.
- gpu_experience_collector.rs: extend reward_contrib_fractions docstring
  to mark the micro=0.000 slot as a SEMANTIC value when the loaded profile
  has micro_reward_scale=0.0, not a wiring regression.
- track2-triage.md: R5 verdict changed from DELETE to FIX-documented-disable
  with the rationale above; "Proposed Phase 2 changes" section 1 and
  "Next Track 2 steps" updated accordingly.

Smoke tests: 3 / 4 pass (reward_component_audit, controller_activity,
exploration_coverage). magnitude_distribution is failing on baseline
HEAD c0fee5a9b as well — pre-existing H10 magnitude-head collapse
regression (eval dist_mag ef=0.000, eh=0.000), unrelated to R5.
Verified via git-stash round-trip: baseline fails, changes do not
introduce new failures.

Closes Track 2 R5 verdict (originally DELETE, now FIX-documented-disable).
2026-04-22 11:39:27 +02:00
jgrusewski
d1068de2b8 plan(policy-quality): reframe DELETE tasks per no-functionality-removal rule
User directive: "we don't remove functionality at all, we fix what's
broken". Standing rule captured in
~/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_no_functionality_removal.md.

Phase 2 plan reframed:
  * Task 2.3 (R5 micro-reward): was DELETE; now FIX — either (a) set a
    non-zero scale that delivers real signal, or (b) document why it's
    intentionally disabled while preserving the code path.
  * Task 2.4 (R6 loss-aversion): unchanged — the relocation to C51
    Bellman target is consolidation, not removal. Invariant preserved.
  * Task 2.6 (E4 entropy-reg): was DELETE-CANDIDATE; now TUNE — if the
    signal doesn't reach magnitude, re-route it rather than remove.
  * Task 2.7 (C4 grad-clip): was DELETE-CANDIDATE; now
    KEEP-AND-DOCUMENT — gradient clipping is a safety feature; run the
    ablation for diagnostic, not for removal.
  * H9 delete-magnitude-branch fallback: rejected permanently. On
    magnitude convergence failure the fallback is per-magnitude reward
    shaping / per-bin advantage weighting / curriculum / state enrichment.

Task 2.5 wiring-bug sweep unchanged (correctness fixes, not removal).

This commit updates only the planning narrative. Task execution hasn't
started on 2.3/2.6/2.7 yet, so no code changes required here. When
those tasks run, they will implement the fix-paths, not the
delete-paths.
2026-04-22 11:18:28 +02:00
jgrusewski
7e78bf4f85 plan(policy-quality): Phase 2 pivot — H4 REJECTED by Task 2.0 data
Task 2.0 instrumentation (commits d60e5375a / 980f3b07f / 41b0c559c)
revealed two silent bugs in Task 0.4's grad_ratio_mag_dir accessor:

  Bug A — readback size mismatch: pinned buffer allocated at
  total_params, but grad_buf length is total_params+cutlass_tile_pad,
  so size check always failed → Err silently coerced to 0.0 by proxy.

  Bug B — readback timing wipe: estimate_avg_q_value_with_early_stopping
  in process_epoch_boundary replays the forward graph, which zeros
  grad_buf. Any subsequent readback sees all zeros.

Both fixed in 41b0c559c; snapshot hoisted to top of process_epoch_boundary.

With those bugs fixed, the measured gradient ratio is NOT 0.0000 — it is
50–400× mag/dir across 60 epochs. Magnitude branch is over-fed, not
starved. Direction gradient is small but non-zero (~2e-2 to 7e0).
Direction policy is observably healthy (Short/Hold/Long/Flat 38/12/42/14%).

Track 1 triage's H4 CONFIRMED verdict was a measurement artefact
produced by Bugs A+B. H4 as originally defined is now REJECTED.

Plan changes:
  - Add new "Task 2.0 findings" section after cross-cutting concerns,
    documenting bug A, bug B, observed data, and revised strategy.
  - Task 2.1 marked DEFERRED (not deleted — kept for reference).
  - Task 2.2 promoted to PRIMARY fix.
  - New fallback: H9 delete-magnitude-branch as replacement for Task 2.1
    if Task 2.2 alone is insufficient.
  - Task 2.0 inventory row updated with LANDED status and commit chain.

Net effect: Phase 2 simplifies. The hardest task (2.1 three-branch
architectural fix) is skipped; primary path is Task 2.2 (~25 LOC).
2026-04-22 11:13:48 +02:00
jgrusewski
5a4d561451 plan(policy-quality): Task 2.0 revised approach — in-graph pinned snapshots
First Task 2.0 dispatch escalated BLOCKED: the four loss-component
backward kernels are captured inside the fused training graph, so
host-side snapshot-between-components isn't possible mid-graph without
a force-ungraphed diagnostic step (~210 LOC + cross-stream sync risk).

Revised approach (chosen after cost analysis):

  cudaMemcpyAsync(device → pinned host) IS captureable in a CUDA graph.
  Even better: DtoD into per-component scratch buffers, then an in-graph
  reduction kernel computes per-component (mag_norm, dir_norm) and writes
  8 floats to a pinned result slot. Only the 8-float result crosses
  PCIe (at epoch boundary), keeping per-step PCIe traffic to zero.

Changes to the plan's Step 2 + Step 3 + Step 4:
  - Step 2: added 4 device-side scratch buffers (one per component,
    ~10 MB each = 40 MB device) + 8-float pinned result slot + new
    reduction kernel grad_decomp_kernel.cu spec'd out.
  - Step 3: clarified that DtoD snapshot + backward + reduction kernel
    are ALL captured in the graph; graph replays them every step;
    no force-ungraphed dance needed.
  - Step 4: added refresh_grad_component_norms() accessor that reads
    the 8-float pinned slot at epoch boundary (zero-copy) and populates
    the host-side cache.

Approach matches Task 0.4 pattern (commit bb42c9963) extended four-fold.
No atomicAdd (reduction uses shared-mem tree), no non-captured replay,
no cross-stream sync risk.

LOC estimate: ~90 (was ~210 for the rejected option).
2026-04-22 09:46:17 +02:00
jgrusewski
2a54edc92d plan(policy-quality): Phase 2 plan refinements (tolerance bands + decision tables)
Three polish items from the plan review:

1. Task 2.4 Step 5 — replaced vague "must not regress" with a numeric
   tolerance-band table: multi_fold best_val_metric ±15% per fold,
   Best Sharpe ≥ 20 floor, and hard F_Half/F_Full ≥ 0.05 + cf_flip ≥ 0.1
   BLOCKERS. Anchors to the baseline metrics doc values captured at
   policy-quality-baseline.

2. Task 2.6 — converted the ad-hoc text decision at Step 1 into the same
   five-row decision table format Task 2.1 uses (DELETE / KEEP / KEEP-as-
   safety-net / INCONCLUSIVE / DELETE-because-inseparable). Step 2
   ablation got its own 4-row outcome table keyed to ent_mag delta,
   multi-fold Sharpe regression, and NaN appearance.

3. Task 2.7 Step 1 — added "Repeat 3× with different seeds" clause and
   explicit total wall-clock note: ~75 min (3 × 25 min) for the ablation
   pass before the Step 2 decision. Aligns with the Cross-cutting concern
   #6 about sample-noise rejection.

No task count change (still 11: 2.0–2.10). Net code-delta estimate
unchanged. Standing-rule compliance unchanged (no stubs / no atomics
/ no quickfixes / no hiding / no feature flags / no push-per-task).
2026-04-22 09:29:16 +02:00
jgrusewski
1ce99efc53 plan(policy-quality): Phase 2 implementation — synthesis of 4 tracks
Consolidates Phase 1 triage findings from Tracks 1-4 into an 11-task
Phase 2 plan at docs/superpowers/plans/2026-04-21-policy-quality-phase2.md.

Task inventory:
  - 2.0  Per-component gradient decomposition (H4 keystone diagnostic)
  - 2.1  H4 fix — magnitude-head gradient starvation (decision tree from 2.0)
  - 2.2  H10 fix — stable argmax tie-break at eval
  - 2.3  DELETE R5 micro-reward
  - 2.4  DELETE R6 loss-aversion; relocate neg-tail to C51 target smoothing
  - 2.5  Wiring-bug sweep (7 bugs: C1 fire, epsilon gen_range, if !true,
         sigma_mean scale, fold-boundary reset, stale docstring, C5 ISV null)
  - 2.6  E4 entropy-reg DELETE-or-KEEP (data-driven, post-2.0)
  - 2.7  C4 adaptive grad-clip ablation + DELETE-or-KEEP
  - 2.8  L40S validation run — all 4 tracks re-measured
  - 2.9  Mandatory-gate verification + phase3-results.md
  - 2.10 Tag policy-quality-phase2-complete (and policy-quality-v1 if soft pass)

Matches Phase 0/1 plan formatting (checkbox steps, concrete file paths,
code snippets, bash commands, per-task commit templates). References
project standing rules (no quickfixes, no stubs, no atomic-adds on hot
paths, no feature flags, no hiding errors) and the pinned-readback
pattern from Task 0.4 (commit bb42c9963).
2026-04-22 09:23:52 +02:00
jgrusewski
0611d32b06 docs(policy-quality): Track 3 controllers audit (Phase 1) — 0 load-bearing, 6 diagnostic, 1 candidate-for-delete
V7 audit of the 7 adaptive controllers named in spec §5.3 against the
baseline controller_activity smoke run (3 folds × 20 epochs = 60 epochs,
intervention-based fire detection per commit ed4b30b49):

* C1 anti_lr: DIAGNOSTIC (0/60; wiring surprise — fire detector reads
  base scheduler, not post-anti_lr LR, flagged for Phase 2 fix)
* C2 adaptive tau: DIAGNOSTIC (2/60; fires at fold boundaries only)
* C3 adaptive gamma: DIAGNOSTIC (1/60; pinned to floor by health-coupled
  correction; re-measure at L40S when health is real)
* C4 adaptive grad_clip: CANDIDATE FOR DELETE pending ablation (12/60,
  20% — above diagnostic, below load-bearing; needs Track 3 Step 2)
* C5 cql_alpha: DIAGNOSTIC (2/60; regime_stability gate uninformative
  at smoke scale)
* C6 cost_anneal: DIAGNOSTIC by design (deterministic sigmoid; fire_cost
  hard-coded false at training_loop.rs:2475)
* C7 base LR scheduler: DIAGNOSTIC by construction (pure open-loop, not
  in the closed-loop controller battery)

Cross-controller finding: no controller fires in > 50% of epochs, so
the policy is not currently held on the rails by adaptive intervention
at smoke scale. However, C2 / C3 / C5 are structurally inert here —
their trigger signals (health, regime_stability) are degenerate at
smoke scale, so their 0-ish fire rates are lower bounds, not
representative. Promote to final after L40S validation.

Wiring surprise (anti_lr): fire_lr detection reads lr_scheduler.get_lr()
*before* the anti_lr multiplier applies. Under smoke Constant LR this
reports 0 correctly-for-the-wrong-reason; under L40S Cosine/Linear it
will false-positive on pure decay drift. Recommend detecting via the
anti_mult != 1.0 branch directly (training_loop.rs:2936–2942) for an
unambiguous intervention semantic matching C4/C6.

Follow-ups flagged for Phase 2:
- Fix C1 fire-detection wiring before L40S re-run
- Run C4 grad-clip ablation (25 min, gates the final C4 verdict)
- Optional: reset fire_counts per fold to remove C2/C5 boundary artefact

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:09:09 +02:00
jgrusewski
207fce8778 docs(policy-quality): Track 2 reward audit (Phase 1) — 2 DELETE, 3 KEEP
V7 audit of reward terms R1-R8 against the baseline smoke run
(magnitude_distribution.rs, 3 folds x 20 epochs = 60 HEALTH_DIAG rows):

* R1 step_return: KEEP (base reward, denominator)
* R2 PopArt drift: PENDING (warmup-gated zero at smoke; re-check at L40S)
* R3 CF-flip: KEEP (49-74% contribution, dominates shaping)
* R4 trail_r: KEEP-WITH-CAVEAT (fold-3 dominance, trade-volume gated)
* R5 micro-reward: DELETE (micro_reward_scale=0 in smoke, intended)
* R6 loss-aversion: DELETE (sub-1% in 54/60 epochs, relocate protection)
* R7 segment-patience: ALREADY-REMOVED (stub deleted 83d524f86)
* R8 raw_returns_out: KEEP (load-bearing for Sharpe/MaxDD pipeline)

Cross-term finding: R3 is the dominant reward shaper at smoke scale;
every other shaping term is <=3% of reward magnitude except R4 in fold
3. Confirms Track 1's H4 gradient-starvation diagnosis is compounded
by thin shaping elsewhere in the reward landscape.

Follow-ups flagged for Phase 2:
- stale patience_mult docstring at experience_kernels.cu:1049
- R5 deletion gated on TD-propagation diagnostic (Task #8)
- R6 relocation to Q-target smoothing in C51 Bellman
2026-04-22 09:06:50 +02:00
jgrusewski
5c70c68a15 docs(policy-quality): Track 1 magnitude triage (Phase 1) — H4 + H10 CONFIRMED
Preliminary triage of spec §5.1 hypotheses H1–H10 using the Phase 0
baseline capture on RTX 3050 Ti. L40S validation pending per plan.

Verdicts:
  H1  PENDING     (needs forced-exploration instrumentation not yet wired)
  H2  REJECTED    var_scale=0.96 across 19/20 epochs; Var[Q] inactive at smoke scale
  H3  INCONCLUSIVE kelly degenerate (insufficient win/loss counts at smoke scale)
  H4  CONFIRMED   grad_ratio_mag_dir=0.0000 across 20/20 epochs (threshold <0.1)
  H5  REJECTED    ent_mag stays ≥0.98 throughout; no bootstrap collapse
  H6  REJECTED    Full fire rate (0) is lower than Quarter fire rate, not higher
  H7  REJECTED    vsn and sigma symmetric between mag and dir branches
  H8  REJECTED    target-net drift equal (mag=dir=0.001)
  H9  PENDING     (same instrumentation gap as H1)
  H10 CONFIRMED   training ent_mag=0.98, eval F_Quarter=100%

Synthesis: H4 is the root cause. Magnitude branch receives ~0 gradient →
weights stay near init → three magnitude Q-values near-identical → argmax
picks bin 0 (Quarter) on ties → H10 manifests at eval time. H2, H5, H7,
H8 all ruled out as contributors.

Proposed Phase 2 priority: fix H4 (gradient-flow path into magnitude head
— likely per-component advantage weighting or direction-conditioning of
w_b1fc) + H10 (Q-margin argmax + stochastic eval rollouts as safety net).

Phase 1 next: validate preliminary verdicts on L40S, instrument
per-component gradient decomposition for magnitude, proceed with
Tracks 2/3/4 in parallel.
2026-04-22 08:51:51 +02:00
jgrusewski
8ab368de58 docs(policy-quality): Task 0.17 baseline metrics — captured (Phase 0 complete)
All <PENDING> markers replaced with real values from a clean 20-epoch
magnitude_distribution smoke run + 3-fold multi_fold_convergence run
on RTX 3050 Ti at HEAD 0472b9730.

All 5 Phase 0 smoke tests PASS:
  magnitude_distribution  — Quarter=0.596 Half=0.150 Full=0.255
  reward_component_audit  — cf_flip=0.619 trail=0.290 la=0.013
  controller_activity     — all 6 controllers < 20% firing
  exploration_coverage    — ent_mag @ep5=0.987 @ep20=0.985
  multi_fold_convergence  — 3/3 folds, Best Sharpe 51 / 39 / 87

This commit closes Phase 0. The next commit tags it as
policy-quality-baseline — the reference state against which Phase 2
interventions are measured.
2026-04-22 08:42:50 +02:00
jgrusewski
e1ac2c7578 docs(policy-quality): Task 0.17 baseline metrics scaffold (PENDING GPU run)
Phase 0 instrumentation is complete (Tasks 0.4 / 0.5 / 0.8 / 0.10 / 0.16
shipped this session; 0.6 partial; 0.3 partial). This scaffold encodes
the full metric set with explicit <PENDING …> markers per row so the
GPU-run capture can fill in values without forgetting any HEALTH_DIAG
field.

The policy-quality-baseline tag is intentionally NOT applied yet —
tagging requires real captured values, not the scaffold. Runner workflow:

  for t in magnitude_distribution reward_component_audit \\
           controller_activity exploration_coverage multi_fold_convergence; do
    SQLX_OFFLINE=true CUBLAS_WORKSPACE_CONFIG=:4096:8 \\
      FOXHUNT_TEST_DATA=test_data/futures-baseline \\
      cargo test -p ml --release --lib -- \$t --ignored --nocapture 2>&1 | tee out_\$t.log
  done

Then edit this doc, replace markers with values, amend, and tag.
2026-04-21 23:25:02 +02:00
jgrusewski
944fecd913 docs(policy-quality): main-branch workflow — cut feat/wip branches
User preference: everything lands on main, ~500 LOC is small enough
that feature-branch isolation isn't needed.

Spec changes:
  * §2.3 outcome paths: drop "unmerged branch" vocabulary
  * §3 architecture: main-branch timeline, no feat/, no wip/
  * §4 Phase 0: commits on main, tag policy-quality-baseline at end
  * §5 Phase 1: experiments are worktree edits reverted after, only
    triage docs commit (to main)
  * §6 Phase 2: commits land on main, author chooses granularity,
    each leaves smokes green
  * §7.3 outcome handling: iterate on main, baseline tag for rollback
  * §7.4 closing: tag policy-quality-v1, update status

Plan changes:
  * Task 0.1: verify-starting-state (not branch creation)
  * Task 1.x: temp-edit-then-revert (not wip branches)
  * Task 1.5: triage docs commit directly to main
  * Phase 4: tag + close (not merge + cleanup)
  * Rollback: one git reset --hard command
2026-04-21 21:06:33 +02:00
jgrusewski
1456094a5a plan(policy-quality): implementation plan for 4-track V7 audit
Produced by superpowers:writing-plans from the approved spec at
docs/superpowers/specs/2026-04-21-policy-quality-design.md.

Plan structure:
  * Phase 0 (tasks 0.1-0.17): measurement substrate — TDD-detailed,
    exact code + commands. Produces 6 new smoke tests + ~25 HEALTH_DIAG
    fields across 4 tracks.
  * Phase 1 (tasks 1.0-1.5): parallel investigation tracks — process
    tasks (run instrumentation, record evidence, classify hypotheses).
    No code to pre-write; output is 4 triage documents + a Phase-2
    planning doc.
  * Phase 2 (template only): convergence fix. Concrete tasks produced
    by a Phase-2-specific plan AFTER Phase 1 triage — can't pre-write
    the fixes without knowing which hypotheses confirm.
  * Phase 3 (tasks 3.1-3.4): L40S validation gates, surrogate-noise,
    branch decision (merge or iterate).
  * Phase 4 (tasks 4.1-4.4): merge via --no-ff, tag, cleanup.
  * Rollback procedure: git reset --hard policy-quality-baseline.
2026-04-21 20:56:45 +02:00
jgrusewski
ad677466c8 design(policy-quality): revision 4 — functional gaps
Addressed functional concerns from third review:

1. Surrogate-noise gate was statistically weak — "random Sharpe ≤ 0.5×
   trained Sharpe on same val data" could be satisfied by sample
   noise. Replaced with percentile-based test:
   * Pool all 6 val folds (~600 trades) for statistical power.
   * Run N=30 surrogate random-action rollouts (matched marginal
     action distribution — so difference is state-action mapping,
     not action frequency).
   * Assert trained pooled Sharpe > 95th-percentile of surrogate
     distribution. Explicit false-positive control.

2. Escalation path ("scope exceeds sub-project A v2") was hand-wavy.
   Now concrete: failure after 3 iterations → paused, not closed →
   triage spec opened with evidence summary + diagnosis (policy/
   reward/state/architecture/data bottleneck) + explicit decision
   (continue on feat/policy-quality or fork feat/policy-quality-v2).
   Triage itself is a full brainstorming cycle.

3. HEALTH_DIAG §4.2 Track 1 fields only covered H1–H5 — H6–H10
   needed their own detection signals. Added:
   - trail_fire_rate_{quarter,half,full}        (H6)
   - hold_time_at_exit_{quarter,half,full}      (H6)
   - vsn_mask_{magnitude,direction}             (H7)
   - noisy_sigma_{mag_head,dir_head}            (H7)
   - target_drift_{mag_head,dir_head}           (H8)
   - action_dist_eval_{quarter,half,full}       (H10)
2026-04-21 20:47:06 +02:00
jgrusewski
b5b70fdd8e design(policy-quality): revision 3 — internal consistency pass
Fixed inconsistencies between sections after the rev-2 feature-branch
refactor (sections had gotten out of sync):

1. §2 (success criteria) rewritten — was stale from rev 1. Now has
   clean mandatory/soft split matching §7.2, corrects the trade-count
   gate (per-fold not averaged), references the surrogate-noise gate.
   New §2.3 enumerates the outcome paths.

2. §4 and §6 no longer say "commit on main" — both land on
   feat/policy-quality per the feature-branch architecture. Phase 4
   merges to main at §7.4.

3. §7.3 outcome handling rewritten — was written for single-branch
   model ("Phase 2 commit is NOT reverted"). New wording matches
   feature-branch semantics: failing mandatory = don't merge; failing
   soft = merge + follow-up.

4. §6 smoke-tests rule now lists all six Phase 0 smokes, not just
   Track 1/2.

5. §7.4 Phase-4 merge-strategy added — --no-ff merge commit, tag
   policy-quality-v1, cleanup sequence documented.

6. §5.5 conflict-resolution added — tracks can reach contradictory
   conclusions (e.g. T1 says fix, T2 says delete). Resolution rules:
   mandatory-gate dominance, simplification wins, escalation.

7. §4.3 baseline metrics now committed to the feature branch (not
   "investigation-only"), matching the crash-safety discipline.

8. Budget / risk register aligned — 5.5–6.5 hrs plan + 1 buffer,
   hard-capped at 3 Phase-3 validation runs.
2026-04-21 20:44:09 +02:00
jgrusewski
5552517f61 design(policy-quality): caveat Criterion A for H9 (delete-magnitude-branch) case
If H9 confirms and Phase 2 removes the magnitude branch, F_Half/F_Full
gates become vacuous. Substitute: direction bin distribution healthy
(F_Short + F_Long each >= 20% of non-Flat actions). Preserves the
'model actually trades' intent of Criterion A.
2026-04-21 20:40:10 +02:00
jgrusewski
26be2b8abe design(policy-quality): revision 2 — full 4-track V7 + outside-the-box
Addresses critique from second self-review:

1. SCOPE GAP fixed: spec now covers all 4 V7 categories the user asked
   for. Added Track 3 (controllers) and Track 4 (exploration) with their
   own smoke tests (controller_activity, exploration_coverage) and
   HEALTH_DIAG fields. Previous version only had action-space + reward.

2. H1-H5 expanded to H1-H10 with 5 new codebase-specific hypotheses:
   - H6: Regime-adaptive trailing stop closing Full-positions early
   - H7: VSN / NoisyNets masking magnitude features
   - H8: Target tau too slow for magnitude propagation
   - H9: Data genuinely favors Quarter (no bug, delete magnitude branch)
   - H10: Entropy regularization + argmax tie ordering bias

3. H3 REFRAMED from "Kelly bug" to design critique — Kelly as hard
   multiplier vs soft gate. Fix sketch updated to offer replacement.

4. Feature branch + tag strategy replaces "single branch" containment.
   feat/policy-quality isolates work from main until Phase 4 merge via
   PR. git tag policy-quality-baseline gives named rollback anchor.

5. SURROGATE-NOISE CHECK added as MANDATORY validation gate — catches
   look-ahead/leakage bugs that would otherwise produce false-positive
   edges. Permanent smoke artifact.

6. Validation gate restructured: B + surrogate-noise MANDATORY;
   A criteria soft; controller-activity soft. Partial success path
   explicit per-gate.

7. Risk register expanded — added scenarios for B-mandatory failure
   (feat branch stays unmerged), all-10-hypotheses-REJECTED (fall back
   to H9 delete-branch), surrogate-noise failure (halt spec, open
   diagnostic spec), multi-load-bearing-controllers (tech debt doc).

8. Budget corrected to 5.5-6.5 hrs L40S (was underestimated 3.5-4),
   with per-track breakdown.

Spec now: 464 lines, 10 sections, covers 4 V7 tracks × ~10 hypotheses
+ 8 reward terms + 7 controllers + 5 exploration mechanisms = ~30
audit items total.
2026-04-21 20:39:34 +02:00
jgrusewski
bee81b4967 design(policy-quality): self-review revisions
Critical fixes after honest review:
  1. reward-audit measurement: split into 3 classes (additive / transform
     / sample-selector) — one-size-fits-all metric couldn't work for
     PopArt normalization or CF flip mirroring
  2. H1 detection: replace undefined "Sharpe/state" with concrete
     forced-exploration protocol (epsilon=1.0 for 1 epoch at mid-train,
     sample states × magnitudes, compare Sharpe-per-trade)
  3. H4 scope: clarify this is the per-action-branch gradient (direction
     vs magnitude head), NOT the per-loss-component (IQN/CQL/C51/Ens)
     budget. Fix sketch updated to target the correct knob.
  4. Validation gate: criterion B (multi-fold Sharpe + WinRate) is now
     MANDATORY; criterion A gates are soft. Prevents "partial success"
     hiding a policy-quality failure.
  5. Trade-count gate: per-fold ≥100 on ≥5/6, not an average — 5 lean
     folds pooled with 1 fat one should not pass.

Minor:
  - Budget trimmed to 3.5–4 hrs L40S (was 6–10)
  - L40S pool made explicit as target
  - Phase 1 branches now pushed as wip/* for crash safety
  - Hyperopt-params mismatch handling documented
2026-04-21 20:29:13 +02:00
jgrusewski
3c24c7f49e design(policy-quality): V7 audit + magnitude collapse fix spec
Sub-project A of the production-readiness roadmap. Single-branch,
phase-gated: measurement substrate → parallel investigation tracks
(magnitude collapse + reward V7 audit) → single convergence commit →
6-fold × 50-epoch L40S validation.

Target criteria at close:
  A (action dist): F_Half/F_Full each ≥15% on ≥4/6 folds, WinRate
    55-65%, ≥100 trades/val window
  B (multi-fold):  Best Sharpe > 10 on ≥5/6 folds, WinRate > 55% on
    ≥5/6 folds

Greenfield checkpoint policy — any action-space or weight-shape change
is allowed. All confirmed bugs fix together in one Phase-2 commit.
V7 discipline gates inclusion (measurement confirms the bug), no
ranking across findings.

Out of scope for this spec: paper trading (sub-project C), canary
deployment (sub-project D), hyperopt re-runs, architectural changes
beyond action space.

Awaiting user review before invoking writing-plans.
2026-04-21 20:25:56 +02:00
jgrusewski
69211af40b phase3(env-unification): full unification — all 3 kernels now call unified_env_step_core
Completes Phase 3 of the env-unification effort: experience_env_step (training),
backtest_env_step (val single-step), and backtest_env_step_batch (val batched)
all call the same __device__ helper `unified_env_step_core` in trade_physics.cuh.
Drift between train/val becomes structurally impossible at compile time — any
change to the canonical step applies atomically to both.

Three structural fixes were required to make the training port work:

1) TWO max_position params in unified_env_step_core
   Training scales `max_position` by Var[Q] (Kelly-from-atoms variance sizing)
   → `effective_max_pos`. The original training kernel used this for
   compute_target_position_4branch but the UNSCALED `max_position` for
   apply_kelly_cap and execute_trade. Initial port collapsed both into one
   helper arg, tightening Kelly cap aggressively → tiny positions → Q-values
   converge → q_gap=0.00 collapse. Split into `max_position_target` (scaled)
   and `max_position_physics` (unscaled). Val passes the same value for both.

2) Remove double hold_time update
   Helper now owns `update_hold_time(...)` inside the step. Training's
   inline `hold_time = update_hold_time(...)` at line ~1574 was a double-
   increment. Fix: capture `saved_hold_time` BEFORE helper for the
   patience-multiplier in segment reward, then trust the helper's in-place
   update of hold_time. Segment-hold-time semantics preserved.

3) Remove triple Kelly-stat update
   Helper calls `record_kelly_trade_outcome(...)`. Training ALSO had Kelly
   stat updates inside the reversing_trade block (line 1558) and the
   exiting_trade block (line 1638). TRIPLE update → win_count/loss_count/
   sum_wins/sum_losses inflated 3× → Kelly cap tightened proportionally →
   positions starved → Q-gap collapse. Fix: only the helper now touches
   Kelly win/loss/sum_wins/sum_losses. Training still updates its own
   sum_returns / sum_sq_returns (continuous-Kelly stats, distinct
   buffers not touched by the helper).

Smoke test result (RTX 3050 Ti, 20 epochs, single-trial):

  Before port:      Best Sharpe ~15-25  (varies, range 10-31)
  After broken port: Best Sharpe 1.17    (q_gap=0.00 collapse)
  After fix:        Best Sharpe 39.25   (HIGHEST ever recorded)
                    q_gap 0.00 → 0.27 (growing, healthy separation)
                    sharpe_ema trajectory: -5.3 → 12.7 → 26.9 (rising)

Multi-trial (partial visible): q_gap rising 0.00 → 0.86 by epoch 6,
sharpe_ema 15-25 consistently positive. Both tests passed.

Training kernel now has the CORE STEP in a single helper call — the
kernel body downstream continues to handle training-only concerns
(counterfactuals, plan_params, reward shaping via shaping_scale,
saboteur via exploration_scale, replay buffer writes).

Net −99 LOC across three kernels replaced by shared helper calls.

Files touched:
  crates/ml/src/cuda_pipeline/trade_physics.cuh      (+15/-5)
  crates/ml/src/cuda_pipeline/backtest_env_kernel.cu (+53/-108 net)
  crates/ml/src/cuda_pipeline/experience_kernels.cu  (+48/-102 net)
  docs/superpowers/specs/2026-04-21-unified-train-val-env-design.md
    (Phase 2 documented as helper-extraction; Phase 3 kernel-deletion
    remains deferred — both kernel entry points kept for their distinct
    threading models; the physics is what's now truly unified.)

Verified: cargo check + smoke test pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:04:51 +02:00
jgrusewski
2b14d9dc8b diag+gate: TD-propagation smoke test + entropy→plasticity gate + tx_cost doc
A) TD-propagation diagnostic smoke test (sparse rewards)
- New smoke_tests/td_propagation.rs captures training_sharpe_ema across
  20 epochs with micro_reward_scale=0 to answer: "can sparse-reward
  Q-learning extract policy from trade-completion P&L alone?"
- Asserts: finite sharpe_ema, no monotonic degradation (tolerance 0.1
  over first-3 vs last-3 epoch avg), q_gap > 0.05
- First run answered YES: val Sharpe peaks at +12.49 at epoch 8 with
  pure sparse rewards, confirming the objective is learnable. The
  remaining issue is stability/overfitting, not TD propagation.

B) Entropy → plasticity gate in training_loop
- D3/N3 shrink_perturb trigger now fires on `last_action_entropy < 0.3`
  OR `health_value < 0.3` (OR semantics, 3 consecutive epochs).
- Catches action-collapse cases the generic health metric misses: Q-values
  separate cleanly but argmax stays pinned to a single branch.
- tracing::info now logs both signals.

C) commitment_lambda coverage verified
- Almgren-Chriss sqrt market-impact already in compute_tx_cost
  (trade_physics.cuh:168-171). commitment_lambda was pure duplication.
- Inventory doc updated: P2 marked satisfied, table row status updated.

Files touched:
- crates/ml/src/trainers/dqn/smoke_tests/mod.rs             (+2)
- crates/ml/src/trainers/dqn/smoke_tests/td_propagation.rs  (new)
- crates/ml/src/trainers/dqn/trainer/training_loop.rs       (+15/-3)
- docs/superpowers/specs/2026-04-21-phase1-reward-inventory.md (+6/-3)

Smoke test PASSED locally (RTX 3050 Ti, 30.99s end-to-end).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 02:05:28 +02:00
jgrusewski
5462743e80 refactor(diag): relocate w_dsr/position_entropy intent to HEALTH_DIAG
Phase 2 P1 relocations — the intents behind two deleted reward shaping
terms now live at the correct layer: diagnostic monitoring, not reward.

1. training_sharpe_ema (was w_dsr reward input):
   Field already existed. Added to HEALTH_DIAG log line as `sharpe_ema`.
   Available for early-stopping, model selection, and exploration gating
   without perturbing the objective.

2. action_entropy (was position_entropy_weight reward):
   Computed from summary.action_counts at GPU summary download —
   Shannon entropy normalized to [0, 1] by ln(n_bins). One-epoch lag
   is fine for a diagnostic. Added to HEALTH_DIAG as `action_entropy`.
   Stored in new trainer field `last_action_entropy: Option<f32>`.

New HEALTH_DIAG suffix: `diag [sharpe_ema={:.3} action_entropy={:.2}]`.
Verified visible on E1 smoke test epoch 18/19 output.

Also: honest recalibration of Phase 2 results added to the inventory
doc. The earlier commit messages framed "-150 → -17 Sharpe" as a 5×
improvement; in absolute terms Sharpe -17 is still catastrophic (you'd
blow the account). Phase 2 stopped active capital destruction but did
not find alpha. Sharpe_raw per bar went from -0.39 (lot of loss per bar)
to -0.09 (little loss per bar) — still net losing. q_gap=0.17 is a
capacity metric (network CAN differentiate), not profitability.

Path to actual profitability (Sharpe > 0) remains:
- TD propagation verification (Task #8)
- Unified env kernel (Phase 3)
- Possibly a different objective entirely
- Richer features (42 market + 20 OFI may be information-starved)

The rule going forward: before celebrating future improvements, ask
whether the result *crosses zero* (profitable) or is just *less
negative* (still losing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 01:47:31 +02:00
jgrusewski
93c77b91b7 refactor(reward): delete 8 behavioral shaping terms in one sweep
Mass deletion of the "v7 gem" reward terms identified in the Phase 1
inventory as category errors — each one rewarded an outcome-adjacent
behavior instead of encoding the underlying physics, and each
empirically hurt validation metrics more than it helped training
stability.

Deleted from experience_kernels.cu and all plumbing (Rust configs,
launch args, hyperopt logs, TOML entries):

  * order_credit_weight        - reward redundant with compute_tx_cost
                                 (order_type_idx already differentiates
                                 fills by order type)
  * risk_efficiency_weight     - reward double-counted drawdown penalty
                                 asymmetrically (only on winners)
  * urgency_credit_weight      - reward was vol-normalized unrealized P&L,
                                 pure rename of core return
  * commitment_lambda          - triple-counted churn + tx_cost
  * w_dsr                      - kernel wrote DSR EMA but no longer added
                                 to reward (dead); removed the EMA
                                 bookkeeping too
  * dsr_eta                    - kernel arg for the deleted DSR EMA
  * position_entropy_weight    - rewarded action-bucket diversity
                                 regardless of outcome; histogram buffer
                                 + zero-init removed too
  * exit_timing_weight         - already inactive (used raw_next future
                                 price, comment-deleted earlier)
  * ofi_reward_weight          - dead plumbing; OFI already passed as
                                 feature through state[OFI_START..]
  * opportunity_cost_scale     - penalized flat when Q-gap wide;
                                 redundant with Q-values themselves

Kernel arg count: experience_env_step_batch shrank from ~55 to ~45 args.
Rust-side config surface reduced correspondingly.

Results on E1 smoke test (20-epoch):
  BEFORE any Phase 2 work:
    Val Sharpe -120 to -150, MaxDD 10-15%, Sharpe_raw -0.39
  AFTER reward_noise + Kelly (both envs) + urgency + this sweep:
    Val Sharpe      -17 to -22       (7× better)
    Val MaxDD       0.27%            (40× better)
    Val Sharpe_raw  ~-0.09           (4× better)
    Training Sharpe_raw  ~0          (stabilized from ±20 swings)
    Final q_gap     0.1712           (highest yet, collapse mechanism fine)

The extreme train-Sharpe swings (+17 one epoch, -13 next) were not
learning dynamics — they were shaping-term noise. Core reward (P&L +
drawdown + churn + holding + tx_cost + Kelly physics cap) gives training
metrics that actually reflect what the model does.

Inventory doc (docs/superpowers/specs/2026-04-21-phase1-reward-inventory.md)
extended with a "better-form taxonomy" section: every deleted gem has
a correct layer it belongs to (physics, feature, diagnostic, gradient-
level regularization — not reward). Kelly cap and Q-target smoothing
are already relocated; others are scheduled per the taxonomy's P1/P2/P3
priority list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 01:40:36 +02:00
jgrusewski
7b6641e038 docs(design): phase-1 inventory — gems/pearls/novels for env unification
Extends the reward inventory with a review pass that preserves the
*ideas* behind behavioral terms while deleting the wrong-level mechanics.

Gems (relocate, don't delete):
- Probabilistic fill model (real physics, not shaping) → apply in both
  modes via shared Philox RNG + exploration_scale
- Kelly-as-physics-constraint → hard position-size cap in trade_physics,
  not a soft reward
- Path quality via per-step drawdown integration → no separate term,
  just run drawdown penalty every bar
- Saboteur relocation → both modes, scaled by exploration_scale (training
  1.0, validation 0.5, diagnostic 0.0) — avoids creating a new clean-vs-
  noisy mismatch in the opposite direction

Pearls:
- Sparse reward is fine if TD propagation works — diagnose before
  deleting micro_reward_scale; measure cov(Q(s_entry,a), trade_return)
- Every behavioral term maps to one of four failure modes: double-count,
  misplaced physics, wrong-level regularization, or compensating for a
  downstream bug. None are semantically "neutral" shaping.

Novel architectural moves:
- Diagnostic-only entropy tracking (no reward, just HEALTH_DIAG
  logging)
- Single exploration_scale scalar replacing all mode toggles
  (feedback_no_feature_flags compliant)
- Layered reward with per-term budget caps — makes the train/val Sharpe
  gap computable instead of uncomputable magic
- Q-target smoothing replacing reward_noise_scale (regularization at
  gradient level, not reward level)

Updated disposition table: 4 DELETE, 2 MOVE, 2 RELOCATE-to-physics,
1 DIAGNOSE-then-DELETE, 4 delete-dead-plumbing.

Three relocations (Kelly cap, saboteur scaling, Q-target smoothing) can
land as independent PRs before the unified_env_kernel rewrite, shrinking
Phase 2's change surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:31:58 +02:00
jgrusewski
fc0a8a7966 docs(design): phase-1 reward inventory for env unification
Enumerates every term that contributes to training or validation reward,
classifies each as P&L-aligned (keeper) or behavioral (remover), with
source line references into experience_kernels.cu and backtest_env_kernel.cu.

Findings:
- 7 P&L-aligned terms to preserve in the unified env (core return,
  tx_cost, drawdown, inventory, churn, capital floor, + vol normalization)
- 8 active behavioral training-only terms to delete:
  * order_credit_weight  (rewards theoretical limit-order savings,
    double-counts actual tx_cost)
  * risk_efficiency_weight  (double-counts drawdown asymmetrically)
  * urgency_credit_weight  (redundant with core return)
  * kelly_sizing_weight  (rewards matching a formula, not outcomes)
  * micro_reward_scale  (OFI-momentum signal follower)
  * commitment_lambda  (triple-counts churn)
  * reward_noise_scale  (belongs at gradient level, not reward level)
  * position_entropy_weight  (changes what "optimal" means)
- 4 dormant/dead terms to clean up (w_dsr, exit_timing, ofi_reward,
  opp_cost_scale — all plumbing with no kernel effect)

Biggest offenders by magnitude: urgency_credit and risk_efficiency can
dominate the core signal on a single positioned bar.

Phase 2 (unified_env_kernel implementation) can now proceed with a
concrete deletion list, not a judgment call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:28:15 +02:00
jgrusewski
9223795c70 Merge adaptive-learning-rootcause: distillation collapse fix + env-unify design
Brings the 4-commit work from wip/adaptive-learning-rootcause into main:

  24f96ab78  fix(n1): distillation now actually pulls weights during collapse
  f21a7d661  test(e1): 20-epoch smoke test + raw-q_gap assertion + disable early-stop
  a3fd02a95  fix(early-stop): log real training epoch, not internal call counter
  9dbd8d7e9  docs(design): unify training and validation environments

Key outcomes:
- Distillation mechanism now actually pulls weights during collapse (fixed
  three bugs: epoch-boundary grad_buf erasure, CUDA-graph scalar baking,
  wrong q_gap signal at snapshot gate). E1 smoke test passes deterministically.
- Design doc lays out the next step: unifying training and validation env
  kernels so validation Sharpe tracks training Sharpe (currently diverges
  catastrophically by ~-150 absolute).
- Incidental: early-stopping log now reports the real training epoch
  instead of its internal call counter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:23:52 +02:00
jgrusewski
9dbd8d7e9f docs(design): unify training and validation environments
Design document for the follow-up to the adaptive-learning-rootcause
session. Lays out the evidence, root cause, options considered, and
recommended path for closing the 70% structural gap between training
and validation Sharpe that remained after the distillation collapse
fix landed.

Key findings documented:
- Reward-shaping ablation (2026-04-20) closed ~30% of the gap; ~70%
  remains architectural
- Two env kernels (experience_env_step vs backtest_env_step) have
  drifted: spread scaling, fill model, saboteur noise, reward terms,
  action selection, position dynamics all differ
- Hint from history: experience_kernels.cu:1418 comment "Regime-
  adaptive scaling removed to eliminate train/eval mismatch" shows
  someone aligned *some* things previously

Recommended path (Option C, "unified env with layered reward"):
- Single unified_env_step kernel replaces both
- Core reward = pure P&L; shaping is additive and P&L-units-aligned
- Validation = training with exploration_scale=0 AND shaping_scale=0
- Scale factors are pinned device-mapped scalars (same pattern used by
  the distillation alpha fix)

Phased implementation plan with ~8-day budget and concrete success
criteria: validation Sharpe_raw within 0.05 of training Sharpe_raw by
epoch 30 on L40S production run.

Rejected alternatives: backtest-matches-training (hides real issue),
training-matches-backtest (regresses stability), two-environment with
divergence as metric (fallback only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:22:05 +02:00
jgrusewski
dd9670dac0 cleanup: update scoreboard — iter 10 (PINMEM-007/013 rename + BORROW-001/ROMEM-003 u32 fix) 2026-04-21 00:13:32 +02:00
jgrusewski
b0fdf9b3e4 cleanup: expand scoreboard with iter-9 scans for 5 new categories
Per user directive, ran rg scans for PINMEM, ROMEM, LOCKHOT, BORROW,
CPURO and populated the scoreboard with concrete file:line findings.

PINMEM: 10 new findings (PINMEM-011..019, plus rescan confirmed 001-010)
- Biggest surface: gpu_experience_collector.rs (7 htod sites)
- High priority: gpu_iqn_head.rs dtod_copy for iqn_rewards/iqn_dones
  (PINMEM-013, score 25.0, switch to async-dtod, E=1)

ROMEM: 5 new findings (004 expanded, 005-008 added)
- ROMEM-004 now covers ~40 cuBLAS/cuBLASLt/cuDNN workspace casts across
  shared_cublas_handle.rs, gpu_iql_trainer.rs, gpu_iqn_head.rs,
  gpu_curiosity_trainer.rs, cublaslt_debug.rs — bulk false-positive
  candidates (FFI convention, not actual RO writes)
- ROMEM-007 adds 6 more device-mapped pinned write sites (same pattern
  as ROMEM-001/002 — benign cuMemHostAllocMapped)

LOCKHOT: 5 new findings (006-010)
- LOCKHOT-006/008: tokio::sync::Mutex<PPO> and RwLock<TLOBTransformer>
  held across .await — deadlock risk. High priority.
- LOCKHOT-007: Arc<Mutex<VecDeque<f64>>> history locks per step

BORROW: 1 new finding (002)
- BORROW-002: RefCell<PPO> × 2 in validation/ppo_adapter.rs —
  documented single-threaded, needs invariant verified

CPURO: deferred — .len()/.shape() scan returns hundreds of mostly-Vec
matches; left CPURO-000 task for next iter to classify per-site.

Scoreboard now has ~40 open findings across 12 active categories.
2026-04-21 00:04:10 +02:00
jgrusewski
aa0996f3c6 cleanup: add 4 CPU/GPU memory-safety categories (CPURO, ROMEM, LOCKHOT, BORROW)
Expanding the scanner surface per user directive "all should be
addressed". Four distinct root causes for CPU-side violations around
GPU-owned data — each with its own scan command, fix hierarchy, and
learned-patterns entry.

- CPURO: CPU reads of GPU-resident data (sizes, reductions) that
  force implicit sync. Cache at construction, keep stats on device.
- ROMEM: `*(ptr as *mut T)` writes where ptr came from a const /
  read-only source. CUDA mapped-memory flags matter; cuBLAS/cuDNN
  workspace casts are benign FFI.
- LOCKHOT: Mutex/RwLock on per-step path. High-value hits already
  visible: Mutex<GpuDropout>, Mutex<Option<DropoutScheduler>> in
  network.rs (every forward pass), Arc<Mutex<NStepBuffer>> in dqn.rs.
  Never hold tokio::sync::RwLock across .await.
- BORROW: shared-&T promoted to &mut T via unsafe ptr casts or
  UnsafeCell/RefCell. Real example shipped: gpu_replay_buffer.rs:690
  changes CudaSlice<i32> → CudaSlice<u32> through raw-ptr cast.

Scoreboard seeded with 10 findings. Top scores:
- ROMEM-001 (25.0) - size_pinned mapped write, verify allocation flag
- ROMEM-004 (25.0) - cuBLAS workspace casts, likely false-positive bulk
- ROMEM-002 (15.0) - init-time pinned mapped writes
- LOCKHOT-001/002/003 (8.3) - dropout + nstep_buffer locks on hot path
- BORROW-001 (8.3) - CudaSlice element-type aliasing

CPURO is seeded with a scan-task (CPURO-000) to populate per-site
findings in iter 9 — too many Vec::len() false positives to list upfront.
2026-04-20 23:59:53 +02:00
jgrusewski
f1c9f6025e cleanup: add PINMEM category (unpinned htod/dtod on hot path) — top priority
Per user directive: htod/dtod transfers on the training hot path that
bounce through pageable host memory are a top-priority cleanup target.
Pinned memory (cuMemHostAlloc) + memcpy_htod_async enables DMA + stream
overlap. The codebase already has the pattern (size_pinned,
rng_step_pinned in gpu_replay_buffer.rs) — extend it to every per-step
transfer.

Prompt changes:
- New PINMEM category in §3 table (severity 5)
- Scan command excludes tests/benches/examples/smoke_tests
- New §6 learned pattern entry with 3-tier fix hierarchy:
  (1) eliminate via on-device compute → (2) stage through pinned →
  (3) async for dtod
- Added PINMEM to category ID allowlist in §2

Scoreboard seed: 10 initial findings from an iter-9 scan. Top-scoring:
PINMEM-003 (scratch_f32 scalar, E=1, score=25) and PINMEM-004 (init
scalars, score=15). Hot-path candidates include NoisyNet epsilon
refresh, PER replay-buffer inserts, target-net sync, PPO rollout.
2026-04-20 23:54:23 +02:00
jgrusewski
b27a35ca28 cleanup: close Needs-human escape + document learned patterns
Per user directive from iter 7 ("no deferred tasks, solve properly"),
the Needs-human review escape hatch is closed. Updated:
- Step 6c: replaced "move to Needs human review" with split-into-
  sub-findings guidance; every finding must be solved.
- Step 6e: cargo-check failure now requires diagnosis + retry, not
  deferral. Pre-existing external errors go to Known external state.
- Step 8 summary line: dropped the "deferred j" counter.
- Rule 7: similar rephrasing.
- New §6: learned patterns from iters 1-8 (dead flag chains,
  aspirational config, dishonest fallbacks, inference-side flag flips,
  accounting-only values, orphan files, hidden GPU sync).

Ralph re-feeds this file verbatim, so the next iteration picks up the
updated rules automatically.
2026-04-20 23:51:34 +02:00
jgrusewski
aa43b6dddb cleanup: update scoreboard — iter 8 (4 resolved + Needs-human closed per user directive) 2026-04-20 23:46:24 +02:00
jgrusewski
34b764a91d cleanup: update scoreboard — iter 7 (3 fixed, FFLAG-013b deferral noted + DEAD-003/004 added) 2026-04-20 22:49:57 +02:00
jgrusewski
e2a84c90c4 cleanup: update scoreboard — iter 6 (FFLAG-012, FFLAG-018a resolved; FFLAG-018b split) 2026-04-20 22:42:04 +02:00
jgrusewski
6cece4c4e5 cleanup: update scoreboard — iter 5 (FFLAG-003, FFLAG-011 resolved; FFLAG-007 deferred) 2026-04-20 22:36:33 +02:00
jgrusewski
4f79572783 cleanup: update scoreboard — iter 4 (FFLAG-002, FFLAG-005 resolved) 2026-04-20 22:28:32 +02:00
jgrusewski
e36f63d1d4 cleanup: update scoreboard — iter 3 (FFLAG-006, FFLAG-009 resolved; note user WIP blocker) 2026-04-20 22:24:01 +02:00
jgrusewski
045c6f24e1 cleanup: update scoreboard — iter 2 (UNWRAP-002, FFLAG-015 resolved; UNWRAP-001 false-positive; GPUSYNC-006/008 deferred) 2026-04-20 22:17:11 +02:00
jgrusewski
2153929ed3 cleanup: update scoreboard — iter 1 (DEAD-001, FFLAG-001 resolved) 2026-04-20 22:11:10 +02:00
jgrusewski
e45e58a41a docs(cleanup): ralph-loop prompt + scoreboard for DQN code cleanup sweep
Adds scoreboard-driven cleanup infrastructure for bounded-batch Ralph
iteration across crates/ml*, services/ml_training_service, backtesting,
data_acquisition, and bin/fxt. Ten smell categories (GPUSYNC, FFLAG,
FALLBACK, ACCOUNT, DIMMIX, UNWRAP, DEAD, MAGIC, PDRIFT, TESTROT) with
per-iter ceiling of 3 commits / 200 LOC / 10 files. Completion gate:
empty scoreboard + cargo check --workspace + cargo test -p ml-dqn --lib.
2026-04-20 22:03:14 +02:00
jgrusewski
ab7567875a plan: adaptive learning dynamics — 19 tasks across 5 phases
Implementation plan for unified LearningHealth system (all gems/pearls/novels):
- Phase A (A1-A4): ISV extension, LearningHealth module, training loop wiring,
  HEALTH_DIAG logging
- Phase B (B1-B4): G2 uncertainty-gated CQL, G3 health-coupled tau,
  G4 temp-continuous Expected SARSA, G5 adaptive gradient budget
- Phase C (C1-C4): P1 health-weighted PER priorities, P2/P3 subsumed by A3,
  P4 temporal-coupled gamma
- Phase D (D1-D8): N1 temporal self-distillation, N2 Q-gap barrier,
  N3 health-triggered plasticity, N4 CF curriculum, N5 information bottleneck,
  N6 ensemble oracle, N7 contrarian override, N8 meta-Q collapse predictor
- Phase E (E1-E2): collapse-recovery smoke test + L40S deployment verification

Spec: docs/superpowers/specs/2026-04-20-adaptive-learning-dynamics-design.md
Target: WinRate >55% by epoch 20, Q-gap stays above 0.1 (no collapse).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:12:41 +02:00
jgrusewski
c2f61a0129 spec: review fixes — labels, dedup, P2 as 7th component, WinRate 55%
- Layer 2 labeled as G2-G5, G5 explicit for gradient budget
- P2 spectral_gap_norm added as 7th component in composition (rebalanced weights)
- N6 ensemble oracle has explicit threshold (>0.8 triggers N3 immediately)
- N8 meta-Q wiring clarified: logged but not in composition until validated
- Files Changed deduplicated, paths qualified
- Success criteria: WinRate >55% (above random baseline ~25%)
- HEALTH_DIAG log line includes all 7 components

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:58:35 +02:00
jgrusewski
ff5e578bc3 spec: adaptive learning dynamics — LearningHealth + 4 gems + 4 pearls + 8 novels
Fixes Q-value collapse via unified LearningHealth signal that senses
training health (6 components) and continuously adapts:
- CQL regularization (regime + health gated)
- Gradient budget (IQN/CQL/Ens/C51 dynamic allocation)
- Tau target EMA (health-coupled)
- Expected SARSA temperature (continuous, no hardcoded threshold)

Plus 4 pearls (PER priorities, spectral detection, gradient consistency,
adaptive gamma) and 8 novels (self-distillation, barrier loss, plasticity
injection, CF curriculum, information bottleneck, ensemble oracle,
contrarian override, meta-Q network).

Core principle: training hyperparameters are OUTPUTS of the temporal
pipeline, not static schedules. The system meta-learns its own settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:55:26 +02:00
jgrusewski
bc8fde9a2b plan: unified state layout implementation — 8 tasks
Task 1: Rust constants (ml-core/state_layout.rs)
Task 2: CUDA header (state_layout.cuh + assemble_state())
Task 3: Refactor experience_state_gather to use shared assembly
Task 4: Delete backtest_gather_kernel, replace with shared function
Task 5: Remove configurable state_dim everywhere (131 call sites)
Task 6: Update OFI_DIAG indices + checkpoint validation
Task 7: Layout match integration test
Task 8: Deploy and verify on H100

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:36:03 +02:00
jgrusewski
1d8c82221c spec: unified state layout — fix train/validation mismatch
Training and validation use different kernels with different state layouts,
making generalization impossible. Single state_layout.cuh + one assembly
function + STATE_DIM as compile-time constant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:29:53 +02:00