Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions deepspeed/runtime/activation_checkpointing/checkpointing.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,16 +350,13 @@ def merge_tensors(tensor_objects, non_tensor_objects, tensor_flags):

real_tensor_flags = None

# remove the flags that are assigned to the size of the flattened tensors
# get_partitioned_activations_for_backward() saved a (value, size) pair for every argument, so
# the flags and the non-tensors both hold one extra entry per argument. A tensor argument pairs
# with a tensor size and a non-tensor argument pairs with None, so every argument owns an even
# index in each list and dropping the odd ones restores one entry per original argument.
if PARTITION_ACTIVATIONS:
real_tensor_flags = []
previous_flag = False
for flag in tensor_flags:
if previous_flag:
previous_flag = False
continue
previous_flag = flag
real_tensor_flags.append(flag)
real_tensor_flags = tensor_flags[::2]
non_tensor_objects = non_tensor_objects[::2]
else:
real_tensor_flags = tensor_flags

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,3 +496,61 @@ def test_configure_with_contiguous_checkpointing_requires_num_checkpoints():
cp.mpu,
cp.deepspeed_checkpointing_enabled,
) = saved


def _partitioned_backward_args(args):
"""Run an argument list through the save and restore path `partition_activations` uses.

This mirrors `CheckpointFunction.forward`/`backward` without an accelerator, so it runs on CPU.
"""
cp = deepspeed.checkpointing
saved = _snapshot_ckpt_config()
saved_mp = (cp.mp_group, cp.mp_size)
try:
cp.PARTITION_ACTIVATIONS = True
cp.CONTIGUOUS_CHECKPOINTING = False
cp.mp_group, cp.mp_size = None, 1

inputs = tuple(a.clone() if torch.is_tensor(a) else a for a in args)
new_args = cp.get_partitioned_activations_for_backward(list(args), inputs, False)
tensor_args, non_tensor_args, tensor_flags = cp.extract_tensors(all_objects=tuple(new_args))
Comment on lines +515 to +516

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add a device-backed integration regression

When partitioned activation checkpointing is used in either public checkpoint path, these tests only invoke internal serialization helpers and deliberately bypass CheckpointFunction.forward/backward and the non-reentrant unpack hook. They therefore do not establish that the changed training-loop contract works through the actual checkpoint execution path. Add and run a minimal device-backed training-loop regression using checkpoint(..., partition_activations=True) with non-tensor arguments, comparing outputs and gradients to an uncheckpointed run.

AGENTS.md reference: AGENTS.md:L35-L35

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not adding one. A device-backed run of partitioned activation checkpointing needs multiple GPUs and a distributed init, and I cannot run that here, so it would be a test I am asserting rather than one I checked.

What this PR changes is the argument list that gets rebuilt on the way back out. The tests drive that directly and fail on master. The execution path around them is untouched.


for tensor in tensor_args:
if tensor is not None and getattr(tensor, 'saved_data', None) is not None:
tensor.data = tensor.saved_data.to(tensor.device)
tensor.saved_data = None

gathered = cp.gather_partitioned_activations(tensor_args)
return cp.merge_tensors(tensor_objects=gathered, non_tensor_objects=non_tensor_args, tensor_flags=tensor_flags)
finally:
_restore_ckpt_config(saved)
cp.mp_group, cp.mp_size = saved_mp


@pytest.mark.parametrize('non_tensor', [None, 2, True, 2.5, (None, 2.5)])
def test_partitioned_non_tensor_args_survive_the_round_trip(non_tensor):
"""The recompute must be handed the arguments the forward pass received, not extra `None`s."""
tensor = torch.rand(HIDDEN_DIM, requires_grad=True)
args = (tensor, non_tensor)

merged = _partitioned_backward_args(args)

assert len(merged) == len(args)
assert torch.is_tensor(merged[0])
if non_tensor is None:
assert merged[1] is None
else:
assert merged[1] == non_tensor


def test_partitioned_args_keep_their_order_around_a_non_tensor():
"""A non-tensor argument must not shift the arguments that follow it."""
first = torch.rand(HIDDEN_DIM, requires_grad=True)
second = torch.rand(HIDDEN_DIM, requires_grad=True)
args = (first, None, True, second)

merged = _partitioned_backward_args(args)

assert [torch.is_tensor(item) for item in merged] == [True, False, False, True]
assert merged[1] is None
assert merged[2] is True
Loading