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

513 lines
17 KiB
Python

# Copyright (c) 2024 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, get_device_place, is_custom_device
import paddle
import paddle.nn.functional as F
from paddle.base import core
from paddle.nn.functional.flash_attention import (
flashmask_attention,
)
is_sm8x = (
(core.is_compiled_with_cuda() or is_custom_device())
and paddle.device.cuda.get_device_capability()[0] == 8
and paddle.device.cuda.get_device_capability()[1] >= 0
)
is_sm90 = (
(core.is_compiled_with_cuda() or is_custom_device())
and paddle.device.cuda.get_device_capability()[0] == 9
and paddle.device.cuda.get_device_capability()[1] == 0
)
is_sm_supported = is_sm8x or is_sm90
def is_flashattn_supported():
if (
not (core.is_compiled_with_cuda() or is_custom_device())
or get_cuda_version() < 11040
or not is_sm_supported
):
return False
return True
def attention_naive_with_mask(q, k, v, attn_bias):
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, paddle.transpose(kt, [0, 1, 3, 2]))
s = paddle.scale(s, scale)
p = F.softmax(s + attn_bias)
o = paddle.matmul(p, vt)
return paddle.transpose(o, [0, 2, 1, 3])
def flashmask_to_densemask(startend_row_indices, dtype, causal=True):
bz, num_head, seq_len, bound_num = startend_row_indices.shape
m = paddle.zeros((bz, num_head, seq_len, seq_len), dtype=dtype)
has_end = (causal and bound_num == 2) or ((not causal) and bound_num == 4)
for bi in range(bz):
for hi in range(num_head):
for j in range(seq_len):
downstart = startend_row_indices[bi, hi, j, 0]
if has_end:
downend = startend_row_indices[bi, hi, j, 1]
m[bi, hi, downstart:downend, j] = -np.inf
else:
m[bi, hi, downstart:, j] = -np.inf
if causal:
m[bi, hi, :j, j] = -np.inf
else:
if has_end:
upstart = startend_row_indices[bi, hi, j, 2]
upend = startend_row_indices[bi, hi, j, 3]
m[bi, hi, upstart:upend, j] = -np.inf
else:
upend = startend_row_indices[bi, hi, j, 1]
m[bi, hi, :upend, j] = -np.inf
return m
def gen_random_flashmask(bz, num_head, seqlen, has_end, causal):
mask_num = 1
if not causal:
mask_num *= 2
if has_end:
mask_num *= 2
m = np.random.randint(0, seqlen, (bz, num_head, seqlen, mask_num))
diag = np.arange(seqlen).reshape((1, 1, seqlen))
m[:, :, :, 0] = np.maximum(diag + 1, m[:, :, :, 0])
if not causal:
if has_end:
raise NotImplementedError
else:
m[:, :, :, 1] = np.minimum(diag, m[:, :, :, 1])
else:
if has_end:
m[:, :, :, 1] = m[:, :, :, 0] + 1
m[:, :, :, 1] = np.maximum(m[:, :, :, 0], m[:, :, :, 1])
return paddle.to_tensor(m, dtype="int32")
def gen_casual_document_mask(bz, num_head, seqlen, has_end, causal):
mask_num = 1
assert causal
assert not has_end
rng = np.random.default_rng()
sample_indices = rng.choice(seqlen, size=(int)(seqlen / 10), replace=False)
sample_indices.sort()
m = np.zeros((bz, num_head, seqlen, mask_num))
m[:, :, : sample_indices[0], :] = sample_indices[0]
for i in range(sample_indices.shape[0] - 1):
idx0 = sample_indices[i]
idx1 = sample_indices[i + 1]
m[:, :, idx0:idx1, 0] = idx1
m[:, :, sample_indices[-1] :, 0] = seqlen - 1
diag = np.arange(seqlen).reshape((1, 1, seqlen))
m[:, :, :, 0] = np.maximum(diag + 1, m[:, :, :, 0])
return paddle.to_tensor(m, dtype="int32")
def gen_slide_window_mask(bz, num_head, seqlen, has_end, causal):
mask_num = 1
assert causal
assert not has_end
window_size = np.random.randint(1, 50)
window_size = np.minimum(window_size, seqlen)
m = np.zeros((bz, num_head, seqlen, mask_num))
for i in range(seqlen - window_size):
m[:, :, i, 0] = i + window_size + 1
for i in range(seqlen - window_size, seqlen):
m[:, :, i, 0] = seqlen
diag = np.arange(seqlen).reshape((1, 1, seqlen))
m[:, :, :, 0] = np.maximum(diag + 1, m[:, :, :, 0])
return paddle.to_tensor(m, dtype="int32")
def gen_global_slide_window_mask(bz, num_head, seqlen, has_end, causal):
mask_num = 4
assert not causal
assert has_end
window_size = np.random.randint(1, 50)
window_size = np.minimum(window_size, (int)(seqlen / 4))
m = np.zeros((bz, num_head, seqlen, mask_num))
for i in range(window_size):
m[:, :, i, 0:2] = seqlen
m[:, :, i, 2:4] = 0
for i in range(window_size, 2 * window_size):
m[:, :, i, 0] = i + window_size
m[:, :, i, 1] = seqlen
m[:, :, i, 2] = 0
m[:, :, i, 3] = 0
for i in range(2 * window_size, seqlen - window_size):
m[:, :, i, 0] = i + window_size
m[:, :, i, 1] = seqlen
m[:, :, i, 2] = window_size
m[:, :, i, 3] = i - window_size + 1
for i in range(seqlen - window_size, seqlen):
m[:, :, i, 0] = seqlen
m[:, :, i, 1] = seqlen
m[:, :, i, 2] = window_size
m[:, :, i, 3] = i - window_size + 1
diag = np.arange(seqlen).reshape((1, 1, seqlen))
m[:, :, :, 0] = np.maximum(diag + 1, m[:, :, :, 0])
return paddle.to_tensor(m, dtype="int32")
@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 7.5 or 8.x",
)
class TestFlashMaskAttentionAPI(unittest.TestCase):
def setUp(self):
self.place = get_device_place()
self.shape = (2, 128, 8, 128)
self.dtype = 'float16'
self.dropout = 0.0
self.causal = True
self.has_end = False
self.mask_broadcast = True
self.mask_func = gen_random_flashmask
def get_flashmask(self):
self.startend_row_indices = self.mask_func(
self.shape[0],
1 if self.mask_broadcast else self.shape[2],
self.shape[1],
self.has_end,
self.causal,
)
return self.startend_row_indices
def get_densemask(self):
self.densemask = flashmask_to_densemask(
self.startend_row_indices, "float32", self.causal
)
return self.densemask
def get_inputs(self):
query = np.random.random(self.shape)
key = np.random.random(self.shape)
value = np.random.random(self.shape)
ograd = np.random.random(self.shape)
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
)
ograd = paddle.to_tensor(
ograd, place=self.place, dtype=self.dtype, stop_gradient=False
)
return q, k, v, ograd
def clone_tensors(self, *xs):
ys = [x.detach().clone() for x in xs]
for y in ys:
y.stop_gradient = False
return tuple(ys)
def test_dot_scale_product(self):
# test dynamic
paddle.disable_static()
atol = 1e-2 if self.dtype == "bfloat16" else 1e-3
rtol = 1e-2 if self.dtype == "bfloat16" else 5e-3
q, k, v, ograd = self.get_inputs()
q_, k_, v_, ograd_ = self.clone_tensors(
q.cast("float32"),
k.cast("float32"),
v.cast("float32"),
ograd.cast("float32"),
)
startend_row_indices = self.get_flashmask()
mask = self.get_densemask()
# Note(xhy): ci no supports test on sm90 and blockmask only supports sm >= sm90
blockmask = None
out = flashmask_attention(
q,
k,
v,
startend_row_indices=startend_row_indices,
dropout=self.dropout,
causal=self.causal,
block_mask=blockmask,
)
out_ = attention_naive_with_mask(q_, k_, v_, mask)
out.backward(ograd)
out_.backward(ograd_)
np.testing.assert_allclose(
out.cast("float32").numpy(),
out_.cast("float32").numpy(),
rtol=rtol,
atol=atol,
)
for x, y in [(q, q_), (k, k_), (v, v_)]:
np.testing.assert_allclose(
x.grad.cast("float32").numpy(),
y.grad.cast("float32").numpy(),
rtol=rtol,
atol=atol,
)
class TestFlashMaskAttentionFP16API1(TestFlashMaskAttentionAPI):
def setUp(self):
self.place = get_device_place()
self.shape = (2, 128, 8, 128)
self.dtype = 'float16'
self.dropout = 0.0
self.causal = True
self.has_end = False
self.mask_broadcast = True
self.mask_func = gen_random_flashmask
class TestFlashMaskAttentionBF16API1(TestFlashMaskAttentionAPI):
def setUp(self):
self.place = get_device_place()
self.shape = (2, 128, 8, 128)
self.dtype = 'bfloat16'
self.dropout = 0.0
self.causal = True
self.has_end = False
self.mask_broadcast = True
self.mask_func = gen_random_flashmask
class TestFlashMaskAttentionFP16API2(TestFlashMaskAttentionAPI):
def setUp(self):
self.place = get_device_place()
self.shape = (8, 1024, 16, 128)
self.dtype = 'float16'
self.dropout = 0.0
self.causal = False
self.has_end = False
self.mask_broadcast = True
self.mask_func = gen_random_flashmask
class TestFlashMaskAttentionBF16API2(TestFlashMaskAttentionAPI):
def setUp(self):
self.place = get_device_place()
self.shape = (8, 1024, 16, 128)
self.dtype = 'bfloat16'
self.dropout = 0.0
self.causal = False
self.has_end = False
self.mask_broadcast = True
self.mask_func = gen_random_flashmask
class TestFlashMaskAttentionFP16API3(TestFlashMaskAttentionAPI):
def setUp(self):
self.place = get_device_place()
self.shape = (1, 2048, 16, 96)
self.dtype = 'float16'
self.dropout = 0.0
self.causal = True
self.has_end = False
self.mask_broadcast = False
self.mask_func = gen_random_flashmask
class TestFlashMaskAttentionBF16API3(TestFlashMaskAttentionAPI):
def setUp(self):
self.place = get_device_place()
self.shape = (1, 2048, 16, 96)
self.dtype = 'bfloat16'
self.dropout = 0.0
self.causal = True
self.has_end = False
self.mask_broadcast = False
self.mask_func = gen_random_flashmask
class TestFlashMaskAttentionFP16API4(TestFlashMaskAttentionAPI):
def setUp(self):
self.place = get_device_place()
self.shape = (1, 2048 * 4, 16, 96)
self.dtype = 'float16'
self.dropout = 0.0
self.causal = True
self.has_end = False
self.mask_broadcast = False
self.mask_func = gen_casual_document_mask
class TestFlashMaskAttentionFP16API5(TestFlashMaskAttentionAPI):
def setUp(self):
self.place = get_device_place()
self.shape = (1, 2048 * 4, 16, 96)
self.dtype = 'float16'
self.dropout = 0.0
self.causal = True
self.has_end = False
self.mask_broadcast = False
self.mask_func = gen_slide_window_mask
class TestFlashMaskAttentionFP16API6(TestFlashMaskAttentionAPI):
def setUp(self):
self.place = get_device_place()
self.shape = (1, 2048, 16, 96)
self.dtype = 'float16'
self.dropout = 0.0
self.causal = False
self.has_end = True
self.mask_broadcast = False
self.mask_func = gen_global_slide_window_mask
@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 7.5 or 8.x",
)
class TestFlashMaskAttentionZeroSize(unittest.TestCase):
"""Test flashmask_attention with 0-size tensors.
When any input tensor has a dimension of size 0, the function should
return a zero tensor with the same shape as query without calling
the CUDA kernel to avoid invalid memory access.
"""
def setUp(self):
self.place = get_device_place()
self.dtype = 'float16'
def _run_test(self, q_shape, k_shape, v_shape, startend_shape, causal=True):
"""Helper method to run a single test case."""
paddle.disable_static()
q = paddle.to_tensor(
np.random.randn(*q_shape).astype(self.dtype), place=self.place
)
k = paddle.to_tensor(
np.random.randn(*k_shape).astype(self.dtype), place=self.place
)
v = paddle.to_tensor(
np.random.randn(*v_shape).astype(self.dtype), place=self.place
)
startend = paddle.to_tensor(
np.ones(startend_shape, dtype="int32"), place=self.place
)
result = flashmask_attention(
q,
k,
v,
startend_row_indices=startend,
causal=causal,
)
# According to FlashAttnInferMeta:
# - Output shape is based on q.shape
# - head_dim uses v's head_dim
# - If batch is 0 (q.batch=0 or k.batch=0 or v.batch=0), output batch is 0
expected_shape = list(q.shape)
expected_shape[3] = v_shape[3] # head_dim from v
if q_shape[0] == 0 or k_shape[0] == 0 or v_shape[0] == 0:
expected_shape[0] = 0 # batch is 0
# Verify result shape
self.assertEqual(list(result.shape), expected_shape)
# Verify result is all zeros
self.assertTrue(paddle.all(result == 0).item())
def test_query_zero_seqlen(self):
"""Test when query sequence length is 0."""
self._run_test(
q_shape=[1, 128, 8, 96],
k_shape=[1, 128, 8, 96],
v_shape=[1, 0, 8, 96], # query seqlen = 0
startend_shape=[1, 1, 128, 1],
)
def test_key_zero_seqlen(self):
"""Test when key sequence length is 0."""
self._run_test(
q_shape=[1, 128, 8, 96],
k_shape=[1, 0, 8, 96], # key seqlen = 0
v_shape=[1, 0, 8, 96], # value must have same seqlen as key
startend_shape=[1, 1, 0, 1],
)
def test_key_zero_head_dim(self):
"""Test when key head_dim is 0."""
self._run_test(
q_shape=[1, 128, 8, 96],
k_shape=[1, 128, 8, 0], # key head_dim = 0
v_shape=[1, 128, 8, 96],
startend_shape=[1, 1, 128, 1],
)
def test_value_zero_batch(self):
"""Test when value batch size is 0."""
self._run_test(
q_shape=[1, 128, 8, 96],
k_shape=[1, 128, 8, 96],
v_shape=[0, 128, 8, 96], # value batch = 0
startend_shape=[1, 1, 128, 1],
)
def test_value_zero_seqlen(self):
"""Test when value sequence length is 0."""
self._run_test(
q_shape=[1, 128, 8, 96],
k_shape=[1, 128, 8, 96],
v_shape=[1, 0, 8, 96], # value seqlen = 0
startend_shape=[1, 1, 128, 1],
)
def test_value_zero_head_dim(self):
"""Test when value head_dim is 0."""
self._run_test(
q_shape=[1, 128, 8, 96],
k_shape=[1, 128, 8, 96],
v_shape=[1, 128, 8, 0], # value head_dim = 0
startend_shape=[1, 1, 128, 1],
)
def test_all_zero_batch(self):
"""Test when all tensors have batch size 0."""
self._run_test(
q_shape=[0, 128, 8, 96],
k_shape=[0, 128, 8, 96],
v_shape=[0, 128, 8, 96],
startend_shape=[0, 1, 128, 1],
)
if __name__ == "__main__":
unittest.main()