# Supervised Architecture Transfer — Design Spec

**Goal:** Transfer 7 architectural concepts from the supervised model suite (KAN, xLSTM, Diffusion, TGNN, Liquid ODE, TFT Quantile, TLOB) into the DQN training pipeline to improve action selection quality, representational power, and training dynamics.

**Problem:** The DQN has fixed sigmoid activations (no adaptive shapes), no temporal memory of Q-value trajectories, single-shot Q-estimation (no refinement), flat Q-value coordination (no structural knowledge of branch dependencies), first-order training dynamics (Euler), mean-only action selection (ignores distributional uncertainty), and uniform feature routing (all branches see same trunk output).

**Architecture:** 7 independent components that compose through the existing forward/backward/training pipeline. Each borrows a specific concept from a supervised architecture already implemented in `crates/ml-supervised/`.

---

## Component 1: KAN Spline Activations → Adaptive Branch Gates

### Source
`crates/ml-supervised/src/kan/layer.rs` — B-spline basis with learnable coefficients + residual connection.

### Problem
The GLU gate uses `sigmoid(pre)` — a fixed S-curve. Every gate neuron has the same activation shape. Sigmoid can't learn sharp thresholds, asymmetric responses, or multi-modal gating patterns.

### Solution
Replace sigmoid in `glu_combine` with a learned 1D B-spline per gate neuron:

```
Current:  gate = sigmoid(pre)
KAN:      gate = clamp(sum_k coeff[k] * B_k(pre) + residual_w * pre, 0, 1)
```

- 8 cubic B-splines on [-4, 4] per neuron (same grid as KAN default)
- `coeff[8]` learnable per neuron, initialized to approximate sigmoid
- Residual connection: `residual_w * pre` (linear bypass, from KAN's residual_weight)
- Clamped to [0, 1] (gate must be a probability)

### Parameters
- Per branch: AH × 8 (spline coeffs) + AH × 1 (residual weight) = AH × 9
- Total: 4 × 256 × 9 = 9,216 params
- Stored in params_buf (indices 42-49: spline_coeff_0..3, residual_w_0..3)

### Implementation
- New CUDA kernel `kan_gate_combine`: evaluates B-splines on pre-sigmoid input, combines with coefficients, applies residual, clamps to [0,1], multiplies with value
- Replaces `glu_combine` kernel call (same interface: output, gate_pre, value, n)
- New CUDA kernel `kan_gate_backward`: gradient through spline basis + residual + clamp
- B-spline basis functions are FIXED (not learned) — only coefficients are trained
- Spline evaluation: 8 basis functions × 256 neurons = 2048 evaluations per sample per branch. Each is a cubic polynomial (4 multiplies) = 8192 FLOPs per branch. Negligible vs the GEMM cost.

---

## Component 2: xLSTM Matrix Memory → Temporal Q-Value Context

### Source
`crates/ml-supervised/src/xlstm/mlstm.rs` — matrix memory C_t with exponential input gate, covariance-based update.

### Problem
Training dynamics (liquid tau, backtracking) react to CURRENT Q-stats only. No memory of Q-value TRAJECTORY patterns. "Q-gap grew steadily for 5 steps then collapsed" is a pattern that predicts the plateau — but without temporal memory, each step is evaluated independently.

### Solution
Lightweight mLSTM cell on CPU that processes Q-stats every 50 training steps:

```
Input: q_input[11] = [avg_max_q, q_min, q_max, q_mean, q_var, entropy, utilization, q_gap_0..3]

k_t = W_k @ q_input          // [head_dim] key
v_t = W_v @ q_input          // [head_dim] value  
q_t = W_q @ q_input          // [head_dim] query
i_t = exp(W_i @ q_input)     // exponential input gate (xLSTM novelty)
f_t = sigmoid(W_f @ q_input) // forget gate
o_t = sigmoid(W_o @ q_input) // output gate

C_t = f_t * C_{t-1} + i_t * outer(v_t, k_t)   // [8, 8] matrix update
n_t = f_t * n_{t-1} + i_t * k_t                // normalizer
h_t = o_t * (C_t @ q_t) / max(|n_t @ q_t|, 1) // normalized retrieval

context = h_t  // [8] context vector
```

### Context usage
- `liquid_tau`: context modulates target (1.0 → context-dependent target) via device buffer
- `selectivity`: context concatenated with h_s2 for richer priority prediction
- `backtracking`: context divergence (|h_t - h_{t-1}|) as additional plateau signal
- `adaptive RK4`: switch to RK4 when context norm exceeds threshold

### Parameters
- 6 weight matrices: W_k, W_v, W_q, W_i, W_f, W_o each [11, 8] = 528 total
- C matrix: [8, 8] = 64 floats (persistent device state)
- n vector: [8] = 8 floats (persistent device state)
- **All GPU-resident. Zero CPU compute. Zero HtoD copies in training loop.**

### Implementation
- All state lives on GPU: `qlstm_weights[528]`, `qlstm_C[64]`, `qlstm_n[8]`, `qlstm_context[8]` as CudaSlice
- New CUDA kernel `qlstm_step`: one thread, one block. Reads q_stats_buf[7] + per_branch_q_gaps[4] directly from GPU (already device-resident from reduce_current_q_stats). Runs mLSTM update on device-resident C matrix. Writes context[8] to device buffer. No DtoH readback.
- New CUDA kernel `qlstm_train_step`: self-supervised prediction loss (predict next q_mean from context). SGD weight update in-place on device. One thread.
- Context buffer `qlstm_context[8]` read by liquid_tau_rk4_step kernel and selectivity_forward kernel (both GPU-side)
- Weights initialized once in constructor via alloc_zeros (identity-like init)
- Launched every 50 training steps from the same code path as reduce_current_q_stats (after Q-stats kernel, before eval_v_range update)

---

## Component 3: Diffusion Denoising → Iterative Q-Refinement

### Source
`crates/ml-supervised/src/diffusion/denoiser.rs` — SiLU-activated DenoiserBlock with time conditioning and residual.

### Problem
Q-values are computed in one forward pass. Noisy states produce noisy Q-values. The C51 distribution captures this uncertainty (high Var[Q]) but the E[Q] readout doesn't use it for refinement.

### Solution
K=3 denoising steps that iteratively refine Q-values conditioned on distributional uncertainty:

```
Q_0 = Q_raw[B, 12]
var = Var[Q][B, 12]                          // already computed (Layer 4)

For k = 1..3:
  noise_level = sqrt(var) * (1 - k/3)       // decreasing schedule
  input = [Q_{k-1}; noise_level]             // [B, 24]
  h = SiLU(W1_k @ input + b1_k)             // [B, 24] hidden
  residual = W2_k @ h + b2_k                // [B, 12] project back
  Q_k = Q_{k-1} + 0.1 * residual            // small step

Q_refined = Q_3
```

### Parameters
- Per step: W1[24, 24] + b1[24] + W2[12, 24] + b2[12] = 924
- 3 steps: 2,772 total
- Separate buffer + Adam (like Q-attention)

### Implementation
- New CUDA kernel `q_denoise_step`: one thread per sample, evaluates the FC-SiLU-FC block
- SiLU: `x * sigmoid(x)` — new activation kernel (reuse from diffusion)
- Runs after Q-attention + graph message passing, before action selection
- 3 sequential kernel launches (steps are dependent)

---

## Component 4: TGNN GatingMechanism → Cross-Branch Graph Message Passing

### Source
`crates/ml-supervised/src/tgnn/gating.rs` — `GatingMechanism` with Q/K/V attention + temperature-scaled softmax on graph edges.

### Problem
Cross-branch Q-attention treats 12 Q-values as a flat vector. No structural knowledge that Q[0:3] are direction and Q[3:6] are magnitude. Branch dependencies are domain-specific (direction→magnitude, not urgency→direction).

### Solution
4-node directed graph with learned gated message passing:

```
Edges (domain knowledge):
  direction → magnitude   (position sizing depends on direction)
  direction → order       (order type depends on market direction)
  magnitude → urgency     (position size affects timing pressure)
  order     → urgency     (limit vs market affects urgency)

Per edge (src → dst):
  gate = sigmoid(W_gate @ [Q_src; Q_dst])        // [1] relevance
  message = gate * (W_msg @ Q_src)                // [3] gated message
  
Per node d:
  Q_d_new = Q_d + mean(incoming messages)         // residual update
```

### Parameters
- Per edge: W_gate[1, 6] + W_msg[3, 3] = 15
- 4 edges: 60 total. Negligible.

### Implementation
- New CUDA kernel `branch_graph_message_pass`: one thread per sample, 4 hardcoded edges
- Runs after Q-attention, before diffusion refinement
- In-place update with residual (Q_raw preserved)

---

## Component 5: Liquid RK4 ODE Solver → Higher-Order Training Dynamics

### Source
`crates/ml-supervised/src/liquid/ode_solvers.rs` — `RK4Solver` with 4th-order accuracy, `AdaptiveSolver` for regime-dependent switching.

### Problem
Liquid tau uses Euler integration. Euler is first-order — error accumulates linearly over training steps. Per-branch modulation oscillates instead of smoothly converging.

### Solution
Replace Euler with adaptive RK4/Euler switching:

```
// ODE: dx/dt = (1/tau) * (target - x)
// f(x) = (1/tau_d) * (target - x)

if context_norm > rk4_threshold:   // xLSTM context detects regime transition
  // RK4 step
  k1 = f(x)
  k2 = f(x + 0.5*k1)
  k3 = f(x + 0.5*k2)
  k4 = f(x + k3)
  x_new = x + (k1 + 2*k2 + 2*k3 + k4) / 6
else:
  // Euler step (fast, accurate enough in stable regime)
  x_new = x + f(x)
```

### Parameters
Zero new weights — algorithm change only. **All GPU-resident. No CPU compute.**

### Implementation
- New CUDA kernel `liquid_tau_rk4_step`: one thread, one block. Reads `per_branch_q_gap_ema[4]` (device buffer), `qlstm_context[8]` (device buffer), computes adaptive RK4/Euler, writes `liquid_mod[4]` (device buffer). No pinned memory — pure device read/write.
- `per_branch_q_gap_ema[4]` moves from CPU field to `CudaSlice<f32>` on device. Updated by the kernel directly.
- `liquid_mod[4]` moves from pinned device-mapped to plain `CudaSlice<f32>`. The c51_grad_kernel reads it via raw device pointer (same interface, simpler memory model).
- `rk4_threshold: f32` passed as kernel argument (compile-time tunable)
- Replaces the CPU-side `update_liquid_tau` method entirely — the Rust method just launches the kernel
- Launched every 50 training steps after `qlstm_step` (depends on context output)

---

## Component 6: TFT Quantile Outputs → Uncertainty-Driven Exploration

### Source
`crates/ml-supervised/src/tft/quantile_outputs.rs` — separate quantile projections with monotonicity constraints.

### Problem
Action selection uses E[Q] (mean of C51 distribution). Throws away distributional shape. An action with E[Q]=0.1, Var[Q]=0.001 (confident) is treated identically to E[Q]=0.1, Var[Q]=1.0 (uncertain). No principled exploration — relies on epsilon-greedy / Boltzmann temperature.

### Solution
Extract quantile Q-values directly from C51 atom CDF (zero extra parameters):

```
// From C51 atoms: p[j] probabilities, z[j] support values
// CDF[j] = sum(p[0..j])

Q_10th[a] = z[j] where CDF[j] first exceeds 0.10   // pessimistic
Q_50th[a] = z[j] where CDF[j] first exceeds 0.50   // median
Q_90th[a] = z[j] where CDF[j] first exceeds 0.90   // optimistic

// Adaptive quantile blend using IQN readiness:
alpha = iqn_readiness                                // 0=exploring, 1=exploiting
Q_select[a] = (1-alpha) * Q_90th[a] + alpha * Q_10th[a]
```

### Properties
- OFU (Optimism in the Face of Uncertainty): uncertain actions explored naturally
- No epsilon-greedy needed — exploration is distributional
- As training converges (readiness→1), shifts from optimistic to pessimistic (conservative)
- Uses existing C51 atoms — zero extra parameters, zero extra forward pass
- Replaces the Boltzmann-on-E[Q] action selection

### Implementation
- New CUDA kernel `quantile_q_select`: reads atom distributions, computes CDF, extracts 3 quantiles, blends
- Replaces `compute_expected_q` output for action selection (E[Q] still computed for Q-stats/loss)
- Runs in experience collection (per-timestep) — must be efficient
- readiness from existing `iqn_readiness` field

---

## Component 7: TLOB MBP-10 Features → Direct Microstructure Injection

### Source
`crates/ml-supervised/src/tlob/mbp10_feature_extractor.rs` — 51 LOB features from MBP-10 snapshots.

### Problem
All branches see the same h_s2 trunk output. Order-type decisions depend on microstructure (spread, depth, queue) but these signals are diluted through the shared trunk. Direction decisions don't need microstructure — they need momentum. The trunk can't specialize for both.

### Solution
Route 3 OFI features directly to order branch (d=2) and 3 to urgency branch (d=3), bypassing the trunk:

```
// Already in feature vector (OFI dims 42-49):
// order_extra = [bid_ask_spread, depth_imbalance_L1, queue_pressure]
// urgency_extra = [spread_velocity, depth_change_rate, trade_arrival_rate]

// Order branch: h_input = [vsn_masked; ofi_order[3]]     → [B, SH2+3]
// Urgency branch: h_input = [vsn_masked; ofi_urgency[3]]  → [B, SH2+3]
```

### Parameters
- `w_b2fc`: `[AH, SH2]` → `[AH, SH2+3]` (+768)
- `w_b3fc`: `[AH, SH2]` → `[AH, SH2+3]` (+768)
- `w_gate_2`: `[AH, SH2]` → `[AH, SH2+3]` (+768)
- `w_gate_3`: `[AH, SH2]` → `[AH, SH2+3]` (+768)
- Total: 3,072 extra params

### Implementation
- `concat_ofi_features` CUDA kernel: copies vsn_masked + appends 3 OFI features per sample
- Same pattern as Layer 3 mag_concat_qdir but for branches 2 and 3
- OFI feature indices are FIXED (hardcoded from the feature vector layout)
- Two new concat buffers: `ord_concat_buf[B, SH2+3]` and `urg_concat_buf[B, SH2+3]`
- param_sizes indices [16], [20], [38], [40] widened by 3

---

## Full Pipeline (after all 7 components)

```
INPUT: states[B, SD]

── Shared Trunk ──────────────────────────────────────
h_s1 = ReLU(W_s1 @ states + b_s1)
h_s2 = ReLU(W_s2 @ h_s1 + b_s2)
h_s2 = attention(h_s2)                               [existing]

── Value Head ────────────────────────────────────────
v_logits = W_v2 @ ReLU(W_v1 @ h_s2 + b_v1) + b_v2

── Branch Heads ──────────────────────────────────────
For each branch d:
  h_masked = h_s2 * sigmoid(h_s2 @ W_vsn1_d @ W_vsn2_d)    [CPBI VSN]
  input_d = concat(h_masked, extra_d)                        [Layer 3 + Component 7]
  gate = KAN_spline(W_gate_d @ input_d + b_gate_d)           [Component 1]
  value = W_bdf_d @ input_d + b_bdf_d
  h_bd = gate * value                                        [GLU with KAN gate]
  adv_logits_d = W_bdo_d @ h_bd + b_bdo_d

── Q-Value Pipeline ──────────────────────────────────
Q_raw[B, 12] = compute_expected_q(v_logits, adv_logits)
Q_coord = cross_branch_q_attention(Q_raw)                    [CPBI Q-attn]
Q_graph = branch_graph_message_pass(Q_coord)                 [Component 4]
Q_refined = diffusion_denoise(Q_graph, Var[Q], K=3)          [Component 3]
Q_select = quantile_blend(atoms, iqn_readiness)              [Component 6]

── Action Selection ──────────────────────────────────
action = boltzmann(Q_select)                                 [quantile-driven]

── Training Dynamics (ALL GPU — zero CPU compute) ────
q_stats_buf → qlstm_step kernel → context[8]                 [Component 2, GPU]
liquid_tau_rk4_step kernel(q_gap_ema, context) → liquid_mod[4] [Component 5, GPU]
selectivity(h_s2, context) → PER priorities                  [CPBI selective]
trajectory_backtracking(context_divergence)                   [CPBI backtrack]
```

---

## Component 8: Q-Mean Drift Correction

### Problem (observed in train-vpb4w)
Q-mean drifts 0.0 → +0.47 over 18 epochs. All Q-values shift positive due to bootstrapping bias — the DDQN target overestimates returns. This causes the model to become optimistically biased and eventually degrades action selection quality (val_Sharpe 54→31 at epoch 16-18).

### Solution
Mean-centering Q-values per training step. After `compute_expected_q` produces Q_raw[B, 12]:

```
Q_mean_all = mean(Q_raw)                     // scalar mean across all actions and samples
Q_centered[b, a] = Q_raw[b, a] - Q_mean_all  // zero-mean Q-values
```

This removes the global bias while preserving relative Q-value ordering (which action is best). The centering is applied BEFORE Q-attention, graph message passing, and action selection — so all downstream components see zero-mean Q-values.

### Properties
- Zero parameters
- One reduction kernel (mean over B×12) + one element-wise subtract
- Preserves action ranking (subtracting a constant doesn't change argmax/Boltzmann)
- Prevents bootstrapping drift from accumulating across epochs
- The C51 loss still uses the ORIGINAL Q-values (centering is for action selection only, not for the Bellman target)

### Implementation
- New CUDA kernel `q_mean_center`: compute mean of Q_raw[B, 12], subtract from all elements. One block for reduction, ceil(B*12/256) blocks for subtraction. Two-phase kernel or fused.
- Runs AFTER `compute_expected_q`, BEFORE `cross_branch_q_attention`
- Q-stats monitoring reads from ORIGINAL Q_raw (not centered) to track true Q-mean drift

---

## Component 9: Atom Utilization Recovery

### Problem (observed in train-vpb4w)
Atom utilization drops 100% → 20% over 18 epochs. The C51 distribution collapses onto a few atoms — most atoms carry negligible probability mass. This degrades the distributional representation: less uncertainty information for variance sizing, quantile extraction, and diffusion denoising.

### Solution
Atom entropy regularization with adaptive weight. Add a small loss term that penalizes low atom entropy:

```
// Per-sample, per-action atom entropy (already computed in compute_expected_q)
H = -sum(p[j] * log(p[j]))                    // atom entropy
H_max = log(num_atoms)                         // maximum entropy (uniform)
utilization = H / H_max                        // normalized [0, 1]

// Entropy bonus in the C51 loss (gradient pushes toward higher entropy)
L_entropy = -entropy_weight * mean(H)          // negative = maximize entropy
entropy_weight = base_weight * (1 - utilization_ema)  // adaptive: high weight when util is low
```

When utilization is high (atoms well-used), `entropy_weight ≈ 0` (no interference with C51 learning). When utilization drops below a threshold, entropy weight increases to push the distribution back toward using more atoms.

### Properties
- Zero extra parameters
- Atom entropy and utilization already computed in `compute_expected_q` (atom_stats buffer)
- Entropy gradient already exists in `c51_grad_kernel` (the `entropy_coeff` parameter)
- The fix is to make `entropy_coeff` ADAPTIVE based on utilization, not fixed
- The adaptive entropy_weight is computed on CPU from the Q-stats readback (utilization value) and passed as a kernel argument

### Implementation
- In `reduce_current_q_stats`: read atom_utilization from q_stats readback
- Track `utilization_ema` with adaptive alpha (same pattern as all other EMAs)
- Compute `entropy_weight = base_entropy_coeff * max(0, 1 - utilization_ema / 0.5)` — ramps up when utilization drops below 50%, zero above 50%
- Pass adaptive `entropy_weight` to `c51_grad_kernel` instead of the fixed `entropy_coeff` hyperparameter
- No new CUDA kernels — uses existing `entropy_coeff` parameter in c51_grad_kernel

---

## Implementation Order

1. **Component 8 (Q-mean centering)** — zero params, one kernel, directly fixes the Q-mean drift
2. **Component 9 (Atom entropy recovery)** — zero params, adaptive existing param, directly fixes atom collapse
3. Component 6 (Quantile Q-select) — zero params, uses existing atoms, highest OOS impact
4. Component 4 (Graph message passing) — 60 params, one kernel, structural coordination
5. Component 1 (KAN spline gates) — replaces sigmoid in existing GLU, localized change
6. Component 3 (Diffusion refinement) — 2,772 params, separate Adam, post-Q-pipeline
7. Component 7 (TLOB injection) — widens 4 weight tensors, same pattern as Layer 3
8. Component 2 (xLSTM context) — GPU kernels, feeds into Components 5 and existing systems
9. Component 5 (RK4 ODE) — depends on Component 2, GPU kernel

---

## Component 10: Adam Momentum Reset on Plateau Detection

### Problem (observed in train-vpb4w epochs 23-26)
avg_grad = 10.0 (nonzero, constant) but Q-stats are FROZEN for 4+ epochs. The gradient is active but Adam's momentum (m_buf, v_buf) has converged to a fixed point where the gradient exactly cancels the momentum update. The weights don't change despite nonzero gradient.

This is a DIFFERENT freeze than train-w6qfd (which had zero gradient). Here the optimizer is stuck, not the model.

### Solution
When the trajectory backtracking system detects a plateau (5 consecutive frozen epochs), BEFORE attempting a full rewind, try a lightweight fix: reset Adam momentum for the frozen branch weights only.

```
// In the backtracking plateau detection path:
if plateau_detected && !route_active:
  // Try momentum reset FIRST (cheaper than full rewind)
  // Reset m_buf and v_buf for branch head weights only (indices 8-41)
  // Keep trunk weights (indices 0-7) and bottleneck (24-25) momentum intact
  zero_adam_branch_momentum()
  
  // If this doesn't help after 3 epochs, THEN trigger full rewind
```

### Properties
- Zero parameters
- Targeted reset: only branch heads + VSN + GLU gate momentum (indices 8-41), not trunk
- Trunk momentum carries valuable gradient history — resetting it would lose convergence
- Branch momentum at the plateau is the problem — resetting gives fresh gradient estimates
- Lighter than full rewind (no checkpoint restore, no weight change)
- Can be tried before each rewind attempt as a pre-filter

### Implementation
- New method `reset_branch_adam_momentum()` on GpuDqnTrainer: zeros m_buf and v_buf for byte offsets corresponding to param indices 8-41 only
- Uses `cuMemsetD8Async` on the specific offset ranges (not the full buffer)
- Integrated into backtracking: plateau → try momentum reset → if still frozen after 3 epochs → full rewind

---

## Component 11: Spread Gradient Q-Gap Target Attractor

### Problem (observed in train-vpb4w)
The spread gradient has fixed intensity: `spread_scale = inv_batch * delta_z * liquid_mod`. At Q-gap=0.365 with inv_batch=6e-5 and delta_z=0.146, spread_scale ≈ 8.7e-6. This is too weak to break the plateau — the CE gradient at the fixed point exactly balances the spread.

### Solution
Scale the spread gradient by how far Q-gap is from a target. When Q-gap is below target, amplify spread. When above, reduce. This creates an attractor that prevents both collapse AND explosion:

```
target_q_gap = max(0.3, 10 * eval_q_std_ema)     // adaptive target
gap_ratio = target_q_gap / max(q_gap, 1e-6)       // >1 when below target, <1 when above
spread_amplifier = clamp(gap_ratio, 0.1, 10.0)    // bounded amplification
spread_scale = inv_batch * delta_z * liquid_mod[d] * spread_amplifier
```

At the train-vpb4w plateau: Q-gap=0.365, target=0.3 → gap_ratio=0.82 → amplifier=0.82 (slight reduction, Q-gap is above target). This is correct — the model has enough spread, the issue is Adam momentum not the spread gradient.

But if Q-gap starts declining (early sign of collapse), the amplifier ramps up BEFORE the freeze sets in.

### Properties
- Zero parameters — computed from existing Q-stats
- `spread_amplifier` is a scalar passed to c51_grad_kernel (same pattern as liquid_mod)
- Uses pinned device-mapped memory or device buffer
- target_q_gap is adaptive: 10× eval_q_std_ema, floored at 0.3

### Implementation
- Compute `spread_amplifier` in `update_liquid_tau` (alongside per-branch modulation)
- Add as kernel argument to c51_grad_kernel (or multiply into liquid_mod before passing)
- Simpler: multiply into liquid_mod[d] before writing to pinned memory: `liquid_mod[d] *= spread_amplifier`

---

## Component 12: Cosine Learning Rate Schedule with Warm Restarts

### Problem
Fixed learning rate + Adam momentum convergence = fixed point. The optimizer settles into a basin and can't escape. Standard deep learning solution: cosine annealing with warm restarts (SGDR).

### Solution
```
// Cosine schedule with warm restart every T epochs:
T_restart = 20                                      // restart period
t_local = epoch % T_restart                         // position in current cycle
lr_mult = 0.5 * (1 + cos(pi * t_local / T_restart)) // cosine decay [1.0 → 0.0]
lr = base_lr * max(lr_mult, 0.1)                    // floor at 10% of base

// At each restart (t_local == 0): Adam momentum is partially reset
// m_buf *= 0.5  (half the momentum, not full zero — preserves direction)
```

### Properties
- Zero parameters
- LR varies from base_lr to 0.1×base_lr over T=20 epochs, then restarts
- At restart: half the Adam momentum (not full reset — keep gradient direction)
- The restart acts as a natural plateau breaker: LR jumps → gradient pushes harder → escapes fixed point
- train-vpb4w froze at epoch 23 — with T=20, the first restart would be at epoch 20, potentially preventing the freeze entirely

### Implementation
- Compute `lr_mult` on CPU from epoch counter (cheap)
- Pass as scalar to Adam kernel (existing `lr` parameter)
- At restart epochs: `m_buf *= 0.5` via a simple scale kernel (or cuBLAS SSCAL)
- ~10 lines in training_loop.rs, no CUDA kernel changes

---

## Success Criteria

| Metric | Current (train-vpb4w ep 25) | Target | Source |
|--------|---------------------------|--------|--------|
| Q-mean drift | +0.18 (stabilized but biased) | ±0.05 (centered) | Component 8 |
| Atom utilization | 30% (collapsed) | >50% sustained | Component 9 |
| Adam momentum freeze | avg_grad=10 but weights frozen | No momentum stall >3 epochs | Components 10, 12 |
| Spread gradient effectiveness | 8.7e-6 (too weak at plateau) | Adaptive to Q-gap target | Component 11 |
| val_Sharpe sustained | 51.11 frozen at epoch 23 | >40 sustained 50+ epochs | Components 8-12 |
| val_Sharpe plateau | Epoch 23 freeze (train-vpb4w) | No freeze through 200 epochs | All components |
| Backtracking trigger | NEVER fired (6 frozen epochs) | Fires at 5 frozen epochs | Bug fix (see below) |

---

## Bug: Trajectory Backtracking Never Triggers

### Observed
train-vpb4w froze at epoch 23 (val_Sharpe=51.11 for 6+ epochs). Backtracking has plateau_threshold=5. Should have triggered at epoch 28. No TRAJECTORY BACKTRACK log message appeared.

### Root cause analysis
The code path at `training_loop.rs:2468` requires ALL of:
1. `plateau_epochs >= 5` — should be true (Q-gap frozen for 6+ epochs)
2. `!route_active` — true (no route active)
3. `epoch - last_rewind_epoch >= 10` — true (last_rewind_epoch=0)
4. `!checkpoints.is_empty()` — LIKELY true (improvement_rate > 0.01 early on)

The `is_q_gap_frozen()` check at line 2432 should detect the freeze: `get_per_branch_q_gaps()` returns identical Q-gaps for 6 epochs, EMA converges, `|gap - ema| < 0.001` = true.

### Possible causes
1. **Val_Sharpe detection missing:** The backtracking only checks Q-GAP freeze, not val_Sharpe freeze. If Q-gap jitters by 1e-4 between training steps (floating point), `is_q_gap_frozen` returns false → counter resets to 0. Add: check `|val_sharpe - prev_val_sharpe| < 0.01` as an ALTERNATIVE freeze detector.
2. **reduce_current_q_stats skipped:** If the training step count is zero or the Q-stats frequency check skips some epochs, `last_per_branch_q_gaps` stays stale → EMA never converges → never detected as frozen.
3. **EMA adaptive alpha too slow:** With clamped alpha=0.01, EMA needs ~100 updates to converge. If Q-stats runs less frequently than expected, 6 epochs might not be enough updates.

### Fix (Component 13)
Add val_Sharpe freeze detection as a SECOND plateau trigger:

```rust
// In run_backtracking_epoch_end, AFTER Q-gap check:
let sharpe_frozen = if self.sharpe_history.len() >= 2 {
    let prev = self.sharpe_history[self.sharpe_history.len() - 2] as f32;
    (val_sharpe_f32 - prev).abs() < 0.01  // val_Sharpe identical to previous epoch
} else {
    false
};

if q_gap_frozen || sharpe_frozen {
    self.backtracking.plateau_epochs += 1;
} else {
    self.backtracking.plateau_epochs = 0;
}
```

Also add logging so we can see WHY the trigger didn't fire:
```rust
if self.backtracking.plateau_epochs > 0 {
    info!(
        epoch, plateau_epochs = self.backtracking.plateau_epochs,
        q_gap_frozen, sharpe_frozen,
        checkpoints = self.backtracking.checkpoints.len(),
        "Backtracking: plateau detection in progress"
    );
}
```

---

## train-vpb4w Full Results Summary

### Metrics trajectory
| Epoch | val_Sharpe | Q-gap | Q-mean | atoms | avg_grad | avg_loss |
|-------|-----------|-------|--------|-------|----------|----------|
| 1 | 17.81 | 0.016 | +0.004 | 100% | 1.25 | 1.85 |
| 5 | 45.21 | 0.311 | +0.025 | 72% | 2.18 | 0.84 |
| 11 | 49.22 | 0.242 | -0.019 | 47% | — | — |
| 16 | **54.59** | 0.245 | +0.244 | 32% | — | — |
| 18 | 31.52 | 0.484 | +0.465 | 20% | 10.1 | 0.89 |
| 20 | 51.79 | 0.332 | +0.166 | 20% | 10.2 | 1.02 |
| 22 | 51.05 | 0.365 | +0.199 | 30% | 10.1 | 1.03 |
| 23-28 | **51.11** | **0.365** | **+0.180** | **30%** | **10.0** | **1.03** |

### Key findings
1. **Plateau broken at epoch 22** — val_Sharpe=51.05 vs train-w6qfd's 22.18 (frozen). CPBI system 2.3× better.
2. **New plateau at epoch 23** — different mechanism (Adam momentum cancellation, not zero gradient).
3. **Q-mean drift** — 0→+0.47 (epoch 17-18), partially recovered, settled at +0.18.
4. **Atom utilization collapse** — 100%→20% (epoch 17-18), recovered to 30%, insufficient.
5. **avg_grad = 10 constant** — gradient is NONZERO but weights frozen. Adam momentum stuck.
6. **Trajectory backtracking NEVER triggered** — detection bug, needs val_Sharpe freeze check.
7. **Peak val_Sharpe = 54.59** — proof the architecture CAN produce high-quality trading signals.

### What worked
- VSN sigmoid gating + GLU: faster convergence (avg_loss 1.85→0.84 in 5 epochs)
- Asymmetric spread + per-branch scaling: Q-gap 0.311 at epoch 5 (baseline took 13 epochs)
- Layer 3 direction conditioning: proper magnitude differentiation
- Reward rank normalization: better signal-to-noise
- Liquid tau: per-branch adaptation prevented early collapse

### What needs fixing (for next run)
1. Components 8+9: Q-mean centering + adaptive atom entropy (already implemented)
2. Components 10-12: Branch Adam reset + spread Q-gap attractor + cosine LR
3. Component 13: Val_Sharpe freeze detection for backtracking trigger
4. Components 1-7: The 7 supervised architecture transfers (KAN, xLSTM, diffusion, TGNN, RK4, quantile, TLOB)
| OOS Sharpe | ~0 oscillating | >0 sustained 20+ epochs | Components 1, 3, 6 |
| Exploration quality | Epsilon-greedy / Boltzmann | Distributional (no epsilon) | Component 6 |
| Branch Q-gap variance | Uniform across branches | Direction > urgency | Components 4, 7 |
| Training stability | Euler oscillation | RK4 smooth convergence | Components 2, 5 |
| Gate activation diversity | Uniform sigmoid | Per-neuron learned shapes | Component 1 |

---

## Risks and Mitigations

| Risk | Mitigation |
|------|-----------|
| Q-mean centering removes useful signal | Centering preserves relative ordering; C51 loss uses original Q-values |
| Adaptive entropy too aggressive | Ramps linearly, zero above 50% utilization — no interference when atoms are healthy |
| KAN spline overflow | Clamp gate output to [0, 1] |
| xLSTM C matrix divergence | Forget gate f_t ∈ (0,1) prevents unbounded growth |
| Diffusion refinement distorts Q ordering | Small step (alpha=0.1) + residual preserves ranking |
| Graph message passing creates cycles | Directed graph, no back-edges, residual connection |
| Quantile blend too optimistic | IQN readiness gates toward pessimistic as training converges |
| TLOB feature injection breaks graph capture | Features from existing buffer, no new GPU allocation needed |
| Too many components interact | Each component is independently testable; deploy incrementally |
