Skip to content

Commit 7c23098

Browse files
aymuos15ericspod
andauthored
SwinUNETR: optional flash attention (scaled_dot_product_attention) in WindowAttention (#8977)
Fixes #8973 . ### Description SwinUNETR's WindowAttention builds the full (nWindows*heads, N, N) score matrix by hand before softmax. This adds an opt-in use_flash_attention flag (default False, so existing behaviour is unchanged) that routes attention through torch.nn.functional.scaled_dot_product_attention, folding the relative position bias and, for shifted windows, the attention mask into one additive attn_mask cast to the query dtype. This mirrors the flash-attention option already in MONAI's SelfAttention, CrossAttention and CABlock. Measured on the SwinUNETR encoder (SwinViT) forward, inference, single GPU, best-of-5. Float32 output matches the default path to within 3e-6 and is bit-exact in float64, verified across 2D and 3D, batch sizes 1 to 4, and non-cubic inputs. | ROI | dtype | default | flash | speedup | |---|---|---|---|---| | 96^3 | fp32 | 12.59 ms | 8.34 ms | 1.51x | | 96^3 | bf16 | 10.71 ms | 5.46 ms | 1.96x | | 128^3 | fp32 | 34.27 ms | 21.74 ms | 1.58x | | 128^3 | bf16 | 29.45 ms | 14.12 ms | 2.09x | | 160^3 | fp32 | 59.05 ms | 37.66 ms | 1.57x | | 160^3 | bf16 | 51.25 ms | 25.33 ms | 2.02x | The flag is threaded through SwinTransformer, BasicLayer and SwinTransformerBlock to WindowAttention, exactly as use_v2 and use_checkpoint are. No parameters or buffers change, so pretrained weights load unchanged. The flash path is used only when autograd is disabled and the module is not scripted, so training and TorchScript keep the original path byte-for-byte; this is a deliberate choice to leave training numerics untouched. ### Types of changes - [x] Non-breaking change (fix or new feature that would not break existing functionality). - [x] New tests added to cover the changes. - [x] In-line docstrings updated. --------- Signed-off-by: Soumya Snigdha Kundu <soumya_snigdha.kundu@kcl.ac.uk> Signed-off-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com> Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
1 parent 605611b commit 7c23098

2 files changed

Lines changed: 47 additions & 2 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)

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)))

0 commit comments

Comments
 (0)