Skip to content

Commit 5c658e2

Browse files
committed
Add optional flash attention to SwinUNETR WindowAttention
Add a use_flash_attention flag (default False) threaded from SwinUNETR through SwinTransformer, BasicLayer and SwinTransformerBlock to WindowAttention. When enabled and autograd is disabled (and not scripting), attention is computed with torch.nn.functional.scaled_dot_product_attention, folding the relative position bias and the shifted-window mask into a single additive attn_mask cast to the query dtype. The fused kernel avoids materializing the score matrix. Output matches the default path; training, scripting and the default path are unchanged. This mirrors the flash-attention option already in MONAI's SelfAttention, CrossAttention and CABlock. Signed-off-by: Soumya Snigdha Kundu <soumya_snigdha.kundu@kcl.ac.uk>
1 parent f1dcac4 commit 5c658e2

1 file changed

Lines changed: 33 additions & 2 deletions

File tree

monai/networks/nets/swin_unetr.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def __init__(
8484
spatial_dims: int = 3,
8585
downsample: str | nn.Module = "merging",
8686
use_v2: bool = False,
87+
use_flash_attention: bool = False,
8788
) -> None:
8889
"""
8990
Args:
@@ -110,6 +111,7 @@ def __init__(
110111
user-specified `nn.Module` following the API defined in :py:class:`monai.networks.nets.PatchMerging`.
111112
The default is currently `"merging"` (the original version defined in v0.9.0).
112113
use_v2: using swinunetr_v2, which adds a residual convolution block at the beggining of each swin stage.
114+
use_flash_attention: use flash attention (scaled dot product attention) at inference.
113115
114116
Examples::
115117
@@ -170,6 +172,7 @@ def __init__(
170172
spatial_dims=spatial_dims,
171173
downsample=look_up_option(downsample, MERGING_MODE) if isinstance(downsample, str) else downsample,
172174
use_v2=use_v2,
175+
use_flash_attention=use_flash_attention,
173176
)
174177

175178
self.encoder1 = UnetrBasicBlock(
@@ -457,6 +460,7 @@ def __init__(
457460
qkv_bias: bool = False,
458461
attn_drop: float = 0.0,
459462
proj_drop: float = 0.0,
463+
use_flash_attention: bool = False,
460464
) -> None:
461465
"""
462466
Args:
@@ -466,12 +470,16 @@ def __init__(
466470
qkv_bias: add a learnable bias to query, key, value.
467471
attn_drop: attention dropout rate.
468472
proj_drop: dropout rate of output.
473+
use_flash_attention: if True, use ``torch.nn.functional.scaled_dot_product_attention`` for the
474+
windowed attention. Equivalent to the default path but faster at inference; only used when
475+
autograd is disabled and the module is not scripted.
469476
"""
470477

471478
super().__init__()
472479
self.dim = dim
473480
self.window_size = window_size
474481
self.num_heads = num_heads
482+
self.use_flash_attention = use_flash_attention
475483
head_dim = dim // num_heads
476484
self.scale = head_dim**-0.5
477485
mesh_args = torch.meshgrid.__kwdefaults__
@@ -528,12 +536,26 @@ def forward(self, x, mask):
528536
b, n, c = x.shape
529537
qkv = self.qkv(x).reshape(b, n, 3, self.num_heads, c // self.num_heads).permute(2, 0, 3, 1, 4)
530538
q, k, v = qkv[0], qkv[1], qkv[2]
531-
q = q * self.scale
532-
attn = q @ k.transpose(-2, -1)
533539
relative_position_bias = self.relative_position_bias_table[
534540
self.relative_position_index.clone()[:n, :n].reshape(-1) # type: ignore[operator]
535541
].reshape(n, n, -1)
536542
relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous()
543+
if self.use_flash_attention and not torch.jit.is_scripting() and not torch.is_grad_enabled():
544+
# additive bias combines the relative position bias and, for shifted windows, the attention mask
545+
if mask is not None:
546+
nw = mask.shape[0]
547+
bias = relative_position_bias.view(1, 1, self.num_heads, n, n) + mask.view(1, nw, 1, n, n)
548+
bias = bias.expand(b // nw, nw, self.num_heads, n, n).reshape(b, self.num_heads, n, n)
549+
else:
550+
bias = relative_position_bias.unsqueeze(0)
551+
x = torch.nn.functional.scaled_dot_product_attention(
552+
q, k, v, attn_mask=bias.to(q.dtype), dropout_p=0.0, scale=self.scale
553+
)
554+
x = x.transpose(1, 2).reshape(b, n, c)
555+
return self.proj_drop(self.proj(x))
556+
557+
q = q * self.scale
558+
attn = q @ k.transpose(-2, -1)
537559
attn = attn + relative_position_bias.unsqueeze(0)
538560
if mask is not None:
539561
nw = mask.shape[0]
@@ -572,6 +594,7 @@ def __init__(
572594
act_layer: str = "GELU",
573595
norm_layer: type[LayerNorm] = nn.LayerNorm,
574596
use_checkpoint: bool = False,
597+
use_flash_attention: bool = False,
575598
) -> None:
576599
"""
577600
Args:
@@ -587,6 +610,7 @@ def __init__(
587610
act_layer: activation layer.
588611
norm_layer: normalization layer.
589612
use_checkpoint: use gradient checkpointing for reduced memory usage.
613+
use_flash_attention: use flash attention (scaled dot product attention) at inference.
590614
"""
591615

592616
super().__init__()
@@ -604,6 +628,7 @@ def __init__(
604628
qkv_bias=qkv_bias,
605629
attn_drop=attn_drop,
606630
proj_drop=drop,
631+
use_flash_attention=use_flash_attention,
607632
)
608633

609634
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
@@ -856,6 +881,7 @@ def __init__(
856881
norm_layer: type[LayerNorm] = nn.LayerNorm,
857882
downsample: nn.Module | None = None,
858883
use_checkpoint: bool = False,
884+
use_flash_attention: bool = False,
859885
) -> None:
860886
"""
861887
Args:
@@ -871,6 +897,7 @@ def __init__(
871897
norm_layer: normalization layer.
872898
downsample: an optional downsampling layer at the end of the layer.
873899
use_checkpoint: use gradient checkpointing for reduced memory usage.
900+
use_flash_attention: use flash attention (scaled dot product attention) at inference.
874901
"""
875902

876903
super().__init__()
@@ -893,6 +920,7 @@ def __init__(
893920
drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path,
894921
norm_layer=norm_layer,
895922
use_checkpoint=use_checkpoint,
923+
use_flash_attention=use_flash_attention,
896924
)
897925
for i in range(depth)
898926
]
@@ -961,6 +989,7 @@ def __init__(
961989
spatial_dims: int = 3,
962990
downsample="merging",
963991
use_v2=False,
992+
use_flash_attention: bool = False,
964993
) -> None:
965994
"""
966995
Args:
@@ -983,6 +1012,7 @@ def __init__(
9831012
user-specified `nn.Module` following the API defined in :py:class:`monai.networks.nets.PatchMerging`.
9841013
The default is currently `"merging"` (the original version defined in v0.9.0).
9851014
use_v2: using swinunetr_v2, which adds a residual convolution block at the beginning of each swin stage.
1015+
use_flash_attention: use flash attention (scaled dot product attention) at inference.
9861016
"""
9871017

9881018
super().__init__()
@@ -1025,6 +1055,7 @@ def __init__(
10251055
norm_layer=norm_layer,
10261056
downsample=down_sample_mod,
10271057
use_checkpoint=use_checkpoint,
1058+
use_flash_attention=use_flash_attention,
10281059
)
10291060
if i_layer == 0:
10301061
self.layers1.append(layer)

0 commit comments

Comments
 (0)