From 09202aa9910a7b57fdd6f3453e6a9460238be1b2 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Thu, 7 May 2026 21:43:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(sp14-=CE=B2):=20pre-load=20SP14=20EGF=20ke?= =?UTF-8?q?rnel=20handles=20in=20experience=20collector=20(Option=20B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 1 of β migration — Option B (collector-native): collector loads its own CudaFunction handles for the 3 SP14 producer kernels plus their sub-kernels (4 total: aux_dir_acc_reduce, aux_pred_to_isv_tanh, q_disagreement_update, alpha_grad_compute). Mirrors SP13 hold_rate pattern at gpu_experience_collector.rs:1820. Cleaner than cross-component launcher calls (avoids trainer-stream / collector-stream race; no signature surgery on the existing trainer launchers). Cubin static decls flipped to pub(crate) so the collector can re-load on its own stream. Compile clean; sp14_oracle_tests pass (2/2 non-GPU; GPU-gated 7 ignored on RTX 3050 Ti host). No launches yet — additive infrastructure commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ml/src/cuda_pipeline/gpu_dqn_trainer.rs | 8 +- .../cuda_pipeline/gpu_experience_collector.rs | 108 ++++++++++++++++++ docs/dqn-wire-up-audit.md | 2 + 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs index 4ac6bc5c3..0565433c4 100644 --- a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs +++ b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs @@ -625,7 +625,7 @@ pub(crate) static SP13_HOLD_RATE_OBSERVER_CUBIN: &[u8] = /// the SP13 EMAs at ISV[373..375) (short/long EMAs, sentinel 0.5 per /// `pearl_first_observation_bootstrap`). See /// `aux_dir_acc_reduce_kernel.cu` for kernel contract details. -static SP13_AUX_DIR_ACC_REDUCE_CUBIN: &[u8] = +pub(crate) static SP13_AUX_DIR_ACC_REDUCE_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/aux_dir_acc_reduce_kernel.cubin")); /// SP13 Phase 0a P0a.T4 (2026-05-04): fixed-α EMA applicator. Sibling @@ -652,14 +652,14 @@ pub(crate) static SP13_APPLY_FIXED_ALPHA_EMA_CUBIN: &[u8] = /// preserved for layout stability. Consumed by /// `GpuDqnTrainer::launch_aux_pred_to_isv_tanh`. See /// `aux_pred_to_isv_tanh_kernel.cu` for kernel contract details. -static SP13_AUX_PRED_TO_ISV_TANH_CUBIN: &[u8] = +pub(crate) static SP13_AUX_PRED_TO_ISV_TANH_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/aux_pred_to_isv_tanh_kernel.cubin")); /// SP14 B.3 (2026-05-05): per-step Q-head ↔ aux argmax disagreement EMA /// producer. Reads online Q logits + aux softmax outputs; computes per-step /// argmax-disagreement bool; updates a Wiener-optimal EMA into ISV[388]. /// Loaded from `q_disagreement_update_kernel.cubin`. -static SP14_Q_DISAGREEMENT_CUBIN: &[u8] = +pub(crate) static SP14_Q_DISAGREEMENT_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/q_disagreement_update_kernel.cubin")); /// SP14 B.4 (2026-05-05): α_grad compute kernel (Schmitt + adaptive k + @@ -667,7 +667,7 @@ static SP14_Q_DISAGREEMENT_CUBIN: &[u8] = /// gate), ISV[390] (hack-rate EMA); computes α_grad ∈ [0, 1] and writes to /// ISV[387]. Consumed downstream by the direction Q-head SGEMM weight as an /// input-gate scalar. Loaded from `alpha_grad_compute_kernel.cubin`. -static SP14_ALPHA_GRAD_CUBIN: &[u8] = +pub(crate) static SP14_ALPHA_GRAD_CUBIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/alpha_grad_compute_kernel.cubin")); /// SP14 B.5 (2026-05-05): anti-gradient-hacking circuit breaker. Compares diff --git a/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs b/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs index 701d4be89..925ac4128 100644 --- a/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs +++ b/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs @@ -902,6 +902,43 @@ pub struct GpuExperienceCollector { /// the kernel overwrites it on every launch. sp13_hold_rate_buf: MappedF32Buffer, + // ── SP14 β-migration (Option B, 2026-05-07): collector-native EGF + // producer chain. The trainer's `submit_aux_ops` path only fires once + // per epoch with batches drawn from the replay buffer where Q has + // converged on Hold/Flat (total_cnt=0 → EMA decays toward zero). The + // rollout path on this collector's stream sees Thompson-forced + // Short/Long picks which is where genuine aux↔Q disagreement signal + // lives. Same cubin handles as the trainer's, loaded on this stream + // for same-stream producer→consumer ordering. Mirrors the SP13 + // hold-rate observer's collector-native pattern (loaded above). + /// SP14 β-migration (2026-05-07): SP13 dir-acc reduce kernel handle + /// on the collector's stream. Reads `exp_aux_nb_softmax_buf [N, 2]` + + /// `exp_aux_nb_label_buf [N]` and writes 6 floats to + /// `exp_aux_dir_acc_buf` (mapped-pinned). Same cubin as the trainer's + /// `aux_dir_acc_reduce` (`SP13_AUX_DIR_ACC_REDUCE_CUBIN`). + exp_sp13_aux_dir_acc_reduce_kernel: CudaFunction, + /// SP14 β-migration (2026-05-07): aux-pred → ISV[375] tanh producer + /// kernel handle on the collector's stream. Reads + /// `exp_aux_nb_softmax_buf [N, 2]` and writes + /// `mean(softmax[:,1] - softmax[:,0])` to ISV[AUX_DIR_PREDICTION_INDEX=375]. + /// Same cubin as the trainer's `aux_pred_to_isv_tanh_kernel` + /// (`SP13_AUX_PRED_TO_ISV_TANH_CUBIN`). + exp_sp13_aux_pred_to_isv_tanh_kernel: CudaFunction, + /// SP14 β-migration (2026-05-07): SP14 q-disagreement EMA producer + /// kernel handle on the collector's stream. Reads + /// `exp_aux_nb_softmax_buf [N, K=2]` + collector's `q_values [N, q_stride]` + /// (post-`expected_q_kernel`) and updates ISV[388] + + /// ISV[Q_DISAGREEMENT_VAR_INDEX=389]. Same cubin as the trainer's + /// `sp14_q_disagreement_update_kernel` (`SP14_Q_DISAGREEMENT_CUBIN`). + exp_sp14_q_disagreement_update_kernel: CudaFunction, + /// SP14 β-migration (2026-05-07): SP14 α_grad compute kernel handle + /// on the collector's stream. Pure ISV-state kernel — reads + /// ISV[372..395) drivers and writes ISV[392] / ISV[393] (raw and + /// smoothed α_grad) plus the Schmitt-trigger / k_q / k_aux state + /// machine bookkeeping. Same cubin as the trainer's + /// `sp14_alpha_grad_compute_kernel` (`SP14_ALPHA_GRAD_CUBIN`). + exp_sp14_alpha_grad_compute_kernel: CudaFunction, + /// B.2 Plan 3 Task 3: trade_attempt_rate_ema_update GPU kernel. /// Single-block (1 thread) reduction + adaptive EMA of Flat→Positioned /// transition rate into ISV[71]. Launched alongside reward_component_ema @@ -1801,6 +1838,69 @@ impl GpuExperienceCollector { format!("SP13 v3 sp13_hold_rate_buf alloc (1 f32, collector): {e}") ))?; + // SP14 β-migration (Option B, 2026-05-07): pre-load the 4 SP14/SP13 + // EGF kernel handles on the collector's stream. The diagnostic + // motivation lives in commit 9d0c124ce (kernel-gate fix) — even + // with that gate, the trainer-time `submit_aux_ops` chain feeds + // the EGF producers a Q-head that has converged to Hold/Flat on + // replayed batches (total_cnt=0 every batch → EMAs decay to + // zero). The rollout path with Thompson-forced Short/Long picks + // is where the genuine aux↔Q disagreement signal lives. Each + // load mirrors the SP13 hold-rate pattern at line 1820 — same + // cubin as the trainer, loaded once on this stream so the + // chained launches submit on `self.stream` and producer→consumer + // ordering is stream-implicit (no cross-stream race, no host + // sync). Per `feedback_no_partial_refactor.md`: handles allocate + // here in step 1 of the β migration, launches wire in step 4. + let exp_sp13_aux_dir_acc_reduce_kernel = { + use super::gpu_dqn_trainer::SP13_AUX_DIR_ACC_REDUCE_CUBIN; + let m = stream.context() + .load_cubin(SP13_AUX_DIR_ACC_REDUCE_CUBIN.to_vec()) + .map_err(|e| MLError::ModelError(format!( + "sp14-β: aux_dir_acc_reduce cubin (collector): {e}" + )))?; + m.load_function("aux_dir_acc_reduce_kernel") + .map_err(|e| MLError::ModelError(format!( + "sp14-β: aux_dir_acc_reduce_kernel load (collector): {e}" + )))? + }; + let exp_sp13_aux_pred_to_isv_tanh_kernel = { + use super::gpu_dqn_trainer::SP13_AUX_PRED_TO_ISV_TANH_CUBIN; + let m = stream.context() + .load_cubin(SP13_AUX_PRED_TO_ISV_TANH_CUBIN.to_vec()) + .map_err(|e| MLError::ModelError(format!( + "sp14-β: aux_pred_to_isv_tanh cubin (collector): {e}" + )))?; + m.load_function("aux_pred_to_isv_tanh_kernel") + .map_err(|e| MLError::ModelError(format!( + "sp14-β: aux_pred_to_isv_tanh_kernel load (collector): {e}" + )))? + }; + let exp_sp14_q_disagreement_update_kernel = { + use super::gpu_dqn_trainer::SP14_Q_DISAGREEMENT_CUBIN; + let m = stream.context() + .load_cubin(SP14_Q_DISAGREEMENT_CUBIN.to_vec()) + .map_err(|e| MLError::ModelError(format!( + "sp14-β: q_disagreement cubin (collector): {e}" + )))?; + m.load_function("q_disagreement_update_kernel") + .map_err(|e| MLError::ModelError(format!( + "sp14-β: q_disagreement_update_kernel load (collector): {e}" + )))? + }; + let exp_sp14_alpha_grad_compute_kernel = { + use super::gpu_dqn_trainer::SP14_ALPHA_GRAD_CUBIN; + let m = stream.context() + .load_cubin(SP14_ALPHA_GRAD_CUBIN.to_vec()) + .map_err(|e| MLError::ModelError(format!( + "sp14-β: alpha_grad cubin (collector): {e}" + )))?; + m.load_function("alpha_grad_compute_kernel") + .map_err(|e| MLError::ModelError(format!( + "sp14-β: alpha_grad_compute_kernel load (collector): {e}" + )))? + }; + // SP15 Phase 1.3.b-followup (2026-05-07): per-env DD state tile // sized `[alloc_episodes * 6]`. The redesigned `dd_state_kernel` // writes 6 stats per env to this tile (Path A → Path B @@ -2156,6 +2256,14 @@ impl GpuExperienceCollector { sp13_hold_rate_observer_kernel, sp13_apply_fixed_alpha_ema_kernel, sp13_hold_rate_buf, + // SP14 β-migration (Option B, 2026-05-07): kernel handles for + // the collector-native EGF producer chain. Wired in step 4 of + // the β migration; this commit just allocates the handles + // (additive, no behavior change). + exp_sp13_aux_dir_acc_reduce_kernel, + exp_sp13_aux_pred_to_isv_tanh_kernel, + exp_sp14_q_disagreement_update_kernel, + exp_sp14_alpha_grad_compute_kernel, trade_attempt_rate_ema_kernel, plan_threshold_update_kernel, state_kl_kernel, diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index 5612dd3d8..9a37b61bf 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -2,6 +2,8 @@ **Status:** Populated during Plan 1 Task 6 (A.5 orphan audit). Updated on every commit per Invariant 7. +SP14 β-migration step 1 — pre-load EGF kernel handles in experience collector (2026-05-07): Step 1 of 5 of the Option B (collector-native) β-migration unwinding the `train-6fcml`-fix follow-up. Even with the empty-batch decay gate (commit `9d0c124ce`) the trainer-time `submit_aux_ops` path can only fire the EGF chain against batches whose Q-direction picks are dominated by Hold/Flat (the natural argmax distribution of the converged 4-way direction head); the genuine aux↔Q disagreement signal lives in the rollout path on `gpu_experience_collector`'s stream where Thompson sampling forces Short/Long picks every step. Cross-stream launches from a trainer-stream-captured graph would race the collector-stream forward producers; the cleanest fix is collector-native — let the collector load its own copies of the EGF kernel handles and launch them with `self.stream`. Mirror of the existing SP13 hold_rate observer pattern (`crates/ml/src/cuda_pipeline/gpu_experience_collector.rs:1820`) where the collector loads `SP13_HOLD_RATE_OBSERVER_CUBIN` + `SP13_APPLY_FIXED_ALPHA_EMA_CUBIN` once on its own stream because `batch_actions` lives on this stream and cross-stream sync would defeat the no-host-sync per-step contract. This commit allocates 4 `CudaFunction` fields on `GpuExperienceCollector` (`exp_sp13_aux_dir_acc_reduce_kernel`, `exp_sp13_aux_pred_to_isv_tanh_kernel`, `exp_sp14_q_disagreement_update_kernel`, `exp_sp14_alpha_grad_compute_kernel`) — same cubins as the trainer's, loaded once on `self.stream` so producer→consumer ordering inside the collector's per-step body becomes stream-implicit. Cubin static decls in `gpu_dqn_trainer.rs` flipped from `static` to `pub(crate) static` for `SP13_AUX_DIR_ACC_REDUCE_CUBIN`, `SP13_AUX_PRED_TO_ISV_TANH_CUBIN`, `SP14_Q_DISAGREEMENT_CUBIN`, `SP14_ALPHA_GRAD_CUBIN`. Per `feedback_no_partial_refactor.md` the β migration is split into 5 atomic commits — this commit is additive (new fields, no launches) so behavior is bit-identical; step 4 lands the launches and step 5 deletes the trainer-time path. Per `pearl_no_host_branches_in_captured_graph` the `cuModuleLoadData` calls all happen in the constructor before any per-step launch, identical pattern to SP13 hold_rate. Verification: `SQLX_OFFLINE=true cargo check -p ml --lib` clean (dev profile, warnings unrelated to this change); `cargo test -p ml --test sp14_oracle_tests` 2/2 non-GPU pass (7 GPU tests `ignored` on RTX 3050 Ti host). Files modified: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` (4 visibility flips); `crates/ml/src/cuda_pipeline/gpu_experience_collector.rs` (+4 struct fields, +4 cubin loads in `new()`, +4 field placements in struct literal). + SP14 B.3 empty-batch decay fix — gate q_disagreement EMA writes on `total_cnt > 0` (2026-05-07): root-caused the L40S `train-6fcml` 5-epoch trajectory's `α_smoothed=0.0002` + `gate1=closed-forever` symptoms (continuation of the per-step cadence migration in commit `5608b866b`) to a second-order bug exposed by that migration. Pre-migration the kernel fired once per epoch with batches that had meaningful Q-direction diversity; post-migration it fires per training step against replay batches whose Q-direction picks are dominated by Hold/Flat (the natural distribution of the policy's argmax over the 4-way direction head). HEALTH_DIAG sequence proved the regression: HEALTH_DIAG[0] post-experience-collection showed `q_dis_s=0.0595 q_dis_l=0.1329 var_q=0.00091` (meaningful rollout signal); HEALTH_DIAG[1+] post-training showed `q_dis_s=0.0000 q_dis_l=0.0000 var_q=0.00000` — signal decayed to zero inside ONE epoch (~178 training steps × 0.7^n ≈ 0). Mechanism: in `q_disagreement_update_kernel.cu` the `tid==0` ISV write block ran unconditionally even when `total_cnt` (post-Hold/Flat-mask non-empty contribution count) was 0; the `fmaxf(total_cnt, 1.0f)` guard kept the divide finite but produced `batch_mean = 0/1 = 0`, which then blended into the EMAs as `(1-α)·prev + α·0` — exponential decay of the rollout signal. Fix (atomic, single kernel): wrap the entire ISV write block in `if (total_cnt > 0.0f) { ... }`; remove the now-redundant `&& (total_cnt > 0.0f)` clause from the `is_first` Pearl-A bootstrap check (guaranteed by the outer gate). When the training batch has no non-masked rows the kernel becomes a no-op for that step — EMAs stay at the prior step's values; stream-ordered launches still run, only the ISV write is skipped. Per `pearl_first_observation_bootstrap`: "no observation" preserves prior; only "first observation" replaces sentinel — decay-on-empty was inconsistent with both rules. Other EGF-chain kernels audited: `alpha_grad_compute_kernel.cu` operates on persistent ISV state with no batch concept (var_aux/var_alpha Welford updates use diffs of persistent EMAs, not batch means) — SAFE; `aux_dir_acc_reduce_kernel.cu` emits sentinel 0.5 when denom==0 and the downstream `apply_fixed_alpha_ema` blends 0.5 toward EMA, which pulls toward the harmless random-baseline rather than zero (different semantics from the destructive 0 in q_disagreement) — SAFE; `gradient_hack_detect_kernel.cu` is a single-thread state machine on persistent ISV with no batch — SAFE. Files modified: `crates/ml/src/cuda_pipeline/q_disagreement_update_kernel.cu` (+27 / −20 lines: outer gate, dedented body, redundant-guard removal, post-train-6fcml audit-trail comment); `crates/ml/tests/sp14_oracle_tests.rs` (+111 lines: new `q_disagreement_empty_batch_preserves_ema` strict bit-equality test, pre-seeds the train-6fcml HEALTH_DIAG[0] values 0.0595/0.1329/0.0009 — deliberately NOT the 0.5 sentinel — and asserts identity preservation across an all-Hold batch). Existing `q_disagreement_all_hold_no_contribution` test still passes — its loose bound `[0.0, 0.5]` accepted both the old blend-toward-0.35 behavior and the new preserve-at-0.5; the new strict test is what catches the warm-start regression. All 7 sp14_oracle_tests pass on RTX 3050 Ti (CUDA 12.4, sm_86): `alpha_grad_adaptive_beta`, `alpha_grad_schmitt_hysteresis`, `dir_concat_qaux_correct`, `gradient_hack_circuit_breaker_fires`, `q_disagreement_all_hold_no_contribution`, `q_disagreement_empty_batch_preserves_ema` (NEW), `q_disagreement_k4_k2_mapping`. Build clean: `SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo check -p ml --tests --all-targets`. Invariants honored: `pearl_no_host_branches_in_captured_graph` (kernel modification is pure GPU code, no host branches added — the `if (total_cnt > 0.0f)` is a device-side branch on a value already in shared memory, identical hardware semantics to the existing `if (tid == 0)` and `if (gate1 >= 0.5f)` patterns elsewhere in the EGF chain); `feedback_no_partial_refactor` (the 3 ISV slots — short EMA 383, long EMA 384, variance EMA 389 — are gated atomically inside the same `if` block, no partial coverage); `feedback_no_stubs` (no commented-out gates, no `if false`, no dead params); `pearl_first_observation_bootstrap` (sentinel 0.5 still bootstraps on first valid observation; preservation on empty is the missing third case the pearl already mandates). HEALTH_DIAG validation pending L40S re-dispatch — expect post-training `q_dis_s/q_dis_l/var_q` to track the post-rollout values rather than collapse to zero, unblocking the EGF gate1 from its closed-forever state. Producer-cadence sweep — 4 more per-step producers MOVED out of `process_epoch_boundary` (2026-05-07): continuation of SP14 B.11 (commit `200f05fce`). Audit of `training_loop.rs:4100-4262` per the `feedback_no_partial_refactor` rule found 4 ADDITIONAL producers whose docstrings explicitly claim "per-step" cadence but whose launches lived in `process_epoch_boundary` (single call site at `training_loop.rs:780`, fires once per epoch — not per training step inside `run_training_steps_slices`). Each has a verified per-step consumer reading the ISV slot every training step inside the parent captured graph: `launch_h_s2_rms_ema` (ISV[H_S2_RMS_EMA_INDEX=96] → `mag_concat_kernel` per-step in forward graph for adaptive `save_h_s2` magnitude-branch concat scale, AND `dqn_clamp_finite_f32_kernel`'s `1e6 × ISV[96]` cuBLAS-backward IQN-trunk sanitiser bound), `launch_fold_warmup_factor` (ISV[FOLD_WARMUP_FACTOR_INDEX=130] → `training_loop.rs:2662` per-step host read deriving `lr_eff = lr_base × max(0.05, factor)` and `clip_eff = clip_base × (0.1 + 0.9 × factor)`), `launch_moe_expert_util_ema` (ISV[MOE_EXPERT_UTIL_EMA_BASE..+8) + ISV[MOE_GATE_ENTROPY_EMA_INDEX=126] → consumed by `launch_moe_lambda_eff_update` immediately below), and `launch_moe_lambda_eff_update` (ISV[MOE_LAMBDA_EFF_INDEX=128] → `moe_load_balance_loss` kernel reads `λ` per-step at `gpu_dqn_trainer.rs:16864` in the captured-graph backward path). All 4 launchers use pre-loaded `CudaFunction` fields with no per-call `load_cubin` (`gpu_dqn_trainer.rs:12646, 30983, 16901, 16969`) — graph-capture safe per `pearl_no_host_branches_in_captured_graph`. Atomic migration per `feedback_no_partial_refactor`. MoE producer + λ-controller order preserved (`launch_moe_lambda_eff_update` MUST run AFTER `launch_moe_expert_util_ema` per the kernel docstring at `gpu_dqn_trainer.rs:16962`). Cold-start ordering: `h_s2_rms_ema` reads `save_h_s2` populated by this step's online forward, MoE producers read `moe_gate_softmax_buf` populated by `launch_moe_forward` (in `submit_forward_ops_main`); both run BEFORE `submit_aux_ops` per `capture_training_graph` ordering (forward_child → ddqn_child → aux_child). State-reset registry coverage already in place (`isv_h_s2_rms_ema`, `isv_fold_warmup_factor`, `isv_moe_expert_util_ema`, `isv_moe_gate_entropy_ema`, `isv_moe_lambda_eff` at `state_reset_registry.rs:304/427/380/387/398` plus companion `isv_grad_norm_fast_ema` at line 442 for the warmup factor's numerator). Producers DELIBERATELY KEPT per-epoch (audit trail): `launch_trade_attempt_rate_ema_inplace` / `launch_plan_threshold_update_inplace` / `launch_seed_step_counter_update_inplace` / `launch_cql_alpha_seed_update_inplace` (collector EMAs reading per-sample buffers populated by `experience_env_step` during `collect_experiences_gpu` — input data only updates per-epoch, so per-step launches would compute the same EMA repeatedly off the same data); `launch_iqn_quantile_ema` / `launch_vsn_mask_ema` / `launch_aux_heads_loss_ema` (HEALTH_DIAG diag-only consumers — slots 99..103, 105..111, 113, 114 are read by per-epoch HEALTH_DIAG emit lines and the GPU `health_diag_kernel`'s per-epoch snapshot path; no per-step consumer). HEALTH_DIAG read points unchanged — per-epoch reads of per-step-updated slots get the latest value (strict improvement over per-epoch reads of per-epoch-stale slots). Verification: `cargo check -p ml --tests --all-targets` clean (dev profile, only pre-existing unused-var warnings unrelated to this change). Files modified: `crates/ml/src/trainers/dqn/fused_training.rs` (+72 lines: 4 launches + comment block in `submit_aux_ops` immediately after the SP14 EGF chain); `crates/ml/src/trainers/dqn/trainer/training_loop.rs` (−45 / +21 lines: deletion of stale per-epoch launches + replacement audit comments). DEFERRED follow-up — additional SP4 Layer A / SP5 Pearl producers in `process_epoch_boundary` lines 4264-4406 (`launch_sp4_target_q_p99`, `launch_sp4_atom_pos_p99_all_branches`, `launch_sp4_grad_norm_p99`, `launch_sp4_h_s2_p99`, `launch_sp4_param_group_oracles_all_groups`, `launch_sp5_pearl_1_atom`, `launch_sp5_pearl_3_sigma`, `launch_sp5_pearl_2_budget`, `launch_max_budget_compute`, `launch_loss_balance_controller`, `launch_sp5_pearl_4_adam_hparams`, `launch_sp5_pearl_5_iqn_tau`, `launch_sp5_pearl_8_trail`, `launch_sp5_pearl_1_ext_num_atoms`) write ISV bounds with VERIFIED per-step consumers (Mech 1 target_q clip, ISV-driven `read_group_adam_bounds` at `submit_aux_ops` lines 1648/2062/2074, `apply_c51_budget_scale` at line 1941, IQN τ schedule, atoms_update via `pearl_1_ext_num_atoms`'s ATOM_NUM_ATOMS_BASE and trail_dist consumers in `experience_env_step`); their docstrings DON'T explicitly claim per-step cadence (silence on cadence) so they do not match the explicit migration heuristic from this commit's audit. Per the user-specified strict heuristic ("docstring per-step + per-step consumer ⇒ migrate"), this sweep deliberately scoped only to docstring-claimant producers; the SP4/SP5 epoch-block producers staleness is a known follow-up tracked here but NOT changed in this commit to keep the scope tight and the validation surface manageable. The atom_pos/SP4 bound producers AND the SP7 controller chain were intentionally LEFT in `process_epoch_boundary` for that reason.