@@ -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+
802869def 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."""
0 commit comments