| ```instructions | |
| # Testing Instructions (pytest) | |
| > Para tests en PAMPAr-Coder usando pytest. | |
| ## Regla de Oro | |
| **Cada módulo nuevo DEBE tener:** | |
| 1. Un test de happy path | |
| 2. Un test de error/edge case | |
| 3. Un test de shapes (para tensores) | |
| ## Estructura de Tests | |
| ``` | |
| tests/ | |
| ├── test_modelo.py # Tests del modelo principal | |
| ├── test_talamo.py # Tests del tálamo | |
| ├── test_llaves.py # Tests de LLAVES | |
| ├── test_generation.py # Tests de generación | |
| └── conftest.py # Fixtures compartidos | |
| ```` | |
| ## Fixtures (conftest.py) | |
| ```python | |
| import pytest | |
| import torch | |
| from pampar.coder.v2.config import ConfigPampaRCoderV2 | |
| @pytest.fixture | |
| def device(): | |
| """Device para tests: CUDA si disponible, else CPU.""" | |
| return torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| @pytest.fixture | |
| def small_config(): | |
| """Configuración mínima para tests rápidos.""" | |
| return ConfigPampaRCoderV2( | |
| vocab_size=1000, | |
| hidden_size=64, | |
| num_layers=2, | |
| num_heads=4, | |
| intermediate_size=256, | |
| ) | |
| @pytest.fixture | |
| def batch(): | |
| """Batch de ejemplo para tests.""" | |
| return { | |
| "input_ids": torch.randint(0, 1000, (2, 16)), | |
| "attention_mask": torch.ones(2, 16, dtype=torch.long), | |
| "labels": torch.randint(0, 1000, (2, 16)), | |
| } | |
| ```` | |
| ## Patrones de Test | |
| ### Test de Shapes | |
| ```python | |
| class TestModelShapes: | |
| def test_embedding_output_shape(self, small_config): | |
| from pampar.coder.v2.modelo import PampaRCoderV2 | |
| model = PampaRCoderV2(small_config) | |
| input_ids = torch.randint(0, small_config.vocab_size, (2, 16)) | |
| output = model(input_ids) | |
| assert output.logits.shape == (2, 16, small_config.vocab_size) | |
| assert output.hidden_states.shape == (2, 16, small_config.hidden_size) | |
| def test_attention_shape(self, small_config): | |
| from pampar.coder.v2.talamo import TalamoBrodmann | |
| talamo = TalamoBrodmann(small_config) | |
| x = torch.randn(2, 16, small_config.hidden_size) | |
| out = talamo(x) | |
| assert out.shape == x.shape | |
| ``` | |
| ### Test de Gradientes | |
| ```python | |
| class TestGradientFlow: | |
| def test_all_parameters_have_gradients(self, small_config): | |
| model = PampaRCoderV2(small_config) | |
| input_ids = torch.randint(0, small_config.vocab_size, (1, 8)) | |
| output = model(input_ids, labels=input_ids) | |
| output.loss.backward() | |
| for name, param in model.named_parameters(): | |
| if param.requires_grad: | |
| assert param.grad is not None, f"No grad: {name}" | |
| assert not torch.isnan(param.grad).any(), f"NaN grad: {name}" | |
| def test_gradient_clipping(self, small_config): | |
| model = PampaRCoderV2(small_config) | |
| # ... setup con gradientes grandes | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| total_norm = sum(p.grad.norm() ** 2 for p in model.parameters()).sqrt() | |
| assert total_norm <= 1.0 + 1e-6 | |
| ``` | |
| ### Test con Mock | |
| ```python | |
| from unittest.mock import MagicMock, patch | |
| class TestTraining: | |
| @patch("torch.cuda.is_available", return_value=False) | |
| def test_cpu_fallback(self, mock_cuda): | |
| """Verifica que funciona sin GPU.""" | |
| from pampar.coder.v2.modelo import PampaRCoderV2 | |
| config = ConfigPampaRCoderV2.from_preset("mini") | |
| model = PampaRCoderV2(config) | |
| input_ids = torch.randint(0, config.vocab_size, (1, 8)) | |
| output = model(input_ids) | |
| assert output.logits is not None | |
| ``` | |
| ### Test Parametrizado | |
| ```python | |
| @pytest.mark.parametrize("batch_size", [1, 2, 4]) | |
| @pytest.mark.parametrize("seq_len", [8, 16, 32]) | |
| def test_variable_batch_seq(small_config, batch_size, seq_len): | |
| model = PampaRCoderV2(small_config) | |
| input_ids = torch.randint(0, small_config.vocab_size, (batch_size, seq_len)) | |
| output = model(input_ids) | |
| assert output.logits.shape == (batch_size, seq_len, small_config.vocab_size) | |
| @pytest.mark.parametrize("preset", ["mini", "1.5B", "3B"]) | |
| def test_preset_configs(preset): | |
| config = ConfigPampaRCoderV2.from_preset(preset) | |
| assert config.vocab_size == 48000 | |
| assert config.hidden_size > 0 | |
| ``` | |
| ### Test de LLAVES | |
| ```python | |
| class TestLlaves: | |
| def test_llaves_are_not_trainable(self): | |
| from pampar.coder.v2.llaves import LlavesModule | |
| llaves = LlavesModule() | |
| for param in llaves.parameters(): | |
| assert not param.requires_grad, "LLAVES no deben ser entrenables" | |
| def test_llaves_int8_quantization(self): | |
| from pampar.coder.v2.llaves import LlavesModule | |
| llaves = LlavesModule() | |
| assert llaves.lookup_table.dtype == torch.int8 | |
| def test_llaves_pattern_matching(self): | |
| from pampar.coder.v2.llaves import classify_token | |
| # Declaración Python | |
| assert classify_token("def ") in range(1, 16) # SINTAXIS | |
| # Operador lógico | |
| assert classify_token("if ") in range(31, 43) # LÓGICO | |
| ``` | |
| ## Markers | |
| ```python | |
| # En pyproject.toml o pytest.ini: | |
| # [tool.pytest.ini_options] | |
| # markers = [ | |
| # "slow: marks tests as slow", | |
| # "gpu: marks tests requiring GPU", | |
| # ] | |
| @pytest.mark.slow | |
| def test_full_training_loop(): | |
| """Test lento de training completo.""" | |
| ... | |
| @pytest.mark.gpu | |
| @pytest.mark.skipif(not torch.cuda.is_available(), reason="GPU required") | |
| def test_cuda_forward(): | |
| """Test que requiere GPU.""" | |
| ... | |
| ``` | |
| ## Ejecutar Tests | |
| ```bash | |
| # Todos los tests | |
| pytest | |
| # Solo tests rápidos | |
| pytest -m "not slow" | |
| # Con coverage | |
| pytest --cov=pampar --cov-report=html | |
| # Verbose con print output | |
| pytest -v -s | |
| # Solo un archivo | |
| pytest tests/test_modelo.py | |
| # Solo un test específico | |
| pytest tests/test_modelo.py::TestModelShapes::test_embedding_output_shape | |
| ``` | |
| ``` | |
| ``` | |