feat(dqn-v2): C.3 state-distribution KL divergence + adaptive amplification
Plan 3 Task 7.
ISV tail-append:
- [78] STATE_KL_TRAIN_VAL_EMA_INDEX — moment-match KL on OFI dims, EMA
- [79] STATE_KL_AMPLIFICATION_INDEX — Flat-trap escape multiplier ∈ [1,2]
- Fingerprint shifted [76,77] → [80,81]; ISV_TOTAL_DIM 78 → 82
Producer (state_kl_divergence_kernel.cu):
- Per-dim Gaussian moment-match KL summed over OFI block (32 dims)
- Adaptive α matches Task 3 convention: α_base × (1 + 0.5 × |sharpe|), α_base=0.05
- Amp formula: ratio = new_ema / prev_ema; target = 1 + clamp(ratio−1, 0, 1)
- Kernel-internal trailing-EMA-of-self pattern — no separate threshold ISV slot
- Single block, one thread per OFI dim, smem reduction, no atomicAdd, no DtoH
Val-side launch: GpuBacktestEvaluator exposes val_state_sample() returning
(ptr, n) into the chunked_states_buf the val pass populates. Train side uses
the experience-collector's existing states buffer. Both buffers in same
CUDA context; launch enqueues on collector's stream after compute_validation_loss.
Consumer (experience_kernels.cu):
- B.1 opp_cost ×= kl_amp (penalty intensifies on train/val drift)
- B.2 bonus ×= kl_amp (novelty bonus intensifies on drift)
- fmaxf(1.0, ISV[STATE_KL_AMP]) guard at consumers handles cold-start 0 → no-op
Per pearl_one_unbounded_signal_per_reward.md: amp ∈ [1,2] is bounded; stacks
safely with B.1's q_abs_ref unbounded multiplicand. No formula blow-out risk.
Constructor cold-start: KL=0.0, amp=1.0 (consumer no-op until kernel fires).
Registry: both slots FoldReset with cold-start values restored on fold boundary.
Read-only StateKLMonitor surfaces both slots in diagnose snapshot.
Smoke results: All 3 folds pass cleanly with diverse epoch convergence
patterns (best at ep5/ep2/ep3 — addresses prior concern about always-ep1
in fold 2). Per-fold best Sharpe: 105.13, 83.28, 103.49. Average
best_val_metric 10.64.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>