test: cover padded SDPA outputs gradients and fallback paths
parent
c401918813
commit
e4872e6898
|
|
@ -0,0 +1,160 @@
|
|||
"""Regression tests for padded SDPA; no model weights or datasets are needed.
|
||||
|
||||
Run: python -m unittest discover -s tests -p test_attention_sdpa.py -v
|
||||
"""
|
||||
import copy
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from model.model_minimind import Attention, MiniMindConfig, precompute_freqs_cis
|
||||
|
||||
|
||||
class AttentionSDPATest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.old_threads = torch.get_num_threads()
|
||||
torch.set_num_threads(min(cls.old_threads, 2))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
torch.set_num_threads(cls.old_threads)
|
||||
|
||||
def make_inputs(self, device="cpu", dtype=torch.float32, dropout=0.0):
|
||||
torch.manual_seed(42)
|
||||
config = MiniMindConfig(hidden_size=64, num_hidden_layers=1,
|
||||
num_attention_heads=4, num_key_value_heads=2,
|
||||
head_dim=16, dropout=dropout)
|
||||
attention = Attention(config).to(device=device, dtype=dtype).eval()
|
||||
inputs = torch.randn(2, 12, 64, device=device, dtype=dtype)
|
||||
positions = tuple(t.to(device) for t in precompute_freqs_cis(16, end=12))
|
||||
return attention, inputs, positions
|
||||
|
||||
def check_parity(self, device, dtype):
|
||||
for causal in (True, False):
|
||||
for padding in ("none", "ones", "left", "right", "interior"):
|
||||
with self.subTest(device=device, causal=causal, padding=padding):
|
||||
candidate, inputs, positions = self.make_inputs(device, dtype)
|
||||
candidate.is_causal = causal
|
||||
reference = copy.deepcopy(candidate)
|
||||
candidate.flash, reference.flash = True, False
|
||||
mask = torch.ones(2, 12, device=device, dtype=torch.long)
|
||||
if padding == "left":
|
||||
mask[0, :4], mask[1, :2] = 0, 0
|
||||
elif padding == "right":
|
||||
mask[0, 8:], mask[1, 10:] = 0, 0
|
||||
elif padding == "interior":
|
||||
mask[0, 3:5], mask[1, 6:8] = 0, 0
|
||||
valid = mask.bool()
|
||||
passed_mask = None if padding == "none" else mask
|
||||
outputs, gradients = [], []
|
||||
target = torch.randn_like(inputs).float()
|
||||
for attention in (reference, candidate):
|
||||
x = inputs.detach().clone().requires_grad_(True)
|
||||
output, _ = attention(x, positions, attention_mask=passed_mask)
|
||||
# Fully masked padded query rows are not equivalent to the
|
||||
# old finite -1e9 mask, and must not contribute to the loss.
|
||||
loss = (output[valid].float() - target[valid]).square().mean()
|
||||
loss.backward()
|
||||
self.assertTrue(torch.isfinite(output).all().item())
|
||||
gradient = torch.cat([x.grad.float().flatten()] + [
|
||||
p.grad.float().flatten() for p in attention.parameters()
|
||||
if p.grad is not None
|
||||
])
|
||||
self.assertTrue(torch.isfinite(gradient).all().item())
|
||||
outputs.append(output.detach()[valid].float())
|
||||
gradients.append(gradient)
|
||||
bf16 = dtype == torch.bfloat16
|
||||
torch.testing.assert_close(outputs[1], outputs[0],
|
||||
atol=1e-2 if bf16 else 1e-5,
|
||||
rtol=3e-2 if bf16 else 1e-5)
|
||||
# Compare the gradient vectors, not just their norms.
|
||||
relative_error = ((gradients[1] - gradients[0]).norm()
|
||||
/ gradients[0].norm().clamp_min(1e-12)).item()
|
||||
self.assertLess(relative_error, 3e-2 if bf16 else 1e-4)
|
||||
|
||||
def test_cpu_outputs_and_full_gradients(self):
|
||||
self.check_parity("cpu", torch.float32)
|
||||
|
||||
@unittest.skipUnless(torch.cuda.is_available() and torch.cuda.is_bf16_supported(),
|
||||
"CUDA BF16 is optional")
|
||||
def test_cuda_bf16_outputs_and_full_gradients(self):
|
||||
self.check_parity("cuda", torch.bfloat16)
|
||||
|
||||
def test_padded_inputs_use_combined_sdpa_mask(self):
|
||||
attention, inputs, positions = self.make_inputs()
|
||||
mask = torch.ones(2, 12, dtype=torch.long)
|
||||
mask[0, :3], mask[1, 9:] = 0, 0
|
||||
for causal in (True, False):
|
||||
attention.is_causal = causal
|
||||
with self.subTest(causal=causal), patch(
|
||||
"model.model_minimind.F.scaled_dot_product_attention",
|
||||
wraps=F.scaled_dot_product_attention,
|
||||
) as sdpa:
|
||||
attention(inputs, positions, attention_mask=mask)
|
||||
sdpa.assert_called_once()
|
||||
kwargs = sdpa.call_args.kwargs
|
||||
expected = torch.ones(12, 12, dtype=torch.bool)
|
||||
if causal:
|
||||
expected = expected.tril()
|
||||
expected = expected & mask.bool()[:, None, None, :]
|
||||
torch.testing.assert_close(kwargs["attn_mask"], expected)
|
||||
self.assertFalse(kwargs["is_causal"])
|
||||
|
||||
def test_unpadded_inputs_keep_original_causal_sdpa(self):
|
||||
attention, inputs, positions = self.make_inputs()
|
||||
for mask in (None, torch.ones(2, 12, dtype=torch.long)):
|
||||
with self.subTest(mask_is_none=mask is None), patch(
|
||||
"model.model_minimind.F.scaled_dot_product_attention",
|
||||
wraps=F.scaled_dot_product_attention,
|
||||
) as sdpa:
|
||||
attention(inputs, positions, attention_mask=mask)
|
||||
sdpa.assert_called_once()
|
||||
self.assertIsNone(sdpa.call_args.kwargs["attn_mask"])
|
||||
self.assertTrue(sdpa.call_args.kwargs["is_causal"])
|
||||
|
||||
def test_manual_fallbacks_and_cached_continuation(self):
|
||||
attention, inputs, positions = self.make_inputs()
|
||||
mask = torch.ones(2, 12, dtype=torch.long)
|
||||
mask[0, :2] = 0
|
||||
attention.flash = False
|
||||
with torch.no_grad():
|
||||
_, cache = attention(inputs[:, :8], tuple(t[:8] for t in positions),
|
||||
use_cache=True, attention_mask=mask[:, :8])
|
||||
for length, past, enabled in ((12, None, False), (1, None, True),
|
||||
(1, cache, True), (4, cache, True)):
|
||||
with self.subTest(length=length, cached=past is not None, flash=enabled):
|
||||
start = 8 if past is not None else 0
|
||||
x = inputs[:, start:start + length]
|
||||
pos = tuple(t[start:start + length] for t in positions)
|
||||
passed_mask = mask[:, :start + length]
|
||||
attention.flash = enabled
|
||||
with patch("model.model_minimind.F.scaled_dot_product_attention",
|
||||
wraps=F.scaled_dot_product_attention) as sdpa:
|
||||
output, _ = attention(x, pos, past_key_value=past,
|
||||
attention_mask=passed_mask)
|
||||
sdpa.assert_not_called()
|
||||
reference = copy.deepcopy(attention)
|
||||
reference.flash = False
|
||||
expected, _ = reference(x, pos, past_key_value=past,
|
||||
attention_mask=passed_mask)
|
||||
torch.testing.assert_close(output, expected, atol=0, rtol=0)
|
||||
|
||||
def test_dropout_probability_tracks_training_mode(self):
|
||||
attention, inputs, positions = self.make_inputs(dropout=0.2)
|
||||
mask = torch.ones(2, 12, dtype=torch.long)
|
||||
mask[:, -2:] = 0
|
||||
for training, probability in ((True, 0.2), (False, 0.0)):
|
||||
attention.train(training)
|
||||
with self.subTest(training=training), patch(
|
||||
"model.model_minimind.F.scaled_dot_product_attention",
|
||||
wraps=F.scaled_dot_product_attention,
|
||||
) as sdpa:
|
||||
attention(inputs, positions, attention_mask=mask)
|
||||
self.assertEqual(sdpa.call_args.kwargs["dropout_p"], probability)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue