@@ -838,6 +838,77 @@ def fake_forward(self, **kwargs):
838838
839839
840840class TestGemma4TEDotProductAttention :
841+ def test_image_attention_keeps_future_image_tokens_in_window (self , monkeypatch ):
842+ from megatron .core .transformer .enums import AttnMaskType
843+
844+ calls = {}
845+
846+ def fake_init (self , ** kwargs ):
847+ calls .update (kwargs )
848+
849+ monkeypatch .setattr ("megatron.bridge.models.gemma.modeling_gemma4.TEDotProductAttention.__init__" , fake_init )
850+ config = SimpleNamespace (
851+ interleaved_attn_pattern = (1 , 1 ),
852+ window_size = 512 ,
853+ text_config = SimpleNamespace (use_bidirectional_attention = "vision" ),
854+ )
855+ Gemma4TEDotProductAttention (
856+ config = config , layer_number = 1 , attn_mask_type = AttnMaskType .causal , attention_type = "self"
857+ )
858+
859+ assert calls ["config" ].window_size is None
860+ assert calls ["attn_mask_type" ] == AttnMaskType .arbitrary
861+ assert config .window_size == 512
862+
863+ @pytest .mark .parametrize ("image_bidirectional" , [True , False ])
864+ def test_forward_selects_explicit_image_mask_mode (self , monkeypatch , image_bidirectional ):
865+ from megatron .core .transformer .enums import AttnMaskType
866+
867+ calls = {}
868+
869+ def fake_forward (self , query , key , value , attention_mask , attn_mask_type , ** kwargs ):
870+ calls .update (mask = attention_mask , mask_type = attn_mask_type , ** kwargs )
871+ return value
872+
873+ monkeypatch .setattr ("megatron.bridge.models.gemma.modeling_gemma4.TEDotProductAttention.forward" , fake_forward )
874+ attention = object .__new__ (Gemma4TEDotProductAttention )
875+ attention ._image_bidirectional_attention = image_bidirectional
876+ attention ._image_attention_window = 512 if image_bidirectional else None
877+ mask = torch .tensor ([[[[False , False ], [False , False ]]]])
878+ value = torch .ones (2 , 1 , 1 , 4 )
879+
880+ output = attention .forward (value , value , value , mask , AttnMaskType .causal , attention_bias = None )
881+
882+ assert output is value
883+ torch .testing .assert_close (calls ["mask" ], mask )
884+ assert calls ["mask_type" ] == (AttnMaskType .arbitrary if image_bidirectional else AttnMaskType .causal )
885+ assert calls ["attention_bias" ] is None
886+
887+ @pytest .mark .parametrize ("with_image_mask" , [True , False ])
888+ def test_image_attention_applies_left_window_without_losing_future_image_tokens (
889+ self , monkeypatch , with_image_mask
890+ ):
891+ from megatron .core .transformer .enums import AttnMaskType
892+
893+ calls = {}
894+
895+ def fake_forward (self , query , key , value , attention_mask , attn_mask_type , ** kwargs ):
896+ calls ["mask" ] = attention_mask
897+ return value
898+
899+ monkeypatch .setattr ("megatron.bridge.models.gemma.modeling_gemma4.TEDotProductAttention.forward" , fake_forward )
900+ attention = object .__new__ (Gemma4TEDotProductAttention )
901+ attention ._image_bidirectional_attention = True
902+ attention ._image_attention_window = 2
903+ value = torch .ones (4 , 1 , 1 , 4 )
904+ mask = torch .zeros (1 , 1 , 4 , 4 , dtype = torch .bool ) if with_image_mask else None
905+
906+ attention .forward (value , value , value , mask , AttnMaskType .causal )
907+
908+ assert calls ["mask" ][0 , 0 , 3 , 0 ].item () is True
909+ assert calls ["mask" ][0 , 0 , 3 , 2 ].item () is False
910+ assert calls ["mask" ][0 , 0 , 0 , 1 ].item () is (not with_image_mask )
911+
841912 def test_init_sets_local_window_size (self , monkeypatch ):
842913 calls = []
843914
@@ -870,7 +941,11 @@ def fake_init(self, **kwargs):
870941 "megatron.bridge.models.gemma.modeling_gemma4.TEDotProductAttention.__init__" ,
871942 fake_init ,
872943 )
873- cfg = SimpleNamespace (interleaved_attn_pattern = (1 , 1 ), window_size = 512 )
944+ cfg = SimpleNamespace (
945+ interleaved_attn_pattern = (1 , 1 ),
946+ window_size = 512 ,
947+ text_config = SimpleNamespace (use_bidirectional_attention = "vision" ),
948+ )
874949
875950 Gemma4TEDotProductAttention (
876951 config = cfg ,
0 commit comments