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.
This commit is contained in:
jgrusewski
2026-04-22 08:51:51 +02:00
parent 8ab368de58
commit 5c70c68a15

View File

@@ -0,0 +1,239 @@
# Track 1 — Magnitude Collapse Triage
**Phase:** Phase 1 — Track 1 (Magnitude investigation)
**Captured against commit:** `8ab368de5` (tag: `policy-quality-baseline`)
**Hardware:** RTX 3050 Ti Laptop GPU (4 GB) — **preliminary smoke-scale verdicts**; L40S validation pending per plan §Task 1.1
**Data source:** `magnitude_distribution.rs` smoke run (20 epochs, 176k-bar fxcache, `dqn-smoketest` profile) from the Phase 0 baseline capture.
**Log ref:** `/tmp/foxhunt_smoke/magnitude_distribution.log` (local; not committed).
---
## Executive summary
Preliminary triage against spec §5.1 hypotheses. At smoke scale:
- **2 CONFIRMED** — H4 (magnitude-head gradient starvation), H10 (argmax tie-breaking at eval)
- **5 REJECTED** — H2, H5, H6 (preliminary), H7, H8
- **2 PENDING** — H1, H9 (both require the forced-exploration + per-magnitude-return instrumentation not yet in-tree)
- **1 INCONCLUSIVE** — H3 (Kelly degenerate at smoke scale; needs a run long enough for meaningful win/loss counts)
**Most load-bearing finding**: H4 alone would explain the entire phenomenon — if the magnitude branch receives near-zero gradient for 20/20 epochs, it stays at initialization and cannot learn to prefer any magnitude. H10 (eval-mode argmax locks to the first bin on near-ties) compounds the observable symptom at evaluation time.
---
## Per-hypothesis verdicts
### H1 — Reward gradient asymmetry (Full-position variance > Quarter)
**Signal:** forced-exploration epoch (ε=1.0 for 1 epoch at mid-training), sample 100 states × 3 magnitudes, log realized step-return per (state, magnitude).
**Threshold:** `mean(R_full) / std(R_full) ≤ 0.5 × mean(R_quarter) / std(R_quarter)` → policy correctly prefers Quarter under Sharpe-per-trade logic.
**Observed:** not measured. The forced-exploration counterfactual experiment requires per-(state, magnitude) return logging that isn't yet instrumented — the kernel writes one realized step-return per sample, not all three magnitudes.
**Verdict:** **PENDING**
**To resolve:** Task 1.1 Step 2 of the plan (edit `training_loop.rs` to force `epsilon = 1.0` at epoch 10) + add a counterfactual-return logging hook that evaluates all three magnitudes for each of N sampled states. Not a one-liner — needs an additional per-(state, magnitude) buffer on the collector side.
---
### H2 — Var[Q] conviction-sizer over-shrinking
**Signal:** HEALTH_DIAG `mag [var_scale=…]`
**Threshold:** `var_scale_mean < 0.3` across ≥ 10 / 20 epochs.
**Observed trajectory (20 epochs)**:
```
0.314, 0.958, 0.959, 0.961, 0.965, 0.967, 0.968, 0.968, 0.969, 0.969,
0.968, 0.968, 0.967, 0.967, 0.966, 0.966, 0.966, 0.960, 0.960, 0.961
```
Only epoch 0 is near-threshold (0.314); all 19 other epochs are ≥ 0.958. Var[Q] conviction scaling is essentially inactive at smoke scale (Var[Q] disabled for most samples because q_variance estimator needs warmup).
**Verdict:** **REJECTED** (1 / 20 epochs below threshold vs required 10 / 20).
**Caveat:** At production scale with warm Var[Q], this may tighten. Re-run at L40S before permanent REJECTED.
---
### H3 — Kelly as hard multiplier for marginal edges
**Signal:** HEALTH_DIAG `mag [kelly_f=… avg_win_ratio=…]`
**Threshold:** `kelly_f_mean ≤ 0.25` AND `avg_win / avg_loss < 1.3`.
**Observed trajectory**:
- `kelly_f = 0.000` every epoch (20/20)
- `avg_win_ratio = 0.000 to 0.003` every epoch
**Verdict:** **INCONCLUSIVE**
The smoke-scale data has degenerate win/loss statistics — `trade_stats_history.last()` returns a fold with insufficient win OR loss counts, so `kelly_f_mean` computation short-circuits to 0. The accessor is correct; the input is pathological at smoke scale. At production scale (50+ epochs, higher trade volume), kelly_f and avg_win_ratio should carry meaningful values.
**To resolve:** re-measure on L40S with 50-epoch training. If kelly_f_mean stays ≤ 0.25 with avg_win/avg_loss < 1.3 on real data, the hypothesis becomes CONFIRMED and the fix is to replace the Kelly hard-multiplier with a soft gate.
---
### H4 — Magnitude-head gradient starvation
**Signal:** HEALTH_DIAG `mag [grad_ratio_mag_dir=…]``‖∇ W[mag_head]‖ / ‖∇ W[dir_head]‖` per epoch.
**Threshold:** `grad_ratio_mag_dir < 0.1` across ≥ 15 / 20 epochs.
**Observed trajectory (20 epochs)**: `0.0000` on every single epoch (4-decimal precision).
Cross-check: the accessor returns `mag / dir` only when `dir > 1e-9`; else returns `0.0`. The `grad_stable` component of `learning_health.components` is `0.00` for most epochs (suggesting direction gradient is changing meaningfully) — so `dir_grad_norm > 1e-9` and the division is live, meaning `mag_grad_norm` is legitimately < `1e-4 * dir_grad_norm`.
**Verdict:** **CONFIRMED** (20 / 20 epochs below threshold vs required 15 / 20).
**Evidence strength:** STRONG. Magnitude branch receives gradient three-to-four orders of magnitude smaller than direction across the entire run. It cannot learn to distinguish Quarter / Half / Full if it never receives meaningful gradient signal. This alone would produce the observed phenomenon.
**Proposed fix** (per spec §5.1): rebalance 4-branch advantage weights OR adjust initial weight scale for the magnitude head. Investigating which branch of loss propagation skips magnitude is Phase 2 implementation work.
**Follow-up investigation needed**:
- Which loss component (IQN / CQL / C51 / Ens) propagates gradient to magnitude head? Per-component grad norms would pin this down.
- Is the direction-conditioned `w_b1fc` layout (magnitude FC depends on direction sliced input `[AH, SH2+3]`) blocking gradient flow? Direction-conditioning may inadvertently mask magnitude updates when direction is one-hot-dominant (Short or Long).
---
### H5 — Bootstrap bias (initial epsilon decayed too fast)
**Signal:** action entropy vs epsilon schedule.
**Threshold:** entropy < 0.3 before epoch 5 AND epsilon below 0.1.
**Observed**: `ent_mag` trajectory over 20 epochs: `0.98, 0.98, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.98, 0.99, 0.98`. Magnitude entropy stays ≥ 0.98 throughout (near-uniform distribution across bins). Direction entropy (`ent_dir`) stays ≥ 0.98 as well.
**Verdict:** **REJECTED**
Entropy is *high*, not low. The policy is not stuck in a low-entropy-early pathology. If anything, the opposite is true — entropy is high throughout, suggesting NoisyNets and/or epsilon are well-mixed in training mode. The eval-time collapse (H10) is the relevant signal, not a training-time bootstrap.
---
### H6 — Regime-adaptive trailing stop closes Full positions early
**Signal:** HEALTH_DIAG `trail [fire_q=… fire_h=… fire_f=…]` per-magnitude trail firing rate.
**Threshold:** Full fire rate > 3× Quarter rate AND hold_time_at_exit on Full < 0.5× Quarter.
**Observed**: across the magnitude_distribution run, trail fire rates fluctuate between:
- `fire_q = 0.12 0.66` per epoch (healthy Quarter trail activity)
- `fire_h = 0.000` every epoch (Half positions too rare to generate fires)
- `fire_f = 0.000` every epoch (Full positions too rare)
Full fire rate is *lower* than Quarter, not higher. Hypothesis direction contradicts the data.
**Verdict:** **REJECTED** (preliminary — very low Full-position sample size; re-measure at L40S scale for a strict rejection).
**Caveat:** `fire_f = 0` may simply mean "no Full-position trades ever entered that could trail-stop", which is itself a consequence of the magnitude collapse rather than a rebuttal of the trail-stop hypothesis. At production scale where Full positions actually enter (if H4/H10 are fixed), re-measure.
---
### H7 — VSN / NoisyNets noise masks magnitude more than direction
**Signal:** HEALTH_DIAG `noisy [vsn_mag=… vsn_dir=… sigma_mag=… sigma_dir=…]`.
**Threshold:** `vsn_mag < 0.5 × vsn_dir` OR `sigma_mag > 2 × sigma_dir`.
**Observed (stable across 20 epochs)**:
- `vsn_mag = 0.010`, `vsn_dir = 0.010` — ratio 1.0 (not < 0.5)
- `sigma_mag = 0.0320`, `sigma_dir = 0.0320` — ratio 1.0 (not > 2)
**Verdict:** **REJECTED**
Both VSN projection-weight magnitudes and NoisyNets σ values are symmetric across magnitude and direction branches. No asymmetric noise treatment is present.
**Note on `vsn_mag`**: the accessor is a projection-weight surrogate (Task 0.6 partial — live VSN mask isn't materialized), so this REJECT holds on the proxy metric. If the live mask ever gets instrumented, H7 becomes worth re-testing.
---
### H8 — Target-network tau too slow for magnitude propagation
**Signal:** HEALTH_DIAG `noisy [drift_mag=… drift_dir=…]` — cumulative target/online drift per branch.
**Threshold:** `drift_mag < 0.1 × drift_dir` cumulatively.
**Observed (stable across 20 epochs)**: `drift_mag = 0.001`, `drift_dir = 0.001`. Ratio 1.0.
**Verdict:** **REJECTED**
Target-network drift is identical between magnitude and direction branches. This rules out "magnitude bins are sampled too rarely for target propagation to converge".
**Note:** drift = 0.001 in both branches is quite low. Combined with H4 (mag gradient ~0), the magnitude branch's target AND online weights both stay near init. A stronger reading: H4 is upstream of the target-drift symmetry — target drift equals online drift equals zero because nothing is learning. H8 is correctly REJECTED as the *proximate cause*, but the rejection says nothing about whether tau is appropriate — just that tau isn't preferentially slowing magnitude.
---
### H9 — Data favors Quarter (not a bug — Quarter IS optimal)
**Signal:** per-magnitude Sharpe during forced-exploration epoch (requires H1 experiment).
**Threshold:** all three magnitudes show statistically similar Sharpe → Quarter-collapse is correct behavior.
**Observed:** not measured (same instrumentation gap as H1).
**Verdict:** **PENDING**
**To resolve:** the forced-exploration experiment required for H1 produces the data for H9 as a by-product. Defer both until instrumented.
**Side note:** H4 being CONFIRMED independent of H9 already means "the magnitude branch *isn't allowed to choose*", so even if H9 is also true, the pathway isn't through learning. H9 would be a "why remove magnitude" argument, not a "why the policy collapsed" explanation.
---
### H10 — Entropy regularization + argmax tie-breaking at eval
**Signal:** training-mode action distribution vs eval-mode action distribution.
**Threshold:** training `ent_mag > 0.8` (near-uniform) AND eval `F_Quarter > 90%`.
**Observed**:
- Training `ent_mag = 0.86 0.99` across all 20 epochs (always ≥ 0.8)
- Eval `eval_dist [eq=1.000 eh=0.000 ef=0.000]` every epoch (F_Quarter = 100%, well above 90%)
**Verdict:** **CONFIRMED** (strong).
**Evidence strength:** STRONG. The diagnostic produces a perfect signal — training distribution has near-maximum entropy (model outputs uniform), but eval argmax collapses 100% to bin 0 (Quarter). This is exactly the shape predicted by "uniform Q-values + stable argmax tie-break picks first bin".
**Combined with H4**: since H4 CONFIRMED means the magnitude branch hasn't learned to distinguish anything, its output IS near-uniform (three near-equal Q-values). Argmax over near-equal values is numerically unstable; Rust's `.iter().max_by()` returns the first-seen index on ties (Quarter = bin 0). H10 is the *observable consequence* of H4's underlying cause.
**Proposed fix** (per spec §5.1): break argmax ties by Q-value margin (no bin-index bias) OR match training / eval distributions via stochastic eval rollouts. The root-cause fix is H4; H10 is a safety-net patch to prevent accidentally relying on argmax tie-breaking order.
---
## Cross-hypothesis synthesis
1. **H4 is the root cause** at smoke scale. Magnitude branch gets effectively zero gradient → weights stay near init → all three magnitude Q-values are near-identical → argmax picks bin 0 (Quarter) on ties → H10 manifests at eval time.
2. **H2, H5, H7, H8 are ruled out** as contributors: Var[Q], bootstrap, VSN, and target-drift are all symmetric between mag and dir, or inactive at smoke scale.
3. **H3 needs production-scale data** (Kelly formula degenerates on low-volume trades). If H3 also CONFIRMS at L40S, a Kelly soft-gate is a compound fix on top of H4.
4. **H1 and H9 need new instrumentation** (forced-exploration counterfactual). Both could land in a Phase-0-bis micro-task if judged worth the scope — but the H4 fix alone should open up enough magnitude variation for Phase 2 to revisit H1/H9 with real distribution data.
5. **H6 needs real Full-position trades** before it can be fairly tested. Currently trivially-REJECTED by the data, but the data is itself a product of the collapse.
## Proposed Phase 2 fix priority order
1. **Primary (H4)**: diagnose gradient-flow path into the magnitude head. Likely candidates:
- Per-component advantage weighting in the 4-branch head (direction branch may consume most of the per-batch gradient budget)
- Direction-conditioning of `w_b1fc` ([AH, SH2+3]) — when direction is one-hot-dominant, the `+3` input slice is effectively zeroed for non-active directions
- Initial weight scale for magnitude head (Xavier may be too small relative to direction head)
2. **Secondary (H10)**: replace argmax tie-breaking at eval with Q-margin-aware selection OR add stochastic eval rollouts. Low cost, high safety-net value — prevents the symptom from reappearing even if H4 regresses.
3. **Post-fix validation**: re-run baseline smoke + multi_fold with H4 + H10 patches applied. Expect:
- `grad_ratio_mag_dir` > 0.1 across most epochs
- Eval-mode `eq / eh / ef` converges toward training distribution (not 1/0/0)
- `q_full`, `q_half`, `q_quarter` spread meaningfully (currently clustered at ~-0.05)
---
## Limitations of this preliminary triage
- **Smoke scale ≠ production scale.** `dqn-smoketest` uses batch=64, buffer=256, 5000-bar data slice. L40S production is batch=16384, buffer=500k, 176k bars. Some hypotheses may flip direction at scale (H2, H3, H6).
- **4 GB VRAM constrains what we can test locally.** No production-scale ablations.
- **Per-component gradient decomposition missing.** H4 is CONFIRMED but we don't yet know *which* loss component (IQN / CQL / C51 / Ens) sends zero gradient to magnitude. Phase 2 needs that breakdown before choosing a fix.
- **H1 / H9 not measured.** The forced-exploration counterfactual is the one measurement not yet wired; both would give useful extra evidence if instrumented.
## Next Phase 1 steps
- Re-run this triage on L40S (20 epochs × 2 folds, per plan Task 1.1 Step 1) to promote each preliminary verdict to final.
- Instrument per-component gradient norms for the magnitude head (IQN / CQL / C51 / Ens split) to localize H4.
- Proceed with Tracks 2 (Reward V7 audit), 3 (Controllers), 4 (Exploration) in parallel — they reuse the same baseline run's HEALTH_DIAG output.