torch-linear-assignment 0.1.0__py3-none-any.whl
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.
- torch_linear_assignment/__init__.py +5 -0
- torch_linear_assignment/_triton.py +309 -0
- torch_linear_assignment/assignment.py +163 -0
- torch_linear_assignment-0.1.0.dist-info/METADATA +218 -0
- torch_linear_assignment-0.1.0.dist-info/RECORD +8 -0
- torch_linear_assignment-0.1.0.dist-info/WHEEL +5 -0
- torch_linear_assignment-0.1.0.dist-info/licenses/LICENSE +201 -0
- torch_linear_assignment-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Triton implementation of batched rectangular linear assignment.
|
|
2
|
+
|
|
3
|
+
Purpose:
|
|
4
|
+
Provide the private CUDA implementation selected by ``assignment.py`` on
|
|
5
|
+
supported Linux NVIDIA systems. The module owns no public dispatch policy;
|
|
6
|
+
importing it is deliberately deferred until the caller has established that
|
|
7
|
+
CUDA, the Torch version, and the GPU capability are eligible.
|
|
8
|
+
|
|
9
|
+
Scope:
|
|
10
|
+
One Triton program solves one batch item. The program is intentionally
|
|
11
|
+
correctness-first: outer augmenting-path steps remain sequential while each
|
|
12
|
+
current set of candidate columns is processed as a masked lane vector.
|
|
13
|
+
Per-search state remains program-local. The correctness launch currently
|
|
14
|
+
keeps its loop-carried vectors within one warp; wider launch configurations
|
|
15
|
+
remain gated on exact GPU parity. Its ordering reproduces the legacy CUDA
|
|
16
|
+
and SciPy rectangular-LSAP algorithm.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
``batch_linear_assignment(cuda_cost)`` accepts a three-dimensional CUDA
|
|
20
|
+
cost tensor and returns the stable public ``(B, W)`` long assignment shape.
|
|
21
|
+
The optional validation setting is private benchmark instrumentation; normal
|
|
22
|
+
dispatch always uses the default full validation.
|
|
23
|
+
|
|
24
|
+
Outputs:
|
|
25
|
+
The kernel writes assignment and temporary Torch workspaces on the input
|
|
26
|
+
device. The wrapper returns worker-to-task indices, using ``-1`` for an
|
|
27
|
+
unmatched worker, and raises ordinary ``ValueError`` exceptions for invalid
|
|
28
|
+
numeric values or infeasible matrices.
|
|
29
|
+
|
|
30
|
+
Failure:
|
|
31
|
+
NaN and negative infinity are rejected by one host reduction before launch.
|
|
32
|
+
The kernel records one infeasibility flag per batch item instead of asserting
|
|
33
|
+
on device; the normal wrapper synchronizes once to turn any flag into an
|
|
34
|
+
exception. Triton import, compilation, and GPU execution failures are not
|
|
35
|
+
swallowed because they do not mean the optional dependency is merely absent.
|
|
36
|
+
|
|
37
|
+
Used by:
|
|
38
|
+
``torch_linear_assignment.assignment.batch_linear_assignment_cuda``. The
|
|
39
|
+
module is private so its benchmark validation switch and workspace layout do
|
|
40
|
+
not become part of the package API.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from typing import Literal
|
|
44
|
+
|
|
45
|
+
import torch
|
|
46
|
+
import triton
|
|
47
|
+
import triton.language as tl
|
|
48
|
+
|
|
49
|
+
ValidationMode = Literal["full", "off", "nonfinite_only", "infeasibility_flag_only"]
|
|
50
|
+
_VALIDATION_MODES: frozenset[str] = frozenset({"full", "off", "nonfinite_only", "infeasibility_flag_only"})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@triton.jit
|
|
54
|
+
def _linear_assignment_kernel(
|
|
55
|
+
cost,
|
|
56
|
+
u,
|
|
57
|
+
v,
|
|
58
|
+
col4row,
|
|
59
|
+
row4col,
|
|
60
|
+
infeasible,
|
|
61
|
+
NUM_ROWS: tl.constexpr,
|
|
62
|
+
NUM_COLUMNS: tl.constexpr,
|
|
63
|
+
BLOCK_N: tl.constexpr,
|
|
64
|
+
IS_FP64: tl.constexpr,
|
|
65
|
+
):
|
|
66
|
+
"""Solve one rectangular assignment problem in a single Triton program."""
|
|
67
|
+
batch_index = tl.program_id(0)
|
|
68
|
+
lane = tl.arange(0, BLOCK_N)
|
|
69
|
+
column_mask = lane < NUM_COLUMNS
|
|
70
|
+
row_mask = lane < NUM_ROWS
|
|
71
|
+
|
|
72
|
+
cost = cost + batch_index * NUM_ROWS * NUM_COLUMNS
|
|
73
|
+
u = u + batch_index * NUM_ROWS
|
|
74
|
+
v = v + batch_index * NUM_COLUMNS
|
|
75
|
+
col4row = col4row + batch_index * NUM_ROWS
|
|
76
|
+
row4col = row4col + batch_index * NUM_COLUMNS
|
|
77
|
+
|
|
78
|
+
for current_row in tl.range(0, NUM_ROWS, num_stages=1):
|
|
79
|
+
scanned_rows = tl.zeros((BLOCK_N,), tl.int1)
|
|
80
|
+
scanned_columns = tl.zeros((BLOCK_N,), tl.int1)
|
|
81
|
+
active_columns = column_mask
|
|
82
|
+
scan_positions = NUM_COLUMNS - lane - 1
|
|
83
|
+
path = tl.full((BLOCK_N,), -1, tl.int32)
|
|
84
|
+
if IS_FP64:
|
|
85
|
+
shortest_path_costs = tl.full((BLOCK_N,), float("inf"), tl.float64)
|
|
86
|
+
scanned_row_costs = tl.zeros((BLOCK_N,), tl.float64)
|
|
87
|
+
else:
|
|
88
|
+
shortest_path_costs = tl.full((BLOCK_N,), float("inf"), tl.float32)
|
|
89
|
+
scanned_row_costs = tl.zeros((BLOCK_N,), tl.float32)
|
|
90
|
+
|
|
91
|
+
sink = -1
|
|
92
|
+
current_potential = tl.load(u + current_row)
|
|
93
|
+
# A workspace-derived zero keeps this loop-carried value FP32 or FP64.
|
|
94
|
+
min_value = current_potential - current_potential
|
|
95
|
+
search_row = current_row
|
|
96
|
+
num_remaining = NUM_COLUMNS
|
|
97
|
+
|
|
98
|
+
# Wide matrices usually reach an unmatched sink early; do not execute
|
|
99
|
+
# the remaining masked column-search iterations after that point.
|
|
100
|
+
searching = True
|
|
101
|
+
while searching:
|
|
102
|
+
scanned_rows = scanned_rows | (row_mask & (lane == search_row) & searching)
|
|
103
|
+
candidate_mask = active_columns & searching
|
|
104
|
+
previous_cost = shortest_path_costs
|
|
105
|
+
reduced_cost = (
|
|
106
|
+
min_value
|
|
107
|
+
- tl.load(u + search_row)
|
|
108
|
+
+ tl.load(
|
|
109
|
+
cost + search_row * NUM_COLUMNS + lane,
|
|
110
|
+
mask=candidate_mask,
|
|
111
|
+
other=float("inf"),
|
|
112
|
+
)
|
|
113
|
+
- tl.load(v + lane, mask=candidate_mask, other=0.0)
|
|
114
|
+
)
|
|
115
|
+
improved = candidate_mask & (reduced_cost < previous_cost)
|
|
116
|
+
candidate_cost = tl.where(improved, reduced_cost, previous_cost)
|
|
117
|
+
shortest_path_costs = candidate_cost
|
|
118
|
+
path = tl.where(improved, search_row, path)
|
|
119
|
+
|
|
120
|
+
# The legacy solver scans a swap-removed ``remaining`` array. Track
|
|
121
|
+
# each column's current scan position locally to preserve its exact
|
|
122
|
+
# last-unmatched/first-matched tie rule without shared scratch writes.
|
|
123
|
+
lowest = tl.min(
|
|
124
|
+
tl.where(candidate_mask, candidate_cost, float("inf")),
|
|
125
|
+
axis=0,
|
|
126
|
+
)
|
|
127
|
+
has_candidate = lowest != float("inf")
|
|
128
|
+
step_valid = searching & has_candidate
|
|
129
|
+
tied = candidate_mask & (candidate_cost == lowest)
|
|
130
|
+
matched_rows = tl.load(row4col + lane, mask=candidate_mask, other=0)
|
|
131
|
+
last_unmatched_position = tl.max(
|
|
132
|
+
tl.where(tied & (matched_rows == -1), scan_positions, -1),
|
|
133
|
+
axis=0,
|
|
134
|
+
)
|
|
135
|
+
first_tied_position = tl.min(
|
|
136
|
+
tl.where(tied, scan_positions, NUM_COLUMNS),
|
|
137
|
+
axis=0,
|
|
138
|
+
)
|
|
139
|
+
selected_position = tl.where(
|
|
140
|
+
last_unmatched_position >= 0,
|
|
141
|
+
last_unmatched_position,
|
|
142
|
+
first_tied_position,
|
|
143
|
+
)
|
|
144
|
+
selected_column = tl.max(
|
|
145
|
+
tl.where(candidate_mask & (scan_positions == selected_position), lane, -1),
|
|
146
|
+
axis=0,
|
|
147
|
+
)
|
|
148
|
+
selected_column = tl.where(step_valid, selected_column, 0)
|
|
149
|
+
selected_row = tl.load(row4col + selected_column)
|
|
150
|
+
|
|
151
|
+
# A matched row enters the search tree through ``selected_column``.
|
|
152
|
+
# Save that column's shortest cost by row now, avoiding a dynamic
|
|
153
|
+
# gather from the loop-carried column vector after the search.
|
|
154
|
+
matched_step = step_valid & (selected_row != -1)
|
|
155
|
+
scanned_row_costs = tl.where(
|
|
156
|
+
row_mask & (lane == selected_row) & matched_step,
|
|
157
|
+
lowest,
|
|
158
|
+
scanned_row_costs,
|
|
159
|
+
)
|
|
160
|
+
sink = tl.where(step_valid & (selected_row == -1), selected_column, sink)
|
|
161
|
+
search_row = tl.where(
|
|
162
|
+
matched_step,
|
|
163
|
+
selected_row,
|
|
164
|
+
search_row,
|
|
165
|
+
)
|
|
166
|
+
scanned_columns = scanned_columns | (column_mask & (lane == selected_column) & step_valid)
|
|
167
|
+
last_index = num_remaining - 1
|
|
168
|
+
last_column = tl.max(
|
|
169
|
+
tl.where(active_columns & (scan_positions == last_index), lane, -1),
|
|
170
|
+
axis=0,
|
|
171
|
+
)
|
|
172
|
+
scan_positions = tl.where(
|
|
173
|
+
active_columns & (lane == last_column) & step_valid,
|
|
174
|
+
selected_position,
|
|
175
|
+
scan_positions,
|
|
176
|
+
)
|
|
177
|
+
active_columns = active_columns & ~((lane == selected_column) & step_valid)
|
|
178
|
+
num_remaining = tl.where(step_valid, num_remaining - 1, num_remaining)
|
|
179
|
+
min_value = tl.where(step_valid, lowest, min_value)
|
|
180
|
+
tl.store(infeasible + batch_index, 1, mask=searching & ~has_candidate)
|
|
181
|
+
searching = matched_step
|
|
182
|
+
|
|
183
|
+
solved = sink != -1
|
|
184
|
+
tl.store(infeasible + batch_index, 1, mask=~solved)
|
|
185
|
+
|
|
186
|
+
tl.store(u + current_row, current_potential + min_value, mask=solved)
|
|
187
|
+
visited_rows = scanned_rows
|
|
188
|
+
update_row = solved & row_mask & visited_rows & (lane != current_row)
|
|
189
|
+
tl.store(
|
|
190
|
+
u + lane,
|
|
191
|
+
tl.load(u + lane, mask=row_mask, other=0.0) + min_value - scanned_row_costs,
|
|
192
|
+
mask=update_row,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
update_column = solved & column_mask & scanned_columns
|
|
196
|
+
tl.store(
|
|
197
|
+
v + lane,
|
|
198
|
+
tl.load(v + lane, mask=column_mask, other=0.0) - min_value + shortest_path_costs,
|
|
199
|
+
mask=update_column,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
augmenting = solved
|
|
203
|
+
augmenting_column = sink
|
|
204
|
+
augmentation_steps = 0
|
|
205
|
+
# Recover only the actual path while retaining the row-count safety cap.
|
|
206
|
+
while augmenting & (augmentation_steps < NUM_ROWS):
|
|
207
|
+
safe_column = tl.where(augmenting, augmenting_column, 0)
|
|
208
|
+
augmenting_row = tl.max(
|
|
209
|
+
tl.where(column_mask & (lane == safe_column), path, -1),
|
|
210
|
+
axis=0,
|
|
211
|
+
)
|
|
212
|
+
safe_row = tl.where(augmenting, augmenting_row, 0)
|
|
213
|
+
previous_column = tl.load(col4row + safe_row)
|
|
214
|
+
tl.store(row4col + safe_column, augmenting_row, mask=augmenting)
|
|
215
|
+
tl.store(col4row + safe_row, augmenting_column, mask=augmenting)
|
|
216
|
+
augmenting = augmenting & (augmenting_row != current_row)
|
|
217
|
+
augmenting_column = tl.where(augmenting, previous_column, augmenting_column)
|
|
218
|
+
augmentation_steps += 1
|
|
219
|
+
|
|
220
|
+
# The next row consumes the assignments and potentials written above.
|
|
221
|
+
tl.debug_barrier()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _validate_mode(validation: ValidationMode) -> None:
|
|
225
|
+
"""Reject an unsupported private validation configuration."""
|
|
226
|
+
if validation not in _VALIDATION_MODES:
|
|
227
|
+
raise ValueError(f"Unknown Triton validation mode: {validation!r}.")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _reject_invalid_numeric_entries(cost: torch.Tensor) -> None:
|
|
231
|
+
"""Raise the SciPy-compatible error for NaN or negative-infinite costs."""
|
|
232
|
+
if not cost.is_floating_point():
|
|
233
|
+
return
|
|
234
|
+
invalid = torch.logical_or(torch.isnan(cost), torch.isneginf(cost)).any()
|
|
235
|
+
if bool(invalid.item()):
|
|
236
|
+
raise ValueError("matrix contains invalid numeric entries")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _solver_cost_dtype(cost: torch.Tensor) -> torch.Tensor:
|
|
240
|
+
"""Preserve float64 costs and promote every other solver input to float32."""
|
|
241
|
+
if cost.dtype == torch.float64:
|
|
242
|
+
return cost
|
|
243
|
+
return cost.to(torch.float32)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _empty_assignment(cost: torch.Tensor) -> torch.Tensor:
|
|
247
|
+
"""Return the public assignment shape without launching a zero-size kernel."""
|
|
248
|
+
batch_size, workers, _ = cost.shape
|
|
249
|
+
return torch.full((batch_size, workers), -1, dtype=torch.long, device=cost.device)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _solve(cost: torch.Tensor, validation: ValidationMode) -> tuple[torch.Tensor, torch.Tensor]:
|
|
253
|
+
"""Launch the one-program-per-batch kernel and return both assignment views."""
|
|
254
|
+
batch_size, rows, columns = cost.shape
|
|
255
|
+
block_n = triton.next_power_of_2(columns)
|
|
256
|
+
workspace_options = {"device": cost.device}
|
|
257
|
+
scalar_options = {**workspace_options, "dtype": cost.dtype}
|
|
258
|
+
integer_options = {**workspace_options, "dtype": torch.int32}
|
|
259
|
+
u = torch.zeros((batch_size, rows), **scalar_options)
|
|
260
|
+
v = torch.zeros((batch_size, columns), **scalar_options)
|
|
261
|
+
col4row = torch.full((batch_size, rows), -1, **integer_options)
|
|
262
|
+
row4col = torch.full((batch_size, columns), -1, **integer_options)
|
|
263
|
+
infeasible = torch.zeros((batch_size,), **integer_options)
|
|
264
|
+
|
|
265
|
+
_linear_assignment_kernel[(batch_size,)](
|
|
266
|
+
cost,
|
|
267
|
+
u,
|
|
268
|
+
v,
|
|
269
|
+
col4row,
|
|
270
|
+
row4col,
|
|
271
|
+
infeasible,
|
|
272
|
+
NUM_ROWS=rows,
|
|
273
|
+
NUM_COLUMNS=columns,
|
|
274
|
+
BLOCK_N=block_n,
|
|
275
|
+
IS_FP64=cost.dtype == torch.float64,
|
|
276
|
+
# Multi-warp loop-carried state produces false infeasibility on the
|
|
277
|
+
# saved 512-lane L4 case; keep one warp until a wider design is exact.
|
|
278
|
+
num_warps=1,
|
|
279
|
+
)
|
|
280
|
+
if validation in {"full", "infeasibility_flag_only"} and bool(infeasible.any().item()):
|
|
281
|
+
raise ValueError("cost matrix is infeasible")
|
|
282
|
+
return col4row, row4col
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def batch_linear_assignment(
|
|
286
|
+
cost: torch.Tensor,
|
|
287
|
+
*,
|
|
288
|
+
validation: ValidationMode = "full",
|
|
289
|
+
) -> torch.Tensor:
|
|
290
|
+
"""Solve a CUDA batch with full validation unless private benchmarks opt out."""
|
|
291
|
+
_validate_mode(validation)
|
|
292
|
+
if cost.ndim != 3:
|
|
293
|
+
raise ValueError("Need 3-dimensional tensor with shape (B, W, T).")
|
|
294
|
+
if not cost.is_cuda:
|
|
295
|
+
raise ValueError("Triton linear assignment requires a CUDA tensor.")
|
|
296
|
+
if validation in {"full", "nonfinite_only"}:
|
|
297
|
+
_reject_invalid_numeric_entries(cost)
|
|
298
|
+
|
|
299
|
+
cost = _solver_cost_dtype(cost)
|
|
300
|
+
batch_size, workers, tasks = cost.shape
|
|
301
|
+
if batch_size == 0 or workers == 0 or tasks == 0:
|
|
302
|
+
return _empty_assignment(cost)
|
|
303
|
+
|
|
304
|
+
with torch.amp.autocast("cuda", enabled=False):
|
|
305
|
+
if tasks < workers:
|
|
306
|
+
_, row4col = _solve(cost.transpose(1, 2).contiguous(), validation)
|
|
307
|
+
return row4col.to(torch.long)
|
|
308
|
+
col4row, _ = _solve(cost.contiguous(), validation)
|
|
309
|
+
return col4row.to(torch.long)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Public linear-assignment API with optional CUDA acceleration."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import warnings
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
from scipy.optimize import linear_sum_assignment
|
|
11
|
+
|
|
12
|
+
_CUDA_FALLBACK_WARNING_EMITTED = False
|
|
13
|
+
_TRITON_MINIMUM_TORCH_VERSION = (2, 4)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _prepare_solver_cost(cost: torch.Tensor) -> torch.Tensor:
|
|
17
|
+
"""Apply the documented solver dtype without discarding complex values."""
|
|
18
|
+
if cost.dtype in {torch.float32, torch.float64} or cost.is_complex():
|
|
19
|
+
return cost
|
|
20
|
+
return cost.to(torch.float32)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def batch_linear_assignment_cpu(cost: torch.Tensor) -> torch.Tensor:
|
|
24
|
+
"""Solve batched assignment with SciPy on the cost tensor's CPU device."""
|
|
25
|
+
cost = _prepare_solver_cost(cost)
|
|
26
|
+
batch_size, workers, _ = cost.shape
|
|
27
|
+
matching = torch.full([batch_size, workers], -1, dtype=torch.long, device=cost.device)
|
|
28
|
+
for batch_index in range(batch_size):
|
|
29
|
+
row_indices, column_indices = linear_sum_assignment(cost[batch_index].numpy(), maximize=False)
|
|
30
|
+
matching[batch_index].scatter_(
|
|
31
|
+
0,
|
|
32
|
+
torch.from_numpy(row_indices),
|
|
33
|
+
torch.from_numpy(column_indices),
|
|
34
|
+
)
|
|
35
|
+
return matching
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _torch_supports_triton() -> bool:
|
|
39
|
+
"""Return whether the installed Torch version meets the Triton-path floor."""
|
|
40
|
+
version = re.match(r"(\d+)\.(\d+)", torch.__version__)
|
|
41
|
+
return version is not None and tuple(map(int, version.groups())) >= _TRITON_MINIMUM_TORCH_VERSION
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _load_triton_backend() -> ModuleType | None:
|
|
45
|
+
"""Load the optional Triton module without hiding nested import failures."""
|
|
46
|
+
try:
|
|
47
|
+
return importlib.import_module("torch_linear_assignment._triton")
|
|
48
|
+
except ModuleNotFoundError as error:
|
|
49
|
+
if error.name in {"torch_linear_assignment._triton", "triton"}:
|
|
50
|
+
return None
|
|
51
|
+
raise
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load_legacy_backend() -> ModuleType | None:
|
|
55
|
+
"""Load the optional legacy extension for private benchmark comparisons only."""
|
|
56
|
+
try:
|
|
57
|
+
return importlib.import_module("torch_linear_assignment._backend")
|
|
58
|
+
except ModuleNotFoundError as error:
|
|
59
|
+
if error.name == "torch_linear_assignment._backend":
|
|
60
|
+
return None
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cuda_uses_triton(cost: torch.Tensor) -> bool:
|
|
65
|
+
"""Return whether this CUDA cost tensor can use the supported Triton path."""
|
|
66
|
+
if (
|
|
67
|
+
sys.platform != "linux"
|
|
68
|
+
or not _torch_supports_triton()
|
|
69
|
+
or not torch.cuda.is_available()
|
|
70
|
+
or torch.version.cuda is None
|
|
71
|
+
):
|
|
72
|
+
return False
|
|
73
|
+
return torch.cuda.get_device_capability(cost.device) >= (8, 0) and _load_triton_backend() is not None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _warn_cuda_fallback() -> None:
|
|
77
|
+
"""Warn once when a CUDA input must use the SciPy CPU fallback."""
|
|
78
|
+
global _CUDA_FALLBACK_WARNING_EMITTED
|
|
79
|
+
if _CUDA_FALLBACK_WARNING_EMITTED:
|
|
80
|
+
return
|
|
81
|
+
_CUDA_FALLBACK_WARNING_EMITTED = True
|
|
82
|
+
warnings.warn(
|
|
83
|
+
"Triton linear-assignment support is unavailable for this CUDA input; using SciPy on CPU.",
|
|
84
|
+
RuntimeWarning,
|
|
85
|
+
stacklevel=3,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _batch_linear_assignment_cuda_legacy(cost: torch.Tensor) -> torch.Tensor:
|
|
90
|
+
"""Run the legacy extension for private benchmark comparison when installed."""
|
|
91
|
+
backend = _load_legacy_backend()
|
|
92
|
+
if backend is None:
|
|
93
|
+
raise RuntimeError("The legacy CUDA extension is not installed.")
|
|
94
|
+
|
|
95
|
+
cost = _prepare_solver_cost(cost)
|
|
96
|
+
_, workers, tasks = cost.shape
|
|
97
|
+
if tasks < workers:
|
|
98
|
+
_, row4col = backend.batch_linear_assignment(cost.transpose(1, 2).contiguous())
|
|
99
|
+
return row4col.long()
|
|
100
|
+
col4row, _ = backend.batch_linear_assignment(cost.contiguous())
|
|
101
|
+
return col4row.long()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def batch_linear_assignment_cuda(cost: torch.Tensor) -> torch.Tensor:
|
|
105
|
+
"""Solve a CUDA batch through the private Triton implementation."""
|
|
106
|
+
backend = _load_triton_backend()
|
|
107
|
+
if backend is None:
|
|
108
|
+
raise RuntimeError("Triton is unavailable for CUDA linear assignment.")
|
|
109
|
+
return backend.batch_linear_assignment(cost)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def batch_linear_assignment(cost: torch.Tensor) -> torch.Tensor:
|
|
113
|
+
"""Solve a batch of linear assignment problems.
|
|
114
|
+
|
|
115
|
+
The method minimizes the cost.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
cost: Cost matrix with shape (B, W, T), where W is the number of workers
|
|
119
|
+
and T is the number of tasks.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Matching tensor with shape (B, W), with assignments for each worker. If the
|
|
123
|
+
task was not assigned, the corresponding index will be -1.
|
|
124
|
+
"""
|
|
125
|
+
if cost.ndim != 3:
|
|
126
|
+
raise ValueError("Need 3-dimensional tensor with shape (B, W, T).")
|
|
127
|
+
if cost.is_cuda and _cuda_uses_triton(cost):
|
|
128
|
+
return batch_linear_assignment_cuda(cost)
|
|
129
|
+
|
|
130
|
+
device = cost.device
|
|
131
|
+
if cost.is_cuda:
|
|
132
|
+
_warn_cuda_fallback()
|
|
133
|
+
cost = cost.cpu()
|
|
134
|
+
return batch_linear_assignment_cpu(cost).to(device)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def assignment_to_indices(
|
|
138
|
+
assignment: torch.Tensor,
|
|
139
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
140
|
+
"""Convert assignment to the SciPy format.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
assignment: The assignment with shape (B, W).
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
row_ind, col_ind: An array of row indices and one of corresponding column indices
|
|
147
|
+
giving the optimal assignment, each with shape (B, K).
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ValueError if batch assignments have different sizes.
|
|
151
|
+
"""
|
|
152
|
+
batch_size = assignment.shape[0]
|
|
153
|
+
if batch_size == 0:
|
|
154
|
+
indices = torch.zeros(0, 0, dtype=torch.long, device=assignment.device)
|
|
155
|
+
return indices, indices
|
|
156
|
+
mask = assignment >= 0
|
|
157
|
+
n_matches = mask.sum(1)
|
|
158
|
+
if (n_matches[1:] != n_matches[0]).any():
|
|
159
|
+
raise ValueError("Inconsistent matching sizes.")
|
|
160
|
+
n_matches = n_matches[0].item()
|
|
161
|
+
row_ind = mask.nonzero()[:, 1].reshape(batch_size, n_matches)
|
|
162
|
+
col_ind = assignment.masked_select(mask).reshape(batch_size, n_matches)
|
|
163
|
+
return row_ind, col_ind
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: torch-linear-assignment
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Batched linear assignment with PyTorch and CUDA.
|
|
5
|
+
Author: Ivan Karpukhin
|
|
6
|
+
Author-email: karpuhini@yandex.ru
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: torch>=1.12.0
|
|
11
|
+
Requires-Dist: scipy>=1.6.0
|
|
12
|
+
Requires-Dist: triton; platform_system == "Linux"
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
# Batch linear assignment for PyTorch
|
|
23
|
+
|
|
24
|
+
[](https://badge.fury.io/py/torch-linear-assignment) [](https://github.com/ivan-chai/torch-linear-assignment/actions) [](https://pepy.tech/project/torch-linear-assignment) [](https://opensource.org/licenses/Apache-2.0)
|
|
25
|
+
|
|
26
|
+
<h4 align="left">
|
|
27
|
+
<p>
|
|
28
|
+
<a href="#install">Installation</a> |
|
|
29
|
+
<a href="#backend-and-release-policy">Backend policy</a> |
|
|
30
|
+
<a href="#example">Usage</a> |
|
|
31
|
+
<a href="#support">Support</a> |
|
|
32
|
+
<a href="#legacy-cuda-implementation-00x">Legacy CUDA</a> |
|
|
33
|
+
<a href="#acknowledgments">Acknowledgments</a> |
|
|
34
|
+
<a href="#citation">Citation</a>
|
|
35
|
+
<p>
|
|
36
|
+
</h4>
|
|
37
|
+
Batch computation of the linear assignment problem with PyTorch. The active
|
|
38
|
+
`0.1.0+` release line is pure source: CPU calls use SciPy, and supported CUDA
|
|
39
|
+
calls use the lazy Triton backend.
|
|
40
|
+
|
|
41
|
+
## Backend and release policy
|
|
42
|
+
|
|
43
|
+
The active release line is `0.1.0+`. It uses Triton on validated Linux NVIDIA GPUs with compute capability 8.0 or newer and SciPy everywhere else. Unsupported CUDA inputs are solved by the SciPy CPU fallback and returned to the input device. The compiled CUDA extension belongs to the maintenance-only `0.0.x` line and is documented separately in [Legacy CUDA implementation (`0.0.x`)](#legacy-cuda-implementation-00x). If that backend needs a compatibility or correctness fix, the project can issue another `0.0.x` release without adopting the Triton overhaul.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
Python 3.10 or newer is required. Install the `0.1.0` release candidate from PyPI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python -m pip install "torch-linear-assignment==0.1.0rc0"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
No editable install, local CUDA compilation, or `--no-build-isolation` flag is needed for normal use. On Linux, the package declares Triton through a platform marker and imports it lazily only for an eligible CUDA input. macOS and Windows installs remain usable through SciPy without requiring Triton.
|
|
54
|
+
|
|
55
|
+
## Example
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import torch
|
|
59
|
+
from torch_linear_assignment import batch_linear_assignment
|
|
60
|
+
|
|
61
|
+
cost = torch.tensor([
|
|
62
|
+
8, 4, 7,
|
|
63
|
+
5, 2, 3,
|
|
64
|
+
9, 6, 7,
|
|
65
|
+
9, 4, 8,
|
|
66
|
+
]).reshape(1, 4, 3)
|
|
67
|
+
|
|
68
|
+
assignment = batch_linear_assignment(cost)
|
|
69
|
+
print(assignment)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The output is:
|
|
73
|
+
|
|
74
|
+
```py
|
|
75
|
+
tensor([[ 0, 2, -1, 1]])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
To get indices in the SciPy's format:
|
|
79
|
+
|
|
80
|
+
```py
|
|
81
|
+
from torch_linear_assignment import assignment_to_indices
|
|
82
|
+
|
|
83
|
+
row_ind, col_ind = assignment_to_indices(assignment)
|
|
84
|
+
print(row_ind)
|
|
85
|
+
print(col_ind)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The output is:
|
|
89
|
+
|
|
90
|
+
```py
|
|
91
|
+
tensor([[0, 1, 3]])
|
|
92
|
+
tensor([[0, 2, 1]])
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Support
|
|
96
|
+
|
|
97
|
+
The public API is `batch_linear_assignment(cost)` with cost shape `(B, W, T)`. Assignments have shape `(B, W)`, use `torch.long`, and contain `-1` for an unmatched worker.
|
|
98
|
+
|
|
99
|
+
| Input and runtime | Backend and behavior |
|
|
100
|
+
| --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
101
|
+
| CPU input on any supported OS | SciPy on CPU. This remains the reference path. |
|
|
102
|
+
| Linux NVIDIA GPU with compute capability 8.0 or newer, Torch >= 2.4, and Triton available | Lazy Triton CUDA backend. The first call can include Triton compilation. |
|
|
103
|
+
| CUDA input on an unsupported OS; non-NVIDIA Linux accelerator; NVIDIA GPU below compute capability 8.0 (including T4); or missing/ineligible Triton/Torch | Warn once, solve with SciPy on CPU, and return the assignment on the input device. |
|
|
104
|
+
|
|
105
|
+
The solver promotes `float16`, `bfloat16`, and integer costs to `float32`; `float64` costs remain `float64`. Triton execution disables autocast, so precision already lost before entry cannot be recovered.
|
|
106
|
+
|
|
107
|
+
Non-finite costs follow the SciPy contract. `NaN` and `-inf` raise `ValueError("matrix contains invalid numeric entries")`. `+inf` represents a forbidden edge when a perfect matching still exists; if the matrix is infeasible, the call raises `ValueError("cost matrix is infeasible")` instead of terminating the process with a device assertion.
|
|
108
|
+
|
|
109
|
+
## Validation and performance status
|
|
110
|
+
|
|
111
|
+
GPU validation is author-run rather than hosted CI. The validation targets are Linux NVIDIA GPUs with compute capability 8.0 or newer (for example, L4, A100, H100, or RTX PRO 6000 Blackwell):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
make validate # correctness once; prints GPU metadata when available
|
|
115
|
+
make benchmark # SciPy CPU and installed public CUDA; writes JSONL and a cold/warm table
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`make validate` prints Python/platform, Torch/CUDA, Triton, and GPU compute capability metadata when a CUDA device is visible. `make benchmark` separately measures the SciPy CPU reference and installed public CUDA backend, writes complete JSONL evidence, and prints a compact cold/warm table. A clean GPU metadata skip on macOS or a machine without CUDA is expected, but is not GPU evidence.
|
|
119
|
+
|
|
120
|
+
The first Triton call may be slower because it includes cold compilation; measure cold compilation separately from warm execution. Do not generalize performance across GPU models or workloads. The active `0.1.0+` line requires the correctness, AMP/dtype, fallback, packaging, GPU, and warm-performance gates to pass on each advertised GPU class.
|
|
121
|
+
|
|
122
|
+
Author-run evidence captured on 2026-08-20 at commit `eee95ffc` (`perf(triton): terminate completed augmentations early`) supports this matrix:
|
|
123
|
+
|
|
124
|
+
| GPU | SciPy reference comparison | Triton validation |
|
|
125
|
+
| ---------------------------------------- | ------------------------------- | ------------------ |
|
|
126
|
+
| NVIDIA L4 (`sm_89`) | Exact in all 30 benchmark cases | `78/78` tests pass |
|
|
127
|
+
| NVIDIA A100 (`sm_80`) | Exact in all 30 benchmark cases | `78/78` tests pass |
|
|
128
|
+
| NVIDIA H100 (`sm_90`) | ? | ? |
|
|
129
|
+
| NVIDIA RTX PRO 6000 Blackwell (`sm_120`) | Exact in all 30 benchmark cases | `78/78` tests pass |
|
|
130
|
+
|
|
131
|
+
Representative batch-208 float32 performance is shown below. Each timing cell is `transpose / square / direct` in milliseconds for `300 x 100`, `300 x 300`, and `300 x 600`. Ratios above `1.0x` mean Triton is faster. Values are rounded to one decimal; `~` marks ratios derived from the displayed timings.
|
|
132
|
+
|
|
133
|
+
| GPU / timing | SciPy CPU (ms) | Triton (ms) | SciPy / Triton |
|
|
134
|
+
| ------------------ | -------------------- | ---------------- | ----------------------- |
|
|
135
|
+
| L4 (first\*) | 71.8 / 788.6 / 421.9 | 1.0 / 7.6 / 4.3 | ~71.8x / 103.8x / 98.1x |
|
|
136
|
+
| L4 (warm) | 71.3 / 782.5 / 422.8 | 1.0 / 7.6 / 4.4 | 71.3x / 103.5x / 96.4x |
|
|
137
|
+
| A100 (first\*) | 74.8 / 788.6 / 430.3 | 1.3 / 11.6 / 4.3 | ~57.5x / 68.0x / 100.1x |
|
|
138
|
+
| A100 (warm) | 73.9 / 788.4 / 431.9 | 1.3 / 11.6 / 4.4 | 56.7x / 67.7x / 99.1x |
|
|
139
|
+
| H100 (first\*) | ? | ? | ? |
|
|
140
|
+
| H100 (warm) | ? | ? | ? |
|
|
141
|
+
| RTX 6000 (first\*) | 32.1 / 409.8 / 204.8 | 0.7 / 5.5 / 2.2 | ~45.9x / 74.5x / 93.1x |
|
|
142
|
+
| RTX 6000 (warm) | 32.3 / 410.1 / 204.8 | 0.5 / 5.4 / 2.2 | 60.8x / 75.8x / 94.4x |
|
|
143
|
+
|
|
144
|
+
The Triton implementation parallelizes batch items and vectorizes each current candidate-column scan. Commit `eee95ffc` also terminates the shortest-path search when an augmentation finds an unmatched sink. This removes the post-completion iterations that dominated direct matrices. Against the same-machine SciPy CPU reference, representative warm Triton speedups span `56.7x`--`103.5x` while retaining exact assignment parity.
|
|
145
|
+
|
|
146
|
+
`first*` means the first call for that case in the shared benchmark process. It is not an isolated compilation measurement: earlier shapes and dtypes can populate driver, process, and Triton disk caches. The three notebooks also predate the fresh-process benchmark-integrity harness, so these measurements are provisional author-run evidence rather than the final cold-performance record. Publication-grade cold claims require a source-bound, five-process rerun.
|
|
147
|
+
|
|
148
|
+
Do not publish a Triton-only timing as a speedup. Each performance report pairs the same seeded workload with its same-machine SciPy CPU baseline.
|
|
149
|
+
|
|
150
|
+
For direct benchmark-script options, the underlying command writes complete JSONL evidence to a run-specific file and prints a compact table containing the backend, validation mode, shape, status, cold/warm latency, and exact-parity result:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
make benchmark
|
|
154
|
+
python tests/benchmark.py --backends scipy,public_cuda --output benchmark-results.jsonl
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The command exits nonzero if any invocation fails exact parity. The JSONL file retains replay diagnostics that the console table intentionally omits.
|
|
158
|
+
|
|
159
|
+
## Legacy CUDA implementation (`0.0.x`)
|
|
160
|
+
|
|
161
|
+
Release `0.0.6` contains the frozen, compiled plain-CUDA implementation. It is not the active default and should be pinned only when that legacy GPU path is explicitly required, including T4 (`sm_75`) execution:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
python -m pip install "torch-linear-assignment==0.0.6"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The extension needs a compatible CUDA build environment and must include `sm_75`; the project does not promise a portable T4 wheel. Pinning it is also workload-dependent: in the recorded T4 comparison, SciPy was faster through batch 50, while legacy CUDA was materially faster for the tested batch-208 transpose and direct cases.
|
|
168
|
+
|
|
169
|
+
For a private contributor comparison from a current checkout, the legacy extension can instead be built explicitly:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
TLA_BUILD_LEGACY_CUDA=1 python -m pip install -e . --no-build-isolation
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
This opt-in is not required for normal `0.1.0+` use and does not expose a public backend selector. For a clean cross-version benchmark, copy `tests/benchmark.py` outside the checkout first. In a separate baseline environment, `make install-legacy` installs the newest PyPI release matching `torch-linear-assignment<0.1.0`; the copied runner then measures its installed public CUDA backend. Pair those results with the same shapes, dtypes, seed, GPU, and validation mode from the current Triton run.
|
|
176
|
+
|
|
177
|
+
The following author-run batch-208 FP32 comparison uses the same `transpose / square / direct` ordering as the main table. Ratios above `1.0x` mean Triton is faster; values are rounded to one decimal, and `~` marks ratios derived from separately displayed legacy and current-package timings.
|
|
178
|
+
|
|
179
|
+
| GPU / timing | Legacy 0.0.6 (ms) | Triton (ms) | Legacy / Triton |
|
|
180
|
+
| ------------------ | -------------------- | ---------------- | ------------------------ |
|
|
181
|
+
| T4 (first\*) | 37.0 / 879.5 / 273.9 | — | — |
|
|
182
|
+
| T4 (warm) | 37.7 / 738.6 / 272.6 | — | — |
|
|
183
|
+
| L4 (first\*) | 37.9 / 648.5 / 274.5 | 1.0 / 7.6 / 4.3 | ~37.9x / 85.3x / 63.8x |
|
|
184
|
+
| L4 (warm) | 37.8 / 652.5 / 273.3 | 1.0 / 7.6 / 4.4 | ~37.8x / 85.9x / 62.1x |
|
|
185
|
+
| A100 (first\*) | 36.2 / 755.5 / 271.7 | 1.3 / 11.6 / 4.3 | ~27.8x / 65.1x / 63.2x |
|
|
186
|
+
| A100 (warm) | 36.2 / 731.3 / 271.6 | 1.3 / 11.6 / 4.4 | ~27.8x / 63.0x / 61.7x |
|
|
187
|
+
| H100 (first\*) | ? | ? | ? |
|
|
188
|
+
| H100 (warm) | ? | ? | ? |
|
|
189
|
+
| RTX 6000 (first\*) | 35.4 / 607.1 / 248.0 | 0.7 / 5.5 / 2.2 | ~50.6x / 110.4x / 112.7x |
|
|
190
|
+
| RTX 6000 (warm) | 35.4 / 607.6 / 249.3 | 0.5 / 5.4 / 2.2 | ~70.8x / 112.5x / 113.3x |
|
|
191
|
+
|
|
192
|
+
Triton is faster in all 30 paired warm cases on each measured supported GPU; the weakest displayed ratio is about `6.0x`. T4 has no Triton timing because it is below the supported compute capability, and H100 remains unmeasured. The `first*` caveat from the main table applies equally here.
|
|
193
|
+
|
|
194
|
+
## Acknowledgments
|
|
195
|
+
|
|
196
|
+
The `0.1.0+` pure-source Triton backend, GPU validation workflow, and benchmark revamp were implemented by [@Borda](https://github.com/Borda).
|
|
197
|
+
|
|
198
|
+
## Citation
|
|
199
|
+
|
|
200
|
+
The code was originally developed for the [HoTPP Benchmark](https://github.com/ivan-chai/hotpp-benchmark). If you use this code in your research project, please cite one of the following papers:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
@article{karpukhin2024hotppbenchmark,
|
|
204
|
+
title={HoTPP Benchmark: Are We Good at the Long Horizon Events Forecasting?},
|
|
205
|
+
author={Karpukhin, Ivan and Shipilov, Foma and Savchenko, Andrey},
|
|
206
|
+
journal={arXiv preprint arXiv:2406.14341},
|
|
207
|
+
year={2024},
|
|
208
|
+
url ={https://arxiv.org/abs/2406.14341}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@article{karpukhin2024detpp,
|
|
212
|
+
title={DeTPP: Leveraging Object Detection for Robust Long-Horizon Event Prediction},
|
|
213
|
+
author={Karpukhin, Ivan and Savchenko, Andrey},
|
|
214
|
+
journal={arXiv preprint arXiv:2408.13131},
|
|
215
|
+
year={2024},
|
|
216
|
+
url ={https://arxiv.org/abs/2408.13131}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
torch_linear_assignment/__init__.py,sha256=Dkb0lPgPrrtzMmsKxGpZsVhjKIwtUnoDdT71LArxBYM,188
|
|
2
|
+
torch_linear_assignment/_triton.py,sha256=0EPQpI793Lkq9mtJtcoDqVHjf4m6zCOsx3sp6dWpHlM,13154
|
|
3
|
+
torch_linear_assignment/assignment.py,sha256=MkeY2kGZNW7RPLePBA07RefwT_lWBczszeGXzJiENl0,5816
|
|
4
|
+
torch_linear_assignment-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
5
|
+
torch_linear_assignment-0.1.0.dist-info/METADATA,sha256=UxH4hrhjFwAhwBV-vnTOVgwS7oT4H7S7RFJf_oMKnuo,13960
|
|
6
|
+
torch_linear_assignment-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
torch_linear_assignment-0.1.0.dist-info/top_level.txt,sha256=eV_iv2yNOwDzuo6EUQVtdx-8Ar-HX03wRmnl6Ge1vGA,24
|
|
8
|
+
torch_linear_assignment-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
torch_linear_assignment
|