[Feature] Efficient DTW in C++ - #3141
Conversation
- Update build config. - Add `dtw_cost_matrix_no_window_1d` (limited use cases for now). Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Otherwise, GIL is enabled globally. Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Co-authored-by: Zhihao Dai <zhihao.dai@eng.ox.ac.uk>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3141 +/- ##
==========================================
- Coverage 96.73% 95.96% -0.77%
==========================================
Files 163 164 +1
Lines 17536 17678 +142
==========================================
+ Hits 16963 16965 +2
- Misses 573 713 +140 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Thanks a lot @daidahao for starting the initiative towards optimizing some of our heavy-duty python code. Since we don't have any code yet in another language, I would like to first think about what the best / most future-proof language would be for us. Any future refactor should ideally then use the selected language to improve maintainability. I feel that the momentum has been shifting towards Rust since a while now, and I see many other libraries / software using it at the moment (Polars, uv, ty, ...). It seems to have proven itself to be robust at scale. Have you thought about Rust or at least why C++ would be the right choice for us compared to other languages? |
I am neutral on Rust because I have not used Rust before. I chose C++ and nanobind mainly for two reasons:
That said, it might still be worth comparing others to C++/nanobind. My LLM suggests that Rust might be preferred if memory safety is a key concern (that might not be the case here due to Darts nature). In terms of future-proofness, I am quite confident about nanobind given its vast usage in ML. @dennisbader |
|
Following offline discussion, I will postpone this PR in order to give us time to think about clustering API design first. We could circle back should we need distance-based clustering and efficient distance metrics in Darts in the future. |
|
Thanks @daidahao, let's come back to this once we progressed more on the clustering design. |
Checklist before merging this PR:
Fixes #3140 .
Summary
This is a draft PR to showcase the potential speedup of DTW in C++ and invite discussion.
This PR proposes migrating the DTW distance to C++ in nanobind to significantly speed up the distance calculation.
This would also be a first step towards classification and clustering support in Darts and would allow us to implement distance measures other than DTW, classification and clustering algorithms in C++ in the future.
Distance metrics are central to time series classification and clustering, and DTW is a widely used distance measure for time series.
Classification and clustering algorithms often require computing pairwise distances between time series, which can be computationally expensive.
However, the current DTW implementation in Darts is in pure Python, which can be slow for large datasets or long time series.
The new implementation (
dtw.cpp) replaces_dtw_cost_matrix()and is written in C++ and exposed to Python using nanobind.Current Limitations:
DTWAlignmentwould not be supported anymore. Only distance would be returned, not the optimal warping path.Benchmark Results
Note that both implementations are running on a single thread, which is intentional because in clustering and classification, we would be computing pairwise distances in parallel across multiple threads or processes.
Design Decisions
Implications
TODO
Should we agree on this approach, the next steps in this PR would be:
Backward compatible: Migrate the fast DTW (I would now suggest deprecating fastDTW algorithm, because DTW with proper windowing is faster and exact, see this paper.multi_grid_radius>=0) algorithm to C++ as well.distanceargument (inDistanceFunc) inDTWAlignmentand only support pre-defined distance options (instr) such as euclidean, manhattan, etc. that can be directly implemented in C++. Otherwise, calling aDistanceFuncfrom C++ would add overhead and reduce the speedup.CostMatrixclass and subclasses in the Python scope but exposes.densenumy array to support the C++ implementation.Windowclass and subclasses in the Python scope but expose the necessary information (column_ranges) to support windowing in the C++ implementation.darts.distancesmodule with DTW distance and other distance measures (e.g., euclidean, manhattan) functions, likedarts.metricsfor forecasting.darts.distances.dtwfunction, we can implement a more efficient version using two-row rolling cost matrix.Wheel Distribution
To distribute the C++ extension, we would need to build binary wheels for combinations of platforms, architectures, and Python minors:
We can use
cibuildwheelto automate the building and testing of those wheels. However,cibuildwheelby default builds for all combinations, resulting in a large number of wheels (see scikit-learn for example). We will not have the means and resources to maintain such a large number of wheels.I propose two strategies to reduce the number of wheels:
nanobindsupports building with stable ABI for Python 3.12+. There are, however, two caveats: (1) we still need to build separate wheels for 3.10 and 3.11; (2) there is currently no stable ABI for free-threading 3.14t, though it is expected for 3.15 (PEP 803). So we need to build for 3.10, 3.11, 3.12+ (stable ABI), and 3.14t (reduced by 33%).Including the most common platforms and architectures, here is a breakdown of 28 wheels needed with the above strategies:
I created an example repository here to test building and distributing nanobind extensions with
cibuildwheel.We could reduce the number further by excluding
musllinux_1_2for Linux andarm64for Windows. So we would have 16 wheels in total:Other Information