""" Unit tests for the OpenMind Transformer model. """ import os import sys import tempfile import pytest import torch sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) from models.config_openmind import OpenMindConfig from models.modeling_openmind import ( RMSNorm, RotaryEmbedding, GroupedQueryAttention, SwiGLU, TransformerBlock, OpenMindModel, ) # Use a small config for fast testing TEST_CONFIG = OpenMindConfig( vocab_size=1000, max_seq_len=128, dim=64, n_layers=2, n_heads=4, n_kv_heads=4, intermediate_dim=128, dropout=0.0, tie_embeddings=True, ) class TestRMSNorm: """Test RMSNorm layer.""" def test_output_shape(self): norm = RMSNorm(64) x = torch.randn(2, 10, 64) out = norm(x) assert out.shape == x.shape def test_normalization(self): norm = RMSNorm(64) x = torch.randn(2, 10, 64) out = norm(x) # RMS should be approximately 1 after normalization rms = out.pow(2).mean(-1).sqrt() assert rms.mean().item() < 5.0 # Reasonable range class TestRotaryEmbedding: """Test Rotary Positional Embeddings.""" def test_output_shapes(self): rope = RotaryEmbedding(64, max_seq_len=128) x = torch.randn(2, 4, 10, 64) cos, sin = rope(x, seq_len=10) assert cos.shape == (10, 64) assert sin.shape == (10, 64) def test_dynamic_extension(self): rope = RotaryEmbedding(64, max_seq_len=32) x = torch.randn(2, 4, 64, 64) # Should auto-extend cache cos, sin = rope(x, seq_len=64) assert cos.shape == (64, 64) class TestGroupedQueryAttention: """Test GQA module.""" def test_mha_output_shape(self): config = OpenMindConfig(dim=64, n_heads=4, n_kv_heads=4, max_seq_len=128, vocab_size=100, n_layers=1, intermediate_dim=128) attn = GroupedQueryAttention(config) x = torch.randn(2, 10, 64) out, cache = attn(x) assert out.shape == (2, 10, 64) assert cache is None def test_gqa_output_shape(self): config = OpenMindConfig(dim=64, n_heads=4, n_kv_heads=2, max_seq_len=128, vocab_size=100, n_layers=1, intermediate_dim=128) attn = GroupedQueryAttention(config) x = torch.randn(2, 10, 64) out, cache = attn(x) assert out.shape == (2, 10, 64) def test_kv_cache(self): config = OpenMindConfig(dim=64, n_heads=4, n_kv_heads=4, max_seq_len=128, vocab_size=100, n_layers=1, intermediate_dim=128) attn = GroupedQueryAttention(config) # First pass x = torch.randn(1, 5, 64) out1, cache = attn(x, use_cache=True) assert cache is not None assert cache[0].shape[2] == 5 # K cache seq_len # Second pass with cache x2 = torch.randn(1, 1, 64) out2, cache2 = attn(x2, past_key_value=cache, use_cache=True) assert out2.shape == (1, 1, 64) assert cache2[0].shape[2] == 6 # Extended cache class TestSwiGLU: """Test SwiGLU feed-forward network.""" def test_output_shape(self): config = OpenMindConfig(dim=64, intermediate_dim=128, vocab_size=100, n_heads=4, n_kv_heads=4, n_layers=1, max_seq_len=128) ffn = SwiGLU(config) x = torch.randn(2, 10, 64) out = ffn(x) assert out.shape == (2, 10, 64) class TestTransformerBlock: """Test full transformer block.""" def test_output_shape(self): block = TransformerBlock(TEST_CONFIG) x = torch.randn(2, 10, 64) out, cache = block(x) assert out.shape == (2, 10, 64) def test_residual_connection(self): block = TransformerBlock(TEST_CONFIG) x = torch.randn(2, 10, 64) out, _ = block(x) # Output should be different from input (not zero residual) assert not torch.allclose(out, x, atol=1e-6) class TestOpenMindModel: """Test the full OpenMind model.""" def test_forward_pass(self): model = OpenMindModel(TEST_CONFIG) input_ids = torch.randint(0, 1000, (2, 10)) outputs = model(input_ids) assert outputs["logits"].shape == (2, 10, 1000) assert outputs["loss"] is None def test_forward_with_labels(self): model = OpenMindModel(TEST_CONFIG) input_ids = torch.randint(0, 1000, (2, 10)) labels = torch.randint(0, 1000, (2, 10)) outputs = model(input_ids, labels=labels) assert outputs["loss"] is not None assert outputs["loss"].dim() == 0 # Scalar def test_gradient_flow(self): model = OpenMindModel(TEST_CONFIG) input_ids = torch.randint(0, 1000, (2, 10)) labels = torch.randint(0, 1000, (2, 10)) outputs = model(input_ids, labels=labels) outputs["loss"].backward() # Check gradients exist for name, param in model.named_parameters(): if param.requires_grad: assert param.grad is not None, f"No gradient for {name}" def test_generate(self): model = OpenMindModel(TEST_CONFIG) input_ids = torch.randint(0, 1000, (1, 5)) generated = model.generate(input_ids, max_new_tokens=10) assert generated.shape[1] > 5 assert generated.shape[1] <= 15 def test_kv_cache_generation(self): model = OpenMindModel(TEST_CONFIG) input_ids = torch.randint(0, 1000, (1, 3)) outputs = model(input_ids, use_cache=True) assert outputs["past_key_values"] is not None assert len(outputs["past_key_values"]) == TEST_CONFIG.n_layers def test_count_parameters(self): model = OpenMindModel(TEST_CONFIG) counts = model.count_parameters() assert "total" in counts assert counts["total"] > 0 assert "embedding" in counts assert "attention" in counts def test_save_and_load(self): model = OpenMindModel(TEST_CONFIG) input_ids = torch.randint(0, 1000, (1, 5)) with torch.no_grad(): original_output = model(input_ids)["logits"] with tempfile.TemporaryDirectory() as tmpdir: model.save_pretrained(tmpdir) loaded = OpenMindModel.from_pretrained(tmpdir) with torch.no_grad(): loaded_output = loaded(input_ids)["logits"] assert torch.allclose(original_output, loaded_output, atol=1e-5) class TestOpenMindConfig: """Test model configuration.""" def test_default_config(self): config = OpenMindConfig() assert config.dim == 768 assert config.n_heads == 12 assert config.head_dim == 64 def test_head_dim_computation(self): config = OpenMindConfig(dim=128, n_heads=8, n_kv_heads=8, n_layers=1, vocab_size=100, intermediate_dim=256, max_seq_len=128) assert config.head_dim == 16 def test_invalid_config(self): with pytest.raises(AssertionError): OpenMindConfig(dim=100, n_heads=3, n_kv_heads=3, n_layers=1, vocab_size=100, intermediate_dim=200, max_seq_len=128) def test_save_and_load_config(self): config = OpenMindConfig(dim=256, n_heads=8, n_kv_heads=4, n_layers=6, vocab_size=5000, intermediate_dim=512, max_seq_len=512) with tempfile.TemporaryDirectory() as tmpdir: config.save_pretrained(tmpdir) loaded = OpenMindConfig.from_pretrained(tmpdir) assert loaded.dim == 256 assert loaded.n_heads == 8 assert loaded.n_kv_heads == 4 if __name__ == "__main__": pytest.main([__file__, "-v"])