T3: rayon-backed concurrency (opt-in) (#2)
Implements T3 of `docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md` Section 6. Plan: `docs/superpowers/plans/2026-04-24-t3-concurrency.md` (11 tasks).
## Summary
### Breaking
- `Send + Sync` bounds added to public traits: `Time`, `Drift<T>`, `Observer<T>`, `Factor`, `Schedule`. All built-in impls satisfy these via auto-derive; downstream custom impls will need the bounds.
### New
- Opt-in `rayon` cargo feature. When enabled:
- Within-slice event iteration runs color-group events in parallel via `par_iter_mut` (`TimeSlice::sweep_color_groups`).
- `History::learning_curves` computes per-slice posteriors in parallel; merges sequentially in slice order.
- `History::log_evidence` / `log_evidence_for` use per-slice parallel computation with deterministic sequential reduction (sum in slice order) — bit-identical to the sequential baseline.
- `ColorGroups` infrastructure (`src/color_group.rs`) with greedy graph coloring. Events sharing no `Index` go into the same color group; events in the same group can run concurrently without touching each other's skills.
- `tests/determinism.rs` asserts bit-identical posteriors across `RAYON_NUM_THREADS={1, 2, 4, 8}`.
- `benches/history_converge.rs` measures end-to-end convergence on three workload shapes.
## Performance
### Sequential (no rayon, default build)
| Metric | Before T3 | After T3 | Delta |
|---|---|---|---|
| `Batch::iteration` | 22.88 µs | 23.23 µs | **+1.5%** (noise) |
| `Gaussian::*` | ≈218–264 ps | ≈236 ps | within noise |
**No sequential regression.** Default build is as fast as T2.
### Parallel (`--features rayon`, Apple M5 Pro, auto thread count)
| Workload | Sequential | Parallel | Speedup |
|---|---:|---:|---:|
| 500 events / 100 competitors / 10 per slice | 4.03 ms | 4.24 ms | **1.0×** |
| 2000 events / 200 competitors / 20 per slice | 20.18 ms | 19.82 ms | **1.0×** |
| 5000 events / 50000 competitors / 1 slice | 11.88 ms | 9.10 ms | **1.3×** |
### ⚠️ The spec's >=2× target was not met on realistic workloads.
T3's within-slice color-group parallelism only shows material benefit when a slice holds many events AND the competitor pool is large enough to give the greedy coloring room to partition. Typical TrueSkill workloads (tens of events per slice) don't fit that profile — rayon's task-spawn overhead dominates.
**Cross-slice parallelism (dirty-bit slice skipping per spec Section 5) is the natural next step** for real-workload speedup and would deliver the spec's ~50–500× online-add speedup. Deferred to a future tier.
## Determinism
`tests/determinism.rs` runs a 200-event history at thread counts {1, 2, 4, 8} via `rayon::ThreadPoolBuilder::install` and asserts every `(time, posterior)` pair has bit-identical `mu` and `sigma` (compared via `f64::to_bits()`). Passes.
## Internals
- Parallel path uses an `unsafe` block to concurrently write to `SkillStore` from color-group-disjoint events. Soundness rests on the color-group invariant (events in the same color touch no shared `Index`), guaranteed by construction in `TimeSlice::recompute_color_groups`. Sequential path unchanged from T2.
- `RAYON_THRESHOLD = 64` — color groups smaller than this fall back to sequential inside `sweep_color_groups` to avoid task-spawn overhead.
- Thread-local `ScratchArena` per rayon worker thread.
## Test plan
- [x] `cargo test --features approx` — 96 tests pass (74 lib + 22 integration)
- [x] `cargo test --features approx,rayon` — 97 tests pass (+1 determinism)
- [x] `cargo clippy --all-targets --features approx -- -D warnings` — clean
- [x] `cargo clippy --all-targets --features approx,rayon -- -D warnings` — clean
- [x] `cargo +nightly fmt --check` — clean
- [x] `cargo bench --bench batch --features approx` — 23.23 µs (no regression vs T2)
- [x] `cargo bench --bench history_converge --features approx,rayon` — runs on all three workloads
- [x] Bit-identical posteriors across `RAYON_NUM_THREADS={1, 2, 4, 8}` — verified
## Commit history
13 commits on `t3-concurrency`. Each task is self-contained and bisectable. See `git log main..t3-concurrency` for the full list.
## Deferred
- **Cross-slice parallelism** (dirty-bit slice skipping) — the path that would actually speed up typical TrueSkill workloads.
- **Default-on `rayon` feature** — spec called for default-on; we keep it opt-in until the feature proves stable in production use.
- **Synchronous-EP schedule with barrier merge** — alternative parallel strategy per spec Section 6.
- **`MarginFactor` / `Outcome::Scored`** — T4.
- **`Damped` / `Residual` schedules** — T4.
- **N-team `predict_outcome`** — T4.
- **`Game::custom` full ergonomics** — T4.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #2
Co-authored-by: Anders Olsson <anders.e.olsson@gmail.com>
Co-committed-by: Anders Olsson <anders.e.olsson@gmail.com>
This commit was merged in pull request #2.
This commit is contained in:
@@ -98,3 +98,35 @@ Gaussian::tau 260.80 ps (unchanged)
|
||||
# learning_curves_by_index(), nested-Vec public add_events().
|
||||
# - 90 tests green: 68 lib + 10 api_shape + 6 game + 4 record_winner +
|
||||
# 2 equivalence.
|
||||
|
||||
# After T3 (2026-04-24, same hardware)
|
||||
|
||||
Batch::iteration (seq, no rayon) 23.23 µs (matches T2 baseline; no regression)
|
||||
Batch::iteration (rayon, small slice) 24.57 µs (within noise; small workloads pay rayon overhead)
|
||||
Gaussian::add 236.62 ps (unchanged)
|
||||
Gaussian::sub 236.43 ps (unchanged)
|
||||
Gaussian::mul 237.05 ps (unchanged)
|
||||
Gaussian::div 236.07 ps (unchanged)
|
||||
|
||||
# End-to-end history_converge benchmark (Apple M5 Pro, RAYON_NUM_THREADS=auto):
|
||||
# workload seq rayon speedup
|
||||
# 500 events, 100 competitors, 10/slice 4.03 ms 4.24 ms 1.0x
|
||||
# 2000 events, 200 competitors, 20/slice 20.18 ms 19.82 ms 1.0x
|
||||
# 5000 events, 50000 competitors, 1 slice 11.88 ms 9.10 ms 1.3x
|
||||
#
|
||||
# Notes:
|
||||
# - T3's within-slice color-group parallelism only materializes a speedup
|
||||
# when a slice holds many events with disjoint competitor sets. Typical
|
||||
# TrueSkill workloads (tens of events per slice) don't show measurable
|
||||
# benefit from rayon.
|
||||
# - The pre-revert SmallVec experiment hit 2x on the 5000-event workload
|
||||
# but regressed sequential Batch::iteration by 28%. The tradeoff wasn't
|
||||
# worth it for typical workloads — ShipVec<[_; 8]> inline size (1 KB per
|
||||
# Game struct) hurt cache locality on the hot path.
|
||||
# - Cross-slice parallelism (dirty-bit slice skipping per spec Section 5)
|
||||
# is the natural next step for realistic TrueSkill workloads and would
|
||||
# deliver the spec's ~50-500x online-add speedup. Deferred to T4+.
|
||||
# - Determinism verified: tests/determinism.rs asserts bit-identical
|
||||
# posteriors across RAYON_NUM_THREADS={1, 2, 4, 8}.
|
||||
# - Send + Sync bounds added on Time, Drift<T>, Observer<T>, Factor, Schedule.
|
||||
# - Rayon is opt-in via `--features rayon`. Default build is unchanged from T2.
|
||||
|
||||
Reference in New Issue
Block a user