torchmatch 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- torchmatch-1.0.0/LICENSE +21 -0
- torchmatch-1.0.0/PKG-INFO +186 -0
- torchmatch-1.0.0/README.md +156 -0
- torchmatch-1.0.0/pyproject.toml +174 -0
- torchmatch-1.0.0/setup.cfg +4 -0
- torchmatch-1.0.0/setup.py +97 -0
- torchmatch-1.0.0/sources/torchmatch/__init__.py +119 -0
- torchmatch-1.0.0/sources/torchmatch/_cpu.py +74 -0
- torchmatch-1.0.0/sources/torchmatch/_cuda.py +73 -0
- torchmatch-1.0.0/sources/torchmatch/_loader.py +102 -0
- torchmatch-1.0.0/sources/torchmatch/cpu/jv_compact_avx2.h +71 -0
- torchmatch-1.0.0/sources/torchmatch/cpu/jv_compact_core.h +303 -0
- torchmatch-1.0.0/sources/torchmatch/cpu/jv_dense_avx2.h +35 -0
- torchmatch-1.0.0/sources/torchmatch/cpu/jv_dense_core.cpp +62 -0
- torchmatch-1.0.0/sources/torchmatch/cpu/jv_dense_core.h +19 -0
- torchmatch-1.0.0/sources/torchmatch/cpu/jv_dense_flat.h +276 -0
- torchmatch-1.0.0/sources/torchmatch/cpu/jv_scalar.cpp +198 -0
- torchmatch-1.0.0/sources/torchmatch/cpu/jv_scalar.h +11 -0
- torchmatch-1.0.0/sources/torchmatch/cpu/ops.cpp +496 -0
- torchmatch-1.0.0/sources/torchmatch/cuda/control.cuh +64 -0
- torchmatch-1.0.0/sources/torchmatch/cuda/dispatch.cu +114 -0
- torchmatch-1.0.0/sources/torchmatch/cuda/jv_tiled.cuh +203 -0
- torchmatch-1.0.0/sources/torchmatch/cuda/munkres_classical.cu +703 -0
- torchmatch-1.0.0/sources/torchmatch/cuda/munkres_hybrid.cu +1297 -0
- torchmatch-1.0.0/sources/torchmatch/cuda/munkres_tree.cu +1161 -0
- torchmatch-1.0.0/sources/torchmatch/cuda/ops.cpp +35 -0
- torchmatch-1.0.0/sources/torchmatch/cuda/workspace.cuh +108 -0
- torchmatch-1.0.0/sources/torchmatch/py.typed +0 -0
- torchmatch-1.0.0/sources/torchmatch.egg-info/PKG-INFO +186 -0
- torchmatch-1.0.0/sources/torchmatch.egg-info/SOURCES.txt +36 -0
- torchmatch-1.0.0/sources/torchmatch.egg-info/dependency_links.txt +1 -0
- torchmatch-1.0.0/sources/torchmatch.egg-info/requires.txt +13 -0
- torchmatch-1.0.0/sources/torchmatch.egg-info/top_level.txt +1 -0
- torchmatch-1.0.0/tests/test_lap_jv_batch.py +26 -0
- torchmatch-1.0.0/tests/test_lap_jv_float32.py +24 -0
- torchmatch-1.0.0/tests/test_lap_jv_scalar.py +76 -0
- torchmatch-1.0.0/tests/test_lap_jv_single.py +24 -0
- torchmatch-1.0.0/tests/test_lap_opcheck.py +92 -0
torchmatch-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Kurt Stolle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: torchmatch
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Linear assignment problem solvers for PyTorch.
|
|
5
|
+
Author-email: Kurt Stolle <kurt@computer.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: pytorch,linear assignment problem,lap,lapjv,hungarian algorithm,bipartite matching,cuda
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Requires-Python: <3.14,>=3.13
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: torch>=2.11
|
|
21
|
+
Provides-Extra: cpu
|
|
22
|
+
Requires-Dist: torch>=2.11; extra == "cpu"
|
|
23
|
+
Provides-Extra: cu126
|
|
24
|
+
Requires-Dist: torch>=2.11; extra == "cu126"
|
|
25
|
+
Provides-Extra: cu128
|
|
26
|
+
Requires-Dist: torch>=2.11; extra == "cu128"
|
|
27
|
+
Provides-Extra: cu130
|
|
28
|
+
Requires-Dist: torch>=2.12; extra == "cu130"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# torchmatch
|
|
32
|
+
|
|
33
|
+
Linear assignment problem (LAP) solvers for PyTorch, registered under
|
|
34
|
+
`torch.ops.matching`. Inputs are `torch.Tensor`; outputs are integer
|
|
35
|
+
row-to-column assignments.
|
|
36
|
+
|
|
37
|
+
📖 **Documentation**: <https://khwstolle.github.io/torchmatch/>. Tutorials, API reference, and the full benchmark report.
|
|
38
|
+
|
|
39
|
+
Two algorithmic families share the op surface:
|
|
40
|
+
|
|
41
|
+
- **Munkres** (Kuhn-Munkres 1957): the primed/starred-zeros 6-step
|
|
42
|
+
augmenting-path procedure. CUDA only; three parallelization
|
|
43
|
+
strategies.
|
|
44
|
+
- **Jonker-Volgenant / successive-shortest-path (SSP)**: the
|
|
45
|
+
Dijkstra-like family.
|
|
46
|
+
|
|
47
|
+
Both families return optimal assignments. They differ in the
|
|
48
|
+
augmentation routine, the parallelization story, and constant
|
|
49
|
+
factors.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install torchmatch
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The Munkres CUDA ops and the CUDA backend of `jv_dense_batch` need a
|
|
58
|
+
CUDA-capable PyTorch build. CPU ops run on any platform; the
|
|
59
|
+
SIMD-optimized variants need AVX2/FMA at build time (sdist falls back
|
|
60
|
+
to the AVX2/FMA-aware JIT path, and `jv_scalar` works without SIMD).
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import torch
|
|
66
|
+
import torchmatch # extensions load eagerly at import
|
|
67
|
+
|
|
68
|
+
cost = torch.randn(8, 8, device="cuda")
|
|
69
|
+
row_to_col = torchmatch.munkres_hybrid(cost)
|
|
70
|
+
|
|
71
|
+
cost_cpu = torch.randn(8, 8)
|
|
72
|
+
row_to_col_cpu = torchmatch.jv_scalar(cost_cpu)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The same ops live at `torch.ops.matching.<op>`, which is useful inside
|
|
76
|
+
`torch.compile` regions or when dispatching by name.
|
|
77
|
+
|
|
78
|
+
For control over extension loading (preforked workers, latency-sensitive
|
|
79
|
+
serving), the loaders stay exported and idempotent:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
torchmatch.load_cpu() # already ran at import; no-op
|
|
83
|
+
torchmatch.load_cuda()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Every op returns an `int64` row→col mapping of length `N` (the number
|
|
87
|
+
of rows). A `-1` entry marks an unmatched row.
|
|
88
|
+
|
|
89
|
+
## Ops
|
|
90
|
+
|
|
91
|
+
### Munkres family (CUDA only)
|
|
92
|
+
|
|
93
|
+
Primed/starred-zeros Hungarian/Munkres algorithm; three GPU
|
|
94
|
+
parallelization strategies. All carry the `cudagraph_unsafe` tag
|
|
95
|
+
(host-side syncs read managed-memory flags).
|
|
96
|
+
|
|
97
|
+
| Op | dtype (internal) | Variant |
|
|
98
|
+
| ------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| `munkres_classical` | float32, column-major | Single-path augmentation per outer iteration; CUB segmented reductions for column-min |
|
|
100
|
+
| `munkres_hybrid` | float32, row-major | Adaptive: starts single-path, switches to tree augmentation when single-path stalls |
|
|
101
|
+
| `munkres_tree` | float64, row-major | Always-tree: parallel BFS finds all vertex-disjoint augmenting paths per outer iteration; cooperative-groups + Thrust scan |
|
|
102
|
+
|
|
103
|
+
### Jonker-Volgenant family, CPU (single problem)
|
|
104
|
+
|
|
105
|
+
Successive-shortest-path (Dijkstra-like over reduced costs). All accept
|
|
106
|
+
rectangular `(N, M)` cost matrices in float32 or float64.
|
|
107
|
+
|
|
108
|
+
| Op | Variant |
|
|
109
|
+
| ------------ | --------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
+
| `jv_scalar` | Sequential reference; no SIMD. Implements Crouse 2016. Rejects NaN / -inf; treats +inf as an infeasible edge. |
|
|
111
|
+
| `jv_dense` | AVX2 flat-pointer inner loop; rectangular-capable |
|
|
112
|
+
| `jv_compact` | AVX2-gather inner loop; square-only internal kernel (rectangular inputs padded by the wrapper) |
|
|
113
|
+
|
|
114
|
+
### Jonker-Volgenant family, batched
|
|
115
|
+
|
|
116
|
+
| Op | Devices | Constraints |
|
|
117
|
+
| --------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
118
|
+
| `jv_dense_batch` | CPU + CUDA | CPU: `(B, N, M)`, any size, `at::parallel_for` over per-problem `jv_dense`. CUDA: shared-memory tiled kernel, one block per problem; requires `(B, K, K)` square with `K ≤ MAX_TILE = 64`. |
|
|
119
|
+
| `jv_compact_batch` | CPU | `at::parallel_for` over per-problem `jv_compact` |
|
|
120
|
+
| `jv_dense_batch_unpacked` | CPU | Returns `(matches, unmatched_rows, unmatched_cols, n_matched)`, which saves a per-problem Python unpack |
|
|
121
|
+
| `jv_compact_batch_unpacked` | CPU | Same shape, compact variant |
|
|
122
|
+
|
|
123
|
+
## torch.compile / torch.export
|
|
124
|
+
|
|
125
|
+
Every op carries a FakeTensor kernel. The Munkres CUDA ops carry the
|
|
126
|
+
`cudagraph_unsafe` tag (host-side syncs); the CUDA backend of
|
|
127
|
+
`jv_dense_batch` is fully capturable.
|
|
128
|
+
|
|
129
|
+
## Build modes
|
|
130
|
+
|
|
131
|
+
The per-device loaders prefer a prebuilt `.so` shipped in the wheel and
|
|
132
|
+
fall back to JIT-compiling the C++/CUDA sources via
|
|
133
|
+
`torch.utils.cpp_extension.load`. Both paths register the same
|
|
134
|
+
`torch.ops.matching.*` ops; the choice is transparent to callers.
|
|
135
|
+
|
|
136
|
+
### Building wheels
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# default: builds CPU extension; builds CUDA extension if a CUDA
|
|
140
|
+
# toolchain is detected (torch.utils.cpp_extension.CUDA_HOME)
|
|
141
|
+
pip wheel . -w dist/
|
|
142
|
+
|
|
143
|
+
# CPU-only wheel
|
|
144
|
+
TORCHMATCH_SKIP_CUDA=1 pip wheel . -w dist/
|
|
145
|
+
|
|
146
|
+
# CUDA SM targets (default: PyTorch's current-device default)
|
|
147
|
+
TORCH_CUDA_ARCH_LIST="8.0;8.6;8.9;9.0" pip wheel . -w dist/
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The build system pairs `setuptools` with `torch.utils.cpp_extension.BuildExtension`.
|
|
151
|
+
Sdists ship the full `` tree so the JIT path works on any
|
|
152
|
+
CUDA-capable machine, even without a matching wheel.
|
|
153
|
+
|
|
154
|
+
### Runtime overrides
|
|
155
|
+
|
|
156
|
+
- `TORCHMATCH_FORCE_JIT=1`: skip the prebuilt `.so` and recompile from
|
|
157
|
+
source via `cpp_extension.load`. Useful for development and for
|
|
158
|
+
diagnosing ABI mismatches.
|
|
159
|
+
|
|
160
|
+
## Development environment
|
|
161
|
+
|
|
162
|
+
A self-contained `flake.nix` provides Python 3.13, uv, and a chosen
|
|
163
|
+
CUDA toolkit (default 12.8) without touching your global system. With
|
|
164
|
+
direnv:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
direnv allow # picks devShells.default = cu128
|
|
168
|
+
NIX_DEVSHELL_NAME=cpu direnv reload
|
|
169
|
+
NIX_DEVSHELL_NAME=cu130 direnv reload
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Without direnv:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
nix develop # default = cu128
|
|
176
|
+
nix develop .#cpu # CPU-only (sets TORCHMATCH_SKIP_CUDA=1)
|
|
177
|
+
nix develop .#cu126
|
|
178
|
+
nix develop .#cu130
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Once inside the shell:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
uv sync --all-groups # populate .venv from pyproject.toml
|
|
185
|
+
uv run python -m pytest tests/ # run the test suite
|
|
186
|
+
```
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# torchmatch
|
|
2
|
+
|
|
3
|
+
Linear assignment problem (LAP) solvers for PyTorch, registered under
|
|
4
|
+
`torch.ops.matching`. Inputs are `torch.Tensor`; outputs are integer
|
|
5
|
+
row-to-column assignments.
|
|
6
|
+
|
|
7
|
+
📖 **Documentation**: <https://khwstolle.github.io/torchmatch/>. Tutorials, API reference, and the full benchmark report.
|
|
8
|
+
|
|
9
|
+
Two algorithmic families share the op surface:
|
|
10
|
+
|
|
11
|
+
- **Munkres** (Kuhn-Munkres 1957): the primed/starred-zeros 6-step
|
|
12
|
+
augmenting-path procedure. CUDA only; three parallelization
|
|
13
|
+
strategies.
|
|
14
|
+
- **Jonker-Volgenant / successive-shortest-path (SSP)**: the
|
|
15
|
+
Dijkstra-like family.
|
|
16
|
+
|
|
17
|
+
Both families return optimal assignments. They differ in the
|
|
18
|
+
augmentation routine, the parallelization story, and constant
|
|
19
|
+
factors.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install torchmatch
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The Munkres CUDA ops and the CUDA backend of `jv_dense_batch` need a
|
|
28
|
+
CUDA-capable PyTorch build. CPU ops run on any platform; the
|
|
29
|
+
SIMD-optimized variants need AVX2/FMA at build time (sdist falls back
|
|
30
|
+
to the AVX2/FMA-aware JIT path, and `jv_scalar` works without SIMD).
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import torch
|
|
36
|
+
import torchmatch # extensions load eagerly at import
|
|
37
|
+
|
|
38
|
+
cost = torch.randn(8, 8, device="cuda")
|
|
39
|
+
row_to_col = torchmatch.munkres_hybrid(cost)
|
|
40
|
+
|
|
41
|
+
cost_cpu = torch.randn(8, 8)
|
|
42
|
+
row_to_col_cpu = torchmatch.jv_scalar(cost_cpu)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The same ops live at `torch.ops.matching.<op>`, which is useful inside
|
|
46
|
+
`torch.compile` regions or when dispatching by name.
|
|
47
|
+
|
|
48
|
+
For control over extension loading (preforked workers, latency-sensitive
|
|
49
|
+
serving), the loaders stay exported and idempotent:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
torchmatch.load_cpu() # already ran at import; no-op
|
|
53
|
+
torchmatch.load_cuda()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Every op returns an `int64` row→col mapping of length `N` (the number
|
|
57
|
+
of rows). A `-1` entry marks an unmatched row.
|
|
58
|
+
|
|
59
|
+
## Ops
|
|
60
|
+
|
|
61
|
+
### Munkres family (CUDA only)
|
|
62
|
+
|
|
63
|
+
Primed/starred-zeros Hungarian/Munkres algorithm; three GPU
|
|
64
|
+
parallelization strategies. All carry the `cudagraph_unsafe` tag
|
|
65
|
+
(host-side syncs read managed-memory flags).
|
|
66
|
+
|
|
67
|
+
| Op | dtype (internal) | Variant |
|
|
68
|
+
| ------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `munkres_classical` | float32, column-major | Single-path augmentation per outer iteration; CUB segmented reductions for column-min |
|
|
70
|
+
| `munkres_hybrid` | float32, row-major | Adaptive: starts single-path, switches to tree augmentation when single-path stalls |
|
|
71
|
+
| `munkres_tree` | float64, row-major | Always-tree: parallel BFS finds all vertex-disjoint augmenting paths per outer iteration; cooperative-groups + Thrust scan |
|
|
72
|
+
|
|
73
|
+
### Jonker-Volgenant family, CPU (single problem)
|
|
74
|
+
|
|
75
|
+
Successive-shortest-path (Dijkstra-like over reduced costs). All accept
|
|
76
|
+
rectangular `(N, M)` cost matrices in float32 or float64.
|
|
77
|
+
|
|
78
|
+
| Op | Variant |
|
|
79
|
+
| ------------ | --------------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `jv_scalar` | Sequential reference; no SIMD. Implements Crouse 2016. Rejects NaN / -inf; treats +inf as an infeasible edge. |
|
|
81
|
+
| `jv_dense` | AVX2 flat-pointer inner loop; rectangular-capable |
|
|
82
|
+
| `jv_compact` | AVX2-gather inner loop; square-only internal kernel (rectangular inputs padded by the wrapper) |
|
|
83
|
+
|
|
84
|
+
### Jonker-Volgenant family, batched
|
|
85
|
+
|
|
86
|
+
| Op | Devices | Constraints |
|
|
87
|
+
| --------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
88
|
+
| `jv_dense_batch` | CPU + CUDA | CPU: `(B, N, M)`, any size, `at::parallel_for` over per-problem `jv_dense`. CUDA: shared-memory tiled kernel, one block per problem; requires `(B, K, K)` square with `K ≤ MAX_TILE = 64`. |
|
|
89
|
+
| `jv_compact_batch` | CPU | `at::parallel_for` over per-problem `jv_compact` |
|
|
90
|
+
| `jv_dense_batch_unpacked` | CPU | Returns `(matches, unmatched_rows, unmatched_cols, n_matched)`, which saves a per-problem Python unpack |
|
|
91
|
+
| `jv_compact_batch_unpacked` | CPU | Same shape, compact variant |
|
|
92
|
+
|
|
93
|
+
## torch.compile / torch.export
|
|
94
|
+
|
|
95
|
+
Every op carries a FakeTensor kernel. The Munkres CUDA ops carry the
|
|
96
|
+
`cudagraph_unsafe` tag (host-side syncs); the CUDA backend of
|
|
97
|
+
`jv_dense_batch` is fully capturable.
|
|
98
|
+
|
|
99
|
+
## Build modes
|
|
100
|
+
|
|
101
|
+
The per-device loaders prefer a prebuilt `.so` shipped in the wheel and
|
|
102
|
+
fall back to JIT-compiling the C++/CUDA sources via
|
|
103
|
+
`torch.utils.cpp_extension.load`. Both paths register the same
|
|
104
|
+
`torch.ops.matching.*` ops; the choice is transparent to callers.
|
|
105
|
+
|
|
106
|
+
### Building wheels
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# default: builds CPU extension; builds CUDA extension if a CUDA
|
|
110
|
+
# toolchain is detected (torch.utils.cpp_extension.CUDA_HOME)
|
|
111
|
+
pip wheel . -w dist/
|
|
112
|
+
|
|
113
|
+
# CPU-only wheel
|
|
114
|
+
TORCHMATCH_SKIP_CUDA=1 pip wheel . -w dist/
|
|
115
|
+
|
|
116
|
+
# CUDA SM targets (default: PyTorch's current-device default)
|
|
117
|
+
TORCH_CUDA_ARCH_LIST="8.0;8.6;8.9;9.0" pip wheel . -w dist/
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The build system pairs `setuptools` with `torch.utils.cpp_extension.BuildExtension`.
|
|
121
|
+
Sdists ship the full `` tree so the JIT path works on any
|
|
122
|
+
CUDA-capable machine, even without a matching wheel.
|
|
123
|
+
|
|
124
|
+
### Runtime overrides
|
|
125
|
+
|
|
126
|
+
- `TORCHMATCH_FORCE_JIT=1`: skip the prebuilt `.so` and recompile from
|
|
127
|
+
source via `cpp_extension.load`. Useful for development and for
|
|
128
|
+
diagnosing ABI mismatches.
|
|
129
|
+
|
|
130
|
+
## Development environment
|
|
131
|
+
|
|
132
|
+
A self-contained `flake.nix` provides Python 3.13, uv, and a chosen
|
|
133
|
+
CUDA toolkit (default 12.8) without touching your global system. With
|
|
134
|
+
direnv:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
direnv allow # picks devShells.default = cu128
|
|
138
|
+
NIX_DEVSHELL_NAME=cpu direnv reload
|
|
139
|
+
NIX_DEVSHELL_NAME=cu130 direnv reload
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Without direnv:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
nix develop # default = cu128
|
|
146
|
+
nix develop .#cpu # CPU-only (sets TORCHMATCH_SKIP_CUDA=1)
|
|
147
|
+
nix develop .#cu126
|
|
148
|
+
nix develop .#cu130
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Once inside the shell:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
uv sync --all-groups # populate .venv from pyproject.toml
|
|
155
|
+
uv run python -m pytest tests/ # run the test suite
|
|
156
|
+
```
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
# Pinned to the same range as [project.dependencies]: a wider build-system
|
|
3
|
+
# constraint lets the isolated build env resolve a torch whose libtorch ABI
|
|
4
|
+
# does not match the runtime torch, producing a .so that fails to load with
|
|
5
|
+
# `undefined symbol: torch::Library::_def(...)` style errors.
|
|
6
|
+
requires = ["setuptools>=70", "wheel", "torch>=2.11,<2.13"]
|
|
7
|
+
build-backend = "setuptools.build_meta"
|
|
8
|
+
|
|
9
|
+
[project]
|
|
10
|
+
name = "torchmatch"
|
|
11
|
+
description = "Linear assignment problem solvers for PyTorch."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
version = "1.0.0"
|
|
14
|
+
requires-python = ">=3.13, <3.14"
|
|
15
|
+
keywords = [
|
|
16
|
+
"pytorch",
|
|
17
|
+
"linear assignment problem",
|
|
18
|
+
"lap",
|
|
19
|
+
"lapjv",
|
|
20
|
+
"hungarian algorithm",
|
|
21
|
+
"bipartite matching",
|
|
22
|
+
"cuda",
|
|
23
|
+
]
|
|
24
|
+
authors = [{ name = "Kurt Stolle", email = "kurt@computer.org" }]
|
|
25
|
+
license = "MIT"
|
|
26
|
+
license-files = ["LICENSE"]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 3 - Alpha",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"Intended Audience :: Science/Research",
|
|
31
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
32
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
33
|
+
"Programming Language :: Python",
|
|
34
|
+
"Programming Language :: Python :: 3",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Operating System :: POSIX :: Linux",
|
|
37
|
+
]
|
|
38
|
+
dependencies = ["torch>=2.11"]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
cpu = ["torch>=2.11"]
|
|
42
|
+
cu126 = ["torch>=2.11"]
|
|
43
|
+
cu128 = ["torch>=2.11"]
|
|
44
|
+
cu130 = ["torch>=2.12"]
|
|
45
|
+
|
|
46
|
+
[dependency-groups]
|
|
47
|
+
dev = [
|
|
48
|
+
"pytest>=8.4.2",
|
|
49
|
+
"pytest-benchmark>=5.1.0",
|
|
50
|
+
"pytest-timeout>=2.4.0",
|
|
51
|
+
"ruff>=0.9.0",
|
|
52
|
+
"scipy>=1.10",
|
|
53
|
+
]
|
|
54
|
+
docs = [
|
|
55
|
+
"mkdocs-material>=9.5",
|
|
56
|
+
"mkdocstrings[python]>=0.27",
|
|
57
|
+
"pymdown-extensions>=10.0",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[[tool.uv.index]]
|
|
61
|
+
name = "pytorch-cpu"
|
|
62
|
+
url = "https://download.pytorch.org/whl/cpu"
|
|
63
|
+
explicit = true
|
|
64
|
+
|
|
65
|
+
[[tool.uv.index]]
|
|
66
|
+
name = "pytorch-cu126"
|
|
67
|
+
url = "https://download.pytorch.org/whl/cu126"
|
|
68
|
+
explicit = true
|
|
69
|
+
|
|
70
|
+
[[tool.uv.index]]
|
|
71
|
+
name = "pytorch-cu128"
|
|
72
|
+
url = "https://download.pytorch.org/whl/cu128"
|
|
73
|
+
explicit = true
|
|
74
|
+
|
|
75
|
+
[[tool.uv.index]]
|
|
76
|
+
name = "pytorch-cu130"
|
|
77
|
+
url = "https://download.pytorch.org/whl/cu130"
|
|
78
|
+
explicit = true
|
|
79
|
+
|
|
80
|
+
[tool.uv.sources]
|
|
81
|
+
torch = [
|
|
82
|
+
{ index = "pytorch-cpu", extra = "cpu" },
|
|
83
|
+
{ index = "pytorch-cu126", extra = "cu126" },
|
|
84
|
+
{ index = "pytorch-cu128", extra = "cu128" },
|
|
85
|
+
{ index = "pytorch-cu130", extra = "cu130" },
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
[tool.uv]
|
|
89
|
+
# [build-system].requires uses a separate isolated env that does NOT see
|
|
90
|
+
# [tool.uv.sources], so pip would pull torch from PyPI's default index
|
|
91
|
+
# (which bundles CUDA 13 in current releases) and produce a .so whose
|
|
92
|
+
# torch ABI / CUDA version does not match the venv torch. Disable build
|
|
93
|
+
# isolation for torchmatch so the build reuses the venv's pinned torch
|
|
94
|
+
# (cu126 / cu128 / cu130, picked by the user-selected extra).
|
|
95
|
+
no-build-isolation-package = ["torchmatch"]
|
|
96
|
+
|
|
97
|
+
conflicts = [
|
|
98
|
+
[
|
|
99
|
+
{ extra = "cpu" },
|
|
100
|
+
{ extra = "cu126" },
|
|
101
|
+
{ extra = "cu128" },
|
|
102
|
+
{ extra = "cu130" },
|
|
103
|
+
],
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
[tool.setuptools]
|
|
107
|
+
include-package-data = true
|
|
108
|
+
|
|
109
|
+
[tool.setuptools.packages.find]
|
|
110
|
+
where = ["sources"]
|
|
111
|
+
|
|
112
|
+
[tool.setuptools.package-dir]
|
|
113
|
+
"" = "sources"
|
|
114
|
+
|
|
115
|
+
[tool.setuptools.package-data]
|
|
116
|
+
"torchmatch" = [
|
|
117
|
+
"py.typed",
|
|
118
|
+
"cuda/*.cpp",
|
|
119
|
+
"cuda/*.cu",
|
|
120
|
+
"cuda/*.cuh",
|
|
121
|
+
"cpu/*.cpp",
|
|
122
|
+
"cpu/*.h",
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
[tool.ruff]
|
|
126
|
+
target-version = 'py313'
|
|
127
|
+
line-length = 88
|
|
128
|
+
|
|
129
|
+
[tool.ruff.lint]
|
|
130
|
+
select = [
|
|
131
|
+
"ARG",
|
|
132
|
+
"B",
|
|
133
|
+
"C",
|
|
134
|
+
"C4",
|
|
135
|
+
"D",
|
|
136
|
+
"E",
|
|
137
|
+
"EM",
|
|
138
|
+
"F",
|
|
139
|
+
"FURB",
|
|
140
|
+
"G",
|
|
141
|
+
"I",
|
|
142
|
+
"ICN",
|
|
143
|
+
"N",
|
|
144
|
+
"NPY",
|
|
145
|
+
"PERF",
|
|
146
|
+
"PGH",
|
|
147
|
+
"PIE",
|
|
148
|
+
"PL",
|
|
149
|
+
"PT",
|
|
150
|
+
"PTH",
|
|
151
|
+
"PYI",
|
|
152
|
+
"RUF",
|
|
153
|
+
"RET",
|
|
154
|
+
"SIM",
|
|
155
|
+
"T20",
|
|
156
|
+
"TRY",
|
|
157
|
+
"UP",
|
|
158
|
+
]
|
|
159
|
+
ignore = ["PLR2004", "PLW2901", "D212", "D203", "D107"]
|
|
160
|
+
|
|
161
|
+
[tool.ruff.lint.per-file-ignores]
|
|
162
|
+
# pytest.mark.parametrize functions take one parameter per fixture, so
|
|
163
|
+
# PLR0913's 5-argument cap fires on every multi-parametrize test.
|
|
164
|
+
"tests/*" = ["D", "T", "PLC0415", "PLR0913", "PLR1714"]
|
|
165
|
+
# Utility scripts: print is expected (CLI tools), docstrings are optional,
|
|
166
|
+
# RUF002 ambiguous-unicode rules are docstring-only false positives.
|
|
167
|
+
"scripts/*" = ["D", "T", "RUF002", "C901", "PERF401"]
|
|
168
|
+
|
|
169
|
+
[tool.ruff.format]
|
|
170
|
+
docstring-code-format = true
|
|
171
|
+
docstring-code-line-length = "dynamic"
|
|
172
|
+
|
|
173
|
+
[tool.pytest.ini_options]
|
|
174
|
+
timeout = 10
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build the C++/CUDA extensions for torchmatch.
|
|
3
|
+
|
|
4
|
+
Project metadata lives in ``pyproject.toml``; this file defines only
|
|
5
|
+
``ext_modules`` so :class:`torch.utils.cpp_extension.BuildExtension` can
|
|
6
|
+
compile the native sources into precompiled ``.so`` files.
|
|
7
|
+
|
|
8
|
+
Build-time behaviour
|
|
9
|
+
--------------------
|
|
10
|
+
- ``TORCHMATCH_SKIP_CUDA=1`` skips the CUDA extension entirely.
|
|
11
|
+
- ``TORCHMATCH_SKIP_CPU=1`` skips the CPU extension.
|
|
12
|
+
- When no CUDA toolchain is detected (``torch.utils.cpp_extension.CUDA_HOME``
|
|
13
|
+
is None), the CUDA extension drops out silently, so an sdist install
|
|
14
|
+
on a CPU-only box still produces a usable wheel with just the CPU
|
|
15
|
+
extension; CUDA ops then fall back to JIT at runtime on machines that
|
|
16
|
+
do have a toolchain.
|
|
17
|
+
- ``TORCH_CUDA_ARCH_LIST`` controls which SM versions ``nvcc`` targets;
|
|
18
|
+
honor it when set, otherwise PyTorch's default kicks in.
|
|
19
|
+
- CPU compile flags default to ``-O3 -mavx2 -mfma``. Older CPUs without
|
|
20
|
+
AVX2 install from sdist (runtime JIT detects flags from
|
|
21
|
+
``/proc/cpuinfo``).
|
|
22
|
+
|
|
23
|
+
Runtime behaviour
|
|
24
|
+
-----------------
|
|
25
|
+
``torchmatch.load_cpu()`` / ``torchmatch.load_cuda()`` look for a
|
|
26
|
+
prebuilt ``_cpu_impl*.so`` / ``_cuda_impl*.so`` next to the package and
|
|
27
|
+
``dlopen`` it via ``torch.ops.load_library``. When no prebuilt is
|
|
28
|
+
present, they JIT-compile via ``torch.utils.cpp_extension.load``. Set
|
|
29
|
+
``TORCHMATCH_FORCE_JIT=1`` to bypass the prebuilt path.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from setuptools import Extension, setup
|
|
38
|
+
from torch.utils.cpp_extension import (
|
|
39
|
+
CUDA_HOME,
|
|
40
|
+
BuildExtension,
|
|
41
|
+
CppExtension,
|
|
42
|
+
CUDAExtension,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
CSRC = Path("sources/torchmatch")
|
|
46
|
+
CUDA_CSRC = CSRC / "cuda"
|
|
47
|
+
CPU_CSRC = CSRC / "cpu"
|
|
48
|
+
|
|
49
|
+
SKIP_CUDA = os.environ.get("TORCHMATCH_SKIP_CUDA") == "1"
|
|
50
|
+
SKIP_CPU = os.environ.get("TORCHMATCH_SKIP_CPU") == "1"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _ext_modules() -> list[Extension]:
|
|
54
|
+
mods: list[Extension] = []
|
|
55
|
+
|
|
56
|
+
if not SKIP_CPU:
|
|
57
|
+
mods.append(
|
|
58
|
+
CppExtension(
|
|
59
|
+
name="torchmatch._cpu_impl",
|
|
60
|
+
sources=[
|
|
61
|
+
str(CPU_CSRC / "jv_scalar.cpp"),
|
|
62
|
+
str(CPU_CSRC / "jv_dense_core.cpp"),
|
|
63
|
+
str(CPU_CSRC / "ops.cpp"),
|
|
64
|
+
],
|
|
65
|
+
include_dirs=[str(CPU_CSRC)],
|
|
66
|
+
extra_compile_args={
|
|
67
|
+
"cxx": ["-O3", "-std=c++17", "-mavx2", "-mfma"],
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not SKIP_CUDA and CUDA_HOME is not None:
|
|
73
|
+
mods.append(
|
|
74
|
+
CUDAExtension(
|
|
75
|
+
name="torchmatch._cuda_impl",
|
|
76
|
+
sources=[
|
|
77
|
+
str(CUDA_CSRC / "ops.cpp"),
|
|
78
|
+
str(CUDA_CSRC / "dispatch.cu"),
|
|
79
|
+
str(CUDA_CSRC / "munkres_classical.cu"),
|
|
80
|
+
str(CUDA_CSRC / "munkres_hybrid.cu"),
|
|
81
|
+
str(CUDA_CSRC / "munkres_tree.cu"),
|
|
82
|
+
],
|
|
83
|
+
include_dirs=[str(CUDA_CSRC)],
|
|
84
|
+
extra_compile_args={
|
|
85
|
+
"cxx": ["-O3"],
|
|
86
|
+
"nvcc": ["-O3", "-std=c++17"],
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return mods
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
setup(
|
|
95
|
+
ext_modules=_ext_modules(),
|
|
96
|
+
cmdclass={"build_ext": BuildExtension},
|
|
97
|
+
)
|