Diff Coverage

Diff: origin/master...HEAD, staged and unstaged changes

Source File Diff Coverage (%) Missing Lines
hyper_parallel/core/activation_memory/__init__.py 100%  
hyper_parallel/core/activation_memory/_backend.py 43.9% 22-25,30,35,40,45,50,55,60,65,70,75,80-82,87-88,93-94,99-100
hyper_parallel/core/activation_memory/api.py 22.3% 51-52,55-62,65,80-84,86,91,97-98,114-115,121-122,139-143,160-164,170-171,177-178,185-186,197-198,204-205,211-212,218-219,224-225,227-228,234-235,278-279,281-294,300,306-313,315-316,318,320-324,326-330,332-333,364-365,369-373
hyper_parallel/core/activation_memory/checkpoint.py 16.8% 58,70-72,81-82,87-91,93,95,99,101-102,107-108,116-127,131-134,140-147,151-154,156-157,161,167-173,178-183,192-196,201-203,208-212,216-217,221,225,230,232,234-235,237-240,245-249,253-255,260,262,264-265,267-273,277-282,287-289,294,299,304,306,308-312,314-318,322-324,328-330,332-334,336,338,340,347,352-353,355-369,371-374,379,381-386,388,390-393,395-398,400-409,411,415-416,430-431,453-455,458,460-466,468-476,478,480-482,487-490,492-496,498-500,502-504,506-508,510-516,518-519,523-526,545-550,553-556,566-567,569,578-589,595-598,600,605-609,614-618,620-622,626-631,637-646,648-649,651,656-661
hyper_parallel/core/activation_memory/checkpoint_exclude.py 30.1% 35-36,40,44,48-50,75,79,83-89,93,98-102,107-109,114,127-136,141-146,155-160,165-175,180-183,185,190-194,203-208,211,216,222,227-232,238,244,247-248,250-251,253-254,256-257,259,270-279,283-284,288-292,305,315
hyper_parallel/core/activation_memory/compile_adapter.py 32.0% 33,35-36,39-42,46-49,53,63,68-70,72
hyper_parallel/core/activation_memory/pinned_memory_pool.py 20.0% 44-45,50-53,76-84,89-90,95,100,103-108,114-125,127-131,133-135,137,141-150,154-155,158-165,168-175,179-180,182-191,193-195,197-200,207-209,215-227,229-246,250-259,261-263
hyper_parallel/core/activation_memory/policy.py 100%  
hyper_parallel/core/activation_memory/recompute_state.py 40.8% 25-26,30-32,36-40,48-49,54,58,62,76-77,81-82,86-90,95,100-101,106-107
hyper_parallel/core/activation_memory/sac.py 22.4% 40-43,49-50,55-56,58,61,67-68,79,81-82,88-89,115,120,140,146-151,153-154,157-158,160-161,163-164,166,168,170,172,174,176-177,180-188,193-197,203-207,210-211,213-214,216-217,219,221-223,226,228-232,236,238-239,301-307,311,316-318,320-322,324,326-328
hyper_parallel/core/activation_memory/swap.py 13.4% 58-65,67-69,74-76,81,83-87,89-90,102-116,118-122,126-129,139,143-146,154-157,161,165-166,168-173,177-180,182-183,187,189-193,195-196,200-205,209-211,215-217,221-226,230-231,233-236,240-244,248-251,253-254,258,260-261,265-266,271-273,277-286,288-296,300-301,303-306,310-314,319,322-324,338,341,345,349,353,355-358,360-363,367-377,381-382,384-387,389-391,395-398,400-402,406-412,416-419,421-424,428-431,433-435,439-442,444-446,450,452-454,456-458,474-478,480-483,487,490,494-496,499,503-507,532-536,538-541,544-547,551-552,556-558,560,570-574,576-579,581-587,595-602,604-607,616-622,626-631,633,635-641,643-657,659-662,666-668,672-673,676-682,684,695-697,699-704,706-713,717-718,722-727,731-733,737-742,744-745,749-750,753-761,764,769-773,789-792,795-796,799-803,807-808,812-813,817-822,826-829,833-836,840-845,849-852,856-858,862-871,875,879,883,888-890,892,896-900,911-920,932-933,940,942-949,951-954,956-959,961-962,964-967,969,1004-1012,1014,1049-1052,1054-1057,1059-1060,1062-1063,1081-1084,1086-1087,1091-1093
hyper_parallel/core/activation_memory/wrapper.py 23.1% 42-43,68-69,73,78,92-98,102-105,109-114,118,125-127,131-134,138,143-144,148-156,159-161,166-172,181-185,193-198,201,203-219,222-223,225-229,231,243-253,256,259,263,268,272-275,279,304-309,325-326,343-344,360,374-377,381-382,387-388,408,427-431,434-436,438-439,441,443-444,446-448,451-452,454,458-460,471-472,476-478,480-481,489,494,502-507,511-519,524,530,535-537,539-542,548-550,554,580
hyper_parallel/core/pipeline_parallel/pipeline_swap.py 100%  
hyper_parallel/distributed/activation_checkpoint.py 50.0% 265,380
hyper_parallel/distributed/attention_swap.py 100%  
hyper_parallel/core/activation_memory/_backend.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104


def get_device_handle(device_type: str = "npu"):
    """Return the torch device module (e.g. ``torch.npu`` or ``torch.cuda``)."""
    try:
        return getattr(torch, device_type)
    except AttributeError as e:
        raise RuntimeError(f"expect got device handle: 'torch.{device_type}' failed.") from e


def new_stream():
    """Create a new device stream on the current accelerator."""
    return get_device_handle().Stream()


def get_stream_context():
    """Return the stream context manager (``torch.npu.stream`` / ``torch.cuda.stream``)."""
    return get_device_handle().stream


def get_current_stream():
    """Return the current device stream."""
    return get_device_handle().current_stream()


def new_event():
    """Create a new device event on the current accelerator."""
    return get_device_handle().Event()


def no_grad():
    """Return ``torch.no_grad()``."""
    return torch.no_grad()


def preserve_version_counter(tensor):
    """Temporarily keep the tensor version counter unchanged across an in-place write."""
    return torch.autograd._unsafe_preserve_version_counter(tensor)  # pylint: disable=W0212


def cat(tensors, dim=0):
    """Concatenate tensors along *dim*."""
    return torch.cat(tensors, dim=dim)


def empty_like(tensor, *, dtype=None, device=None, pin_memory=False):
    """Allocate an uninitialized tensor shaped like *tensor*."""
    return torch.empty_like(tensor, dtype=dtype, device=device, pin_memory=pin_memory)


def tree_map(fn, tree):
    """Apply *fn* to every leaf of *tree* and rebuild the same structure."""
    return torch.utils._pytree.tree_map(fn, tree)  # pylint: disable=W0212


def get_element_size(tensor) -> int:
    """Return the size in bytes of one element of *tensor*."""
    return tensor.element_size()


def alloc_tensor_buffer(numel: int, dtype, device="cpu", pin_memory: bool = False):
    """Allocate an uninitialized 1-D tensor buffer."""
    if pin_memory:
        return torch.empty(numel, dtype=dtype, device="cpu", pin_memory=True)
    return torch.empty(numel, dtype=dtype, device=device)


def register_forward_pre_hook(module, hook, prepend=False, with_kwargs=False):
    """Register a forward pre-hook on *module*, ignoring *prepend*."""
    del prepend
    return module.register_forward_pre_hook(hook, with_kwargs=with_kwargs)


def register_full_backward_hook(module, hook, prepend=False):
    """Register a full backward hook on *module*, ignoring *prepend*."""
    del prepend
    return module.register_full_backward_hook(hook)


def register_full_backward_pre_hook(module, hook, prepend=False):
    """Register a full backward pre-hook on *module*, ignoring *prepend*."""
    del prepend
    return module.register_full_backward_pre_hook(hook)


__all__ = [
    "Tensor",
hyper_parallel/core/activation_memory/api.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class _StackedCtx:
    """Compose multiple context managers as one — enter in order, exit reversed."""

    def __init__(self, ctxs) -> None:
        self._ctxs = list(ctxs)
        self._stack = contextlib.ExitStack()

    def __enter__(self):
        self._stack.__enter__()
        try:
            for ctx in self._ctxs:
                self._stack.enter_context(ctx)
        except BaseException as exc:
            self._stack.__exit__(type(exc), exc, exc.__traceback__)
            raise
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        return self._stack.__exit__(exc_type, exc_val, exc_tb)


def _compose_context_fns(
    factories: Tuple[Callable[[], Tuple[object, object]], ...],
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
    once, then stacks all forward contexts and all recompute contexts into
    two :class:`_StackedCtx` instances so the composite respects the
    single-call contract.
    """
    def factory() -> Tuple[_StackedCtx, _StackedCtx]:
        pairs = [fn() for fn in factories]
        fwd_ctxs = [pair[0] for pair in pairs]
        rec_ctxs = [pair[1] for pair in pairs]
        return _StackedCtx(fwd_ctxs), _StackedCtx(rec_ctxs)

    return factory


def is_compiling() -> bool:
    """Return whether the caller runs inside a ``torch.compile`` capture."""
    return torch.compiler.is_compiling()


def noop_context_fn():
    """The Torch-native no-op ``context_fn``, for callers that need an explicit one."""
 93
 94
 95
 96
 97
 98
 99
100
101
102

def noop_context_fn():
    """The Torch-native no-op ``context_fn``, for callers that need an explicit one."""
    # pylint: disable=C0415
    from torch.utils.checkpoint import noop_context_fn as _torch_noop_context_fn
    return _torch_noop_context_fn


def checkpoint_exclude_wrapper(module: Any, *, save_output: bool = True) -> Any:
    """Wrap a callable whose region is excluded from activation recomputation.
110
111
112
113
114
115
116
117
118
119
    Returns:
        The checkpoint exclusion wrapper.
    """
    # pylint: disable=C0415
    from .wrapper import checkpoint_exclude_wrapper as _wrapper
    return _wrapper(module, save_output=save_output)


def checkpoint_wrapper(module, **checkpoint_kwargs):
    """Wrap *module* with HyperParallel activation checkpointing."""
117
118
119
120
121
122
123
124
125
126

def checkpoint_wrapper(module, **checkpoint_kwargs):
    """Wrap *module* with HyperParallel activation checkpointing."""
    # pylint: disable=C0415
    from .wrapper import ckpt_wrapper
    return ckpt_wrapper(module, **checkpoint_kwargs)


def swap_wrapper(module, policy_fn=None, group_swap=False, cpu_pool=None):
    """Wrap *module* so its activations are offloaded to host memory.
135
136
137
138
139
140
141
142
143
144
145
146
147
    Returns:
        The configured swap wrapper.
    """
    # pylint: disable=C0415
    from .wrapper import swap_wrapper as _swap_wrapper
    kwargs = {}
    if cpu_pool is not None:
        kwargs["cpu_pool"] = cpu_pool
    return _swap_wrapper(module, policy_fn=policy_fn, group_swap=group_swap, **kwargs)


def swap_tensor_wrapper(target, tag=None, group_swap=False, cpu_pool=None):
    """Wrap a single tensor or callable for activation swap.
156
157
158
159
160
161
162
163
164
165
166
167
168
    Returns:
        The registered tensor structure.
    """
    # pylint: disable=C0415
    from .wrapper import swap_tensor_wrapper as _swap_tensor_wrapper
    kwargs = {}
    if cpu_pool is not None:
        kwargs["cpu_pool"] = cpu_pool
    return _swap_tensor_wrapper(target, tag=tag, group_swap=group_swap, **kwargs)


def get_class_activation_wrapper():
    """Return the :class:`ActivationWrapper` base class of this backend."""
166
167
168
169
170
171
172
173
174
175

def get_class_activation_wrapper():
    """Return the :class:`ActivationWrapper` base class of this backend."""
    # pylint: disable=C0415
    from .wrapper import ActivationWrapper
    return ActivationWrapper


def ignore_sac_ops(ignore_ops: list) -> None:
    """Exclude backend operators from selective-AC replay accounting."""
173
174
175
176
177
178
179
180
181
182

def ignore_sac_ops(ignore_ops: list) -> None:
    """Exclude backend operators from selective-AC replay accounting."""
    # pylint: disable=C0415
    from .sac import ignore_sac_ops as _ignore_sac_ops
    _ignore_sac_ops(ignore_ops)


def create_selective_checkpoint_contexts(policy_fn_or_list, allow_cache_entry_mutation=False,
                                         group_swap=False, cpu_pool=None):
181
182
183
184
185
186
187
188
189
190
def create_selective_checkpoint_contexts(policy_fn_or_list, allow_cache_entry_mutation=False,
                                         group_swap=False, cpu_pool=None):
    """Create HyperParallel selective-checkpoint ``(forward_ctx, recompute_ctx)`` contexts."""
    # pylint: disable=C0415
    from .sac import create_selective_checkpoint_contexts as _create_contexts
    return _create_contexts(
        policy_fn_or_list,
        allow_cache_entry_mutation=allow_cache_entry_mutation,
        group_swap=group_swap,
        cpu_pool=cpu_pool,
193
194
195
196
197
198
199
200
201
202

def create_native_selective_checkpoint_contexts(policy_fn):
    """Create Torch-native selective-checkpoint contexts for compile capture."""
    # pylint: disable=C0415
    from .compile_adapter import create_native_selective_checkpoint_contexts as _create_contexts
    return _create_contexts(policy_fn)


def async_save_on_cpu(policy_fn=None, group_swap: bool = False, cpu_pool=None):
    """Return the async host-offload context used by :func:`swap` and ``swap_inputs``."""
200
201
202
203
204
205
206
207
208
209

def async_save_on_cpu(policy_fn=None, group_swap: bool = False, cpu_pool=None):
    """Return the async host-offload context used by :func:`swap` and ``swap_inputs``."""
    # pylint: disable=C0415
    from .wrapper import AsyncSaveOnCpu
    return AsyncSaveOnCpu(policy_fn=policy_fn, group_swap=group_swap, cpu_pool=cpu_pool)


def recompute_handle_collector_ctx():
    """Return the context that collects recompute handles for a checkpoint region."""
207
208
209
210
211
212
213
214
215
216

def recompute_handle_collector_ctx():
    """Return the context that collects recompute handles for a checkpoint region."""
    # pylint: disable=C0415
    from .checkpoint import recompute_handle_collector_ctx as _collector_ctx
    return _collector_ctx()


def recompute_handle(handle, session_id):
    """Recompute the region behind *handle* under *session_id*."""
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232

def recompute_handle(handle, session_id):
    """Recompute the region behind *handle* under *session_id*."""
    # pylint: disable=C0415
    from .checkpoint import recompute_handle as _recompute_handle
    return _recompute_handle(handle, session_id)


def recompute_session_ctx(session_id, retain_on_unpack=False):
    """Open a recompute session so replay can unpack saved activations."""
    if session_id is None:
        raise ValueError("session_id must not be None.")
    # pylint: disable=C0415
    from .checkpoint import recompute_session_ctx as _session_ctx
    return _session_ctx(session_id=session_id, retain_on_unpack=retain_on_unpack)


def clear_recompute_session(session_id):
    """Release all state held for *session_id*."""
230
231
232
233
234
235
236
237
238
239

def clear_recompute_session(session_id):
    """Release all state held for *session_id*."""
    # pylint: disable=C0415
    from .checkpoint import clear_recompute_session as _clear
    return _clear(session_id)


def checkpoint(
    function,
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298

    Returns:
        The result of applying the function with checkpointing.
    """
    if not isinstance(early_stop, bool):
        raise ValueError(f"early_stop must be bool, but got {type(early_stop).__name__}.")

    if is_compiling():
        unsupported = []
        if swap_inputs:
            unsupported.append("swap_inputs")
        if group_swap:
            unsupported.append("group_swap")
        if cpu_pool is not None:
            unsupported.append("cpu_pool")
        if context_fn is not None:
            unsupported.append("custom context_fn")
        if kwargs.get("use_reentrant", False):
            unsupported.append("use_reentrant=True")
        if unsupported:
            raise ValueError(
                "HyperParallel checkpoint compile mode does not support: "
                + ", ".join(unsupported)
                + ". Use Torch-native non-reentrant checkpointing with optional "
                "SAVE/RECOMPUTE selective policies."
296
297
298
299
300
301
302
303
304
                + ", ".join(unsupported)
                + ". Use Torch-native non-reentrant checkpointing with optional "
                "SAVE/RECOMPUTE selective policies."
            )
        composed_context_fn = (
            partial(create_native_selective_checkpoint_contexts, policy_fn)
            if policy_fn is not None
            else None
        )
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
            if policy_fn is not None
            else None
        )
    else:
        factories: list = [create_recompute_contexts]
        if policy_fn is not None:
            selective_kwargs = {"group_swap": group_swap}
            if cpu_pool is not None:
                selective_kwargs["cpu_pool"] = cpu_pool
            factories.append(partial(create_selective_checkpoint_contexts, policy_fn, **selective_kwargs))
        if context_fn is not None:
            factories.append(context_fn)

        if len(factories) == 1:
            composed_context_fn = factories[0]
        else:
            composed_context_fn = _compose_context_fns(tuple(factories))

    if swap_inputs:
        async_kwargs = {"group_swap": group_swap}
        if cpu_pool is not None:
            async_kwargs["cpu_pool"] = cpu_pool
        context = partial(async_save_on_cpu, **async_kwargs)
    else:
        context = contextlib.nullcontext
    with context():
        checkpoint_kwargs = {**kwargs, "use_reentrant": False, "early_stop": early_stop}
        if composed_context_fn is not None:
            checkpoint_kwargs["context_fn"] = composed_context_fn
        # pylint: disable=C0415
        from .checkpoint import checkpoint as hyper_checkpoint
        return hyper_checkpoint(function, *args, **checkpoint_kwargs)


def swap(function, *args, policy_fn=None, group_swap=False, cpu_pool=None, **kwargs):
    """Apply activation swap to a function call.
360
361
362
363
364
365
366
367
368
369
370
371
372
373

    Example:
        >>> output = swap(layer, x, policy_fn=lambda t: CheckpointPolicy.MUST_SAVE)
    """
    if is_compiling():
        raise ValueError(
            "HyperParallel activation swap is not supported in compile mode. "
            "Use Torch-native non-reentrant checkpointing with SAVE/RECOMPUTE policies."
        )
    async_kwargs = {"policy_fn": policy_fn, "group_swap": group_swap}
    if cpu_pool is not None:
        async_kwargs["cpu_pool"] = cpu_pool
    with async_save_on_cpu(**async_kwargs):
        return function(*args, **kwargs)
hyper_parallel/core/activation_memory/checkpoint.py
54
55
56
57
58
59
60
61
62
    """Saved-tensor placeholder containing handles keyed by recompute session."""

    def __init__(self) -> None:
        """Initialize an empty per-session handle mapping."""
        self.handles: Dict[Any, Optional[_Handle]] = {}


class _StopRecomputationError(Exception):
    """Internal control-flow exception used by early-stop recomputation."""
66
67
68
69
70
71
72
73
74
75
76
    """Control-plane state shared by checkpoint frames in one session scope."""

    def __init__(self, session_id: Any, retain_on_unpack: bool) -> None:
        """Initialize one scoped session activation."""
        self.session_id = session_id
        self.retain_on_unpack = retain_on_unpack
        self.frames: weakref.WeakSet = weakref.WeakSet()


class _NoopSaveInputs(torch.autograd.Function):
    """Save checkpoint inputs without adding a meaningful forward operation."""
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112

    @staticmethod
    def forward(*args: Any) -> Any:
        """Return a dummy output whose grad function retains checkpoint inputs."""
        del args
        return torch.empty((0,))

    @staticmethod
    def setup_context(ctx: Any, inputs: Tuple[Any, ...], output: Any) -> None:
        """Save tensor inputs while retaining non-tensor input structure."""
        del output
        tensor_pairs = [(index, value) for index, value in enumerate(inputs) if isinstance(value, torch.Tensor)]
        tensor_indices, tensors = zip(*tensor_pairs)
        index_to_saved_index = {input_index: saved_index for saved_index, input_index in enumerate(tensor_indices)}
        stored_args = [None if isinstance(value, torch.Tensor) else value for value in inputs]

        def get_args(saved_tensors: Tuple[Any, ...]) -> List[Any]:
            """Reconstruct the original checkpoint arguments."""
            restored_args = [
                saved_tensors[index_to_saved_index[index]] if index in tensor_indices else value
                for index, value in enumerate(stored_args)
            ]
            return restored_args[1:]

        ctx.get_args = get_args
        ctx.save_for_backward(*tensors)

    @staticmethod
    def backward(ctx: Any, *grad_outputs: Any) -> None:
        """Reject direct backward through the internal input saver."""
        del ctx, grad_outputs
        raise CheckpointError("The internal checkpoint input saver must not be backwarded directly.")


class _CheckpointFrame:
    """State shared by one checkpoint forward and its recomputations."""
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
    """State shared by one checkpoint forward and its recomputations."""

    def __init__(self, recompute_fn: Callable, early_stop: bool, metadata_fn: Optional[Callable]) -> None:
        """Initialize frame state captured by the saved-tensor hooks."""
        self.recompute_fn = recompute_fn
        self.input_saver = None
        self.weak_holders: List[weakref.ReferenceType] = []
        self.recomputed: DefaultDict[Any, weakref.WeakKeyDictionary] = defaultdict(weakref.WeakKeyDictionary)
        self.recomp_counter: DefaultDict[Any, int] = defaultdict(int)
        self.is_recomputed: DefaultDict[Any, bool] = defaultdict(bool)
        self.early_stop = early_stop
        self.metadata_fn = metadata_fn
        self.x_metadatas: List[Any] = []
        self.forward_completed = False
        self.ignore_saved_mismatch = False
        self.active_session: Optional[_SessionActivation] = None

    def check_recomputed_tensors_match(self, session_id: Any) -> None:
        """Validate saved-tensor count and metadata after recomputation."""
        if self.ignore_saved_mismatch:
            return
        if len(self.weak_holders) != self.recomp_counter[session_id]:
            raise CheckpointError(
                "Hyper checkpoint saved a different number of tensors during forward and recomputation. "
                f"Forward saved {len(self.weak_holders)} tensors, but recomputation saved "
                f"{self.recomp_counter[session_id]} tensors."
            )
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
                f"Forward saved {len(self.weak_holders)} tensors, but recomputation saved "
                f"{self.recomp_counter[session_id]} tensors."
            )

        mismatches = []
        for index, weak_holder in enumerate(self.weak_holders):
            holder = weak_holder()
            if holder is None:
                continue
            handle = holder.handles.get(session_id)
            _internal_assert(handle is not None, "Missing recomputed tensor handle during metadata validation.")
            _internal_assert(
                handle in self.recomputed[session_id],
                "Missing recomputed tensor during metadata validation.",
            )
            recomputed_tensor = self.recomputed[session_id][handle]
            recomputed_metadata = self.metadata_fn(recomputed_tensor)
            if self.x_metadatas[index] != recomputed_metadata:
                mismatches.append((index, self.x_metadatas[index], recomputed_metadata))

        if mismatches:
            details = "\n".join(
                f"tensor {index}: forward={forward_metadata}, recompute={recomputed_metadata}"
                for index, forward_metadata, recomputed_metadata in mismatches
            )
            raise CheckpointError(
                "Hyper checkpoint detected different tensor metadata during recomputation:\n" + details
            )

    def clear_session(self, session_id: Any) -> None:
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
            )

    def clear_session(self, session_id: Any) -> None:
        """Release all tensors and handles associated with one session."""
        for weak_holder in self.weak_holders:
            holder = weak_holder()
            if holder is not None:
                holder.handles.pop(session_id, None)
        self.recomputed.pop(session_id, None)
        self.recomp_counter.pop(session_id, None)
        self.is_recomputed.pop(session_id, None)


def _bind_session_activation(frame: _CheckpointFrame, activation: _SessionActivation) -> None:
    """Bind one activation to a frame outside the unpack hot path."""
    if frame.active_session is activation:
        return
    if frame.active_session is not None:
        raise CheckpointError("Concurrent recompute sessions on the same checkpoint frame are not supported.")
    frame.active_session = activation
    activation.frames.add(frame)


def _register_session_frame(
    frame: _CheckpointFrame,
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
    session_id: Any,
    activation: Optional[_SessionActivation] = None,
) -> None:
    """Register a frame for cleanup and bind its current activation when present."""
    with _SESSION_FRAMES_LOCK:
        _SESSION_FRAMES[session_id].add(frame)
        if activation is not None:
            _internal_assert(activation.session_id == session_id, "Session activation key does not match its frame.")
            _bind_session_activation(frame, activation)


def _activate_registered_frames(activation: _SessionActivation) -> None:
    """Install an activation on every frame already registered for its session."""
    with _SESSION_FRAMES_LOCK:
        for frame in list(_SESSION_FRAMES.get(activation.session_id, ())):
            _bind_session_activation(frame, activation)


def _deactivate_session(activation: _SessionActivation) -> None:
    """Remove one activation from every frame bound at context entry."""
    with _SESSION_FRAMES_LOCK:
        for frame in list(activation.frames):
            if frame.active_session is activation:
                frame.active_session = None
        activation.frames.clear()


def _internal_assert(condition: bool, message: str) -> None:
    if not condition:
        raise CheckpointError(message)


def _noop_context_fn() -> Tuple[contextlib.AbstractContextManager, contextlib.AbstractContextManager]:
    return contextlib.nullcontext(), contextlib.nullcontext()


def _default_metadata_fn(tensor: Any) -> Dict[str, Any]:
    return {"shape": tensor.shape, "dtype": tensor.dtype, "device": tensor.device}


def _infer_device_type(*args: Any) -> str:
    """Return the preferred non-CPU device type found in checkpoint inputs."""
    device_types = []

    def add_device_type(value: Any) -> None:
        """Record one non-CPU tensor device type."""
        if isinstance(value, torch.Tensor) and value.device.type != "cpu":
            device_types.append(value.device.type)

    tree_map(add_device_type, args)
    device_types_set = set(device_types)
    if len(device_types_set) > 1:
        warnings.warn(
            "Hyper checkpoint received tensors on multiple non-CPU device types. RNG state is preserved only for "
            "one device type; CUDA is preferred when present.",
            stacklevel=3,
        )
    if not device_types:
        return DefaultDeviceType.get_device_type()
    if "cuda" in device_types_set:
        return "cuda"
    return device_types[0]


def _get_device_module(device_type: str) -> Any:
    if device_type == "meta":
        return torch.device("meta")
    return getattr(torch, device_type)


def _get_device_states(device_type: str, *args: Any) -> Tuple[List[int], List[Any]]:
    """Capture RNG states for non-CPU input devices of the requested type."""
    device_ids = []

    def add_device_id(value: Any) -> None:
        """Record one non-CPU tensor device index."""
        if isinstance(value, torch.Tensor) and value.device.type not in {"cpu", "meta"}:
            device_ids.append(value.get_device())

    tree_map(add_device_id, args)
    device_module = _get_device_module(device_type)
    states = []
    for device_id in device_ids:
        with device_module.device(device_id):
            states.append(device_module.get_rng_state())
    return device_ids, states


def _set_device_states(device_type: str, devices: List[int], states: List[Any]) -> None:
    if device_type == "meta":
        return
    device_module = _get_device_module(device_type)
    for device, state in zip(devices, states):
        with device_module.device(device):
            device_module.set_rng_state(state)


def _get_autocast_kwargs(device_type: str) -> Tuple[Optional[Dict[str, Any]], Dict[str, Any]]:
    """Return active autocast settings for the selected device and CPU."""
    device_kwargs = None
    if torch.amp.is_autocast_available(device_type):
        device_kwargs = {
            "enabled": torch.is_autocast_enabled(device_type),
            "dtype": torch.get_autocast_dtype(device_type),
            "cache_enabled": torch.is_autocast_cache_enabled(),
        }
    cpu_kwargs = {
        "enabled": torch.is_autocast_enabled("cpu"),
        "dtype": torch.get_autocast_dtype("cpu"),
        "cache_enabled": torch.is_autocast_cache_enabled(),
    }
    return device_kwargs, cpu_kwargs


def _create_recomputation_hooks(frame: _CheckpointFrame, session_id: Any) -> Any:
    """Create saved-tensor hooks that retain tensors from one recomputation."""
    frame_ref = weakref.ref(frame)

    def pack_hook(tensor: Any) -> Any:
        """Store recomputed tensors in their forward holders."""
        tensor = tensor.detach() if tensor.requires_grad else tensor
        target_frame = frame_ref()
        _internal_assert(target_frame is not None, "Checkpoint frame was released during recomputation.")
        recompute_index = target_frame.recomp_counter[session_id]
        target_frame.recomp_counter[session_id] += 1

        if recompute_index >= len(target_frame.weak_holders):
            if not target_frame.early_stop and not target_frame.forward_completed:
                target_frame.ignore_saved_mismatch = True
                return tensor
            raise CheckpointError(
                "Hyper checkpoint tried to save more tensors during recomputation than during forward."
            )

        holder = target_frame.weak_holders[recompute_index]()
        if holder is not None:
            _internal_assert(
                holder.handles.get(session_id) is None,
                "A recomputed tensor handle already exists for this session.",
            )
            handle = _Handle()
            holder.handles[session_id] = handle
            target_frame.recomputed[session_id][handle] = tensor

        if target_frame.early_stop and target_frame.recomp_counter[session_id] == len(target_frame.weak_holders):
            raise _StopRecomputationError
        return tensor

    def unpack_hook(tensor: Any) -> Any:
        """Return tensors saved by operations inside the recomputation."""
        return tensor

    return torch.autograd.graph.saved_tensors_hooks(pack_hook, unpack_hook)


# PyTorch exposes this tracing guard only as a private decorator.
@torch._disable_dynamo  # pylint: disable=protected-access
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# PyTorch exposes this tracing guard only as a private decorator.
@torch._disable_dynamo  # pylint: disable=protected-access
def _run_fn_with_dynamo_disabled(function: Callable, *args: Any, **kwargs: Any) -> Any:
    """Run recomputation without tracing the saved-tensor unpack hook with Dynamo."""
    return function(*args, **kwargs)


def _run_recomputation(frame: _CheckpointFrame, session_id: Any) -> None:
    """Run and validate a frame recomputation for the given session."""
    if frame.is_recomputed[session_id]:
        return

    activation = frame.active_session
    if activation is not None:
        _internal_assert(activation.session_id == session_id, "Active session key does not match recomputation key.")
    previous_activation = _RECOMPUTE_SESSION.get()
    token = None
    if activation is not None and previous_activation is not activation:
        token = _RECOMPUTE_SESSION.set(activation)
    try:
        input_context = frame.input_saver.grad_fn
        args = input_context.get_args(input_context.saved_tensors)
        try:
            with _create_recomputation_hooks(frame, session_id), torch.autograd.enable_grad():
                _run_fn_with_dynamo_disabled(frame.recompute_fn, *args)
        except _StopRecomputationError:
            pass
    finally:
        if token is not None:
            _RECOMPUTE_SESSION.reset(token)
    frame.is_recomputed[session_id] = True
    frame.check_recomputed_tensors_match(session_id)


def _create_checkpoint_hooks(frame: _CheckpointFrame) -> Any:
    """Create hooks that lazily recompute tensors saved during forward."""
    def pack_hook(tensor: Any) -> _Holder:
        """Replace a forward saved tensor with an opaque holder."""
        holder = _Holder()
        frame.weak_holders.append(weakref.ref(holder))
        if frame.metadata_fn is not None:
            with torch.no_grad():
                frame.x_metadatas.append(frame.metadata_fn(tensor))
        return holder

    def unpack_hook(holder: _Holder) -> Any:
        """Return the corresponding tensor from lazy or prefired recomputation."""
        activation = frame.active_session
        if activation is not None:
            session_id = activation.session_id
            retain_on_unpack = activation.retain_on_unpack
        else:
            session_id = torch._C._current_graph_task_id()  # pylint: disable=W0212
            if session_id == -1:
                session_id = int(uuid.uuid4())
            retain_on_unpack = False

        _run_recomputation(frame, session_id)
        _internal_assert(session_id in holder.handles, "No recomputed tensor was saved for this checkpoint value.")
        handle = holder.handles[session_id]
        if handle is None:
            raise CheckpointError("A checkpoint tensor was unpacked more than once in the same recompute session.")
        _internal_assert(handle in frame.recomputed[session_id], "The recomputed tensor has already been released.")
        tensor = frame.recomputed[session_id][handle]
        if not retain_on_unpack:
            holder.handles[session_id] = None
        return tensor

    return torch.autograd.graph.saved_tensors_hooks(pack_hook, unpack_hook)


def _is_compiling() -> bool:
    compiler = getattr(torch, "compiler", None)
    return bool(compiler is not None and compiler.is_compiling())


def _native_checkpoint(
    function: Callable,
426
427
428
429
430
431
432
433
434
435
    early_stop: bool,
    **kwargs: Any,
) -> Any:
    """Use the public native API for compile, adapting 2.6/2.7 early-stop."""
    with set_checkpoint_early_stop(early_stop):
        return torch_checkpoint(
            function,
            *args,
            use_reentrant=False,
            context_fn=context_fn,
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
    *args: Any,
    **kwargs: Any,
) -> Generator[None, None, None]:
    """Set up eager checkpoint state around the caller's forward execution."""
    metadata_functions = {_DEFAULT_DETERMINISM_MODE: _default_metadata_fn, "none": lambda tensor: None}
    if determinism_check not in metadata_functions:
        raise ValueError(
            f"determinism_check must be one of {list(metadata_functions)}, but got {determinism_check!r}."
        )
    metadata_fn = metadata_functions[determinism_check]

    device_type = _infer_device_type(*args)
    device_module = _get_device_module(device_type)
    contexts = context_fn()
    if not isinstance(contexts, tuple) or len(contexts) != 2:
        raise ValueError("context_fn must return a (forward_context, recompute_context) tuple.")
    forward_context, recompute_context = contexts
    device_autocast_kwargs, cpu_autocast_kwargs = _get_autocast_kwargs(device_type)

    had_device_in_forward = False
    forward_devices: List[int] = []
    forward_device_states: List[Any] = []
    forward_cpu_state = None
    if preserve_rng_state:
        forward_cpu_state = torch.get_rng_state()
        if getattr(device_module, "_initialized", False):
            had_device_in_forward = True
            forward_devices, forward_device_states = _get_device_states(device_type, *args)

    def recompute_fn(*inputs: Any) -> None:
        """Restore execution state and rerun the checkpointed function."""
        function_kwargs, *function_args = inputs
        rng_devices = forward_devices if preserve_rng_state and had_device_in_forward else []
        with torch.random.fork_rng(
            devices=rng_devices,
            enabled=preserve_rng_state,
            device_type=device_type,
        ):
            if preserve_rng_state:
                torch.set_rng_state(forward_cpu_state)
                if had_device_in_forward:
                    _set_device_states(device_type, forward_devices, forward_device_states)

            device_autocast_context = contextlib.nullcontext()
            if device_autocast_kwargs is not None:
                device_autocast_context = torch.amp.autocast(device_type=device_type, **device_autocast_kwargs)
            with device_autocast_context, torch.amp.autocast("cpu", **cpu_autocast_kwargs), recompute_context:
                function(*function_args, **function_kwargs)

    frame = _CheckpointFrame(recompute_fn, early_stop, metadata_fn)
    dummy = torch.empty((0,), requires_grad=True)
    frame.input_saver = _NoopSaveInputs.apply(dummy, kwargs, *args)

    if frame.input_saver.grad_fn is None:
        yield
        return

    activation = _RECOMPUTE_SESSION.get()
    if activation is not None:
        raise CheckpointError("Nested checkpoint is not supported during scheduled recomputation.")

    collector = _RECOMPUTE_COLLECTOR.get()
    if collector is not None:
        collector.append(frame)
    try:
        with _create_checkpoint_hooks(frame), forward_context:
            yield
        frame.forward_completed = True

        if getattr(device_module, "_initialized", False) and preserve_rng_state and not had_device_in_forward:
            raise RuntimeError(
                "The device state was initialized inside a Hyper checkpoint forward, so its initial RNG state "
                "could not be preserved. Initialize the device before entering checkpoint."
            )
    except BaseException:
        if collector is not None and frame in collector:
            collector.remove(frame)
        raise


def checkpoint(
    function: Callable,
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560

    Eager execution always uses this implementation. Compile execution falls
    back to PyTorch's public non-reentrant checkpoint API.
    """
    if use_reentrant is not False:
        raise ValueError("Hyper checkpoint only supports use_reentrant=False.")
    if not isinstance(early_stop, bool):
        raise ValueError(f"early_stop must be bool, but got {type(early_stop).__name__}.")
    if not isinstance(preserve_rng_state, bool):
        raise ValueError(
            f"preserve_rng_state must be bool, but got {type(preserve_rng_state).__name__}."
        )
    if not callable(context_fn):
        raise ValueError("context_fn must be callable.")
    if _is_compiling():
        return _native_checkpoint(
            function,
            *args,
            context_fn=context_fn,
            preserve_rng_state=preserve_rng_state,
562
563
564
565
566
567
568
569
570
571
572
573
            debug=debug,
            early_stop=early_stop,
            **kwargs,
        )
    if debug:
        raise ValueError("debug=True is not supported by Hyper eager checkpoint yet.")

    generator = _checkpoint_without_reentrant_generator(
        function,
        preserve_rng_state,
        context_fn,
        determinism_check,
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
        early_stop,
        *args,
        **kwargs,
    )
    next(generator)
    try:
        result = function(*args, **kwargs)
    except BaseException:
        generator.close()
        raise
    try:
        next(generator)
    except StopIteration:
        return result
    generator.close()
    raise CheckpointError("The internal checkpoint generator yielded more than once.")


@contextlib.contextmanager
def recompute_handle_collector_ctx() -> Iterator[List[Any]]:
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635

@contextlib.contextmanager
def recompute_handle_collector_ctx() -> Iterator[List[Any]]:
    """Collect opaque checkpoint handles created in this context."""
    handles = []
    token = _RECOMPUTE_COLLECTOR.set(handles)
    try:
        yield handles
    finally:
        _RECOMPUTE_COLLECTOR.reset(token)


def recompute_handle(handle: Any, session_id: Any) -> None:
    """Run one collected checkpoint recomputation ahead of backward."""
    if not isinstance(handle, _CheckpointFrame):
        raise ValueError("handle must be produced by recompute_handle_collector_ctx().")
    _validate_session_id(session_id)
    activation = _RECOMPUTE_SESSION.get()
    if (
        activation is not None
        and activation.session_id == session_id
        and activation.retain_on_unpack
    ):
        _register_session_frame(handle, session_id, activation)
        _run_recomputation(handle, session_id)
        return
    if activation is not None:
        raise CheckpointError("recompute_handle cannot enter another active recompute session.")

    _register_session_frame(handle, session_id)
    with recompute_session_ctx(session_id=session_id, retain_on_unpack=True):
        _run_recomputation(handle, session_id)


def _validate_session_id(session_id: Any) -> None:
    if session_id is None:
        raise ValueError("session_id must not be None.")
    try:
        hash(session_id)
    except TypeError as error:
        raise ValueError("session_id must be hashable.") from error


@contextlib.contextmanager
def recompute_session_ctx(session_id: Any, retain_on_unpack: bool = False) -> Iterator[Any]:
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661

@contextlib.contextmanager
def recompute_session_ctx(session_id: Any, retain_on_unpack: bool = False) -> Iterator[Any]:
    """Select the key and retention policy used by checkpoint unpack hooks."""
    _validate_session_id(session_id)
    if not isinstance(retain_on_unpack, bool):
        raise ValueError(f"retain_on_unpack must be bool, but got {type(retain_on_unpack).__name__}.")
    if _RECOMPUTE_SESSION.get() is not None:
        raise CheckpointError("Nested recompute session contexts are not supported.")
    activation = _SessionActivation(session_id, retain_on_unpack)
    token = _RECOMPUTE_SESSION.set(activation)
    try:
        _activate_registered_frames(activation)
        yield session_id
    finally:
        try:
            _deactivate_session(activation)
        finally:
            _RECOMPUTE_SESSION.reset(token)


def clear_recompute_session(session_id: Any) -> None:
    """Release retained recomputation data for a session; repeated calls are safe."""
    _validate_session_id(session_id)
    with _SESSION_FRAMES_LOCK:
        registered_frames = _SESSION_FRAMES.pop(session_id, None)
        frames = list(registered_frames) if registered_frames is not None else []
    for frame in frames:
        frame.clear_session(session_id)
hyper_parallel/core/activation_memory/checkpoint_exclude.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    """Defer one excluded-region saved input until checkpoint replay."""

    def __init__(self) -> None:
        """Initialize an unused and unresolved handle."""
        self.used = False
        self._tensor = None

    def mark_used(self) -> None:
        """Record that an exclude operation saved this input for backward."""
        self.used = True

    def materialize(self, tensor: Any) -> None:
        """Bind the handle to the matching input produced during replay."""
        self._tensor = tensor

    def get_recomputed_tensor(self) -> Any:
        """Return the replay-produced input for backward."""
        if self._tensor is None:
            raise RuntimeError("Checkpoint-excluded input was requested before recomputation")
        return self._tensor


@dataclass(frozen=True)
class _InputBinding:
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
    """Store excluded-region call entries for one checkpoint invocation."""

    def __init__(self) -> None:
        """Initialize an empty per-checkpoint output cache."""
        self._entries: Dict[int, Deque[_ExcludeCacheEntry]] = defaultdict(deque)

    def save(self, wrapper_id: int, entry: _ExcludeCacheEntry) -> None:
        """Save one call entry produced by a checkpoint-excluded region."""
        self._entries[wrapper_id].append(entry)

    def pop(self, wrapper_id: int) -> _ExcludeCacheEntry:
        """Return the matching forward call entry during recomputation."""
        entries = self._entries.get(wrapper_id)
        if not entries:
            raise RuntimeError("No cached forward output is available for this checkpoint exclusion wrapper")
        entry = entries.popleft()
        if not entries:
            self._entries.pop(wrapper_id)
        return entry

    def clear(self) -> None:
        """Release outputs not consumed because recomputation stopped early."""
        self._entries.clear()


def _pack_saved_tensor(tensor: Any) -> Any:
    """Return a deferred input handle or detached tensor data."""
    handle = getattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, None)
    if isinstance(handle, _RecomputedInputHandle):
        handle.mark_used()
        return handle
    return tensor.detach() if tensor.requires_grad else tensor


def _unpack_saved_tensor(value: Any) -> Any:
    """Restore the saved tensor for backward."""
    if isinstance(value, _RecomputedInputHandle):
        return value.get_recomputed_tensor()
    return value


def _saved_tensors_context() -> Any:
    """Create an inner hook that stores real tensors instead of outer holders."""
    return torch.autograd.graph.saved_tensors_hooks(_pack_saved_tensor, _unpack_saved_tensor)


_EXCLUDE_CACHE_KEY = object()
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
    leaves: List[_TensorInput],
    tensor_type: type,
) -> None:
    """Append tensor leaves without creating a self-referential local function."""
    if isinstance(value, tensor_type):
        leaves.append((path, value))
        return
    if isinstance(value, (tuple, list)):
        for index, item in enumerate(value):
            _append_tensor_inputs(item, path + (("index", index),), leaves, tensor_type)
        return
    if isinstance(value, dict):
        for key, item in value.items():
            _append_tensor_inputs(item, path + (("key", key),), leaves, tensor_type)


def _collect_tensor_inputs(args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> List[_TensorInput]:
    """Return tensor leaves and self-describing paths from one excluded-region call."""
    leaves = []
    for index, arg in enumerate(args):
        _append_tensor_inputs(arg, (("arg", index),), leaves, torch.Tensor)
    for key, value in kwargs.items():
        _append_tensor_inputs(value, (("kwarg", key),), leaves, torch.Tensor)
    return leaves


def _mark_recompute_inputs(
    invocation_id: object,
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
    args: Tuple[Any, ...],
    kwargs: Dict[str, Any],
) -> Tuple[List[_InputBinding], List[Tuple[Any, Any]]]:
    """Attach deferred handles to inputs that replay reproduces."""
    bindings = []
    previous_handles = []
    seen_tensor_ids = set()
    try:
        for path, tensor in _collect_tensor_inputs(args, kwargs):
            if (
                isinstance(tensor, torch.nn.Parameter)
                or id(tensor) in seen_tensor_ids
                or getattr(tensor, _SAVE_OUTPUT_SOURCE_ATTR, None) is invocation_id
            ):
                continue
            handle = _RecomputedInputHandle()
            previous = getattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, _MISSING)
            previous_handles.append((tensor, previous))
            setattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, handle)
            bindings.append(_InputBinding(path, handle))
            seen_tensor_ids.add(id(tensor))
    except BaseException:
        _restore_recompute_inputs(previous_handles)
        raise
    return bindings, previous_handles


def _restore_recompute_inputs(previous_handles: List[Tuple[Any, Any]]) -> None:
    """Restore Tensor attributes overwritten for one excluded call."""
    for tensor, previous in previous_handles:
        if previous is _MISSING:
            if hasattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR):
                delattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR)
        else:
            setattr(tensor, _RECOMPUTE_INPUT_HANDLE_ATTR, previous)


def _resolve_input(args: Tuple[Any, ...], kwargs: Dict[str, Any], path: _InputPath) -> Any:
    """Resolve one replay input from its forward argument path."""
    root_kind, root_key = path[0]
    value = args[root_key] if root_kind == "arg" else kwargs[root_key]
    for _, token_value in path[1:]:
        value = value[token_value]
    return value


def _materialize_recompute_inputs(
    entry: _ExcludeCacheEntry,
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
    args: Tuple[Any, ...],
    kwargs: Dict[str, Any],
) -> None:
    """Bind used input handles to tensors produced during checkpoint replay."""
    for binding in entry.input_bindings:
        if not binding.handle.used:
            continue
        tensor = _resolve_input(args, kwargs, binding.path)
        if not isinstance(tensor, torch.Tensor):
            raise RuntimeError(
                "Checkpoint replay did not reproduce a tensor input required by a checkpoint-excluded region"
            )
        binding.handle.materialize(tensor.detach())


def _has_used_input(input_bindings: List[_InputBinding]) -> bool:
    """Return whether the excluded call saved any marked input."""
    return any(binding.handle.used for binding in input_bindings)


@lru_cache(maxsize=1)
def _get_replay_placeholder() -> Any:
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236

@lru_cache(maxsize=1)
def _get_replay_placeholder() -> Any:
    """Create a zero-element placeholder returned by an elided SAVE replay."""
    return torch.empty(0, device="cpu")


def _make_replay_placeholder_output(tensor_count: int) -> Any:
    """Create one placeholder leaf for each forward output tensor."""
    if tensor_count == 0:
        return ()
    placeholder = _get_replay_placeholder()
    if tensor_count == 1:
        return placeholder
    return (placeholder,) * tensor_count


@lru_cache(maxsize=1)
def _get_recompute_trigger() -> Any:
234
235
236
237
238
239
240
241
242

@lru_cache(maxsize=1)
def _get_recompute_trigger() -> Any:
    """Create the differentiable zero-element input used by recompute boundaries."""
    return torch.empty(0, device="cpu", requires_grad=True)


@lru_cache(maxsize=1)
def _get_recompute_boundary() -> Any:
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263

@lru_cache(maxsize=1)
def _get_recompute_boundary() -> Any:
    """Create the autograd Function used to trigger outer checkpoint replay."""
    class _RecomputeBoundary(torch.autograd.Function):
        """Trigger the outer checkpoint hook before excluded-region backward."""

        @staticmethod
        def forward(ctx: Any, tensor: Any, trigger: Any) -> Any:
            """Save one zero-element outer-hook dependency and return the tensor unchanged."""
            ctx.save_for_backward(trigger)
            return tensor

        @staticmethod
        def backward(ctx: Any, grad_output: Any) -> Tuple[Any, None]:
            """Trigger dependency unpack and pass the gradient through."""
            _ = ctx.saved_tensors
            return grad_output, None

    return _RecomputeBoundary


def _finalize_save_outputs_impl(
    output: Any,
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
    tensor_leaf_count: Optional[List[int]],
    tensor_type: type,
) -> Any:
    """Recursively finalize SAVE output leaves."""
    if isinstance(output, tensor_type):
        if tensor_leaf_count is not None:
            tensor_leaf_count[0] += 1
        if add_recompute_boundary:
            output = _get_recompute_boundary().apply(output, _get_recompute_trigger())
        if invocation_id is not None:
            setattr(output, _SAVE_OUTPUT_SOURCE_ATTR, invocation_id)
        return output
    if isinstance(output, list):
        return [
            _finalize_save_outputs_impl(item, add_recompute_boundary, invocation_id, tensor_leaf_count, tensor_type)
            for item in output
        ]
    if isinstance(output, tuple):
        items = [
            _finalize_save_outputs_impl(item, add_recompute_boundary, invocation_id, tensor_leaf_count, tensor_type)
            for item in output
        ]
        if hasattr(output, "_fields"):
            return type(output)(*items)
        return tuple(items)
    if isinstance(output, dict):
        return type(output)(
            (
                key,
                _finalize_save_outputs_impl(
                    value,
301
302
303
304
305
306
307
308
309
                ),
            )
            for key, value in output.items()
        )
    return output


def _finalize_save_outputs(
    output: Any,
311
312
313
314
315
316
317
318
319
    invocation_id: Optional[object],
    tensor_leaf_count: Optional[List[int]] = None,
) -> Any:
    """Apply the required boundary and SAVE provenance to output tensor leaves."""
    return _finalize_save_outputs_impl(
        output,
        add_recompute_boundary,
        invocation_id,
        tensor_leaf_count,
hyper_parallel/core/activation_memory/compile_adapter.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56


def _to_torch_checkpoint_policy(policy: Any) -> Any:
    """Convert supported HyperParallel policies to the native Torch enum."""
    from torch.utils import checkpoint as torch_checkpoint  # pylint: disable=C0415

    torch_policy_cls = torch_checkpoint.CheckpointPolicy
    supported_native = {
        getattr(torch_policy_cls, name) for name in _SUPPORTED_POLICY_NAMES
    }
    if isinstance(policy, torch_policy_cls):
        if policy in supported_native:
            return policy
        raise ValueError(
            f"Torch checkpoint policy {policy.name} is not supported by "
            "HyperParallel compile mode. Only SAVE and RECOMPUTE policies are supported."
        )
    if isinstance(policy, CheckpointPolicy):
        if policy.name in _SUPPORTED_POLICY_NAMES:
            return getattr(torch_policy_cls, policy.name)
        raise ValueError(
            f"HyperParallel checkpoint policy {policy.name} is not supported in compile mode. "
            "Only SAVE and RECOMPUTE policies are supported."
        )
    raise TypeError(
        "Selective checkpoint policy_fn must return a HyperParallel or Torch "
        f"CheckpointPolicy, but got {type(policy).__name__}."
    )
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def _torch_policy_adapter(
    policy_fn: Callable, torch_context: Any, op: Any, *args: Any, **kwargs: Any
) -> Any:
    """Pass native Torch inputs through and adapt only the policy result."""
    return _to_torch_checkpoint_policy(policy_fn(torch_context, op, *args, **kwargs))


def create_native_selective_checkpoint_contexts(policy_fn: Callable) -> Tuple[Any, Any]:
    """Create Torch-native selective-checkpoint contexts for compile capture."""
    if not callable(policy_fn):
        raise TypeError("policy_fn must be callable in HyperParallel compile mode.")
    from torch.utils import checkpoint as torch_checkpoint  # pylint: disable=C0415

    return torch_checkpoint.create_selective_checkpoint_contexts(
        partial(_torch_policy_adapter, policy_fn)
    )

hyper_parallel/core/activation_memory/pinned_memory_pool.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    storage_key: Optional[int] = None


def _valid_positive_int(value: int, name: str) -> None:
    if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
        raise ValueError(f"{name} must be a positive integer, but got {value!r}.")


def _storage_key(tensor: Any) -> int:
    """Return a stable identity for a tensor's underlying storage."""
    try:
        return int(tensor.untyped_storage().data_ptr())
    except (AttributeError, TypeError, ValueError) as exc:
        raise ValueError("release() expects a tensor with an identifiable storage.") from exc


class PinnedMemoryPool:
    """Thread-safe, capacity-bounded pool of pinned CPU byte buffers.
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
            sufficient reusable block exists.
    """

    def __init__(self, max_host_bytes: int, align_limit: int = 2 * 1024**3) -> None:
        _valid_positive_int(max_host_bytes, "max_host_bytes")
        _valid_positive_int(align_limit, "align_limit")
        self._max_host_bytes = max_host_bytes
        self._align_limit = align_limit
        self._available: Dict[int, List[_Block]] = defaultdict(list)
        self._pending: Dict[int, List[_Block]] = defaultdict(list)
        self._blocks: Dict[int, _Block] = {}
        self._total_allocated = 0
        self._lock = threading.Lock()

    @property
    def total_allocated(self) -> int:
        """Return total aligned pinned capacity held by this pool."""
        with self._lock:
            return self._total_allocated

    @property
    def max_host_bytes(self) -> int:
        """Return the pool's hard capacity limit in bytes."""
        return self._max_host_bytes

    @property
    def align_limit(self) -> int:
        """Return the largest alignment bucket size in bytes."""
        return self._align_limit

    def _aligned_size(self, size: int) -> int:
        if size > self._align_limit:
            return size
        minimum = max(size, _MIN_BUCKET_BYTES)
        power = 1 << (minimum - 1).bit_length()
        aligned = min(power, self._align_limit)
        return max(aligned, size)

    def _reclaim_completed_locked(self, minimum_capacity: int) -> None:
        """Move completed pending blocks into the available buckets."""
        # A transfer batch may release many blocks with one event. Retain the
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
    def _reclaim_completed_locked(self, minimum_capacity: int) -> None:
        """Move completed pending blocks into the available buckets."""
        # A transfer batch may release many blocks with one event. Retain the
        # event objects so each runtime event is queried only once per scan.
        event_results: Dict[int, Tuple[Any, bool]] = {}
        for capacity in list(self._pending):
            if capacity < minimum_capacity:
                continue
            still_pending = []
            for block in self._pending[capacity]:
                event = block.event
                event_key = id(event)
                event_result = event_results.get(event_key)
                if event_result is None:
                    completed = event.query()
                    event_results[event_key] = (event, completed)
                else:
                    completed = event_result[1]
                if completed:
                    block.event = None
                    block.state = _AVAILABLE
                    self._available[capacity].append(block)
                else:
                    still_pending.append(block)
            if still_pending:
                self._pending[capacity] = still_pending
            else:
                del self._pending[capacity]

    def _register_view_locked(self, block: _Block, view: Any) -> Any:
        """Associate a checked-out view with its owning allocation."""
        key = _storage_key(view)
        owner = self._blocks.get(key)
        if owner is not None and owner is not block:
            raise RuntimeError("Two host buffers exposed the same storage identity.")
        old_key = block.storage_key
        if old_key is not None and old_key != key and self._blocks.get(old_key) is block:
            del self._blocks[old_key]
        self._blocks[key] = block
        block.storage_key = key
        return view

    def _checkout_locked(self, block: _Block, size: int) -> Any:
        """Create and register the exact view returned by :meth:`acquire`."""
        view = block.buffer if size == block.capacity else block.buffer[:size]
        return self._register_view_locked(block, view)

    def _find_available_locked(self, minimum_capacity: int) -> Optional[_Block]:
        for capacity in sorted(self._available):
            if capacity >= minimum_capacity and self._available[capacity]:
                block = self._available[capacity].pop()
                if not self._available[capacity]:
                    del self._available[capacity]
                block.state = _IN_USE
                return block
        return None

    def _find_pending_locked(self, minimum_capacity: int) -> Optional[_Block]:
        for capacity in sorted(self._pending):
            if capacity >= minimum_capacity and self._pending[capacity]:
                block = self._pending[capacity].pop(0)
                if not self._pending[capacity]:
                    del self._pending[capacity]
                block.state = _IN_USE
                return block
        return None

    def acquire(self, size: int) -> Any:
        """Acquire a pooled pinned CPU ``uint8`` view."""
        _valid_positive_int(size, "size")
        aligned_size = self._aligned_size(size)

        with self._lock:
            self._reclaim_completed_locked(aligned_size)
            block = self._find_available_locked(aligned_size)
            if block is not None:
                try:
                    return self._checkout_locked(block, size)
                except Exception:
                    block.state = _AVAILABLE
                    self._available[block.capacity].append(block)
                    raise

            if self._total_allocated + aligned_size <= self._max_host_bytes:
                self._total_allocated += aligned_size
                reserved = True
            else:
                reserved = False
                block = self._find_pending_locked(aligned_size)
                if block is None:
                    raise RuntimeError(
                        "PinnedMemoryPool capacity exceeded: "
                        f"requested_bytes={size}, aligned_bytes={aligned_size}, "
                        f"pooled_bytes={self._total_allocated}, "
                        f"max_host_bytes={self._max_host_bytes}."
203
204
205
206
207
208
209
210
211
212
213
                        f"pooled_bytes={self._total_allocated}, "
                        f"max_host_bytes={self._max_host_bytes}."
                    )

        if reserved:
            try:
                buffer = _backend.alloc_tensor_buffer(
                    aligned_size,
                    torch.uint8,
                    device="cpu",
                    pin_memory=True,
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
                    torch.uint8,
                    device="cpu",
                    pin_memory=True,
                )
            except Exception:
                with self._lock:
                    self._total_allocated -= aligned_size
                raise
            block = _Block(buffer, aligned_size, _IN_USE)
            try:
                with self._lock:
                    view = self._checkout_locked(block, size)
            except Exception:
                with self._lock:
                    self._total_allocated -= aligned_size
                raise
            return view

        event = block.event
        try:
            event.synchronize()
        except Exception:
            with self._lock:
                block.state = _PENDING
                block.event = event
                self._pending[block.capacity].insert(0, block)
            raise
        block.event = None
        try:
            with self._lock:
                return self._checkout_locked(block, size)
        except Exception:
            with self._lock:
                block.state = _AVAILABLE
                self._available[block.capacity].append(block)
            raise

    def release(self, tensor: Any, event: Optional[Any] = None) -> None:
        """Return an acquired view to the pool, optionally after an async event."""
        key = _storage_key(tensor)
        with self._lock:
            block = self._blocks.get(key)
            if block is None:
                raise ValueError("The tensor does not belong to this PinnedMemoryPool.")
            if block.state != _IN_USE:
                raise ValueError("The tensor has already been released to this PinnedMemoryPool.")
            if event is None:
                block.state = _AVAILABLE
                self._available[block.capacity].append(block)
            else:
                block.state = _PENDING
                block.event = event
                self._pending[block.capacity].append(block)
hyper_parallel/core/activation_memory/recompute_state.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
    """Own resources whose lifetime is one checkpoint invocation."""

    def __init__(self) -> None:
        """Initialize an empty invocation resource registry."""
        self.identity = object()
        self.resources: Dict[object, Any] = {}

    def get_resource(self, key: object, factory: Callable[[], Any]) -> Any:
        """Get or create one invocation-local resource."""
        if key not in self.resources:
            self.resources[key] = factory()
        return self.resources[key]

    def clear(self) -> None:
        """Release invocation resources, including partially consumed caches."""
        for resource in self.resources.values():
            clear = getattr(resource, "clear", None)
            if clear is not None:
                clear()
        self.resources.clear()


class RecomputeState:
    """Describe the current checkpoint invocation and execution phase."""
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
    """Describe the current checkpoint invocation and execution phase."""

    def __init__(self, invocation: _RecomputeInvocation, recomputing: bool) -> None:
        """Initialize state for one phase of a checkpoint invocation."""
        self._invocation = invocation
        self.is_recomputing = recomputing

    @property
    def invocation_id(self) -> object:
        """Return an identity that is stable across forward and recomputation."""
        return self._invocation.identity

    def get_resource(self, key: object, factory: Callable[[], Any]) -> Any:
        """Get an invocation-local resource shared by both execution phases."""
        return self._invocation.get_resource(key, factory)

    def _clear_resources(self) -> None:
        """Release all resources owned by this checkpoint invocation."""
        self._invocation.clear()


_CURRENT_RECOMPUTE_STATE: contextvars.ContextVar[Optional[RecomputeState]] = contextvars.ContextVar(
    "hyper_parallel_recompute_state",
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
    """Install one checkpoint execution phase in the current dynamic scope."""

    def __init__(self, state: RecomputeState) -> None:
        """Initialize a context for the supplied execution state."""
        self._state = state
        self._token = None

    def __enter__(self) -> "_RecomputeContext":
        """Expose this phase as the current recompute state."""
        self._token = _CURRENT_RECOMPUTE_STATE.set(self._state)
        return self

    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> bool:
        """Restore the outer state and clear resources after recomputation."""
        if self._token is not None:
            _CURRENT_RECOMPUTE_STATE.reset(self._token)
        if self._state.is_recomputing or exc_type is not None:
            self._state._clear_resources()  # pylint: disable=protected-access
        return False


def get_recompute_state() -> Optional[RecomputeState]:
    """Return the current checkpoint execution state, if one is active."""
    return _CURRENT_RECOMPUTE_STATE.get()


def is_recomputing() -> bool:
    """Return whether the current dynamic scope is replaying a checkpoint."""
    state = get_recompute_state()
    return state is not None and state.is_recomputing


def create_recompute_contexts() -> Tuple[_RecomputeContext, _RecomputeContext]:
    """Create forward and recompute contexts for one checkpoint invocation."""
    invocation = _RecomputeInvocation()
    return (
        _RecomputeContext(RecomputeState(invocation, recomputing=False)),
        _RecomputeContext(RecomputeState(invocation, recomputing=True)),
    )
hyper_parallel/core/activation_memory/sac.py
36
37
38
39
40
41
42
43
44
45
46
47
def _is_compiling(func, args, kwargs):
    # Check if we are under AOTAutograd tracing
    # There should probably be a better way to do this...
    # NOTE: unify _is_compiling across all compile stacks
    for arg in args:
        if isinstance(arg, torch.Tensor) and is_fun(arg):
            return True
    return False


class _VersionWrapper:
    # Check that cached tensors are not mutated.
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

class _VersionWrapper:
    # Check that cached tensors are not mutated.
    def __init__(self, val):
        self.val: Union[torch.Tensor, Any] = val
        self.version: Optional[int] = (
            val._version if isinstance(val, torch.Tensor) else None
        )

    def get_val(self, allow_cache_entry_mutation):
        if self.version is not None and not allow_cache_entry_mutation:
            if self.val._version != self.version:
                # Can we give user a stack trace of where the mutation happened?
                raise RuntimeError(
                    "Tensor cached during selective activation checkpoint has been mutated"
                )
        return self.val


class _SwapCacheEntry:
    """Pair the recompute cache and swap record around the same tensor object."""
63
64
65
66
67
68
69
70
71
72

class _SwapCacheEntry:
    """Pair the recompute cache and swap record around the same tensor object."""
    def __init__(self, val, funcname, group_swap=False, cpu_pool=None):
        self.save = _VersionWrapper(val)
        self.swap = SwapTensor(val, funcname, group_swap=group_swap, cpu_pool=cpu_pool)


def _maybe_detach(x, any_ret_has_alias_info):
    # We detach for two separate reasons:
75
76
77
78
79
80
81
82
83
84
85
86
    # - Avoid reference cycles
    # For case 1, it is not enough to check whether x has differentiable dtype
    # because non-differentiable dtype can have non-nullptr AutogradMeta, e.g.
    # when the tensor is a view.
    need_detach = (isinstance(x, torch.Tensor)
                   and (x.is_floating_point() or x.is_complex() or any_ret_has_alias_info))
    if need_detach:
        with torch._C._SetExcludeDispatchKeyGuard(torch._C.DispatchKey.ADInplaceOrView, False):
            # Ensure that view performed beneath autograd properly propagates
            # version counter. TODO: Use reentrant_dispatch instead of
            # manually manipulating dispatch keys. Using reentrant_dispatch
            # would respect inference_mode, though that is not relevant for
84
85
86
87
88
89
90
91
92
93
            # version counter. TODO: Use reentrant_dispatch instead of
            # manually manipulating dispatch keys. Using reentrant_dispatch
            # would respect inference_mode, though that is not relevant for
            # this case.
            x = x.detach()
    return x


class SelectiveCheckpointContext:
    """
111
112
113
114
115
116
117
118
119
120
121
122
123
124
        >>>     context_fn=context_fn,
        >>> )
    """
    def __init__(self, *, is_recompute):
        self.is_recompute = is_recompute


def _policy_from_bool(b):
    # For backward compatibility
    return CheckpointPolicy.MUST_SAVE if b else CheckpointPolicy.PREFER_RECOMPUTE


SAC_IGNORED_OPS = {
    # AC inserts different number of detach during forward and recompute.
136
137
138
139
140
141
142
143
144
    Args:
        ops (List[Optional[object]]): Operators to execute without selective-AC replay accounting.
            ``None`` entries are ignored for optional-version compatibility.
    """
    SAC_IGNORED_OPS.update(op for op in ignore_ops if op is not None)


class _CachingTorchDispatchMode(TorchDispatchMode):
    # Used together with _CachedTorchDispatchMode to implement SAC.
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

class _CachingTorchDispatchMode(TorchDispatchMode):
    # Used together with _CachedTorchDispatchMode to implement SAC.
    def __init__(self, policy_fn, swap_storage, storage, group_swap=False, cpu_pool=None):
        self.policy_fn = policy_fn
        self.swap_storage = swap_storage
        self.storage = storage
        self.add_to_storage = False
        self.group_swap = group_swap
        self.cpu_pool = cpu_pool
        # Cache context and singleton to avoid per-dispatch allocation / lookup.
        self._swap_manager = SwapManager()
        self._group_prefix = ""

    def __torch_dispatch__(self, func, types, args=(), kwargs=None):
        if func in SAC_IGNORED_OPS:
            return func(*args, **kwargs)

        kwargs = {} if kwargs is None else kwargs
        policy = self.policy_fn(SelectiveCheckpointContext(is_recompute=False),
                                func, *args, **kwargs)
        if isinstance(policy, bool):
            policy = _policy_from_bool(policy)

        is_compiling = _is_compiling(func, args, kwargs)

        if is_compiling:
            # Overwrite each node's "recompute" tag to add in the user annotation.
            fx_traceback.current_meta["recompute"] = policy

        out = func(*args, **kwargs)

        has_alias = any(ret.alias_info is not None for ret in func._schema.returns)

        if policy in (CheckpointPolicy.MUST_SAVE, CheckpointPolicy.PREFER_SAVE):
            self.storage[func].append(
                tree_map(lambda x: _VersionWrapper(_maybe_detach(x, has_alias)), out)
            )
        elif policy == CheckpointPolicy.MUST_SWAP:  # patch code
            if not self.add_to_storage:
                group_name = self._swap_manager.get_current_group_name()
                self._group_prefix = f"{group_name}::"
                self._swap_manager.add_storage(group_name, self.swap_storage)
                self.add_to_storage = True
            funcname = f"{self._group_prefix}{func}"
            group_swap = self.group_swap
            entries = tree_map(
                lambda x: _SwapCacheEntry(
                    _maybe_detach(x, has_alias), funcname, group_swap=group_swap, cpu_pool=self.cpu_pool
                ), out,
            )
            self.storage[func].append(tree_map(lambda x: x.save, entries))
            self.swap_storage[func].append(tree_map(lambda x: x.swap, entries))
        elif policy != CheckpointPolicy.MUST_RECOMPUTE:
            raise RuntimeError(f"Checkpoint Activation: {func} encountered an invalid policy {policy}")
        return out


class _CachedTorchDispatchMode(TorchDispatchMode):
    # Used together with _CachingTorchDispatchMode to implement SAC.
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243

class _CachedTorchDispatchMode(TorchDispatchMode):
    # Used together with _CachingTorchDispatchMode to implement SAC.
    def __init__(self, policy_fn, swap_storage, storage, allow_cache_entry_mutation):
        self.policy_fn = policy_fn
        self.swap_storage = swap_storage
        self.storage = storage
        self.allow_cache_entry_mutation = allow_cache_entry_mutation
        self._swap_cleared = False

    def __torch_dispatch__(self, func, types, args=(), kwargs=None):
        if func in SAC_IGNORED_OPS:
            return func(*args, **kwargs)

        kwargs = {} if kwargs is None else kwargs
        policy = self.policy_fn(SelectiveCheckpointContext(is_recompute=True),
                                func, *args, **kwargs)
        if isinstance(policy, bool):
            policy = _policy_from_bool(policy)

        is_compiling = _is_compiling(func, args, kwargs)

        if not self._swap_cleared:
            self.swap_storage.clear()
            self._swap_cleared = True

        # MUST_SAVE, PREFER_SAVE, and MUST_SWAP all restore from storage identically.
        if (policy in (CheckpointPolicy.MUST_SAVE, CheckpointPolicy.PREFER_SAVE, CheckpointPolicy.MUST_SWAP)
           or is_compiling):
            storage = self.storage.get(func)  # patch code
            if storage is None:
                raise RuntimeError(f"{func} encountered during backward, but not found in storage")
            if len(storage) == 0:
                raise RuntimeError(
                    "Trying to backward an extra time. You are only allowed to backward once "
                    "on any region computed under selective activation checkpoint."
                )
            out = tree_map(lambda x: x.get_val(self.allow_cache_entry_mutation), storage.pop(0))
        else:
            out = func(*args, **kwargs)
        return out


def create_selective_checkpoint_contexts(
    policy_fn_or_list, allow_cache_entry_mutation=False, group_swap=False, cpu_pool=None
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
        >>> )
    """
    # NB: If grad_mode is disabled, checkpoint would not run forward under
    #     context_fn anyway, so proceed as usual.
    if policy_fn_or_list is None:
        def policy_fn(_ctx, _op, *_args, **_kwargs):
            return CheckpointPolicy.PREFER_RECOMPUTE
    elif isinstance(policy_fn_or_list, list):
        for op in policy_fn_or_list:
            if not isinstance(op, torch._ops.OpOverload):
                _extra_msg = (
                    "Please update the OpOverloadPacket to a specific OpOverload."
                    "For example, if you have `torch.ops.aten.mm`, change it to `torch.ops.aten.mm.default`."
                ) if isinstance(op, torch._ops.OpOverloadPacket) else ""
                raise ValueError(
                    f"Expected op in `op_list` to be an OpOverload but got: {op} "
                    f"of type {type(op)}. {_extra_msg}"
                )

        def policy_fn(ctx, op, *args, **kwargs):
            if op in policy_fn_or_list:
                return CheckpointPolicy.MUST_SAVE
            else:
                return CheckpointPolicy.PREFER_RECOMPUTE
    elif callable(policy_fn_or_list):
        policy_fn = policy_fn_or_list
    else:
        raise TypeError("policy_fn_or_list must be either a function or a list of ops.")

    swap_storage = Storage()  # patch code
    storage: Dict[Any, List[Any]] = defaultdict(list)
    return (
        _CachingTorchDispatchMode(
            policy_fn, swap_storage, storage, group_swap=group_swap, cpu_pool=cpu_pool
        ),
        _CachedTorchDispatchMode(policy_fn, swap_storage, storage, allow_cache_entry_mutation),
hyper_parallel/core/activation_memory/swap.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
    ``buf[:total_numel]`` for the actual copy so the returned reference can be
    passed back to :func:`_return_cpu_pinned_buf` without any platform-specific
    introspection.
    """
    pool = _CPU_PINNED_POOL[dtype_key]
    best_i = -1
    for i, buf in enumerate(pool):
        if buf.numel() >= total_numel:
            if best_i == -1 or buf.numel() < pool[best_i].numel():
                best_i = i
    if best_i != -1:
        return pool.pop(best_i)
    # No suitable buffer — discard one stale undersized entry.
    if pool:
        pool.pop()
    return _backend.alloc_tensor_buffer(total_numel, dtype, device='cpu', pin_memory=True)


def _return_cpu_pinned_buf(buf):
    """Return a full pinned CPU buffer to the pool for reuse."""
    if buf is None:
        return
    _CPU_PINNED_POOL[str(buf.dtype)].append(buf)


def _collect_device_storage_ptrs(tensors: Any) -> Set[int]:
    """Collect device storage pointers from a nested tensor structure."""
    storage_ptrs = set()

    def _collect(x):
        local_tensor = x.to_local() if isinstance(x, DTensor) else x
        if isinstance(local_tensor, _backend.Tensor) and str(local_tensor.device).lower() != "cpu":
            storage_ptrs.add(local_tensor.untyped_storage().data_ptr())
        return x

    _backend.tree_map(_collect, tensors)
    return storage_ptrs


class SwapTensor:
    """A tensor that can be swapped between device and host memory asynchronously."""
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
    STATE_H2D = "h2d"
    STATE_NON_TENSOR = "non_tensor"

    def __init__(self, val: Any, funcname: str, group_swap: bool = False, cpu_pool=None) -> None:
        self.val = val
        self.funcname = funcname
        self._keep_on_device = False
        self._duplicate_swap = False
        self._group_managed = False # True when this tensor is handled by SwapGroup bulk copy
        self.group_swap = group_swap # opt-in for group copy fusion (MUST_SWAP tensors only)
        self.cpu_pool = cpu_pool
        self._cpu_pool_buffer = None
        if isinstance(val, _backend.Tensor) and str(val.device).lower() != 'cpu':
            self.ver = val._version
            self._state = self.STATE_DEVICE
            val_storage = val.untyped_storage()
            self.storage_size = val_storage.size()
            self.is_slice_tensor = self.storage_size != val.numel() * _backend.get_element_size(val)
            self.val_cpu = None
        else:
            self.ver = None
            self._state = self.STATE_NON_TENSOR
            self.val_cpu = None
            self.is_slice_tensor = False
            self.storage_size = 0

    def dedup_key(self):
        """Return a stable identity key for duplicate-swap detection."""
        if self._state == self.STATE_NON_TENSOR:
            return None
        val_storage = self.val.untyped_storage()
        return (
            str(self.val.device),
            val_storage.data_ptr(),
            self.val.storage_offset(),
            val_storage.size(),
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
        )

    def mark_duplicate_swap(self) -> None:
        """Mark this wrapper as a duplicate registration in the same swap group."""
        self._duplicate_swap = True

    def protect_if_aliases(self, alias_storage_ptrs: Set[int]) -> None:
        """Keep tensors that alias externally-owned tensors on device."""
        if self._state == self.STATE_NON_TENSOR:
            return
        if self.val.untyped_storage().data_ptr() in alias_storage_ptrs:
            self._keep_on_device = True

    def get_val(self) -> Any:
        """Return the underlying tensor value.
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327

        Raises RuntimeError if the tensor is not currently in the 'device' state.
        Non-tensor values are returned directly regardless of state.
        """
        if self._state == self.STATE_NON_TENSOR:
            return self.val
        if self._state != self.STATE_DEVICE:
            raise RuntimeError(
                f"Cannot call get_val(): tensor is in '{self._state}' state. "
                f"Must be in 'device' state."
            )
        return self.val

    def resize_device_storage(self):
        """Reallocate device memory on compute stream."""
        if self._state == self.STATE_NON_TENSOR or self._duplicate_swap:
            return

        if self._state != self.STATE_HOST:
            return
        storage = self.val.untyped_storage()
        if storage.size() == self.storage_size:
            return
        storage.resize_(self.storage_size)

    def async_load(self):
        """async load tensor from host to device"""
        if self._state == self.STATE_NON_TENSOR or self._keep_on_device or self._duplicate_swap:
            return
        if self._group_managed:
            return

        if self._state != self.STATE_HOST:
            warnings.warn(
                f"[SwapTensor.async_load] Invalid state: current={self._state}, "
                f"expected 'host'. Operation skipped."
            )
            return

        if self.val_cpu is None:
            raise ValueError("val_cpu must not be None during async_load")
        with _backend.preserve_version_counter(self.val):
            if self.cpu_pool is not None or self.is_slice_tensor:
                self.val.data.copy_(self.val_cpu, non_blocking=True)
            else:
                self.val.untyped_storage().copy_(self.val_cpu.untyped_storage(), non_blocking=True)
        self._state = self.STATE_H2D

    def async_group_load(self, source):
        """Copy a packed device-buffer slice back into the original storage."""
        if self._state == self.STATE_NON_TENSOR or self._keep_on_device or self._duplicate_swap:
            return
        if not self._group_managed:
            return
        if self._state != self.STATE_HOST:
            warnings.warn(
                f"[SwapTensor.async_group_load] Invalid state: current={self._state}, "
                f"expected 'host'. Operation skipped."
            )
            return
        if self.val.untyped_storage().size() != self.storage_size:
            raise RuntimeError(
                f"Cannot load grouped tensor from {self.funcname}: device storage was not restored. "
                f"expected size:{self.storage_size}, current size:{self.val.untyped_storage().size()}"
            )
        with _backend.preserve_version_counter(self.val):
            self.val.copy_(source.reshape(self.val.shape), non_blocking=True)
        self._state = self.STATE_H2D

    def release_cpu_buffer(self, event=None):
        """Release an explicitly pooled host buffer exactly once."""
        if self.cpu_pool is None or self._cpu_pool_buffer is None:
            return
        release_tensor = self.val_cpu if self.val_cpu is not None else self._cpu_pool_buffer
        self.cpu_pool.release(release_tensor, event=event)
        self._cpu_pool_buffer = None
        self.val_cpu = None

    def wait_load(self, release_event=None):
        """change state to device after async load is done"""
        if self._state == self.STATE_NON_TENSOR or self._keep_on_device or self._duplicate_swap:
            return

        if self._state == self.STATE_DEVICE:
            return  # already loaded
        if self._state != self.STATE_H2D:
            warnings.warn(
                f"[SwapTensor.wait_load] Called in invalid state: {self._state}. "
                f"Expected 'h2d'. Skipped."
            )
            return
        self._state = self.STATE_DEVICE
        self.release_cpu_buffer(release_event)
        if self.cpu_pool is None:
            self.val_cpu = None

    def async_offload(self):
        """async offload tensor from device to host"""
        if self._state == self.STATE_NON_TENSOR or self._keep_on_device or self._duplicate_swap:
            return
        if self._group_managed:
            return

        if self._state != self.STATE_DEVICE:
            warnings.warn(
                f"[SwapTensor.async_offload] Invalid state: current={self._state}, "
                f"expected 'device'. Operation skipped."
            )
            return

        if self.storage_size != self.val.untyped_storage().size():
            raise RuntimeError(
                f"There is a tensor from {self.funcname} cannot be SWAPPED! Its storage has been resized "
                f"presize:{self.storage_size}, current size:{self.val.untyped_storage().size()}"
            )
        if self.ver != self.val._version:
            raise RuntimeError(
                f"There is a tensor from {self.funcname} cannot be SWAPPED! In-place modification happened "
                f"preversion:{self.ver}, current version:{self.val._version}"
            )

        if self.val_cpu is None:
            if self.cpu_pool is None:
                self.val_cpu = _backend.empty_like(
                    self.val, device="cpu", pin_memory=True
                )
            else:
                logical_bytes = self.val.numel() * _backend.get_element_size(self.val)
                self._cpu_pool_buffer = self.cpu_pool.acquire(logical_bytes)
                try:
                    self.val_cpu = self._cpu_pool_buffer.view(self.val.dtype).reshape(self.val.shape)
                except Exception:
                    self.release_cpu_buffer()
                    raise
        try:
            if self.cpu_pool is not None or self.is_slice_tensor:
                self.val_cpu.copy_(self.val, non_blocking=True)
            else:
                self.val_cpu.untyped_storage().copy_(self.val.untyped_storage(), non_blocking=True)
        except Exception:
            if self.cpu_pool is not None and self._cpu_pool_buffer is not None:
                release_event = _backend.new_event()
                release_event.record(_backend.get_current_stream())
                self.release_cpu_buffer(release_event)
            self.val_cpu = None
            raise
        self._state = self.STATE_D2H

    def wait_offload(self):
        """wait offload to host and free device memory"""
        if self._state == self.STATE_NON_TENSOR or self._keep_on_device or self._duplicate_swap:
            return

        if self._state == self.STATE_HOST:
            return
        if self._state != self.STATE_D2H:
            warnings.warn(
                f"[SwapTensor.wait_offload] Called in invalid state: {self._state}. "
                f"Expected 'd2h'. Skipped."
            )
            return
        storage = self.val.untyped_storage()
        if storage.size() != 0:
            storage.resize_(0)
        self._state = self.STATE_HOST

    @property
    def state(self) -> str:
        """Return the current swap state of this tensor (device, host, d2h, h2d, or non_tensor)."""
        return self._state

    def __repr__(self):
        if self._state == self.STATE_NON_TENSOR:
            return f"<SwapTensor state=non_tensor, val_type={type(self.val).__name__}>"
        return (
            f"<SwapTensor state={self._state}, duplicate={self._duplicate_swap}, "
            f"device_val={'exists' if self.val is not None else 'None'}>"
        )
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
    ``for batch in storage.values(): ...``.
    """

    def __init__(self):
        self._data: Dict[Any, List[Any]] = defaultdict(list)

    def __getitem__(self, key: Any) -> List[Any]:
        return self._data[key]

    def values(self):
        """Return an iterable view of all stored lists."""
        return self._data.values()

    def clear(self):
        """Remove all entries from the storage."""
        self._data.clear()

    def iter_swap_tensors(self):
        """Iterate all SwapTensor objects stored in this storage."""
        collected = []

        def _collect(x):
            if isinstance(x, SwapTensor):
                collected.append(x)
            return x

        for storage_list in self.values():
            for item in storage_list:
                _backend.tree_map(_collect, item)
        return collected

    def mark_duplicate_swaps(self, seen_keys) -> int:
        """Mark tensors already registered in the same swap group as duplicates."""
        duplicate_count = 0
        for swap_tensor in self.iter_swap_tensors():
            dedup_key = swap_tensor.dedup_key()
            if dedup_key is None:
                continue
            if dedup_key in seen_keys:
                swap_tensor.mark_duplicate_swap()
                duplicate_count += 1
                continue
            seen_keys.add(dedup_key)
        return duplicate_count

    def protect_alias_storage_ptrs(self, alias_storage_ptrs: Set[int]):
        """Avoid offloading swap entries that alias externally-owned storage."""
        if not alias_storage_ptrs:
            return

        def _protect_tensor(x):
            if isinstance(x, SwapTensor):
                x.protect_if_aliases(alias_storage_ptrs)
            return x

        for storage_list in self.values():
            for item in storage_list:
                _backend.tree_map(_protect_tensor, item)

    def launch_load(self):
        """launch async load for all tensors in swap storage"""
        def _async_load(x):
            if isinstance(x, SwapTensor):
                x.async_load()
            return x

        for storage_list in self.values():
            for item in storage_list:
                _backend.tree_map(_async_load, item)

    def resize_device_storage(self):
        """Resize device storage for all swap tensors (runs on compute stream)."""
        def _resize(x):
            if isinstance(x, SwapTensor):
                x.resize_device_storage()
            return x
        for storage_list in self.values():
            for item in storage_list:
                _backend.tree_map(_resize, item)

    def wait_load(self, release_event=None):
        """wait load for all tensors in swap storage"""
        def _wait_load(x):
            if isinstance(x, SwapTensor):
                x.wait_load(release_event=release_event)
            return x

        for storage_list in self.values():
            for item in storage_list:
                _backend.tree_map(_wait_load, item)
        self.clear()

    def release_cpu_buffers(self, event=None):
        """Release all explicitly pooled host buffers held by this storage."""
        def _release(x):
            if isinstance(x, SwapTensor):
                x.release_cpu_buffer(event=event)
            return x

        for storage_list in self.values():
            for item in storage_list:
                _backend.tree_map(_release, item)

    def wait_offload(self):
        """wait offload for all tensors in swap storage"""
        def _wait_offload(x):
            if isinstance(x, SwapTensor):
                x.wait_offload()
            return x

        for storage_list in self.values():
            for item in storage_list:
                _backend.tree_map(_wait_offload, item)

    def launch_offload(self):
        """launch async offload for all tensors in swap storage"""
        def _async_offload(x):

            if isinstance(x, SwapTensor):
                x.async_offload()
            return x

        for storage_list in self.values():
            for item in storage_list:
                _backend.tree_map(_async_offload, item)


class SwapGroup:
    """Manager for a group of storages to coordinate swap operations.
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
    per-tensor copy path.
    """

    def __init__(self, group_name: str):
        self.group_name = group_name
        self.is_last_group: bool = False
        self._storages: List[Storage] = []
        self._load_event: Optional[Any] = None
        self._offload_event: Optional[Any] = None
        # Group-level contiguous buffers for non-slice tensors.
        self._packed_tensor_info: List = []   # [(SwapTensor, bucket_key, element_offset), ...]
        self._packed_buckets: Dict[str, Dict[str, Any]] = {}
        self._group_cpu_buf = None            # pinned CPU bufs; live offload→load
        self._group_device_buf = None         # temp device bufs; cleared after each phase
        # Persistent dedup set accumulated across add() calls; avoids O(N²) rebuild.
        # mark_duplicate_swaps mutates it in-place, so new keys are added automatically.
        # Reset at wait_load() so stale data_ptrs don't leak into the next iteration.
        self._seen_dedup_keys: set = set()
        # Per-bucket SwapTensor lists built in _collect_packable_tensors and consumed
        # in launch_offload, eliminating a redundant pass over _packed_tensor_info.
        self._packed_by_bucket: Dict[str, List] = {}

    def add(self, storage):
        """Add a storage to the swap group."""
        duplicate_count = storage.mark_duplicate_swaps(self._seen_dedup_keys)
        if duplicate_count > 0:
            warnings.warn(
                f"SwapGroup '{self.group_name}' skipped {duplicate_count} duplicate tensor swap registration(s)."
            )
        self._storages.append(storage)

    def protect_alias_tensors(self, tensors: Any):
        """Protect externally-owned tensors from premature offload."""
        alias_storage_ptrs = _collect_device_storage_ptrs(tensors)
        if not alias_storage_ptrs:
            return
        for storage in self._storages:
            storage.protect_alias_storage_ptrs(alias_storage_ptrs)

    def _collect_packable_tensors(self) -> int:
        """Identify tensors eligible for group packing and mark them for bulk copy.
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564

        Returns:
            Total byte count of all packable tensors.
        """
        candidate_buckets: Dict[str, List[Dict[str, Any]]] = {}
        packed_info: List = []
        packed_buckets: Dict[str, Dict[str, Any]] = {}
        packed_by_bucket: Dict[str, List] = {}
        total_bytes = 0

        def _try_pack(x):
            if not isinstance(x, SwapTensor):
                return x
            no_pack = (not x.group_swap or x._state != SwapTensor.STATE_DEVICE or x._keep_on_device
                       or x.is_slice_tensor or x._duplicate_swap or x.storage_size >= _GROUP_SWAP_MAX_BULK_COPY_BYTES
                       or not x.val.is_contiguous())
            if no_pack:
                return x
            if x.storage_size != x.val.untyped_storage().size():
                raise RuntimeError(
                    f"There is a tensor from {x.funcname} cannot be SWAPPED! Its storage has been resized "
                    f"presize:{x.storage_size}, current size:{x.val.untyped_storage().size()}"
                )
            if x.ver != x.val._version:
                raise RuntimeError(
                    f"There is a tensor from {x.funcname} cannot be SWAPPED! In-place modification happened "
                    f"preversion:{x.ver}, current version:{x.val._version}"
                )
            dtype_key = (str(x.val.dtype), id(x.cpu_pool))
            dtype_buckets = candidate_buckets.setdefault(dtype_key, [])
            if (not dtype_buckets or
                    dtype_buckets[-1]["total_bytes"] + x.storage_size > _GROUP_SWAP_MAX_BULK_COPY_BYTES):
                dtype_buckets.append({
                    "bucket_key": f"{dtype_key}#{len(dtype_buckets)}",
                    "dtype": x.val.dtype,
                    "dtype_key": str(x.val.dtype),
                    "device": x.val.device,
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
                    "total_bytes": 0,
                    "total_numel": 0,
                    "cpu_pool": x.cpu_pool,
                })
            bucket = dtype_buckets[-1]
            bucket["tensors"].append(x)
            bucket["total_bytes"] += x.storage_size
            bucket["total_numel"] += x.val.numel()
            return x

        for storage in self._storages:
            for storage_list in storage.values():
                for item in storage_list:
                    _backend.tree_map(_try_pack, item)

        for dtype_bucket_list in candidate_buckets.values():
            for candidate_bucket in dtype_bucket_list:
                tensors = candidate_bucket["tensors"]
                if len(tensors) < 2:
                    continue
                bucket_key = candidate_bucket["bucket_key"]
                packed_buckets[bucket_key] = {
                    "dtype": candidate_bucket["dtype"],
                    "dtype_key": candidate_bucket["dtype_key"],
                    "device": candidate_bucket["device"],
                    "total_numel": candidate_bucket["total_numel"],
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
                    "total_numel": candidate_bucket["total_numel"],
                    "total_bytes": candidate_bucket["total_bytes"],
                    "cpu_pool": candidate_bucket["cpu_pool"],
                }
                element_offset = 0
                for tensor in tensors:
                    tensor._group_managed = True
                    tensor._state = SwapTensor.STATE_D2H
                    packed_info.append((tensor, bucket_key, element_offset))
                    element_offset += tensor.val.numel()
                packed_by_bucket[bucket_key] = tensors
                total_bytes += candidate_bucket["total_bytes"]

        self._packed_tensor_info = packed_info
        self._packed_buckets = packed_buckets
        self._packed_by_bucket = packed_by_bucket
        return total_bytes

    def launch_offload(self, copy_stream):
        """Launch async offload for all storages in the group.
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
        Non-slice tensors are first packed into bounded contiguous device
        buffers, then transferred to pinned CPU memory.  Slice tensors are
        offloaded individually via the existing per-tensor path.
        """
        total_bytes = self._collect_packable_tensors()
        with _backend.no_grad():
            if total_bytes > 0:
                group_device_bufs = {}
                group_cpu_bufs = {}
                for bucket_key, swap_tensors in self._packed_by_bucket.items():
                    group_device_bufs[bucket_key] = _backend.cat(
                        [st.val.reshape(-1) for st in swap_tensors], dim=0
                    )

        compute_event = _backend.new_event()
        compute_event.record(_backend.get_current_stream())
        self._offload_event = _backend.new_event()
        stream_context = _backend.get_stream_context()
        with _backend.no_grad(), stream_context(copy_stream):
            compute_event.wait(copy_stream)

            if total_bytes > 0:
                # One-shot D2H per packed bucket. MindSpore requires tensor/storage dtype consistency.
                try:
                    for bucket_key, bucket in self._packed_buckets.items():
                        dtype_key = bucket["dtype_key"]
                        numel = bucket["total_numel"]
                        cpu_pool = bucket["cpu_pool"]
                        if cpu_pool is None:
                            cpu_buf = _get_cpu_pinned_buf(dtype_key, numel, bucket["dtype"])
                        else:
                            raw_buf = cpu_pool.acquire(bucket["total_bytes"])
                            try:
                                cpu_buf = raw_buf.view(bucket["dtype"])
                            except Exception:
                                cpu_pool.release(raw_buf)
                                raise
                        group_cpu_bufs[bucket_key] = cpu_buf
                        cpu_buf[:numel].copy_(group_device_bufs[bucket_key], non_blocking=True)
                except Exception:
                    release_event = _backend.new_event()
                    release_event.record(copy_stream)
                    for bucket_key, cpu_buf in group_cpu_bufs.items():
                        bucket = self._packed_buckets[bucket_key]
                        if bucket["cpu_pool"] is not None:
                            bucket["cpu_pool"].release(cpu_buf, event=release_event)
                        else:
                            _return_cpu_pinned_buf(cpu_buf)
                    raise
                self._group_device_buf = group_device_bufs
                self._group_cpu_buf = group_cpu_bufs

            # Slice tensors use the existing per-tensor path.
            # Group-managed tensors are already STATE_D2H so async_offload is a no-op.
            for storage in self._storages:
                storage.launch_offload()
            self._offload_event.record(copy_stream)

    def wait_offload(self):
        """Wait for offload to complete for all storages in the group."""
        if self._offload_event is None:
            raise RuntimeError(
                f"SwapGroup '{self.group_name}' wait_offload() called before launch_offload()."
            )
        compute_stream = _backend.get_current_stream()
        stream_context = _backend.get_stream_context()
        with _backend.no_grad(), stream_context(compute_stream):
            self._offload_event.wait(compute_stream)
            self._offload_event = None
            for storage in self._storages:
                storage.wait_offload()
        # Release the temporary device packing buffer; _group_cpu_buf persists until launch_load.
        self._group_device_buf = None

    def launch_load(self, copy_stream):
        """Prepare storage and launch async load for all storages in the group.
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
        original storages.  Slice tensors use the existing per-tensor path.
        """
        # Restore original storages before scheduling copies. Keeping the same
        # storage object is required for autograd-saved views of packed tensors.
        with _backend.no_grad():
            for storage in self._storages:
                storage.resize_device_storage()

        compute_event = _backend.new_event()
        compute_event.record(_backend.get_current_stream())
        self._load_event = _backend.new_event()
        stream_context = _backend.get_stream_context()
        with _backend.no_grad(), stream_context(copy_stream):
            compute_event.wait(copy_stream)

            if self._packed_tensor_info and self._group_cpu_buf is not None:
                group_device_bufs = {}
                for bucket_key, bucket in self._packed_buckets.items():
                    cpu_buf = self._group_cpu_buf.get(bucket_key)
                    if cpu_buf is None:
                        continue
                    numel = bucket["total_numel"]
                    group_device_bufs[bucket_key] = _backend.alloc_tensor_buffer(
                        numel, bucket["dtype"], bucket["device"]
                    )
                    # One-shot H2D per packed bucket.
                    group_device_bufs[bucket_key].copy_(cpu_buf[:numel], non_blocking=True)
                self._group_device_buf = group_device_bufs

                # Unpack with D2D copies into the original storages. Rebinding
                # st.val with set_() would leave existing aliases on freed storage.
                for st, bucket_key, element_offset in self._packed_tensor_info:
                    group_device_buf = group_device_bufs.get(bucket_key)
                    if group_device_buf is None:
                        continue
                    source = group_device_buf[element_offset:element_offset + st.val.numel()]
                    st.async_group_load(source)

            # Slice tensors use the existing per-tensor path.
            # Group-managed tensors skip async_load via _group_managed flag.
            for storage in self._storages:
                storage.launch_load()    # Only copy, no resize
            self._load_event.record(copy_stream)

    def release_cpu_buffers(self, event=None):
        """Release staging buffers immediately or defer until ``event`` completes."""
        if self._group_cpu_buf is None:
            return
        for bucket_key, buf in self._group_cpu_buf.items():
            bucket = self._packed_buckets.get(bucket_key)
            if bucket is not None and bucket["cpu_pool"] is not None:
                bucket["cpu_pool"].release(buf, event=event)
            else:
                _return_cpu_pinned_buf(buf)
        self._group_cpu_buf = None

    def wait_load(self):
        """Wait for grouped H2D and D2D loads to complete."""
        if self._load_event is None:
            raise RuntimeError(
                f"SwapGroup '{self.group_name}' wait_load() called before launch_load()."
            )
        compute_stream = _backend.get_current_stream()
        load_event = self._load_event
        stream_context = _backend.get_stream_context()
        with _backend.no_grad(), stream_context(compute_stream):
            load_event.wait(compute_stream)
            for storage in self._storages:
                storage.wait_load(release_event=load_event)
            self._load_event = None
        self._storages.clear()
        # Keep explicit-pool buffers pending until the copy event completes;
        # legacy buffers retain their existing immediate-reuse behavior.
        self.release_cpu_buffers(event=load_event)
        # Device buffer: the pool holds the staging reference; just drop
        # the local reference.  Tensors aliasing _group_device_buf's
        # storage keep it alive via their own storage references until
        # they are consumed in backward.
        self._group_device_buf = None
        self._packed_tensor_info = []
        self._packed_buckets = {}
        self._packed_by_bucket = {}
        self._seen_dedup_keys = set()


class SwapManager:
    """Singleton manager for swap groups and their operations."""
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
    )

    def __init__(self) -> None:
        """Initialize process-local swap groups once for the singleton."""
        if hasattr(self, '_groups'):
            return
        self._groups: Dict[str, SwapGroup] = {}
        self._current_group_name: ContextVar[str] = ContextVar(
            "swap_current_group_name", default=""
        )
        self._layer_count: int = 0
        self._copy_stream: Optional[Any] = None

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

    def add_storage(self, group_name: str, storage: Storage) -> None:
        """Add a storage to a specified swap group."""
        self.ensure_group(group_name)
        self._groups[group_name].add(storage)

    def ensure_group(self, group_name: str) -> None:
        """Create the swap group if it does not exist yet."""
        if group_name not in self._groups:
            self._groups[group_name] = SwapGroup(group_name)

    def launch_offload(self, group_name: str, copy_stream=None):
        """Launch async offload for a specified swap group."""
        group = self._groups.get(group_name)
        if group is None:
            raise RuntimeError(f"Group {group_name} does not exist.")
        if copy_stream is None:
            copy_stream = self._get_copy_stream()
        group.launch_offload(copy_stream)

    def protect_alias_tensors(self, group_name: str, tensors: Any):
        """Keep tensors that alias externally-owned tensors on device."""
        group = self._groups.get(group_name)
        if group is None:
            raise RuntimeError(f"Group {group_name} does not exist.")
        group.protect_alias_tensors(tensors)

    def wait_offload(self, group_name: str):
        """Wait for offload to complete for a specified swap group."""
        group = self._groups.get(group_name)
        if group is None:
            raise RuntimeError(f"Group {group_name} does not exist.")
        group.wait_offload()

    def launch_load(self, group_name: str, copy_stream=None):
        """Launch async load for a specified swap group."""
        group = self._groups.get(group_name)
        if group is None:
            raise RuntimeError(f"Group {group_name} does not exist.")
        if copy_stream is None:
            copy_stream = self._get_copy_stream()
        group.launch_load(copy_stream)

    def wait_load(self, group_name: str):
        """Wait for load to complete for a specified swap group."""
        group = self._groups.get(group_name)
        if group is None:
            raise RuntimeError(f"Group {group_name} does not exist.")
        group.wait_load()

    def release_group_storage(self, group_name: str) -> None:
        """Release storage references held by the swap group."""
        group = self._groups.get(group_name)
        if group is not None:
            group._storages.clear()

    def abort_group(self, group_name: str) -> None:
        """Synchronize in-flight transfers and remove a failed run's group."""
        group = self._groups.pop(group_name, None)
        if group is None:
            return
        for event in (group._offload_event, group._load_event):
            if event is not None:
                event.synchronize()
        for storage in group._storages:
            storage.release_cpu_buffers()
        group.release_cpu_buffers()
        group._storages.clear()

    def get_current_group_name(self) -> str:
        """Return the name of the currently active swap group."""
        return self._current_group_name.get()

    def set_current_group_name(self, group_name: str) -> None:
        """Set the name of the currently active swap group."""
        self._current_group_name.set(group_name)

    def active_group_count(self) -> int:
        """Return the number of live swap groups for lifecycle diagnostics."""
        return len(self._groups)

    @contextmanager
    def group_context(self, group_name: str) -> Iterator[None]:
        """Activate a swap group within the current execution context."""
        token = self._current_group_name.set(group_name)
        try:
            yield
        finally:
            self._current_group_name.reset(token)

    def is_last_group(self, group_name: Optional[str] = None) -> bool:
        """Return whether the specified swap group is the terminal group in the chain."""
        group_name = self.get_current_group_name() if group_name is None else group_name
        group = self._groups.get(group_name)
        if group is None:
            return False
        return group.is_last_group

    def unregister_forward_prefetch_hooks(self, module: Any) -> int:
        """Remove hooks installed by :meth:`set_forward_prefetch_layer`.
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924

        Returns:
            Number of removed hook handles.
        """
        removed_count = 0
        for attr_name in self._FORWARD_PREFETCH_HOOK_HANDLE_ATTRS:
            if not hasattr(module, attr_name):
                continue
            handle = getattr(module, attr_name)
            if handle is not None:
                handle.remove()
            delattr(module, attr_name)
            removed_count += 1
        return removed_count

    def set_forward_prefetch_layer(self, first_layer, second_layer):
        """
        Configure prefetching and offloading order between two consecutive layers.
928
929
930
931
932
933
934
935
936
937
                set_forward_prefetch_layer(model.layers[i], model.layers[i + 1])

        Ensures idempotency: safe to call multiple times on the same layer pair.
        """
        if first_layer is second_layer:
            warnings.warn(
                "set_forward_prefetch_layer: "
                "Prefetching between identical layers has no effect.",
                UserWarning,
                stacklevel=2,
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
                UserWarning,
                stacklevel=2,
            )

        def _ensure_group_name(module):
            """Assign a unique swap group name to the module if not already assigned."""
            if not hasattr(module, "_swap_group_name"):
                name = f"swap_group_{self._layer_count}"
                self._layer_count += 1
                module._swap_group_name = name
                module._swap_group_order = {"prev": None, "next": None}
            return module._swap_group_name
        first_name = _ensure_group_name(first_layer)
        second_name = _ensure_group_name(second_layer)

        if first_name not in self._groups:
            self._groups[first_name] = SwapGroup(first_name)
        if second_name not in self._groups:
            self._groups[second_name] = SwapGroup(second_name)

        if first_layer._swap_group_order["next"] is None:
            first_layer._swap_group_order["next"] = second_name
        if second_layer._swap_group_order["prev"] is None:
            second_layer._swap_group_order["prev"] = first_name

        self._groups[first_name].is_last_group = first_layer._swap_group_order["next"] is None
        self._groups[second_name].is_last_group = second_layer._swap_group_order["next"] is None

        def _forward_pre_hook(group_name, module, _):  # pylint: disable=W0613
            if getattr(module, "_swap_state", None) == "pre_backward":
                return
            SwapManager().set_current_group_name(group_name)

        def _forward_hook(group_name, module, args, output):  # pylint: disable=W0613
            """
            Forward post-hook executed immediately after forward computation
            of the current layer finishes.
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
            with forward computation of subsequent layers.
            - If the module is already in 'pre_backward' state, this hook is skipped
            to avoid triggering offload during backward phase.
            """
            if getattr(module, "_swap_state", None) == "pre_backward":
                return
            next_name = module._swap_group_order.get('next', None)
            if next_name:
                SwapManager().protect_alias_tensors(group_name, output)
                SwapManager().launch_offload(group_name)
            prev_name = module._swap_group_order.get('prev', None)
            if prev_name:
                SwapManager().wait_offload(prev_name)

        def _backward_pre_hook(group_name, module, grad_input):  # pylint: disable=W0613
            """
            Pre-backward hook executed immediately before backward computation
            of the current layer starts.
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
            with backward computation of the current layer.
            - The swap state is marked as 'pre_backward' to prevent forward hooks
            from issuing offload operations during backward phase.
            """
            module._swap_state = "pre_backward"
            prev_name = module._swap_group_order.get('prev', None)
            if prev_name:
                SwapManager().launch_load(prev_name)

            next_name = module._swap_group_order.get('next', None)
            if next_name:
                SwapManager().wait_load(group_name)
            SwapManager().release_group_storage(group_name)

        def _backward_hook(group_name, module, grad_input, grad_output):  # pylint: disable=W0613
            module._swap_state = "backward"

        def _register_hooks_once(module, group_name):
            hooks = [
                ("_swap_forward_pre_hook_handle",
                 lambda h: _backend.register_forward_pre_hook(module, h, prepend=True),
                 functools.partial(_forward_pre_hook, group_name)),
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
                 lambda h: _backend.register_full_backward_hook(module, h),
                 functools.partial(_backward_hook, group_name)),
            ]

            for attr_name, register_func, hook in hooks:
                if not hasattr(module, attr_name):
                    handle = register_func(hook)
                    setattr(module, attr_name, handle)
        # Register for both layers
        _register_hooks_once(first_layer, first_name)
        _register_hooks_once(second_layer, second_name)

    def _get_copy_stream(self):
        """Return a singleton copy stream, created on first access."""
        if self._copy_stream is None:
            self._copy_stream = _backend.new_stream()
        return self._copy_stream
hyper_parallel/core/activation_memory/wrapper.py
38
39
40
41
42
43
44
45
46


def _raise_if_compiling(feature_name: str) -> None:
    """Reject HyperParallel activation-swap extensions during Torch capture."""
    if torch.compiler.is_compiling():
        raise ValueError(
            f"HyperParallel {feature_name} is not supported in compile mode. "
            "Use Torch-native non-reentrant checkpointing with SAVE/RECOMPUTE policies."
        )
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
        >>> wrapped = swap_wrapper(lambda x: x * 2)
    """

    def __init__(self, fn: Callable):
        super().__init__()
        self._fn = fn

    def forward(self, *args, **kwargs):
        """Invoke the wrapped callable with the given arguments."""
        return self._fn(*args, **kwargs)


def _is_callable_exempt_from_overlap_check(callable_obj: Callable) -> bool:
    """Return True for callables that cannot be reliably overlap-tracked by object marks."""
    return isinstance(callable_obj, (types.FunctionType, types.BuiltinFunctionType, types.MethodType))


def _iter_wrappable_callable_attrs(module: nn.Module) -> Iterator[tuple[str, Callable]]:
    """Yield public per-instance callable attributes not registered as child modules.
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
    would both mutate a global object and falsely flag every sibling module that
    references the same function as an overlapping wrap.  Only per-instance
    callables participate in overlap tracking.
    """
    for attr_name, attr_value in vars(module).items():
        if attr_name.startswith("_") or isinstance(attr_value, nn.Module):
            continue
        if _is_callable_exempt_from_overlap_check(attr_value):
            continue
        if callable(attr_value):
            yield attr_name, attr_value


def _mark_wrapped(obj: Any) -> None:
    try:
        obj._is_wrapped = True  # pylint: disable=W0212
    except (AttributeError, TypeError):
        pass


def _get_wrapped_callable(module: nn.Module) -> Optional[Callable]:
    wrapped_module = getattr(module, _SWAP_WRAPPED_MODULE, None)
    if isinstance(wrapped_module, FuncModule):
        return getattr(wrapped_module, "_fn", None)
    if isinstance(module, FuncModule):
        return getattr(module, "_fn", None)
    return None


def _raise_callable_already_wrapped(callable_obj: Callable) -> None:
    warnings.warn(
        f"Callable '{callable_obj.__class__.__name__}' is already wrapped. "
        "Wrapping overlapping module regions is not allowed."
    )
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
    )


def _check_callable_attr_not_wrapped(owner: nn.Module, attr_name: str, attr_value: Callable) -> None:
    del owner, attr_name
    if getattr(attr_value, '_is_wrapped', False):
        _raise_callable_already_wrapped(attr_value)


def _check_and_mark_callable(callable_obj: Callable) -> None:
    if _is_callable_exempt_from_overlap_check(callable_obj):
        return
    if getattr(callable_obj, '_is_wrapped', False):
        warnings.warn(
            f"Callable '{callable_obj.__class__.__name__}' or one of its ancestors is already wrapped. "
            "Wrapping overlapping module regions is not allowed."
        )
    _mark_wrapped(callable_obj)


def _check_and_mark_wrapped(module: nn.Module) -> None:
    """Validate no wrapping overlap, then mark module and all descendants as wrapped."""
    if getattr(module, '_is_wrapped', False):
        warnings.warn(
            f"Module '{module.__class__.__name__}' or one of its ancestors is already wrapped. "
            "Wrapping overlapping module regions is not allowed."
        )
    for submodule in module.modules():
        if submodule is module:
            continue
        wrapped_callable = _get_wrapped_callable(submodule)
        if wrapped_callable is not None and _is_callable_exempt_from_overlap_check(wrapped_callable):
            continue
        if getattr(submodule, '_is_wrapped', False):
            if wrapped_callable is not None:
                _raise_callable_already_wrapped(wrapped_callable)
            # A param-free module shared across siblings (one rotary embedding
            # per decoder layer) is never its own region; skip, do not flag.
            if next(submodule.parameters(recurse=True), None) is None:
                continue
            warnings.warn(
                f"Submodule '{getattr(submodule, '_swap_wrapped_module', submodule).__class__.__name__}' of "
                f"'{module.__class__.__name__}' is already wrapped. "
                "Wrapping overlapping module regions is not allowed."
            )
    for submodule in module.modules():
        for attr_name, attr_value in _iter_wrappable_callable_attrs(submodule):
            _check_callable_attr_not_wrapped(submodule, attr_name, attr_value)
    for submodule in module.modules():
        _mark_wrapped(submodule)
        for _, attr_value in _iter_wrappable_callable_attrs(submodule):
            _mark_wrapped(attr_value)


def base_check_fn(tensor) -> bool:
    """
177
178
179
180
181
182
183
184
185
186
187
188
189
    Basic check to determine if a tensor is eligible for offloading.
    - Skip Parameters and their views.
    - Skip empty storage tensors.
    """
    if isinstance(tensor._base, torch.nn.parameter.Parameter) or isinstance(tensor, torch.nn.parameter.Parameter):  # pylint: disable=W0212
        return False
    if tensor.untyped_storage().size() == 0:
        return False
    return True


class AsyncSaveOnCpu(torch.autograd.graph.saved_tensors_hooks):
    """
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
    """
    Context manager to offload tensors to CPU during forward pass.
    """
    def __init__(self, policy_fn=None, group_swap: bool = False, cpu_pool=None) -> None:
        _raise_if_compiling("AsyncSaveOnCpu")
        self.add_to_storage = False
        self.storage = Storage()
        self.count_idx = 0
        self.policy_fn = policy_fn
        self.cpu_pool = cpu_pool

        # Cache per-context-manager state once to avoid per-tensor singleton lookups.
        swap_manager = SwapManager()

        def pack_to_cpu(tensor: torch.Tensor):
            if not base_check_fn(tensor):
                return tensor.detach()
            if policy_fn is not None:
                if policy_fn(tensor) == CheckpointPolicy.MUST_SAVE:
                    return tensor.detach()
                if policy_fn(tensor) != CheckpointPolicy.MUST_SWAP:
                    raise RuntimeError(f"Swap :set an invalid policy {policy_fn(tensor)}")
            group_name = swap_manager.get_current_group_name()
            if not group_name:
                return tensor.detach()
            if not self.add_to_storage:
                swap_manager.add_storage(group_name, self.storage)
                self.add_to_storage = True
            funcname = f"{group_name}::{tensor.shape}"
            detached = tensor.detach()
            self.storage[self.count_idx].append(
                SwapTensor(detached, funcname, group_swap=group_swap, cpu_pool=cpu_pool)
            )
            self.count_idx += 1
            return detached

        def unpack_from_cpu(tensor) -> torch.Tensor:
            if self.storage is not None:
                self.storage.clear()
                self.storage = None
            return tensor

        super().__init__(pack_to_cpu, unpack_from_cpu)


class ActivationWrapper(torch.nn.Module, ABC):
    """
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
    """

    def __init__(self, module: Union[nn.Module, Callable], *, track_overlaps: bool = True):
        """Initialize a wrapper and optionally participate in overlap tracking."""
        if callable(module) and not isinstance(module, nn.Module):
            if track_overlaps:
                _check_and_mark_callable(module)
            module = FuncModule(module)
            if track_overlaps:
                _mark_wrapped(module)
        elif track_overlaps:
            _check_and_mark_wrapped(module)
        super().__init__()
        self._swap_wrapped_module = module
        self._is_wrapped = track_overlaps
        # state_dict post hook to remove prefix to allow loading into a
        # non-swap wrapped module.
        self._register_state_dict_hook(self._post_state_dict_hook)
        # load_state_dict pre-hook to allow loading back into
        # swap-wrapped module.
        self.register_load_state_dict_pre_hook(self._pre_load_state_dict_hook)

    @property
    def _wrapped_module(self):
        return self._swap_wrapped_module

    @abstractmethod
    def forward(self, *args, **kwargs):
        """Run the wrapped module's forward pass with activation swapping. Must be implemented by subclasses."""
        raise ValueError("Subclasses should implement forward().")

    def __getattr__(self, name: str) -> Any:
        """Forward missing attributes to wrapped module."""
        try:
            return super().__getattr__(name)  # defer to nn.Module's logic
        except AttributeError:
            return getattr(self._swap_wrapped_module, name)

    def __getitem__(self, key: int) -> Any:
        """Forward indexing calls in case the module is a nn.Sequential."""
        return self._swap_wrapped_module.__getitem__(key)  # type: ignore[operator]

    def named_modules(
        self,
        memo: Optional[set[nn.Module]] = None,
300
301
302
303
304
305
306
307
308
309
310
311
312
313

        Returns:
            Iterator[tuple[str, nn.Module]] An iterator of (name, module) pairs.
        """
        if memo is None:
            memo = set()
        if self not in memo:
            memo.add(self)
            yield prefix, self
        yield from self._swap_wrapped_module.named_modules(
            memo=memo,
            prefix=prefix,
            remove_duplicate=remove_duplicate,
        )
321
322
323
324
325
326
327
328
329
330
        Override :meth:`named_parameters()` to intercept parameter names.

        remove all occurrences of ``_SWAP_PREFIX``.
        """
        for param_name, param in super().named_parameters(*args, **kwargs):
            yield param_name.replace(_SWAP_PREFIX, ""), param

    @staticmethod
    def _post_state_dict_hook(
        module: nn.Module,  # pylint: disable=W0613
339
340
341
342
343
344
345
346
347
348
        so that this module can be loaded into non-swapped modules.
        It would still be able to be loaded into swap-wrapped modules as this class,
        adds the prefix back before loading the state_dict.
        """
        _replace_by_prefix(state_dict, f"{prefix}{_SWAP_PREFIX}", prefix)
        return state_dict

    @staticmethod
    def _pre_load_state_dict_hook(
        module: nn.Module,
356
357
358
359
360
361
362
363
364
        For ``swap_wrapper``, it will add back the module
        prefix so that non-swapped modules can be loaded into
        swap_wrapper modules properly.
        """
        _replace_by_prefix(state_dict, prefix, prefix + f"{_SWAP_PREFIX}")


class SwapWrapper(ActivationWrapper):
    """
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
        policy_fn: Optional[Callable] = None,
        group_swap: bool = False,
        cpu_pool=None,
    ):
        super().__init__(mod)
        self.policy_fn = policy_fn
        self.group_swap = group_swap
        self.cpu_pool = cpu_pool

    def forward(self, *args, **kwargs):
        """Run the wrapped module inside an AsyncSaveOnCpu context for activation swapping."""
        _raise_if_compiling("swap_wrapper")
        async_kwargs = {
            "policy_fn": self.policy_fn,
            "group_swap": self.group_swap,
            "cpu_pool": self.cpu_pool,
        }
        with AsyncSaveOnCpu(**async_kwargs):
            return self._swap_wrapped_module(*args, **kwargs)


def swap_wrapper(
    module: Union[nn.Module, Callable],
404
405
406
407
408
409
410
411
412

    Returns:
        Configured activation swap wrapper.
    """
    return SwapWrapper(module, policy_fn, group_swap, cpu_pool)


def swap_tensor_wrapper(target, tag: Optional[str] = None, group_swap: bool = False, cpu_pool=None):
    """Register selected tensors into the current swap group.
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464

    Returns:
        The registered tensor structure.
    """
    _raise_if_compiling("swap_tensor_wrapper")
    swap_manager = SwapManager()
    group_name = swap_manager.get_current_group_name()
    if not group_name:
        warnings.warn(
            f"Tensor {tag} cannot be swapped, for its group is unregistered."
        )
        return target
    if swap_manager.is_last_group(group_name):
        return target

    storage = Storage()
    count_idx = 0

    def _register_tensor(tensor):
        nonlocal count_idx
        if not base_check_fn(tensor):
            return tensor

        tensor_tag = tag or f"{group_name}_swap_tensor"
        funcname = f"{tensor_tag}::{tuple(tensor.shape)}"
        storage[count_idx].append(
            SwapTensor(tensor, funcname, group_swap=group_swap, cpu_pool=cpu_pool)
        )
        count_idx += 1
        return tensor

    wrapped = torch.utils._pytree.tree_map(  # pylint: disable=protected-access
        lambda x: _register_tensor(x) if isinstance(x, torch.Tensor) else x,
        target,
    )
    if count_idx > 0:
        swap_manager.add_storage(group_name, storage)
    return wrapped


class CheckpointWrapper(ActivationWrapper):
    """Wrap a module with activation checkpointing."""
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
        self,
        mod: Union[nn.Module, Callable],
        **checkpoint_kwargs: Any,
    ):
        super().__init__(mod)
        self.checkpoint_kwargs = checkpoint_kwargs

    def _do_checkpoint(self, wrapped_module: Any, *args: Any, **kwargs: Any) -> Any:
        # Checkpoint may save inputs before the wrapped module's pre-hook runs.
        group_name = getattr(self, "_swap_group_name", None)
        if group_name is not None:
            SwapManager().set_current_group_name(group_name)

        from hyper_parallel.core.activation_memory.api import checkpoint  # pylint: disable=C0415
        return checkpoint(
            wrapped_module,
            *args,
            **self.checkpoint_kwargs,
            **kwargs,
485
486
487
488
489
490
491
492
493
494
495
496
497
498
            **kwargs,
        )

    def forward(self, *args: Any, **kwargs: Any) -> Any:
        return self._do_checkpoint(self._wrapped_module, *args, **kwargs)


def ckpt_wrapper(module: Union[nn.Module, Callable], **checkpoint_kwargs: Any) -> CheckpointWrapper:
    """Wrap *module* with activation checkpointing."""
    return CheckpointWrapper(module, **checkpoint_kwargs)


class CheckpointExcludeWrapper(ActivationWrapper):
    """Exclude a callable region from checkpoint recomputation."""
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
    """Exclude a callable region from checkpoint recomputation."""

    def __init__(self, module: Callable[..., Any], *, save_output: bool = True) -> None:
        """Initialize a checkpoint exclusion wrapper for a PyTorch module or function."""
        if not callable(module):
            raise ValueError("module must be a PyTorch Module or callable")
        if not isinstance(save_output, bool):
            raise ValueError(f"save_output must be a bool, got {type(save_output).__name__}")
        super().__init__(module, track_overlaps=False)
        self.save_output = save_output

    def forward(self, *args: Any, **kwargs: Any) -> Any:
        """Execute normally outside recompute and return the cached output in recompute."""
        wrapped_module = cast(Callable[..., Any], self._wrapped_module)
        state = get_recompute_state()
        if state is None:
            return wrapped_module(*args, **kwargs)
        cache = state.get_resource(_checkpoint_exclude._EXCLUDE_CACHE_KEY, _checkpoint_exclude._ExcludeCache)
        if state.is_recomputing:
            entry = cache.pop(id(self))
            _checkpoint_exclude._materialize_recompute_inputs(entry, args, kwargs)
            output = (
                entry.output
                if self.save_output
                else _checkpoint_exclude._make_replay_placeholder_output(entry.output_tensor_count)
            )
            return _checkpoint_exclude._finalize_save_outputs(
                output,
                _checkpoint_exclude._has_used_input(entry.input_bindings),
                None,
            )
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
                _checkpoint_exclude._has_used_input(entry.input_bindings),
                None,
            )

        input_bindings, previous_handles = _checkpoint_exclude._mark_recompute_inputs(
            state.invocation_id,
            args,
            kwargs,
        )
        try:
            with _checkpoint_exclude._saved_tensors_context():
                output = wrapped_module(*args, **kwargs)
        finally:
            _checkpoint_exclude._restore_recompute_inputs(previous_handles)
        needs_recompute_boundary = _checkpoint_exclude._has_used_input(input_bindings)
        tensor_leaf_count = None if self.save_output else [0]
        finalized_output = _checkpoint_exclude._finalize_save_outputs(
            output,
            needs_recompute_boundary,
            state.invocation_id,
            tensor_leaf_count,
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
            needs_recompute_boundary,
            state.invocation_id,
            tensor_leaf_count,
        )
        replay_output = output if self.save_output else None
        output_tensor_count = 1 if tensor_leaf_count is None else tensor_leaf_count[0]
        cache.save(
            id(self),
            _checkpoint_exclude._ExcludeCacheEntry(replay_output, input_bindings, output_tensor_count),
        )
        return finalized_output


def checkpoint_exclude_wrapper(
    module: Callable[..., Any],
576
577
578
579
580
        This feature requires eager mode and a surrounding HyperParallel
        checkpoint configured with ``use_reentrant=False``. Nested checkpoint
        exclusion wrappers are not supported.
    """
    return CheckpointExcludeWrapper(module, save_output=save_output)
hyper_parallel/distributed/activation_checkpoint.py
261
262
263
264
265
266
267
268
269
    Args:
        ops: Backend operators to ignore. ``None`` entries represent optional
            operators that are unavailable in the installed PyTorch version.
    """
    _ignore_sac_ops(ops)


def ensure_profiler_ops_sac_ignored() -> None:
    """Keep profiler record-function operators out of selective-AC replay.
376
377
378
379
380
381
382
383
384
        Returns:
            The ``(forward_context, recompute_context)`` pair expected by the
            non-reentrant checkpointing ``context_fn`` contract.
        """
        return create_selective_checkpoint_contexts(
            _make_selective_checkpoint_policy_fn()
        )

    return selective_checkpoint_context_fn