Skip to content

perf(elastic): ~1.5-1.8x faster DP alignment via integer sub-interval walk - #32

Merged
sipemu merged 3 commits into
mainfrom
perf/elastic-dp-inner-loop
Aug 6, 2026
Merged

perf(elastic): ~1.5-1.8x faster DP alignment via integer sub-interval walk#32
sipemu merged 3 commits into
mainfrom
perf/elastic-dp-inner-loop

Conversation

@sipemu

@sipemu sipemu commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

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 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_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, we instead walk the merged breakpoints in integer units of 1/(n1·n2) (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)

Benchmark Baseline Now Speedup
align_pair m=50 2.28 ms 1.46 ms 1.56×
align_pair m=100 10.03 ms 6.41 ms 1.56×
align_pair m=200 42.1 ms 26.7 ms 1.58×
distance_matrix n=30 139 ms 76.2 ms 1.82×
distance_matrix n=50 374 ms 205 ms 1.82×
karcher_mean n=10 109 ms 53.4 ms 2.04×
karcher_mean n=20 112 ms 69.9 ms 1.60×

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 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 tests pass unchanged; clippy clean.

🤖 Generated with Claude Code

sipemu and others added 2 commits August 7, 2026 00:15
… 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>
@sipemu

sipemu commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Added two follow-up micro-opts (commit 6797876):

Incremental effect (on top of the integer-walk commit):

Benchmark integer-walk +#4+#5
align_pair m=100 6.41 ms 6.14 ms (−4%)
align_pair m=200 26.7 ms 25.8 ms (−3%)
distance_matrix n=10 20.0 ms 17.7 ms (−12%)
karcher n=20 69.9 ms 61.5 ms (−12%)
karcher n=10 53.4 ms 49.4 ms (−8%)

#4 landed below estimate (~3–4% on the pure DP pair cost, not the hoped 10–20% — sqrt is cheap once the per-step divisions were already gone). Cumulative vs the original baseline is now ~1.6× on pair alignment and up to ~2.2× on karcher n=10. All tests pass; clippy clean.

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>
@sipemu

sipemu commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Added the opt-in Sakoe–Chiba band (#1) — the algorithmic lever (commit 8572b2d):

  • Core: dp_grid_solve_banded restricts the fill to |tr − tc| ≤ r; band_radius(band_frac, m) maps a domain-fraction to an index radius. Off by default — existing functions forward band = None and are byte-for-byte unchanged.
  • New opt-in API: elastic_align_pair_banded, elastic_distance_banded, elastic_self/cross_distance_matrix_banded, karcher_mean_banded (band applied on both coarse and fine grids).

Speedup at band_frac = 0.1 (same-run ratios):

Benchmark unbanded band 10% Speedup
align_pair m=200 51.7 ms 11.25 ms 4.6×
self_distance_matrix n=50 757 ms 187 ms 4.0×

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); karcher_mean_banded warps stay non-decreasing. Full suite green; clippy (all-targets) + rustfmt clean.

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.

@sipemu
sipemu merged commit 6a2bbf5 into main Aug 6, 2026
@sipemu
sipemu deleted the perf/elastic-dp-inner-loop branch August 6, 2026 22:56
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