perf(elastic): ~1.5-1.8x faster DP alignment via integer sub-interval walk - #32
Conversation
… walk The elastic warp search (dp_grid_solve / dp_edge_weight in alignment/mod.rs) dominates all elastic FDA cost — pairwise alignment, distance matrices, and Karcher means. Profiling showed ~99% of the time in the DP edge-weight inner loop. Main win — dp_edge_weight integer walk: The inner loop computed four `i/n` float divisions per sub-interval step to place breakpoint fractions, plus float max/min. Since the coprime neighborhood bounds n1, n2 <= 7, walk the merged breakpoints in integer units of 1/(n1*n2) instead (integer max/min, no per-step division) and hoist the single /(n1*n2) divisor out of the loop. Control flow and tie-handling are exact; only the dt representation changes (integer-exact rational vs float fraction), so all tolerance-based tests — including the R-validation suites — pass unchanged. Measured (release, 20 cores), baseline -> now: align_pair m=50/100/200 : 2.28/10.03/42.1 ms -> 1.46/6.41/26.7 ms (~1.56x) distance_matrix n=30/50 : 139/374 ms -> 76.2/205 ms (~1.82x) karcher_mean n=10/20 : 109/112 ms -> 53.4/69.9 ms (1.6-2.0x) Benefits every DP caller (constrained, partial_match, nd, closed) since they share dp_edge_weight / dp_grid_solve. Supporting cleanups (correctness-preserving, perf-neutral on their own): - dp_grid_solve reuses a thread-local scratch (cost grid + parent pointers) instead of allocating two m^2 buffers per alignment; compatible with the parallel outer loops (one scratch per rayon worker). - karcher_mean caches each curve's SRSF once (invariant across iterations) rather than recomputing srsf_single every iteration in the coarse and fine loops. Tests: 238 alignment + 116 elastic + 245 R-validation pass unchanged; clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-up micro-opts on the DP alignment path: - dp_edge_weight: split out a shared `dp_edge_weight_core` taking a precomputed `rslope` (=√γ') and `span`. On a uniform argvals grid (detected once via `uniform_spacing`) the slope is exactly dr/dc, so `dp_alignment_core` precomputes the 35 possible √(dr/dc) values once and looks them up instead of doing a sqrt + division on every one of the m²·35 edge evaluations. The non-uniform path is unchanged. Measured ~3-4% on pair alignment (smaller than hoped — sqrt is cheap once the per-step divisions were already removed). - elastic_self/cross_distance_matrix: hoist `simpsons_weights(argvals)` out of the per-pair helper (it depends only on argvals) so it is computed once instead of O(n²) times. Numerics: uniform path uses dr/dc slopes and dc·h spans (ULP-level differences vs the argvals-derived values), so results shift only within tolerance — all 238 alignment + 116 elastic + 245 R-validation tests pass; clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Added two follow-up micro-opts (commit 6797876):
Incremental effect (on top of the integer-walk commit):
#4 landed below estimate (~3–4% on the pure DP pair cost, not the hoped 10–20% — |
Adds an optional diagonal band to the elastic warp search. When enabled, the DP grid fill is restricted to cells with |tr - tc| <= r, turning the O(m²) search into O(m·r) — the main algorithmic speedup for near-diagonal warps, at the cost of disallowing warps wider than the band. Opt-in and off by default; existing functions are unchanged (they forward band = None), so results and the public API are fully backward compatible. Core (alignment/mod.rs): - dp_grid_solve_banded(nrows, ncols, band, edge_cost): skips out-of-band cells; dp_grid_solve is now a band = None wrapper. - dp_alignment_core_banded(q1, q2, argvals, lambda, band); dp_alignment_core wraps it with None. - band_radius(band_frac, m): maps a domain-fraction band width to an index radius (None unless 0 < band_frac < 1; radius >= 1). Public opt-in entry points (unbanded variants delegate to shared impls): - elastic_align_pair_banded / elastic_distance_banded - elastic_self_distance_matrix_banded / elastic_cross_distance_matrix_banded - karcher_mean_banded (band applied on both the coarse and fine grids) Speedup at band_frac = 0.1 (same-run ratios; machine was loaded so absolute numbers run high): align_pair m=50/100/200 : 4.9x / 4.7x / 4.6x self_distance_matrix n=10/30/50 : 3.3x / 4.1x / 4.0x Tests (alignment/tests.rs): band_radius semantics; a wide band reproduces the unbanded result bit-for-bit; a narrow band yields a valid monotone warp whose distance is >= the unbounded optimum (constrained-optimum invariant); banded distance matrix matches unbanded for a wide band; karcher_mean_banded produces valid non-decreasing warps. Benches gain banded cases. Full suite green; clippy (all-targets) and rustfmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Added the opt-in Sakoe–Chiba band (#1) — the algorithmic lever (commit 8572b2d):
Speedup at
Tests: wide band reproduces the unbanded result bit-for-bit; narrow band gives a valid monotone warp with distance ≥ the unbounded optimum (constrained-optimum invariant); This grew past the original inner-loop scope into the full perf set — happy to split the band into its own PR if you'd prefer a tighter diff. |
Summary
The elastic warp search (
dp_grid_solve/dp_edge_weightinalignment/mod.rs) dominates all elastic FDA cost — pairwise alignment, distance matrices, and Karcher means. Profiling put ~99% of the time in the DP edge-weight inner loop. This PR makes that loop ~1.5–1.8× faster with no API change and no accuracy regression.Main win —
dp_edge_weightinteger walkThe inner loop computed four
i/nfloat divisions per sub-interval step to place breakpoint fractions, plus floatmax/min. Since the coprime neighborhood boundsn1, n2 ≤ 7, we instead walk the merged breakpoints in integer units of1/(n1·n2)(integermax/min, no per-step division) and hoist the single/(n1·n2)divisor out of the loop.Control flow and tie-handling are exact; only the
dtrepresentation changes (integer-exact rational vs float fraction), so all tolerance-based tests — including the R-validation suites — pass unchanged.Measured (release, 20 cores)
align_pairm=50align_pairm=100align_pairm=200distance_matrixn=30distance_matrixn=50karcher_meann=10karcher_meann=20Benefits every DP caller (
constrained,partial_match,nd,closed) since they sharedp_edge_weight/dp_grid_solve.Supporting cleanups (correctness-preserving, perf-neutral on their own)
dp_grid_solvereuses a thread-local scratch (cost grid + parent pointers) instead of allocating twom²buffers per alignment; compatible with the parallel outer loops (one scratch per rayon worker).karcher_meancaches each curve's SRSF once (invariant across iterations) rather than recomputingsrsf_singleevery iteration in the coarse and fine loops.Tests
238 alignment + 116 elastic + 245 R-validation tests pass unchanged; clippy clean.
🤖 Generated with Claude Code