Skip to content

Commit ff6d3e7

Browse files
committed
fix: only instantiate CrossAttentionBlock when with_cross_attention=True
[200~Fixes #8845 TransformerBlock previously instantiated norm_cross_attn and cross_attn unconditionally, even when with_cross_attention=False. These unused modules registered dead parameters in model.parameters(), wasting memory. Wrapped both instantiations in `if with_cross_attention:` to match the existing guard in forward(). Added tests to verify the modules and their parameters are absent when disabled, present when enabled, and that the forward pass with a context tensor works correctly.~ Signed-off-by: chhayankjain <chhayank44@gmail.com>
1 parent 586dea1 commit ff6d3e7

3 files changed

Lines changed: 54 additions & 9 deletions

File tree

monai/networks/blocks/transformerblock.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,20 @@ def __init__(
4646
dropout_rate (float, optional): fraction of the input units to drop. Defaults to 0.0.
4747
qkv_bias(bool, optional): apply bias term for the qkv linear layer. Defaults to False.
4848
save_attn (bool, optional): to make accessible the attention matrix. Defaults to False.
49+
causal (bool, optional): whether to apply causal masking in self-attention. Defaults to False.
50+
sequence_length (int | None, optional): sequence length required for causal masking. Defaults to None.
51+
with_cross_attention (bool, optional): whether to include cross-attention layers that attend to an
52+
external context tensor. When False, cross_attn is set to nn.Identity() so that the attribute
53+
always exists for typing and checkpoint compatibility. Defaults to False.
4954
use_flash_attention: if True, use Pytorch's inbuilt flash attention for a memory efficient attention mechanism
5055
(see https://pytorch.org/docs/2.2/generated/torch.nn.functional.scaled_dot_product_attention.html).
5156
include_fc: whether to include the final linear layer. Default to True.
5257
use_combined_linear: whether to use a single linear layer for qkv projection, default to True.
5358
59+
Raises:
60+
ValueError: if dropout_rate is not in [0, 1].
61+
ValueError: if hidden_size is not divisible by num_heads.
62+
5463
"""
5564

5665
super().__init__()
@@ -79,14 +88,18 @@ def __init__(
7988
self.with_cross_attention = with_cross_attention
8089

8190
self.norm_cross_attn = nn.LayerNorm(hidden_size)
82-
self.cross_attn = CrossAttentionBlock(
83-
hidden_size=hidden_size,
84-
num_heads=num_heads,
85-
dropout_rate=dropout_rate,
86-
qkv_bias=qkv_bias,
87-
causal=False,
88-
use_flash_attention=use_flash_attention,
89-
)
91+
self.cross_attn: CrossAttentionBlock | nn.Identity
92+
if with_cross_attention:
93+
self.cross_attn = CrossAttentionBlock(
94+
hidden_size=hidden_size,
95+
num_heads=num_heads,
96+
dropout_rate=dropout_rate,
97+
qkv_bias=qkv_bias,
98+
causal=False,
99+
use_flash_attention=use_flash_attention,
100+
)
101+
else:
102+
self.cross_attn = nn.Identity()
90103

91104
def forward(
92105
self, x: torch.Tensor, context: torch.Tensor | None = None, attn_mask: torch.Tensor | None = None

monai/networks/nets/transformer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def load_old_state_dict(self, old_state_dict: dict, verbose=False) -> None:
147147

148148
# fix the renamed norm blocks first norm2 -> norm_cross_attention , norm3 -> norm2
149149
for k in list(old_state_dict.keys()):
150-
if "norm2" in k:
150+
if "norm2" in k and k.replace("norm2", "norm_cross_attn") in new_state_dict:
151151
new_state_dict[k.replace("norm2", "norm_cross_attn")] = old_state_dict.pop(k)
152152
if "norm3" in k:
153153
new_state_dict[k.replace("norm3", "norm2")] = old_state_dict.pop(k)

tests/networks/blocks/test_transformerblock.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
import numpy as np
1818
import torch
19+
import torch.nn as nn
1920
from parameterized import parameterized
2021

2122
from monai.networks import eval_mode
23+
from monai.networks.blocks.crossattention import CrossAttentionBlock
2224
from monai.networks.blocks.transformerblock import TransformerBlock
2325
from monai.utils import optional_import
2426
from tests.test_utils import dict_product
@@ -53,6 +55,36 @@ def test_ill_arg(self):
5355
with self.assertRaises(ValueError):
5456
TransformerBlock(hidden_size=622, num_heads=8, mlp_dim=3072, dropout_rate=0.4)
5557

58+
@skipUnless(has_einops, "Requires einops")
59+
def test_cross_attention_is_identity_when_disabled(self):
60+
block = TransformerBlock(hidden_size=128, mlp_dim=256, num_heads=4, with_cross_attention=False)
61+
# attributes always exist for typing and checkpoint compatibility
62+
self.assertTrue(hasattr(block, "cross_attn"))
63+
self.assertTrue(hasattr(block, "norm_cross_attn"))
64+
# cross_attn is nn.Identity (no parameters) when disabled
65+
self.assertIsInstance(block.cross_attn, nn.Identity)
66+
param_names = [name for name, _ in block.named_parameters()]
67+
self.assertFalse(any(n.startswith("cross_attn") for n in param_names))
68+
69+
@skipUnless(has_einops, "Requires einops")
70+
def test_cross_attention_params_registered_when_enabled(self):
71+
block = TransformerBlock(hidden_size=128, mlp_dim=256, num_heads=4, with_cross_attention=True)
72+
self.assertIsInstance(block.cross_attn, CrossAttentionBlock)
73+
self.assertTrue(hasattr(block, "norm_cross_attn"))
74+
param_names = [name for name, _ in block.named_parameters()]
75+
self.assertTrue(any("cross_attn" in n for n in param_names))
76+
self.assertTrue(any("norm_cross_attn" in n for n in param_names))
77+
78+
@skipUnless(has_einops, "Requires einops")
79+
def test_cross_attention_forward_with_context(self):
80+
hidden_size = 128
81+
block = TransformerBlock(hidden_size=hidden_size, mlp_dim=256, num_heads=4, with_cross_attention=True)
82+
x = torch.randn(2, 16, hidden_size)
83+
context = torch.randn(2, 8, hidden_size)
84+
with eval_mode(block):
85+
out = block(x, context=context)
86+
self.assertEqual(out.shape, x.shape)
87+
5688
@skipUnless(has_einops, "Requires einops")
5789
def test_access_attn_matrix(self):
5890
# input format

0 commit comments

Comments
 (0)