Skip to content

Commit 32ccb51

Browse files
committed
add util function get_save_and_offload_names
1 parent 329fd7b commit 32ccb51

3 files changed

Lines changed: 46 additions & 28 deletions

File tree

src/maxtext/layers/decoders.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,9 +414,9 @@ def get_remat_policy(self):
414414
elif cfg.remat_policy in ("qkv_proj_offloaded", "minimal_offloaded", "custom"):
415415
# minimal_offloaded offloads all except context. All three share a single
416416
# source of truth for their save/offload name lists (see
417-
# maxtext_utils.get_offload_remat_names) so that offloading configured via
417+
# maxtext_utils.get_save_and_offload_names) so that offloading configured via
418418
# `custom` resolves identically to the named presets.
419-
save_names, offload_names = maxtext_utils.get_offload_remat_names(cfg)
419+
save_names, offload_names = maxtext_utils.get_save_and_offload_names(cfg)
420420
policy = jax.checkpoint_policies.save_and_offload_only_these_names(
421421
names_which_can_be_saved=save_names,
422422
names_which_can_be_offloaded=offload_names,

src/maxtext/utils/maxtext_utils.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -195,18 +195,26 @@ def should_prevent_cse_in_remat(config):
195195
return True
196196

197197

198-
def get_offload_remat_names(config):
199-
"""Returns ``(save_names, offload_names)`` for offloading remat policies, else ``None``.
200-
201-
Single source of truth for which checkpointed tensors a remat policy saves on
202-
device vs. offloads to pinned host. Shared by ``Decoder.get_remat_policy`` (which
203-
builds the save-and-offload policy) and by models that must convert an offload
204-
into a device-save at a scan boundary that cannot carry pinned-host residuals
205-
(e.g. Gemma4's global-layer trip-count-one scan). Keeping both callers on this
206-
helper ensures ``remat_policy=qkv_proj_offloaded`` and ``remat_policy=custom``
207-
with the same tensors marked ``offload`` resolve to identical name sets.
208-
209-
Returns ``None`` for non-offloading policies.
198+
def get_save_and_offload_names(config) -> tuple[list[str], list[str]]:
199+
"""Returns the ``(save_names, offload_names)`` split for remat policies built via
200+
``jax.checkpoint_policies.save_and_offload_only_these_names``.
201+
202+
``save_names`` are checkpointed tensors kept in device HBM; ``offload_names`` are moved to
203+
pinned host. This is the single source of truth shared by ``Decoder.get_remat_policy`` (which
204+
builds the save-and-offload policy) and by models that use custom ways to handle offload (
205+
e.g. Gemma4's global-layer with scan). It also makes ``remat_policy=custom`` with tensors marked
206+
``offload`` resolve to the same name sets as the named presets.
207+
208+
Returns a ``(save_names, offload_names)`` tuple:
209+
* ``custom``: ``(config.tensors_on_device, config.tensors_to_offload)`` -- the per-tensor
210+
assignments. Either list may be empty: all tensors set to ``device`` gives an empty offload
211+
list, all set to ``remat`` gives ``([], [])``.
212+
* ``qkv_proj_offloaded`` / ``minimal_offloaded``: ``([], <hardcoded offload names>)`` -- presets
213+
that only offload and save nothing on device.
214+
* any other policy (``full``, ``minimal``, ``save_*``, ``none``, ...): ``([], [])`` -- these do
215+
not use ``save_and_offload_only_these_names``, so they contribute no names to this split.
216+
Note ``([], [])`` here means "no names for this split", not that the policy saves nothing
217+
overall (e.g. ``save_out_proj`` still saves ``out_proj`` via ``save_only_these_names``).
210218
"""
211219
if config.remat_policy == "qkv_proj_offloaded":
212220
return [], ["query_proj", "value_proj", "key_proj", "kv_proj"]
@@ -225,7 +233,7 @@ def get_offload_remat_names(config):
225233
]
226234
if config.remat_policy == "custom":
227235
return list(config.tensors_on_device or []), list(config.tensors_to_offload or [])
228-
return None
236+
return [], []
229237

230238

231239
def load_compiled(config, partial_train, state, execution_devices):

tests/unit/maxtext_utils_test.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,8 +1731,9 @@ def test_update_kv_caches_after_scan_invalid_type(self):
17311731
maxtext_utils.update_kv_caches_after_scan(kv_caches_tuple, returned_kv_cache, scan_length=1, block_len=2)
17321732

17331733

1734-
class TestGetOffloadRematNames(unittest.TestCase):
1735-
"""Tests for maxtext_utils.get_offload_remat_names."""
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)."""
17361737

17371738
@staticmethod
17381739
def _cfg(remat_policy, tensors_on_device=None, tensors_to_offload=None):
@@ -1743,33 +1744,42 @@ def _cfg(remat_policy, tensors_on_device=None, tensors_to_offload=None):
17431744
)
17441745

17451746
def test_named_preset_matches_equivalent_custom(self):
1746-
"""qkv_proj_offloaded resolves identically to custom with the same offloads."""
1747-
preset = maxtext_utils.get_offload_remat_names(self._cfg("qkv_proj_offloaded"))
1748-
custom = maxtext_utils.get_offload_remat_names(
1749-
self._cfg("custom", tensors_on_device=[], tensors_to_offload=["query_proj", "value_proj", "key_proj", "kv_proj"])
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+
)
17501759
)
1751-
self.assertEqual(preset, custom)
1760+
self.assertEqual(custom_offload, preset_offload)
17521761

17531762
def test_kv_proj_retained_in_offload_presets(self):
17541763
"""Regression guard: kv_proj must stay in the offload presets (it is a real checkpoint name)."""
1755-
_, qkv_offload = maxtext_utils.get_offload_remat_names(self._cfg("qkv_proj_offloaded"))
1756-
_, minimal_offload = maxtext_utils.get_offload_remat_names(self._cfg("minimal_offloaded"))
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"))
17571766
self.assertIn("kv_proj", qkv_offload)
17581767
self.assertIn("kv_proj", minimal_offload)
17591768

17601769
def test_custom_reads_config_lists(self):
1761-
save, offload = maxtext_utils.get_offload_remat_names(
1770+
save, offload = maxtext_utils.get_save_and_offload_names(
17621771
self._cfg("custom", tensors_on_device=["context"], tensors_to_offload=["out_proj"])
17631772
)
17641773
self.assertEqual(save, ["context"])
17651774
self.assertEqual(offload, ["out_proj"])
17661775

17671776
def test_custom_handles_none_lists(self):
1768-
self.assertEqual(maxtext_utils.get_offload_remat_names(self._cfg("custom")), ([], []))
1777+
self.assertEqual(maxtext_utils.get_save_and_offload_names(self._cfg("custom")), ([], []))
17691778

1770-
def test_non_offloading_policies_return_none(self):
1779+
def test_non_offloading_policies_return_empty(self):
1780+
"""Policies that don't use the save/offload split contribute no names to it."""
17711781
for policy in ("full", "minimal", "save_out_proj", "save_qkv_proj", "none"):
1772-
self.assertIsNone(maxtext_utils.get_offload_remat_names(self._cfg(policy)))
1782+
self.assertEqual(maxtext_utils.get_save_and_offload_names(self._cfg(policy)), ([], []))
17731783

17741784

17751785
if __name__ == "__main__":

0 commit comments

Comments
 (0)