1414# limitations under the License.
1515
1616import gc
17+ import logging
1718
1819import pytest
1920import torch
2021
2122from diffusers .models .attention import AttentionModuleMixin
22- from diffusers .models .attention_processor import (
23- AttnProcessor ,
23+ from diffusers .models .attention_dispatch import AttentionBackendName , _AttentionBackendRegistry , attention_backend
24+ from diffusers .models .attention_processor import AttnProcessor
25+ from diffusers .utils import is_kernels_available , is_torch_version
26+
27+ from ...testing_utils import assert_tensors_close , backend_empty_cache , is_attention , is_torch_compile , torch_device
28+ from .utils import _maybe_cast_to_bf16
29+
30+
31+ logger = logging .getLogger (__name__ )
32+
33+
34+ # ---------------------------------------------------------------------------
35+ # Module-level backend parameter sets for AttentionBackendTesterMixin
36+ # ---------------------------------------------------------------------------
37+
38+ _CUDA_AVAILABLE = torch .cuda .is_available ()
39+
40+ _PARAM_NATIVE_CUDNN = pytest .param (
41+ AttentionBackendName ._NATIVE_CUDNN ,
42+ id = "native_cudnn" ,
43+ marks = pytest .mark .skipif (
44+ not _CUDA_AVAILABLE ,
45+ reason = "CUDA is required for _native_cudnn backend." ,
46+ ),
47+ )
48+
49+ _PARAM_FLASH_HUB = pytest .param (
50+ AttentionBackendName .FLASH_HUB ,
51+ id = "flash_hub" ,
52+ marks = [
53+ pytest .mark .skipif (not _CUDA_AVAILABLE , reason = "CUDA is required for flash_hub backend." ),
54+ pytest .mark .skipif (
55+ not is_kernels_available (),
56+ reason = "`kernels` package is required for flash_hub backend. Install with `pip install kernels`." ,
57+ ),
58+ ],
2459)
2560
26- from ...testing_utils import (
27- assert_tensors_close ,
28- backend_empty_cache ,
29- is_attention ,
30- torch_device ,
61+ _PARAM_FLASH_3_HUB = pytest .param (
62+ AttentionBackendName ._FLASH_3_HUB ,
63+ id = "flash_3_hub" ,
64+ marks = [
65+ pytest .mark .skipif (not _CUDA_AVAILABLE , reason = "CUDA is required for _flash_3_hub backend." ),
66+ pytest .mark .skipif (
67+ not is_kernels_available (),
68+ reason = "`kernels` package is required for _flash_3_hub backend. Install with `pip install kernels`." ,
69+ ),
70+ ],
3171)
3272
73+ # All backends under test.
74+ _ALL_BACKEND_PARAMS = [_PARAM_NATIVE_CUDNN , _PARAM_FLASH_HUB , _PARAM_FLASH_3_HUB ]
75+
76+ # Backends that perform non-deterministic operations and therefore cannot run when
77+ # torch.use_deterministic_algorithms(True) is active (e.g. after enable_full_determinism()).
78+ _NON_DETERMINISTIC_BACKENDS = {AttentionBackendName ._NATIVE_CUDNN }
79+
80+
81+ def _skip_if_backend_requires_nondeterminism (backend ):
82+ """Skip at runtime when torch.use_deterministic_algorithms(True) blocks the backend.
83+
84+ This check is intentionally deferred to test execution time because
85+ enable_full_determinism() is typically called at module level in test files *after*
86+ the module-level pytest.param() objects in this file have already been evaluated,
87+ making it impossible to catch via a collection-time skipif condition.
88+ """
89+ if backend in _NON_DETERMINISTIC_BACKENDS and torch .are_deterministic_algorithms_enabled ():
90+ pytest .skip (
91+ f"Backend '{ backend .value } ' performs non-deterministic operations and cannot run "
92+ f"while `torch.use_deterministic_algorithms(True)` is active."
93+ )
94+
3395
3496@is_attention
3597class AttentionTesterMixin :
@@ -39,7 +101,6 @@ class AttentionTesterMixin:
39101 Tests functionality from AttentionModuleMixin including:
40102 - Attention processor management (set/get)
41103 - QKV projection fusion/unfusion
42- - Attention backends (XFormers, NPU, etc.)
43104
44105 Expected from config mixin:
45106 - model_class: The model class to test
@@ -179,3 +240,191 @@ def test_attention_processor_count_mismatch_raises_error(self):
179240 model .set_attn_processor (wrong_processors )
180241
181242 assert "number of processors" in str (exc_info .value ).lower (), "Error should mention processor count mismatch"
243+
244+
245+ @is_attention
246+ class AttentionBackendTesterMixin :
247+ """
248+ Mixin class for testing attention backends on models. Following things are tested:
249+
250+ 1. Backends can be set with the `attention_backend` context manager and with
251+ `set_attention_backend()` method.
252+ 2. SDPA outputs don't deviate too much from backend outputs.
253+ 3. Backend works with (regional) compilation.
254+ 4. Backends can be restored.
255+
256+ Tests the backends using the model provided by the host test class. The backends to test
257+ are defined in `_ALL_BACKEND_PARAMS`.
258+
259+ Expected from the host test class:
260+ - model_class: The model class to instantiate.
261+
262+ Expected methods from the host test class:
263+ - get_init_dict(): Returns dict of kwargs to construct the model.
264+ - get_dummy_inputs(): Returns dict of inputs for the model's forward pass.
265+
266+ Pytest mark: attention
267+ Use `pytest -m "not attention"` to skip these tests.
268+ """
269+
270+ def setup_method (self ):
271+ gc .collect ()
272+ backend_empty_cache (torch_device )
273+
274+ def teardown_method (self ):
275+ gc .collect ()
276+ backend_empty_cache (torch_device )
277+
278+ @torch .no_grad ()
279+ @pytest .mark .parametrize ("backend" , _ALL_BACKEND_PARAMS )
280+ def test_set_attention_backend_matches_context_manager (self , backend ):
281+ """set_attention_backend() and the attention_backend() context manager must yield identical outputs."""
282+ _skip_if_backend_requires_nondeterminism (backend )
283+
284+ init_dict = self .get_init_dict ()
285+ inputs_dict = self .get_dummy_inputs ()
286+ model = self .model_class (** init_dict )
287+ model .to (torch_device )
288+ model .eval ()
289+
290+ model , inputs_dict = _maybe_cast_to_bf16 (backend , model , inputs_dict )
291+
292+ with attention_backend (backend ):
293+ ctx_output = model (** inputs_dict , return_dict = False )[0 ]
294+
295+ initial_registry_backend , _ = _AttentionBackendRegistry .get_active_backend ()
296+
297+ model .set_attention_backend (backend .value )
298+
299+ try :
300+ set_output = model (** inputs_dict , return_dict = False )[0 ]
301+ finally :
302+ model .reset_attention_backend ()
303+ _AttentionBackendRegistry .set_active_backend (initial_registry_backend )
304+
305+ assert_tensors_close (
306+ set_output ,
307+ ctx_output ,
308+ atol = 0 ,
309+ rtol = 0 ,
310+ msg = (
311+ f"Output from model.set_attention_backend('{ backend .value } ') should be identical "
312+ f"to the output from `with attention_backend('{ backend .value } '):`."
313+ ),
314+ )
315+
316+ @torch .no_grad ()
317+ @pytest .mark .parametrize ("backend" , _ALL_BACKEND_PARAMS )
318+ def test_output_close_to_native (self , backend , atol = 1e-2 , rtol = 1e-2 ):
319+ """All backends should produce model output numerically close to the native SDPA reference."""
320+ _skip_if_backend_requires_nondeterminism (backend )
321+
322+ init_dict = self .get_init_dict ()
323+ inputs_dict = self .get_dummy_inputs ()
324+ model = self .model_class (** init_dict )
325+ model .to (torch_device )
326+ model .eval ()
327+
328+ model , inputs_dict = _maybe_cast_to_bf16 (backend , model , inputs_dict )
329+
330+ with attention_backend (AttentionBackendName .NATIVE ):
331+ native_output = model (** inputs_dict , return_dict = False )[0 ]
332+
333+ initial_registry_backend , _ = _AttentionBackendRegistry .get_active_backend ()
334+
335+ try :
336+ model .set_attention_backend (backend .value )
337+ except Exception as e :
338+ logger .warning ("Skipping test for backend '%s': %s" , backend .value , e )
339+ pytest .skip (str (e ))
340+
341+ try :
342+ backend_output = model (** inputs_dict , return_dict = False )[0 ]
343+ finally :
344+ model .reset_attention_backend ()
345+ _AttentionBackendRegistry .set_active_backend (initial_registry_backend )
346+
347+ assert_tensors_close (
348+ backend_output ,
349+ native_output ,
350+ atol = atol ,
351+ rtol = rtol ,
352+ msg = f"Output from { backend } should be numerically close to native SDPA." ,
353+ )
354+
355+ @pytest .mark .parametrize ("backend" , _ALL_BACKEND_PARAMS )
356+ def test_context_manager_switches_and_restores_backend (self , backend ):
357+ """attention_backend() should activate the requested backend and restore the previous one on exit."""
358+ initial_backend , _ = _AttentionBackendRegistry .get_active_backend ()
359+
360+ with attention_backend (backend ):
361+ active_backend , _ = _AttentionBackendRegistry .get_active_backend ()
362+ assert active_backend == backend , (
363+ f"Backend should be { backend } inside the context manager, got { active_backend } ."
364+ )
365+
366+ restored_backend , _ = _AttentionBackendRegistry .get_active_backend ()
367+ assert restored_backend == initial_backend , (
368+ f"Backend should be restored to { initial_backend } after exiting the context manager, "
369+ f"got { restored_backend } ."
370+ )
371+
372+ @pytest .mark .parametrize ("backend" , _ALL_BACKEND_PARAMS )
373+ @is_torch_compile
374+ def test_compile (self , backend , atol = 1e-2 , rtol = 1e-2 ):
375+ """
376+ `torch.compile` tests checking for recompilation, graph breaks, forward can run, etc.
377+ For speed, we use regional compilation here (`model.compile_repeated_blocks()`
378+ as opposed to `model.compile`).
379+ """
380+ _skip_if_backend_requires_nondeterminism (backend )
381+ if getattr (self .model_class , "_repeated_blocks" , None ) is None :
382+ pytest .skip ("Skipping tests as regional compilation is not supported." )
383+
384+ if backend == AttentionBackendName .NATIVE and not is_torch_version (">=" , "2.9.0" ):
385+ pytest .xfail (
386+ "test_compile with the native backend requires torch >= 2.9.0 for stable "
387+ "fullgraph compilation with error_on_recompile=True."
388+ )
389+
390+ init_dict = self .get_init_dict ()
391+ inputs_dict = self .get_dummy_inputs ()
392+ model = self .model_class (** init_dict )
393+ model .to (torch_device )
394+ model .eval ()
395+
396+ model , inputs_dict = _maybe_cast_to_bf16 (backend , model , inputs_dict )
397+
398+ with torch .no_grad (), attention_backend (AttentionBackendName .NATIVE ):
399+ native_output = model (** inputs_dict , return_dict = False )[0 ]
400+
401+ initial_registry_backend , _ = _AttentionBackendRegistry .get_active_backend ()
402+
403+ try :
404+ model .set_attention_backend (backend .value )
405+ except Exception as e :
406+ logger .warning ("Skipping test for backend '%s': %s" , backend .value , e )
407+ pytest .skip (str (e ))
408+
409+ try :
410+ model .compile_repeated_blocks (fullgraph = True )
411+ torch .compiler .reset ()
412+
413+ with (
414+ torch ._inductor .utils .fresh_inductor_cache (),
415+ torch ._dynamo .config .patch (error_on_recompile = True ),
416+ ):
417+ with torch .no_grad ():
418+ compile_output = model (** inputs_dict , return_dict = False )[0 ]
419+ model (** inputs_dict , return_dict = False )
420+ finally :
421+ model .reset_attention_backend ()
422+ _AttentionBackendRegistry .set_active_backend (initial_registry_backend )
423+
424+ assert_tensors_close (
425+ compile_output ,
426+ native_output ,
427+ atol = atol ,
428+ rtol = rtol ,
429+ msg = f"Compiled output with backend '{ backend .value } ' should be numerically close to eager native SDPA." ,
430+ )
0 commit comments