@@ -78,7 +78,6 @@ def _sdpa_reference(query: torch.Tensor,
7878@pytest .mark .parametrize ("dtype" , [torch .float16 , torch .bfloat16 ])
7979@pytest .mark .parametrize ("head_size" , [64 , 128 , 256 ])
8080def test_flashinfer_real_cuda_kernel_matches_sdpa (dtype : torch .dtype , head_size : int ) -> None :
81- """Launch the real single-GPU FlashInfer kernel for every supported head size."""
8281 _require_flashinfer_cuda ()
8382 torch .manual_seed (0 )
8483 device = torch .device ("cuda" , torch .cuda .current_device ())
@@ -98,7 +97,6 @@ def test_flashinfer_real_cuda_kernel_matches_sdpa(dtype: torch.dtype, head_size:
9897
9998@pytest .mark .parametrize ("mode" , ["causal" , "cross_gqa" , "causal_padding" ])
10099def test_flashinfer_real_cuda_kernel_attention_modes (mode : str ) -> None :
101- """Exercise native causal, GQA/cross-attention, and combined custom masks."""
102100 _require_flashinfer_cuda ()
103101 torch .manual_seed (1 )
104102 device = torch .device ("cuda" , torch .cuda .current_device ())
@@ -138,3 +136,117 @@ def test_flashinfer_real_cuda_kernel_attention_modes(mode: str) -> None:
138136
139137 torch .cuda .synchronize (device )
140138 torch .testing .assert_close (actual , expected , atol = 3e-2 , rtol = 3e-2 )
139+
140+
141+ def test_metadata_builder_instantiates_through_backend_api () -> None :
142+ from fastvideo .attention .backends .abstract import AttentionMetadataBuilder
143+ from fastvideo .attention .backends .flashinfer import FlashInferBackend
144+ builder = FlashInferBackend .get_builder_cls ()()
145+ assert isinstance (builder , AttentionMetadataBuilder )
146+ builder .prepare ()
147+ mask = torch .ones (2 , 3 , dtype = torch .bool )
148+ metadata = builder .build (current_timestep = 7 , attn_mask = mask )
149+ assert metadata .current_timestep == 7
150+ assert metadata .attn_mask is mask
151+
152+
153+ @pytest .mark .parametrize ("shared_head" , [False , True ])
154+ @pytest .mark .parametrize ("additive" , [False , True ])
155+ def test_shared_masks_preserve_each_sample (shared_head , additive ) -> None :
156+ mask = torch .tensor ([[[True , False , True ], [False , True , True ]],
157+ [[False , True , False ], [True , True , False ]]])
158+ source = torch .where (mask , 0.0 , float ("-inf" )) if additive else mask
159+ if shared_head :
160+ source = source [:, None ]
161+ for sample in range (2 ):
162+ torch .testing .assert_close (_mask_for_sample (source , sample , 2 , 3 ), mask [sample ])
163+
164+
165+ def test_per_head_mask_rejected_before_kernel (monkeypatch ) -> None :
166+ prefill = types .ModuleType ("flashinfer.prefill" )
167+ def unexpected_kernel (* args , ** kwargs ):
168+ pytest .fail ("per-head mask reached FlashInfer kernel" )
169+ prefill .single_prefill_with_kv_cache = unexpected_kernel
170+ monkeypatch .setitem (sys .modules , "flashinfer.prefill" , prefill )
171+ query = torch .randn (2 , 3 , 4 , 64 , dtype = torch .bfloat16 )
172+ metadata = FlashInferMetadata (current_timestep = 0 , attn_mask = torch .ones (2 , 4 , 3 , 3 , dtype = torch .bool ))
173+ impl = FlashInferImpl (num_heads = 4 , head_size = 64 , causal = False , softmax_scale = 0.125 )
174+ with pytest .raises (ValueError , match = "per-head" ):
175+ impl .forward (query , query , query , metadata )
176+
177+
178+ @pytest .mark .parametrize ("dtype" , [torch .float16 , torch .bfloat16 ])
179+ @pytest .mark .parametrize ("shared_head" , [False , True ])
180+ def test_shared_mask_cuda_matches_flash_attention (dtype , shared_head ) -> None :
181+ _require_flashinfer_cuda ()
182+ flash_attn = pytest .importorskip ("flash_attn" )
183+ torch .manual_seed (19 )
184+ q = torch .randn (2 , 16 , 2 , 64 , device = "cuda" , dtype = dtype )
185+ k , v = torch .randn_like (q ), torch .randn_like (q )
186+ mask = torch .zeros (2 , 16 , 16 , device = "cuda" , dtype = torch .bool )
187+ mask [0 , :, :11 ] = True
188+ mask [1 , :, :7 ] = True
189+ metadata = FlashInferMetadata (current_timestep = 0 , attn_mask = mask [:, None ] if shared_head else mask )
190+ impl = FlashInferImpl (num_heads = 2 , head_size = 64 , causal = False , softmax_scale = 0.2 )
191+ actual = impl .forward (q , k , v , metadata )
192+ expected = torch .cat ([
193+ flash_attn .flash_attn_func (q [i :i + 1 ], k [i :i + 1 , :length ], v [i :i + 1 , :length ], softmax_scale = 0.2 )
194+ for i , length in enumerate ([11 , 7 ])
195+ ])
196+ tolerance = 3e-2 if dtype == torch .bfloat16 else 1e-2
197+ torch .testing .assert_close (actual , expected , atol = tolerance , rtol = tolerance )
198+
199+
200+ @pytest .mark .parametrize ("dtype" , [torch .float16 , torch .bfloat16 ])
201+ @pytest .mark .parametrize ("i2v" , [False , True ])
202+ def test_wan_attention_executes_flashinfer (monkeypatch , request , dtype , i2v ) -> None :
203+ _require_flashinfer_cuda ()
204+ request .getfixturevalue ("distributed_setup" )
205+ import flashinfer .prefill as prefill
206+ import fastvideo .envs as envs
207+ from flash_attn import flash_attn_func
208+ from fastvideo .forward_context import set_forward_context
209+ from fastvideo .models .wan .config import WanVideoConfig
210+ from fastvideo .models .wan .transformer import WanTransformerBlock
211+ from fastvideo .platforms import AttentionBackendEnum
212+
213+ real_kernel = prefill .single_prefill_with_kv_cache
214+ calls = []
215+ tolerance = 3e-2 if dtype == torch .bfloat16 else 1e-2
216+ def checked_kernel (q , k , v , ** kwargs ):
217+ actual = real_kernel (q , k , v , ** kwargs )
218+ expected = flash_attn_func (q [None ], k [None ], v [None ],
219+ softmax_scale = kwargs ["sm_scale" ], causal = kwargs ["causal" ])[0 ]
220+ torch .testing .assert_close (actual , expected , atol = tolerance , rtol = tolerance )
221+ calls .append ((q .shape , k .shape ))
222+ return actual
223+ monkeypatch .setattr (prefill , "single_prefill_with_kv_cache" , checked_kernel )
224+ with envs .FASTVIDEO_ATTENTION_BACKEND .override ("FLASHINFER" ), torch .device ("cuda" ):
225+ block = WanTransformerBlock (
226+ dim = 256 , ffn_dim = 512 , num_heads = 2 , cross_attn_norm = True ,
227+ added_kv_proj_dim = 256 if i2v else None ,
228+ supported_attention_backends = WanVideoConfig ()._supported_attention_backends ,
229+ ).to (dtype = dtype ).eval ()
230+ assert block .attn1 .backend == AttentionBackendEnum .FLASHINFER
231+ assert block .attn2 .attn .backend == AttentionBackendEnum .FLASHINFER
232+ for param in block .parameters ():
233+ torch .nn .init .normal_ (param , std = 0.02 )
234+ q = torch .randn (1 , 16 , 2 , 128 , device = "cuda" , dtype = dtype )
235+ k , v = torch .randn_like (q ), torch .randn_like (q )
236+ x = torch .randn (1 , 16 , 256 , device = "cuda" , dtype = dtype )
237+ context = torch .randn (1 , 264 if i2v else 7 , 256 , device = "cuda" , dtype = dtype )
238+ with torch .inference_mode (), set_forward_context (current_timestep = 0 , attn_metadata = None ):
239+ self_output , _ = block .attn1 (q , k , v )
240+ cross_output = block .attn2 (x , context , None )
241+ assert self_output .shape == q .shape
242+ assert cross_output .shape == x .shape
243+ assert len (calls ) == (3 if i2v else 2 )
244+
245+
246+ def test_wan_cuda_skip_precedes_distributed_setup (monkeypatch ):
247+ monkeypatch .setattr (torch .cuda , "is_available" , lambda : False )
248+ class Request :
249+ def getfixturevalue (self , name ):
250+ pytest .fail ("distributed setup ran before the CUDA availability check" )
251+ with pytest .raises (pytest .skip .Exception , match = "NVIDIA CUDA GPU" ):
252+ test_wan_attention_executes_flashinfer (monkeypatch , Request (), torch .float16 , False )
0 commit comments