docs(plans): rebuild plan v2 — strict memory + GPU-oracle gates
Audited the v1 plan against the full project memory catalog. Found
several rule violations and gaps:
A3 (was: 'host memcpy_htod canonical defaults to ISV[400..406]') →
violated feedback_no_htod_htoh_only_mapped_pinned ('tests not exempt')
and short-circuited pearl_first_observation_bootstrap. Replaced with
launch-once-at-init: each controller fires once with sentinel-zero
input, kernel's first-observation-bootstrap path writes the canonical
value. No host write to ISV. Canonical pattern across the codebase.
G1-G7 gates (was: 'matches host reference' for argmax_expected_q) →
violated feedback_no_cpu_test_fallbacks. Replaced every CPU oracle
with GPU oracle: analytical synthetic inputs, property assertions,
cross-kernel validation only.
Added explicit catalog of memory rules the rebuild MUST honor (30+
rules grouped by domain). Every R-phase + every architectural decision
now cites which rules it applies.
Added A9 (PER actually wired into step) per feedback_always_per — the
flawed branch had ReplayBuffer struct but never sampled from it.
Added new ISV slots for 7 EMA inputs (RL_*_EMA_INDEX) → RL_SLOTS_END
extends to 424. The controllers' inputs live on device, not host.
Added cluster smoke discipline section: per pearl_single_window_oos
the G8 backtest gate requires >=3 walk-forward folds. Per
feedback_kill_runs_on_anomaly_quickly the dispatcher kills on NaN /
ISV saturation / kernel hang. Per pearl_q_spread misaligned, the
dispatcher MUST NOT kill on Q_SPREAD. Per feedback_argo_template_must_apply
the dispatcher refuses to submit if template not applied since edit.
Per feedback_push_before_deploy the dispatcher hard-errors if local
HEAD != origin HEAD.
Added pre-cluster validation checklist (R9 prerequisite): full
local-CUDA gate matrix that must be green before push.
Reconciled feedback_mbp10_mandatory: --mbp10-data-dir is mandatory;
--trades-data-dir is flagged as a gap (ml-alpha's MultiHorizonLoader
does not currently consume a separate trades stream — OFI is derived
from MBP-10 snapshot deltas). Either wire trades in a future plan or
ship explicitly without; the rebuild does the latter and flags it.
Plan grew from 381 to ~580 lines because the memory audit exposed
several decisions that needed explicit treatment instead of implicit
'follow project pattern' references.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4,122 +4,148 @@
|
||||
> on branch `ml-alpha-phase-f-g-flawed` (commits 99a125cdb..b3808a5ac).
|
||||
> Baseline: `ml-alpha-phase-a` reset to commit `9114374d2`
|
||||
> (Phase E.3b activation).
|
||||
>
|
||||
> **Discipline:** Smokes are expensive ($5–20 / 30–120 min L40S each)
|
||||
> and producing G8 numbers from a buggy trainer wastes both cluster
|
||||
> credit and gate-evaluation cycles. Every rule below is non-negotiable.
|
||||
> Every gate is falsifiable locally before any `argo submit`.
|
||||
|
||||
## Why this rebuild
|
||||
|
||||
The prior Phase F + G attempt landed 9 commits that shipped a trainer
|
||||
with multiple production-blocking defects. The defects were not bugs in
|
||||
the new code so much as **propagation of pre-existing violations** in
|
||||
`step_with_lobsim` plus **incomplete wiring of the ISV adaptive-control
|
||||
infrastructure**. The convergence-gate fixtures (`dqn_reward_signal`,
|
||||
`ppo_reward_signal`, then named `dqn_toy` / `ppo_toy`) hid every defect
|
||||
because their `MockLobEnv` has `done = true` every step, reward in
|
||||
`[-1, +1]`, and horizon = 1. The cluster-bound smoke was the only
|
||||
mechanism that would have exposed the problems — at $5–20 / 30–120 min
|
||||
each.
|
||||
The prior Phase F + G attempt landed 9 commits with multiple
|
||||
production-blocking defects. None of the defects were exotic bugs;
|
||||
all of them were direct violations of pre-existing project memory
|
||||
that I missed because the `MockLobEnv` "toy bandit" fixture has
|
||||
`done = true` every step, reward ∈ `[-1, +1]`, and horizon = 1 —
|
||||
which hides every failure mode the production cluster will see.
|
||||
|
||||
### Defects in the flawed branch
|
||||
### Defect table — each row maps to a memory rule the flawed branch violated
|
||||
|
||||
1. **ISV[400..406] uninitialised in production paths.** The trainer
|
||||
`alloc_zeros` the ISV array; only `rl_lr_controller` was launched.
|
||||
Production consumers read sentinel `0`:
|
||||
- `bellman_target_projection.cu:133` reads γ = 0 → Bellman target
|
||||
reduces to `r + 0·V(s_{t+1}) = r`. Q-head learns immediate-reward
|
||||
only, zero credit assignment.
|
||||
- `ppo_clipped_surrogate.cu:205,305` reads ε = 0 → `clip(r, 1, 1) = 1`
|
||||
always. PPO trust region collapses.
|
||||
- `ppo_clipped_surrogate.cu:210` reads entropy coef = 0 → no entropy
|
||||
bonus, policy collapses to deterministic immediately.
|
||||
| # | Defect | Memory rule violated |
|
||||
|---|--------|----------------------|
|
||||
| 1 | ISV[400..406] uninitialised in production paths (γ=0, ε=0, entropy=0 in kernels) | `feedback_wire_everything_up`, `feedback_isv_for_adaptive_bounds` |
|
||||
| 2 | `rl_reward_scale_controller` drifts to 1e3 on no-trade steps | `pearl_first_observation_bootstrap` |
|
||||
| 3 | 6 controllers exist as .cu files but never launched | `feedback_wire_everything_up`, `feedback_no_partial_refactor` |
|
||||
| 4 | Target net never soft-updated (τ has no consumer) | `feedback_no_stubs`, `feedback_wire_everything_up` |
|
||||
| 5 | `step_with_lobsim` has 5 DtoH + host argmax + host EMA + host advantage loops | `feedback_cpu_is_read_only`, `pearl_cold_path_no_exception_to_gpu_drives` |
|
||||
| 6 | PER buffer exists in `src/rl/replay.rs` but trainer never samples from it | `feedback_always_per` |
|
||||
| 7 | "Toy bandit" framing leaked into production (`alpha_rl_train.rs` shipped with `next_snapshots = snapshots` until caught mid-review) | `feedback_no_stubs`, `feedback_no_quickfixes`, `feedback_extending_existing_code_audits_for_existing_violations` |
|
||||
| 8 | No NaN / divergence abort in production CLI | `feedback_stop_on_anomaly`, `feedback_kill_runs_on_anomaly_quickly` |
|
||||
| 9 | Convergence-gate fixtures (`dqn_toy.rs`, `ppo_toy.rs`) used `MockLobEnv` with state-invariant rewards — intrinsically can't catch defects #1, #2, #5 | `pearl_tests_must_prove_not_lock_observations`, `feedback_no_cpu_test_fallbacks` |
|
||||
|
||||
2. **`rl_reward_scale_controller` drifts to `1e3` on no-trade steps.**
|
||||
The launcher fired every step regardless of `mean_abs_pnl_ema`
|
||||
readiness; with `ema = 0` and `prev = REWARD_SCALE_BOOTSTRAP = 1.0`,
|
||||
the kernel computed `target = 1/max(0, 1e-3) = 1000`, Wiener-blended
|
||||
toward it, and ramped the scale to `1e3` over the first ~10 hold
|
||||
steps. Toy fixture hid this because `done = true` every step.
|
||||
### Root-cause pattern
|
||||
|
||||
3. **6 controllers exist as .cu files but never launched.**
|
||||
`rl_gamma`, `rl_target_tau`, `rl_ppo_clip`, `rl_entropy_coef`,
|
||||
`rl_rollout_steps`, `rl_per_alpha`. The build system compiles their
|
||||
cubins; no Rust launcher fires them. ISV bootstrap (defect #1)
|
||||
would have masked this even if the controllers had been wired.
|
||||
Defects #1, #3, #5, #6 are all the same shape: **the pre-existing
|
||||
Phase E.3b code already violated current memory rules, and the
|
||||
flawed Phase F extended the pattern instead of flagging it.** The new
|
||||
memory pearl `feedback_extending_existing_code_audits_for_existing_violations`
|
||||
exists specifically to prevent this in future work.
|
||||
|
||||
4. **Target-net soft update missing.** `DqnHead` owns `target_w_d /
|
||||
target_b_d` but no kernel updates them. The "target network" is
|
||||
actually the initial random init for the entire run — no soft Polyak
|
||||
update, no hard sync. Double-DQN Bellman target uses an
|
||||
untrained-static target.
|
||||
## Strict memory rules this rebuild MUST honor
|
||||
|
||||
5. **`feedback_cpu_is_read_only` violation in `step_with_lobsim`.**
|
||||
Host code:
|
||||
- 5 DtoH copies per step (Q / V / π logits at h_t, V / Q at h_{t+1})
|
||||
- Host-side softmax + Thompson categorical sample + argmax loops
|
||||
over `[B][N_ACTIONS][Q_N_ATOMS]`
|
||||
- Host EMA updates for `mean_abs_pnl_ema`
|
||||
- Host advantage / return computation
|
||||
- HtoD copies for `actions`, `rewards`, `dones`
|
||||
The pre-existing Phase E.3b code already had this pattern; the
|
||||
flawed Phase F extended it instead of rebuilding it as GPU-resident
|
||||
per `pearl_cold_path_no_exception_to_gpu_drives`.
|
||||
Explicit catalog, every rule named below is gating. If the rebuild
|
||||
diverges from any of these the work does NOT ship.
|
||||
|
||||
6. **"Toy" framing in production code.** Test files `dqn_toy.rs` /
|
||||
`ppo_toy.rs`, fixture constructor `MockLobEnv::toy_bandit()`, and
|
||||
pervasive "toy bandit" / "for the toy bandit smoke" comments leaked
|
||||
into production source. Worse: `alpha_rl_train.rs` shipped with
|
||||
`next_snapshots = snapshots` — production code applying a
|
||||
toy-fixture simplification — until that one issue got caught
|
||||
mid-review.
|
||||
### GPU/CPU contract
|
||||
- `feedback_cpu_is_read_only` — no CPU compute, no CPU forwards, no GPU↔CPU roundtrips on hot path
|
||||
- `feedback_no_htod_htoh_only_mapped_pinned` — mapped-pinned only; tests not exempt
|
||||
- `feedback_no_atomicadd` — block tree-reduce / per-batch scratch + `reduce_axis0`, never atomicAdd
|
||||
- `feedback_no_cpu_test_fallbacks` — GPU oracle tests only; no CPU reference implementations
|
||||
- `feedback_no_nvrtc` — pre-compiled cubins via `build.rs`
|
||||
- `pearl_no_host_branches_in_captured_graph` — host branches inside captured graphs break recording
|
||||
- `pearl_cold_path_no_exception_to_gpu_drives` — "cold path" is not a license for CPU compute
|
||||
- `pearl_fused_per_group_statistics_oracle` — K groups × N stats = ONE fused kernel
|
||||
- `pearl_build_rs_rerun_if_env_changed` — every `std::env::var()` paired with `cargo:rerun-if-env-changed`
|
||||
|
||||
7. **No NaN / divergence abort in `alpha_rl_train.rs`.** A NaN-poisoned
|
||||
run would burn the full 14400-second `activeDeadlineSeconds` budget
|
||||
silently.
|
||||
### Code quality
|
||||
- `feedback_no_stubs` — wire it for real or delete
|
||||
- `feedback_no_todo_fixme` — no TODO/FIXME/XXX markers
|
||||
- `feedback_no_hiding` — never suppress with `_` prefix or `#[allow]`
|
||||
- `feedback_no_legacy_aliases` — no deprecated wrappers
|
||||
- `feedback_no_feature_flags` — no `enable_*` / `use_*` booleans
|
||||
- `feedback_no_quickfixes` — every issue gets the proper fix
|
||||
- `feedback_no_functionality_removal` — never delete features; fix what's broken
|
||||
- `feedback_fix_everything_aggressively` — zero tolerance, no deferrals
|
||||
- `feedback_extending_existing_code_audits_for_existing_violations` — when extending existing code, audit it FIRST against current rules
|
||||
|
||||
### Pattern lesson
|
||||
### Architecture discipline
|
||||
- `feedback_single_source_of_truth_no_duplicates` — no `v2_` / parallel impls
|
||||
- `feedback_no_partial_refactor` — contract change = every consumer migrates atomically
|
||||
- `feedback_wire_everything_up` — every kernel/module wired in same commit; no orphans
|
||||
- `feedback_isv_for_adaptive_bounds` — adaptive bounds live in ISV, not hardcoded constants
|
||||
- `feedback_adaptive_not_tuned` — signal-driven (EMA/windowed), not tuned constants
|
||||
- `feedback_magnitude_must_be_useful` — DQN magnitude branch never deleted
|
||||
|
||||
The convergence-gate fixtures verified the gradient mechanics
|
||||
(`reward_signal_drives_argmax_to_rewarded_action`) but their
|
||||
state-invariant fixture intrinsically *can't* test:
|
||||
- Sparse-reward credit assignment (defect #1's γ = 0 effect)
|
||||
- Reward-magnitude drift (defect #2)
|
||||
- Trust-region clipping (defect #1's ε = 0 effect)
|
||||
- Target-network staleness (defect #4)
|
||||
### Training discipline
|
||||
- `feedback_always_per` — PER always enabled; non-PER paths are dead code
|
||||
- `feedback_mbp10_mandatory` — `--mbp10-data-dir` mandatory; trades-data-dir paired for OFI completeness
|
||||
- `feedback_default_to_l40s_pool` — defaults to `ci-training-l40s`, not H100
|
||||
- `feedback_stop_on_anomaly` — terminate on training anomaly, diagnose, fix, re-run
|
||||
- `feedback_kill_runs_on_anomaly_quickly` — kill smoke at first useful signal
|
||||
|
||||
**Pre-cluster falsifiability requires fixtures that exercise the same
|
||||
failure modes the cluster will see.** A short bull-market loop with
|
||||
state-dependent rewards + per-step holds + multi-step trades is the
|
||||
minimum.
|
||||
### Workflow
|
||||
- `feedback_push_before_deploy` — always `git push` before `argo submit`
|
||||
- `feedback_argo_template_must_apply` — `kubectl apply -f <template>` before `argo submit --from=wftmpl/<name>`
|
||||
- `feedback_no_concurrent_agents_shared_tree` — worktrees or serial dispatch
|
||||
- `feedback_registry_entries_need_dispatch_arms` — registry entries require matching reset arms in same commit
|
||||
- `feedback_smoke_validation_before_structural_priors` — validate wiring with neutral config first
|
||||
|
||||
### Reward + controller pearls
|
||||
- `pearl_first_observation_bootstrap` — sentinel=0; first observation replaces directly
|
||||
- `pearl_wiener_alpha_floor_for_nonstationary` — α-floor 0.4 in control loops
|
||||
- `pearl_wiener_optimal_adaptive_alpha` — α = diff_var / (diff_var + sample_var + ε); no tuned α
|
||||
- `pearl_blend_formulas_must_have_permanent_floor` — `max(real, floor)`, not blend
|
||||
- `pearl_one_unbounded_signal_per_reward` — exactly ONE unbounded multiplicand per reward term
|
||||
- `pearl_symmetric_clamp_audit` — kernel-derived bounded scalars use bilateral `fmaxf(lo, fminf(x, hi))`
|
||||
- `pearl_audit_unboundedness_for_implicit_asymmetry` — symmetric bounds may erase implicit asymmetry; use asymmetric bounded clamp
|
||||
- `pearl_event_driven_reward_density_alignment` — event-driven objectives need event-density reward
|
||||
- `pearl_bounded_modifier_outputs_require_structural_activation` — bounded outputs feeding multiplicative modifiers MUST use structural activation (sigmoid/tanh), not runtime clamps
|
||||
- `pearl_controller_anchors_isv_driven` — every controller anchor/target/cap is ISV-driven
|
||||
- `pearl_thompson_for_distributional_action_selection` — Thompson at rollout, argmax for Bellman target only
|
||||
|
||||
### Backtest / OOS discipline
|
||||
- `pearl_single_window_oos_is_not_oos` — ≥3 walk-forward folds, report mean ± SD
|
||||
- `pearl_q_spread_kill_criterion_misaligned_for_distributional_q` — DON'T use Q_SPREAD as kill metric (spikes signal GOOD balance under C51+Thompson)
|
||||
- `pearl_tests_must_prove_not_lock_observations` — assert invariants, not observed values
|
||||
- `pearl_action_pruning_falsified` — KEEP 9 actions (pruning 9→4 regressed Sharpe 2.4×)
|
||||
- `pearl_c51_thompson_closed_phase_e3_gap` — C51 + Thompson is the proven combo
|
||||
|
||||
### Kernel hygiene pearls
|
||||
- `pearl_kernel_input_filter_must_match_validation_surface` — per-depth validation
|
||||
- `pearl_behavioral_sentinel_values_poison_downstream_consumers` — per-emission-site counters localize bugs
|
||||
- `pearl_cooperative_staging_eliminates_redundant_reads` — stage per-batch rows once
|
||||
- `pearl_coalesce_via_thread_role_swap` — output-row → output-column thread role for non-coalesced writes
|
||||
- `pearl_reduce_axis0_column_tile` — block-per-column 32-wide column tile
|
||||
|
||||
## Architectural decisions
|
||||
|
||||
### A1. GPU-pure action submission via the existing decision-policy path
|
||||
### A1 — Delete `LobEnv` trait; drive `LobSimCuda` via kernel-driven decision path
|
||||
|
||||
The pre-existing production decision pipeline (`step_decision_with_latency`
|
||||
in `crates/ml-backtesting/src/sim/mod.rs`) already submits actions
|
||||
The production decision pipeline (`step_decision_with_latency` in
|
||||
`crates/ml-backtesting/src/sim/mod.rs`) already submits actions
|
||||
GPU-pure: `alpha_probs_d` (device) feeds `decision_policy_kernel`
|
||||
which writes `market_targets_d` (device) which the fill kernel reads.
|
||||
No host roundtrip.
|
||||
which writes `market_targets_d` (device). No host roundtrip.
|
||||
|
||||
**Decision:** the integrated RL trainer drives the same kernel path.
|
||||
Action sampling happens IN THE DECISION KERNEL (or a new
|
||||
`rl_action_kernel` that lives parallel to it), reading Q / π logits +
|
||||
ISV[entropy, ε] from device + writing to `market_targets_d`. The
|
||||
`LobEnv` trait + `LobSimEnvAdapter` from the flawed branch are
|
||||
DELETED. The trainer interacts with `LobSimCuda` via the existing
|
||||
decision-policy entry point.
|
||||
**Decision:** the RL trainer drives the same kernel path. Action
|
||||
sampling lives in a new `rl_action_kernel` (Thompson over Q logits)
|
||||
parallel to the production decision kernel; both write to
|
||||
`market_targets_d`. `LobEnv` trait, `LobSimEnvAdapter`, and
|
||||
`MockLobEnv` are DELETED. Convergence-gate fixtures use real
|
||||
`LobSimCuda` with synthetic book data.
|
||||
|
||||
**Consequence:** the trait-abstraction layer that let `MockLobEnv` mock
|
||||
the env is removed. Convergence-gate tests must run against a real
|
||||
`LobSimCuda` with synthetic book data (still feasible — fixtures
|
||||
inject snapshots).
|
||||
Rules honored: `feedback_cpu_is_read_only`,
|
||||
`pearl_cold_path_no_exception_to_gpu_drives`,
|
||||
`feedback_single_source_of_truth_no_duplicates` (no duplicate
|
||||
action-submission paths), `feedback_no_legacy_aliases` (no trait
|
||||
adapter left as a shim).
|
||||
|
||||
### A2. All EMAs live in ISV
|
||||
### A2 — All EMAs live in ISV
|
||||
|
||||
Currently `mean_abs_pnl_ema` lives as a host `f32` field on
|
||||
`IntegratedTrainer`. **Decision:** every EMA the controllers consume
|
||||
lives in an ISV slot, updated by a dedicated per-step kernel that reads
|
||||
the source signal from device buffers (rewards_d, dones_d, etc.).
|
||||
Every controller-input EMA lives in an ISV slot, updated by a
|
||||
dedicated per-step kernel that reads the source signal from device
|
||||
buffers (rewards_d, dones_d, actions_d, etc.).
|
||||
|
||||
New ISV slots needed for the EMA inputs:
|
||||
New ISV slots:
|
||||
- `RL_MEAN_TRADE_DURATION_EMA_INDEX` (input to `rl_gamma_controller`)
|
||||
- `RL_Q_DIVERGENCE_EMA_INDEX` (input to `rl_target_tau_controller`)
|
||||
- `RL_KL_PI_EMA_INDEX` (input to `rl_ppo_clip_controller`)
|
||||
@@ -128,254 +154,498 @@ New ISV slots needed for the EMA inputs:
|
||||
- `RL_TD_KURTOSIS_EMA_INDEX` (input to `rl_per_alpha_controller`)
|
||||
- `RL_MEAN_ABS_PNL_EMA_INDEX` (input to `rl_reward_scale_controller`)
|
||||
|
||||
That's 7 new slots → `RL_SLOTS_END = 424`.
|
||||
7 new slots → `RL_SLOTS_END = 424`.
|
||||
|
||||
### A3. ISV bootstrap at trainer construction
|
||||
All EMA updates use `pearl_wiener_optimal_adaptive_alpha` (adaptive α
|
||||
from observed diff/sample variance ratio) with floor 0.4 per
|
||||
`pearl_wiener_alpha_floor_for_nonstationary`. Sentinel-zero bootstrap
|
||||
per `pearl_first_observation_bootstrap`.
|
||||
|
||||
Defect #1 and the Wiener-blend behavior of the controllers both
|
||||
require slots to be at known-good values before any kernel reads them.
|
||||
**Decision:** trainer `new()` does ONE memcpy_htod writing canonical
|
||||
defaults to ISV[400..406]:
|
||||
Rules honored: `feedback_isv_for_adaptive_bounds`,
|
||||
`pearl_controller_anchors_isv_driven`, `feedback_adaptive_not_tuned`.
|
||||
|
||||
| Slot | Default | Source |
|
||||
|------|---------|--------|
|
||||
| RL_GAMMA_INDEX (400) | 0.99 | DQN convention |
|
||||
| RL_TARGET_TAU_INDEX (401) | 0.005 | DQN convention |
|
||||
| RL_PPO_CLIP_INDEX (402) | 0.2 | PPO original paper |
|
||||
| RL_ENTROPY_COEF_INDEX (403) | 0.01 | PPO convention |
|
||||
| RL_N_ROLLOUT_STEPS_INDEX (404) | 2048 | PPO convention |
|
||||
| RL_PER_ALPHA_INDEX (405) | 0.6 | Schaul et al. 2016 |
|
||||
| RL_REWARD_SCALE_INDEX (406) | 1.0 | identity pass-through |
|
||||
### A3 — Bootstrap controllers via launch-once at init, NOT host memcpy_htod
|
||||
|
||||
Controllers then ADAPT from these defaults rather than bootstrapping
|
||||
from sentinel zero. The sentinel-zero bootstrap path in each kernel
|
||||
stays as a safety net but is never the production cold-start path.
|
||||
The prior plan said "memcpy_htod canonical defaults to ISV[400..406]".
|
||||
That violated `feedback_no_htod_htoh_only_mapped_pinned` (tests not
|
||||
exempt) and short-circuited `pearl_first_observation_bootstrap`.
|
||||
|
||||
### A4. Target-net soft update consumer for ISV[401]
|
||||
**Decision:** in `IntegratedTrainer::new`, after the ISV alloc_zeros,
|
||||
launch each controller ONCE with a zero (sentinel) input EMA. Each
|
||||
kernel sees `prev = 0` (sentinel), executes its first-observation
|
||||
bootstrap path, and writes its canonical value (γ=0.99, τ=0.005, ε=0.2,
|
||||
entropy_coef=0.01, n_rollout_steps=2048, per_α=0.6, reward_scale=1.0).
|
||||
|
||||
New `dqn_target_soft_update` kernel: per-step element-wise
|
||||
Subsequent in-loop launches see non-sentinel ISV → execute Wiener
|
||||
blend with real EMA input.
|
||||
|
||||
No host write to ISV. Bootstrap path is canonical, observable in HEALTH
|
||||
diagnostics, and follows the same `pearl_first_observation_bootstrap`
|
||||
pattern as every other adaptive controller in the codebase.
|
||||
|
||||
Rules honored: `pearl_first_observation_bootstrap`,
|
||||
`feedback_isv_for_adaptive_bounds`,
|
||||
`feedback_no_htod_htoh_only_mapped_pinned`,
|
||||
`feedback_wire_everything_up`.
|
||||
|
||||
### A4 — Target-net soft update consumer for ISV[401]
|
||||
|
||||
New `dqn_target_soft_update` kernel: element-wise per-weight
|
||||
`target_w[i] = (1-τ)·target_w[i] + τ·w[i]` reading τ from `ISV[401]`.
|
||||
Parallel across weight elements (no contention; no atomicAdd needed).
|
||||
Launched from `step_after_encoder_forward` after the Q-head Adam
|
||||
update.
|
||||
|
||||
### A5. GPU-resident action sampling
|
||||
Rules honored: `feedback_wire_everything_up`,
|
||||
`feedback_no_atomicadd`, `feedback_no_stubs`.
|
||||
|
||||
### A5 — GPU-resident action sampling kernels
|
||||
|
||||
New kernels:
|
||||
- `thompson_sample_categorical` — input: Q logits, ISV[entropy_coef],
|
||||
device PRNG state. Output: per-batch action index in `market_targets`
|
||||
format directly.
|
||||
- `argmax_expected_q` — input: Q logits. Output: per-batch
|
||||
next-action index.
|
||||
- `log_pi_at_action` — input: π logits + action_d. Output: per-batch
|
||||
log π(a_t) — feeds the PPO importance-ratio path.
|
||||
- `thompson_sample_categorical` — per-batch: softmax over atoms,
|
||||
sample one atom per action via device PRNG, pick argmax over
|
||||
sampled per-action returns. Writes action index to `market_targets_d`
|
||||
directly. Single-writer-per-block. Device PRNG state =
|
||||
`curandStatePhilox4_32_10_t` allocated at trainer init, advanced per
|
||||
step. Reads ISV[entropy_coef] if exploration-temperature scaling
|
||||
applies.
|
||||
- `argmax_expected_q` — per-batch: softmax + expected value + argmax.
|
||||
Writes next_actions for the Bellman target argmax. Single-writer.
|
||||
- `log_pi_at_action` — per-batch: softmax + log-prob lookup at action.
|
||||
Writes log_pi_old for PPO importance ratio. Single-writer.
|
||||
|
||||
Device PRNG via a per-batch `curandStatePhilox4_32_10_t` allocated at
|
||||
trainer init + advanced per step.
|
||||
Per `pearl_thompson_for_distributional_action_selection`: Thompson at
|
||||
rollout (rl_action_kernel uses `thompson_sample_categorical`), argmax
|
||||
only for Bellman target (uses `argmax_expected_q`).
|
||||
|
||||
### A6. GPU-resident advantage / return / TD computation
|
||||
Rules honored: `feedback_cpu_is_read_only`, `feedback_no_atomicadd`,
|
||||
`pearl_thompson_for_distributional_action_selection`,
|
||||
`pearl_no_host_branches_in_captured_graph`.
|
||||
|
||||
New `compute_advantage_return` kernel: per-batch `r + γ(1-done)·V_tp1`
|
||||
and `r_t - V_t`, reading γ from `ISV[400]`. Replaces the host loop in
|
||||
the flawed branch's `step_with_lobsim`.
|
||||
### A6 — GPU-resident advantage / return / TD computation
|
||||
|
||||
### A7. Fee model
|
||||
New `compute_advantage_return` kernel: per-batch
|
||||
`returns[b] = r_b + γ(1 − done_b) · v_tp1[b]`,
|
||||
`advantages[b] = returns[b] − v_t[b]`. Reads γ from `ISV[400]`,
|
||||
v_t / v_tp1 from device buffers. Element-wise; trivially parallel.
|
||||
|
||||
The flawed F.3a's approach (`order_match.cu` deduct fee + new
|
||||
`upload_cost_per_lot_per_side` API) was correct in principle, mirroring
|
||||
the existing `apply_fill_to_pos` pattern from `resting_orders.cu`. The
|
||||
rebuild keeps this change.
|
||||
Replaces the host advantage/return loop in the flawed branch's
|
||||
`step_with_lobsim`.
|
||||
|
||||
### A8. Loader pair API
|
||||
Rules honored: `feedback_cpu_is_read_only`,
|
||||
`pearl_controller_anchors_isv_driven`.
|
||||
|
||||
The flawed Phase G's `MultiHorizonLoader::next_sequence_pair` was
|
||||
correct (true `(s_t, s_{t+1})` consecutive sampling). The rebuild keeps
|
||||
this addition.
|
||||
### A7 — Fee model: port F.3a unchanged
|
||||
|
||||
## Falsifiability gates — pre-cluster
|
||||
`order_match.cu::submit_market_immediate` adds per-fill cost
|
||||
deduction mirroring `apply_fill_to_pos` from `resting_orders.cu:213-219`.
|
||||
`upload_cost_per_lot_per_side` API on `LobSimCuda`. Default zero,
|
||||
opt-in (CLI defaults to ES $1.25/side, $2.50 round-trip).
|
||||
|
||||
Each gate is a local CUDA test that exercises the failure mode it
|
||||
covers. None of these existed in the flawed branch.
|
||||
Single-writer-per-block plain `+=` for `total_fees_per_b[b]` (line 79
|
||||
of `submit_market_immediate` has `threadIdx.x != 0 → return`).
|
||||
|
||||
| Gate | Asserts | Catches defect |
|
||||
|------|---------|----------------|
|
||||
| G1 | ISV[400..406] held at canonical defaults after `IntegratedTrainer::new` | #1 |
|
||||
| G2 | `RL_REWARD_SCALE_INDEX` stays at 1.0 for first N hold-only steps | #2 |
|
||||
| G3 | Each controller's launcher visibly mutates its ISV slot after first non-sentinel input | #3 |
|
||||
| G4 | `target_w` Frobenius norm drifts from `w` only after the soft-update kernel fires | #4 |
|
||||
| G5 | `cudaMemcpy` count per `step_with_lobsim` is 0 host-initiated (other than ISV diagnostic readback) | #5 |
|
||||
| G6 | Bull-market forced-long 50-trade loop: mean reward per trade > 0 (already in flawed F.3b's `reward_calibration.rs`) | (regression-coverage for fee + scale) |
|
||||
| G7 | Run for N steps; abort + non-zero exit code if any per-head loss is non-finite | #7 |
|
||||
Rules honored: `feedback_no_atomicadd` (single-writer),
|
||||
`feedback_single_source_of_truth_no_duplicates` (mirrors existing
|
||||
`apply_fill_to_pos` pattern).
|
||||
|
||||
G5 is the load-bearing gate for the GPU-pure decision (A1). Tooling:
|
||||
hook `cudaMemcpyAsync` via a CUPTI counter or count Rust-side
|
||||
`memcpy_htod` / `memcpy_dtoh` calls during the step.
|
||||
### A8 — Loader pair API: port G fix unchanged
|
||||
|
||||
`MultiHorizonLoader::next_sequence_pair` returns
|
||||
`(LabeledSequence, LabeledSequence)` at adjacent anchors in the same
|
||||
file. Refactor `next_sequence`'s body into `build_sequence_at(file_idx,
|
||||
anchor)` helper shared by both API surfaces.
|
||||
|
||||
Rules honored: `feedback_no_partial_refactor` (helper shared by both
|
||||
call sites; no duplicate windowing logic).
|
||||
|
||||
### A9 — Wire PER replay into the training step
|
||||
|
||||
`feedback_always_per` says PER is always on; the flawed branch had
|
||||
the `ReplayBuffer` struct but never sampled from it in
|
||||
`step_with_lobsim`. The rebuild WIRES the buffer:
|
||||
|
||||
- Every step pushes the current transition to the replay buffer.
|
||||
- Every step samples a batch from the buffer (priority^α) for the
|
||||
off-policy Q-head update.
|
||||
- TD-error magnitudes from the Q backward feed
|
||||
`update_priorities`.
|
||||
- `RL_PER_ALPHA_INDEX` (already in ISV[405], adaptive via
|
||||
`rl_per_alpha_controller`) drives the priority exponent.
|
||||
|
||||
PER buffer storage: device-resident `h_t`/`next_h_t` per
|
||||
`pearl_cold_path_no_exception_to_gpu_drives`. Sum-tree starts
|
||||
naive O(N) per `replay.rs` doc (acceptable at capacity ≤ 4096); GPU
|
||||
sum-tree is a Phase R-future enhancement.
|
||||
|
||||
Rules honored: `feedback_always_per`, `feedback_no_stubs` (replay
|
||||
buffer becomes load-bearing instead of orphan), `feedback_wire_everything_up`.
|
||||
|
||||
## Falsifiability gates — local CUDA BEFORE cluster
|
||||
|
||||
Each gate has a local CUDA test (`#[ignore = "requires CUDA"]`,
|
||||
runnable on the dev RTX 3050 Ti per CLAUDE.md) that exercises the
|
||||
failure mode the convergence-gate fixtures couldn't catch. Per
|
||||
`feedback_no_cpu_test_fallbacks`: every test oracle is either
|
||||
analytical synthetic, property-based, or cross-kernel GPU oracle.
|
||||
NEVER a CPU reference implementation.
|
||||
|
||||
| Gate | Asserts (invariant, not observed value) | Test design (GPU oracle only) | Catches defect |
|
||||
|------|----------------------------------------|-------------------------------|----------------|
|
||||
| G1 | ISV[400..406] equal canonical values after `IntegratedTrainer::new` | Construct trainer, read ISV slice via HEALTH_DIAG mapped-pinned, assert each slot equals its bootstrap value | #1 |
|
||||
| G2 | `RL_REWARD_SCALE_INDEX` stays in `[1e-3, 1e3]` and remains at 1.0 across N hold-only env steps (no closed trades) | Drive trainer for 100 steps with synthetic book where market never crosses (no fills); assert ISV[406] never moves from 1.0 | #2 |
|
||||
| G3 | Each controller's launcher visibly mutates its ISV slot after first non-sentinel EMA input | Construct trainer, manually launch one controller with a non-zero scalar input via the EMA-update kernel, assert ISV slot moved off bootstrap value | #3 |
|
||||
| G4 | `target_w` Frobenius distance from `w` is zero before `dqn_target_soft_update` ever fires, and >0 after one fire | Construct trainer, snapshot `target_w_d`, fire 1 step (which fires soft-update once), snapshot again, assert any element changed | #4 |
|
||||
| G5 | Zero host-initiated `cudaMemcpyAsync` calls in `step_with_lobsim`'s hot path (other than HEALTH_DIAG readback) | Hook `cudaMemcpyAsync` via CUPTI counter OR count Rust-side `memcpy_htod`/`memcpy_dtoh` calls; assert count = expected ISV-readback count | #5 |
|
||||
| G6 | PER buffer's `len()` grows by exactly 1 per `step_with_lobsim` call; `sample_indices` returns batch of correct size | Drive trainer for 100 steps, assert `replay.len() == min(100, capacity)`; sample, assert batch size | #6 |
|
||||
| G7 | Bull-market 50-cycle forced-long produces mean per-trade reward > 0 on synthetic `LobSimCuda` with ES-realistic fees | Port the flawed branch's `reward_calibration.rs` to drive `LobSimCuda` directly (no `LobEnv` adapter) | (regression coverage for fee + scale wiring) |
|
||||
| G8 | NaN abort: trainer exits with non-zero code if any per-head loss is non-finite | Inject NaN into a synthetic reward stream, assert process exits 2 within 1 step | #8 |
|
||||
|
||||
G5 is the load-bearing gate for the GPU-pure decision (A1).
|
||||
Implementation: a thin Rust wrapper around `CudaStream::memcpy_*`
|
||||
that increments a counter; tests assert the counter after one
|
||||
`step_with_lobsim` call. CUPTI is a follow-up enhancement.
|
||||
|
||||
## Cluster smoke discipline (Phase H equivalent)
|
||||
|
||||
Per `pearl_single_window_oos_is_not_oos`: the G8 backtest gate
|
||||
(`profit_factor > 1.0`) MUST be evaluated across ≥3 walk-forward
|
||||
folds, reporting mean ± SD. A single-window pass is NOT G8 pass.
|
||||
|
||||
Per `feedback_kill_runs_on_anomaly_quickly`: the cluster smoke
|
||||
script tails logs and kills the run if any of the following fire:
|
||||
- Any per-head loss non-finite (G8)
|
||||
- `RL_REWARD_SCALE_INDEX` clipped at `1e-3` or `1e3` (instrumentation
|
||||
saturation indicator)
|
||||
- Per-step wall time > 10× the rolling median (kernel hang indicator)
|
||||
|
||||
DO NOT kill on Q-distribution spread (per
|
||||
`pearl_q_spread_kill_criterion_misaligned_for_distributional_q`:
|
||||
Q_SPREAD spikes signal GOOD balance under C51+Thompson).
|
||||
|
||||
Per `feedback_push_before_deploy`: dispatcher script verifies
|
||||
`git push` succeeded before `argo submit`. Hard-error if local HEAD
|
||||
differs from `origin/<branch>` after push.
|
||||
|
||||
Per `feedback_argo_template_must_apply`: dispatcher prints a
|
||||
top-of-output reminder `kubectl apply -n foxhunt -f
|
||||
infra/k8s/argo/alpha-rl-template.yaml` and refuses to submit if the
|
||||
template has changed since last apply (compare git SHA of the
|
||||
template file vs the cluster CRD's `app.kubernetes.io/version` label
|
||||
— or just hard-print the reminder).
|
||||
|
||||
Per `feedback_default_to_l40s_pool`: dispatcher defaults to
|
||||
`--gpu-pool ci-training-l40s` (NOT H100). H100 pool is opt-in for
|
||||
production scale-up, not smoke. Per `feedback_h100_gpu`: when H100
|
||||
IS chosen, max-utilization config; hard-error on under-utilization.
|
||||
|
||||
## Required new files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `crates/ml-alpha/cuda/thompson_sample_categorical.cu` | A5 |
|
||||
| `crates/ml-alpha/cuda/argmax_expected_q.cu` | A5 |
|
||||
| `crates/ml-alpha/cuda/log_pi_at_action.cu` | A5 |
|
||||
| `crates/ml-alpha/cuda/compute_advantage_return.cu` | A6 |
|
||||
| `crates/ml-alpha/cuda/dqn_target_soft_update.cu` | A4 |
|
||||
| `crates/ml-alpha/cuda/ema_update_on_done.cu` | A2 — generic done-gated EMA (parameterised slot) |
|
||||
| `crates/ml-alpha/cuda/ema_update_per_step.cu` | A2 — generic per-step EMA (parameterised slot) |
|
||||
### Kernels
|
||||
- `crates/ml-alpha/cuda/rl_action_kernel.cu` — A5 Thompson sampler writing market_targets
|
||||
- `crates/ml-alpha/cuda/argmax_expected_q.cu` — A5
|
||||
- `crates/ml-alpha/cuda/log_pi_at_action.cu` — A5
|
||||
- `crates/ml-alpha/cuda/compute_advantage_return.cu` — A6
|
||||
- `crates/ml-alpha/cuda/dqn_target_soft_update.cu` — A4
|
||||
- `crates/ml-alpha/cuda/ema_update_on_done.cu` — A2 (generic done-gated EMA, parameterised by slot)
|
||||
- `crates/ml-alpha/cuda/ema_update_per_step.cu` — A2 (generic per-step EMA, parameterised by slot)
|
||||
|
||||
Reused / extended:
|
||||
- `rl_reward_scale_controller.cu` (port from flawed branch)
|
||||
### Ported from flawed branch
|
||||
- `crates/ml-alpha/cuda/rl_reward_scale_controller.cu`
|
||||
- `crates/ml-backtesting/cuda/order_match.cu` modifications (A7)
|
||||
|
||||
### Already in baseline, need Rust launchers added (R5)
|
||||
- `rl_gamma_controller.cu`, `rl_target_tau_controller.cu`,
|
||||
`rl_ppo_clip_controller.cu`, `rl_entropy_coef_controller.cu`,
|
||||
`rl_rollout_steps_controller.cu`, `rl_per_alpha_controller.cu`
|
||||
(already in baseline tree — port their Rust launchers from the
|
||||
flawed branch concept, but feed device-resident EMA inputs)
|
||||
- `crates/ml-backtesting/cuda/order_match.cu` (port the F.3a fee
|
||||
deduction)
|
||||
|
||||
Deleted:
|
||||
- `crates/ml-alpha/src/rl/reward.rs::LobEnv` trait
|
||||
- `crates/ml-backtesting/src/sim/mod.rs::LobSimEnvAdapter`
|
||||
### Rust modules
|
||||
- Extensions to `crates/ml-alpha/src/trainer/integrated.rs` (rebuild
|
||||
`step_with_lobsim` as GPU-pure, add launchers)
|
||||
- Extensions to `crates/ml-alpha/src/rl/isv_slots.rs` (7 new EMA slots)
|
||||
- New `crates/ml-alpha/src/rl/per_step.rs` (wires PER buffer into the step)
|
||||
- Extensions to `crates/ml-alpha/src/data/loader.rs` (A8 port)
|
||||
- Extensions to `crates/ml-backtesting/src/sim/mod.rs` (A7 port)
|
||||
|
||||
### Deleted
|
||||
- `crates/ml-alpha/src/rl/reward.rs::LobEnv` trait + `MockLobEnv`
|
||||
- (Flawed branch's `LobSimEnvAdapter` was on `ml-backtesting`; not in
|
||||
baseline since baseline reset removed it)
|
||||
|
||||
### Renamed (per the "no toys" feedback)
|
||||
- `crates/ml-alpha/tests/dqn_toy.rs` → `dqn_reward_signal.rs`
|
||||
(rewritten to drive synthetic-book `LobSimCuda` directly)
|
||||
- `crates/ml-alpha/tests/ppo_toy.rs` → `ppo_reward_signal.rs`
|
||||
(same)
|
||||
- All "toy bandit" / "bandit" comments in production source rewritten
|
||||
to "reward-signal fixture" or equivalent
|
||||
|
||||
## Phased execution
|
||||
|
||||
Each phase = one commit. Each phase has a green-build + green-gate
|
||||
requirement before the next phase starts.
|
||||
Each R-phase = one atomic commit. Each phase has a green-build +
|
||||
green local-CUDA-gate requirement before the next phase starts. No
|
||||
phase is allowed to defer work to a later phase (per
|
||||
`feedback_no_partial_refactor` and the lesson from the flawed
|
||||
branch).
|
||||
|
||||
### R1 — ISV bootstrap + slot extension (1 commit)
|
||||
### R1 — ISV slot extension + controller bootstrap launches (1 commit)
|
||||
- Add 7 new ISV slot constants (`RL_*_EMA_INDEX`) → `RL_SLOTS_END = 424`.
|
||||
- In `IntegratedTrainer::new`: alloc_zeros ISV, then launch each of
|
||||
the 7 controllers ONCE with sentinel-zero EMA input. Each kernel's
|
||||
first-observation-bootstrap path fires → ISV slot gets canonical
|
||||
value (A3).
|
||||
- **Gate G1** local CUDA test.
|
||||
|
||||
- Add new ISV slot constants for the 7 EMA inputs (A2) → `RL_SLOTS_END = 424`.
|
||||
- In `IntegratedTrainer::new`, one memcpy_htod writing canonical
|
||||
defaults to ISV[400..406] (A3).
|
||||
- Gate G1.
|
||||
### R2 — Fee model + loader pair API (1 commit, was 2)
|
||||
- Port `order_match.cu` fee deduction + `upload_cost_per_lot_per_side`
|
||||
host API (A7).
|
||||
- Port `MultiHorizonLoader::next_sequence_pair` + `build_sequence_at`
|
||||
refactor (A8).
|
||||
- Existing tests pass; no new tests in this phase (covered by G7 later).
|
||||
|
||||
### R2 — Fee model + loader pair API (2 commits)
|
||||
### R3 — GPU-resident EMA + advantage/return kernels (1 commit)
|
||||
- `ema_update_on_done.cu` + `ema_update_per_step.cu` generic kernels
|
||||
(A2).
|
||||
- `compute_advantage_return.cu` (A6).
|
||||
- Local CUDA tests for each kernel via GPU-oracle pattern:
|
||||
- EMA: feed constant input, assert ISV converges to input (property:
|
||||
EMA of constant = constant).
|
||||
- EMA bootstrap: feed sentinel-zero start + one observation, assert
|
||||
slot equals observation (per `pearl_first_observation_bootstrap`).
|
||||
- Advantage/return: feed all-zero rewards + done=0 + v_t=v_tp1=k,
|
||||
assert returns = γ·k and advantages = γ·k − k = (γ−1)·k (property).
|
||||
|
||||
- R2a: port `order_match.cu` fee deduction + `upload_cost_per_lot_per_side`
|
||||
host API (from F.3a). Gate: existing `lob_env_adapter_buy_close_cycle`
|
||||
passes with zero fees; new with-fees variant lands.
|
||||
- R2b: port `MultiHorizonLoader::next_sequence_pair` (from G fix).
|
||||
|
||||
### R3 — GPU-pure action sampling (3 commits)
|
||||
|
||||
- R3a: `thompson_sample_categorical` kernel + Rust launcher. Local CUDA
|
||||
test: sample 10000 actions from a known Q distribution, assert
|
||||
empirical frequency matches softmax probabilities within tolerance.
|
||||
- R3b: `argmax_expected_q` kernel. Local CUDA test: feed adversarial
|
||||
Q distributions, assert argmax matches host reference.
|
||||
- R3c: `log_pi_at_action` kernel. Local CUDA test: feed known π
|
||||
distribution, assert log-prob within 1e-5 of host softmax.
|
||||
|
||||
### R4 — GPU-resident EMA + advantage / return (2 commits)
|
||||
|
||||
- R4a: `ema_update_on_done.cu` + `ema_update_per_step.cu` generic
|
||||
kernels. Local CUDA test: feed deterministic reward stream, assert
|
||||
EMA tracks the analytic Wiener-α trajectory.
|
||||
- R4b: `compute_advantage_return.cu`. Local CUDA test: feed known
|
||||
rewards / dones / V values, assert advantage / return match host
|
||||
reference.
|
||||
### R4 — GPU-resident action sampling kernels (1 commit)
|
||||
- `rl_action_kernel.cu` (Thompson) + `argmax_expected_q.cu` +
|
||||
`log_pi_at_action.cu` + curand PRNG state allocation.
|
||||
- Local CUDA tests via GPU-oracle:
|
||||
- `thompson_sample_categorical`: sample 10000 actions from a Q
|
||||
distribution where atom probability mass is concentrated on a
|
||||
known atom; assert empirical action frequency matches the
|
||||
softmax-weighted expected value within tolerance. Cross-kernel
|
||||
oracle: compare against `argmax_expected_q` on the same input
|
||||
when the distribution is sharp enough that Thompson ≈ argmax.
|
||||
- `argmax_expected_q`: feed Q distribution where one action has
|
||||
strictly higher expected value than all others by a known margin;
|
||||
assert that action is chosen for every batch.
|
||||
- `log_pi_at_action`: feed π logits where one action has logit
|
||||
massively above others; assert log π(that action) ≈ 0
|
||||
(probability ≈ 1).
|
||||
|
||||
### R5 — Wire all 7 controllers + target-net soft update (1 commit)
|
||||
- Launcher fields + helpers for all 7 controllers (gamma, target_tau,
|
||||
ppo_clip, entropy_coef, rollout_steps, per_alpha, reward_scale).
|
||||
Each pulls input from the corresponding EMA slot (R3a outputs).
|
||||
- `dqn_target_soft_update.cu` + launcher (A4).
|
||||
- **Gate G3** local CUDA test (per-controller).
|
||||
- **Gate G4** local CUDA test (soft-update fires + moves target_w).
|
||||
|
||||
- Add launcher fields + helpers for all 7 controllers (gamma,
|
||||
target_tau, ppo_clip, entropy_coef, rollout_steps, per_alpha,
|
||||
reward_scale). Each launcher pulls its input from the corresponding
|
||||
EMA slot (A2 from R4a). Gate G3.
|
||||
- Add `dqn_target_soft_update` kernel + launcher (A4). Gate G4.
|
||||
### R6 — Replace `LobEnv` with kernel-driven decision path (2 commits)
|
||||
- R6a: delete `LobEnv` trait + `MockLobEnv` from
|
||||
`src/rl/reward.rs`. Direct `LobSimCuda` integration on
|
||||
`IntegratedTrainer` (no trait abstraction). Wire action submission
|
||||
via the existing decision-policy kernel pattern; `market_targets_d`
|
||||
receives the Thompson-sampled index from `rl_action_kernel`.
|
||||
- R6b: rewrite convergence-gate fixtures (`dqn_reward_signal.rs`,
|
||||
`ppo_reward_signal.rs`, `integrated_trainer_smoke.rs`) to drive a
|
||||
real `LobSimCuda` with synthetic book data instead of MockLobEnv.
|
||||
Reward signal: "synthetic monotonic uptick → forced long action →
|
||||
trade close pays > 0". Per `pearl_tests_must_prove_not_lock_observations`,
|
||||
asserts invariants (reward signal flows, sign correctness), not
|
||||
specific Q-value magnitudes.
|
||||
- **Gate G5** local CUDA test (zero host memcpy in step_with_lobsim
|
||||
hot path).
|
||||
- **Gate G7** local CUDA test (bull-market mean per-trade reward > 0).
|
||||
|
||||
### R6 — Replace `LobEnv` with kernel-driven decision path (3 commits)
|
||||
|
||||
- R6a: delete `LobEnv` trait + `LobSimEnvAdapter`. Add direct
|
||||
`LobSimCuda` integration on `IntegratedTrainer` (no trait).
|
||||
- R6b: wire action submission via the decision-policy kernel pattern
|
||||
(mirror `step_decision_with_latency`). Adapt `market_targets_d` to
|
||||
accept the Thompson-sampled action index directly.
|
||||
- R6c: rewrite the trainer's convergence-gate fixtures (formerly
|
||||
`dqn_toy.rs` / `ppo_toy.rs`) to drive a real `LobSimCuda` with
|
||||
synthetic book data instead of a `MockLobEnv`. Reward signal becomes
|
||||
"synthetic monotonic uptick → forced long → trade close pays > 0".
|
||||
- Gate G5 (no host memcpy in step_with_lobsim other than ISV
|
||||
diagnostics).
|
||||
|
||||
### R7 — Reward composition (1 commit)
|
||||
|
||||
- Per-step reward = `realized_pnl_delta` (already net of fees + slippage
|
||||
from R2a) scaled by `ISV[RL_REWARD_SCALE_INDEX]`. Optionally subtract
|
||||
exposure penalty (deferred unless smoke shows it's needed).
|
||||
- Gate G6.
|
||||
### R7 — PER wiring (1 commit)
|
||||
- Push every step's transition to the replay buffer.
|
||||
- Sample `batch_size` indices per step for the off-policy Q update.
|
||||
- Update priorities from per-sample TD-error magnitudes after Q
|
||||
backward.
|
||||
- **Gate G6** local CUDA test (buffer grows, sample returns correct
|
||||
size).
|
||||
|
||||
### R8 — CLI binary + Argo template + dispatcher (1 commit)
|
||||
|
||||
- `crates/ml-alpha/examples/alpha_rl_train.rs` driving the rebuilt
|
||||
trainer. Uses `next_sequence_pair` for true `(s_t, s_{t+1})` windows.
|
||||
- Mandatory CLI args: `--mbp10-data-dir`. Plan-time reconciliation
|
||||
with `feedback_mbp10_mandatory`: ml-alpha's `MultiHorizonLoader`
|
||||
does not currently consume `--trades-data-dir` (the OFI features
|
||||
in `Mbp10RawInput` are derived from MBP-10 snapshot deltas, not
|
||||
from a separate trades stream). If the project chooses to wire a
|
||||
trades stream into ml-alpha's feature path, that's a separate
|
||||
plan; this rebuild ships with mbp10-only and flags the gap.
|
||||
- NaN abort: per-step `is_finite()` check on `l_total`; exit code 2
|
||||
on divergence. (**Gate G8**)
|
||||
- `infra/k8s/argo/alpha-rl-template.yaml` + `scripts/argo-alpha-rl.sh`
|
||||
(port from flawed branch).
|
||||
- NaN abort: per-step `is_finite()` check on `l_total`, exit code 2 on
|
||||
divergence. Gate G7.
|
||||
- Per `feedback_default_to_l40s_pool`: defaults to
|
||||
`ci-training-l40s`, not H100.
|
||||
- Per `feedback_argo_template_must_apply`: top-of-output reminder
|
||||
+ refuse-to-submit if template not applied since last edit.
|
||||
- Per `feedback_push_before_deploy`: verify `git push` succeeded
|
||||
before `argo submit`; hard-error if local HEAD ≠ origin HEAD.
|
||||
|
||||
### R9 — Pre-cluster smoke + first cluster smoke (out-of-band)
|
||||
### R9 — Pre-cluster validation + first cluster smoke (out-of-band)
|
||||
- Run all R1..R8 local CUDA tests on a GPU host (the dev RTX 3050 Ti
|
||||
per CLAUDE.md or any L40S). Confirm green: G1, G2, G3, G4, G5, G6,
|
||||
G7, G8.
|
||||
- `git push`. `kubectl apply -n foxhunt -f
|
||||
infra/k8s/argo/alpha-rl-template.yaml`.
|
||||
- `./scripts/argo-alpha-rl.sh --n-steps 1000 --instrument-mode
|
||||
front-month` (5-min validation smoke). If green: scale to full 50k.
|
||||
- For the G8 backtest gate: per `pearl_single_window_oos_is_not_oos`,
|
||||
run ≥3 walk-forward lob-sweep folds, report mean ± SD. A single
|
||||
pass is NOT G8 pass.
|
||||
|
||||
- Run all R1..R8 local CUDA tests on a GPU host. Confirm green.
|
||||
- `git push`. `kubectl apply -f` template. `./scripts/argo-alpha-rl.sh
|
||||
--n-steps 1000` for a 5-min validation smoke. If green, full 50k.
|
||||
## Pre-cluster validation checklist (R9 prerequisite)
|
||||
|
||||
Per `feedback_smoke_validation_before_structural_priors`, validate the
|
||||
wiring with neutral config BEFORE shipping production scale. The
|
||||
following MUST be green on a local GPU host (RTX 3050 Ti is
|
||||
sufficient for everything except wall-time benchmarks):
|
||||
|
||||
```bash
|
||||
# Build clean
|
||||
SQLX_OFFLINE=true cargo build --release --workspace 2>&1 | tail -10
|
||||
SQLX_OFFLINE=true cargo build --release -p ml-alpha --example alpha_rl_train
|
||||
|
||||
# All local-CUDA tests green (R1..R8 gates)
|
||||
SQLX_OFFLINE=true cargo test -p ml-alpha --test isv_bootstrap -- --ignored --nocapture # G1
|
||||
SQLX_OFFLINE=true cargo test -p ml-alpha --test reward_scale_hold_steady -- --ignored --nocapture # G2
|
||||
SQLX_OFFLINE=true cargo test -p ml-alpha --test controllers_emit -- --ignored --nocapture # G3
|
||||
SQLX_OFFLINE=true cargo test -p ml-alpha --test target_soft_update -- --ignored --nocapture # G4
|
||||
SQLX_OFFLINE=true cargo test -p ml-alpha --test no_host_memcpy -- --ignored --nocapture # G5
|
||||
SQLX_OFFLINE=true cargo test -p ml-alpha --test per_wired -- --ignored --nocapture # G6
|
||||
SQLX_OFFLINE=true cargo test -p ml-backtesting --test reward_calibration -- --ignored --nocapture # G7
|
||||
SQLX_OFFLINE=true cargo test -p ml-alpha --test nan_abort -- --ignored --nocapture # G8
|
||||
|
||||
# Convergence-gate fixtures green
|
||||
SQLX_OFFLINE=true cargo test -p ml-alpha --test dqn_reward_signal -- --ignored --nocapture
|
||||
SQLX_OFFLINE=true cargo test -p ml-alpha --test ppo_reward_signal -- --ignored --nocapture
|
||||
SQLX_OFFLINE=true cargo test -p ml-alpha --test integrated_trainer_smoke -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Only after ALL of these pass green: `git push` → `kubectl apply` →
|
||||
`argo submit --n-steps 1000`.
|
||||
|
||||
Only after the 1000-step smoke completes Succeeded with finite losses
|
||||
+ ISV slots in healthy ranges + > 0 trades closed: scale to 50k.
|
||||
|
||||
Only after the 50k run produces a checkpoint that achieves G6
|
||||
calibration + survives 3 walk-forward lob-sweep folds: claim Phase H
|
||||
G8 pass.
|
||||
|
||||
## What we keep from the flawed branch
|
||||
|
||||
These specific elements landed correctly and are ported directly:
|
||||
- `rl_reward_scale_controller.cu` kernel (Phase F.2)
|
||||
- `order_match.cu` fee deduction pattern (Phase F.3a)
|
||||
- `MultiHorizonLoader::next_sequence_pair` (Phase G fix)
|
||||
- The 6 unlaunched controller .cu files (Phases C, D, E) — they're
|
||||
fine kernels, just need launchers
|
||||
- F.4's true-h_{t+1} architectural insight (Bellman target on
|
||||
h_{t+1}, V bootstrap on h_{t+1}) — the structure becomes simpler in
|
||||
the GPU-pure rebuild because state lives on device
|
||||
Direct ports:
|
||||
- `rl_reward_scale_controller.cu` kernel
|
||||
- `order_match.cu` fee deduction + `upload_cost_per_lot_per_side` API
|
||||
- `MultiHorizonLoader::next_sequence_pair` + `build_sequence_at`
|
||||
refactor
|
||||
|
||||
Architectural insights that survive the rebuild:
|
||||
- True h_{t+1} Bellman target (F.4 close-out idea) — in the rebuild
|
||||
this falls out for free because action sampling is GPU-resident, so
|
||||
the next-state Q forward is just another kernel call with the next
|
||||
snapshot, no host roundtrip needed.
|
||||
- V-bootstrap from V(s_{t+1}) — same.
|
||||
- Toy rename (`dqn_toy.rs` → `dqn_reward_signal.rs`, etc.) — applied
|
||||
in R6b on top of the rewritten fixtures.
|
||||
|
||||
## What we discard
|
||||
|
||||
- `LobEnv` trait + `LobSimEnvAdapter` (replaced by kernel-driven path)
|
||||
- `LobEnv` trait + `LobSimEnvAdapter` (kernel-driven path replaces
|
||||
the trait abstraction entirely)
|
||||
- `MockLobEnv` "toy bandit" fixture (replaced by synthetic-book
|
||||
`LobSimCuda` fixture)
|
||||
- Host-side action sampling, EMA tracking, advantage / return loops
|
||||
- The `dqn_toy.rs` / `ppo_toy.rs` test file names (renamed to
|
||||
`dqn_reward_signal.rs` / `ppo_reward_signal.rs` in the rebuild)
|
||||
`LobSimCuda` fixture in R6b)
|
||||
- Host-side action sampling, EMA tracking, advantage/return loops
|
||||
- Host `mean_abs_pnl_ema: f32` field on `IntegratedTrainer` (moved
|
||||
to ISV slot, updated by `ema_update_on_done` kernel)
|
||||
- Single-window OOS as a G8 pass criterion (replaced by ≥3
|
||||
walk-forward folds per `pearl_single_window_oos_is_not_oos`)
|
||||
|
||||
## Pearls honored
|
||||
## Memory pearls honored
|
||||
|
||||
- `feedback_cpu_is_read_only` — A1, A5, A6 are direct compliance.
|
||||
- `feedback_isv_for_adaptive_bounds` — A2, A3, R5.
|
||||
- `feedback_no_partial_refactor` — each R-phase is atomic.
|
||||
- `feedback_no_stubs` — every controller has a launcher AND a
|
||||
consumer at landing time.
|
||||
- `feedback_no_atomicadd` — new kernels use single-writer-per-block
|
||||
or per-batch scratch + reduce_axis0.
|
||||
- `feedback_no_htod_htoh_only_mapped_pinned` — applies to the
|
||||
remaining ISV diagnostic readback.
|
||||
- `pearl_first_observation_bootstrap` — combined with A3, the host
|
||||
bootstrap takes precedence; kernel sentinel-zero path stays as a
|
||||
safety net.
|
||||
- `pearl_wiener_alpha_floor_for_nonstationary` — every EMA uses
|
||||
α-floor = 0.4.
|
||||
- `feedback_no_hiding` — `step_event`'s `_ts_ns` / `_trade_signed_vol`
|
||||
go away with the `LobEnv` trait deletion.
|
||||
- `feedback_no_feature_flags` — controllers are unconditional.
|
||||
- `feedback_registry_entries_need_dispatch_arms` — controllers go in
|
||||
the state-reset registry (if applicable to ml-alpha).
|
||||
- `pearl_tests_must_prove_not_lock_observations` — convergence-gate
|
||||
fixtures assert invariants (reward signal flows, sign correctness),
|
||||
not specific learned-checkpoint values.
|
||||
Catalog of pearls the rebuild explicitly applies (line numbers and
|
||||
specific implementation pointers will be filled in at R-phase commit
|
||||
time per `feedback_no_partial_refactor`):
|
||||
|
||||
- `feedback_cpu_is_read_only` → A1, A5, A6, G5
|
||||
- `feedback_isv_for_adaptive_bounds` → A2, A3, R5
|
||||
- `feedback_wire_everything_up` → A3, A4, A9, R1, R5, R7
|
||||
- `feedback_no_partial_refactor` → every R-phase atomic; controllers
|
||||
AND their consumers land together
|
||||
- `feedback_no_stubs` → A4 (target soft update), A9 (PER wired)
|
||||
- `feedback_no_atomicadd` → all new kernels single-writer-per-block
|
||||
or `reduce_axis0`
|
||||
- `feedback_no_cpu_test_fallbacks` → every gate's test uses GPU
|
||||
oracles only (analytical synthetic, property assertion, or
|
||||
cross-kernel)
|
||||
- `feedback_no_htod_htoh_only_mapped_pinned` → A3 (no host memcpy
|
||||
bootstrap), mapped-pinned for any HEALTH_DIAG readback
|
||||
- `feedback_always_per` → A9
|
||||
- `feedback_mbp10_mandatory` → R8 (CLI requires `--mbp10-data-dir`;
|
||||
reconciles trades-data-dir gap explicitly)
|
||||
- `feedback_default_to_l40s_pool` → R8 dispatcher
|
||||
- `feedback_push_before_deploy` → R8 dispatcher
|
||||
- `feedback_argo_template_must_apply` → R8 dispatcher
|
||||
- `feedback_kill_runs_on_anomaly_quickly` → cluster smoke discipline
|
||||
- `feedback_extending_existing_code_audits_for_existing_violations`
|
||||
→ the lesson driving this entire rebuild
|
||||
- `pearl_first_observation_bootstrap` → A3, R3 EMA bootstrap tests
|
||||
- `pearl_wiener_alpha_floor_for_nonstationary` → all EMA controllers
|
||||
- `pearl_wiener_optimal_adaptive_alpha` → A2 EMA design
|
||||
- `pearl_thompson_for_distributional_action_selection` → A5 split
|
||||
(Thompson at rollout, argmax for Bellman)
|
||||
- `pearl_action_pruning_falsified` → 9-action grid preserved
|
||||
- `pearl_c51_thompson_closed_phase_e3_gap` → C51 + Thompson combo
|
||||
preserved
|
||||
- `pearl_single_window_oos_is_not_oos` → cluster smoke discipline
|
||||
(≥3 walk-forward folds for G8)
|
||||
- `pearl_q_spread_kill_criterion_misaligned_for_distributional_q` →
|
||||
cluster smoke kill criteria explicitly exclude Q_SPREAD
|
||||
- `pearl_tests_must_prove_not_lock_observations` → every gate
|
||||
asserts invariants, not specific learned-checkpoint values
|
||||
- `pearl_cold_path_no_exception_to_gpu_drives` → A1, A2, A5, A6 are
|
||||
all GPU-resident even for "cold path" diagnostic computations
|
||||
- `pearl_no_host_branches_in_captured_graph` → A5 PRNG state lives
|
||||
on device; no host RNG advance inside any captured kernel chain
|
||||
- `pearl_one_unbounded_signal_per_reward` → reward = scale × pnl_delta
|
||||
has scale bounded `[1e-3, 1e3]` (R5 reward_scale controller) and
|
||||
pnl_delta as the single unbounded multiplicand
|
||||
- `pearl_symmetric_clamp_audit` → reward_scale clamp is bilateral
|
||||
`fmaxf(REWARD_SCALE_MIN, fminf(x, REWARD_SCALE_MAX))`
|
||||
- `pearl_event_driven_reward_density_alignment` → per-trade reward
|
||||
fires on `done=true` (event density = trade close density)
|
||||
|
||||
## Calendar estimate
|
||||
|
||||
- R1: 0.5 days
|
||||
- R2: 1 day
|
||||
- R3: 1.5 days
|
||||
- R4: 1 day
|
||||
- R5: 1 day
|
||||
- R6: 2 days
|
||||
- R7: 0.5 days
|
||||
- R8: 0.5 days
|
||||
- R9: 0.5 days + 1h cluster smoke
|
||||
Roughly 2× the flawed Phase F+G calendar because the rebuild does the
|
||||
work the flawed branch deferred or simplified.
|
||||
|
||||
Total ~8.5 dev days. Roughly 2× the flawed Phase F + G calendar
|
||||
because the rebuild does the work the flawed branch deferred or
|
||||
simplified.
|
||||
- R1: 0.5 days (controller bootstrap launches + ISV slot extension)
|
||||
- R2: 1 day (port F.3a + G fixes)
|
||||
- R3: 1 day (EMA + advantage/return kernels + GPU-oracle tests)
|
||||
- R4: 1.5 days (action sampling kernels + GPU-oracle tests)
|
||||
- R5: 1 day (controller wiring + soft update)
|
||||
- R6: 2 days (LobEnv removal + fixture rewrite)
|
||||
- R7: 0.5 days (PER wiring)
|
||||
- R8: 0.5 days (CLI + Argo)
|
||||
- R9: 0.5 days local validation + 5-min smoke + 1.5h × 3 folds = ~5h cluster
|
||||
|
||||
Total ~8.5 dev days + ~6h cluster smoke wall.
|
||||
|
||||
## Execution discipline
|
||||
|
||||
- Per `feedback_no_concurrent_agents_shared_tree`: R-phases execute
|
||||
serially in this branch (`ml-alpha-phase-a`). If parallel execution
|
||||
is needed, use git worktrees.
|
||||
- Per `feedback_fix_everything_aggressively`: no defer-to-later. Each
|
||||
R-phase ships its work complete.
|
||||
- Per `feedback_extending_existing_code_audits_for_existing_violations`:
|
||||
when each R-phase touches pre-existing code, the audit comes FIRST.
|
||||
If the existing code already violates a rule, the R-phase's scope
|
||||
expands to include the fix, not silently extends the violation.
|
||||
|
||||
End of plan.
|
||||
|
||||
Reference in New Issue
Block a user