TensorPlay
latest (dev)
Copy
View Markdown

Latest development documentation · Updated 2026-10-15

Source code for tensorplay.utils.checkpoint

"""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
Ask DeepWiki