Skip to content

perf: sweep-line Timeline.co_iter and Timeline.segmentation (~26x faster DER on long files) - #116

Open
TheNetherWatcher wants to merge 2 commits into
pyannote:mainfrom
TheNetherWatcher:perf/co-iter-sweep
Open

TheNetherWatcher wants to merge 2 commits into
pyannote:mainfrom
TheNetherWatcher:perf/co-iter-sweep

Conversation

@TheNetherWatcher

@TheNetherWatcher TheNetherWatcher commented Oct 31, 2026 •

Copy link
Copy Markdown

Summary

Timeline.co_iter re-scans every segment of other that starts before the current segment ends, so it is O(n·m). It sits under crop, get_overlap, Annotation.__mul__ and therefore under every pyannote.metrics computation. Profiling DiarizationErrorRate on a 4k-segment file: 92% of the time is spent in co_iter, with ~70M Segment.intersects calls. Timeline.segmentation had the same pattern via support.overlapping(middle).

This PR replaces both with single-pass sweeps:

  • co_iter: keep a sorted list of candidate segments of other; stop scanning at the first one sorted after Segment(end, end) (same bound as before); drop candidates that start before the current segment and do not intersect it, since they cannot intersect any later segment either.
  • segmentation: track the running maximum end time of support segments sorted before Segment(middle, middle) and starting at or before middle, since middles only increase.

Output is unchanged: same pairs, same order.

Benchmark

Synthetic diarization (4 speakers, ~15% overlapped speech), pyannote.metrics 4.2.0, Python 3.11, Apple M-series:

segments DER before DER after IER before IER after
1,000 0.30s 0.06s 0.26s 0.04s
4,000 3.94s 0.28s 3.35s 0.18s
8,000 14.8s 0.58s 13.1s 0.39s

DER with collar=0.5, skip_overlap=True at 8k segments: 27.0s → 0.74s. All metric values are identical to the last digit. 32k segments now takes ~2.5s for DER.

Correctness

  • New tests compare both methods against naive reference implementations on random timelines, including ties, nested, touching and sub-SEGMENT_PRECISION segments (these are needed to exercise the precision-dependent branches of Segment.intersects).
  • Differential fuzzing against the previous implementation: 78k random cases, including Segment.set_precision(...), zero-length and invalid segments, all identical.
  • Lean 4 proof that both sweeps return exactly what the original loops return, for any total order on times and any rounding of end - SEGMENT_PRECISION: https://github.com/TheNetherWatcher/lean-perf-proofs/tree/main/pyannote-core-sweep

Related: #14 (also about the cost of support/crop on large annotations).

Timeline.co_iter re-scanned every segment of 'other' starting before the
current segment ends, which is O(n*m). It underlies crop, get_overlap,
Annotation.__mul__ and therefore every pyannote.metrics computation.
Timeline.segmentation had the same pattern via support.overlapping().

Replace both with single-pass sweeps. Output (pairs and their order) is
unchanged.

This branch has not been deployed

No deployments
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