Skip to content

Commit 1942d0e

Browse files
Merge pull request AI-Hypercomputer#4506 from AI-Hypercomputer:aireen/pr-offload-remat-names
PiperOrigin-RevId: 949660366
2 parents 4b357c1 + 32ccb51 commit 1942d0e

3 files changed

Lines changed: 101 additions & 29 deletions

File tree

src/maxtext/layers/decoders.py

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -411,36 +411,15 @@ def get_remat_policy(self):
411411
"kv_proj",
412412
"qkv_proj",
413413
)
414-
elif cfg.remat_policy == "qkv_proj_offloaded":
414+
elif cfg.remat_policy in ("qkv_proj_offloaded", "minimal_offloaded", "custom"):
415+
# minimal_offloaded offloads all except context. All three share a single
416+
# source of truth for their save/offload name lists (see
417+
# maxtext_utils.get_save_and_offload_names) so that offloading configured via
418+
# `custom` resolves identically to the named presets.
419+
save_names, offload_names = maxtext_utils.get_save_and_offload_names(cfg)
415420
policy = jax.checkpoint_policies.save_and_offload_only_these_names(
416-
names_which_can_be_saved=[],
417-
names_which_can_be_offloaded=["query_proj", "value_proj", "key_proj", "kv_proj"],
418-
offload_src="device",
419-
offload_dst="pinned_host",
420-
)
421-
elif cfg.remat_policy == "minimal_offloaded":
422-
# offload all except context
423-
policy = jax.checkpoint_policies.save_and_offload_only_these_names(
424-
names_which_can_be_saved=[],
425-
names_which_can_be_offloaded=[
426-
"query_proj",
427-
"value_proj",
428-
"key_proj",
429-
"kv_proj",
430-
"qkv_proj",
431-
"out_proj",
432-
"mlpwi_0",
433-
"mlpwi_1",
434-
"mlpwi",
435-
"mlpwo",
436-
],
437-
offload_src="device",
438-
offload_dst="pinned_host",
439-
)
440-
elif cfg.remat_policy == "custom":
441-
policy = jax.checkpoint_policies.save_and_offload_only_these_names(
442-
names_which_can_be_saved=cfg.tensors_on_device,
443-
names_which_can_be_offloaded=cfg.tensors_to_offload,
421+
names_which_can_be_saved=save_names,
422+
names_which_can_be_offloaded=offload_names,
444423
offload_src="device",
445424
offload_dst="pinned_host",
446425
)

src/maxtext/utils/maxtext_utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,47 @@ def should_prevent_cse_in_remat(config):
195195
return True
196196

197197

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``).
218+
"""
219+
if config.remat_policy == "qkv_proj_offloaded":
220+
return [], ["query_proj", "value_proj", "key_proj", "kv_proj"]
221+
if config.remat_policy == "minimal_offloaded":
222+
return [], [
223+
"query_proj",
224+
"value_proj",
225+
"key_proj",
226+
"kv_proj",
227+
"qkv_proj",
228+
"out_proj",
229+
"mlpwi_0",
230+
"mlpwi_1",
231+
"mlpwi",
232+
"mlpwo",
233+
]
234+
if config.remat_policy == "custom":
235+
return list(config.tensors_on_device or []), list(config.tensors_to_offload or [])
236+
return [], []
237+
238+
198239
def load_compiled(config, partial_train, state, execution_devices):
199240
"""# Loading a serialized compiled train step function."""
200241

tests/unit/maxtext_utils_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from collections.abc import Callable
1818
from dataclasses import dataclass, field
1919
import functools
20+
from types import SimpleNamespace
2021
from typing import Any, Sequence
2122
import unittest
2223
from unittest.mock import MagicMock, Mock, patch
@@ -1730,5 +1731,56 @@ def test_update_kv_caches_after_scan_invalid_type(self):
17301731
maxtext_utils.update_kv_caches_after_scan(kv_caches_tuple, returned_kv_cache, scan_length=1, block_len=2)
17311732

17321733

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+
17331785
if __name__ == "__main__":
17341786
unittest.main()

0 commit comments

Comments
 (0)