From 06989cfdf9dc0a4bfac73ad0122bea617c554d60 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Fri, 24 Apr 2026 10:18:26 +0200 Subject: [PATCH] infra(dqn-v2): audit doc scaffolding + pre-commit enforcement Plan 1 Task 1. Creates the five audit docs plus config/metric-bands.toml that track Invariants 2, 7, 8 per the DQN v2 spec, and extends the pre-commit hook with two checks: - component-adding commits must touch an audit doc (Invariant 7) - added code may not contain TODO/FIXME/XXX/HACK/TBD/unimplemented!/ todo! markers (Invariant 9) Tests: manually verified by staging a TODO-marked file; commit rejected with the correct error message. Co-Authored-By: Claude Opus 4.7 (1M context) --- config/metric-bands.toml | 13 +++ docs/dqn-gpu-hot-path-audit.md | 14 +++ docs/dqn-named-dims.md | 111 +++++++++++++++++++++ docs/dqn-wire-up-audit.md | 15 +++ docs/isv-slots.md | 19 ++++ docs/ml-supervised-to-dqn-concept-audit.md | 18 ++++ scripts/pre-commit-hook.sh | 39 ++++++++ 7 files changed, 229 insertions(+) create mode 100644 config/metric-bands.toml create mode 100644 docs/dqn-gpu-hot-path-audit.md create mode 100644 docs/dqn-named-dims.md create mode 100644 docs/dqn-wire-up-audit.md create mode 100644 docs/isv-slots.md create mode 100644 docs/ml-supervised-to-dqn-concept-audit.md diff --git a/config/metric-bands.toml b/config/metric-bands.toml new file mode 100644 index 000000000..4bda913dd --- /dev/null +++ b/config/metric-bands.toml @@ -0,0 +1,13 @@ +# Regression-detection bands for DQN training metrics. +# Invariant 7. Per A.4, bands populated from last 3 known-good runs. +# Band = [mean − 3σ, mean + 3σ] per metric; recomputed after baseline promotion. +# N consecutive out-of-band epochs → WARN; 2N consecutive → ERROR + SIGTERM-self. + +[settings] +consecutive_epochs_for_warn = 3 +consecutive_epochs_for_error = 6 + +# Placeholder bands — populated by A.4.1 nsys baseline run + populate_metric_bands.sh +# (populated during Plan 5 execution; this file lands with empty metric entries for Plan 1). + +[bands] diff --git a/docs/dqn-gpu-hot-path-audit.md b/docs/dqn-gpu-hot-path-audit.md new file mode 100644 index 000000000..4739a9f8f --- /dev/null +++ b/docs/dqn-gpu-hot-path-audit.md @@ -0,0 +1,14 @@ +# DQN GPU Hot-Path Audit + +**Invariant 3 enforcement.** Every cross-boundary call (DtoH, HtoD, memcpy between host and device) in the DQN code path is classified below. No hot-path `memcpy` DtoH/HtoD is allowed — zero tolerance. Pinned + device-mapped memory is the ONLY permitted mechanism for per-step host-visible data. + +**Hot path definition:** every function called from `fused_training.rs::step_fused` or its descendants that executes once per training step (178 times per epoch at batch size 8192), including kernel invocations inside the captured CUDA graph (`adam_grad_child`, `forward_child`, `aux_grad_child`) and host-side orchestration between them. + +**Classification:** +- `OK-pinned` — uses the zero-copy pinned + device-mapped pattern (allowed). +- `MIGRATE` — uses `memcpy` but should be pinned. Fix during spec impl. +- `COLD-PATH` — acceptable placement (checkpoint save/load, fold transition, epoch-boundary validation), annotated with reason. + +| File:line | Call signature | Path class | Annotation | Status | +|---|---|---|---|---| +| (populated during A.6 audit) | | | | | diff --git a/docs/dqn-named-dims.md b/docs/dqn-named-dims.md new file mode 100644 index 000000000..828c1ea7d --- /dev/null +++ b/docs/dqn-named-dims.md @@ -0,0 +1,111 @@ +# DQN Named Dimensions Registry + +**Invariant 8 enforcement.** Every semantic index has a named constant. Raw indices (`[0]`, `[6]`, `[23]`) appear ONLY at definition sites. Consumer code always uses the named constant. + +## State vector offsets + +Defined in `crates/ml/src/cuda_pipeline/state_layout.cuh` and mirrored in Rust via `ml-core::state_layout`. + +| Constant | Value | Meaning | +|---|---|---| +| `SL_STATE_DIM` | 104 | Total state vector length | +| `SL_STATE_DIM_PADDED` | 128 | Padded for cuBLAS K-tile alignment | +| `SL_MARKET_DIM` | 42 | Market-feature block width | +| `SL_OFI_DIM` | 32 | OFI block width | +| `SL_MTF_DIM` | 16 | Multi-timeframe block width | +| `SL_PORTFOLIO_BASE_DIM` | 8 | Portfolio-base block width | +| `SL_PORTFOLIO_PLAN_DIM` | 6 | Plan-ISV block width (extended to 7 in Plan 2) | +| `SL_MARKET_START` | 0 | | +| `SL_OFI_START` | 42 | | +| `SL_MTF_START` | 74 | | +| `SL_PORTFOLIO_START` | 90 | | +| `SL_PLAN_ISV_START` | 98 | | + +## Portfolio state slots (ps[0..30)) + +Defined in `crates/ml/src/cuda_pipeline/experience_kernels.cu` header comments. Migrated to named constants during Task 4 of this plan. + +| Index | Constant | Meaning | +|---|---|---| +| 0 | `PS_VALUE` | Equity | +| 1 | `PS_POSITION` | Current position | +| 2 | `PS_CASH` | Available cash | +| 3 | `PS_ENTRY_PRICE` | Entry price | +| 4 | `PS_MAX_EQUITY` | Peak equity (for drawdown) | +| 5 | `PS_HOLD_TIME` | Bars held | +| 6 | `PS_CUM_RETURN` | Cumulative return | +| 7 | `PS_STEP_COUNT` | Bars in window | +| 8 | `PS_SPREAD_COST` | Current spread cost | +| 9 | `PS_PREV_EQUITY` | Equity at previous step | +| 10 | `PS_HOLD_TIME_TOTAL` | Total hold time | +| 11 | `PS_REALIZED_PNL` | Cumulative realised PnL | +| 12 | `PS_WIN_COUNT_UNUSED` | (slot retained, not currently used) | +| 13 | `PS_LOSS_COUNT_UNUSED` | (slot retained, not currently used) | +| 14 | `PS_KELLY_WIN_COUNT` | Kelly stats: winning trades | +| 15 | `PS_KELLY_LOSS_COUNT` | Kelly stats: losing trades | +| 16 | `PS_KELLY_SUM_WINS` | Kelly stats: sum of winning returns | +| 17 | `PS_KELLY_SUM_LOSSES` | Kelly stats: sum of \|losing returns\| | +| 18 | `PS_RESERVED_18` | reserved | +| 19 | `PS_RESERVED_19` | reserved | +| 20 | `PS_INTRA_TRADE_MAX_DD` | worst intra-trade drawdown (v8 reward) | +| 21 | `PS_INTRA_TRADE_MAX_PNL` | best intra-trade unrealised PnL (hindsight) | +| 22 | `PS_RESERVED_22` | (was interval_sum, slot retained) | +| 23 | `PS_PLAN_TARGET_BARS` | plan: max hold bars (>0.5 = plan active) | +| 24 | `PS_PLAN_PROFIT_TARGET` | plan: raw profit threshold | +| 25 | `PS_PLAN_STOP_LOSS` | plan: raw stop threshold | +| 26 | `PS_PLAN_SCALE_AGGRESSION` | plan: position ramp speed | +| 27 | `PS_PLAN_CONVICTION` | plan: conviction at entry ∈ [0,1] | +| 28 | `PS_PLAN_ASYMMETRY` | plan: profit/stop asymmetry | +| 29 | `PS_PLAN_ENTRY_REGIME` | plan: regime_stability at entry (for P12) | + +## Plan_isv dimensions (plan_isv[0..6)) + +Will extend to 7 in Plan 2 (D.6 remaining_fraction). These constants live alongside `SL_PLAN_ISV_START` in `state_layout.cuh`. + +| Index | Constant | Meaning | +|---|---|---| +| 0 | `PLAN_ISV_PROGRESS` | hold_time / target_bars (clamped 2.0) | +| 1 | `PLAN_ISV_PNL_VS_TARGET` | unrealized / (target × equity) | +| 2 | `PLAN_ISV_PNL_VS_STOP` | -unrealized / (stop × equity) | +| 3 | `PLAN_ISV_ENTRY_CONVICTION` | conviction at entry | +| 4 | `PLAN_ISV_CONVICTION_DRIFT` | current conviction − entry conviction | +| 5 | `PLAN_ISV_REGIME_SHIFT` | \|isv[11] − entry_stability\| | + +## Plan_params output (plan_params[0..6)) + +From the trade_plan MLP output. + +| Index | Constant | Meaning | +|---|---|---| +| 0 | `PLAN_PARAM_TARGET_BARS` | | +| 1 | `PLAN_PARAM_PROFIT_TARGET` | | +| 2 | `PLAN_PARAM_STOP_LOSS` | | +| 3 | `PLAN_PARAM_SCALE_AGGRESSION` | | +| 4 | `PLAN_PARAM_CONVICTION` | | +| 5 | `PLAN_PARAM_ASYMMETRY` | | + +## Branch indices (4-branch factored action) + +| Index | Constant | Meaning | +|---|---|---| +| 0 | `BRANCH_DIR` | Direction branch (4 actions) | +| 1 | `BRANCH_MAG` | Magnitude branch (3 actions) | +| 2 | `BRANCH_ORD` | Order-type branch (3 actions) | +| 3 | `BRANCH_URG` | Urgency branch (3 actions) | + +## Direction action sub-indices + +| Index | Constant | Meaning | +|---|---|---| +| 0 | `DIR_SHORT` | Open short (or close long + open short) | +| 1 | `DIR_HOLD` | Keep current position (no-op on position) | +| 2 | `DIR_LONG` | Open long (or close short + open long) | +| 3 | `DIR_FLAT` | Close any position to zero | + +## Magnitude action sub-indices + +| Index | Constant | Meaning | +|---|---|---| +| 0 | `MAG_QUARTER` | 0.25× max_position | +| 1 | `MAG_HALF` | 0.50× max_position | +| 2 | `MAG_FULL` | 1.00× max_position | diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md new file mode 100644 index 000000000..f7858b709 --- /dev/null +++ b/docs/dqn-wire-up-audit.md @@ -0,0 +1,15 @@ +# DQN v2 Wire-Up Audit + +**Status:** Populated during task #95 (A.5 orphan audit). Updated on every commit per Invariant 7. + +**Legend:** +- `Wired` — consumed by production training + val path. +- `Partial` — consumed on one side only (training OR val, or forward OR backward). +- `Orphan` — built but no production consumer. +- `Ghost` — consumer path is stubbed. + +## Components + +| Module / kernel | Consumer path | Classification | Notes | +|---|---|---|---| +| (populated during A.5 audit) | | | | diff --git a/docs/isv-slots.md b/docs/isv-slots.md new file mode 100644 index 000000000..28059a7e0 --- /dev/null +++ b/docs/isv-slots.md @@ -0,0 +1,19 @@ +# ISV Slot Allocation Registry + +**Source of truth for ISV bus slot allocations.** Every slot has a named constant in `gpu_dqn_trainer.rs`. This doc is the cross-reference. + +**Current `ISV_TOTAL_DIM` as of this plan's start:** 37. Post-full DQN v2 rollout: 72. + +| Index | Name constant | Type | Producer | Consumers | Reset-category | Notes | +|---|---|---|---|---|---|---| +| [0] | `ISV_SCHEMA_VERSION_INDEX` | u32 | construct | load | SchemaContract | Bumped on any slot-layout change | +| [1..12) | (pre-existing) | f32 | varies | varies | varies | Legacy — audit during A.2 | +| [12] | `LEARNING_HEALTH_INDEX` | f32 | isv_signal_update | many | FoldReset | health score ∈ [0, 1] | +| [13..17) | `Q_MAG_MEAN_*_INDEX` | f32 | q_mag_means_reduce | c51 kernels | FoldReset | Quarter/Half/Full mag Q-mean EMAs + |Q| ref | +| [17..22) | `Q_DIR_MEAN_*_INDEX` | f32 | q_dir_means_reduce | c51 kernels | FoldReset | Short/Hold/Long/Flat dir Q-mean EMAs + |Q| ref | +| [22] | `SHARPE_EMA_INDEX` | f32 | training_loop host | isv_signal_update | FoldReset | Training Sharpe EMA | +| [23..31) | `V_{CENTER,HALF}_{DIR,MAG,ORD,URG}_INDEX` | f32 | update_eval_v_range | adaptive_atoms, warm_start | FoldReset | Per-branch Q-support | +| [31..35) | `GRAD_NORM_TARGET_*_INDEX` | f32 | grad_balance_isv_update | branch_grad_rescale | SoftReset(decay_bars=500) | Per-branch grad-norm target | +| [35] | `GRAD_SCALE_LIMIT_INDEX` | f32 | grad_balance_isv_update | branch_grad_rescale | SoftReset(decay_bars=500) | Scale clamp limit | +| [36] | `IQL_BRANCH_SCALE_FLOOR_INDEX` | f32 | construct (static) | iql_per_branch_advantage | SchemaContract | Per-sample branch_scales floor; safety bound | +| [37..72) | (reserved for DQN v2) | | | | | Allocated incrementally by Plans 2-5 | diff --git a/docs/ml-supervised-to-dqn-concept-audit.md b/docs/ml-supervised-to-dqn-concept-audit.md new file mode 100644 index 000000000..7d4f5bd7f --- /dev/null +++ b/docs/ml-supervised-to-dqn-concept-audit.md @@ -0,0 +1,18 @@ +# Supervised → DQN Concept Audit (Part E deliverable) + +Authoritative decisions on which ml-supervised concepts the DQN adopts. Per Invariant 9 (no deferred work), every entry is either `IN`, `OUT`, or `AUDIT-THEN-DECIDE` with the audit producing one of the two terminal classifications during this spec's implementation. + +| Concept | Supervised home | Classification | Action plan | Status | +|---|---|---|---|---| +| TFT Variable Selection Network | ml-supervised::tft | IN | E.1: extend vsn_mag/vsn_dir to full VSN over state feature groups | pending | +| Gated Residual Network | ml-supervised::tft | IN-IF-ABSENT | E.2: if A.5 audit finds absent, adopt; else mark existing as canonical | pending | +| TFT multi-quantile heads | ml-supervised::tft | IN | E.3: extend IQN to 5/25/50/75/95 quantile decomposition | pending | +| TLOB attention encoder | ml-supervised::tlob | IN | D.8 (Plan 2): wire to DQN trunk | pending | +| Mamba2 forward | ml-supervised::mamba2 | WIRED | already in DQN trunk | — | +| Mamba2 backward | ml-supervised::mamba2 | IN | D.1 (Plan 2): complete | pending | +| Liquid Time-constant | ml-supervised::liquid | AUDIT-THEN-WIRE-OR-DELETE | D.7 (Plan 2): audit decides | pending | +| xLSTM | ml-supervised::xlstm | OUT | Redundant with Mamba2 + TLOB; no DQN wiring | — | +| KAN | ml-supervised::kan | OUT | Function-approx not a bottleneck; no DQN wiring | — | +| Encoder-decoder separation | ml-supervised::tft | IN | E.4: extend D.3 horizon-decomposed V to full trunk/head separation | pending | +| Attention-weight interpretability | various | IN | E.5: ISV-expose attention weights from all temporal heads | pending | +| Multi-task auxiliary heads | ml-supervised | IN | E.6: next-return + regime-classification auxiliary heads | pending | diff --git a/scripts/pre-commit-hook.sh b/scripts/pre-commit-hook.sh index b0d425763..90fe72627 100755 --- a/scripts/pre-commit-hook.sh +++ b/scripts/pre-commit-hook.sh @@ -69,6 +69,45 @@ if [ -x "$SCRIPT_DIR/gpu-hotpath-guard.sh" ]; then echo "" fi +# DQN v2 Invariant 7 enforcement: component-adding commits must update audit docs. +check_audit_doc_updates() { + local staged=$(git diff --cached --name-only) + # Detect "component-adding" commits: new .rs or .cu files, or modifications to + # files that define ISV slots (gpu_dqn_trainer.rs), kernels (cuda_pipeline/*.cu), + # or state layout (state_layout.cuh). + local component_changes=$(echo "$staged" | grep -E '(^crates/ml/src/(cuda_pipeline|trainers/dqn)/|^crates/ml/src/cuda_pipeline/state_layout\.cuh$)') + if [ -z "$component_changes" ]; then return 0; fi + + local audit_docs_touched=$(echo "$staged" | grep -E '^docs/(dqn-wire-up-audit|isv-slots|dqn-gpu-hot-path-audit|dqn-named-dims|ml-supervised-to-dqn-concept-audit)\.md$') + if [ -z "$audit_docs_touched" ]; then + echo "❌ Invariant 7 violation: component changes without audit-doc update" + echo " Changed components:" + echo "$component_changes" | sed 's/^/ /' + echo " You must update one of:" + echo " docs/dqn-wire-up-audit.md" + echo " docs/isv-slots.md" + echo " docs/dqn-gpu-hot-path-audit.md" + echo " docs/dqn-named-dims.md" + echo " docs/ml-supervised-to-dqn-concept-audit.md" + return 1 + fi +} + +# DQN v2 Invariant 9 enforcement: reject TODO/FIXME/XXX/HACK/TBD markers in added code. +check_no_todo_fixme() { + local added_lines=$(git diff --cached --diff-filter=AM -U0 -- '*.rs' '*.cu' '*.cuh' \ + | grep -E '^\+' | grep -vE '^\+\+\+') + local bad=$(echo "$added_lines" | grep -E '(TODO|FIXME|\bXXX\b|\bHACK\b|\bTBD\b|unimplemented!|todo!\()') + if [ -n "$bad" ]; then + echo "❌ Invariant 9 violation: deferred-work markers found in staged code" + echo "$bad" | sed 's/^/ /' + return 1 + fi +} + +check_audit_doc_updates || exit 1 +check_no_todo_fixme || exit 1 + echo "✅ All pre-commit checks passed!" echo "" echo "Summary:"