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>
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
# Unified Training and Validation Environments — Design
|
||||
|
||||
## Problem
|
||||
|
||||
The DQN training loop produces a model whose training Sharpe and validation
|
||||
Sharpe diverge catastrophically:
|
||||
|
||||
| Metric | Training | Validation |
|
||||
|---|---|---|
|
||||
| `Sharpe_raw` (per-bar) | +0.03 to +0.07 | -0.31 to -0.39 |
|
||||
| `WinRate` | 15% – 28% (oscillating) | 26% – 32% (stable) |
|
||||
| `MaxDD` | 0.002% – 0.006% | 10% – 15% |
|
||||
| Sharpe (annualized) | -13 to +20 (noisy) | -97 to -150 (flat) |
|
||||
|
||||
Measured over a 50-epoch E1 smoke test run on 2026-04-20 after the
|
||||
distillation-collapse fixes landed (commit `24f96ab78`).
|
||||
|
||||
**Same network, same weights, same data — drastically different outcomes.**
|
||||
Validation metrics are *stable* across 50 epochs (the network converges to a
|
||||
deterministic losing policy) while training metrics oscillate (shaped reward
|
||||
varies with exploration noise). This is not a bug in any single component;
|
||||
it is the product of two environments that have drifted apart over many
|
||||
incremental design decisions.
|
||||
|
||||
### Evidence that it's architectural, not a single bug
|
||||
|
||||
A reward-shaping ablation (2026-04-20, E1 smoke) closed ~30% of the gap:
|
||||
|
||||
- Dropping `order_credit_weight`, `urgency_credit_weight`,
|
||||
`risk_efficiency_weight`, `exit_timing_weight`, `ofi_reward_weight`,
|
||||
`kelly_sizing_weight`, `reward_noise_scale`, `cea_weight`
|
||||
- Result: val `Sharpe_raw` -0.39 → -0.31, val WinRate 30% → 41%, val MaxDD
|
||||
11% → 8%
|
||||
|
||||
The remaining ~70% gap is architectural: the environments execute the same
|
||||
weights against different trade physics.
|
||||
|
||||
### Concrete divergences between the two environments
|
||||
|
||||
| Aspect | `experience_kernels.cu` (training) | `backtest_env_kernel.cu` (validation) |
|
||||
|---|---|---|
|
||||
| Spread scaling | `0.5× – 2.0×` via CUSUM regime proxy (line 1395) | Fixed (no scaling; `-1.0f` override) |
|
||||
| Fill model | Saboteur: per-episode `(spread_mult, fill_prob, slippage_mult)` adversarial noise | Always fill at close price |
|
||||
| Transaction cost | `tx_cost_multiplier` scaled by saboteur + curriculum | Fixed `tx_cost_bps` |
|
||||
| Reward composition | 18 shaping terms (DSR, DD, churn, micro-reward, credits, entropy, HER, counterfactuals, plan params, OFI, Kelly…) | Pure P&L + inventory penalty + churn penalty |
|
||||
| Action selection | ε-greedy with `ε ≈ 0.02` + noisy-net parameter noise + plan-constrained | Deterministic argmax (no noise) |
|
||||
| Position dynamics | `plan_params` influence target position + counterfactual flips (N4) | Pure action → target position mapping |
|
||||
| Trailing stop | Fixed parameters (aligned per comment: "removed regime scaling to eliminate mismatch") | Same fixed parameters |
|
||||
| Capital floor | 25% drawdown liquidation | 25% drawdown liquidation (matched) |
|
||||
|
||||
The trailing-stop comment at `experience_kernels.cu:1418` tells the story:
|
||||
*"Regime-adaptive scaling removed to eliminate train/eval mismatch."*
|
||||
Someone was here before. They aligned *some* things. Many more remain
|
||||
unaligned.
|
||||
|
||||
## Design goals
|
||||
|
||||
1. **Training and validation must produce comparable scalar P&L**, up to the
|
||||
noise floor introduced by intentional randomization (exploration).
|
||||
2. **Validation must be an honest measure of deployed-model behavior** —
|
||||
adding shaping to the validation env to close the gap is the wrong
|
||||
direction.
|
||||
3. **Training must still be stable** — the shaping terms were added to
|
||||
address real pathologies (churn, over-trading, drawdown); stripping them
|
||||
without replacement risks regressing training dynamics.
|
||||
4. **The fix must be testable within the existing smoke-test budget** (~30s
|
||||
per run on RTX 3050 Ti; 20 epochs). Otherwise iteration becomes too slow
|
||||
to converge on the right answer.
|
||||
|
||||
## Options considered
|
||||
|
||||
### Option A — Backtest matches training
|
||||
|
||||
Make the backtest kernel execute the full shaping stack: DSR reward, churn,
|
||||
credits, saboteur noise, counterfactuals, plan params.
|
||||
|
||||
- **Pro**: closes the gap immediately — val Sharpe would track training
|
||||
Sharpe by construction.
|
||||
- **Con**: val is no longer an honest measure of deployed P&L. Training
|
||||
metric "looks good" but model still bleeds money in the real world. This
|
||||
is strictly worse than the current state — the gap was informing us of a
|
||||
real issue, and we'd be hiding it.
|
||||
- **Verdict**: **Rejected**. Measurement degradation.
|
||||
|
||||
### Option B — Training matches backtest (strip shaping)
|
||||
|
||||
Remove all shaping terms. Training reward = trading P&L + minimal safety
|
||||
(drawdown penalty only).
|
||||
|
||||
- **Pro**: pure alignment. One source of truth for "good model."
|
||||
- **Con**: the shaping terms exist because naive P&L training exhibits
|
||||
pathologies — churning, over-trading, gambling for variance, collapse in
|
||||
sparse-reward regimes. Stripping without replacement regresses to those
|
||||
failure modes.
|
||||
- **Verdict**: **Rejected**. Throws the baby out with the bathwater.
|
||||
|
||||
### Option C — Unified environment with conditional shaping layer (RECOMMENDED)
|
||||
|
||||
One environment kernel that implements trade physics identically for
|
||||
training and validation. Reward is a *layered* computation: a core P&L term
|
||||
(identical in both modes) plus a tunable *shaping contribution* that is
|
||||
**additive, P&L-aligned, and documented in units of "dollars per bar."**
|
||||
|
||||
Key design principles:
|
||||
|
||||
1. **Core reward = per-step change in portfolio equity.** Same in training
|
||||
and validation. This is the ground truth.
|
||||
2. **Shaping terms must be P&L-units additive.** Each shaping term's
|
||||
contribution is measured in the same units as the core reward (dollars,
|
||||
or normalized portfolio fraction). The sum of `core + sum(shaping)` IS
|
||||
the training signal; the validation signal is `core` alone. The gap
|
||||
between training and validation Sharpe becomes a *computable* quantity:
|
||||
`E[shaping] / σ(core)` — and can be bounded by caps on shaping
|
||||
magnitudes.
|
||||
3. **Non-P&L-aligned shaping is forbidden.** Terms like
|
||||
`order_credit_weight` (reward for picking a certain order-type branch
|
||||
regardless of outcome) violate this and are deleted, not disabled —
|
||||
per `feedback_no_feature_flags.md`.
|
||||
4. **Adversarial/exploration noise (saboteur, ε-greedy, plan nudges) only
|
||||
modulates the core state transition, not the reward.** The reward is
|
||||
whatever actually happened — fills, slippage, costs all reflected.
|
||||
Exploration doesn't get "credit" for trying; it gets credit only for
|
||||
what its trials actually produced.
|
||||
5. **Validation = training with exploration = 0 AND shaping = 0.**
|
||||
Single flag at the env entry point (a scalar mode multiplier, not a
|
||||
boolean: `exploration_scale: f32`, `shaping_scale: f32`). In training,
|
||||
both are 1.0; in validation, both are 0.0. The env kernel itself is
|
||||
*identical code* — no branches on mode.
|
||||
|
||||
This is the same architectural move that fixed the collapse-prevention
|
||||
distillation: compute the varying signal at the source (GPU, per-step)
|
||||
from a scalar mode factor baked via pinned device-mapped memory, rather
|
||||
than maintaining two parallel code paths.
|
||||
|
||||
### Option D — Keep both environments; use both signals meaningfully
|
||||
|
||||
Accept the two environments as-is. Use training Sharpe for hyperparameter
|
||||
search and validation Sharpe for deployment gating. Report the gap as a
|
||||
model-quality metric.
|
||||
|
||||
- **Pro**: no code changes. Documents reality.
|
||||
- **Con**: doesn't *solve* anything. Leaves the divergent environment
|
||||
structure as permanent technical debt. Future changes to one env will
|
||||
continue to drift from the other.
|
||||
- **Verdict**: **Fallback only**. Choose this only if C turns out to be too
|
||||
expensive to implement.
|
||||
|
||||
## Recommended path: Option C
|
||||
|
||||
### Phase 1 — Inventory shaping terms (≤1 day)
|
||||
|
||||
Document every term currently summed into training reward. For each:
|
||||
|
||||
- What was it added for? (find the commit)
|
||||
- Is its contribution P&L-aligned? (does it reward outcomes or behaviors?)
|
||||
- Can it be re-expressed as an additive dollar-cost term?
|
||||
|
||||
Expected output: two lists.
|
||||
- **Keepers**: drawdown penalty, transaction-cost penalty, inventory
|
||||
penalty, capital-floor liquidation cost — all in P&L units.
|
||||
- **Removers**: credit weights, entropy bonuses, DSR, micro-reward,
|
||||
counterfactual reward scaling — all behavioral.
|
||||
|
||||
Store the inventory as a commit-tracked doc. This is the one-time
|
||||
reckoning.
|
||||
|
||||
### Phase 2 — Rewrite env as a single kernel (≤3 days)
|
||||
|
||||
New file: `crates/ml/src/cuda_pipeline/unified_env_kernel.cu`. One
|
||||
`__global__ void unified_env_step(...)` that:
|
||||
|
||||
- Takes the exploration/shaping scale scalars as `const float*` device
|
||||
pointers (pinned device-mapped, same pattern as distillation alpha).
|
||||
- Computes trade physics once.
|
||||
- Computes reward = `pnl_per_step + shaping_scale × shaping_bundle`.
|
||||
- Writes `step_return = pnl_per_step` to a separate output buffer — this
|
||||
is the **authentic** P&L signal used by validation regardless of
|
||||
training/validation mode.
|
||||
|
||||
Both training and validation call the same kernel. The only difference is
|
||||
the scalar values at the pinned pointers.
|
||||
|
||||
### Phase 3 — Deprecate the two old kernels (≤1 day)
|
||||
|
||||
Delete `experience_env_step_batch` and `backtest_env_step_batch`. Rewire
|
||||
all callers to the unified kernel. Remove any dead utility code that was
|
||||
specific to one path.
|
||||
|
||||
### Phase 4 — Re-tune adaptive-learning mechanisms (≤2 days)
|
||||
|
||||
The distillation, collapse-prevention, and health-EMA machinery was tuned
|
||||
against the old training reward magnitudes. With shaping removed or
|
||||
re-scaled, the health-signal thresholds (`SNAPSHOT_HEALTH_THRESHOLD`,
|
||||
`DISTILL_HEALTH_THRESHOLD`, etc.) may need recalibration. The E1 smoke
|
||||
test is the canary.
|
||||
|
||||
### Phase 5 — Verify on L40S full-scale run (≤1 day)
|
||||
|
||||
Production 50-epoch run. Expected signatures of success:
|
||||
|
||||
- Training `Sharpe_raw` ≥ 0 by epoch 10; ≥ +0.03 by epoch 30
|
||||
- Validation `Sharpe_raw` ≥ training `Sharpe_raw` − 0.05
|
||||
- Validation `MaxDD` ≤ 3× training `MaxDD`
|
||||
- Validation `WinRate` tracks training `WinRate` ± 5 percentage points
|
||||
|
||||
If any of these fail, the analysis is wrong and we revisit Phase 1.
|
||||
|
||||
## Risks
|
||||
|
||||
1. **Collapse-prevention regresses.** The distillation mechanism was tuned
|
||||
with the existing shaping weights in the loop. Pure-P&L training may
|
||||
cause the network to find attractor states the current mechanisms don't
|
||||
cover. Mitigation: Phase 4 recalibration, E1 smoke test as the gate.
|
||||
2. **Training becomes too noisy.** Sparse P&L reward may have insufficient
|
||||
gradient signal for the Q-network to learn. Mitigation: the counterfactual
|
||||
curriculum (N4) should provide dense gradient signal even with pure P&L
|
||||
reward — verify this empirically.
|
||||
3. **Historical reward-shaping decisions are load-bearing in ways we don't
|
||||
see.** Some terms may have been added to fix subtle numerical instability
|
||||
(e.g., `reward_noise_scale = 0.05` — label smoothing proxy). Mitigation:
|
||||
Phase 1's per-term inventory must include the provenance commit, not just
|
||||
the semantic purpose.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Changing the underlying state representation (96-dim layout, OFI
|
||||
integration, MTF).
|
||||
- Changing the network architecture (4-branch factored DQN, ensemble heads,
|
||||
IQN/C51).
|
||||
- Re-tuning transaction cost parameters (`tx_cost_bps`, `spread_cost`).
|
||||
These are calibrated to actual broker execution and should remain as-is.
|
||||
|
||||
## Success criteria
|
||||
|
||||
A pull request that:
|
||||
|
||||
1. Deletes `experience_env_step` and `backtest_env_step` kernels in favor
|
||||
of a single unified env step.
|
||||
2. Deletes the non-P&L-aligned shaping terms (from Phase 1 inventory).
|
||||
3. Passes the E1 collapse-prevention smoke test with final
|
||||
`epoch_q_gap > 0.05`.
|
||||
4. Shows validation `Sharpe_raw` within 0.05 of training `Sharpe_raw` by
|
||||
epoch 30 on the L40S 50-epoch production run.
|
||||
5. Documents the reward semantics in a single comment block at the
|
||||
unified kernel site.
|
||||
|
||||
## References
|
||||
|
||||
- Current distillation root-cause analysis: commit `24f96ab78`
|
||||
- Smoke test run showing divergence: 2026-04-20 E1 trajectory
|
||||
- Reward shaping ablation result: 30% gap closure from disabling 8 credit
|
||||
weights
|
||||
- Existing hint at the problem: `experience_kernels.cu:1418` comment
|
||||
*"Regime-adaptive scaling removed to eliminate train/eval mismatch"*
|
||||
Reference in New Issue
Block a user