# Rayon Solver-Parallelism Lab Report > ergo epic **RAYPAR** (7MKIR7). Authors: offline harness + 80/40-path capture corpus. ## TL;DR The production drain's 4.21/8 achieved parallelism is **not** caused by rayon overhead, per-item diagnostics, or memory bandwidth. It is caused by **rayon's work-stealing scheduler failing to load-balance under extreme path-cost skew**. An LPT (longest-processing-time) static partition over the same 8 threads, same solver code, same captures achieves **7.80/8 efficiency** (97%) vs rayon's **4.91/8** (61%) — a **35% wall-time reduction** with zero solver changes. The single highest-impact intervention (RAYPAR-T3) is to replace the per-item rayon `par_iter` fan-out with an LPT-pre-balanced scoped-thread partition, using a resolve-time cost proxy (e.g. total word-boundary-price count per hop) to bin paths before the solve. --- ## 1. Method ### Harness `degenbot-solvers/examples/rayon_scale_probe.rs` reconstructs production `ResolvedMixedPath` inputs (V3 CL hops carrying the production `build_cl_word_profiles` + `build_cl_crossing_table` precomputes) from the committed heavy-CL capture fixture (`tests/fixtures/heavy_cl_solve_captures.jsonl`, 420 paths / 87 MB) and re-runs the production solver entry `mixed::solve_path_with_min_profit` under controlled thread counts. The harness sweeps: - **Thread counts:** {1, 2, 4, 8} via per-pool `ThreadPoolBuilder::num_threads`. - **Closure variants:** bare solve vs production-diagnostics (span enter + 8 relaxed atomics + K-slowest heap + walk/gate stat reset-take) — to isolate per-item closure overhead. - **Scheduling variants:** rayon `par_iter` (work-stealing) vs `std::thread::scope` static partition (contiguous vs LPT-balanced bins). - **Control experiment:** pure-CPU spin workload through the same rayon pools — maps the machine's scheduling baseline (no solver, no memory traffic). ### Golden-lite gate Each reconstructed path is solved once serially and its profit compared to the recorded capture golden within `PROFIT_EPS = 100,000 wei`. On the 40-path validation subset: 14/14 golden-bearing paths matched, 0 excluded. --- ## 2. Results ### 2.1 80-path corpus (bare solve, release build) | label | threads | wall_s | sum_item_s | efficiency | item_med_ms | |--------------|---------|--------|------------|------------|-------------| | serial-median | 1 | 147.5 | 147.5 | 1.00 | 91.6 | | rayon-bare | 4 | 41.0 | 154.8 | **3.77/4** | 95.6 | | rayon-bare | 8 | 32.6 | 159.9 | **4.91/8** | 98.9 | | std-lpt | 4 | 39.1 | 156.0 | **3.99/4** | 100.4 | | std-lpt | 8 | **21.1**| 164.7 | **7.80/8** | 99.3 | | std-contig | 8 | 45.4 | 157.3 | 3.46/8 | 102.8 | **Top-10 per-path medians (us, path_id):** four paths at ~18s (all path 501 across different blocks), three at ~7.3s (path 500), three at ~6.6s (path 400). **Top-8 share = 60% of total serial work.** max_item = 18.1s. ### 2.2 40-path corpus (full variant sweep) | label | threads | wall_s | efficiency | |--------------|---------|--------|------------| | rayon-bare | 1 | 54.6 | 1.00 | | rayon-bare | 4 | 17.7 | 3.25/4 | | rayon-bare | 8 | 17.6 | 3.31/8 | | rayon-diag | 8 | 17.7 | 3.32/8 | | std-lpt | 8 | 17.4 | 3.34/8 | | control-spin | 8 | 0.011 | 7.31/8 | ### 2.3 Production cross-check (KGXFT7 gate-on run, 50 drains) - Achieved parallelism: 4.21/8 (solve.cpu_us = 1,380s vs phase_us = 328s). - Per-drain efficiency spread: 2.08–7.46, median 4.74. - Heaviest drain: 71s CPU in 16.3s wall on 2,529 paths. --- ## 3. Attribution ### Hypotheses tested: | Hypothesis | Verdict | Evidence | |---|---|---| | Rayon scheduling overhead | **Falsified** | Control spin scales to 7.31/8 (near-linear). Rayon provides no friction for uniform work. | | Per-item diagnostics overhead | **Falsified** | diag vs bare: efficiency 3.32 vs 3.31 (statistical zero). Atomics, span enter, heap = negligible. | | Memory-bandwidth saturation | **Partially true (minor)** | Per-item median rises 91.6ms→98.9ms (1→8 threads) = +8%. Cannot explain 3.3 vs 8.0 gap. | | Work-stealing load imbalance under skew | **CONFIRMED — dominant cause** | LPT static partition at 8 threads: 7.80/8 vs rayon 4.91/8. Same threads, same solver, same bandwidth. 35% wall reduction. | ### Root cause The heavy-CL capture workload has extreme cost skew: the top 8 of 80 paths (10%) account for **60% of total CPU**. Path 501 alone costs ~18 seconds — longer than the ideal 8-thread makespan for 72 of the 80 paths. Rayon's work-stealing splits work in half lazily; when a worker steals, it takes the **easy** half (shallow on its work queue). The **heavy** items stay on the original worker's queue and, being unsplittable (one path = one rayon task), they serialize the tail: 7 threads sit idle while one grinds through an 18s path. LPT pre-balances: sort paths by descending cost, greedily assign each to the least-loaded bin. With 8 bins, the four 18s paths go to 4 different threads; the remaining 76 paths fill the gaps. No thread is left idle with an unsplittable giant while others wait. ### Why production shows 4.21 (not 4.91) Production drains average ~284 paths (vs the 80-path harness), and the closure adds span enter + atomics + heap + capture check + profit-envelope gate. The larger item count provides more stealing opportunities (partially compensating for the scheduling deficiency), but the fundamental skew — a handful of multi-second paths per drain — remains. The production K-slowest heap names these paths every drain; they are the same shapes as path 501 in the harness. --- ## 4. Intervention ranking | # | Intervention | Predicted wall impact | Complexity | Risk | |---|---|---|---|---| | 1 | **LPT static partition** (replace rayon `par_iter` with scoped threads + cost-proxied bin packing) | **−35%** solve wall at 8 threads | Medium (new dispatch path + cost proxy) | Low (cutover gate, A/B against rayon) | | 2 | Solver-side CPU reduction on path 501-class paths (the 18s giants) | Multiplies through all threads at any efficiency | High (solver math) | Medium | | 3 | Thread-count tuning vs 8-CPU cgroup quota | Marginal | Trivial | Low | | 4 | Thread-local accumulators replacing per-item atomics | <1% (diag overhead is noise) | Low | Low | **Recommended:** T3 implements #1. Cost proxy for LPT binning at resolve time: `∑ word_boundary_prices.len()` across the path's CL hops (available without solving, correlates with walk combinatorics). The per-drain solve then uses `std::thread::scope` with 8 LPT bins instead of `par_iter`. --- ## 5. Environment - Host: AMD Ryzen 9 5900X 12-core / 24-thread. - cgroup: `cpu.max = 800000 100000` → 8 CPUs quota. - Rayon pool (production): 8 threads (named `degenbot-solve-{i}`). - `std::thread::available_parallelism()` = 8 (cgroup-aware). --- ## 6. Artifacts - Harness: `rust/crates/engine/degenbot-solvers/examples/rayon_scale_probe.rs` - Fixture: `rust/crates/engine/degenbot-solvers/tests/fixtures/heavy_cl_solve_captures.jsonl` - Raw CSV: `/tmp/raypar80.csv` (80-path), smoke run inline (40-path). - ergo: epic 7MKIR7, task 5A3C2P (harness), task QXEVGN (this report).