Spaces:
Sleeping
Sleeping
| import pytest | |
| import torch | |
| from mini_transformer.modules.attention import MultiHeadAttention | |
| # ----------------------- | |
| # Helpers | |
| # ----------------------- | |
| def _rand(B=2, S=5, D=12, device="cpu", dtype=torch.float32): | |
| return torch.randn(B, S, D, device=device, dtype=dtype) | |
| def _pad_mask(batch: int, heads: int, seq_len: int, device) -> torch.Tensor: | |
| return torch.zeros(batch, heads, 1, seq_len, dtype=torch.bool, device=device) | |
| # ======================= | |
| # Constructor checks | |
| # ======================= | |
| def test_ctor_type_checks(): | |
| with pytest.raises(TypeError): | |
| MultiHeadAttention(d_model="64", num_heads=4, dropout_rate=0.1) | |
| with pytest.raises(TypeError): | |
| MultiHeadAttention(d_model=64, num_heads=4.0, dropout_rate=0.1) | |
| with pytest.raises(TypeError): | |
| MultiHeadAttention(d_model=64, num_heads=4, dropout_rate="0.1") | |
| def test_ctor_value_checks(): | |
| with pytest.raises(ValueError): | |
| MultiHeadAttention(d_model=0, num_heads=4, dropout_rate=0.1) | |
| with pytest.raises(ValueError): | |
| MultiHeadAttention(d_model=64, num_heads=0, dropout_rate=0.1) | |
| with pytest.raises(ValueError): | |
| MultiHeadAttention(d_model=63, num_heads=4, dropout_rate=0.1) # not divisible | |
| def test_ctor_happy_path(): | |
| mha = MultiHeadAttention(64, 8, 0.1) | |
| assert mha.d_model == 64 and mha.num_heads == 8 and mha.d_head == 8 | |
| # ======================= | |
| # Forward: type/shape checks | |
| # ======================= | |
| def test_forward_requires_tensors_and_mask_type(): | |
| mha = MultiHeadAttention(32, 4, 0.1) | |
| q, k, v = _rand(2, 5, 32), _rand(2, 5, 32), _rand(2, 5, 32) | |
| mask = _pad_mask(2, mha.num_heads, 5, q.device) | |
| with pytest.raises(TypeError): | |
| mha("q", k, v, mask, mask) | |
| with pytest.raises(TypeError): | |
| mha(q, "k", v, mask, mask) | |
| with pytest.raises(TypeError): | |
| mha(q, k, "v", mask, mask) | |
| with pytest.raises(TypeError): | |
| mha(q, k, v, "bad", mask) | |
| with pytest.raises(TypeError): | |
| mha(q, k, v, mask, "bad") | |
| def test_forward_rank_and_lastdim_checks(): | |
| mha = MultiHeadAttention(32, 4, 0.1) | |
| q = torch.randn(2, 5, 32) | |
| k = torch.randn(2, 5, 32) | |
| v = torch.randn(2, 5, 32) | |
| mask = _pad_mask(2, mha.num_heads, 5, q.device) | |
| with pytest.raises(ValueError): | |
| bad_q = q.unsqueeze(0) | |
| bad_q_mask = _pad_mask(bad_q.shape[0], mha.num_heads, bad_q.shape[1], bad_q.device) | |
| mha(bad_q, k, v, bad_q_mask, mask) | |
| with pytest.raises(ValueError): | |
| mha(q, k.view(10, 32), v, mask, mask) | |
| with pytest.raises(ValueError): | |
| mha(q[..., :16], k, v, mask, mask) | |
| with pytest.raises(ValueError): | |
| mha(q, k[..., :16], v, mask, mask) | |
| with pytest.raises(ValueError): | |
| mha(q, k, v[..., :16], mask, mask) | |
| def test_forward_batch_and_seq_mismatch_checks(): | |
| mha = MultiHeadAttention(24, 3, 0.1) | |
| q = torch.randn(2, 5, 24) | |
| k = torch.randn(3, 5, 24) # batch mismatch | |
| v = torch.randn(2, 5, 24) # seq mismatch with k | |
| q_mask = _pad_mask(q.shape[0], mha.num_heads, q.shape[1], q.device) | |
| k_mask = _pad_mask(k.shape[0], mha.num_heads, k.shape[1], k.device) | |
| with pytest.raises(ValueError): | |
| mha(q, k, torch.randn(3, 5, 24), q_mask, k_mask) | |
| with pytest.raises(ValueError): | |
| bad_k = torch.randn(2, 6, 24) | |
| bad_k_mask = _pad_mask(bad_k.shape[0], mha.num_heads, bad_k.shape[1], bad_k.device) | |
| mha(q, bad_k, v, q_mask, bad_k_mask) | |
| # ======================= | |
| # Forward: happy path + masks + zero-length | |
| # ======================= | |
| def test_forward_shapes_and_device_dtype(B, S, D, H): | |
| device = ( | |
| torch.device(f"cuda:{torch.cuda.current_device()}") | |
| if torch.cuda.is_available() | |
| else torch.device("cpu") | |
| ) | |
| mha = MultiHeadAttention(D, H, 0.1).to(device) | |
| q = _rand(B, S, D, device=device) | |
| k = _rand(B, S, D, device=device) | |
| v = _rand(B, S, D, device=device) | |
| mask = _pad_mask(B, H, S, device) | |
| out = mha(q, k, v, mask, mask) | |
| assert out.shape == (B, S, D) | |
| assert out.device == device | |
| assert out.dtype == q.dtype | |
| def test_boolean_mask_blocks_positions(): | |
| B, S, D, H = 2, 6, 24, 3 | |
| mha = MultiHeadAttention(D, H, 0.0) # no dropout for determinism | |
| q = _rand(B, S, D) | |
| k = _rand(B, S, D) | |
| v = _rand(B, S, D) | |
| q_mask = _pad_mask(B, H, S, q.device) | |
| k_mask = _pad_mask(B, H, S, q.device) | |
| k_mask[..., -2:] = True | |
| out1 = mha(q, k, v, q_mask, q_mask) | |
| out2 = mha(q, k, v, q_mask, k_mask) | |
| assert not torch.allclose(out1, out2) | |
| def test_causal_mask_blocks_future_positions(): | |
| B, S, D, H = 2, 5, 32, 4 | |
| mha = MultiHeadAttention(D, H, 0.0) | |
| q, k, v = _rand(B, S, D), _rand(B, S, D), _rand(B, S, D) | |
| base_mask = _pad_mask(B, H, S, q.device) | |
| causal = torch.ones(B, H, S, S, dtype=torch.bool, device=q.device).triu(1) | |
| out_free = mha(q, k, v, base_mask, base_mask, None) | |
| out_causal = mha(q, k, v, base_mask, base_mask, causal) | |
| assert not torch.allclose(out_free, out_causal) | |
| # ======================= | |
| # Gradients smoke | |
| # ======================= | |
| def test_gradients_flow(): | |
| mha = MultiHeadAttention(32, 4, 0.1) | |
| q = _rand( | |
| 2, | |
| 5, | |
| 32, | |
| dtype=torch.float32, | |
| device="cpu", | |
| ) | |
| k = _rand( | |
| 2, | |
| 5, | |
| 32, | |
| dtype=torch.float32, | |
| device="cpu", | |
| ) | |
| v = _rand( | |
| 2, | |
| 5, | |
| 32, | |
| dtype=torch.float32, | |
| device="cpu", | |
| ) | |
| q.requires_grad_(True) | |
| k.requires_grad_(True) | |
| v.requires_grad_(True) | |
| mask = _pad_mask(2, 4, 5, q.device) | |
| out = mha(q, k, v, mask, mask) | |
| loss = out.pow(2).mean() | |
| loss.backward() | |
| for t in (q, k, v): | |
| assert t.grad is not None | |
| assert torch.isfinite(t.grad).all() | |