Skip to content

Commit 7927445

Browse files
authored
Merge branch 'dev' into codeql_update
2 parents ef028a0 + 02201b8 commit 7927445

4 files changed

Lines changed: 97 additions & 18 deletions

File tree

monai/networks/nets/swin_unetr.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def __init__(
9898
hyena_omega_0: float = 10.0,
9999
hyena_l_cache: int = 32,
100100
hyena_short_conv_fft_chunks: int = 0,
101+
use_flash_attention: bool = False,
101102
) -> None:
102103
"""
103104
Args:
@@ -149,6 +150,7 @@ def __init__(
149150
hyena_omega_0: SIREN frequency. Default 10.0 (stable).
150151
hyena_l_cache: SIREN coordinate-grid cache size per spatial dim.
151152
hyena_short_conv_fft_chunks: channel chunk size for the FFT short conv (0 = no chunking).
153+
use_flash_attention: use flash attention (scaled dot product attention) at inference.
152154
153155
Examples::
154156
@@ -224,6 +226,7 @@ def __init__(
224226
hyena_omega_0=hyena_omega_0,
225227
hyena_l_cache=hyena_l_cache,
226228
hyena_short_conv_fft_chunks=hyena_short_conv_fft_chunks,
229+
use_flash_attention=use_flash_attention,
227230
)
228231

229232
self.encoder1 = UnetrBasicBlock(
@@ -513,6 +516,7 @@ def __init__(
513516
qkv_bias: bool = False,
514517
attn_drop: float = 0.0,
515518
proj_drop: float = 0.0,
519+
use_flash_attention: bool = False,
516520
) -> None:
517521
"""
518522
Args:
@@ -522,12 +526,17 @@ def __init__(
522526
qkv_bias: add a learnable bias to query, key, value.
523527
attn_drop: attention dropout rate.
524528
proj_drop: dropout rate of output.
529+
use_flash_attention: if True, use ``torch.nn.functional.scaled_dot_product_attention`` for the
530+
windowed attention. Equivalent to the default path but faster at inference; only used when
531+
autograd is disabled (e.g. under ``torch.no_grad()`` or ``torch.inference_mode()``, not
532+
``eval()`` alone) and the module is not scripted.
525533
"""
526534

527535
super().__init__()
528536
self.dim = dim
529537
self.window_size = window_size
530538
self.num_heads = num_heads
539+
self.use_flash_attention = use_flash_attention
531540
head_dim = dim // num_heads
532541
self.scale = head_dim**-0.5
533542
mesh_args = torch.meshgrid.__kwdefaults__
@@ -584,12 +593,26 @@ def forward(self, x, mask):
584593
b, n, c = x.shape
585594
qkv = self.qkv(x).reshape(b, n, 3, self.num_heads, c // self.num_heads).permute(2, 0, 3, 1, 4)
586595
q, k, v = qkv[0], qkv[1], qkv[2]
587-
q = q * self.scale
588-
attn = q @ k.transpose(-2, -1)
589596
relative_position_bias = self.relative_position_bias_table[
590597
self.relative_position_index.clone()[:n, :n].reshape(-1) # type: ignore[operator]
591598
].reshape(n, n, -1)
592599
relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous()
600+
if self.use_flash_attention and not torch.jit.is_scripting() and not torch.is_grad_enabled():
601+
# additive bias combines the relative position bias and, for shifted windows, the attention mask
602+
if mask is not None:
603+
nw = mask.shape[0]
604+
bias = relative_position_bias.view(1, 1, self.num_heads, n, n) + mask.reshape(1, nw, 1, n, n)
605+
bias = bias.expand(b // nw, nw, self.num_heads, n, n).reshape(b, self.num_heads, n, n)
606+
else:
607+
bias = relative_position_bias.unsqueeze(0)
608+
x = torch.nn.functional.scaled_dot_product_attention(
609+
q, k, v, attn_mask=bias.to(q.dtype), dropout_p=0.0, scale=self.scale
610+
)
611+
x = x.transpose(1, 2).reshape(b, n, c)
612+
return self.proj_drop(self.proj(x))
613+
614+
q = q * self.scale
615+
attn = q @ k.transpose(-2, -1)
593616
attn = attn + relative_position_bias.unsqueeze(0)
594617
if mask is not None:
595618
nw = mask.shape[0]
@@ -628,6 +651,7 @@ def __init__(
628651
act_layer: str = "GELU",
629652
norm_layer: type[LayerNorm] = nn.LayerNorm,
630653
use_checkpoint: bool = False,
654+
use_flash_attention: bool = False,
631655
) -> None:
632656
"""
633657
Args:
@@ -643,6 +667,7 @@ def __init__(
643667
act_layer: activation layer.
644668
norm_layer: normalization layer.
645669
use_checkpoint: use gradient checkpointing for reduced memory usage.
670+
use_flash_attention: use flash attention (scaled dot product attention) at inference.
646671
"""
647672

648673
super().__init__()
@@ -660,6 +685,7 @@ def __init__(
660685
qkv_bias=qkv_bias,
661686
attn_drop=attn_drop,
662687
proj_drop=drop,
688+
use_flash_attention=use_flash_attention,
663689
)
664690

665691
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
@@ -924,6 +950,7 @@ def __init__(
924950
hyena_omega_0: float = 10.0,
925951
hyena_l_cache: int = 32,
926952
hyena_short_conv_fft_chunks: int = 0,
953+
use_flash_attention: bool = False,
927954
) -> None:
928955
"""
929956
Args:
@@ -946,6 +973,7 @@ def __init__(
946973
hyena_use_chunked_fft, hyena_use_fft_short_conv, hyena_omega_0, hyena_l_cache,
947974
hyena_short_conv_fft_chunks: forwarded to :class:`HyenaTransformerBlock`. See its
948975
docstring for semantics.
976+
use_flash_attention: use flash attention (scaled dot product attention) at inference.
949977
"""
950978

951979
super().__init__()
@@ -996,6 +1024,7 @@ def __init__(
9961024
drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path,
9971025
norm_layer=norm_layer,
9981026
use_checkpoint=use_checkpoint,
1027+
use_flash_attention=use_flash_attention,
9991028
)
10001029
for i in range(depth)
10011030
]
@@ -1079,6 +1108,7 @@ def __init__(
10791108
hyena_omega_0: float = 10.0,
10801109
hyena_l_cache: int = 32,
10811110
hyena_short_conv_fft_chunks: int = 0,
1111+
use_flash_attention: bool = False,
10821112
) -> None:
10831113
"""
10841114
Args:
@@ -1112,6 +1142,7 @@ def __init__(
11121142
hyena_use_chunked_fft, hyena_use_fft_short_conv, hyena_omega_0, hyena_l_cache,
11131143
hyena_short_conv_fft_chunks: HyenaND configuration. See
11141144
:class:`monai.networks.blocks.HyenaTransformerBlock` for semantics.
1145+
use_flash_attention: use flash attention (scaled dot product attention) at inference.
11151146
"""
11161147

11171148
super().__init__()
@@ -1193,6 +1224,7 @@ def __init__(
11931224
hyena_omega_0=hyena_omega_0,
11941225
hyena_l_cache=hyena_l_cache,
11951226
hyena_short_conv_fft_chunks=hyena_short_conv_fft_chunks,
1227+
use_flash_attention=use_flash_attention,
11961228
)
11971229
if i_layer == 0:
11981230
self.layers1.append(layer)

monai/transforms/croppad/array.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,24 @@ def compute_pad_width(self, spatial_shape: Sequence[int]) -> tuple[tuple[int, in
342342
return spatial_pad.compute_pad_width(spatial_shape)
343343

344344

345+
def _to_int_list(data: Sequence[int] | int | NdarrayOrTensor) -> list[int]:
346+
"""Coerce an ROI spec (scalar, sequence, tensor or ndarray) to a list of Python ints."""
347+
if isinstance(data, (str, bytes)):
348+
raise TypeError("ROI specs must be integers or sequences of integers, not strings.")
349+
return [int(i) for i in ensure_tuple(data)]
350+
351+
352+
def _broadcast_int_pair(
353+
a: Sequence[int] | int | NdarrayOrTensor, b: Sequence[int] | int | NdarrayOrTensor
354+
) -> tuple[list[int], list[int]]:
355+
"""Coerce a pair of ROI specs to two equal-length int lists, broadcasting a scalar to match."""
356+
list_a, list_b = _to_int_list(a), _to_int_list(b)
357+
n = max(len(list_a), len(list_b))
358+
if len(list_a) not in (1, n) or len(list_b) not in (1, n):
359+
raise ValueError(f"ROI specs must have matching lengths or be scalar, got {len(list_a)} and {len(list_b)}.")
360+
return (list_a * n if len(list_a) == 1 else list_a), (list_b * n if len(list_b) == 1 else list_b)
361+
362+
345363
class Crop(InvertibleTransform, LazyTransform):
346364
"""
347365
Perform crop operations on the input image.
@@ -379,31 +397,22 @@ def compute_slices(
379397
roi_slices: list of slices for each of the spatial dimensions.
380398
381399
"""
382-
roi_start_t: torch.Tensor
383-
384400
if roi_slices:
385401
if not all(s.step is None or s.step == 1 for s in roi_slices):
386402
raise ValueError(f"only slice steps of 1/None are currently supported, got {roi_slices}.")
387403
return ensure_tuple(roi_slices)
388404
else:
389405
if roi_center is not None and roi_size is not None:
390-
roi_center_t = convert_to_tensor(data=roi_center, dtype=torch.int16, wrap_sequence=True, device="cpu")
391-
roi_size_t = convert_to_tensor(data=roi_size, dtype=torch.int16, wrap_sequence=True, device="cpu")
392-
_zeros = torch.zeros_like(roi_center_t)
393-
half = torch.divide(roi_size_t, 2, rounding_mode="floor")
394-
roi_start_t = torch.maximum(roi_center_t - half, _zeros)
395-
roi_end_t = torch.maximum(roi_start_t + roi_size_t, roi_start_t)
406+
centers, sizes = _broadcast_int_pair(roi_center, roi_size)
407+
starts = [max(c - s // 2, 0) for c, s in zip(centers, sizes)]
408+
ends = [st + s for st, s in zip(starts, sizes)]
396409
else:
397410
if roi_start is None or roi_end is None:
398411
raise ValueError("please specify either roi_center, roi_size or roi_start, roi_end.")
399-
roi_start_t = convert_to_tensor(data=roi_start, dtype=torch.int16, wrap_sequence=True)
400-
roi_start_t = torch.maximum(roi_start_t, torch.zeros_like(roi_start_t))
401-
roi_end_t = convert_to_tensor(data=roi_end, dtype=torch.int16, wrap_sequence=True)
402-
roi_end_t = torch.maximum(roi_end_t, roi_start_t)
403-
# convert to slices (accounting for 1d)
404-
if roi_start_t.numel() == 1:
405-
return ensure_tuple([slice(int(roi_start_t.item()), int(roi_end_t.item()))])
406-
return ensure_tuple([slice(int(s), int(e)) for s, e in zip(roi_start_t.tolist(), roi_end_t.tolist())])
412+
starts, ends = _broadcast_int_pair(roi_start, roi_end)
413+
starts = [max(s, 0) for s in starts]
414+
# clamp each end to its own start so no slice has negative width
415+
return ensure_tuple(slice(s, max(e, s)) for s, e in zip(starts, ends))
407416

408417
def __call__( # type: ignore[override]
409418
self, img: torch.Tensor, slices: tuple[slice, ...], lazy: bool | None = None

tests/networks/nets/test_swin_unetr.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ def test_invalid_input_shape(self):
111111
with self.assertRaises(ValueError):
112112
net_2d(torch.randn(1, 1, 48, 33)) # 33 is not divisible by 32
113113

114+
@skipUnless(has_einops, "Requires einops")
115+
def test_flash_attention(self):
116+
input_param = {"in_channels": 1, "out_channels": 2, "feature_size": 12, "spatial_dims": 3}
117+
net_ref = SwinUNETR(use_flash_attention=False, **input_param).double()
118+
net_flash = SwinUNETR(use_flash_attention=True, **input_param).double()
119+
net_flash.load_state_dict(net_ref.state_dict())
120+
x = torch.randn(1, 1, 64, 64, 64, dtype=torch.float64)
121+
with eval_mode(net_ref, net_flash):
122+
ref = net_ref.swinViT(x, net_ref.normalize)
123+
out = net_flash.swinViT(x, net_flash.normalize)
124+
for a, b in zip(ref, out, strict=True):
125+
assert_allclose(a, b, atol=1e-6, rtol=1e-6, type_test=False)
126+
114127
def test_patch_merging(self):
115128
dim = 10
116129
t = PatchMerging(dim)(torch.zeros((1, 21, 20, 20, dim)))

tests/transforms/test_center_spatial_crop.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
import unittest
1515

1616
import numpy as np
17+
import torch
1718
from parameterized import parameterized
1819

20+
from monai.data.meta_obj import get_track_meta, set_track_meta
1921
from monai.transforms import CenterSpatialCrop
22+
from monai.transforms.croppad.array import Crop
2023
from tests.croppers import CropTest
2124

2225
TEST_SHAPES = [
@@ -50,6 +53,28 @@ def test_value(self, input_param, input_arr, expected_arr):
5053
def test_pending_ops(self, input_param, input_shape, _, align_corners):
5154
self.crop_test_pending_ops(input_param, input_shape, align_corners)
5255

56+
def test_compute_slices_broadcast(self):
57+
self.assertEqual(Crop.compute_slices(roi_center=2, roi_size=(4, 6, 8)), (slice(0, 4), slice(0, 6), slice(0, 8)))
58+
self.assertEqual(Crop.compute_slices(roi_start=1, roi_end=(3, 5, 7)), (slice(1, 3), slice(1, 5), slice(1, 7)))
59+
with self.assertRaises(ValueError):
60+
Crop.compute_slices(roi_center=(2, 3), roi_size=(4, 5, 6))
61+
with self.assertRaises(ValueError):
62+
Crop.compute_slices(roi_start=(1, 2), roi_end=(3, 5, 7))
63+
with self.assertRaises(TypeError):
64+
Crop.compute_slices(roi_center="10", roi_size=(4, 6))
65+
66+
def test_torch_compile(self):
67+
prev_track_meta = get_track_meta()
68+
set_track_meta(False)
69+
try:
70+
# eager backend traces the transform without needing the Inductor C++ compiler
71+
cropper = torch.compile(CenterSpatialCrop(roi_size=(1, 16, 16)), backend="eager")
72+
img = torch.rand(1, 1, 32, 32, dtype=torch.float32)
73+
self.assertEqual(tuple(cropper(img).shape), (1, 1, 16, 16))
74+
finally:
75+
set_track_meta(prev_track_meta)
76+
torch._dynamo.reset()
77+
5378

5479
if __name__ == "__main__":
5580
unittest.main()

0 commit comments

Comments
 (0)