Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / activation_memory / checkpoint_exclude.py: 100%
163 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +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.
14# ============================================================================
15"""Saved-tensor helpers for regions excluded from checkpoint recomputation."""
16from collections import defaultdict, deque
17from dataclasses import dataclass
18from functools import lru_cache
19from typing import Any, Deque, Dict, List, Optional, Tuple
21import torch
23_RECOMPUTE_INPUT_HANDLE_ATTR = "_hyper_parallel_recompute_input_handle"
24_SAVE_OUTPUT_SOURCE_ATTR = "_hyper_parallel_save_output_source"
25_MISSING = object()
26_InputPath = Tuple[Tuple[str, Any], ...]
27_TensorInput = Tuple[_InputPath, Any]
30class _RecomputedInputHandle:
31 """Defer one excluded-region saved input until checkpoint replay."""
33 def __init__(self) -> None:
34 """Initialize an unused and unresolved handle."""
35 self.used = False
36 self._tensor = None
38 def mark_used(self) -> None:
39 """Record that an exclude operation saved this input for backward."""
40 self.used = True
42 def materialize(self, tensor: Any) -> None:
43 """Bind the handle to the matching input produced during replay."""
44 self._tensor = tensor
46 def get_recomputed_tensor(self) -> Any:
47 """Return the replay-produced input for backward."""
48 if self._tensor is None:
49 raise RuntimeError("Checkpoint-excluded input was requested before recomputation")
50 return self._tensor
53@dataclass(frozen=True)
54class _InputBinding:
55 """Map one input path to its deferred saved-tensor handle."""
57 path: _InputPath
58 handle: _RecomputedInputHandle
61@dataclass
62class _ExcludeCacheEntry:
63 """Store one excluded call's replay value and deferred input bindings."""
65 output: Any
66 input_bindings: List[_InputBinding]
67 output_tensor_count: int = 1
70class _ExcludeCache:
71 """Store excluded-region call entries for one checkpoint invocation."""
73 def __init__(self) -> None:
74 """Initialize an empty per-checkpoint output cache."""
75 self._entries: Dict[int, Deque[_ExcludeCacheEntry]] = defaultdict(deque)
77 def save(self, wrapper_id: int, entry: _ExcludeCacheEntry) -> None:
78 """Save one call entry produced by a checkpoint-excluded region."""
79 self._entries[wrapper_id].append(entry)
81 def pop(self, wrapper_id: int) -> _ExcludeCacheEntry:
82 """Return the matching forward call entry during recomputation."""
83 entries = self._entries.get(wrapper_id)
84 if not entries:
85 raise RuntimeError("No cached forward output is available for this checkpoint exclusion wrapper")
86 entry = entries.popleft()
87 if not entries:
88 self._entries.pop(wrapper_id)
89 return entry
91 def clear(self) -> None:
92 """Release outputs not consumed because recomputation stopped early."""
93 self._entries.clear()
96def _pack_saved_tensor(tensor: Any) -> Any:
97 """Return a deferred input handle or detached tensor data."""
98 handle = getattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, None)
99 if isinstance(handle, _RecomputedInputHandle):
100 handle.mark_used()
101 return handle
102 return tensor.detach() if tensor.requires_grad else tensor
105def _unpack_saved_tensor(value: Any) -> Any:
106 """Restore the saved tensor for backward."""
107 if isinstance(value, _RecomputedInputHandle):
108 return value.get_recomputed_tensor()
109 return value
112def _saved_tensors_context() -> Any:
113 """Create an inner hook that stores real tensors instead of outer holders."""
114 return torch.autograd.graph.saved_tensors_hooks(_pack_saved_tensor, _unpack_saved_tensor)
117_EXCLUDE_CACHE_KEY = object()
120def _append_tensor_inputs(
121 value: Any,
122 path: _InputPath,
123 leaves: List[_TensorInput],
124 tensor_type: type,
125) -> None:
126 """Append tensor leaves without creating a self-referential local function."""
127 if isinstance(value, tensor_type):
128 leaves.append((path, value))
129 return
130 if isinstance(value, (tuple, list)):
131 for index, item in enumerate(value):
132 _append_tensor_inputs(item, path + (("index", index),), leaves, tensor_type)
133 return
134 if isinstance(value, dict):
135 for key, item in value.items():
136 _append_tensor_inputs(item, path + (("key", key),), leaves, tensor_type)
139def _collect_tensor_inputs(args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> List[_TensorInput]:
140 """Return tensor leaves and self-describing paths from one excluded-region call."""
141 leaves = []
142 for index, arg in enumerate(args):
143 _append_tensor_inputs(arg, (("arg", index),), leaves, torch.Tensor)
144 for key, value in kwargs.items():
145 _append_tensor_inputs(value, (("kwarg", key),), leaves, torch.Tensor)
146 return leaves
149def _mark_recompute_inputs(
150 invocation_id: object,
151 args: Tuple[Any, ...],
152 kwargs: Dict[str, Any],
153) -> Tuple[List[_InputBinding], List[Tuple[Any, Any]]]:
154 """Attach deferred handles to inputs that replay reproduces."""
155 bindings = []
156 previous_handles = []
157 seen_tensor_ids = set()
158 try:
159 for path, tensor in _collect_tensor_inputs(args, kwargs):
160 if (
161 isinstance(tensor, torch.nn.Parameter)
162 or id(tensor) in seen_tensor_ids
163 or getattr(tensor, _SAVE_OUTPUT_SOURCE_ATTR, None) is invocation_id
164 ):
165 continue
166 handle = _RecomputedInputHandle()
167 previous = getattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, _MISSING)
168 previous_handles.append((tensor, previous))
169 setattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, handle)
170 bindings.append(_InputBinding(path, handle))
171 seen_tensor_ids.add(id(tensor))
172 except BaseException:
173 _restore_recompute_inputs(previous_handles)
174 raise
175 return bindings, previous_handles
178def _restore_recompute_inputs(previous_handles: List[Tuple[Any, Any]]) -> None:
179 """Restore Tensor attributes overwritten for one excluded call."""
180 for tensor, previous in previous_handles:
181 if previous is _MISSING:
182 if hasattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR):
183 delattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR)
184 else:
185 setattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, previous)
188def _resolve_input(args: Tuple[Any, ...], kwargs: Dict[str, Any], path: _InputPath) -> Any:
189 """Resolve one replay input from its forward argument path."""
190 root_kind, root_key = path[0]
191 value = args[root_key] if root_kind == "arg" else kwargs[root_key]
192 for _, token_value in path[1:]:
193 value = value[token_value]
194 return value
197def _materialize_recompute_inputs(
198 entry: _ExcludeCacheEntry,
199 args: Tuple[Any, ...],
200 kwargs: Dict[str, Any],
201) -> None:
202 """Bind used input handles to tensors produced during checkpoint replay."""
203 for binding in entry.input_bindings:
204 if not binding.handle.used:
205 continue
206 tensor = _resolve_input(args, kwargs, binding.path)
207 if not isinstance(tensor, torch.Tensor):
208 raise RuntimeError(
209 "Checkpoint replay did not reproduce a tensor input required by a checkpoint-excluded region"
210 )
211 binding.handle.materialize(tensor.detach())
214def _has_used_input(input_bindings: List[_InputBinding]) -> bool:
215 """Return whether the excluded call saved any marked input."""
216 return any(binding.handle.used for binding in input_bindings)
219@lru_cache(maxsize=1)
220def _get_replay_placeholder() -> Any:
221 """Create a zero-element placeholder returned by an elided SAVE replay."""
222 return torch.empty(0, device="cpu")
225def _make_replay_placeholder_output(tensor_count: int) -> Any:
226 """Create one placeholder leaf for each forward output tensor."""
227 if tensor_count == 0:
228 return ()
229 placeholder = _get_replay_placeholder()
230 if tensor_count == 1:
231 return placeholder
232 return (placeholder,) * tensor_count
235@lru_cache(maxsize=1)
236def _get_recompute_trigger() -> Any:
237 """Create the differentiable zero-element input used by recompute boundaries."""
238 return torch.empty(0, device="cpu", requires_grad=True)
241@lru_cache(maxsize=1)
242def _get_recompute_boundary() -> Any:
243 """Create the autograd Function used to trigger outer checkpoint replay."""
244 class _RecomputeBoundary(torch.autograd.Function):
245 """Trigger the outer checkpoint hook before excluded-region backward."""
247 @staticmethod
248 def forward(ctx: Any, tensor: Any, trigger: Any) -> Any:
249 """Save one zero-element outer-hook dependency and return the tensor unchanged."""
250 ctx.save_for_backward(trigger)
251 return tensor
253 @staticmethod
254 def backward(ctx: Any, grad_output: Any) -> Tuple[Any, None]:
255 """Trigger dependency unpack and pass the gradient through."""
256 _ = ctx.saved_tensors
257 return grad_output, None
259 return _RecomputeBoundary
262def _finalize_save_outputs_impl(
263 output: Any,
264 add_recompute_boundary: bool,
265 invocation_id: Optional[object],
266 tensor_leaf_count: Optional[List[int]],
267 tensor_type: type,
268) -> Any:
269 """Recursively finalize SAVE output leaves."""
270 if isinstance(output, tensor_type):
271 if tensor_leaf_count is not None:
272 tensor_leaf_count[0] += 1
273 if add_recompute_boundary:
274 output = _get_recompute_boundary().apply(output, _get_recompute_trigger())
275 if invocation_id is not None:
276 setattr(output, _SAVE_OUTPUT_SOURCE_ATTR, invocation_id)
277 return output
278 if isinstance(output, list):
279 return [
280 _finalize_save_outputs_impl(item, add_recompute_boundary, invocation_id, tensor_leaf_count, tensor_type)
281 for item in output
282 ]
283 if isinstance(output, tuple):
284 items = [
285 _finalize_save_outputs_impl(item, add_recompute_boundary, invocation_id, tensor_leaf_count, tensor_type)
286 for item in output
287 ]
288 if hasattr(output, "_fields"):
289 return type(output)(*items)
290 return tuple(items)
291 if isinstance(output, dict):
292 return type(output)(
293 (
294 key,
295 _finalize_save_outputs_impl(
296 value,
297 add_recompute_boundary,
298 invocation_id,
299 tensor_leaf_count,
300 tensor_type,
301 ),
302 )
303 for key, value in output.items()
304 )
305 return output
308def _finalize_save_outputs(
309 output: Any,
310 add_recompute_boundary: bool,
311 invocation_id: Optional[object],
312 tensor_leaf_count: Optional[List[int]] = None,
313) -> Any:
314 """Apply the required boundary and SAVE provenance to output tensor leaves."""
315 return _finalize_save_outputs_impl(
316 output,
317 add_recompute_boundary,
318 invocation_id,
319 tensor_leaf_count,
320 torch.Tensor,
321 )