# Performance audit: path to 2x on the hot paths (2026-07-18) Audit read-only: no engine code was modified. Machine: i5-13600KF (6P+8E, 20 threads), RTX 3090, build `maturin develop --release --features cuda` (LTO, cgu=1, debuginfo kept via env overrides). Every number is the median of at least 3 runs; machine load was verified with the MC-10M-CPU sentinel (6.3-6.5s clean on both sides of each suite; one loaded suite at 8.6-9.3s was discarded and rerun). **Conclusion: the CPU lite sweep can reach 2x (three workstreams). The GPU sweep cannot under the fp64 bit-parity constraint (ceiling ~1.35x). The single run tops out around 1.7x. The full `run_sweep` API still carries the unfixed twin of the JSON-getter bug: its Python post-processing costs 3.6x the entire lite compute.** ## Method Sampling profilers were unavailable (samply/ETW needs Administrator; py-spy cannot see rayon threads). Attribution comes from the engine's own instrumentation, which proved sufficient: 98% of wall*threads is attributed. - `ProfileData` (orchestrator.rs:400): per-phase us counters on every result, including each lite sweep combo. Summed across combos and compared to wall*threads. - `BT_PHASE6_DEBUG=1`: sub-splits output_build (metrics / capm / trade_stats / gather). - `MBT_GPU_PROBE=1`: GPU phases (nvrtc / hoist / h2d / sim / metrics / d2h). - Ablation: bars scaling (26k vs 100k), buy_and_hold vs ema_cross, max_parallelism 1..20, full vs lite API. Harness: copy of the session scratchpad `audit_harness.py` (subcommands: sentinel, profile_sweep, full_vs_lite, single1m, par_scale, batch3, load, access). ## Measured baselines (synthetic BTCUSDT 1h store, `benchmarks/_sweep_common.py` config) | Path | Result | Phase split | |---|---|---| | Single run, 26k bars | 0.85ms | signal 43%, sim 22%, output 32% | | Single run, 1M bars | 27ms | signal 9.9ms (37%), sim 8.4ms (31%), output 8.3ms (31%: metrics 4.5, capm 1.0, trade_stats ~2, gather/positions ~2.3) | | CPU lite sweep, 10k combos x 26k bars | 171ms = 58k c/s | per combo: signal_eval 96us (28%), sim phase 239us (70%) | | inside the sim phase | | per-bar loop ~200us (~8ns/bar = 36 cycles/bar), CAPM ~25us (measured 1ns/bar), O(days) metrics ~10us | | CPU sweep, 500k combos | rsi 9.1s / ema 9.9s / trix 12.4s | 40-55k c/s | | GPU sweep, 500k combos | rsi 1.67s / ema 0.98s / trix 2.35s | 5.4-10.2x vs CPU | | GPU, 100k combos x 26k bars | 240ms = 418k c/s | sim kernel 176ms (73%), metrics kernel 34ms (14%), h2d 6ms, d2h ~2ms | | Data load, 1M bars | cold 57ms, warm re-align ~0.5ms | store cache holds across calls | | Lite packaging/access | ~1us per `.metrics` | fixed path is healthy | | Full `run_sweep`, 900 combos | 80ms vs 16ms lite (5x) | then `.best()` 15us/combo, `.to_df()` 49us/combo in Python | Cross-checks: these numbers predict the historical 1M x 100k CPU sweep at ~107s, inside the observed 84-150s band. Parallel scaling is 9.9x on 20 threads, near the realistic ceiling (~11-12x) for 6P+8E: rayon granularity is not a lever. Combo enumeration is now lazy mixed-radix (sweep.rs:98); the old ~500B/combo materialization survives only in walk_forward/sweep_2d. Per-combo strategy recompile (dynamic periods): ~2%, cleared. ## Ranked opportunities Gain = on the bench that path owns. Effort: L (<1 day), M (days), H (week+). Parity = risk to CPU==GPU==general bit-identity (anchored to bt-expr, never a mirror). | # | Opportunity | Evidence | Where | Approach | Gain | Effort | Parity risk | |---|---|---|---|---|---|---|---| | 1 | Full-result `.metrics` = unfixed JSON twin | `.to_df()`+`.best()` = 58ms vs 16ms lite compute @900 combos | bt-python/src/result.rs:24-31, python sweep.py:75-93, dataframe.py:146-166 | Reuse `metrics_to_pydict` (backtest.rs:331); better: route `SweepResult.to_df/best` through `sweep_columns` | ~50x on full-sweep post-processing | L | None | | 2 | CPU transpiled sweep: kill per-combo signal materialization | signal_eval = 28% of sweep CPU | orchestrator.rs:2402-2709; reference core orchestrator.rs:5766 | Do on CPU what the GPU does: hoist plan + in-loop target eval; `sim_fast_lite_core_single` is the transpile-ready reference | ~1.33x CPU sweep | H | Low; re-anchor goldens, deliberately break the old path to prove coverage | | 3 | Hoist combo-invariant work: (a) CAPM benchmark pass, (b) ohl_nan/funding_nan copies | (a) measured 1ns/bar = ~7% of sweep; (b) 3 full O(bars) copies per combo on SL/TP sweeps, est. 25-40% tax (code-confirmed, not yet measured) | (a) orchestrator.rs:3395-3411 and 2082-2108; (b) orchestrator.rs:2821-2849 | Compute once in the sweep/batch drivers, pass by reference | 1.08x plain sweeps; ~1.2-1.4x SL/TP sweeps | L-M | None (same values, computed once) | | 4 | Per-bar loop tightening | loop ~58% of sweep CPU at 36 cycles/bar; dependency floor ~12-18 cycles | simulate_fast_lite_single / core (orchestrator.rs:5307/5766) | Branch elimination, layout. No FP reordering, no FMA: op order is the parity anchor | 1.15-1.3x sweep | H | High if careless; bit-verify vs bt-expr | | 5 | Single-run: parallelize independent signals + output_build components | @1M: signal 37% (fast/slow EMA sequential), output 31% (independent components) | orchestrator.rs:833-1204, 2059-2230 | rayon-join independent signal exprs (bit-safe); overlap output components; never parallelize the metric reductions | ~1.4-1.7x single run | M | Low if reductions stay sequential | | 6 | SIMD via runtime dispatch (ships SSE2 baseline: no target-cpu anywhere) | elementwise ops are a large slice of signal eval | bt-expr evaluator kernels | `is_x86_feature_detected!` dispatch on elementwise kernels only; folds/scans stay scalar | ~1.1-1.15x sweep | M | None for elementwise; forbidden for folds | | 7 | GPU metrics kernel fuse/overlap | 34ms of 240ms (14%) | gpu_sweep.rs:4395-4656 | Fuse into sim epilogue or stream overlap, same op order | <=1.16x GPU | M-H | Low | | 8 | Full `run_sweep` materializes full traces per combo | 5x lite; ~600KB/combo retained | orchestrator.rs:3480-3547 | Optional trace retention; steer to lite + `sweep_columns` | up to 5x for full-sweep users | M | None | | 9 | Coverage gates (funding on `run()` fast path, multi-asset+brackets on GPU, multi-asset kernel 33% occupancy) | gate list in bt-core | orchestrator.rs:1358-1364, 3998-4002 | Extend fast/GPU coverage case by case | 2-5x for affected configs | M-H each | Per-gate golden work | | 10 | Data loading (no mmap on Arrow IPC, exact-key cache only, N+1 sqlite symbol_info) | cold 1M load 57ms | bt-data arrow_ipc_store.rs:327/461, metadata.rs:28 | mmap like mega_store, superset-slice cache, batched lookups | <1% on benches | L-M | None | Maintenance note: the per-bar loop exists in four near-identical transcriptions (full/lite x general/fast) plus two ~130-LOC bracket macros; every loop optimization is written and parity-tested four times. Mechanical extraction of the shared fill/equity blocks is a safe enabler for #4. Any unification touching WHICH metrics lite computes would hit the lite contract (cagr/calmar/ulcer != run()), which stays as-is. ## Top 3 to reach 2x (CPU lite sweep) 1. **#3 hoists** (CAPM + bracket/funding copies): low effort, zero parity risk, ~1.08x plain sweeps, biggest single win on SL/TP sweeps. 2. **#2 CPU transpiled sweep**: ~1.33x, structural; the architecture is proven on GPU and the CPU core is already the kernel's reference. With #3: ~1.45x. 3. **#4 loop tightening** (+ #6 SIMD on residual signal eval): 36 -> ~24 cycles/bar closes the gap. Composite: **~1.9-2.2x**. ## Theoretical ceilings - CPU sweep: signal+CAPM removed, loop untouched -> max 1.44x. 2x requires the loop work; at the ~12-18 cycle dependency floor the composite ceiling is ~3x. - GPU sweep: sim kernel 73%, already SASS-audited to the fp64 parity ceiling; everything else free -> 1.37x max. 2x is not reachable under parity; opt-in fp32 remains the out. - Single run 1M bars: sim loop is serial; practical ceiling ~1.7x. - Data loading and lite packaging: <1% at bench scale, nothing to win. ## Compatibility guarantee for every item above None of the proposals removes or restricts any strategy feature. #2/#4/#6 follow the same pattern as the GPU path: strategies that qualify take the faster path, everything else falls back to today's code, and the fallback stays golden-tested (the "break the old path on purpose" rule applies when work moves). #3/#5/#7/#8/#10 compute identical values in fewer places. #9 strictly widens fast-path coverage. Bit-for-bit parity is re-verified against bt-expr for every change.