Files
2026-07-13 12:40:42 +08:00

559 lines
18 KiB
Python

# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
#
# 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 unittest
import numpy as np
from op_test import get_cuda_version, is_custom_device
import paddle
import paddle.nn.functional as F
from paddle.nn.attention import (
SDPBackend,
_cur_sdpa_kernel_backends,
sdpa_kernel,
)
from paddle.nn.functional import scaled_dot_product_attention
def is_flashattn_supported():
if (
not paddle.base.core.is_compiled_with_cuda()
or get_cuda_version() < 11040
):
return False
if paddle.device.cuda.device_count() == 0:
return False
try:
capability = paddle.device.cuda.get_device_capability()
major, minor = capability[0], capability[1]
# Support sm8x or sm90
return (major == 8 and minor >= 0) or (major == 9 and minor == 0)
except:
return False
def attention_naive(q, k, v, causal=False):
"""Reference implementation for attention calculation."""
qt = paddle.transpose(q, [0, 2, 1, 3])
kt = paddle.transpose(k, [0, 2, 1, 3])
vt = paddle.transpose(v, [0, 2, 1, 3])
scale = 1.0 / np.sqrt(q.shape[-1])
s = paddle.matmul(qt * scale, paddle.transpose(kt, [0, 1, 3, 2]))
if causal:
mask = paddle.triu(paddle.ones_like(s) * -float('inf'), diagonal=1)
s = s + mask
p = F.softmax(s)
o = paddle.matmul(p, vt)
return paddle.transpose(o, [0, 2, 1, 3])
@unittest.skipIf(
paddle.is_compiled_with_xpu(),
"sdpa backend selection logic fails on XPU when testing CPU place",
)
class TestSDPAKernelCPU(unittest.TestCase):
"""Test sdpa_kernel on CPU specifically."""
def setUp(self):
self.place = paddle.CPUPlace()
self.shape = (2, 128, 8, 16)
self.dtype = 'float32'
def test_cpu_math_backend(self):
"""Test MATH backend on CPU."""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
key = np.random.random(self.shape).astype(self.dtype)
value = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(
query, place=self.place, dtype=self.dtype, stop_gradient=False
)
k = paddle.to_tensor(
key, place=self.place, dtype=self.dtype, stop_gradient=False
)
v = paddle.to_tensor(
value, place=self.place, dtype=self.dtype, stop_gradient=False
)
q_ = paddle.to_tensor(
query, place=self.place, dtype=self.dtype, stop_gradient=False
)
k_ = paddle.to_tensor(
key, place=self.place, dtype=self.dtype, stop_gradient=False
)
v_ = paddle.to_tensor(
value, place=self.place, dtype=self.dtype, stop_gradient=False
)
with sdpa_kernel(SDPBackend.MATH):
out = scaled_dot_product_attention(q, k, v)
ref_out = attention_naive(q_, k_, v_, causal=False)
np.testing.assert_allclose(
out.numpy(), ref_out.numpy(), rtol=5e-3, atol=1e-3
)
# Test backward
out.backward()
ref_out.backward()
np.testing.assert_allclose(
q.grad.numpy(), q_.grad.numpy(), rtol=5e-3, atol=1e-3
)
np.testing.assert_allclose(
k.grad.numpy(), k_.grad.numpy(), rtol=5e-3, atol=1e-3
)
np.testing.assert_allclose(
v.grad.numpy(), v_.grad.numpy(), rtol=5e-3, atol=1e-3
)
def test_cpu_with_mask(self):
"""Test CPU with attention mask."""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(
query, place=self.place, dtype=self.dtype, stop_gradient=False
)
# Create a mask
mask_shape = (self.shape[0], 1, self.shape[1], self.shape[1])
mask = np.random.random(mask_shape).astype(self.dtype)
m = paddle.to_tensor(mask, place=self.place, dtype=self.dtype)
with sdpa_kernel(SDPBackend.MATH):
out = scaled_dot_product_attention(q, q, q, attn_mask=m)
# Verify output shape and test backward
self.assertEqual(out.shape, q.shape)
out.backward()
@unittest.skipIf(
not (paddle.is_compiled_with_cuda() or is_custom_device())
or paddle.is_compiled_with_rocm(),
"CUDA is not available, this test requires GPU support.",
)
class TestSDPAKernelBasic(unittest.TestCase):
"""Test basic functionality of sdpa_kernel context manager (defaults to available device)."""
def setUp(self):
self.shape = (2, 128, 8, 16)
self.dtype = 'float32'
def test_cur_sdpa_kernel_backends(self):
result = _cur_sdpa_kernel_backends()
self.assertIsInstance(result, list)
def test_single_backend(self):
"""Test with single backend."""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
key = np.random.random(self.shape).astype(self.dtype)
value = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(query, dtype=self.dtype, stop_gradient=False)
k = paddle.to_tensor(key, dtype=self.dtype, stop_gradient=False)
v = paddle.to_tensor(value, dtype=self.dtype, stop_gradient=False)
q_ = paddle.to_tensor(query, dtype=self.dtype, stop_gradient=False)
k_ = paddle.to_tensor(key, dtype=self.dtype, stop_gradient=False)
v_ = paddle.to_tensor(value, dtype=self.dtype, stop_gradient=False)
with sdpa_kernel(SDPBackend.MATH):
out = scaled_dot_product_attention(q, k, v)
ref_out = attention_naive(q_, k_, v_, causal=False)
np.testing.assert_allclose(
out.numpy(), ref_out.numpy(), rtol=5e-3, atol=1e-3
)
# Test backward
out.backward()
ref_out.backward()
np.testing.assert_allclose(
q.grad.numpy(), q_.grad.numpy(), rtol=5e-3, atol=1e-3
)
np.testing.assert_allclose(
k.grad.numpy(), k_.grad.numpy(), rtol=5e-3, atol=1e-3
)
np.testing.assert_allclose(
v.grad.numpy(), v_.grad.numpy(), rtol=5e-3, atol=1e-3
)
def test_multiple_backends(self):
"""Test with multiple backends."""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
key = np.random.random(self.shape).astype(self.dtype)
value = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(query, dtype=self.dtype, stop_gradient=False)
k = paddle.to_tensor(key, dtype=self.dtype, stop_gradient=False)
v = paddle.to_tensor(value, dtype=self.dtype, stop_gradient=False)
q_ = paddle.to_tensor(query, dtype=self.dtype, stop_gradient=False)
k_ = paddle.to_tensor(key, dtype=self.dtype, stop_gradient=False)
v_ = paddle.to_tensor(value, dtype=self.dtype, stop_gradient=False)
# Test with multiple backends
backends = [SDPBackend.MATH, SDPBackend.EFFICIENT_ATTENTION]
with sdpa_kernel(backends):
out = scaled_dot_product_attention(q, k, v)
ref_out = attention_naive(q_, k_, v_, causal=False)
np.testing.assert_allclose(
out.numpy(), ref_out.numpy(), rtol=5e-3, atol=1e-3
)
# Test backward
out.backward()
ref_out.backward()
np.testing.assert_allclose(
q.grad.numpy(), q_.grad.numpy(), rtol=5e-3, atol=1e-3
)
np.testing.assert_allclose(
k.grad.numpy(), k_.grad.numpy(), rtol=5e-3, atol=1e-3
)
np.testing.assert_allclose(
v.grad.numpy(), v_.grad.numpy(), rtol=5e-3, atol=1e-3
)
def test_multiple_backends_with_priority(self):
"""
Test set_priority=True with available backends (MATH, EFFICIENT).
"""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
key = np.random.random(self.shape).astype(self.dtype)
value = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(query, dtype=self.dtype, stop_gradient=False)
k = paddle.to_tensor(key, dtype=self.dtype, stop_gradient=False)
v = paddle.to_tensor(value, dtype=self.dtype, stop_gradient=False)
q_ = paddle.to_tensor(query, dtype=self.dtype, stop_gradient=False)
k_ = paddle.to_tensor(key, dtype=self.dtype, stop_gradient=False)
v_ = paddle.to_tensor(value, dtype=self.dtype, stop_gradient=False)
backends = [SDPBackend.MATH, SDPBackend.EFFICIENT_ATTENTION]
with sdpa_kernel(backends, set_priority=True):
out = scaled_dot_product_attention(q, k, v)
ref_out = attention_naive(q_, k_, v_, causal=False)
np.testing.assert_allclose(
out.numpy(), ref_out.numpy(), rtol=5e-3, atol=1e-3
)
out.backward()
ref_out.backward()
np.testing.assert_allclose(
q.grad.numpy(), q_.grad.numpy(), rtol=5e-3, atol=1e-3
)
np.testing.assert_allclose(
k.grad.numpy(), k_.grad.numpy(), rtol=5e-3, atol=1e-3
)
np.testing.assert_allclose(
v.grad.numpy(), v_.grad.numpy(), rtol=5e-3, atol=1e-3
)
@unittest.skipIf(
not is_flashattn_supported(),
"Priority test requires flash attention support (CUDA SM80+)",
)
class TestSDPAKernelPriority(unittest.TestCase):
"""Test priority settings for sdpa_kernel."""
def setUp(self):
self.shape = (2, 64, 4, 32)
self.dtype = 'float16'
def test_set_priority_true(self):
"""Test set_priority=True."""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(query, dtype=self.dtype, stop_gradient=False)
q_ = paddle.to_tensor(query, dtype=self.dtype, stop_gradient=False)
backends = [SDPBackend.FLASH_ATTENTION, SDPBackend.MATH]
with sdpa_kernel(backends, set_priority=True):
out = scaled_dot_product_attention(q, q, q)
# Verify output correctness
ref_out = attention_naive(q_, q_, q_, causal=False)
np.testing.assert_allclose(
out.numpy(), ref_out.numpy(), rtol=5e-3, atol=1e-3
)
# Test backward
out.backward()
ref_out.backward()
np.testing.assert_allclose(
q.grad.numpy(), q_.grad.numpy(), rtol=5e-3, atol=1e-3
)
def test_set_priority_false(self):
"""Test set_priority=False (default)."""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(query, dtype=self.dtype, stop_gradient=False)
q_ = paddle.to_tensor(query, dtype=self.dtype, stop_gradient=False)
backends = [SDPBackend.MATH, SDPBackend.EFFICIENT_ATTENTION]
with sdpa_kernel(backends, set_priority=False):
out = scaled_dot_product_attention(q, q, q)
ref_out = attention_naive(q_, q_, q_, causal=False)
np.testing.assert_allclose(
out.numpy(), ref_out.numpy(), rtol=5e-3, atol=1e-3
)
# Test backward
out.backward()
ref_out.backward()
np.testing.assert_allclose(
q.grad.numpy(), q_.grad.numpy(), rtol=5e-3, atol=1e-3
)
class TestSDPAKernelExceptions(unittest.TestCase):
"""Test exception handling in sdpa_kernel."""
def test_invalid_backend_type(self):
"""Test with invalid backend type."""
with self.assertRaises(AssertionError), sdpa_kernel("invalid_backend"):
pass
def test_invalid_backend_in_list(self):
"""Test with invalid backend in list."""
with (
self.assertRaises(TypeError),
sdpa_kernel([SDPBackend.MATH, "invalid"]),
):
pass
def test_empty_backend_list(self):
"""Test with empty backend list."""
with self.assertRaises(ValueError), sdpa_kernel([]):
pass
@unittest.skipIf(
not is_flashattn_supported(),
"core is not compiled with CUDA and cuda version need larger than or equal to 11.4"
"and device's compute capability must be 8.x or 90",
)
class TestSDPAKernelGPU(unittest.TestCase):
"""Test sdpa_kernel on GPU with different backends."""
def setUp(self):
self.place = paddle.CUDAPlace(0)
self.shape = (2, 128, 8, 32)
self.dtype = 'float16'
def test_gpu_math_backend(self):
"""Test MATH backend on GPU."""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
key = np.random.random(self.shape).astype(self.dtype)
value = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(
query, place=self.place, dtype=self.dtype, stop_gradient=False
)
k = paddle.to_tensor(
key, place=self.place, dtype=self.dtype, stop_gradient=False
)
v = paddle.to_tensor(
value, place=self.place, dtype=self.dtype, stop_gradient=False
)
q_ = paddle.to_tensor(
query, place=self.place, dtype=self.dtype, stop_gradient=False
)
k_ = paddle.to_tensor(
key, place=self.place, dtype=self.dtype, stop_gradient=False
)
v_ = paddle.to_tensor(
value, place=self.place, dtype=self.dtype, stop_gradient=False
)
with sdpa_kernel(SDPBackend.MATH):
out = scaled_dot_product_attention(q, k, v)
# Convert to float32 for comparison
q_fp32 = q_.astype('float32')
k_fp32 = k_.astype('float32')
v_fp32 = v_.astype('float32')
ref_out = attention_naive(q_fp32, k_fp32, v_fp32, causal=False)
np.testing.assert_allclose(
out.astype('float32').numpy(), ref_out.numpy(), rtol=5e-3, atol=1e-3
)
# Test backward
out.backward()
ref_out.backward()
np.testing.assert_allclose(
q.grad.astype('float32').numpy(),
q_.grad.numpy(),
rtol=5e-3,
atol=1e-3,
)
def test_flash_attention_backend(self):
"""Test FLASH_ATTENTION backend on GPU."""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
key = np.random.random(self.shape).astype(self.dtype)
value = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(
query, place=self.place, dtype=self.dtype, stop_gradient=False
)
k = paddle.to_tensor(
key, place=self.place, dtype=self.dtype, stop_gradient=False
)
v = paddle.to_tensor(
value, place=self.place, dtype=self.dtype, stop_gradient=False
)
q_ = paddle.to_tensor(
query, place=self.place, dtype=self.dtype, stop_gradient=False
)
k_ = paddle.to_tensor(
key, place=self.place, dtype=self.dtype, stop_gradient=False
)
v_ = paddle.to_tensor(
value, place=self.place, dtype=self.dtype, stop_gradient=False
)
try:
with sdpa_kernel(SDPBackend.FLASH_ATTENTION):
out = scaled_dot_product_attention(q, k, v)
# Convert to float32 for comparison
q_fp32 = q_.astype('float32')
k_fp32 = k_.astype('float32')
v_fp32 = v_.astype('float32')
ref_out = attention_naive(q_fp32, k_fp32, v_fp32, causal=False)
np.testing.assert_allclose(
out.astype('float32').numpy(),
ref_out.numpy(),
rtol=5e-3,
atol=1e-3,
)
# Test backward
out.backward()
ref_out.backward()
np.testing.assert_allclose(
q.grad.astype('float32').numpy(),
q_.grad.numpy(),
rtol=5e-3,
atol=1e-3,
)
except RuntimeError:
# Flash attention might not be available
self.skipTest("Flash attention not available on this GPU")
def test_efficient_attention_backend(self):
"""Test EFFICIENT_ATTENTION backend on GPU."""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(
query, place=self.place, dtype=self.dtype, stop_gradient=False
)
q_ = paddle.to_tensor(
query, place=self.place, dtype=self.dtype, stop_gradient=False
)
try:
with sdpa_kernel(SDPBackend.EFFICIENT_ATTENTION):
out = scaled_dot_product_attention(q, q, q)
# Convert to float32 for comparison
q_fp32 = q_.astype('float32')
ref_out = attention_naive(q_fp32, q_fp32, q_fp32, causal=False)
np.testing.assert_allclose(
out.astype('float32').numpy(),
ref_out.numpy(),
rtol=5e-3,
atol=1e-3,
)
# Test backward
out.backward()
ref_out.backward()
np.testing.assert_allclose(
q.grad.astype('float32').numpy(),
q_.grad.numpy(),
rtol=5e-3,
atol=1e-3,
)
except RuntimeError:
# Efficient attention might not be available
self.skipTest("Efficient attention not available on this GPU")
def test_all_backends_gpu(self):
"""Test all backends on GPU."""
paddle.disable_static()
query = np.random.random(self.shape).astype(self.dtype)
q = paddle.to_tensor(
query, place=self.place, dtype=self.dtype, stop_gradient=False
)
backends = [
SDPBackend.FLASH_ATTENTION,
SDPBackend.EFFICIENT_ATTENTION,
SDPBackend.MATH,
]
with sdpa_kernel(backends):
out = scaled_dot_product_attention(q, q, q)
# Verify output shape and test backward
self.assertEqual(out.shape, q.shape)
out.backward()
if __name__ == '__main__':
unittest.main()