Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / platform / torch / activation_checkpoint / sac.py: 97%
124 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-22 04:23 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-22 04:23 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15# Adapted from https://github.com/pytorch/pytorch/blob/release/2.6/torch/utils/checkpoint.py
16# enhanced with selective checkpoint support swap
17# ============================================================================
18"""enhanced with selective checkpoint support swap"""
19# pylint: disable=W0212, W0613, C0115, C0116, C0103, R1705
20from collections import defaultdict
21from typing import Any, Dict, List, Optional, Union
23import torch
24import torch.fx.traceback as fx_traceback
25from torch._functorch._aot_autograd.functional_utils import is_fun
26from torch.utils._pytree import tree_map
27from torch.utils._python_dispatch import TorchDispatchMode
28from hyper_parallel.core.activation_checkpoint import CheckpointPolicy # patch code
29from hyper_parallel.core.activation_checkpoint.swap import ( # patch code
30 SwapManager,
31 SwapTensor,
32 Storage,
33)
36def _is_compiling(func, args, kwargs):
37 # Check if we are under AOTAutograd tracing
38 # There should probably be a better way to do this...
39 # NOTE: unify _is_compiling across all compile stacks
40 for arg in args:
41 if isinstance(arg, torch.Tensor) and is_fun(arg):
42 return True
43 return False
46class _VersionWrapper:
47 # Check that cached tensors are not mutated.
48 def __init__(self, val):
49 self.val: Union[torch.Tensor, Any] = val
50 self.version: Optional[int] = (
51 val._version if isinstance(val, torch.Tensor) else None
52 )
54 def get_val(self, allow_cache_entry_mutation):
55 if self.version is not None and not allow_cache_entry_mutation:
56 if self.val._version != self.version:
57 # Can we give user a stack trace of where the mutation happened?
58 raise RuntimeError(
59 "Tensor cached during selective activation checkpoint has been mutated"
60 )
61 return self.val
64class _SwapCacheEntry:
65 """Pair the recompute cache and swap record around the same tensor object."""
66 def __init__(self, val, funcname, group_swap=False):
67 self.save = _VersionWrapper(val)
68 self.swap = SwapTensor(val, funcname, group_swap=group_swap)
71def _maybe_detach(x, any_ret_has_alias_info):
72 # We detach for two separate reasons:
73 # - For view ops, we need to ensure that when the tensor is returned from
74 # CachedDispatchMode, as_view sees that the AutogradMeta is nullptr
75 # - Avoid reference cycles
76 # For case 1, it is not enough to check whether x has differentiable dtype
77 # because non-differentiable dtype can have non-nullptr AutogradMeta, e.g.
78 # when the tensor is a view.
79 need_detach = (isinstance(x, torch.Tensor)
80 and (x.is_floating_point() or x.is_complex() or any_ret_has_alias_info))
81 if need_detach:
82 with torch._C._SetExcludeDispatchKeyGuard(torch._C.DispatchKey.ADInplaceOrView, False):
83 # Ensure that view performed beneath autograd properly propagates
84 # version counter. TODO: Use reentrant_dispatch instead of
85 # manually manipulating dispatch keys. Using reentrant_dispatch
86 # would respect inference_mode, though that is not relevant for
87 # this case.
88 x = x.detach()
89 return x
92class SelectiveCheckpointContext:
93 """
94 Context passed to policy function during selective checkpointing.
96 This class is used to pass relevant metadata to the policy function during
97 selective checkpointing. The metadata includes whether the current invocation
98 of the policy function is during recomputation or not.
100 Example:
101 >>> # xdoctest: +SKIP(stub)
102 >>>
103 >>> def policy_fn(ctx, op, *args, **kwargs):
104 >>> print(ctx.is_recompute)
105 >>>
106 >>> context_fn = functools.partial(create_selective_checkpoint_contexts, policy_fn)
107 >>>
108 >>> out = torch.utils.checkpoint.checkpoint(
109 >>> fn, x, y,
110 >>> use_reentrant=False,
111 >>> context_fn=context_fn,
112 >>> )
113 """
114 def __init__(self, *, is_recompute):
115 self.is_recompute = is_recompute
118def _policy_from_bool(b):
119 # For backward compatibility
120 return CheckpointPolicy.MUST_SAVE if b else CheckpointPolicy.PREFER_RECOMPUTE
123SAC_IGNORED_OPS = {
124 # AC inserts different number of detach during forward and recompute.
125 torch.ops.aten.detach.default,
126 # AC's determinism check invokes additional metadata ops during forward.
127 # With subclasses involved, these metadata ops become dispatchable, this
128 # can result in incorrectness if these ops are selected cached.
129 torch.ops.prim.device.default,
130} | set(torch._subclasses.functional_tensor.FunctionalTensor.metadata_fns)
133def ignore_sac_ops(ignore_ops: List[Optional[object]]) -> None:
134 """Add available operators to the selective-checkpoint ignore set.
136 Args:
137 ops (List[Optional[object]]): Operators to execute without selective-AC replay accounting.
138 ``None`` entries are ignored for optional-version compatibility.
139 """
140 SAC_IGNORED_OPS.update(op for op in ignore_ops if op is not None)
143class _CachingTorchDispatchMode(TorchDispatchMode):
144 # Used together with _CachedTorchDispatchMode to implement SAC.
145 def __init__(self, policy_fn, swap_storage, storage, group_swap=False):
146 self.policy_fn = policy_fn
147 self.swap_storage = swap_storage
148 self.storage = storage
149 self.add_to_storage = False
150 self.group_swap = group_swap
151 # Cache context and singleton to avoid per-dispatch allocation / lookup.
152 self._swap_manager = SwapManager()
153 self._group_prefix = ""
155 def __torch_dispatch__(self, func, types, args=(), kwargs=None):
156 if func in SAC_IGNORED_OPS:
157 return func(*args, **kwargs)
159 kwargs = {} if kwargs is None else kwargs
160 policy = self.policy_fn(SelectiveCheckpointContext(is_recompute=False),
161 func, *args, **kwargs)
162 if isinstance(policy, bool):
163 policy = _policy_from_bool(policy)
165 is_compiling = _is_compiling(func, args, kwargs)
167 if is_compiling:
168 # Overwrite each node's "recompute" tag to add in the user annotation.
169 fx_traceback.current_meta["recompute"] = policy
171 out = func(*args, **kwargs)
173 has_alias = any(ret.alias_info is not None for ret in func._schema.returns)
175 if policy in (CheckpointPolicy.MUST_SAVE, CheckpointPolicy.PREFER_SAVE):
176 self.storage[func].append(
177 tree_map(lambda x: _VersionWrapper(_maybe_detach(x, has_alias)), out)
178 )
179 elif policy == CheckpointPolicy.MUST_SWAP: # patch code
180 if not self.add_to_storage:
181 group_name = self._swap_manager.get_current_group_name()
182 self._group_prefix = f"{group_name}::"
183 self._swap_manager.add_storage(group_name, self.swap_storage)
184 self.add_to_storage = True
185 funcname = f"{self._group_prefix}{func}"
186 group_swap = self.group_swap
187 entries = tree_map(
188 lambda x: _SwapCacheEntry(_maybe_detach(x, has_alias), funcname, group_swap=group_swap), out,
189 )
190 self.storage[func].append(tree_map(lambda x: x.save, entries))
191 self.swap_storage[func].append(tree_map(lambda x: x.swap, entries))
192 elif policy != CheckpointPolicy.MUST_RECOMPUTE:
193 raise RuntimeError(f"Checkpoint Activation: {func} encountered an invalid policy {policy}")
194 return out
197class _CachedTorchDispatchMode(TorchDispatchMode):
198 # Used together with _CachingTorchDispatchMode to implement SAC.
199 def __init__(self, policy_fn, swap_storage, storage, allow_cache_entry_mutation):
200 self.policy_fn = policy_fn
201 self.swap_storage = swap_storage
202 self.storage = storage
203 self.allow_cache_entry_mutation = allow_cache_entry_mutation
204 self._swap_cleared = False
206 def __torch_dispatch__(self, func, types, args=(), kwargs=None):
207 if func in SAC_IGNORED_OPS:
208 return func(*args, **kwargs)
210 kwargs = {} if kwargs is None else kwargs
211 policy = self.policy_fn(SelectiveCheckpointContext(is_recompute=True),
212 func, *args, **kwargs)
213 if isinstance(policy, bool):
214 policy = _policy_from_bool(policy)
216 is_compiling = _is_compiling(func, args, kwargs)
218 if not self._swap_cleared:
219 self.swap_storage.clear()
220 self._swap_cleared = True
222 # MUST_SAVE, PREFER_SAVE, and MUST_SWAP all restore from storage identically.
223 if (policy in (CheckpointPolicy.MUST_SAVE, CheckpointPolicy.PREFER_SAVE, CheckpointPolicy.MUST_SWAP)
224 or is_compiling):
225 storage = self.storage.get(func) # patch code
226 if storage is None:
227 raise RuntimeError(f"{func} encountered during backward, but not found in storage")
228 if len(storage) == 0:
229 raise RuntimeError(
230 "Trying to backward an extra time. You are only allowed to backward once "
231 "on any region computed under selective activation checkpoint."
232 )
233 out = tree_map(lambda x: x.get_val(self.allow_cache_entry_mutation), storage.pop(0))
234 else:
235 out = func(*args, **kwargs)
236 return out
239def create_selective_checkpoint_contexts(policy_fn_or_list, allow_cache_entry_mutation=False, group_swap=False):
240 """
241 Helper to avoid recomputing certain ops during activation checkpointing.
243 Use this with `torch.utils.checkpoint.checkpoint` to control which
244 operations are recomputed during the backward pass.
246 Args:
247 policy_fn_or_list (Callable or List):
248 - If a policy function is provided, it should accept a
249 :class:`SelectiveCheckpointContext`, the :class:`OpOverload`, args and
250 kwargs to the op, and return a :class:`CheckpointPolicy` enum value
251 indicating whether the execution of the op should be recomputed or not.
252 - If a list of operations is provided, it is equivalent to a policy
253 returning `CheckpointPolicy.MUST_SAVE` for the specified
254 operations and `CheckpointPolicy.PREFER_RECOMPUTE` for all other
255 operations.
256 allow_cache_entry_mutation (bool, optional): By default, an error is
257 raised if any tensors cached by selective activation checkpoint are
258 mutated in order to ensure correctness. If set to `True`, this check
259 is disabled.
260 Returns:
261 A tuple of two context managers.
263 Example:
264 >>> # xdoctest: +REQUIRES(LINUX)
265 >>> import functools
266 >>>
267 >>> x = torch.rand(10, 10, requires_grad=True)
268 >>> y = torch.rand(10, 10, requires_grad=True)
269 >>>
270 >>> ops_to_save = [
271 >>> torch.ops.aten.mm.default,
272 >>> ]
273 >>>
274 >>> def policy_fn(ctx, op, *args, **kwargs):
275 >>> if op in ops_to_save:
276 >>> return CheckpointPolicy.MUST_SAVE
277 >>> else:
278 >>> return CheckpointPolicy.PREFER_RECOMPUTE
279 >>>
280 >>> context_fn = functools.partial(create_selective_checkpoint_contexts, policy_fn)
281 >>>
282 >>> # or equivalently
283 >>> context_fn = functools.partial(create_selective_checkpoint_contexts, ops_to_save)
284 >>>
285 >>> def fn(x, y):
286 >>> return torch.sigmoid(torch.matmul(torch.matmul(x, y), y)) * y
287 >>>
288 >>> out = torch.utils.checkpoint.checkpoint(
289 >>> fn, x, y,
290 >>> use_reentrant=False,
291 >>> context_fn=context_fn,
292 >>> )
293 """
294 # NB: If grad_mode is disabled, checkpoint would not run forward under
295 # context_fn anyway, so proceed as usual.
296 if policy_fn_or_list is None:
297 def policy_fn(_ctx, _op, *_args, **_kwargs):
298 return CheckpointPolicy.PREFER_RECOMPUTE
299 elif isinstance(policy_fn_or_list, list):
300 for op in policy_fn_or_list:
301 if not isinstance(op, torch._ops.OpOverload):
302 _extra_msg = (
303 "Please update the OpOverloadPacket to a specific OpOverload."
304 "For example, if you have `torch.ops.aten.mm`, change it to `torch.ops.aten.mm.default`."
305 ) if isinstance(op, torch._ops.OpOverloadPacket) else ""
306 raise ValueError(
307 f"Expected op in `op_list` to be an OpOverload but got: {op} "
308 f"of type {type(op)}. {_extra_msg}"
309 )
311 def policy_fn(ctx, op, *args, **kwargs):
312 if op in policy_fn_or_list:
313 return CheckpointPolicy.MUST_SAVE
314 else:
315 return CheckpointPolicy.PREFER_RECOMPUTE
316 elif callable(policy_fn_or_list):
317 policy_fn = policy_fn_or_list
318 else:
319 raise TypeError("policy_fn_or_list must be either a function or a list of ops.")
321 swap_storage = Storage() # patch code
322 storage: Dict[Any, List[Any]] = defaultdict(list)
323 return (
324 _CachingTorchDispatchMode(policy_fn, swap_storage, storage, group_swap=group_swap),
325 _CachedTorchDispatchMode(policy_fn, swap_storage, storage, allow_cache_entry_mutation),
326 )