Mini-Transformer / tests /units /test_utils.py
AlaBoussoffara's picture
added infer docker & /config option & various bug fixes & new tests
b22e8ba
Raw
History Blame Contribute Delete
23.1 kB
import math
import pytest
import torch
import torch.nn.functional as F
from mini_transformer import BasicEncoderDecoderTransformer
from mini_transformer.configs import ModelCfg
from mini_transformer.utils import (
calculate_attention,
combine_masks,
create_causal_mask,
extract_all_attention_maps,
join_heads,
sample_from_logits,
sinusoidal_positional_encoding,
split_heads,
)
###___split_heads___###
def test_happy_path_shape_dtype_device_cpu():
x = torch.randn(2, 10, 16) # B=2, S=10, D=16
out = split_heads(x, num_heads=4)
assert out.shape == (2, 4, 10, 4)
assert out.dtype == x.dtype
assert out.device == x.device
def test_smallest_valid():
x = torch.randn(1, 1, 4)
out = split_heads(x, num_heads=2)
assert out.shape == (1, 2, 1, 2)
def test_zero_length_seq_allowed():
x = torch.randn(2, 0, 8)
out = split_heads(x, num_heads=2)
assert out.shape == (2, 2, 0, 4)
def test_non_3d_input_raises_value_error():
x2 = torch.randn(10, 16)
with pytest.raises(ValueError) as ei2:
split_heads(x2, 4)
assert "(B, S, D)" in str(ei2.value)
x4 = torch.randn(2, 3, 4, 5)
with pytest.raises(ValueError) as ei4:
split_heads(x4, 4)
assert "(B, S, D)" in str(ei4.value)
def test_invalid_num_heads_type_raises_type_error():
x = torch.randn(2, 10, 16)
with pytest.raises(TypeError):
split_heads(x, 4.0) # float
with pytest.raises(TypeError):
split_heads(x, torch.tensor(4)) # Tensor
def test_num_heads_leq_zero_raises_value_error():
x = torch.randn(2, 10, 16)
with pytest.raises(ValueError):
split_heads(x, 0)
with pytest.raises(ValueError):
split_heads(x, -2)
def test_non_divisible_d_model_raises_value_error_message_has_values():
x = torch.randn(2, 5, 10)
with pytest.raises(ValueError) as e:
split_heads(x, 3)
msg = str(e.value)
assert "d_model (10)" in msg and "num_heads (3)" in msg
@pytest.mark.parametrize(
"B,S,D,H",
[
(1, 1, 8, 1),
(2, 3, 12, 3),
(4, 7, 32, 8),
(3, 0, 24, 6),
],
)
def test_invariants_random_valid(B, S, D, H):
x = torch.randn(B, S, D)
out = split_heads(x, H)
assert out.shape[0] == B
assert out.shape[2] == S
assert (H * (D // H)) == D
assert x.numel() == out.numel()
def test_grad_propagates():
x = torch.randn(2, 10, 16, requires_grad=True)
out = split_heads(x, 4)
# Do a simple scalar reduction to make grad non-trivial
loss = out.square().mean()
loss.backward()
assert x.grad is not None
assert x.grad.shape == x.shape
def test_non_contiguous_input():
# Create a non-contiguous view by transposing and slicing
base = torch.randn(10, 2, 16)
x = base.transpose(0, 1)[:, :8, :] # shape (2, 8, 16), likely non-contiguous
assert not x.is_contiguous()
out = split_heads(x, 4)
assert out.shape == (2, 4, 8, 4)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
def test_device_preserved_cuda():
x = torch.randn(2, 10, 16, device="cuda")
out = split_heads(x, 4)
assert out.device.type == "cuda"
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
def test_dtype_half_bfloat_on_gpu():
x_half = torch.randn(2, 10, 16, device="cuda", dtype=torch.float16)
out_half = split_heads(x_half, 4)
assert out_half.dtype == torch.float16
# bfloat16 may not be available on all GPUs; guard with try
try:
x_bf16 = torch.randn(2, 10, 16, device="cuda", dtype=torch.bfloat16)
out_bf16 = split_heads(x_bf16, 4)
assert out_bf16.dtype == torch.bfloat16
except RuntimeError:
pytest.skip("bfloat16 not supported on this device")
###___calculate_attention___###
# ----------------------------
# Helpers
# ----------------------------
def _rand(B=2, H=3, Sq=4, Sk=5, D=8, device="cpu", dtype=torch.float32):
q = torch.randn(B, H, Sq, D, device=device, dtype=dtype)
k = torch.randn(B, H, Sk, D, device=device, dtype=dtype)
v = torch.randn(B, H, Sk, D, device=device, dtype=dtype)
return q, k, v
# ----------------------------
# A. Basic correctness
# ----------------------------
def test_basic_shapes_and_row_sums():
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
q, k, v = _rand(B, H, Sq, Sk, D)
attn, probs = calculate_attention(q, k, v, mask=None, return_probs=True)
assert attn.shape == (B, H, Sq, D)
assert probs.shape == (B, H, Sq, Sk)
# softmax rows sum to 1
assert torch.allclose(probs.sum(-1), torch.ones(B, H, Sq), atol=1e-6)
def test_identity_prefers_diagonal():
# q == k == v as orthogonal-ish basis to encourage diagonal peak
Sq = Sk = D = 4
base = torch.eye(D).view(1, 1, Sk, D).expand(1, 1, Sk, D).contiguous()
q = base.clone()
k = base.clone()
v = base.clone()
_, probs = calculate_attention(q, k, v, mask=None, return_probs=True)
assert torch.equal(probs[0, 0].argmax(-1), torch.arange(Sq))
# ----------------------------
# B. Mask behavior (boolean & causal)
# ----------------------------
def test_padding_mask_boolean_last_two_keys():
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
q, k, v = _rand(B, H, Sq, Sk, D)
mask = torch.zeros(B, H, Sq, Sk, dtype=torch.bool)
mask[:, :, :, -2:] = True # mask last two keys
_, probs = calculate_attention(q, k, v, mask=mask, return_probs=True)
assert (probs[:, :, :, -2:] < 1e-6).all()
# After zeroing masked entries, rows renormalize to ~1
assert torch.allclose(probs.masked_fill(mask, 0).sum(-1), torch.ones(B, H, Sq), atol=1e-6)
def test_causal_mask_upper_triangle_zero():
B, H, S, D = 2, 3, 6, 8
q, k, v = _rand(B, H, S, S, D)
causal = torch.ones(B, H, S, S, dtype=torch.bool).triu(1) # True above diagonal
_, probs = calculate_attention(q, k, v, mask=causal, return_probs=True)
assert (probs.triu(1) < 1e-6).all()
def test_fully_masked_row_zero_probs_and_output():
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
q, k, v = _rand(B, H, Sq, Sk, D)
mask = torch.zeros(B, H, Sq, Sk, dtype=torch.bool)
mask[:, :, 1, :] = True # fully mask a query row
attn, probs = calculate_attention(q, k, v, mask=mask, return_probs=True)
assert torch.allclose(probs[:, :, 1, :], torch.zeros(B, H, Sk))
assert torch.allclose(attn[:, :, 1, :], torch.zeros(B, H, D))
###___extract_all_attention_maps___###
def _make_model_cfg(num_layers: int, layer_norm_style: str | None = None) -> ModelCfg:
return ModelCfg(
name="test",
best_checkpoint_path="/tmp/best.ckpt",
latest_checkpoint_path="/tmp/latest.ckpt",
tokenizer="demo",
d_model=16,
num_layers=num_layers,
num_heads=4,
d_ff=32,
dropout_rate=0.0,
max_seq_len=32,
vocab_size=64,
pad_id=0,
bos_id=1,
eos_id=2,
layer_norm_style=layer_norm_style,
)
def _run_attention_maps(
*,
num_layers: int,
layer_norm_style: str | None,
) -> tuple[ModelCfg, BasicEncoderDecoderTransformer, dict, torch.Tensor, torch.Tensor]:
cfg = _make_model_cfg(num_layers=num_layers, layer_norm_style=layer_norm_style)
model = BasicEncoderDecoderTransformer(cfg)
batch, src_len, tgt_len = 2, 5, 4
src_ids = torch.randint(0, cfg.vocab_size, (batch, src_len), dtype=torch.long)
tgt_ids = torch.randint(0, cfg.vocab_size, (batch, tgt_len), dtype=torch.long)
src_pad = torch.zeros(batch, src_len, dtype=torch.bool)
tgt_pad = torch.zeros(batch, tgt_len, dtype=torch.bool)
maps = extract_all_attention_maps(model, src_ids, tgt_ids, src_pad, tgt_pad)
return cfg, model, maps, src_ids, tgt_ids
def _assert_attention_shapes(maps, *, batch: int, heads: int, src_len: int, tgt_len: int) -> None:
for layer_map in maps["enc_self"]:
assert layer_map.shape == (batch, heads, src_len, src_len)
assert torch.isfinite(layer_map).all()
assert torch.allclose(layer_map.sum(-1), torch.ones(batch, heads, src_len), atol=1e-5)
for layer_map in maps["dec_self"]:
assert layer_map.shape == (batch, heads, tgt_len, tgt_len)
assert torch.isfinite(layer_map).all()
assert torch.allclose(layer_map.sum(-1), torch.ones(batch, heads, tgt_len), atol=1e-5)
for layer_map in maps["dec_cross"]:
assert layer_map.shape == (batch, heads, tgt_len, src_len)
assert torch.isfinite(layer_map).all()
assert torch.allclose(layer_map.sum(-1), torch.ones(batch, heads, tgt_len), atol=1e-5)
@pytest.mark.parametrize(
"layer_norm_style, expected_pre_norm",
[
(None, True), # auto-selects pre-LN when num_layers > 4
("post", False),
("pre", True),
],
)
def test_extract_attention_maps_handles_layer_norm_styles(layer_norm_style, expected_pre_norm):
cfg, model, maps, src_ids, tgt_ids = _run_attention_maps(
num_layers=6, layer_norm_style=layer_norm_style
)
batch, src_len = src_ids.shape
tgt_len = tgt_ids.shape[1]
assert len(maps["enc_self"]) == cfg.num_layers
assert len(maps["dec_self"]) == cfg.num_layers
assert len(maps["dec_cross"]) == cfg.num_layers
_assert_attention_shapes(
maps,
batch=batch,
heads=cfg.num_heads,
src_len=src_len,
tgt_len=tgt_len,
)
assert all(layer.pre_norm is expected_pre_norm for layer in model.encoder.layers)
assert all(layer.pre_norm is expected_pre_norm for layer in model.decoder.layers)
# ----------------------------
# C. Numerical stability
# ----------------------------
def test_extreme_logits_no_nans_or_infs():
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
q, k, v = _rand(B, H, Sq, Sk, D)
q = q * 1000
k = k * 1000
attn, probs = calculate_attention(q, k, v, mask=None, return_probs=True)
assert not (torch.isnan(probs).any() or torch.isinf(probs).any())
assert attn.shape == (B, H, Sq, D)
def test_half_precision_close_to_fp32_or_skip():
if not torch.cuda.is_available():
pytest.skip("CUDA not available")
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
q32, k32, v32 = _rand(B, H, Sq, Sk, D, device="cuda", dtype=torch.float32)
q16, k16, v16 = q32.half(), k32.half(), v32.half()
a16 = calculate_attention(q16, k16, v16, mask=None)
a32 = calculate_attention(q32, k32, v32, mask=None)
# looser tolerance for fp16
assert torch.allclose(a16.float(), a32.float(), atol=5e-2, rtol=5e-2)
# ----------------------------
# D. Edge cases
# ----------------------------
def test_singleton_softmax_is_one():
q = torch.randn(1, 1, 1, 1)
k = torch.randn(1, 1, 1, 1)
v = torch.randn(1, 1, 1, 1)
_, p = calculate_attention(q, k, v, mask=None, return_probs=True)
assert torch.allclose(p, torch.ones_like(p), atol=1e-6)
# ----------------------------
# E. Autograd & determinism
# ----------------------------
def test_gradients_flow_no_inplace_breakage():
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
q = torch.randn(B, H, Sq, D, requires_grad=True)
k = torch.randn(B, H, Sk, D, requires_grad=True)
v = torch.randn(B, H, Sk, D, requires_grad=True)
out = calculate_attention(q, k, v, mask=None)
loss = out.pow(2).sum()
loss.backward()
for t in (q, k, v):
assert t.grad is not None and torch.isfinite(t.grad).all()
def test_repeated_calls_consistent_without_dropout():
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
q, k, v = _rand(B, H, Sq, Sk, D)
a1 = calculate_attention(q, k, v, mask=None)
a2 = calculate_attention(q, k, v, mask=None)
assert torch.allclose(a1, a2)
# ----------------------------
# F. Parity with PyTorch SDPA (when available)
# ----------------------------
def test_sdpa_parity_if_available():
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
q, k, v = _rand(B, H, Sq, Sk, D)
try:
sdpa = F.scaled_dot_product_attention(
q, k, v, dropout_p=0.0, attn_mask=None, is_causal=False
)
manual = calculate_attention(q, k, v, mask=None)
assert torch.allclose(sdpa, manual, atol=1e-5, rtol=1e-4)
except Exception:
# Older PyTorch: skip
pass
# ----------------------------
# G. Performance smoke (sanity only)
# ----------------------------
def test_perf_smoke_runs_reasonably():
# Not asserting timing; just ensure no OOM or pathological slowdowns
q, k, v = _rand(B=1, H=8, Sq=1024, Sk=1024, D=64)
_ = calculate_attention(q, k, v, mask=None, return_probs=False)
q, k, v = _rand(B=2, H=8, Sq=1536, Sk=1536, D=64)
_ = calculate_attention(q, k, v, mask=None, return_probs=False)
# ----------------------------
# H. Device behavior
# ----------------------------
def test_mask_on_cpu_qkv_on_gpu_autofix_or_skip():
if not torch.cuda.is_available():
pytest.skip("CUDA not available")
prev = torch.are_deterministic_algorithms_enabled()
try:
# Disable determinism *only for this test*
if prev:
torch.use_deterministic_algorithms(False)
q, k, v = _rand(device="cuda")
mask = torch.zeros(q.shape[0], 1, 1, k.shape[2], dtype=torch.bool) # CPU
attn, probs = calculate_attention(q, k, v, mask, return_probs=True)
assert attn.device.type == "cuda"
assert probs.device.type == "cuda"
finally:
# restore whatever the global setting was
torch.use_deterministic_algorithms(prev)
def test_qkv_device_mismatch_raises_clear_error():
if not torch.cuda.is_available():
pytest.skip("CUDA not available")
prev = torch.are_deterministic_algorithms_enabled()
try:
if prev:
torch.use_deterministic_algorithms(False)
q, k, v = _rand(device="cuda")
k = k.cpu() # force mismatch
with pytest.raises(RuntimeError) as ei:
_ = calculate_attention(q, k, v, mask=None)
msg = str(ei.value)
assert (
"q/k/v must be on the same device" in msg # our explicit check
or "Expected all tensors to be on the same device" in msg # PyTorch matmul
or "query, key, value must be on the same device" in msg # updated error wording
)
finally:
torch.use_deterministic_algorithms(prev)
# ----------------------------
# I. Mask broadcastability
# ----------------------------
def test_boolean_mask_broadcast_variants_equivalent():
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
q, k, v = _rand(B, H, Sq, Sk, D)
m_exact = torch.zeros(B, H, Sq, Sk, dtype=torch.bool)
m_B1_1Sk = torch.zeros(B, 1, 1, Sk, dtype=torch.bool)
m_11SqSk = torch.zeros(1, 1, Sq, Sk, dtype=torch.bool)
a_exact, p_exact = calculate_attention(q, k, v, m_exact, return_probs=True)
a1, p1 = calculate_attention(q, k, v, m_B1_1Sk, return_probs=True)
a2, p2 = calculate_attention(q, k, v, m_11SqSk, return_probs=True)
assert torch.allclose(a1, a_exact, atol=1e-6, rtol=1e-5)
assert torch.allclose(p1, p_exact, atol=1e-6, rtol=1e-5)
assert torch.allclose(a2, a_exact, atol=1e-6, rtol=1e-5)
assert torch.allclose(p2, p_exact, atol=1e-6, rtol=1e-5)
def test_non_broadcastable_mask_raises_value_error():
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
q, k, v = _rand(B, H, Sq, Sk, D)
bad = torch.zeros(
B, H, Sq, dtype=torch.bool
) # missing last dim => not broadcastable to (B,H,Sq,Sk)
with pytest.raises(ValueError) as ei:
_ = calculate_attention(q, k, v, bad)
assert "not broadcastable" in str(ei.value)
###___join_heads___###
def test_join_heads_type_error():
with pytest.raises(TypeError):
join_heads([1, 2, 3]) # not a tensor
def test_join_heads_dim_error():
with pytest.raises(ValueError):
join_heads(torch.randn(2, 3, 4)) # only 3D
def test_join_heads_shape_values():
x = torch.randn(2, 0, 4, 5) # zero heads
y = join_heads(x)
assert y.shape == (2, 4, 0)
def test_join_heads_roundtrip():
B, H, T, Dh = 2, 3, 5, 7
x = torch.randn(B, H, T, Dh)
y = join_heads(x)
assert y.shape == (B, T, H * Dh)
# Check element mapping correctness
for b in range(B):
for h in range(H):
for t in range(T):
for d in range(Dh):
assert torch.allclose(y[b, t, h * Dh + d], x[b, h, t, d])
###___sinusoidal_positional_encoding___###
@pytest.mark.parametrize("seq_len,dim", [(1, 1), (1, 2), (7, 4), (17, 33)])
def test_sinusoidal_shapes_and_dtype(seq_len, dim):
pe = sinusoidal_positional_encoding(seq_len, dim)
assert pe.shape == (seq_len, dim)
assert pe.dtype == torch.float32
assert pe.requires_grad is False
def test_sinusoidal_matches_reference_small_case():
seq_len, dim = 4, 6
pe = sinusoidal_positional_encoding(seq_len, dim)
ref = torch.zeros_like(pe)
base = 10_000.0
for pos in range(seq_len):
for i in range(0, dim // 2):
denom = base ** (2 * i / dim)
ref[pos, 2 * i] = math.sin(pos / denom)
ref[pos, 2 * i + 1] = math.cos(pos / denom)
paired = (dim // 2) * 2
assert torch.allclose(pe[:, :paired], ref[:, :paired], atol=1e-6, rtol=1e-6)
def test_sinusoidal_deterministic():
a = sinusoidal_positional_encoding(8, 16)
b = sinusoidal_positional_encoding(8, 16)
assert torch.allclose(a, b)
def test_sinusoidal_invalid_inputs():
with pytest.raises(ValueError):
sinusoidal_positional_encoding(0, 8)
with pytest.raises(ValueError):
sinusoidal_positional_encoding(-1, 8)
with pytest.raises(ValueError):
sinusoidal_positional_encoding(1, 0)
###___combine_masks___###
def test_both_none_returns_none():
assert combine_masks(None, None) is None
def test_one_none_returns_other():
m = torch.tensor([[True, False]])
assert torch.equal(combine_masks(m, None), m)
assert torch.equal(combine_masks(None, m), m)
def test_or_combination():
m1 = torch.tensor([[True, False]])
m2 = torch.tensor([[False, True]])
expected = torch.tensor([[True, True]])
out = combine_masks(m1, m2)
assert torch.equal(out, expected)
def test_shape_mismatch_error():
m1 = torch.ones(2, 2, dtype=torch.bool)
m2 = torch.ones(3, 3, dtype=torch.bool)
with pytest.raises(ValueError):
combine_masks(m1, m2)
def test_broadcastable_masks():
m1 = torch.tensor([[True, False, True]])
m2 = torch.tensor([[False]]) # broadcast across
out = combine_masks(m1, m2)
assert torch.equal(out, m1 | m2)
def test_dtype_conversion():
# Non-bool masks still should work if they are 0/1 ints
m1 = torch.tensor([[1, 0]], dtype=torch.int32)
m2 = torch.tensor([[0, 1]], dtype=torch.int32)
out = combine_masks(m1.bool(), m2.bool())
expected = torch.tensor([[True, True]])
assert torch.equal(out, expected)
###___sample_from_logits___###
def test_greedy_equals_argmax():
torch.manual_seed(0)
logits = torch.randn(4, 7)
ids = sample_from_logits(logits) # greedy
ref = torch.argmax(F.softmax(logits, dim=-1), dim=-1)
assert torch.equal(ids, ref)
def test_temperature_effect_no_sampling():
# Greedy with temperature should leave argmax unchanged (monotonic transform)
logits = torch.tensor([[0.1, 1.2, 0.3]], dtype=torch.float32)
ids_t1 = sample_from_logits(logits, do_sample=False, temperature=1.0)
ids_t05 = sample_from_logits(logits, do_sample=False, temperature=0.5)
assert torch.equal(ids_t1, ids_t05) # argmax unchanged
def test_top_k_limits_candidates():
logits = torch.tensor([[1.0, 0.9, 0.1, -1.0]], dtype=torch.float32)
# with top_k=1 greedy must select the max (index 0)
ids = sample_from_logits(logits, do_sample=False, top_k=1)
assert torch.equal(ids, torch.tensor([0]))
# with sampling, and top_k=1, the only candidate is index 0
gen = torch.Generator().manual_seed(123)
ids_s = sample_from_logits(logits, do_sample=True, top_k=1, rng=gen)
assert torch.equal(ids_s, torch.tensor([0]))
def test_top_p_nucleus_filters_tail():
logits = torch.tensor([[5.0, 4.0, 3.0, -5.0]], dtype=torch.float32) # probs heavily skewed
ids = sample_from_logits(logits, do_sample=False, top_p=0.6) # should keep a minimal head
assert ids.item() in (0, 1) # greediest is 0 anyway; check no crash
def test_min_tokens_to_keep_safety():
logits = torch.tensor([[0.0, 0.0, 0.0, 0.0]], dtype=torch.float32)
# Even with very small top_p, we keep at least 1 token
ids = sample_from_logits(logits, do_sample=False, top_p=0.01, min_tokens_to_keep=1)
assert ids.numel() == 1
def test_allow_and_deny_lists():
logits = torch.tensor([[0.1, 2.0, 0.3, 3.0]], dtype=torch.float32) # best is idx=3
# Allow only {0,2} → best among those is 2
ids = sample_from_logits(logits, do_sample=False, allowed_tokens=[0, 2])
assert torch.equal(ids, torch.tensor([2]))
# Deny {3} → next best is 1
ids = sample_from_logits(logits, do_sample=False, disallowed_tokens=[3])
assert torch.equal(ids, torch.tensor([1]))
def test_rng_reproducibility_sampling():
logits = torch.tensor([[1.0, 1.0, 1.0, 1.0]], dtype=torch.float32) # uniform
g1 = torch.Generator().manual_seed(42)
g2 = torch.Generator().manual_seed(42)
s1 = sample_from_logits(logits, do_sample=True, rng=g1)
s2 = sample_from_logits(logits, do_sample=True, rng=g2)
assert torch.equal(s1, s2)
def test_device_is_preserved_cpu_cuda():
target_device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu")
logits = torch.randn(3, 5, device=target_device)
out = sample_from_logits(logits) # greedy by default
assert out.device == target_device
assert out.dtype == torch.long
def test_handles_higher_rank_logits_flattening():
# [B, T, V] -> flattened internally; here we just ensure no crash and correct shape
logits = torch.randn(2, 3, 7)
out = sample_from_logits(logits) # returns [B*T]
assert out.shape == (2 * 3,)
###___create_causal_mask___###
def test_causal_mask_shape_and_dtype_cpu():
x = torch.zeros(2, 5, dtype=torch.long)
mask = create_causal_mask(x, num_heads=3)
assert mask.shape == (2, 3, 5, 5)
assert mask.dtype == torch.bool
assert mask.device == x.device
def test_causal_mask_is_upper_triangular():
x = torch.zeros(1, 4, dtype=torch.long)
mask = create_causal_mask(x, num_heads=2)
tri = mask[0, 0].to(torch.int)
expected = torch.tensor(
[
[0, 1, 1, 1],
[0, 0, 1, 1],
[0, 0, 0, 1],
[0, 0, 0, 0],
]
)
assert torch.equal(tri, expected)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
def test_causal_mask_tracks_device():
x = torch.zeros(1, 3, dtype=torch.long, device="cuda")
mask = create_causal_mask(x, num_heads=4)
assert mask.device.type == "cuda"
def test_causal_mask_rejects_invalid_inputs():
with pytest.raises(ValueError):
create_causal_mask(torch.zeros(2, 0, dtype=torch.long), num_heads=2)
with pytest.raises(ValueError):
create_causal_mask(torch.zeros(2, 3, dtype=torch.long), num_heads=0)