Deferred from #2 (T3). Spec: docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md §6, "Within-slice color-group parallelism" (the Alternative paragraph).
Why
T3 implemented the asynchronous-EP strategy: color-group partitioning, events in the same color run concurrently, colors run sequentially. This preserves today's semantics exactly but only parallelizes as well as the coloring allows — and on realistic workloads the coloring doesn't give rayon enough to chew on (see #4).
Synchronous EP is the alternative the spec names: all events read from a frozen skill snapshot, write deltas to thread-local buffers, and a barrier merges them. It is trivially parallel — no coloring, no unsafe disjoint-index writes — at the cost of weaker per-iteration convergence, which needs damping to compensate.
Current state
src/schedule.rs has exactly one Schedule impl:
EpsilonOrMax (src/schedule.rs:28) — forward/backward sweeps until step ≤ eps or iter == max.
The damping half of the prerequisite already exists, though built game-local rather than as a Schedule: ConvergenceOptions::alpha, Gaussian::damp_natural, and propagate_with_alpha on both TruncFactor and MarginFactor.
Note the Schedule trait signature is currently factor-level:
Spec §5 describes a generic Schedule::run<W: Workload>(&self, workload: &mut W) shared across all three convergence loops. A slice-level synchronous schedule needs that generalization, or a separate seam at the TimeSlice layer — decide which as part of this work.
Scope
Snapshot-read / thread-local-delta-write / barrier-merge sweep at the TimeSlice level.
Opt-in only. Asynchronous EP stays the default; this must not change results for anyone who doesn't ask for it.
Reuse the existing alpha damping rather than inventing a second damping path.
Acceptance
Converges to the same fixed point as the async path (within test epsilons) on the existing history fixtures.
Deterministic across thread counts, same bar as tests/determinism.rs: bit-identical mu/sigma via f64::to_bits() at RAYON_NUM_THREADS={1,2,4,8}.
Benchmarked against the async path on all three benches/history_converge.rs workloads — including iteration count, not just wall-clock, since weaker per-iteration convergence can eat the parallel win.
Related
#4 — cross-slice parallelism, the other deferred parallelism path. If #4 delivers, this may not be worth the complexity; measure before committing.
Deferred from #2 (T3). Spec: `docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md` §6, "Within-slice color-group parallelism" (the *Alternative* paragraph).
## Why
T3 implemented the asynchronous-EP strategy: color-group partitioning, events in the same color run concurrently, colors run sequentially. This preserves today's semantics exactly but only parallelizes as well as the coloring allows — and on realistic workloads the coloring doesn't give rayon enough to chew on (see #4).
Synchronous EP is the alternative the spec names: all events read from a frozen skill snapshot, write deltas to thread-local buffers, and a barrier merges them. It is trivially parallel — no coloring, no `unsafe` disjoint-index writes — at the cost of weaker per-iteration convergence, which needs damping to compensate.
## Current state
`src/schedule.rs` has exactly one `Schedule` impl:
- `EpsilonOrMax` (`src/schedule.rs:28`) — forward/backward sweeps until step ≤ eps or iter == max.
The damping half of the prerequisite already exists, though built game-local rather than as a `Schedule`: `ConvergenceOptions::alpha`, `Gaussian::damp_natural`, and `propagate_with_alpha` on both `TruncFactor` and `MarginFactor`.
Note the `Schedule` trait signature is currently factor-level:
```rust
fn run(&self, factors: &mut [BuiltinFactor], vars: &mut VarStore) -> ScheduleReport;
```
Spec §5 describes a generic `Schedule::run<W: Workload>(&self, workload: &mut W)` shared across all three convergence loops. A slice-level synchronous schedule needs that generalization, or a separate seam at the `TimeSlice` layer — decide which as part of this work.
## Scope
- Snapshot-read / thread-local-delta-write / barrier-merge sweep at the `TimeSlice` level.
- Opt-in only. Asynchronous EP stays the default; this must not change results for anyone who doesn't ask for it.
- Reuse the existing `alpha` damping rather than inventing a second damping path.
## Acceptance
- Converges to the same fixed point as the async path (within test epsilons) on the existing history fixtures.
- Deterministic across thread counts, same bar as `tests/determinism.rs`: bit-identical `mu`/`sigma` via `f64::to_bits()` at `RAYON_NUM_THREADS={1,2,4,8}`.
- Benchmarked against the async path on all three `benches/history_converge.rs` workloads — including iteration *count*, not just wall-clock, since weaker per-iteration convergence can eat the parallel win.
## Related
- #4 — cross-slice parallelism, the other deferred parallelism path. If #4 delivers, this may not be worth the complexity; measure before committing.
Heads-up: this issue's premise no longer holds after 633a503.
It proposes a synchronous-EP Schedule with snapshot and barrier merge. The Schedule trait has been removed, along with EpsilonOrMax, ScheduleReport, Game::custom, and the Factor / BuiltinFactor / RankDiffFactor / TeamSumFactor machinery it was built on — see #42.
The reason matters here rather than just the fact. Game::run_chain drives a local DiffFactor enum directly and has never gone through Schedule. So the trait was not an extension point that this issue would have extended; it was one nothing had ever plugged into, including the engine itself. Implementing this as written would have meant first wiring run_chain through an abstraction it does not use, in the hot loop, with bit-exactness obligations from tests/determinism.rs and the numerical goldens.
Two measurements from #42 that bear on whether this is still worth doing at all:
The within-game EP loop converges in 1 to 8 iterations against a cap of 30, flat in team count — measured on even and deliberately hostile uneven fields up to 20 teams. Whatever a different schedule buys, it is not iterations in the inner loop.
The Damped half of the original schedule plan already shipped, as ConvergenceOptions::alpha, inside that hand-rolled loop.
None of that rules out the parallelism idea — a barrier merge is about concurrency, a different axis from convergence rate, and #5's numbers are the relevant ones there. But it does mean this issue needs redesigning against the engine as it actually is, not reopened against a trait that is gone. Worth rewriting or closing rather than leaving it pointing at removed types.
Related: #5 (rayon default-on) remains blocked on the same measurement question.
Heads-up: this issue's premise no longer holds after `633a503`.
It proposes a synchronous-EP **`Schedule`** with snapshot and barrier merge. The `Schedule` trait has been removed, along with `EpsilonOrMax`, `ScheduleReport`, `Game::custom`, and the `Factor` / `BuiltinFactor` / `RankDiffFactor` / `TeamSumFactor` machinery it was built on — see #42.
The reason matters here rather than just the fact. `Game::run_chain` drives a local `DiffFactor` enum directly and **has never gone through `Schedule`**. So the trait was not an extension point that this issue would have extended; it was one nothing had ever plugged into, including the engine itself. Implementing this as written would have meant first wiring `run_chain` through an abstraction it does not use, in the hot loop, with bit-exactness obligations from `tests/determinism.rs` and the numerical goldens.
Two measurements from #42 that bear on whether this is still worth doing at all:
- The within-game EP loop converges in **1 to 8 iterations against a cap of 30**, flat in team count — measured on even and deliberately hostile uneven fields up to 20 teams. Whatever a different schedule buys, it is not iterations in the inner loop.
- The `Damped` half of the original schedule plan already shipped, as `ConvergenceOptions::alpha`, inside that hand-rolled loop.
None of that rules out the parallelism idea — a barrier merge is about *concurrency*, a different axis from convergence rate, and #5's numbers are the relevant ones there. But it does mean this issue needs redesigning against the engine as it actually is, not reopened against a trait that is gone. Worth rewriting or closing rather than leaving it pointing at removed types.
Related: #5 (rayon default-on) remains blocked on the same measurement question.
To leave a clean record of what was decided rather than just that it was:
The Schedule trait this issue would have implemented is gone as of 633a503 (#42), and it was never an extension point — Game::run_chain has always driven a local DiffFactor enum directly.
Doing this as written would have meant first wiring run_chain through an abstraction it does not use, in the hot loop, against the bit-exactness obligations in tests/determinism.rs and the numerical goldens.
The within-game EP loop converges in 1 to 8 iterations against a cap of 30, flat in team count, so there is no convergence headroom for an alternative schedule to reclaim.
What would justify reopening this as a new issue: a measured case where cross-slice concurrency pays off. A snapshot-and-barrier merge is about parallelism, which is a different axis from convergence rate and is not refuted by any of the above. #5 holds the relevant numbers — T3 measured 1.0x on realistic workloads against a >=2x target — and any redesign should start from those rather than from this issue's shape.
Not reopening this one, because a rewrite against the current engine shares nothing with the text here except the idea.
Closing, per the reasoning above.
To leave a clean record of what was decided rather than just that it was:
- The `Schedule` trait this issue would have implemented is gone as of `633a503` (#42), and it was never an extension point — `Game::run_chain` has always driven a local `DiffFactor` enum directly.
- Doing this as written would have meant first wiring `run_chain` through an abstraction it does not use, in the hot loop, against the bit-exactness obligations in `tests/determinism.rs` and the numerical goldens.
- The within-game EP loop converges in 1 to 8 iterations against a cap of 30, flat in team count, so there is no convergence headroom for an alternative schedule to reclaim.
**What would justify reopening this as a new issue**: a measured case where *cross-slice* concurrency pays off. A snapshot-and-barrier merge is about parallelism, which is a different axis from convergence rate and is not refuted by any of the above. #5 holds the relevant numbers — T3 measured 1.0x on realistic workloads against a >=2x target — and any redesign should start from those rather than from this issue's shape.
Not reopening this one, because a rewrite against the current engine shares nothing with the text here except the idea.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Deferred from #2 (T3). Spec:
docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md§6, "Within-slice color-group parallelism" (the Alternative paragraph).Why
T3 implemented the asynchronous-EP strategy: color-group partitioning, events in the same color run concurrently, colors run sequentially. This preserves today's semantics exactly but only parallelizes as well as the coloring allows — and on realistic workloads the coloring doesn't give rayon enough to chew on (see #4).
Synchronous EP is the alternative the spec names: all events read from a frozen skill snapshot, write deltas to thread-local buffers, and a barrier merges them. It is trivially parallel — no coloring, no
unsafedisjoint-index writes — at the cost of weaker per-iteration convergence, which needs damping to compensate.Current state
src/schedule.rshas exactly oneScheduleimpl:EpsilonOrMax(src/schedule.rs:28) — forward/backward sweeps until step ≤ eps or iter == max.The damping half of the prerequisite already exists, though built game-local rather than as a
Schedule:ConvergenceOptions::alpha,Gaussian::damp_natural, andpropagate_with_alphaon bothTruncFactorandMarginFactor.Note the
Scheduletrait signature is currently factor-level:Spec §5 describes a generic
Schedule::run<W: Workload>(&self, workload: &mut W)shared across all three convergence loops. A slice-level synchronous schedule needs that generalization, or a separate seam at theTimeSlicelayer — decide which as part of this work.Scope
TimeSlicelevel.alphadamping rather than inventing a second damping path.Acceptance
tests/determinism.rs: bit-identicalmu/sigmaviaf64::to_bits()atRAYON_NUM_THREADS={1,2,4,8}.benches/history_converge.rsworkloads — including iteration count, not just wall-clock, since weaker per-iteration convergence can eat the parallel win.Related
Heads-up: this issue's premise no longer holds after
633a503.It proposes a synchronous-EP
Schedulewith snapshot and barrier merge. TheScheduletrait has been removed, along withEpsilonOrMax,ScheduleReport,Game::custom, and theFactor/BuiltinFactor/RankDiffFactor/TeamSumFactormachinery it was built on — see #42.The reason matters here rather than just the fact.
Game::run_chaindrives a localDiffFactorenum directly and has never gone throughSchedule. So the trait was not an extension point that this issue would have extended; it was one nothing had ever plugged into, including the engine itself. Implementing this as written would have meant first wiringrun_chainthrough an abstraction it does not use, in the hot loop, with bit-exactness obligations fromtests/determinism.rsand the numerical goldens.Two measurements from #42 that bear on whether this is still worth doing at all:
Dampedhalf of the original schedule plan already shipped, asConvergenceOptions::alpha, inside that hand-rolled loop.None of that rules out the parallelism idea — a barrier merge is about concurrency, a different axis from convergence rate, and #5's numbers are the relevant ones there. But it does mean this issue needs redesigning against the engine as it actually is, not reopened against a trait that is gone. Worth rewriting or closing rather than leaving it pointing at removed types.
Related: #5 (rayon default-on) remains blocked on the same measurement question.
Closing, per the reasoning above.
To leave a clean record of what was decided rather than just that it was:
Scheduletrait this issue would have implemented is gone as of633a503(#42), and it was never an extension point —Game::run_chainhas always driven a localDiffFactorenum directly.run_chainthrough an abstraction it does not use, in the hot loop, against the bit-exactness obligations intests/determinism.rsand the numerical goldens.What would justify reopening this as a new issue: a measured case where cross-slice concurrency pays off. A snapshot-and-barrier merge is about parallelism, which is a different axis from convergence rate and is not refuted by any of the above. #5 holds the relevant numbers — T3 measured 1.0x on realistic workloads against a >=2x target — and any redesign should start from those rather than from this issue's shape.
Not reopening this one, because a rewrite against the current engine shares nothing with the text here except the idea.