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"""Eager non-reentrant activation checkpointing for the Torch backend.
16
17The saved-tensor hook algorithm is adapted from ``torch.utils.checkpoint`` in
18PyTorch release/2.9. Hyper owns the scheduling and session extensions here so
19the implementation can run consistently on PyTorch 2.6, 2.7, and 2.9 without
20patching the installed framework.
21"""
22import contextlib
23import contextvars
24import threading
25import uuid
26import warnings
27import weakref
28from collections import defaultdict
29from typing import Any, Callable, DefaultDict, Dict, Generator, Iterator, List, Optional, Tuple
30
31import torch
32from torch.utils._pytree import tree_map
33from torch.utils.checkpoint import DefaultDeviceType
34from torch.utils.checkpoint import checkpoint as torch_checkpoint
35from torch.utils.checkpoint import set_checkpoint_early_stop
36
37
38_DEFAULT_DETERMINISM_MODE = "default"
39_RECOMPUTE_COLLECTOR = contextvars.ContextVar("hyper_recompute_collector", default=None)
40_RECOMPUTE_SESSION = contextvars.ContextVar("hyper_recompute_session", default=None)
41_SESSION_FRAMES: DefaultDict[Any, weakref.WeakSet] = defaultdict(weakref.WeakSet)
42_SESSION_FRAMES_LOCK = threading.RLock()
43
44
45class CheckpointError(RuntimeError):
46 """Raised when checkpoint forward and recomputation are inconsistent."""
47
48
49class _Handle:
50 """Identity key for one recomputed saved tensor."""
51
52
53class _Holder:
54 """Saved-tensor placeholder containing handles keyed by recompute session."""
55
56 def __init__(self) -> None:
57 """Initialize an empty per-session handle mapping."""
58 self.handles: Dict[Any, Optional[_Handle]] = {}
59
60
61class _StopRecomputationError(Exception):
62 """Internal control-flow exception used by early-stop recomputation."""
63
64
65class _SessionActivation:
66 """Control-plane state shared by checkpoint frames in one session scope."""
67
68 def __init__(self, session_id: Any, retain_on_unpack: bool) -> None:
69 """Initialize one scoped session activation."""
70 self.session_id = session_id
71 self.retain_on_unpack = retain_on_unpack
72 self.frames: weakref.WeakSet = weakref.WeakSet()
73
74
75class _NoopSaveInputs(torch.autograd.Function):
76 """Save checkpoint inputs without adding a meaningful forward operation."""
77
78 @staticmethod
79 def forward(*args: Any) -> Any:
80 """Return a dummy output whose grad function retains checkpoint inputs."""
81 del args
82 return torch.empty((0,))
83
84 @staticmethod
85 def setup_context(ctx: Any, inputs: Tuple[Any, ...], output: Any) -> None:
86 """Save tensor inputs while retaining non-tensor input structure."""
87 del output
88 tensor_pairs = [(index, value) for index, value in enumerate(inputs) if isinstance(value, torch.Tensor)]
89 tensor_indices, tensors = zip(*tensor_pairs)
90 index_to_saved_index = {input_index: saved_index for saved_index, input_index in enumerate(tensor_indices)}
91 stored_args = [None if isinstance(value, torch.Tensor) else value for value in inputs]
92
93 def get_args(saved_tensors: Tuple[Any, ...]) -> List[Any]:
94 """Reconstruct the original checkpoint arguments."""
95 restored_args = [
96 saved_tensors[index_to_saved_index[index]] if index in tensor_indices else value
97 for index, value in enumerate(stored_args)
98 ]
99 return restored_args[1:]
100
101 ctx.get_args = get_args
102 ctx.save_for_backward(*tensors)
103
104 @staticmethod
105 def backward(ctx: Any, *grad_outputs: Any) -> None:
106 """Reject direct backward through the internal input saver."""
107 del ctx, grad_outputs
108 raise CheckpointError("The internal checkpoint input saver must not be backwarded directly.")
109
110
111class _CheckpointFrame:
112 """State shared by one checkpoint forward and its recomputations."""
113
114 def __init__(self, recompute_fn: Callable, early_stop: bool, metadata_fn: Optional[Callable]) -> None:
115 """Initialize frame state captured by the saved-tensor hooks."""
116 self.recompute_fn = recompute_fn
117 self.input_saver = None
118 self.weak_holders: List[weakref.ReferenceType] = []
119 self.recomputed: DefaultDict[Any, weakref.WeakKeyDictionary] = defaultdict(weakref.WeakKeyDictionary)
120 self.recomp_counter: DefaultDict[Any, int] = defaultdict(int)
121 self.is_recomputed: DefaultDict[Any, bool] = defaultdict(bool)
122 self.early_stop = early_stop
123 self.metadata_fn = metadata_fn
124 self.x_metadatas: List[Any] = []
125 self.forward_completed = False
126 self.ignore_saved_mismatch = False
127 self.active_session: Optional[_SessionActivation] = None
128
129 def check_recomputed_tensors_match(self, session_id: Any) -> None:
130 """Validate saved-tensor count and metadata after recomputation."""
131 if self.ignore_saved_mismatch:
132 return
133 if len(self.weak_holders) != self.recomp_counter[session_id]:
134 raise CheckpointError(
135 "Hyper checkpoint saved a different number of tensors during forward and recomputation. "
136 f"Forward saved {len(self.weak_holders)} tensors, but recomputation saved "
137 f"{self.recomp_counter[session_id]} tensors."
138 )
139
140 mismatches = []
141 for index, weak_holder in enumerate(self.weak_holders):
142 holder = weak_holder()
143 if holder is None:
144 continue
145 handle = holder.handles.get(session_id)
146 _internal_assert(handle is not None, "Missing recomputed tensor handle during metadata validation.")
147 _internal_assert(
148 handle in self.recomputed[session_id],
149 "Missing recomputed tensor during metadata validation.",
150 )
151 recomputed_tensor = self.recomputed[session_id][handle]
152 recomputed_metadata = self.metadata_fn(recomputed_tensor)
153 if self.x_metadatas[index] != recomputed_metadata:
154 mismatches.append((index, self.x_metadatas[index], recomputed_metadata))
155
156 if mismatches:
157 details = "\n".join(
158 f"tensor {index}: forward={forward_metadata}, recompute={recomputed_metadata}"
159 for index, forward_metadata, recomputed_metadata in mismatches
160 )
161 raise CheckpointError(
162 "Hyper checkpoint detected different tensor metadata during recomputation:\n" + details
163 )
164
165 def clear_session(self, session_id: Any) -> None:
166 """Release all tensors and handles associated with one session."""
167 for weak_holder in self.weak_holders:
168 holder = weak_holder()
169 if holder is not None:
170 holder.handles.pop(session_id, None)
171 self.recomputed.pop(session_id, None)
172 self.recomp_counter.pop(session_id, None)
173 self.is_recomputed.pop(session_id, None)
174
175
176def _bind_session_activation(frame: _CheckpointFrame, activation: _SessionActivation) -> None:
177 """Bind one activation to a frame outside the unpack hot path."""
178 if frame.active_session is activation:
179 return
180 if frame.active_session is not None:
181 raise CheckpointError("Concurrent recompute sessions on the same checkpoint frame are not supported.")
182 frame.active_session = activation
183 activation.frames.add(frame)
184
185
186def _register_session_frame(
187 frame: _CheckpointFrame,
188 session_id: Any,
189 activation: Optional[_SessionActivation] = None,
190) -> None:
191 """Register a frame for cleanup and bind its current activation when present."""
192 with _SESSION_FRAMES_LOCK:
193 _SESSION_FRAMES[session_id].add(frame)
194 if activation is not None:
195 _internal_assert(activation.session_id == session_id, "Session activation key does not match its frame.")
196 _bind_session_activation(frame, activation)
197
198
199def _activate_registered_frames(activation: _SessionActivation) -> None:
200 """Install an activation on every frame already registered for its session."""
201 with _SESSION_FRAMES_LOCK:
202 for frame in list(_SESSION_FRAMES.get(activation.session_id, ())):
203 _bind_session_activation(frame, activation)
204
205
206def _deactivate_session(activation: _SessionActivation) -> None:
207 """Remove one activation from every frame bound at context entry."""
208 with _SESSION_FRAMES_LOCK:
209 for frame in list(activation.frames):
210 if frame.active_session is activation:
211 frame.active_session = None
212 activation.frames.clear()
213
214
215def _internal_assert(condition: bool, message: str) -> None:
216 if not condition:
217 raise CheckpointError(message)
218
219
220def _noop_context_fn() -> Tuple[contextlib.AbstractContextManager, contextlib.AbstractContextManager]:
221 return contextlib.nullcontext(), contextlib.nullcontext()
222
223
224def _default_metadata_fn(tensor: Any) -> Dict[str, Any]:
225 return {"shape": tensor.shape, "dtype": tensor.dtype, "device": tensor.device}
226
227
228def _infer_device_type(*args: Any) -> str:
229 """Return the preferred non-CPU device type found in checkpoint inputs."""
230 device_types = []
231
232 def add_device_type(value: Any) -> None:
233 """Record one non-CPU tensor device type."""
234 if isinstance(value, torch.Tensor) and value.device.type != "cpu":
235 device_types.append(value.device.type)
236
237 tree_map(add_device_type, args)
238 device_types_set = set(device_types)
239 if len(device_types_set) > 1:
240 warnings.warn(
241 "Hyper checkpoint received tensors on multiple non-CPU device types. RNG state is preserved only for "
242 "one device type; CUDA is preferred when present.",
243 stacklevel=3,
244 )
245 if not device_types:
246 return DefaultDeviceType.get_device_type()
247 if "cuda" in device_types_set:
248 return "cuda"
249 return device_types[0]
250
251
252def _get_device_module(device_type: str) -> Any:
253 if device_type == "meta":
254 return torch.device("meta")
255 return getattr(torch, device_type)
256
257
258def _get_device_states(device_type: str, *args: Any) -> Tuple[List[int], List[Any]]:
259 """Capture RNG states for non-CPU input devices of the requested type."""
260 device_ids = []
261
262 def add_device_id(value: Any) -> None:
263 """Record one non-CPU tensor device index."""
264 if isinstance(value, torch.Tensor) and value.device.type not in {"cpu", "meta"}:
265 device_ids.append(value.get_device())
266
267 tree_map(add_device_id, args)
268 device_module = _get_device_module(device_type)
269 states = []
270 for device_id in device_ids:
271 with device_module.device(device_id):
272 states.append(device_module.get_rng_state())
273 return device_ids, states
274
275
276def _set_device_states(device_type: str, devices: List[int], states: List[Any]) -> None:
277 if device_type == "meta":
278 return
279 device_module = _get_device_module(device_type)
280 for device, state in zip(devices, states):
281 with device_module.device(device):
282 device_module.set_rng_state(state)
283
284
285def _get_autocast_kwargs(device_type: str) -> Tuple[Optional[Dict[str, Any]], Dict[str, Any]]:
286 """Return active autocast settings for the selected device and CPU."""
287 device_kwargs = None
288 if torch.amp.is_autocast_available(device_type):
289 device_kwargs = {
290 "enabled": torch.is_autocast_enabled(device_type),
291 "dtype": torch.get_autocast_dtype(device_type),
292 "cache_enabled": torch.is_autocast_cache_enabled(),
293 }
294 cpu_kwargs = {
295 "enabled": torch.is_autocast_enabled("cpu"),
296 "dtype": torch.get_autocast_dtype("cpu"),
297 "cache_enabled": torch.is_autocast_cache_enabled(),
298 }
299 return device_kwargs, cpu_kwargs
300
301
302def _create_recomputation_hooks(frame: _CheckpointFrame, session_id: Any) -> Any:
303 """Create saved-tensor hooks that retain tensors from one recomputation."""
304 frame_ref = weakref.ref(frame)
305
306 def pack_hook(tensor: Any) -> Any:
307 """Store recomputed tensors in their forward holders."""
308 tensor = tensor.detach() if tensor.requires_grad else tensor
309 target_frame = frame_ref()
310 _internal_assert(target_frame is not None, "Checkpoint frame was released during recomputation.")
311 recompute_index = target_frame.recomp_counter[session_id]
312 target_frame.recomp_counter[session_id] += 1
313
314 if recompute_index >= len(target_frame.weak_holders):
315 if not target_frame.early_stop and not target_frame.forward_completed:
316 target_frame.ignore_saved_mismatch = True
317 return tensor
318 raise CheckpointError(
319 "Hyper checkpoint tried to save more tensors during recomputation than during forward."
320 )
321
322 holder = target_frame.weak_holders[recompute_index]()
323 if holder is not None:
324 _internal_assert(
325 holder.handles.get(session_id) is None,
326 "A recomputed tensor handle already exists for this session.",
327 )
328 handle = _Handle()
329 holder.handles[session_id] = handle
330 target_frame.recomputed[session_id][handle] = tensor
331
332 if target_frame.early_stop and target_frame.recomp_counter[session_id] == len(target_frame.weak_holders):
333 raise _StopRecomputationError
334 return tensor
335
336 def unpack_hook(tensor: Any) -> Any:
337 """Return tensors saved by operations inside the recomputation."""
338 return tensor
339
340 return torch.autograd.graph.saved_tensors_hooks(pack_hook, unpack_hook)
341
342
343# PyTorch exposes this tracing guard only as a private decorator.
344@torch._disable_dynamo # pylint: disable=protected-access
345def _run_fn_with_dynamo_disabled(function: Callable, *args: Any, **kwargs: Any) -> Any:
346 """Run recomputation without tracing the saved-tensor unpack hook with Dynamo."""
347 return function(*args, **kwargs)
348
349
350def _run_recomputation(frame: _CheckpointFrame, session_id: Any) -> None:
351 """Run and validate a frame recomputation for the given session."""
352 if frame.is_recomputed[session_id]:
353 return
354
355 activation = frame.active_session
356 if activation is not None:
357 _internal_assert(activation.session_id == session_id, "Active session key does not match recomputation key.")
358 previous_activation = _RECOMPUTE_SESSION.get()
359 token = None
360 if activation is not None and previous_activation is not activation:
361 token = _RECOMPUTE_SESSION.set(activation)
362 try:
363 input_context = frame.input_saver.grad_fn
364 args = input_context.get_args(input_context.saved_tensors)
365 try:
366 with _create_recomputation_hooks(frame, session_id), torch.autograd.enable_grad():
367 _run_fn_with_dynamo_disabled(frame.recompute_fn, *args)
368 except _StopRecomputationError:
369 pass
370 finally:
371 if token is not None:
372 _RECOMPUTE_SESSION.reset(token)
373 frame.is_recomputed[session_id] = True
374 frame.check_recomputed_tensors_match(session_id)
375
376
377def _create_checkpoint_hooks(frame: _CheckpointFrame) -> Any:
378 """Create hooks that lazily recompute tensors saved during forward."""
379 def pack_hook(tensor: Any) -> _Holder:
380 """Replace a forward saved tensor with an opaque holder."""
381 holder = _Holder()
382 frame.weak_holders.append(weakref.ref(holder))
383 if frame.metadata_fn is not None:
384 with torch.no_grad():
385 frame.x_metadatas.append(frame.metadata_fn(tensor))
386 return holder
387
388 def unpack_hook(holder: _Holder) -> Any:
389 """Return the corresponding tensor from lazy or prefired recomputation."""
390 activation = frame.active_session
391 if activation is not None:
392 session_id = activation.session_id
393 retain_on_unpack = activation.retain_on_unpack
394 else:
395 session_id = torch._C._current_graph_task_id() # pylint: disable=W0212
396 if session_id == -1:
397 session_id = int(uuid.uuid4())
398 retain_on_unpack = False
399
400 _run_recomputation(frame, session_id)
401 _internal_assert(session_id in holder.handles, "No recomputed tensor was saved for this checkpoint value.")
402 handle = holder.handles[session_id]
403 if handle is None:
404 raise CheckpointError("A checkpoint tensor was unpacked more than once in the same recompute session.")
405 _internal_assert(handle in frame.recomputed[session_id], "The recomputed tensor has already been released.")
406 tensor = frame.recomputed[session_id][handle]
407 if not retain_on_unpack:
408 holder.handles[session_id] = None
409 return tensor
410
411 return torch.autograd.graph.saved_tensors_hooks(pack_hook, unpack_hook)
412
413
414def _is_compiling() -> bool:
415 compiler = getattr(torch, "compiler", None)
416 return bool(compiler is not None and compiler.is_compiling())
417
418
419def _native_checkpoint(
420 function: Callable,
421 *args: Any,
422 context_fn: Callable,
423 preserve_rng_state: bool,
424 determinism_check: str,
425 debug: bool,
426 early_stop: bool,
427 **kwargs: Any,
428) -> Any:
429 """Use the public native API for compile, adapting 2.6/2.7 early-stop."""
430 with set_checkpoint_early_stop(early_stop):
431 return torch_checkpoint(
432 function,
433 *args,
434 use_reentrant=False,
435 context_fn=context_fn,
436 preserve_rng_state=preserve_rng_state,
437 determinism_check=determinism_check,
438 debug=debug,
439 **kwargs,
440 )
441
442
443def _checkpoint_without_reentrant_generator(
444 function: Callable,
445 preserve_rng_state: bool,
446 context_fn: Callable,
447 determinism_check: str,
448 early_stop: bool,
449 *args: Any,
450 **kwargs: Any,
451) -> Generator[None, None, None]:
452 """Set up eager checkpoint state around the caller's forward execution."""
453 metadata_functions = {_DEFAULT_DETERMINISM_MODE: _default_metadata_fn, "none": lambda tensor: None}
454 if determinism_check not in metadata_functions:
455 raise ValueError(
456 f"determinism_check must be one of {list(metadata_functions)}, but got {determinism_check!r}."
457 )
458 metadata_fn = metadata_functions[determinism_check]
459
460 device_type = _infer_device_type(*args)
461 device_module = _get_device_module(device_type)
462 contexts = context_fn()
463 if not isinstance(contexts, tuple) or len(contexts) != 2:
464 raise ValueError("context_fn must return a (forward_context, recompute_context) tuple.")
465 forward_context, recompute_context = contexts
466 device_autocast_kwargs, cpu_autocast_kwargs = _get_autocast_kwargs(device_type)
467
468 had_device_in_forward = False
469 forward_devices: List[int] = []
470 forward_device_states: List[Any] = []
471 forward_cpu_state = None
472 if preserve_rng_state:
473 forward_cpu_state = torch.get_rng_state()
474 if getattr(device_module, "_initialized", False):
475 had_device_in_forward = True
476 forward_devices, forward_device_states = _get_device_states(device_type, *args)
477
478 def recompute_fn(*inputs: Any) -> None:
479 """Restore execution state and rerun the checkpointed function."""
480 function_kwargs, *function_args = inputs
481 rng_devices = forward_devices if preserve_rng_state and had_device_in_forward else []
482 with torch.random.fork_rng(
483 devices=rng_devices,
484 enabled=preserve_rng_state,
485 device_type=device_type,
486 ):
487 if preserve_rng_state:
488 torch.set_rng_state(forward_cpu_state)
489 if had_device_in_forward:
490 _set_device_states(device_type, forward_devices, forward_device_states)
491
492 device_autocast_context = contextlib.nullcontext()
493 if device_autocast_kwargs is not None:
494 device_autocast_context = torch.amp.autocast(device_type=device_type, **device_autocast_kwargs)
495 with device_autocast_context, torch.amp.autocast("cpu", **cpu_autocast_kwargs), recompute_context:
496 function(*function_args, **function_kwargs)
497
498 frame = _CheckpointFrame(recompute_fn, early_stop, metadata_fn)
499 dummy = torch.empty((0,), requires_grad=True)
500 frame.input_saver = _NoopSaveInputs.apply(dummy, kwargs, *args)
501
502 if frame.input_saver.grad_fn is None:
503 yield
504 return
505
506 activation = _RECOMPUTE_SESSION.get()
507 if activation is not None:
508 raise CheckpointError("Nested checkpoint is not supported during scheduled recomputation.")
509
510 collector = _RECOMPUTE_COLLECTOR.get()
511 if collector is not None:
512 collector.append(frame)
513 try:
514 with _create_checkpoint_hooks(frame), forward_context:
515 yield
516 frame.forward_completed = True
517
518 if getattr(device_module, "_initialized", False) and preserve_rng_state and not had_device_in_forward:
519 raise RuntimeError(
520 "The device state was initialized inside a Hyper checkpoint forward, so its initial RNG state "
521 "could not be preserved. Initialize the device before entering checkpoint."
522 )
523 except BaseException:
524 if collector is not None and frame in collector:
525 collector.remove(frame)
526 raise
527
528
529def checkpoint(
530 function: Callable,
531 *args: Any,
532 use_reentrant: bool = False,
533 context_fn: Callable = _noop_context_fn,
534 preserve_rng_state: bool = True,
535 determinism_check: str = _DEFAULT_DETERMINISM_MODE,
536 debug: bool = False,
537 early_stop: bool = True,
538 **kwargs: Any,
539) -> Any:
540 """Run Hyper's non-reentrant checkpoint implementation.
541
542 Eager execution always uses this implementation. Compile execution falls
543 back to PyTorch's public non-reentrant checkpoint API.
544 """
545 if use_reentrant is not False:
546 raise ValueError("Hyper checkpoint only supports use_reentrant=False.")
547 if not isinstance(early_stop, bool):
548 raise ValueError(f"early_stop must be bool, but got {type(early_stop).__name__}.")
549 if not isinstance(preserve_rng_state, bool):
550 raise ValueError(
551 f"preserve_rng_state must be bool, but got {type(preserve_rng_state).__name__}."
552 )
553 if not callable(context_fn):
554 raise ValueError("context_fn must be callable.")
555 if _is_compiling():
556 return _native_checkpoint(
557 function,
558 *args,
559 context_fn=context_fn,
560 preserve_rng_state=preserve_rng_state,
561 determinism_check=determinism_check,
562 debug=debug,
563 early_stop=early_stop,
564 **kwargs,
565 )
566 if debug:
567 raise ValueError("debug=True is not supported by Hyper eager checkpoint yet.")
568
569 generator = _checkpoint_without_reentrant_generator(
570 function,
571 preserve_rng_state,
572 context_fn,
573 determinism_check,
574 early_stop,
575 *args,
576 **kwargs,
577 )
578 next(generator)
579 try:
580 result = function(*args, **kwargs)
581 except BaseException:
582 generator.close()
583 raise
584 try:
585 next(generator)
586 except StopIteration:
587 return result
588 generator.close()
589 raise CheckpointError("The internal checkpoint generator yielded more than once.")
590
591
592@contextlib.contextmanager
593def recompute_handle_collector_ctx() -> Iterator[List[Any]]:
594 """Collect opaque checkpoint handles created in this context."""
595 handles = []
596 token = _RECOMPUTE_COLLECTOR.set(handles)
597 try:
598 yield handles
599 finally:
600 _RECOMPUTE_COLLECTOR.reset(token)
601
602
603def recompute_handle(handle: Any, session_id: Any) -> None:
604 """Run one collected checkpoint recomputation ahead of backward."""
605 if not isinstance(handle, _CheckpointFrame):
606 raise ValueError("handle must be produced by recompute_handle_collector_ctx().")
607 _validate_session_id(session_id)
608 activation = _RECOMPUTE_SESSION.get()
609 if (
610 activation is not None
611 and activation.session_id == session_id
612 and activation.retain_on_unpack
613 ):
614 _register_session_frame(handle, session_id, activation)
615 _run_recomputation(handle, session_id)
616 return
617 if activation is not None:
618 raise CheckpointError("recompute_handle cannot enter another active recompute session.")
619
620 _register_session_frame(handle, session_id)
621 with recompute_session_ctx(session_id=session_id, retain_on_unpack=True):
622 _run_recomputation(handle, session_id)
623
624
625def _validate_session_id(session_id: Any) -> None:
626 if session_id is None:
627 raise ValueError("session_id must not be None.")
628 try:
629 hash(session_id)
630 except TypeError as error:
631 raise ValueError("session_id must be hashable.") from error
632
633
634@contextlib.contextmanager
635def recompute_session_ctx(session_id: Any, retain_on_unpack: bool = False) -> Iterator[Any]:
636 """Select the key and retention policy used by checkpoint unpack hooks."""
637 _validate_session_id(session_id)
638 if not isinstance(retain_on_unpack, bool):
639 raise ValueError(f"retain_on_unpack must be bool, but got {type(retain_on_unpack).__name__}.")
640 if _RECOMPUTE_SESSION.get() is not None:
641 raise CheckpointError("Nested recompute session contexts are not supported.")
642 activation = _SessionActivation(session_id, retain_on_unpack)
643 token = _RECOMPUTE_SESSION.set(activation)
644 try:
645 _activate_registered_frames(activation)
646 yield session_id
647 finally:
648 try:
649 _deactivate_session(activation)
650 finally:
651 _RECOMPUTE_SESSION.reset(token)
652
653
654def clear_recompute_session(session_id: Any) -> None:
655 """Release retained recomputation data for a session; repeated calls are safe."""
656 _validate_session_id(session_id)
657 with _SESSION_FRAMES_LOCK:
658 registered_frames = _SESSION_FRAMES.pop(session_id, None)
659 frames = list(registered_frames) if registered_frames is not None else []
660 for frame in frames:
661 frame.clear_session(session_id)