Skip to content

Commit cbdedba

Browse files
authored
[tests] add attention backend tests. (#13174)
* add attention backend tests. * remove existing tests/others/test_attention_backends.py file * modify generate_model_tests.py * remove native. * account for _keep_in_fp32_modules * don't skip when exception is raised. * use is_kernels_available() * mark with compile. * move rtol and atol to methods as defaults. * Apply suggestions from code review Co-authored-by: Sayak Paul <spsayakpaul@gmail.com> * up * up
1 parent 0ceddf7 commit cbdedba

6 files changed

Lines changed: 268 additions & 172 deletions

File tree

tests/models/testing_utils/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .attention import AttentionTesterMixin
1+
from .attention import AttentionBackendTesterMixin, AttentionTesterMixin
22
from .cache import (
33
CacheTesterMixin,
44
FasterCacheConfigMixin,
@@ -38,6 +38,7 @@
3838

3939

4040
__all__ = [
41+
"AttentionBackendTesterMixin",
4142
"AttentionTesterMixin",
4243
"BaseModelTesterConfig",
4344
"BitsAndBytesCompileTesterMixin",

tests/models/testing_utils/attention.py

Lines changed: 257 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,84 @@
1414
# limitations under the License.
1515

1616
import gc
17+
import logging
1718

1819
import pytest
1920
import torch
2021

2122
from 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
3597
class 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+
)

tests/models/testing_utils/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ def _maybe_cast_to_bf16(backend, model, inputs_dict):
1515
"""Cast model and floating-point inputs to bfloat16 when the backend requires it."""
1616
if not backend or backend not in _BF16_REQUIRED_BACKENDS:
1717
return model, inputs_dict
18+
if getattr(model, "_keep_in_fp32_modules", None):
19+
raise NotImplementedError("Do not know how to define casting for models with `_keep_in_fp32_modules`.")
1820
model = model.to(dtype=torch.bfloat16)
1921
inputs_dict = {
2022
k: v.to(dtype=torch.bfloat16) if isinstance(v, torch.Tensor) and v.is_floating_point() else v

tests/models/transformers/test_models_transformer_flux.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from ...testing_utils import enable_full_determinism, torch_device
2727
from ..testing_utils import (
28+
AttentionBackendTesterMixin,
2829
AttentionTesterMixin,
2930
BaseModelTesterConfig,
3031
BitsAndBytesCompileTesterMixin,
@@ -242,6 +243,10 @@ class TestFluxTransformerAttention(FluxTransformerTesterConfig, AttentionTesterM
242243
"""Attention processor tests for Flux Transformer."""
243244

244245

246+
class TestFluxTransformerAttentionBackend(FluxTransformerTesterConfig, AttentionBackendTesterMixin):
247+
"""Attention backend tests for Flux Transformer."""
248+
249+
245250
class TestFluxTransformerContextParallel(FluxTransformerTesterConfig, ContextParallelTesterMixin):
246251
"""Context Parallel inference tests for Flux Transformer"""
247252

0 commit comments

Comments
 (0)