Skip to content

Reusable + parallel CCH customization (bit-identical) - #2

Merged
VladPr merged 8 commits into
mainfrom
feat/parallel-reusable-customize
Jul 8, 2026
Merged

Reusable + parallel CCH customization (bit-identical)#2
VladPr merged 8 commits into
mainfrom
feat/parallel-reusable-customize

Conversation

@VladPr

@VladPr VladPr commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes per-metric CCH customization reusable (no per-call allocation) and parallel (rayon, level-synchronized), while keeping output bit-identical to the previous serial customize and to the C++ RoutingKit oracle.

Customization is the operation a routing service runs most often (build once, re-customize whenever weights change). This branch addresses its two costs: fresh allocation of the forward/backward/scratch arrays every call, and single-threaded relaxation that leaves all other cores idle.

What changed

  • Customizer<'a> (cch.customizer()) — owns the metric-independent elimination-tree level partition (derived once) and reuses the caller's Metric buffers via customize_into(&self, weights, &mut Metric).
  • Cch::customize(weights) -> Metric is unchanged (same signature, same panics, byte-for-byte identical output) — now a thin wrapper over Customizer. No downstream break.
  • Parallel phase-1 reset via a safe rayon iterator (arcs are independent).
  • Parallel phase-2 relaxation — levels run sequentially with a barrier; nodes within a level run in parallel. Writes are partitioned by owning node (disjoint); reads touch only finalized lower levels. Raw-pointer writes go through a Send + Sync DisjointArcs wrapper, guarded by one-time bounds validation in customizer(). Per-thread relaxation scratch is a reused thread_local! (allocated once, reused across levels and calls).
  • rayon is now an always-on dependency. It is pure Rust, so the crate's "no C++ / no FFI" property is preserved; only the "minimal deps" framing in the README changed.

Correctness

  • Bit-identical is a hard gate. The relaxation is integer min (associative + commutative) over a fixed dependency DAG, so any level-respecting schedule yields the identical result to the serial loop.
  • Gated by: the C++ oracle differential suite (tests/equivalence.rs), an independent #[cfg(test)] serial reference (parallel_relax_equals_serial_reference), a determinism test, and reuse/no-bleed tests.
  • The parallel unsafe's data-race-freedom is contracted to a well-formed (chordal) CCH as produced by build or a faithful load_struct round-trip; the bounds validation additionally prevents OOB for any bounds-valid structure. miri (Tree Borrows) passes on the customize tests; the Stacked-Borrows warning was traced to a known rayon/crossbeam-epoch artifact in the safe phase-1 path, not the new unsafe.
  • 100% line coverage held (CI gate cargo llvm-cov --fail-under-lines 100); clippy --all-targets -D warnings and rustfmt clean.

Notes

  • Reuse bench (benches/cch.rs, 24×24 grid): reused Customizer is never slower than fresh customize; the reuse win grows with structure size (the parallel relaxation dominates the small fixture). Run cargo bench to reproduce.
  • README + rustdoc updated: rayon disclosed, parallel customization moved from planned to shipped, Customizer reuse pattern documented.

VladPr added 8 commits July 8, 2026 23:21
Cch::customize now delegates to a new Customizer (via Cch::customizer,
which computes the elimination-tree Levels partition once). Customizer
adds customize_into for callers who want to reuse a Metric's forward/
backward allocations across repeated customizations of the same
structure. Cch::customize keeps its exact signature, panic behavior,
and byte-for-byte output.
Replace the serial phase-2 relaxation with a level-synchronized parallel
version: levels run sequentially, nodes within a level run in parallel via
rayon with disjoint writes through a raw-pointer DisjointArcs wrapper.
Cch::customizer now validates structural invariants once so the raw-pointer
accesses are sound for any Cch, including one loaded from arbitrary bytes.

Retains an independent serial relaxation as a #[cfg(test)] reference and
adds property tests checking the parallel path against it and against
itself for determinism, plus should_panic tests covering each validation
branch.
…ormed Cch; fix stale comment

The parallel write-disjointness in relax_node/DisjointArcs relies on the
CCH being well-formed (chordal); customizer()'s validation only checks
bounds. Narrow the safety/doc language to say so explicitly instead of
over-claiming soundness "however self was built" or that the asserts
guard against "hand-corrupted or foreign-loaded" input — a bounds-valid
but non-chordal Cch is out of the safety contract. Also fix a stale
SAFETY comment that referenced get_unchecked; the code only uses raw
pointer .add().

No logic or runtime behavior changes.
Add a customize_reuse bench comparing fresh Cch::customize per call
against a reused Customizer::customize_into on the 24x24 grid fixture
(cargo bench --bench cch -- customize). Observed: fresh_each_call
~1.238 ms median vs reused_customizer ~1.200 ms median, ~3% faster
with no allocation and no level-partition recompute; the gap is
capped on this small grid by parallel overhead and grows with
structure size and call frequency.

Update README (deps framing now that rayon is a dependency, a new
Highlights bullet, quick-start reuse comment, performance note, and
moved parallel customization from Planned to shipped) and the crate
rustdoc in src/lib.rs to document Cch::customizer /
Customizer::customize_into for repeated, allocation-free, parallel
customization.

Also fix a pre-existing broken intra-doc link in query.rs
([`Cch::build`] unresolved outside the crate root) that made `cargo
doc --no-deps` emit a warning.
… panics

The phase-2 level loop allocated a fresh node_count-sized Vec per rayon
worker on every level via for_each_init, up to num_levels x num_threads
allocations per customize_into call. Replace it with a thread_local
scratch cache allocated once per worker and reused across levels and
across customize_into calls, grown on demand.

Also document that Cch::customize can panic on a structurally malformed
Cch (delegated to customizer()) and note that repeated callers should
hold a Customizer to avoid re-validating and re-partitioning each call.
Bump 0.1.1 -> 0.2.0 (minor: new Customizer API + rayon dep).
Add 'customize in parallel' to the crates.io description, update the
README install pin to 0.2, and add a Keep-a-Changelog CHANGELOG backfilling
0.1.0/0.1.1.
@VladPr
VladPr merged commit 84bcc36 into main Jul 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant