Skip to content

Commit fb58b1a

Browse files
committed
fix(checkpointing): harden precision state CPU staging
Signed-off-by: Chen Cui <chcui@nvidia.com>
1 parent e921d3c commit fb58b1a

2 files changed

Lines changed: 117 additions & 14 deletions

File tree

src/megatron/bridge/training/optim.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,23 @@ def _get_te_fused_adam_class() -> type[torch.optim.Optimizer] | None:
139139
return cast(type[torch.optim.Optimizer], FusedAdam)
140140

141141

142+
def _cpu_staging_get_unscaled_state(
143+
fallback: Callable[..., object],
144+
) -> Callable[..., torch.Tensor]:
145+
"""Wrap a TE state accessor without depending on its call signature."""
146+
147+
def _get_unscaled_state_on_cpu(*args: object, **kwargs: object) -> torch.Tensor:
148+
state = fallback(*args, **kwargs)
149+
if not isinstance(state, torch.Tensor):
150+
raise TypeError(
151+
"Transformer Engine FusedAdam.get_unscaled_state() must return a torch.Tensor "
152+
f"for CPU checkpoint staging, but returned {type(state).__name__}."
153+
)
154+
return state.cpu()
155+
156+
return _get_unscaled_state_on_cpu
157+
158+
142159
@contextmanager
143160
def memory_efficient_precision_aware_optimizer_state_checkpointing(
144161
optimizer: MegatronOptimizer | None,
@@ -196,21 +213,15 @@ def memory_efficient_precision_aware_optimizer_state_checkpointing(
196213
):
197214
continue
198215

199-
original_get_unscaled_state: Callable[..., torch.Tensor] = inner.get_unscaled_state
200-
201-
def _get_unscaled_state_on_cpu(
202-
fused_adam: torch.optim.Optimizer,
203-
param: torch.nn.Parameter,
204-
state_name: str,
205-
skip_unscale: bool = False,
206-
*,
207-
_fallback: Callable[..., torch.Tensor] = original_get_unscaled_state,
208-
) -> torch.Tensor:
209-
del fused_adam
210-
return _fallback(param, state_name, skip_unscale).cpu()
216+
original_get_unscaled_state = getattr(inner, "get_unscaled_state", None)
217+
if not callable(original_get_unscaled_state):
218+
raise RuntimeError(
219+
"CPU checkpoint staging requires Transformer Engine FusedAdam.get_unscaled_state() "
220+
"to be callable. The installed Transformer Engine checkpoint API is incompatible."
221+
)
211222

212223
previous_instance_method = inner.__dict__.get("get_unscaled_state", missing_method)
213-
setattr(inner, "get_unscaled_state", MethodType(_get_unscaled_state_on_cpu, inner))
224+
setattr(inner, "get_unscaled_state", _cpu_staging_get_unscaled_state(original_get_unscaled_state))
214225
patched.append((inner, previous_instance_method))
215226

216227
if patched:

tests/unit_tests/training/test_optim.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,12 @@ def get_unscaled_state(
194194
param: torch.nn.Parameter,
195195
state_name: str,
196196
skip_unscale: bool = False,
197+
*,
198+
multiplier: float = 1.0,
197199
) -> torch.Tensor:
198200
del skip_unscale
199201
self.get_unscaled_state_calls += 1
200-
return self.state[param][state_name].float()
202+
return self.state[param][state_name].float() * multiplier
201203

202204

203205
class _FakeParamRange:
@@ -275,6 +277,43 @@ def test_stages_unscaled_state_on_cpu_and_restores_method(self):
275277

276278
assert inner.get_unscaled_state_calls == 2
277279

280+
def test_forwards_positional_and_keyword_arguments(self):
281+
"""The wrapper stays compatible when TE extends its accessor signature."""
282+
distributed, inner, param = self._distributed_optimizer()
283+
284+
with patch("megatron.bridge.training.optim._get_te_fused_adam_class", return_value=_FakeFusedAdam):
285+
with memory_efficient_precision_aware_optimizer_state_checkpointing(distributed, enabled=True):
286+
state = inner.get_unscaled_state(param, "exp_avg", True, multiplier=3.0)
287+
288+
torch.testing.assert_close(state, torch.full((4,), 3.0))
289+
290+
def test_rejects_non_tensor_state_and_restores_instance_method(self):
291+
"""TE return-contract drift fails clearly without leaking the patch."""
292+
distributed, inner, param = self._distributed_optimizer()
293+
original_instance_method = MagicMock(return_value="not a tensor")
294+
inner.get_unscaled_state = original_instance_method
295+
296+
with (
297+
patch("megatron.bridge.training.optim._get_te_fused_adam_class", return_value=_FakeFusedAdam),
298+
pytest.raises(TypeError, match="must return a torch.Tensor"),
299+
):
300+
with memory_efficient_precision_aware_optimizer_state_checkpointing(distributed, enabled=True):
301+
inner.get_unscaled_state(param, "exp_avg")
302+
303+
assert inner.__dict__["get_unscaled_state"] is original_instance_method
304+
305+
def test_rejects_missing_te_state_accessor(self):
306+
"""An incompatible TE API fails before checkpoint construction begins."""
307+
distributed, inner, _ = self._distributed_optimizer()
308+
inner.get_unscaled_state = None
309+
310+
with (
311+
patch("megatron.bridge.training.optim._get_te_fused_adam_class", return_value=_FakeFusedAdam),
312+
pytest.raises(RuntimeError, match=r"FusedAdam\.get_unscaled_state\(\).*callable"),
313+
):
314+
with memory_efficient_precision_aware_optimizer_state_checkpointing(distributed, enabled=True):
315+
pass
316+
278317
@pytest.mark.parametrize("incompatibility", ["fp32", "cpu_offload", "fsdp", "stub"])
279318
def test_does_not_patch_incompatible_optimizer(self, incompatibility: str):
280319
state_dtype = torch.float32 if incompatibility == "fp32" else torch.bfloat16
@@ -336,6 +375,59 @@ def test_restores_method_when_checkpointing_raises(self):
336375

337376
assert "get_unscaled_state" not in inner.__dict__
338377

378+
@pytest.mark.run_only_on("gpu")
379+
def test_real_te_fused_adam_stages_state_and_restores_method(self):
380+
"""The pinned TE precision-aware optimizer returns CPU checkpoint state."""
381+
te_optimizers = pytest.importorskip("transformer_engine.pytorch.optimizers")
382+
fused_adam_class = te_optimizers.FusedAdam
383+
param = torch.nn.Parameter(torch.zeros(4, dtype=torch.bfloat16, device="cuda"))
384+
inner = fused_adam_class(
385+
[param],
386+
master_weights=True,
387+
master_weight_dtype=torch.float16,
388+
exp_avg_dtype=torch.bfloat16,
389+
exp_avg_sq_dtype=torch.bfloat16,
390+
use_decoupled_grad=True,
391+
)
392+
inner.initialize_state(param, store_param_remainders=False)
393+
distributed = _FakeDistribOpt(model_param=param, shard_main_param=param, inner=inner)
394+
distributed.config.use_precision_aware_optimizer = True
395+
396+
assert "get_unscaled_state" not in inner.__dict__
397+
with memory_efficient_precision_aware_optimizer_state_checkpointing(distributed, enabled=True) as patched:
398+
portable_state = next(iter(inner.state_dict()["state"].values()))
399+
assert patched == 1
400+
assert portable_state
401+
assert all(state.device.type == "cpu" for state in portable_state.values())
402+
403+
assert "get_unscaled_state" not in inner.__dict__
404+
assert inner.get_unscaled_state(param, "exp_avg").device.type == "cuda"
405+
406+
@pytest.mark.run_only_on("gpu")
407+
def test_real_te_fused_adam_all_fp32_state_is_not_patched(self):
408+
"""The real TE optimizer keeps its native path when no expansion is needed."""
409+
te_optimizers = pytest.importorskip("transformer_engine.pytorch.optimizers")
410+
fused_adam_class = te_optimizers.FusedAdam
411+
param = torch.nn.Parameter(torch.zeros(4, dtype=torch.bfloat16, device="cuda"))
412+
inner = fused_adam_class(
413+
[param],
414+
master_weights=True,
415+
master_weight_dtype=torch.float32,
416+
exp_avg_dtype=torch.float32,
417+
exp_avg_sq_dtype=torch.float32,
418+
use_decoupled_grad=True,
419+
)
420+
inner.initialize_state(param, store_param_remainders=False)
421+
distributed = _FakeDistribOpt(model_param=param, shard_main_param=param, inner=inner)
422+
distributed.config.use_precision_aware_optimizer = True
423+
424+
with memory_efficient_precision_aware_optimizer_state_checkpointing(distributed, enabled=True) as patched:
425+
native_state = next(iter(inner.state_dict()["state"].values()))
426+
assert patched == 0
427+
assert all(state.device.type == "cuda" for state in native_state.values())
428+
429+
assert "get_unscaled_state" not in inner.__dict__
430+
339431

340432
class TestMemoryEfficientFp32OptimizerStateLoading:
341433
"""Tests for the scoped TE FusedAdam checkpoint-load fast path."""

0 commit comments

Comments
 (0)