|
17 | 17 | from collections.abc import Callable |
18 | 18 | from dataclasses import dataclass, field |
19 | 19 | import functools |
| 20 | +from types import SimpleNamespace |
20 | 21 | from typing import Any, Sequence |
21 | 22 | import unittest |
22 | 23 | from unittest.mock import MagicMock, Mock, patch |
@@ -1730,5 +1731,56 @@ def test_update_kv_caches_after_scan_invalid_type(self): |
1730 | 1731 | maxtext_utils.update_kv_caches_after_scan(kv_caches_tuple, returned_kv_cache, scan_length=1, block_len=2) |
1731 | 1732 |
|
1732 | 1733 |
|
| 1734 | +@pytest.mark.cpu_only |
| 1735 | +class TestGetSaveAndOffloadNames(unittest.TestCase): |
| 1736 | + """Tests for maxtext_utils.get_save_and_offload_names (pure config logic, no device needed).""" |
| 1737 | + |
| 1738 | + @staticmethod |
| 1739 | + def _cfg(remat_policy, tensors_on_device=None, tensors_to_offload=None): |
| 1740 | + return SimpleNamespace( |
| 1741 | + remat_policy=remat_policy, |
| 1742 | + tensors_on_device=tensors_on_device, |
| 1743 | + tensors_to_offload=tensors_to_offload, |
| 1744 | + ) |
| 1745 | + |
| 1746 | + def test_named_preset_matches_equivalent_custom(self): |
| 1747 | + """qkv_proj_offloaded's offload names resolve identically to an equivalent custom config. |
| 1748 | +
|
| 1749 | + A real custom config keeps decoder_layer_input on device by default, so its full tuple |
| 1750 | + differs from the preset by that (benign, boundary) save entry -- assert only the offload halves. |
| 1751 | + """ |
| 1752 | + _, preset_offload = maxtext_utils.get_save_and_offload_names(self._cfg("qkv_proj_offloaded")) |
| 1753 | + _, custom_offload = maxtext_utils.get_save_and_offload_names( |
| 1754 | + self._cfg( |
| 1755 | + "custom", |
| 1756 | + tensors_on_device=["decoder_layer_input"], |
| 1757 | + tensors_to_offload=["query_proj", "value_proj", "key_proj", "kv_proj"], |
| 1758 | + ) |
| 1759 | + ) |
| 1760 | + self.assertEqual(custom_offload, preset_offload) |
| 1761 | + |
| 1762 | + def test_kv_proj_retained_in_offload_presets(self): |
| 1763 | + """Regression guard: kv_proj must stay in the offload presets (it is a real checkpoint name).""" |
| 1764 | + _, qkv_offload = maxtext_utils.get_save_and_offload_names(self._cfg("qkv_proj_offloaded")) |
| 1765 | + _, minimal_offload = maxtext_utils.get_save_and_offload_names(self._cfg("minimal_offloaded")) |
| 1766 | + self.assertIn("kv_proj", qkv_offload) |
| 1767 | + self.assertIn("kv_proj", minimal_offload) |
| 1768 | + |
| 1769 | + def test_custom_reads_config_lists(self): |
| 1770 | + save, offload = maxtext_utils.get_save_and_offload_names( |
| 1771 | + self._cfg("custom", tensors_on_device=["context"], tensors_to_offload=["out_proj"]) |
| 1772 | + ) |
| 1773 | + self.assertEqual(save, ["context"]) |
| 1774 | + self.assertEqual(offload, ["out_proj"]) |
| 1775 | + |
| 1776 | + def test_custom_handles_none_lists(self): |
| 1777 | + self.assertEqual(maxtext_utils.get_save_and_offload_names(self._cfg("custom")), ([], [])) |
| 1778 | + |
| 1779 | + def test_non_offloading_policies_return_empty(self): |
| 1780 | + """Policies that don't use the save/offload split contribute no names to it.""" |
| 1781 | + for policy in ("full", "minimal", "save_out_proj", "save_qkv_proj", "none"): |
| 1782 | + self.assertEqual(maxtext_utils.get_save_and_offload_names(self._cfg(policy)), ([], [])) |
| 1783 | + |
| 1784 | + |
1733 | 1785 | if __name__ == "__main__": |
1734 | 1786 | unittest.main() |
0 commit comments