# Source code for tensorplay.utils.checkpoint Source: https://www.tensorplay.cn/docs/_modules/tensorplay/utils/checkpoint.html ``` """Activation checkpointing helpers.""" from __future__ import annotations import functools import warnings from collections import defaultdict from contextlib import contextmanager, nullcontext from enum import Enum from typing import Any, Callable, Iterable from tensorplay._C import _autograd as _native_autograd from tensorplay.overrides import TensorPlayFunctionMode __all__ = [ "checkpoint", "checkpoint_sequential", "CheckpointError", "CheckpointFunction", "check_backward_validity", "detach_variable", "get_device_states", "set_device_states", "noop_context_fn", "set_checkpoint_early_stop", "DefaultDeviceType", "set_checkpoint_debug_enabled", "CheckpointPolicy", "SelectiveCheckpointContext", "create_selective_checkpoint_contexts", "GraphExecGroup", ] CheckpointError = RuntimeError _checkpoint_early_stop = True _checkpoint_debug_enabled: bool | None = None class CheckpointFunction: @staticmethod def apply(function: Callable[..., Any], preserve_rng_state: bool, *args: Any) -> Any: return checkpoint( function, *args, use_reentrant=True, preserve_rng_state=bool(preserve_rng_state), ) def _tensor_type() -> type: import tensorplay return tensorplay.Tensor def _walk_tensors(value: Any) -> Iterable[Any]: tensor_type = _tensor_type() if isinstance(value, tensor_type): yield value elif isinstance(value, (tuple, list)): for item in value: yield from _walk_tensors(item) elif isinstance(value, dict): for item in value.values(): yield from _walk_tensors(item) class DefaultDeviceType: _default_device_type: str | None = "cuda" @staticmethod def set_device_type(device: str = "cuda") -> None: if not isinstance(device, str) or not device: raise TypeError("device type must be a non-empty string") DefaultDeviceType._default_device_type = device @staticmethod def get_device_type() -> str: if not DefaultDeviceType._default_device_type: DefaultDeviceType._default_device_type = "cuda" return DefaultDeviceType._default_device_type def _infer_device_type(*args: Any) -> str: types = {str(t.device.type) for t in _walk_tensors(args) if str(t.device.type) not in {"cpu", "meta"}} if not types: return DefaultDeviceType.get_device_type() if "cuda" in types: return "cuda" return next(iter(types)) def get_device_states(*args: Any) -> tuple[list[int], list[Any]]: devices: list[int] = [] states: list[Any] = [] for tensor in _walk_tensors(args): device = tensor.device device_type = str(device.type) if device_type in {"cpu", "meta"}: continue index = device.index if index is None or index < 0: continue devices.append(int(index)) if not devices: return devices, states if _infer_device_type(*args) == "cuda": from tensorplay import cuda for index in devices: with cuda.device(index): states.append(cuda.get_rng_state(index)) return devices, states [docs] def set_device_states( devices: Iterable[int], states: Iterable[Any], *, device_type: str | None = None, ) -> None: selected_type = device_type or DefaultDeviceType.get_device_type() if selected_type == "meta": return if selected_type != "cuda": return from tensorplay import cuda for device, state in zip(devices, states): with cuda.device(int(device)): cuda.set_rng_state(state, int(device)) def detach_variable(inputs: tuple[Any, ...]) -> tuple[Any, ...]: if not isinstance(inputs, tuple): raise RuntimeError("detach_variable expects a tuple of values") result = [] for value in inputs: if isinstance(value, _tensor_type()): detached = value.detach() if value.requires_grad: detached.requires_grad_(True) result.append(detached) else: result.append(value) return tuple(result) def check_backward_validity(inputs: Iterable[Any]) -> None: if not any( isinstance(value, _tensor_type()) and value.requires_grad for value in inputs ): warnings.warn( "None of the inputs have requires_grad=True. Gradients will be None", stacklevel=2, ) def noop_context_fn(): return nullcontext(), nullcontext() [docs] @contextmanager def set_checkpoint_early_stop(enabled: bool): global _checkpoint_early_stop previous = _checkpoint_early_stop _checkpoint_early_stop = bool(enabled) try: yield finally: _checkpoint_early_stop = previous [docs] @contextmanager def set_checkpoint_debug_enabled(enabled: bool | None): global _checkpoint_debug_enabled previous = _checkpoint_debug_enabled _checkpoint_debug_enabled = enabled try: yield finally: _checkpoint_debug_enabled = previous class _CheckpointedFunction: def __init__(self, function: Callable[..., Any], **options: Any) -> None: self.function = function self.options = dict(options) functools.update_wrapper(self, function, updated=()) def __call__(self, *args: Any, **kwargs: Any) -> Any: return checkpoint(self.function, *args, **self.options, **kwargs) def __get__(self, instance: Any, owner: type | None = None) -> Any: if instance is None: return self getter = getattr(self.function, "__get__", None) if getter is None: return self return type(self)(getter(instance, owner), **self.options) [docs] def checkpoint( function: Callable[..., Any] | None = None, *args: Any, use_reentrant: bool | None = None, preserve_rng_state: bool = True, context_fn: Callable[[], Any] | None = None, determinism_check: str = "default", debug: bool = False, early_stop: bool | None = None, respect_saved_tensors_hooks: bool | None = None, **kwargs: Any, ) -> Any: if function is None: if args or kwargs: raise ValueError("a callable is required before runtime arguments") return lambda wrapped: _CheckpointedFunction( wrapped, use_reentrant=use_reentrant, preserve_rng_state=preserve_rng_state, context_fn=context_fn, determinism_check=determinism_check, debug=debug, early_stop=early_stop, respect_saved_tensors_hooks=respect_saved_tensors_hooks, ) if use_reentrant is None: warnings.warn( "use_reentrant should be passed explicitly; the non-reentrant " "implementation is recommended for new code", stacklevel=2, ) use_reentrant = True if respect_saved_tensors_hooks is not None and use_reentrant: raise ValueError( "respect_saved_tensors_hooks is only supported for non-reentrant checkpointing") if early_stop is None: early_stop = _checkpoint_early_stop if _checkpoint_debug_enabled is not None: debug = _checkpoint_debug_enabled native_entry = getattr(_native_autograd, "_activation_checkpoint", None) if native_entry is None: raise RuntimeError("native activation checkpoint entry is unavailable") return native_entry( function, tuple(args), dict(kwargs), bool(use_reentrant), bool(preserve_rng_state), context_fn, determinism_check, bool(debug), bool(early_stop), ) [docs] def checkpoint_sequential( function: Callable[..., Any], chunks: int, *args: Any, **kwargs: Any, ) -> Any: if not isinstance(chunks, int) or chunks <= 0: raise ValueError("chunks must be a positive integer") if hasattr(function, "children"): modules = tuple(function.children()) elif isinstance(function, (tuple, list)): modules = tuple(function) else: return checkpoint(function, *args, **kwargs) if len(args) != 1: raise ValueError("checkpoint_sequential expects one input value") if not modules: return args[0] segment_count = min(chunks, len(modules)) quotient, remainder = divmod(len(modules), segment_count) segments: list[tuple[Any, ...]] = [] start = 0 for index in range(segment_count): width = quotient + (1 if index < remainder else 0) segments.append(modules[start:start + width]) start += width value = args[0] for index, segment in enumerate(segments): def run_segment(value: Any, segment: tuple[Any, ...] = segment) -> Any: for module in segment: value = module(value) return value def parameters(segment: tuple[Any, ...] = segment): for module in segment: if hasattr(module, "parameters"): yield from module.parameters() run_segment.parameters = parameters if index + 1 != len(segments): value = checkpoint(run_segment, value, **kwargs) else: value = run_segment(value) return value [docs] class CheckpointPolicy(Enum): MUST_SAVE = 0 PREFER_SAVE = 1 MUST_RECOMPUTE = 2 PREFER_RECOMPUTE = 3 MUST_CPU_OFFLOAD = 4 PREFER_CPU_OFFLOAD = 5 [docs] class SelectiveCheckpointContext: def __init__(self, *, is_recompute: bool, op_output: Any = None) -> None: self.is_recompute = is_recompute self.op_output = op_output class _CachedTensor: def __init__(self, value: Any, device: Any = None, requires_grad: bool = False, version: int | None = None) -> None: self.value = value self.device = device self.requires_grad = requires_grad self.version = (getattr(value, "_version", None) if version is None else version) def unpack(self, allow_mutation: bool) -> Any: if not allow_mutation and self.version is not None: current = getattr(self.value, "_version", self.version) if current != self.version: raise RuntimeError( "tensor cached by selective checkpointing was modified") result = (self.value.to(self.device, copy=True) if self.device is not None else self.value) if self.requires_grad: result.requires_grad_(True) return result def _cache_tree(value: Any, *, offload: bool) -> Any: tensor_type = _tensor_type() if isinstance(value, tensor_type): requires_grad = bool(value.requires_grad) version = getattr(value, "_version", None) if offload and str(value.device.type) != "cpu": import tensorplay host = value.detach().to( tensorplay.Device(tensorplay.DeviceType.CPU), copy=True) return _CachedTensor( host, value.device, requires_grad=requires_grad, version=version) return _CachedTensor( value.detach(), requires_grad=requires_grad, version=version) if isinstance(value, tuple): return tuple(_cache_tree(item, offload=offload) for item in value) if isinstance(value, list): return [_cache_tree(item, offload=offload) for item in value] if isinstance(value, dict): return {key: _cache_tree(item, offload=offload) for key, item in value.items()} return value def _uncache_tree(value: Any, *, allow_mutation: bool) -> Any: if isinstance(value, _CachedTensor): return value.unpack(allow_mutation) if isinstance(value, tuple): return tuple(_uncache_tree(item, allow_mutation=allow_mutation) for item in value) if isinstance(value, list): return [_uncache_tree(item, allow_mutation=allow_mutation) for item in value] if isinstance(value, dict): return {key: _uncache_tree(item, allow_mutation=allow_mutation) for key, item in value.items()} return value def _operation_key(function: Any) -> Any: try: hash(function) return function except TypeError: return id(function) def _invoke_function(function: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any: if "self" in kwargs: import tensorplay name = getattr(function, "__name__", None) native = getattr(getattr(tensorplay, "_C", None), name, None) if callable(native): return native(*args, **kwargs) return function(*args, **kwargs) def _call_policy(policy_fn: Callable[..., Any], output: Any, function: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> CheckpointPolicy: from tensorplay.overrides import _disable_tensorplay_function with _disable_tensorplay_function(): policy = policy_fn( SelectiveCheckpointContext(is_recompute=False, op_output=output), function, *args, **kwargs, ) if isinstance(policy, bool): return CheckpointPolicy.MUST_SAVE if policy else CheckpointPolicy.PREFER_RECOMPUTE if not isinstance(policy, CheckpointPolicy): raise TypeError("selective checkpoint policy must return CheckpointPolicy") return policy class _SelectiveCachingMode(TensorPlayFunctionMode): def __init__(self, policy_fn: Callable[..., Any], storage: dict) -> None: self.policy_fn = policy_fn self.storage = storage self.func_counter: defaultdict[Any, int] = defaultdict(int) def __enter__(self): self.func_counter.clear() return super().__enter__() def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: return super().__exit__(exc_type, exc_val, exc_tb) def __tensorplay_function__(self, function, types_, args=(), kwargs=None): del types_ kwargs = {} if kwargs is None else kwargs operation = _native_autograd._checkpoint_operation_begin() try: output = _invoke_function(function, args, kwargs) finally: _native_autograd._checkpoint_operation_end(operation) key = _operation_key(function) index = self.func_counter[key] self.func_counter[key] += 1 policy = _call_policy(self.policy_fn, output, function, args, kwargs) should_cache = policy in { CheckpointPolicy.MUST_SAVE, CheckpointPolicy.PREFER_SAVE, CheckpointPolicy.MUST_CPU_OFFLOAD, CheckpointPolicy.PREFER_CPU_OFFLOAD, } _native_autograd._checkpoint_operation_cache(operation, should_cache) if should_cache: self.storage[key][index] = _cache_tree( output, offload=policy in { CheckpointPolicy.MUST_CPU_OFFLOAD, CheckpointPolicy.PREFER_CPU_OFFLOAD, }, ) else: self.storage[key][index] = _RECOMPUTE return output class _SelectiveReplayMode(TensorPlayFunctionMode): def __init__(self, storage: dict, allow_cache_entry_mutation: bool) -> None: self.storage = storage self.allow_cache_entry_mutation = allow_cache_entry_mutation self.func_counter: defaultdict[Any, int] = defaultdict(int) def __enter__(self): self.func_counter.clear() return super().__enter__() def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: return super().__exit__(exc_type, exc_val, exc_tb) def __tensorplay_function__(self, function, types_, args=(), kwargs=None): del types_ kwargs = {} if kwargs is None else kwargs key = _operation_key(function) index = self.func_counter[key] self.func_counter[key] += 1 entry = self.storage.get(key, {}).get(index) if entry is None: raise RuntimeError( "selective checkpoint replay encountered an unknown operation") if entry is _RECOMPUTE: return _invoke_function(function, args, kwargs) operation = _native_autograd._checkpoint_operation_begin() try: _native_autograd._checkpoint_operation_reuse(operation) return _uncache_tree( entry, allow_mutation=self.allow_cache_entry_mutation, ) finally: _native_autograd._checkpoint_operation_end(operation) _RECOMPUTE = object() [docs] def create_selective_checkpoint_contexts( policy_fn_or_list: Callable[..., Any] | list[Any], allow_cache_entry_mutation: bool = False, ): if isinstance(policy_fn_or_list, list): selected = set(policy_fn_or_list) def policy_fn(context, function, *args, **kwargs): del context, args, kwargs return (CheckpointPolicy.MUST_SAVE if function in selected else CheckpointPolicy.PREFER_RECOMPUTE) elif callable(policy_fn_or_list): policy_fn = policy_fn_or_list else: raise TypeError("policy_fn_or_list must be a function or a list") storage: defaultdict[Any, dict[int, Any]] = defaultdict(dict) return ( _SelectiveCachingMode(policy_fn, storage), _SelectiveReplayMode(storage, allow_cache_entry_mutation), ) [docs] class GraphExecGroup: def __enter__(self): return self def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: return None @classmethod def _get_current_group(cls): return None ```