| import shutil |
| import tempfile |
| import unittest |
| from types import SimpleNamespace |
|
|
| import torch |
| import transformers |
| from transformers import LlamaConfig, LlamaForCausalLM |
|
|
| from models.decoder import Decoder |
|
|
|
|
| |
| |
| VOCAB = 100 |
| HIDDEN = 32 |
| LAYERS = 2 |
| HEADS = 4 |
| BATCH = 2 |
| SEQ = 5 |
|
|
| |
| MODEL_DIR = None |
| DECODER = None |
|
|
|
|
| def make_config(model_dir, lm_hidden_dim=HIDDEN, lm_use_tokens=False): |
| """ |
| Build a minimal config exposing exactly the three attributes Decoder reads. |
| |
| Parameters: |
| * model_dir (str) : filesystem path passed to AutoModelForCausalLM.from_pretrained; |
| requires it to contain a saved HF causal LM |
| |
| * lm_hidden_dim (int) : the hidden size Decoder asserts against the loaded model; |
| requires lm_hidden_dim >= 1. Defaults to the fixture's true hidden size so the |
| assertion passes; override it to provoke the mismatch guard. |
| |
| * lm_use_tokens (bool) : value stored on the Decoder; defaults to False (backbone mode) |
| |
| Returns: |
| A SimpleNamespace with lm_model_type, lm_hidden_dim, and lm_use_tokens. |
| """ |
| return SimpleNamespace( |
| lm_model_type=model_dir, |
| lm_hidden_dim=lm_hidden_dim, |
| lm_use_tokens=lm_use_tokens, |
| ) |
|
|
|
|
| def setUpModule(): |
| |
| |
| global MODEL_DIR, DECODER |
| transformers.logging.set_verbosity_error() |
| torch.manual_seed(0) |
| llama_cfg = LlamaConfig( |
| vocab_size=VOCAB, |
| hidden_size=HIDDEN, |
| intermediate_size=2 * HIDDEN, |
| num_hidden_layers=LAYERS, |
| num_attention_heads=HEADS, |
| num_key_value_heads=HEADS, |
| max_position_embeddings=128, |
| tie_word_embeddings=True, |
| ) |
| model = LlamaForCausalLM(llama_cfg).eval() |
| MODEL_DIR = tempfile.mkdtemp(prefix="decoder_test_") |
| model.save_pretrained(MODEL_DIR) |
| DECODER = Decoder(make_config(MODEL_DIR), load_backbone=True).eval() |
|
|
|
|
| def tearDownModule(): |
| if MODEL_DIR is not None: |
| shutil.rmtree(MODEL_DIR, ignore_errors=True) |
|
|
|
|
| def make_ids(batch=BATCH, seq=SEQ, seed=0): |
| """ |
| Draw a (batch, seq) tensor of valid token ids. |
| |
| Parameters: |
| * batch (int) : number of sequences; requires batch >= 1 |
| * seq (int) : sequence length; requires seq >= 1 |
| * seed (int) : seed for the local generator; requires seed >= 0 |
| |
| Returns: |
| A (batch, seq) int64 tensor with every entry in [0, VOCAB). |
| """ |
| generator = torch.Generator().manual_seed(seed) |
| return torch.randint(0, VOCAB, (batch, seq), generator=generator) |
|
|
|
|
| |
|
|
| class TestDecoderConstruction(unittest.TestCase): |
| """Behavioral spec for constructing a Decoder around a transformers model.""" |
|
|
| def test_construction_returns_nn_module(self): |
| |
| self.assertIsInstance(DECODER, torch.nn.Module) |
|
|
| def test_hidden_size_reflects_model_config(self): |
| |
| |
| self.assertEqual(DECODER.hidden_size, HIDDEN) |
|
|
| def test_asserts_on_hidden_size_mismatch(self): |
| |
| |
| with self.assertRaises(AssertionError): |
| Decoder(make_config(MODEL_DIR, lm_hidden_dim=HIDDEN + 1), load_backbone=True) |
|
|
| def test_lm_use_tokens_is_stored(self): |
| |
| |
| for flag in (True, False): |
| with self.subTest(flag=flag): |
| decoder = Decoder(make_config(MODEL_DIR, lm_use_tokens=flag), load_backbone=True) |
| self.assertEqual(decoder.lm_use_tokens, flag) |
|
|
|
|
| |
|
|
| class TestDecoderAccessors(unittest.TestCase): |
| """Behavioral spec for the property accessors that expose the model's pieces.""" |
|
|
| def test_token_embedding_is_the_input_embedding(self): |
| |
| self.assertIsInstance(DECODER.token_embedding, torch.nn.Embedding) |
| self.assertIs(DECODER.token_embedding, DECODER.model.get_input_embeddings()) |
|
|
| def test_token_embedding_applies_like_the_model(self): |
| |
| ids = make_ids() |
| self.assertTrue(torch.equal( |
| DECODER.token_embedding(ids), DECODER.model.get_input_embeddings()(ids))) |
|
|
| def test_head_projects_hidden_to_vocab(self): |
| |
| self.assertIsInstance(DECODER.head, torch.nn.Linear) |
| self.assertEqual(DECODER.head.in_features, HIDDEN) |
| self.assertEqual(DECODER.head.out_features, VOCAB) |
|
|
| def test_head_produces_logits_shape(self): |
| |
| hidden = torch.randn(BATCH, SEQ, HIDDEN) |
| logits = DECODER.head(hidden) |
| self.assertEqual(logits.shape, (BATCH, SEQ, VOCAB)) |
|
|
| def test_base_is_the_headless_model(self): |
| |
| self.assertIs(DECODER.base, DECODER.model.get_decoder()) |
|
|
| def test_base_returns_hidden_states_not_logits(self): |
| |
| |
| embeds = DECODER.token_embedding(make_ids()) |
| out = DECODER.base(inputs_embeds=embeds) |
| self.assertEqual(out.last_hidden_state.shape[-1], HIDDEN) |
| self.assertNotEqual(HIDDEN, VOCAB) |
|
|
|
|
| |
|
|
| class TestDecoderForward(unittest.TestCase): |
| """Behavioral spec for the training-path forward.""" |
|
|
| def test_forward_returns_two_tuple(self): |
| |
| embeds = DECODER.token_embedding(make_ids()) |
| out = DECODER(embeds) |
| self.assertIsInstance(out, tuple) |
| self.assertEqual(len(out), 2) |
|
|
| def test_forward_second_element_is_none(self): |
| |
| embeds = DECODER.token_embedding(make_ids()) |
| _, cache = DECODER(embeds) |
| self.assertIsNone(cache) |
|
|
| def test_forward_output_shape(self): |
| |
| embeds = DECODER.token_embedding(make_ids()) |
| hidden, _ = DECODER(embeds) |
| self.assertEqual(hidden.shape, (BATCH, SEQ, HIDDEN)) |
|
|
| def test_forward_consumes_embeddings_matching_direct_base_call(self): |
| |
| |
| embeds = DECODER.token_embedding(make_ids()) |
| with torch.no_grad(): |
| hidden, _ = DECODER(embeds) |
| direct = DECODER.base(inputs_embeds=embeds).last_hidden_state |
| self.assertTrue(torch.allclose(hidden, direct, atol=1e-6)) |
|
|
| def test_forward_output_dtype_tracks_model(self): |
| |
| embeds = DECODER.token_embedding(make_ids()) |
| hidden, _ = DECODER(embeds) |
| self.assertEqual(hidden.dtype, DECODER.model.dtype) |
|
|
| def test_forward_respects_attention_mask(self): |
| |
| |
| embeds = DECODER.token_embedding(make_ids()) |
| full_mask = torch.ones(BATCH, SEQ, dtype=torch.long) |
| partial_mask = full_mask.clone() |
| partial_mask[:, -2:] = 0 |
| with torch.no_grad(): |
| hidden_full, _ = DECODER(embeds, attention_mask=full_mask) |
| hidden_partial, _ = DECODER(embeds, attention_mask=partial_mask) |
| self.assertEqual(hidden_full.shape, hidden_partial.shape) |
| self.assertFalse(torch.allclose(hidden_full, hidden_partial, atol=1e-5)) |
|
|
|
|
| |
|
|
| class TestDecoderEquivalenceAndRegistration(unittest.TestCase): |
| """ |
| Behavioral spec tying the split (token_embedding -> base -> head) back to the full |
| CausalLM, and verifying the property accessors do not duplicate weights. |
| """ |
|
|
| def test_embed_base_head_reconstructs_full_logits(self): |
| |
| |
| |
| ids = make_ids() |
| with torch.no_grad(): |
| full_logits = DECODER.model(input_ids=ids).logits |
| hidden, _ = DECODER(DECODER.token_embedding(ids)) |
| reconstructed = DECODER.head(hidden) |
| self.assertEqual(reconstructed.shape, full_logits.shape) |
| self.assertTrue(torch.allclose(reconstructed, full_logits, atol=1e-4)) |
|
|
| def test_only_the_model_is_a_registered_submodule(self): |
| |
| |
| self.assertEqual(list(DECODER._modules.keys()), ["model"]) |
|
|
| def test_no_duplicate_parameters(self): |
| |
| |
| decoder_params = sum(p.numel() for p in DECODER.parameters()) |
| model_params = sum(p.numel() for p in DECODER.model.parameters()) |
| self.assertEqual(decoder_params, model_params) |
| |
| self.assertTrue(all(name.startswith("model.") for name, _ in DECODER.named_parameters())) |
|
|
| def test_weight_tying_is_preserved_through_accessors(self): |
| |
| |
| self.assertIs(DECODER.head.weight, DECODER.token_embedding.weight) |
|
|
|
|
| if __name__ == "__main__": |
| unittest.main() |
|
|