| """ |
| Tests for small but training-critical pure functions that had no coverage: |
| - train.get_lr : warmup + cosine-decay learning-rate schedule |
| - data.data_utils._is_batch_valid / synchronized_dataloader_step (single-GPU path) |
| """ |
| import unittest |
|
|
| from train import get_lr |
| from data.data_utils import _is_batch_valid, synchronized_dataloader_step |
|
|
| import torch |
|
|
|
|
| class TestGetLr(unittest.TestCase): |
| MAX_LR = 1.0 |
| MAX_STEPS = 1000 |
| WARMUP = 30 |
| MIN_LR = 0.1 |
|
|
| def test_warmup_starts_near_zero(self): |
| |
| lr0 = get_lr(0, self.MAX_LR, self.MAX_STEPS) |
| self.assertAlmostEqual(lr0, self.MAX_LR / self.WARMUP, places=6) |
| self.assertGreater(lr0, 0.0) |
|
|
| def test_warmup_reaches_peak_at_end(self): |
| lr = get_lr(self.WARMUP - 1, self.MAX_LR, self.MAX_STEPS) |
| self.assertAlmostEqual(lr, self.MAX_LR, places=6) |
|
|
| def test_warmup_is_monotonic_increasing(self): |
| lrs = [get_lr(it, self.MAX_LR, self.MAX_STEPS) for it in range(self.WARMUP)] |
| for a, b in zip(lrs, lrs[1:]): |
| self.assertLess(a, b) |
|
|
| def test_floor_after_max_steps(self): |
| self.assertAlmostEqual(get_lr(self.MAX_STEPS + 1, self.MAX_LR, self.MAX_STEPS), |
| self.MIN_LR, places=6) |
|
|
| def test_decay_is_monotonic_non_increasing(self): |
| lrs = [get_lr(it, self.MAX_LR, self.MAX_STEPS) |
| for it in range(self.WARMUP, self.MAX_STEPS + 1)] |
| for a, b in zip(lrs, lrs[1:]): |
| self.assertLessEqual(b, a + 1e-9) |
|
|
| def test_cosine_midpoint_is_halfway(self): |
| |
| mid_it = int(self.WARMUP + (self.MAX_STEPS - self.WARMUP) / 2) |
| expected = self.MIN_LR + 0.5 * (self.MAX_LR - self.MIN_LR) |
| self.assertAlmostEqual(get_lr(mid_it, self.MAX_LR, self.MAX_STEPS), expected, places=2) |
|
|
| def test_never_below_floor_after_warmup(self): |
| |
| |
| for it in range(self.WARMUP, self.MAX_STEPS + 1, 17): |
| self.assertGreaterEqual(get_lr(it, self.MAX_LR, self.MAX_STEPS), self.MIN_LR - 1e-9) |
|
|
|
|
| def _valid_batch(): |
| return { |
| "input_ids": torch.zeros(2, 3, dtype=torch.long), |
| "labels": torch.zeros(2, 3, dtype=torch.long), |
| "attention_mask": torch.ones(2, 3, dtype=torch.long), |
| "images": [[torch.zeros(3, 4)], [torch.zeros(3, 4)]], |
| "model_patch_positions": [[torch.zeros(3, 2)], [torch.zeros(3, 2)]], |
| } |
|
|
|
|
| class TestIsBatchValid(unittest.TestCase): |
| def test_none_or_empty_is_invalid(self): |
| self.assertFalse(_is_batch_valid(None)) |
| self.assertFalse(_is_batch_valid({})) |
|
|
| def test_empty_input_ids_is_invalid(self): |
| b = _valid_batch(); b["input_ids"] = [] |
| self.assertFalse(_is_batch_valid(b)) |
|
|
| def test_empty_images_is_invalid(self): |
| b = _valid_batch(); b["images"] = [] |
| self.assertFalse(_is_batch_valid(b)) |
|
|
| def test_images_with_no_actual_image_is_invalid(self): |
| |
| b = _valid_batch(); b["images"] = [[], []] |
| self.assertFalse(_is_batch_valid(b)) |
|
|
| def test_well_formed_batch_is_valid(self): |
| self.assertTrue(_is_batch_valid(_valid_batch())) |
|
|
|
|
| class TestSynchronizedDataloaderStepSingleGPU(unittest.TestCase): |
| def test_filters_invalid_batches_when_not_distributed(self): |
| good1 = _valid_batch() |
| bad = _valid_batch(); bad["images"] = [[], []] |
| good2 = _valid_batch() |
| loader = [good1, bad, good2] |
| out = list(synchronized_dataloader_step(loader, is_dist=False)) |
| self.assertEqual(len(out), 2) |
| self.assertIs(out[0], good1) |
| self.assertIs(out[1], good2) |
|
|
| def test_empty_loader_yields_nothing(self): |
| self.assertEqual(list(synchronized_dataloader_step([], is_dist=False)), []) |
|
|
|
|
| if __name__ == "__main__": |
| unittest.main() |
|
|