1
0
mirror of https://github.com/huggingface/diffusers.git synced 2026-10-24 20:59:27 +03:00
Files
diffusers/tests/models/test_attention_dispatch.py
Wang, Yi 839af3a043 Update FlashAttention Hub integration and guard CUDA-only tests (#15024)
* Update FlashAttention Hub integration and guard CUDA-only tests

Signed-off-by: Wang, Yi <yi.a.wang@intel.com>

* fmt

Signed-off-by: Wang, Yi <yi.a.wang@intel.com>

---------

Signed-off-by: Wang, Yi <yi.a.wang@intel.com>
2026-10-14 14:05:19 +05:30

234 lines
9.5 KiB
Python

# coding=utf-8
# Copyright 2026 HuggingFace Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import contextlib
import os
import pytest
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
import torch.nn.functional as F
from diffusers.models._modeling_parallel import ContextParallelConfig, ParallelConfig
from diffusers.models.attention_dispatch import attention_backend as attention_backend_ctx
from diffusers.models.attention_dispatch import dispatch_attention_fn
from ..testing_utils import (
is_attention,
is_context_parallel,
is_kernels_available,
is_torch_compile,
require_torch_accelerator,
require_torch_multi_accelerator,
torch_device,
)
from .testing_utils.parallelism import DEVICE_CONFIG, _find_free_port
# Max allowed relative error between the context parallel gradients and the single-process reference.
GRAD_RTOL = 2e-2
def _attention_backward_parity_worker(rank, world_size, master_port, cp_dict, attention_backend, return_dict):
"""Op-level worker: check `dispatch_attention_fn` gradients against a single-process reference.
This guards the ring-attention backward pass, which historically produced silently wrong
gradients (see https://github.com/huggingface/diffusers/issues/14265) because it recomputed
every ring iteration against the iteration-0 KV chunk.
"""
try:
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = str(master_port)
os.environ["RANK"] = str(rank)
os.environ["WORLD_SIZE"] = str(world_size)
device_config = DEVICE_CONFIG.get(torch_device, DEVICE_CONFIG["cuda"])
device_type = torch_device.split(":")[0]
device_module = device_config["module"]
dist.init_process_group(backend=device_config["backend"], rank=rank, world_size=world_size)
device_module.set_device(rank)
device = torch.device(f"{device_type}:{rank}")
# Identical inputs on every rank so each rank can compute the same full-sequence reference.
B, S, H, D = 1, 128, 4, 64
torch.manual_seed(777)
q = torch.randn(B, S, H, D, device=device, dtype=torch.bfloat16)
k = torch.randn(B, S, H, D, device=device, dtype=torch.bfloat16)
v = torch.randn(B, S, H, D, device=device, dtype=torch.bfloat16)
grad_out = torch.randn(B, S, H, D, device=device, dtype=torch.bfloat16)
# Reference: full-sequence fp32 SDPA gradients on a single process.
q_ref, k_ref, v_ref = (t.float().requires_grad_(True) for t in (q, k, v))
ref_out = F.scaled_dot_product_attention(
q_ref.transpose(1, 2), k_ref.transpose(1, 2), v_ref.transpose(1, 2)
).transpose(1, 2)
ref_dq, ref_dk, ref_dv = torch.autograd.grad(ref_out, (q_ref, k_ref, v_ref), grad_out.float())
mesh = dist.device_mesh.init_device_mesh(
device_type,
(cp_dict.get("ring_degree", 1), cp_dict.get("ulysses_degree", 1)),
mesh_dim_names=("ring", "ulysses"),
)
cp_config = ContextParallelConfig(**cp_dict)
cp_config.setup(rank, world_size, device, mesh)
parallel_config = ParallelConfig(context_parallel_config=cp_config)
# Each rank runs its sequence shard through the templated CP attention path.
shard = slice(rank * S // world_size, (rank + 1) * S // world_size)
qs, ks, vs = (t.detach()[:, shard].clone().requires_grad_(True) for t in (q, k, v))
with attention_backend_ctx(attention_backend):
out = dispatch_attention_fn(qs, ks, vs, parallel_config=parallel_config)
out.backward(grad_out[:, shard])
rel_errs = {}
for name, got, ref in (("dq", qs.grad, ref_dq), ("dk", ks.grad, ref_dk), ("dv", vs.grad, ref_dv)):
ref_shard = ref[:, shard].to(got.dtype)
rel_errs[name] = ((got - ref_shard).norm() / ref_shard.norm()).item()
if rank == 0:
return_dict["status"] = "success"
return_dict["rel_errs"] = dict(rel_errs)
except Exception as e:
if rank == 0:
return_dict["status"] = "error"
return_dict["error"] = repr(e)
finally:
if dist.is_initialized():
dist.destroy_process_group()
@is_attention
@is_context_parallel
@require_torch_multi_accelerator
class TestContextParallelAttentionBackward:
"""Op-level tests for the context parallel backward of `dispatch_attention_fn`, model independent."""
@pytest.mark.parametrize("cp_type", ["ulysses_degree", "ring_degree"])
@pytest.mark.parametrize(
"attention_backend",
[
"_native_flash",
pytest.param(
"_native_cudnn",
marks=pytest.mark.skipif(torch_device != "cuda", reason="CUDA is required for _native_cudnn backend."),
),
pytest.param(
"flash_hub",
marks=pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available."),
),
pytest.param(
"flash_varlen_hub",
marks=pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available."),
),
pytest.param(
"_flash_3_hub",
marks=[
pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available."),
pytest.mark.skipif(torch_device != "cuda", reason="CUDA is required for _flash_3_hub backend."),
],
),
pytest.param(
"_flash_3_varlen_hub",
marks=[
pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available."),
pytest.mark.skipif(
torch_device != "cuda", reason="CUDA is required for _flash_3_varlen_hub backend."
),
],
),
],
)
def test_attn_backend_backward_parity(self, cp_type, attention_backend):
"""Ring and Ulysses attention gradients must match a single-GPU reference.
Regression test for https://github.com/huggingface/diffusers/issues/14265, where the ring
backward silently used the iteration-0 KV chunk for every ring iteration.
"""
if not torch.distributed.is_available():
pytest.skip("torch.distributed is not available.")
world_size = 2
cp_dict = {cp_type: world_size}
master_port = _find_free_port()
manager = mp.Manager()
return_dict = manager.dict()
mp.spawn(
_attention_backward_parity_worker,
args=(world_size, master_port, cp_dict, attention_backend, return_dict),
nprocs=world_size,
join=True,
)
assert return_dict.get("status") == "success", (
f"Context parallel backward parity run failed: {return_dict.get('error', 'Unknown error')}"
)
rel_errs = return_dict["rel_errs"]
for name, rel in rel_errs.items():
assert rel < GRAD_RTOL, (
f"{attention_backend} {cp_type} gradient `{name}` rel_err={rel:.3e} "
f"exceeds tol={GRAD_RTOL:.1e}: {rel_errs}"
)
@is_attention
class TestVarlenAttentionCompile:
"""Op-level tests for the varlen backends under `torch.compile` with dynamic shapes, model independent."""
@pytest.mark.parametrize(
"attention_backend",
[
pytest.param(
"flash_varlen_hub",
marks=pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available."),
),
pytest.param(
"_flash_3_varlen_hub",
marks=pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available."),
),
],
)
@is_torch_compile
@require_torch_accelerator
def test_dynamic_shapes(self, attention_backend):
def make_qkv(seq_len):
torch.manual_seed(0)
return tuple(torch.randn(2, seq_len, 4, 64, device=torch_device, dtype=torch.bfloat16) for _ in range(3))
with contextlib.ExitStack() as stack, torch.no_grad():
try:
stack.enter_context(attention_backend_ctx(attention_backend))
dispatch_attention_fn(*make_qkv(128))
except Exception as e:
pytest.skip(f"Skipping test for backend '{attention_backend}': {e}")
torch.compiler.reset()
compiled_attention = torch.compile(dispatch_attention_fn, dynamic=True, fullgraph=True)
try:
with (
torch._inductor.utils.fresh_inductor_cache(),
torch._dynamo.config.patch(error_on_recompile=True),
):
for seq_len in (128, 256, 256):
q, k, v = make_qkv(seq_len)
torch.testing.assert_close(
compiled_attention(q, k, v), dispatch_attention_fn(q, k, v), atol=1e-3, rtol=1e-3
)
finally:
torch.compiler.reset()