Skip to content

Commit a6f4134

Browse files
chenghuichenrwightman
authored andcommitted
Add optional key-only masks for NaFlexVit self-attention
1 parent 8ef7380 commit a6f4134

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

tests/test_models.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,73 @@ def test_naflexvit_forward_intermediates_dict_input():
799799
with pytest.raises(ValueError, match='patch dropout'):
800800
model_pd.forward_intermediates(batch, output_fmt='NLC')
801801

802+
803+
@pytest.mark.base
804+
def test_naflexvit_key_only_attn_mask_is_opt_in_and_post_patch_dropout():
805+
batch_size, num_patches = 2, 8
806+
patches = torch.randn(batch_size, num_patches, 16 * 16 * 3)
807+
coord = torch.zeros(batch_size, num_patches, 2, dtype=torch.long)
808+
valid = torch.tensor([
809+
[True, True, True, True, True, False, False, False],
810+
[True, True, True, True, True, True, False, False],
811+
])
812+
813+
default_model = create_model(
814+
'naflexvit_base_patch16_gap', embed_dim=32, depth=1, num_heads=2)
815+
default_embeds = default_model._forward_embeds(patches, coord, valid, attn_mask=None)
816+
default_seq_len = default_embeds['patches'].shape[1]
817+
assert default_model.use_key_only_attn_mask is False
818+
assert default_embeds['attn_mask'].shape == (batch_size, 1, default_seq_len, default_seq_len)
819+
820+
compact_model = create_model(
821+
'naflexvit_base_patch16_gap', embed_dim=32, depth=1, num_heads=2,
822+
patch_drop_rate=0.5, use_key_only_attn_mask=True)
823+
compact_model.train()
824+
compact_embeds = compact_model._forward_embeds(patches, coord, valid, attn_mask=None)
825+
compact_seq_len = compact_embeds['patches'].shape[1]
826+
assert compact_seq_len < default_seq_len
827+
assert compact_embeds['attn_mask'].shape == (batch_size, 1, 1, compact_seq_len)
828+
829+
expected_valid = compact_embeds['patch_valid']
830+
prefix_valid = torch.ones(batch_size, compact_model.num_prefix_tokens, dtype=torch.bool)
831+
expected_valid = torch.cat([prefix_valid, expected_valid], dim=1)
832+
expected_masked = ~expected_valid[:, None, None, :]
833+
mask = compact_embeds['attn_mask']
834+
assert torch.equal(mask == torch.finfo(mask.dtype).min, expected_masked)
835+
836+
837+
@pytest.mark.base
838+
@pytest.mark.parametrize('global_pool', ['avg', 'token', 'map'])
839+
def test_naflexvit_key_only_attn_mask_output_parity(global_pool):
840+
common_kwargs = dict(
841+
embed_dim=32,
842+
depth=2,
843+
num_heads=2,
844+
global_pool=global_pool,
845+
class_token=global_pool == 'token',
846+
reg_tokens=0,
847+
num_classes=7,
848+
)
849+
full_model = create_model('naflexvit_base_patch16_gap', **common_kwargs)
850+
compact_model = create_model(
851+
'naflexvit_base_patch16_gap', use_key_only_attn_mask=True, **common_kwargs)
852+
compact_model.load_state_dict(full_model.state_dict())
853+
full_model.eval()
854+
compact_model.eval()
855+
856+
num_patches = 8
857+
patches = torch.randn(2, num_patches, 16 * 16 * 3)
858+
coord = torch.zeros(2, num_patches, 2, dtype=torch.long)
859+
valid = torch.tensor([
860+
[True, True, True, True, True, False, False, False],
861+
[True, True, True, True, True, True, True, False],
862+
])
863+
with torch.no_grad():
864+
full_out = full_model(patches, patch_coord=coord, patch_valid=valid)
865+
compact_out = compact_model(patches, patch_coord=coord, patch_valid=valid)
866+
867+
assert torch.equal(full_out, compact_out)
868+
802869
def test_gemma4_forward_intermediates_dict_output():
803870
"""gemma4_vit dict-output intermediates match the NaFlexVit contract (API symmetry):
804871
'image_intermediates' / 'image_features' / 'patch_valid' aligned with the token sequence."""

timm/models/naflexvit.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class NaFlexVitCfg:
7777
proj_bias: bool = True
7878
attn_drop_rate: float = 0.0
7979
scale_attn_inner_norm: bool = False # Apply scaling norm to attn context
80+
use_key_only_attn_mask: bool = False # Use [B, 1, 1, N] key-only mask for self-attention
8081

8182
# Regularization
8283
init_values: Optional[float] = None # Layer-scale init values (layer-scale enabled if not None)
@@ -1176,6 +1177,7 @@ def __init__(
11761177
self.num_reg_tokens = cfg.reg_tokens
11771178
self.has_class_token = cfg.class_token
11781179
self.pool_include_prefix = cfg.pool_include_prefix
1180+
self.use_key_only_attn_mask = cfg.use_key_only_attn_mask
11791181
self.grad_checkpointing = False
11801182

11811183
# Initialize embedding module (includes patch, position embedding, and class/reg tokens)
@@ -1573,6 +1575,8 @@ def _forward_embeds(
15731575
attn_mask = create_attention_mask(
15741576
patch_valid,
15751577
num_prefix_tokens=self.num_prefix_tokens,
1578+
symmetric=not self.use_key_only_attn_mask,
1579+
q_len=1 if self.use_key_only_attn_mask else None,
15761580
dtype=x.dtype
15771581
)
15781582

0 commit comments

Comments
 (0)