# test_model.py: Token expansion, embedding resize and the forward pass. import pytest import torch import torch.nn as nn from unittest.mock import MagicMock, patch, PropertyMock from config import get_all_special_tokens, CODEBOOK_SIZE, NUM_SPEECH_TOKENS class MockEmbedding(nn.Embedding): def __init__(self, vocab_size=50280, embed_dim=2048): super().__init__(vocab_size, embed_dim) class MockLinear(nn.Linear): def __init__(self, in_features=2048, out_features=50280): super().__init__(in_features, out_features, bias=False) class TestTokenExpansion: def test_special_token_count(self): import config from config import NUM_SPEECH_TOKENS tokens = get_all_special_tokens() n_plan = len(config.get_plan_tokens()) if config.ENABLE_PLAN_TOKENS else 0 assert len(tokens) == 22 + n_plan + NUM_SPEECH_TOKENS if not config.ENABLE_PLAN_TOKENS: assert len(tokens) == 22 + NUM_SPEECH_TOKENS def test_speech_tokens_in_list(self): tokens = get_all_special_tokens() assert "[SPEECH_0]" in tokens assert f"[SPEECH_{NUM_SPEECH_TOKENS - 1}]" in tokens assert "[SPEECH_512]" in tokens def test_control_tokens_in_list(self): tokens = get_all_special_tokens() assert "" in tokens assert "" in tokens assert "" in tokens assert "[USER_PROMPT]" in tokens def test_emotion_tokens_in_list(self): tokens = get_all_special_tokens() assert "[EMO:sarcastic]" in tokens assert "[EMO:neutral]" in tokens def test_prosody_tokens_in_list(self): tokens = get_all_special_tokens() assert "[PACE:slow]" in tokens assert "[PITCH:high]" in tokens assert "[BREAK:long]" in tokens class TestEmbeddingResize: def test_resize_expands_correctly(self): old_size = 50280 new_size = old_size + 1045 embed_dim = 128 old_embedding = nn.Embedding(old_size, embed_dim) new_embedding = nn.Embedding(new_size, embed_dim) with torch.no_grad(): new_embedding.weight[:old_size] = old_embedding.weight[:old_size] assert new_embedding.weight.shape[0] == new_size assert new_embedding.weight.shape[1] == embed_dim assert torch.allclose( new_embedding.weight[:old_size], old_embedding.weight[:old_size], ) def test_new_rows_initialized(self): old_size = 100 new_size = 150 embed_dim = 64 old_embedding = nn.Embedding(old_size, embed_dim) new_embedding = nn.Embedding(new_size, embed_dim) with torch.no_grad(): new_embedding.weight[:old_size] = old_embedding.weight[:old_size] mean_embed = old_embedding.weight.mean(dim=0) noise = torch.randn(new_size - old_size, embed_dim) * 0.02 new_embedding.weight[old_size:] = mean_embed + noise new_rows = new_embedding.weight[old_size:] assert not torch.all(new_rows == 0) def test_lm_head_resize(self): old_size = 50280 new_size = old_size + 1045 hidden_dim = 128 old_head = nn.Linear(hidden_dim, old_size, bias=False) new_head = nn.Linear(hidden_dim, new_size, bias=False) with torch.no_grad(): new_head.weight[:old_size] = old_head.weight[:old_size] assert new_head.weight.shape[0] == new_size assert new_head.weight.shape[1] == hidden_dim class TestForwardPassLogic: def test_causal_lm_loss_shape(self): batch_size, seq_len, vocab_size = 2, 10, 100 logits = torch.randn(batch_size, seq_len, vocab_size) labels = torch.randint(0, vocab_size, (batch_size, seq_len)) shift_logits = logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() loss_fct = nn.CrossEntropyLoss(ignore_index=-100) loss = loss_fct( shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1), ) assert loss.dim() == 0 assert not torch.isnan(loss) def test_ignore_index_masks_correctly(self): vocab_size = 100 logits = torch.randn(1, 5, vocab_size) labels = torch.tensor([[1, -100, -100, 42, 7]]) shift_logits = logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() loss_fct = nn.CrossEntropyLoss(ignore_index=-100, reduction="none") per_token = loss_fct( shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1), ) assert per_token[0] == 0.0 assert per_token[1] == 0.0 assert per_token[2] > 0.0 assert per_token[3] > 0.0 def test_weighted_loss(self): vocab_size = 100 logits = torch.randn(1, 5, vocab_size) labels = torch.randint(0, vocab_size, (1, 5)) weights = torch.tensor([[1.0, 0.3, 0.3, 1.0, 1.0]]) shift_logits = logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() shift_weights = weights[..., 1:].contiguous() loss_fct = nn.CrossEntropyLoss(ignore_index=-100, reduction="none") per_token = loss_fct( shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1), ) weighted = per_token * shift_weights.view(-1) loss = weighted.sum() / (shift_labels.view(-1) != -100).float().sum().clamp(min=1) assert loss.dim() == 0 assert not torch.isnan(loss) class TestFreezeUnfreeze: def test_freeze_sets_requires_grad_false(self): model = nn.Sequential( nn.Linear(10, 20), nn.Linear(20, 10), ) for param in model.parameters(): param.requires_grad = False trainable = sum(1 for p in model.parameters() if p.requires_grad) assert trainable == 0 def test_unfreeze_sets_requires_grad_true(self): model = nn.Sequential( nn.Linear(10, 20), nn.Linear(20, 10), ) for param in model.parameters(): param.requires_grad = False for param in model.parameters(): param.requires_grad = True trainable = sum(1 for p in model.parameters() if p.requires_grad) total = sum(1 for _ in model.parameters()) assert trainable == total