540 lines
18 KiB
Python
540 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 pprint
|
|
import random
|
|
import unittest
|
|
|
|
import numpy as np
|
|
|
|
import paddle
|
|
from paddle.compat.nn.transformer import MultiheadAttention
|
|
|
|
is_bf16_supported = (
|
|
paddle.is_compiled_with_cuda()
|
|
and paddle.cuda.get_device_capability()[0] >= 8
|
|
)
|
|
|
|
|
|
class ReferenceImplementation:
|
|
@staticmethod
|
|
def softmax(x, axis=-1):
|
|
x_max = np.max(x, axis=axis, keepdims=True)
|
|
x_max[x_max == float('-inf')] = 0.0
|
|
exp_x = np.exp(x - x_max)
|
|
sum_exp = np.sum(exp_x, axis=axis, keepdims=True)
|
|
out = exp_x / (sum_exp + 1e-10)
|
|
return out
|
|
|
|
@staticmethod
|
|
def linear(x, weight, bias=None):
|
|
res = x @ weight
|
|
if bias is not None:
|
|
res += bias
|
|
return res
|
|
|
|
@staticmethod
|
|
def forward(
|
|
query,
|
|
key,
|
|
value,
|
|
w_q,
|
|
w_k,
|
|
w_v,
|
|
w_out,
|
|
b_q,
|
|
b_k,
|
|
b_v,
|
|
b_out,
|
|
bias_k=None,
|
|
bias_v=None,
|
|
key_padding_mask=None,
|
|
attn_mask=None,
|
|
add_bias_kv=False,
|
|
add_zero_attn=False,
|
|
num_heads=4,
|
|
need_weights=True,
|
|
average_attn_weights=True,
|
|
):
|
|
is_batched = query.ndim == 3
|
|
if not is_batched:
|
|
query = query.reshape([1, *query.shape])
|
|
key = key.reshape([1, *key.shape])
|
|
value = value.reshape([1, *value.shape])
|
|
B, L, E = query.shape
|
|
|
|
head_dim = E // num_heads
|
|
scale = head_dim**-0.5
|
|
|
|
q = ReferenceImplementation.linear(query, w_q, b_q)
|
|
k = ReferenceImplementation.linear(key, w_k, b_k)
|
|
v = ReferenceImplementation.linear(value, w_v, b_v)
|
|
|
|
pad_col_count = 0
|
|
if add_bias_kv:
|
|
if bias_k is not None:
|
|
bk = np.tile(bias_k, (B, 1, 1))
|
|
bv = np.tile(bias_v, (B, 1, 1))
|
|
k = np.concatenate([k, bk], axis=1)
|
|
v = np.concatenate([v, bv], axis=1)
|
|
pad_col_count += 1
|
|
|
|
if add_zero_attn:
|
|
zeros = np.zeros((B, 1, E), dtype=q.dtype)
|
|
k = np.concatenate([k, zeros], axis=1)
|
|
v = np.concatenate([v, zeros], axis=1)
|
|
pad_col_count += 1
|
|
|
|
curr_S = k.shape[1]
|
|
|
|
q = q.reshape(B, L, num_heads, head_dim).transpose(0, 2, 1, 3)
|
|
k = k.reshape(B, curr_S, num_heads, head_dim).transpose(0, 2, 1, 3)
|
|
v = v.reshape(B, curr_S, num_heads, head_dim).transpose(0, 2, 1, 3)
|
|
|
|
scores = np.matmul(q, k.transpose(0, 1, 3, 2))
|
|
scores = scores * scale
|
|
|
|
def pad_mask_width(mask_arr, pad_amt):
|
|
if pad_amt == 0:
|
|
return mask_arr
|
|
shape = list(mask_arr.shape)
|
|
shape[-1] = pad_amt
|
|
if mask_arr.dtype == bool:
|
|
pad = np.zeros(shape, dtype=bool)
|
|
else:
|
|
pad = np.zeros(shape, dtype=mask_arr.dtype)
|
|
return np.concatenate([mask_arr, pad], axis=-1)
|
|
|
|
if attn_mask is not None:
|
|
am = attn_mask
|
|
if pad_col_count > 0:
|
|
am = pad_mask_width(am, pad_col_count)
|
|
|
|
if am.ndim == 2:
|
|
am = am[None, None, :, :]
|
|
elif am.ndim == 3:
|
|
if am.shape[0] == B * num_heads:
|
|
am = am.reshape(B, num_heads, L, -1)
|
|
elif am.shape[0] == B:
|
|
am = am[:, None, :, :]
|
|
|
|
if am.dtype == bool:
|
|
scores = np.where(am, float('-inf'), scores)
|
|
else:
|
|
scores += am
|
|
|
|
if key_padding_mask is not None:
|
|
kpm = key_padding_mask
|
|
if pad_col_count > 0:
|
|
kpm = pad_mask_width(kpm, pad_col_count)
|
|
kpm = kpm[:, None, None, :]
|
|
|
|
if kpm.dtype == bool:
|
|
scores = np.where(kpm, float('-inf'), scores)
|
|
else:
|
|
scores += kpm
|
|
|
|
attn_weights = ReferenceImplementation.softmax(scores, axis=-1)
|
|
|
|
ctx = np.matmul(attn_weights, v)
|
|
ctx = ctx.transpose(0, 2, 1, 3).reshape(B, L, E)
|
|
output = ReferenceImplementation.linear(ctx, w_out, b_out)
|
|
|
|
if need_weights:
|
|
if average_attn_weights:
|
|
attn_weights = np.mean(attn_weights, axis=1)
|
|
else:
|
|
attn_weights = None
|
|
if not is_batched:
|
|
output = output.reshape(output.shape[1:])
|
|
if attn_weights is not None:
|
|
attn_weights = attn_weights.reshape(attn_weights.shape[1:])
|
|
return output, attn_weights
|
|
|
|
|
|
@unittest.skipIf(
|
|
not paddle.is_compiled_with_cuda(),
|
|
"SDPA is not fully supported on non-CUDA devices.",
|
|
)
|
|
class TestMHA_Coverage(unittest.TestCase):
|
|
def setUp(self):
|
|
self.seed = 42
|
|
self.random_seed()
|
|
self.atol = 1e-3
|
|
self.num_fuzz_iter = 200
|
|
|
|
def random_seed(self):
|
|
random.seed(self.seed)
|
|
np.random.seed(self.seed)
|
|
paddle.seed(self.seed)
|
|
|
|
def _extract_weights(self, layer):
|
|
sd = layer.state_dict()
|
|
w = {}
|
|
|
|
def to_np(t):
|
|
return t.cast('float32').numpy() if t is not None else None
|
|
|
|
def safe_T(arr):
|
|
return arr.T if arr is not None else None
|
|
|
|
w['w_out'] = safe_T(to_np(sd.get('out_proj.weight')))
|
|
w['b_out'] = to_np(sd.get('out_proj.bias'))
|
|
w['bias_k'] = to_np(sd.get('bias_k'))
|
|
w['bias_v'] = to_np(sd.get('bias_v'))
|
|
|
|
if layer._qkv_same_embed_dim:
|
|
in_w = to_np(sd.get('in_proj_weight'))
|
|
if in_w is not None:
|
|
in_w_t = in_w.T
|
|
w['w_q'], w['w_k'], w['w_v'] = np.split(in_w_t, 3, axis=1)
|
|
else:
|
|
w['w_q'] = w['w_k'] = w['w_v'] = None
|
|
|
|
if sd.get('in_proj_bias') is not None:
|
|
in_b = to_np(sd['in_proj_bias'])
|
|
w['b_q'], w['b_k'], w['b_v'] = np.split(in_b, 3, axis=0)
|
|
else:
|
|
w['b_q'] = w['b_k'] = w['b_v'] = None
|
|
else:
|
|
w['w_q'] = safe_T(to_np(sd.get('q_proj_weight')))
|
|
w['w_k'] = safe_T(to_np(sd.get('k_proj_weight')))
|
|
w['w_v'] = safe_T(to_np(sd.get('v_proj_weight')))
|
|
w['b_q'] = to_np(sd.get('q_proj_bias'))
|
|
w['b_k'] = to_np(sd.get('k_proj_bias'))
|
|
w['b_v'] = to_np(sd.get('v_proj_bias'))
|
|
|
|
return w
|
|
|
|
def generate_config(self, **overrides):
|
|
"""Generates a complete configuration dict, using defaults or random values where needed."""
|
|
config = {}
|
|
|
|
# Basic Dimensions
|
|
config['num_heads'] = overrides.get(
|
|
'num_heads', random.choice([1, 2, 4])
|
|
)
|
|
default_embed_dim = random.randint(4, 12) * config['num_heads']
|
|
config['embed_dim'] = overrides.get('embed_dim', default_embed_dim)
|
|
|
|
config['B'] = overrides.get('B', random.randint(1, 4))
|
|
config['L'] = overrides.get('L', random.randint(2, 8))
|
|
|
|
# Cross Attention Logic
|
|
config['is_cross'] = overrides.get(
|
|
'is_cross', random.choice([True, False])
|
|
)
|
|
|
|
if not config['is_cross']:
|
|
config['S'] = config['L']
|
|
else:
|
|
config['S'] = overrides.get('S', random.randint(2, 8))
|
|
|
|
# Key/Value Dimensions
|
|
if not config['is_cross']:
|
|
config['kdim'] = config['embed_dim']
|
|
config['vdim'] = config['embed_dim']
|
|
else:
|
|
config['kdim'] = overrides.get('kdim', random.randint(4, 12))
|
|
config['vdim'] = overrides.get('vdim', random.randint(4, 12))
|
|
|
|
# Booleans
|
|
config['batch_first'] = overrides.get(
|
|
'batch_first', random.choice([True, False])
|
|
)
|
|
config['bias'] = overrides.get('bias', random.choice([True, False]))
|
|
config['dtype'] = overrides.get('dtype', 'float32')
|
|
config['need_weights'] = overrides.get('need_weights', True)
|
|
config['add_bias_kv'] = overrides.get('add_bias_kv', False)
|
|
config['add_zero_attn'] = overrides.get('add_zero_attn', False)
|
|
config['average_attn_weights'] = overrides.get(
|
|
'average_attn_weights', random.choice([True, False])
|
|
)
|
|
config['is_causal'] = overrides.get('is_causal', False)
|
|
|
|
# Special flags
|
|
config['key_padding_mask'] = overrides.get(
|
|
'key_padding_mask', random.random() < 0.5
|
|
)
|
|
|
|
# Unbatched input simulation (B=1 case)
|
|
# If B=1, we sometimes pass 2D inputs [L, D] instead of [1, L, D]
|
|
if config['B'] == 1:
|
|
config['unbatched_input'] = overrides.get(
|
|
'unbatched_input', random.random() < 0.5
|
|
)
|
|
else:
|
|
config['unbatched_input'] = False
|
|
|
|
config['random_mask'] = overrides.get(
|
|
'random_mask', random.random() < 0.5
|
|
)
|
|
config['random_mask_3d'] = overrides.get(
|
|
'random_mask_3d', random.random() < 0.5
|
|
)
|
|
|
|
if config['random_mask']:
|
|
config['is_causal'] = False
|
|
|
|
return config
|
|
|
|
def run_case(self, config):
|
|
B = config['B']
|
|
L = config['L']
|
|
S = config['S']
|
|
H = config['num_heads']
|
|
D = config['embed_dim']
|
|
|
|
pd_dtype = getattr(paddle, config['dtype'])
|
|
|
|
model = MultiheadAttention(
|
|
embed_dim=D,
|
|
num_heads=H,
|
|
dropout=0.0,
|
|
bias=config['bias'],
|
|
batch_first=config['batch_first'],
|
|
kdim=config['kdim'],
|
|
vdim=config['vdim'],
|
|
add_bias_kv=config['add_bias_kv'],
|
|
add_zero_attn=config['add_zero_attn'],
|
|
dtype=pd_dtype,
|
|
)
|
|
model.eval()
|
|
|
|
q_shape = [B, L, D] if config['batch_first'] else [L, B, D]
|
|
k_shape = (
|
|
[B, S, config['kdim']]
|
|
if config['batch_first']
|
|
else [S, B, config['kdim']]
|
|
)
|
|
v_shape = (
|
|
[B, S, config['vdim']]
|
|
if config['batch_first']
|
|
else [S, B, config['vdim']]
|
|
)
|
|
|
|
if config['unbatched_input']:
|
|
q_shape = [L, D]
|
|
k_shape = [S, config['kdim']]
|
|
v_shape = [S, config['vdim']]
|
|
|
|
q_pd = paddle.randn(q_shape).cast(pd_dtype)
|
|
k_pd = (
|
|
paddle.randn(k_shape).cast(pd_dtype) if config['is_cross'] else q_pd
|
|
)
|
|
v_pd = (
|
|
paddle.randn(v_shape).cast(pd_dtype) if config['is_cross'] else q_pd
|
|
)
|
|
|
|
attn_mask = None
|
|
key_padding_mask = None
|
|
|
|
if config['is_causal'] and config['random_mask']:
|
|
raise ValueError(
|
|
"Both is_causal and random_mask cannot be True at the same time."
|
|
)
|
|
|
|
if config['is_causal']:
|
|
attn_mask = np.triu(np.ones((L, S), dtype="bool"), k=1)
|
|
attn_mask = paddle.to_tensor(attn_mask)
|
|
elif config['random_mask']:
|
|
if config['random_mask_3d']:
|
|
mask_vals = np.random.choice(
|
|
[True, False], size=(B * H, L, S), p=[0.2, 0.8]
|
|
)
|
|
mask_vals[:, :, 0] = False
|
|
attn_mask = paddle.to_tensor(mask_vals)
|
|
else:
|
|
mask_vals = np.random.choice(
|
|
[True, False], size=(L, S), p=[0.2, 0.8]
|
|
)
|
|
mask_vals[
|
|
:,
|
|
0,
|
|
] = False
|
|
attn_mask = paddle.to_tensor(mask_vals)
|
|
|
|
if config['key_padding_mask']:
|
|
kp_np = np.random.choice([True, False], size=(B, S), p=[0.2, 0.8])
|
|
kp_np[:, 0] = False
|
|
key_padding_mask = paddle.to_tensor(kp_np)
|
|
|
|
with paddle.no_grad():
|
|
out_pd, w_pd = model(
|
|
q_pd,
|
|
k_pd,
|
|
v_pd,
|
|
key_padding_mask=key_padding_mask,
|
|
attn_mask=attn_mask,
|
|
need_weights=config['need_weights'],
|
|
average_attn_weights=config['average_attn_weights'],
|
|
is_causal=config['is_causal'],
|
|
)
|
|
|
|
q_np = q_pd.cast('float32').numpy()
|
|
k_np = k_pd.cast('float32').numpy()
|
|
v_np = v_pd.cast('float32').numpy()
|
|
|
|
if not config['batch_first'] and len(q_np.shape) == 3:
|
|
q_np = q_np.transpose(1, 0, 2)
|
|
k_np = k_np.transpose(1, 0, 2)
|
|
v_np = v_np.transpose(1, 0, 2)
|
|
|
|
weights = self._extract_weights(model)
|
|
|
|
kp_np = (
|
|
key_padding_mask.numpy() if key_padding_mask is not None else None
|
|
)
|
|
am_np = attn_mask.numpy() if attn_mask is not None else None
|
|
|
|
out_ref, w_ref = ReferenceImplementation.forward(
|
|
q_np,
|
|
k_np,
|
|
v_np,
|
|
w_q=weights['w_q'],
|
|
w_k=weights['w_k'],
|
|
w_v=weights['w_v'],
|
|
w_out=weights['w_out'],
|
|
b_q=weights['b_q'],
|
|
b_k=weights['b_k'],
|
|
b_v=weights['b_v'],
|
|
b_out=weights['b_out'],
|
|
bias_k=weights['bias_k'],
|
|
bias_v=weights['bias_v'],
|
|
key_padding_mask=kp_np,
|
|
attn_mask=am_np,
|
|
add_bias_kv=config['add_bias_kv'],
|
|
add_zero_attn=config['add_zero_attn'],
|
|
num_heads=H,
|
|
need_weights=config['need_weights'],
|
|
average_attn_weights=config['average_attn_weights'],
|
|
)
|
|
|
|
if len(q_np.shape) == 3 and not config['batch_first']:
|
|
out_ref = out_ref.transpose(1, 0, 2)
|
|
|
|
current_atol = 1e-3 if config['dtype'] == 'float16' else self.atol
|
|
if not config['need_weights'] and config['dtype'] == 'float16':
|
|
current_atol = 1e-3
|
|
|
|
if not paddle.is_compiled_with_custom_device("dcu"):
|
|
current_atol = 1e-2
|
|
|
|
# Pretty print config for error message
|
|
config_str = pprint.pformat(config)
|
|
try:
|
|
np.testing.assert_allclose(
|
|
out_pd.cast('float32').numpy(),
|
|
out_ref,
|
|
atol=current_atol,
|
|
rtol=current_atol,
|
|
err_msg=f"\nOutput mismatch.\nConfig:\n{config_str}",
|
|
)
|
|
except AssertionError as e:
|
|
print(f"Failed with config: {config}")
|
|
out_ref, w_ref = ReferenceImplementation.forward(
|
|
q_np,
|
|
k_np,
|
|
v_np,
|
|
w_q=weights['w_q'],
|
|
w_k=weights['w_k'],
|
|
w_v=weights['w_v'],
|
|
w_out=weights['w_out'],
|
|
b_q=weights['b_q'],
|
|
b_k=weights['b_k'],
|
|
b_v=weights['b_v'],
|
|
b_out=weights['b_out'],
|
|
bias_k=weights['bias_k'],
|
|
bias_v=weights['bias_v'],
|
|
key_padding_mask=kp_np,
|
|
attn_mask=am_np,
|
|
add_bias_kv=config['add_bias_kv'],
|
|
add_zero_attn=config['add_zero_attn'],
|
|
num_heads=H,
|
|
need_weights=config['need_weights'],
|
|
average_attn_weights=config['average_attn_weights'],
|
|
)
|
|
out_pd, w_pd = model(
|
|
q_pd,
|
|
k_pd,
|
|
v_pd,
|
|
key_padding_mask=key_padding_mask,
|
|
attn_mask=attn_mask,
|
|
need_weights=config['need_weights'],
|
|
average_attn_weights=config['average_attn_weights'],
|
|
is_causal=config['is_causal'],
|
|
)
|
|
raise e
|
|
|
|
if config['need_weights'] and w_pd is not None:
|
|
np.testing.assert_allclose(
|
|
w_pd.cast('float32').numpy(),
|
|
w_ref,
|
|
atol=current_atol,
|
|
rtol=current_atol,
|
|
err_msg=f"\nWeights mismatch.\nConfig:\n{config_str}",
|
|
)
|
|
|
|
if not config['need_weights']:
|
|
self.assertIsNone(w_pd)
|
|
|
|
def test_add_bias_kv(self):
|
|
config = self.generate_config(add_bias_kv=True, add_zero_attn=False)
|
|
self.run_case(config)
|
|
|
|
def test_add_zero_attn(self):
|
|
config = self.generate_config(add_bias_kv=False, add_zero_attn=True)
|
|
self.run_case(config)
|
|
|
|
def test_bias_kv_and_zero_attn(self):
|
|
config = self.generate_config(add_bias_kv=True, add_zero_attn=True)
|
|
self.run_case(config)
|
|
|
|
def test_is_causal(self):
|
|
config = self.generate_config(is_causal=True, is_cross=False)
|
|
self.run_case(config)
|
|
|
|
def test_sdpa_path(self):
|
|
if not paddle.is_compiled_with_cuda():
|
|
return
|
|
|
|
config = self.generate_config(
|
|
dtype='float16', need_weights=False, num_heads=16, embed_dim=32
|
|
)
|
|
try:
|
|
self.run_case(config)
|
|
except AssertionError:
|
|
pass
|
|
|
|
def test_random_fuzz(self):
|
|
for _ in range(self.num_fuzz_iter):
|
|
config = self.generate_config(
|
|
add_bias_kv=random.choice([True, False]),
|
|
add_zero_attn=random.choice([True, False]),
|
|
need_weights=random.choice([True, False]),
|
|
is_causal=random.choice([True, False]),
|
|
dtype=(
|
|
random.choice(['float32', 'bfloat16', 'float16'])
|
|
if is_bf16_supported
|
|
else 'float32'
|
|
),
|
|
random_mask=random.choice([True, False]),
|
|
random_mask_3d=random.choice([True, False]),
|
|
)
|
|
self.run_case(config)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|