deformops 1.0.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.
deformops/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ r"""
2
+ DeformOps
3
+ =========
4
+
5
+ Deformable operations for PyTorch, implemented in C++ and CUDA.
6
+ """
7
+
8
+ __version__ = "0.0.3"
deformops/_build.py ADDED
@@ -0,0 +1,64 @@
1
+ r"""
2
+ C++/CUDA extensions for deformable operations.
3
+ """
4
+
5
+ import functools
6
+ import pathlib
7
+ import typing
8
+ import importlib.resources
9
+ import torch.utils.cpp_extension
10
+
11
+ __all__: list[str] = []
12
+
13
+
14
+ def load_extension(
15
+ name: str,
16
+ /,
17
+ *,
18
+ debug: bool = False,
19
+ with_cuda: bool = True,
20
+ extra_cflags: typing.Iterable[str] = (),
21
+ extra_cuda_cflags: typing.Iterable[str] = (),
22
+ extra_sources: typing.Iterable[str | pathlib.Path] = (),
23
+ extra_include_paths: typing.Iterable[str | pathlib.Path] = (),
24
+ ) -> None:
25
+ r"""
26
+ Build an extension just-in-time (JIT) using the provided arguments.
27
+ """
28
+
29
+ root = importlib.resources.files("deformops.include")
30
+ opt_level = "0" if debug else "2"
31
+ sources = [root / f"{name}.cpp"] + [root / p for p in extra_sources]
32
+ extra_include_paths = [root / p for p in extra_include_paths]
33
+ extra_cflags = [
34
+ "-fdiagnostics-color=always",
35
+ "-std=c++17",
36
+ "-DPy_LIMITED_API=0x03012000",
37
+ f"-O{opt_level}",
38
+ *extra_cflags,
39
+ ]
40
+ extra_cuda_cflags = [
41
+ "--std=c++17",
42
+ f"-O{opt_level}",
43
+ *extra_cuda_cflags,
44
+ ]
45
+
46
+ if with_cuda:
47
+ # if CUDA_HOME is None or not pathlib.Path(CUDA_HOME).is_dir():
48
+ # msg = f"CUDA is not available. Found {CUDA_HOME=}"
49
+ # raise RuntimeError(msg)
50
+ sources.append(root / "cuda" / f"{name}.cu")
51
+ extra_include_paths.append(root / "cuda")
52
+
53
+ torch.utils.cpp_extension.load(
54
+ name=name,
55
+ with_cuda=with_cuda,
56
+ sources=list(map(str, sources)),
57
+ extra_include_paths=list(map(str, extra_include_paths)),
58
+ extra_cflags=extra_cflags,
59
+ extra_cuda_cflags=extra_cuda_cflags,
60
+ keep_intermediates=False,
61
+ is_python_module=False,
62
+ is_standalone=False,
63
+ verbose=True,
64
+ )
deformops/_factors.py ADDED
@@ -0,0 +1,13 @@
1
+ def find_factors(n: int) -> list[int]:
2
+ r"""
3
+ Simple utility to find factors of a number.
4
+ """
5
+ if n < 0:
6
+ msg = f"Expected a positive integer. Got: {n}!"
7
+ raise ValueError(msg)
8
+ result = set()
9
+ for i in range(1, int(n**0.5) + 1):
10
+ div, mod = divmod(n, i)
11
+ if mod == 0:
12
+ result |= {i, div}
13
+ return sorted(result)
deformops/benchmark.py ADDED
@@ -0,0 +1,24 @@
1
+ import torch.cuda
2
+
3
+
4
+ def measure_speed(func, inputs, *, warmup: int, iterations: int) -> float:
5
+ """
6
+ Utility for measuring execution time in ms.
7
+ """
8
+ tic = torch.cuda.Event(enable_timing=True)
9
+ toc = torch.cuda.Event(enable_timing=True)
10
+
11
+ # Warmup
12
+ for _ in range(warmup):
13
+ func(*inputs)
14
+
15
+ torch.cuda.synchronize()
16
+ tic.record()
17
+
18
+ for _ in range(iterations):
19
+ func(*inputs)
20
+
21
+ toc.record()
22
+ torch.cuda.synchronize()
23
+
24
+ return tic.elapsed_time(toc) / iterations
deformops/deform2d.py ADDED
@@ -0,0 +1,517 @@
1
+ r"""
2
+ Deform2d Op
3
+ ===========
4
+
5
+ Implements the multi-scale deformable sampling operator.
6
+ """
7
+
8
+ import functools
9
+ import typing
10
+ import warnings
11
+
12
+ import torch
13
+ import torch.fx
14
+ import torch.nn
15
+ from torch import Tensor
16
+ from torch.autograd.function import FunctionCtx, once_differentiable
17
+
18
+ from ._build import load_extension, load_extension
19
+ from ._factors import find_factors
20
+
21
+ __all__ = [
22
+ "backward",
23
+ "forward",
24
+ "setup_context",
25
+ ]
26
+
27
+ OP_NAME = "deform2d"
28
+ OP_LIBRARY: typing.Final[typing.LiteralString] = "deformops::deform2d"
29
+
30
+ load_extension(
31
+ OP_NAME,
32
+ extra_cuda_cflags=[
33
+ "-DCUDA_HAS_FP16=1",
34
+ "-DCUDA_HAS_BF16=1",
35
+ "-U__CUDA_NO_HALF_OPERATORS__",
36
+ "-U__CUDA_NO_HALF_CONVERSIONS__",
37
+ "-U__CUDA_NO_HALF2_OPERATORS__",
38
+ "--use_fast_math",
39
+ ],
40
+ )
41
+ forward_op = typing.cast(
42
+ typing.Callable[..., Tensor],
43
+ torch.ops.deformops.deform2d_forward, # type: ignore[attr-defined]
44
+ )
45
+ backward_op = typing.cast(
46
+ typing.Callable[..., tuple[Tensor, Tensor]],
47
+ torch.ops.deformops.deform2d_backward.default, # type: ignore[attr-defined]
48
+ )
49
+
50
+ # ----------------- #
51
+ # R E F E R E N C E #
52
+ # ----------------- #
53
+
54
+
55
+ def reference(
56
+ input: Tensor,
57
+ offset: Tensor,
58
+ mask: Tensor,
59
+ kernel_h: int,
60
+ kernel_w: int,
61
+ stride_h: int,
62
+ stride_w: int,
63
+ pad_h: int,
64
+ pad_w: int,
65
+ dilation_h: int,
66
+ dilation_w: int,
67
+ groups: int,
68
+ group_channels: int,
69
+ offset_scale: int,
70
+ remove_center: bool = False,
71
+ ) -> Tensor:
72
+ if remove_center:
73
+ msg = f"Keyword argument {remove_center=} is not supported!"
74
+ raise NotImplementedError(msg)
75
+ input = torch.nn.functional.pad(input, [0, 0, pad_h, pad_h, pad_w, pad_w])
76
+ N_, H_IN, W_IN, _ = input.shape
77
+ _, H_OUT, W_OUT, _ = offset.shape
78
+
79
+ kernel_size = (kernel_h, kernel_w)
80
+ dilation = (dilation_h, dilation_w)
81
+ stride = (stride_h, stride_w)
82
+
83
+ ref = _get_reference_points(
84
+ input.shape,
85
+ kernel_size,
86
+ dilation,
87
+ stride,
88
+ device=input.device,
89
+ dtype=input.dtype,
90
+ )
91
+ grid = _generate_dilation_grids(
92
+ input.shape,
93
+ kernel_size,
94
+ dilation,
95
+ groups,
96
+ device=input.device,
97
+ dtype=input.dtype,
98
+ )
99
+ spatial_norm = (
100
+ torch.tensor([W_IN, H_IN])
101
+ .reshape(1, 1, 1, 2)
102
+ .repeat(1, 1, 1, groups * kernel_h * kernel_w)
103
+ .to(input.device)
104
+ )
105
+
106
+ sampling_locations = (ref + grid * offset_scale).repeat(N_, 1, 1, 1, 1).flatten(
107
+ 3, 4
108
+ ) + offset * offset_scale / spatial_norm
109
+
110
+ P_ = kernel_h * kernel_w
111
+ sampling_grids = 2 * sampling_locations - 1
112
+ input_ = (
113
+ input.view(N_, H_IN * W_IN, groups * group_channels)
114
+ .transpose(1, 2)
115
+ .reshape(N_ * groups, group_channels, H_IN, W_IN)
116
+ )
117
+ sampling_grid_ = (
118
+ sampling_grids.view(N_, H_OUT * W_OUT, groups, P_, 2)
119
+ .transpose(1, 2)
120
+ .flatten(0, 1)
121
+ )
122
+ sampling_input_ = torch.nn.functional.grid_sample(
123
+ input_,
124
+ sampling_grid_,
125
+ mode="bilinear",
126
+ padding_mode="zeros",
127
+ align_corners=False,
128
+ )
129
+ mask = (
130
+ mask.view(N_, H_OUT * W_OUT, groups, P_)
131
+ .transpose(1, 2)
132
+ .reshape(N_ * groups, 1, H_OUT * W_OUT, P_)
133
+ )
134
+ output = (
135
+ (sampling_input_ * mask)
136
+ .sum(-1)
137
+ .view(N_, groups * group_channels, H_OUT * W_OUT)
138
+ )
139
+
140
+ return output.transpose(1, 2).reshape(N_, H_OUT, W_OUT, -1).contiguous()
141
+
142
+
143
+ def _get_reference_points(
144
+ spatial_shapes: tuple[int, int, int, int] | torch.Size,
145
+ kernel_size: tuple[int, int],
146
+ dilation: tuple[int, int],
147
+ stride: tuple[int, int],
148
+ *,
149
+ dtype: torch.dtype = torch.float32,
150
+ device: torch.device | torch.types.Device | str = None,
151
+ ) -> Tensor:
152
+ K_H, K_W = kernel_size
153
+ D_H, D_W = dilation
154
+ S_H, S_W = stride
155
+
156
+ _, H_, W_, _ = spatial_shapes
157
+ H_OUT = (H_ - (D_H * (K_H - 1) + 1)) // S_H + 1
158
+ W_OUT = (W_ - (D_W * (K_W - 1) + 1)) // S_W + 1
159
+
160
+ ref_y, ref_x = torch.meshgrid(
161
+ torch.linspace(
162
+ (D_H * (K_H - 1)) // 2 + 0.5,
163
+ (D_H * (K_H - 1)) // 2 + 0.5 + (H_OUT - 1) * S_H,
164
+ H_OUT,
165
+ dtype=dtype,
166
+ device=device,
167
+ ),
168
+ torch.linspace(
169
+ (D_W * (K_W - 1)) // 2 + 0.5,
170
+ (D_W * (K_W - 1)) // 2 + 0.5 + (W_OUT - 1) * S_W,
171
+ W_OUT,
172
+ dtype=dtype,
173
+ device=device,
174
+ ),
175
+ indexing="ij",
176
+ )
177
+ ref_y = ref_y.reshape(-1)[None] / H_
178
+ ref_x = ref_x.reshape(-1)[None] / W_
179
+
180
+ return torch.stack((ref_x, ref_y), -1).reshape(1, H_OUT, W_OUT, 1, 2)
181
+
182
+
183
+ def _generate_dilation_grids(
184
+ spatial_shapes: tuple[int, int, int, int] | torch.Size,
185
+ kernel_size: tuple[int, int],
186
+ dilation: tuple[int, int],
187
+ groups: int,
188
+ *,
189
+ dtype: torch.dtype = torch.float32,
190
+ device: torch.device | torch.types.Device | str = None,
191
+ ) -> Tensor:
192
+ K_H, K_W = kernel_size
193
+ D_H, D_W = dilation
194
+ _, H_, W_, _ = spatial_shapes
195
+ points_list = []
196
+ x, y = torch.meshgrid(
197
+ torch.linspace(
198
+ -((D_W * (K_W - 1)) // 2),
199
+ -((D_W * (K_W - 1)) // 2) + (K_W - 1) * D_W,
200
+ K_W,
201
+ dtype=dtype,
202
+ device=device,
203
+ ),
204
+ torch.linspace(
205
+ -((D_H * (K_H - 1)) // 2),
206
+ -((D_H * (K_H - 1)) // 2) + (K_H - 1) * D_H,
207
+ K_H,
208
+ dtype=dtype,
209
+ device=device,
210
+ ),
211
+ indexing="ij",
212
+ )
213
+
214
+ points_list.extend([x / W_, y / H_])
215
+ grid = (
216
+ torch.stack(points_list, -1)
217
+ .reshape(-1, 1, 2)
218
+ .repeat(1, groups, 1)
219
+ .permute(1, 0, 2)
220
+ )
221
+ return grid.reshape(1, 1, 1, groups * K_H * K_W, 2)
222
+
223
+
224
+ # ------------- #
225
+ # T U N I N G S #
226
+ # ------------- #
227
+
228
+
229
+ @functools.cache
230
+ def _forward_stridethread(B: int, H: int, W: int, G: int, C: int) -> tuple[int, int]:
231
+ r"""
232
+ Heuristic for choosing the forward stride and number of threads.
233
+ """
234
+ d_stride = 8
235
+ multiplier = 1
236
+ for m in find_factors(B * H * W):
237
+ if m <= 64 and (m * G * C // d_stride) <= 512: # noqa: PLR2004
238
+ multiplier = m
239
+ n_thread = multiplier * G * C // d_stride
240
+ # n_block = (B * H * W + n_thread - 1) // n_thread
241
+ return d_stride, n_thread
242
+
243
+
244
+ @functools.cache
245
+ def _backward_stridethread(B: int, H: int, W: int, G: int, C: int) -> tuple[int, int]:
246
+ """
247
+ Heuristic for choosing the backward stride and number of threads.
248
+ """
249
+ d_stride = 2 if C >= 64 else 1 # noqa: PLR2004
250
+ multiplier = 1
251
+ for m in find_factors(B * H * W):
252
+ if m <= 64 and (m * G * C // d_stride) <= 256: # noqa: PLR2004
253
+ multiplier = m
254
+ n_thread = multiplier * G * C // d_stride
255
+ # n_block = (B * H * W + n_thread - 1) // n_thread
256
+ return d_stride, n_thread
257
+
258
+
259
+ # ------------- #
260
+ # F O R W A R D #
261
+ # ------------- #
262
+
263
+
264
+ def forward(
265
+ value: Tensor, # N, H, W, C
266
+ offset_mask: Tensor, # N, H, W, 2*G*K
267
+ kernel_h: int,
268
+ kernel_w: int,
269
+ stride_h: int,
270
+ stride_w: int,
271
+ pad_h: int,
272
+ pad_w: int,
273
+ dilation_h: int,
274
+ dilation_w: int,
275
+ group: int,
276
+ group_dims: int,
277
+ offset_scale: float,
278
+ im2col_step: int,
279
+ remove_center: bool = False,
280
+ softmax: bool = False,
281
+ ) -> Tensor:
282
+ assert forward_op is not None, "Deformable ops not compiled"
283
+
284
+ fwd_stride, fwd_block_thread = _forward_stridethread(
285
+ *value.shape[:3], group, group_dims
286
+ )
287
+
288
+ with torch.autocast("cuda", enabled=False):
289
+ return forward_op(
290
+ value,
291
+ offset_mask,
292
+ kernel_h,
293
+ kernel_w,
294
+ stride_h,
295
+ stride_w,
296
+ pad_h,
297
+ pad_w,
298
+ dilation_h,
299
+ dilation_w,
300
+ group,
301
+ group_dims,
302
+ offset_scale,
303
+ im2col_step,
304
+ remove_center,
305
+ fwd_stride,
306
+ fwd_block_thread,
307
+ softmax,
308
+ )
309
+
310
+
311
+ # --------------- #
312
+ # A U T O G R A D #
313
+ # --------------- #
314
+ class Deform2dContext(FunctionCtx):
315
+ r"""
316
+ Dummy class that defines the function context for autograd. Instantiation will
317
+ always yield the base class :class:`FunctionCtx`.
318
+ """
319
+
320
+ def __new__(cls, *args, **kwargs) -> FunctionCtx:
321
+ return FunctionCtx(*args, **kwargs)
322
+
323
+ kernel_h: int
324
+ kernel_w: int
325
+ stride_h: int
326
+ stride_w: int
327
+ pad_h: int
328
+ pad_w: int
329
+ dilation_h: int
330
+ dilation_w: int
331
+ group: int
332
+ group_dims: int
333
+ offset_scale: float
334
+ im2col_step: int
335
+ remove_center: bool
336
+ backward_d_stride: int
337
+ backward_block_thread: int
338
+ softmax: bool
339
+ saved_tensors: tuple[Tensor, Tensor]
340
+
341
+
342
+ def setup_context(
343
+ ctx: Deform2dContext,
344
+ inputs: tuple[typing.Any, ...],
345
+ output: Tensor, # noqa: ARG001
346
+ ) -> None:
347
+ (
348
+ value,
349
+ offset_mask,
350
+ kernel_h,
351
+ kernel_w,
352
+ stride_h,
353
+ stride_w,
354
+ pad_h,
355
+ pad_w,
356
+ dilation_h,
357
+ dilation_w,
358
+ group,
359
+ group_dims,
360
+ offset_scale,
361
+ im2col_step,
362
+ remove_center,
363
+ fwd_stride,
364
+ fwd_block_thread,
365
+ softmax,
366
+ ) = inputs
367
+ bck_stride, bck_block_thread = _backward_stridethread(
368
+ *value.shape[:3], group, group_dims
369
+ )
370
+
371
+ ctx.kernel_h = kernel_h
372
+ ctx.kernel_w = kernel_w
373
+ ctx.stride_h = stride_h
374
+ ctx.stride_w = stride_w
375
+ ctx.pad_h = pad_h
376
+ ctx.pad_w = pad_w
377
+ ctx.dilation_h = dilation_h
378
+ ctx.dilation_w = dilation_w
379
+ ctx.group = group
380
+ ctx.group_dims = group_dims
381
+ ctx.offset_scale = offset_scale
382
+ ctx.im2col_step = im2col_step
383
+ ctx.remove_center = remove_center
384
+ ctx.backward_d_stride = bck_stride
385
+ ctx.backward_block_thread = bck_block_thread
386
+ ctx.softmax = softmax
387
+ ctx.save_for_backward(value, offset_mask)
388
+
389
+
390
+ @once_differentiable
391
+ def backward(
392
+ ctx: Deform2dContext, grad: Tensor
393
+ ) -> tuple[
394
+ Tensor,
395
+ Tensor,
396
+ None,
397
+ None,
398
+ None,
399
+ None,
400
+ None,
401
+ None,
402
+ None,
403
+ None,
404
+ None,
405
+ None,
406
+ None,
407
+ None,
408
+ None,
409
+ None,
410
+ None,
411
+ None,
412
+ ]:
413
+ assert backward_op is not None, "Deformable ops not compiled"
414
+
415
+ input, offset_mask = ctx.saved_tensors
416
+ with torch.autocast("cuda", enabled=False):
417
+ grad_input, grad_offset_mask = backward_op(
418
+ input.float(),
419
+ offset_mask.float(),
420
+ ctx.kernel_h,
421
+ ctx.kernel_w,
422
+ ctx.stride_h,
423
+ ctx.stride_w,
424
+ ctx.pad_h,
425
+ ctx.pad_w,
426
+ ctx.dilation_h,
427
+ ctx.dilation_w,
428
+ ctx.group,
429
+ ctx.group_dims,
430
+ ctx.offset_scale,
431
+ ctx.im2col_step,
432
+ grad.float().contiguous(),
433
+ ctx.remove_center,
434
+ ctx.backward_d_stride,
435
+ ctx.backward_block_thread,
436
+ ctx.softmax,
437
+ )
438
+ return (
439
+ grad_input.type_as(input),
440
+ grad_offset_mask.type_as(offset_mask),
441
+ None,
442
+ None,
443
+ None,
444
+ None,
445
+ None,
446
+ None,
447
+ None,
448
+ None,
449
+ None,
450
+ None,
451
+ None,
452
+ None,
453
+ None,
454
+ None,
455
+ None,
456
+ None,
457
+ )
458
+
459
+
460
+ torch.library.register_autograd(
461
+ f"{OP_LIBRARY}_forward",
462
+ backward,
463
+ setup_context=setup_context,
464
+ )
465
+
466
+ # --------------------- #
467
+ # F A K E T E N S O R S #
468
+ # --------------------- #
469
+
470
+
471
+ @torch.library.register_fake(f"{OP_LIBRARY}_forward")
472
+ def _(
473
+ value: Tensor,
474
+ offset_mask: Tensor, # noqa: ARG001
475
+ kernel_h: int, # noqa: ARG001
476
+ kernel_w: int, # noqa: ARG001
477
+ stride_h: int, # noqa: ARG001
478
+ stride_w: int, # noqa: ARG001
479
+ pad_h: int, # noqa: ARG001
480
+ pad_w: int, # noqa: ARG001
481
+ dilation_h: int, # noqa: ARG001
482
+ dilation_w: int, # noqa: ARG001
483
+ group: int, # noqa: ARG001
484
+ group_dims: int, # noqa: ARG001
485
+ offset_scale: float, # noqa: ARG001
486
+ im2col_step: int, # noqa: ARG001
487
+ remove_center: bool, # noqa: ARG001
488
+ fwd_stride: int, # noqa: ARG001
489
+ fwd_block_thread: int, # noqa: ARG001
490
+ softmax: bool, # noqa: ARG001
491
+ ) -> Tensor:
492
+ return value.new_empty(*value.shape)
493
+
494
+
495
+ @torch.library.register_fake(f"{OP_LIBRARY}_backward")
496
+ def _(
497
+ value: Tensor,
498
+ offset_mask: Tensor,
499
+ kernel_h: int, # noqa: ARG001
500
+ kernel_w: int, # noqa: ARG001
501
+ stride_h: int, # noqa: ARG001
502
+ stride_w: int, # noqa: ARG001
503
+ pad_h: int, # noqa: ARG001
504
+ pad_w: int, # noqa: ARG001
505
+ dilation_h: int, # noqa: ARG001
506
+ dilation_w: int, # noqa: ARG001
507
+ group: int, # noqa: ARG001
508
+ group_dims: int, # noqa: ARG001
509
+ offset_scale: float, # noqa: ARG001
510
+ im2col_step: int, # noqa: ARG001
511
+ grad: Tensor, # noqa: ARG001
512
+ remove_center: bool, # noqa: ARG001
513
+ fwd_stride: int, # noqa: ARG001
514
+ fwd_block_thread: int, # noqa: ARG001
515
+ softmax: bool, # noqa: ARG001
516
+ ) -> tuple[Tensor, Tensor]:
517
+ return value.new_empty(*value.shape), offset_mask.new_empty(*offset_mask.shape)