torch-remat 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ (c) Meta Platforms, Inc. and affiliates.
4
+
5
+ Redistribution and use in source and binary forms, with or without modification,
6
+ are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this list
9
+ of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice, this
12
+ list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its contributors may
16
+ be used to endorse or promote products derived from this software without specific
17
+ prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
20
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
22
+ SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24
+ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
25
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27
+ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
28
+ DAMAGE.
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: torch_remat
3
+ Version: 0.2.0
4
+ Summary: Explicit activation rematerialization helpers for PyTorch
5
+ Author: Meta Platforms, Inc.
6
+ License: BSD-3-Clause
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: torch>=2.10.0
11
+ Dynamic: license-file
12
+
13
+ # torch_remat
14
+
15
+ `torch_remat` is a modern activation checkpointing implementation for PyTorch.
16
+ It takes the classic recipe of non-reentrant `torch.utils.checkpoint` (recompute
17
+ everything before backwards) and enhances it with the ability to mark regions
18
+ inside the recompute region to save their activations for backward, so they
19
+ don't have to be recomputed.
20
+
21
+ Compared with selective activation checkpointing (SAC), `torch_remat` allows you
22
+ to specify save/recompute decisions on regions of source code, instead of writing
23
+ a generic policy function that operates on a per-ATen operator basis. This
24
+ allows for finer-grained recompute policies (e.g., you can easily express that
25
+ one matmul should be saved while another should be recomputed). It's also
26
+ easier to use `torch_remat` with code that has many custom kernels (you don't
27
+ have to custom op'ify them), and the fact that `torch_remat` doesn't use a
28
+ `TorchDispatchMode` means CPU overhead is lower.
29
+
30
+ ## Quick start
31
+
32
+ Put one checkpoint around the unit you want to replay, usually a transformer
33
+ block:
34
+
35
+ ```python
36
+ import torch_remat as remat
37
+
38
+ # torch.utils.checkpoint.checkpoint compatible API:
39
+ # remat.checkpoint(**remat_kwargs)(func)(*args, **kwargs)
40
+ output = remat.checkpoint(region_name="layers.0")(block)(hidden_states)
41
+ ```
42
+
43
+ (`remat.checkpoint(block)(hidden_states)` is intentionally not supported,
44
+ because this phrasing of the API is ambiguous with `torch.utils.checkpoint`.)
45
+
46
+ Inside the block's `forward` method, annotate calls whose backward activations
47
+ should be saved with `remat.region(..., recompute=False)`, and annotate
48
+ outputs of save regions which will be needed for recompute with
49
+ `remat.recompute_needs_tensor` (you can also omit these and `torch_remat` will
50
+ tell you which ones you need to mark):
51
+
52
+ ```python
53
+ def forward(self, hidden_states):
54
+ x = self.attention_norm(hidden_states)
55
+ attn = remat.region(
56
+ self.attention,
57
+ "attention",
58
+ recompute=False,
59
+ )(x)
60
+ remat.recompute_needs_tensor(attn)
61
+ hidden_states = hidden_states + attn
62
+
63
+ x = self.ffn_norm(hidden_states)
64
+ moe = remat.region(
65
+ self.moe,
66
+ "moe",
67
+ recompute=False,
68
+ )(x)
69
+ remat.recompute_needs_tensor(moe)
70
+ return hidden_states + moe
71
+ ```
72
+
73
+ The interaction between recompute and save regions is somewhat subtle;
74
+ check [Mental model](docs/mental_model.md) for more details.
75
+
76
+ ## State and side effects
77
+
78
+ Code that is replayed must behave consistently with the original forward, even
79
+ if saved region bodies are skipped during replay. In particular, if you rely
80
+ on mutable state in forwards (e.g., for RNG), you need to ensure you can
81
+ snapshot and restore this state. Code `remat.RecomputeStateHook` and pass it
82
+ with `recompute_state_hooks=`. The hook restores state at checkpoint entry and
83
+ at every non-recomputed function. Here is an example that takes care of
84
+ setting both a custom user RNG counter as well as standard PyTorch RNG state.
85
+
86
+ ```python
87
+ from contextvars import ContextVar
88
+
89
+ rng_counter = ContextVar("rng_counter", default=0)
90
+
91
+
92
+ class CudaRNGStateHook:
93
+ def __init__(self, device):
94
+ self.device = device
95
+
96
+ def snapshot(self):
97
+ return torch.cuda.get_rng_state(self.device), rng_counter.get()
98
+
99
+ def restore(self, state):
100
+ cuda_rng_state, counter = state
101
+ torch.cuda.set_rng_state(cuda_rng_state, self.device)
102
+ rng_counter.set(counter)
103
+
104
+
105
+ rng_hook = CudaRNGStateHook(hidden_states.device)
106
+ output = remat.checkpoint(
107
+ region_name="layers.0",
108
+ recompute_state_hooks=(rng_hook,),
109
+ )(block)(hidden_states)
110
+ ```
111
+
112
+ We don't provide a "stock" save/restore hook; in particular, `torch_remat`
113
+ doesn't support the `preserve_rng_state=True` kwarg that
114
+ `torch.utils.checkpoint` supports. The primary reason for this is that
115
+ `preserve_rng_state` is documented to also save CPU RNG state, but in modern
116
+ PyTorch code this is unnecessary (RNG should be sampled on-device) and
117
+ expensive (a 5KB allocation is needed to snapshot the CPU MT19937 state)--and
118
+ unlike `torch.utils.checkpoint`, we will repeatedly save/load RNG state many
119
+ times per a `remat.checkpoint`.
120
+
121
+ Separately, you can check whether other code is being replayed with
122
+ `remat.is_recomputing()`, for example to suppress forward-only logging or
123
+ metrics:
124
+
125
+ ```python
126
+ if not remat.is_recomputing():
127
+ record_metric(value)
128
+ ```
129
+
130
+ TODO: We should offer a simple way of checking that the replay is bitwise
131
+ equivalent to the original.
132
+
133
+ ## Diagnostics
134
+
135
+ `torch_remat` also comes with a number tools for understanding the recompute/save behavior
136
+ and memory usage of your program. Here are some things you can do:
137
+
138
+ **Trace the configured region hierarchy to see what is being saved/recomputed:**
139
+
140
+ ```python
141
+ with remat.collect_trace() as trace:
142
+ output = model(inputs)
143
+ print(trace.format())
144
+ ```
145
+
146
+ ```text
147
+ torch_remat trace
148
+ scope [test_flag]
149
+ sin: save
150
+ cos: recompute
151
+ ```
152
+
153
+ **Inspect retained activations from inside a checkpoint forward:**
154
+
155
+ ```python
156
+ if not remat.is_recomputing():
157
+ remat.print_current_memory_report()
158
+ ```
159
+
160
+ ```text
161
+ layers.0: 28 B resident in 2 storage(s)
162
+ layers.0::attn.softmax: 28 B
163
+ 12 B lse (3,) float32
164
+ 16 B probs (4,) float32
165
+ ```
166
+
167
+ **Inspect all live checkpoint regions plus saves reachable from a loss after
168
+ the full forward:**
169
+
170
+ ```python
171
+ remat.print_saved_tensors_report(loss)
172
+ ```
173
+
174
+ ```text
175
+ saved for backward: 240 B resident -- 2 region(s) 192 B, outside regions 48 B
176
+
177
+ regions:
178
+ 96 B x2 layer.0-1 (2 storages each)
179
+
180
+ outside regions: 48 B in 1 storage
181
+ 48 B TanhBackward0 (x1)
182
+
183
+ region detail:
184
+ [x2: layer.0-1]
185
+ layer.0: 96 B resident in 2 storage(s)
186
+ layer.0::sq: 96 B
187
+ 48 B y (output at idx 0) (3, 4) float32
188
+ 48 B gf (3, 4) float32
189
+ ```
190
+
191
+ Allocation-site annotations and an attachable CUDA OOM observer are also
192
+ provided. See the [Diagnostics API](docs/api/diagnostics.md) for more details.
193
+
194
+ ## License
195
+
196
+ BSD 3-Clause License. See [LICENSE](LICENSE).
@@ -0,0 +1,184 @@
1
+ # torch_remat
2
+
3
+ `torch_remat` is a modern activation checkpointing implementation for PyTorch.
4
+ It takes the classic recipe of non-reentrant `torch.utils.checkpoint` (recompute
5
+ everything before backwards) and enhances it with the ability to mark regions
6
+ inside the recompute region to save their activations for backward, so they
7
+ don't have to be recomputed.
8
+
9
+ Compared with selective activation checkpointing (SAC), `torch_remat` allows you
10
+ to specify save/recompute decisions on regions of source code, instead of writing
11
+ a generic policy function that operates on a per-ATen operator basis. This
12
+ allows for finer-grained recompute policies (e.g., you can easily express that
13
+ one matmul should be saved while another should be recomputed). It's also
14
+ easier to use `torch_remat` with code that has many custom kernels (you don't
15
+ have to custom op'ify them), and the fact that `torch_remat` doesn't use a
16
+ `TorchDispatchMode` means CPU overhead is lower.
17
+
18
+ ## Quick start
19
+
20
+ Put one checkpoint around the unit you want to replay, usually a transformer
21
+ block:
22
+
23
+ ```python
24
+ import torch_remat as remat
25
+
26
+ # torch.utils.checkpoint.checkpoint compatible API:
27
+ # remat.checkpoint(**remat_kwargs)(func)(*args, **kwargs)
28
+ output = remat.checkpoint(region_name="layers.0")(block)(hidden_states)
29
+ ```
30
+
31
+ (`remat.checkpoint(block)(hidden_states)` is intentionally not supported,
32
+ because this phrasing of the API is ambiguous with `torch.utils.checkpoint`.)
33
+
34
+ Inside the block's `forward` method, annotate calls whose backward activations
35
+ should be saved with `remat.region(..., recompute=False)`, and annotate
36
+ outputs of save regions which will be needed for recompute with
37
+ `remat.recompute_needs_tensor` (you can also omit these and `torch_remat` will
38
+ tell you which ones you need to mark):
39
+
40
+ ```python
41
+ def forward(self, hidden_states):
42
+ x = self.attention_norm(hidden_states)
43
+ attn = remat.region(
44
+ self.attention,
45
+ "attention",
46
+ recompute=False,
47
+ )(x)
48
+ remat.recompute_needs_tensor(attn)
49
+ hidden_states = hidden_states + attn
50
+
51
+ x = self.ffn_norm(hidden_states)
52
+ moe = remat.region(
53
+ self.moe,
54
+ "moe",
55
+ recompute=False,
56
+ )(x)
57
+ remat.recompute_needs_tensor(moe)
58
+ return hidden_states + moe
59
+ ```
60
+
61
+ The interaction between recompute and save regions is somewhat subtle;
62
+ check [Mental model](docs/mental_model.md) for more details.
63
+
64
+ ## State and side effects
65
+
66
+ Code that is replayed must behave consistently with the original forward, even
67
+ if saved region bodies are skipped during replay. In particular, if you rely
68
+ on mutable state in forwards (e.g., for RNG), you need to ensure you can
69
+ snapshot and restore this state. Code `remat.RecomputeStateHook` and pass it
70
+ with `recompute_state_hooks=`. The hook restores state at checkpoint entry and
71
+ at every non-recomputed function. Here is an example that takes care of
72
+ setting both a custom user RNG counter as well as standard PyTorch RNG state.
73
+
74
+ ```python
75
+ from contextvars import ContextVar
76
+
77
+ rng_counter = ContextVar("rng_counter", default=0)
78
+
79
+
80
+ class CudaRNGStateHook:
81
+ def __init__(self, device):
82
+ self.device = device
83
+
84
+ def snapshot(self):
85
+ return torch.cuda.get_rng_state(self.device), rng_counter.get()
86
+
87
+ def restore(self, state):
88
+ cuda_rng_state, counter = state
89
+ torch.cuda.set_rng_state(cuda_rng_state, self.device)
90
+ rng_counter.set(counter)
91
+
92
+
93
+ rng_hook = CudaRNGStateHook(hidden_states.device)
94
+ output = remat.checkpoint(
95
+ region_name="layers.0",
96
+ recompute_state_hooks=(rng_hook,),
97
+ )(block)(hidden_states)
98
+ ```
99
+
100
+ We don't provide a "stock" save/restore hook; in particular, `torch_remat`
101
+ doesn't support the `preserve_rng_state=True` kwarg that
102
+ `torch.utils.checkpoint` supports. The primary reason for this is that
103
+ `preserve_rng_state` is documented to also save CPU RNG state, but in modern
104
+ PyTorch code this is unnecessary (RNG should be sampled on-device) and
105
+ expensive (a 5KB allocation is needed to snapshot the CPU MT19937 state)--and
106
+ unlike `torch.utils.checkpoint`, we will repeatedly save/load RNG state many
107
+ times per a `remat.checkpoint`.
108
+
109
+ Separately, you can check whether other code is being replayed with
110
+ `remat.is_recomputing()`, for example to suppress forward-only logging or
111
+ metrics:
112
+
113
+ ```python
114
+ if not remat.is_recomputing():
115
+ record_metric(value)
116
+ ```
117
+
118
+ TODO: We should offer a simple way of checking that the replay is bitwise
119
+ equivalent to the original.
120
+
121
+ ## Diagnostics
122
+
123
+ `torch_remat` also comes with a number tools for understanding the recompute/save behavior
124
+ and memory usage of your program. Here are some things you can do:
125
+
126
+ **Trace the configured region hierarchy to see what is being saved/recomputed:**
127
+
128
+ ```python
129
+ with remat.collect_trace() as trace:
130
+ output = model(inputs)
131
+ print(trace.format())
132
+ ```
133
+
134
+ ```text
135
+ torch_remat trace
136
+ scope [test_flag]
137
+ sin: save
138
+ cos: recompute
139
+ ```
140
+
141
+ **Inspect retained activations from inside a checkpoint forward:**
142
+
143
+ ```python
144
+ if not remat.is_recomputing():
145
+ remat.print_current_memory_report()
146
+ ```
147
+
148
+ ```text
149
+ layers.0: 28 B resident in 2 storage(s)
150
+ layers.0::attn.softmax: 28 B
151
+ 12 B lse (3,) float32
152
+ 16 B probs (4,) float32
153
+ ```
154
+
155
+ **Inspect all live checkpoint regions plus saves reachable from a loss after
156
+ the full forward:**
157
+
158
+ ```python
159
+ remat.print_saved_tensors_report(loss)
160
+ ```
161
+
162
+ ```text
163
+ saved for backward: 240 B resident -- 2 region(s) 192 B, outside regions 48 B
164
+
165
+ regions:
166
+ 96 B x2 layer.0-1 (2 storages each)
167
+
168
+ outside regions: 48 B in 1 storage
169
+ 48 B TanhBackward0 (x1)
170
+
171
+ region detail:
172
+ [x2: layer.0-1]
173
+ layer.0: 96 B resident in 2 storage(s)
174
+ layer.0::sq: 96 B
175
+ 48 B y (output at idx 0) (3, 4) float32
176
+ 48 B gf (3, 4) float32
177
+ ```
178
+
179
+ Allocation-site annotations and an attachable CUDA OOM observer are also
180
+ provided. See the [Diagnostics API](docs/api/diagnostics.md) for more details.
181
+
182
+ ## License
183
+
184
+ BSD 3-Clause License. See [LICENSE](LICENSE).
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "torch_remat"
7
+ version = "0.2.0"
8
+ description = "Explicit activation rematerialization helpers for PyTorch"
9
+ readme = "README.md"
10
+ license = {text = "BSD-3-Clause"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "Meta Platforms, Inc."},
14
+ ]
15
+ dependencies = [
16
+ "torch>=2.10.0",
17
+ ]
18
+
19
+ [tool.setuptools.packages.find]
20
+ include = ["torch_remat*"]
21
+
22
+ [tool.ruff]
23
+ line-length = 88
24
+ target-version = "py310"
25
+
26
+ [tool.ruff.lint]
27
+ select = ["E4", "E7", "E9", "F"]
28
+
29
+ [tool.ruff.format]
30
+ quote-style = "double"
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+ python_files = ["*_test.py"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,37 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from __future__ import annotations
8
+
9
+ from torch_remat._annotate import ( # noqa: F401
10
+ clear_memory_snapshot_cache,
11
+ memory_snapshot_annotate,
12
+ )
13
+ from torch_remat._api import ( # noqa: F401
14
+ checkpoint,
15
+ current_saved_tensor_info,
16
+ is_placeholder,
17
+ recompute_needs_tensor,
18
+ region,
19
+ save_for_backward,
20
+ saved_tensors_hooks,
21
+ )
22
+ from torch_remat._oom import ( # noqa: F401
23
+ discover_autograd_roots,
24
+ format_oom_saved_tensors_report,
25
+ oom_observer,
26
+ )
27
+ from torch_remat._region import is_recomputing, RecomputeStateHook # noqa: F401
28
+ from torch_remat._reporting import ( # noqa: F401
29
+ format_current_memory_report,
30
+ print_current_memory_report,
31
+ )
32
+ from torch_remat._saved_report import ( # noqa: F401
33
+ format_saved_tensors_report,
34
+ print_saved_tensors_report,
35
+ )
36
+ from torch_remat._trace import collect_trace, trace_scope # noqa: F401
37
+ from torch_remat._types import SavedTensorInfo, SavedTensorKind # noqa: F401
@@ -0,0 +1,111 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ # pyre-strict
8
+
9
+ """A default :data:`~torch_remat._reporting.Annotate` backed by the CUDA memory snapshot.
10
+
11
+ The saved-tensors report can tag each storage row with a caller-supplied label. By far the
12
+ most common label is *where the tensor was allocated*, which the CUDA caching allocator
13
+ already records (when memory-history recording is on). This module turns that snapshot into a
14
+ ready-made annotator -- the "which tensor is this" that a positional ``saved.<i>`` row cannot
15
+ give -- so callers don't reinvent the storage-pointer -> allocation-site mapping.
16
+
17
+ The snapshot walk is expensive, so the fetch is memoized (:func:`clear_memory_snapshot_cache`
18
+ resets it) and the built annotator indexes the snapshot once, reusing it across every
19
+ per-tensor lookup -- a whole report costs a single snapshot fetch.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import bisect
25
+ from typing import Any, Mapping
26
+
27
+ import torch
28
+ from torch_remat._reporting import Annotate
29
+
30
+ _snapshot_cache: Mapping[str, Any] | None = None
31
+
32
+
33
+ def _memory_snapshot(*, refresh: bool = False) -> Mapping[str, Any]:
34
+ """Return the CUDA allocator memory snapshot, memoized after the first fetch.
35
+
36
+ ``torch.cuda.memory._snapshot()`` walks the whole allocator and is expensive, so the
37
+ result is cached. Pass ``refresh=True`` (or call :func:`clear_memory_snapshot_cache`) to
38
+ re-fetch after more allocation. The snapshot carries allocation backtraces only while
39
+ memory-history recording is enabled.
40
+ """
41
+ global _snapshot_cache
42
+ if refresh or _snapshot_cache is None:
43
+ _snapshot_cache = torch.cuda.memory._snapshot()
44
+ return _snapshot_cache
45
+
46
+
47
+ def clear_memory_snapshot_cache() -> None:
48
+ """Drop the memoized snapshot so the next annotator re-fetches (see :func:`_memory_snapshot`)."""
49
+ global _snapshot_cache
50
+ _snapshot_cache = None
51
+
52
+
53
+ def _alloc_site_from_frames(frames: list[dict[str, Any]] | None) -> str | None:
54
+ """Pick a short ``file:line name`` label from an allocation backtrace (innermost first).
55
+
56
+ Prefer the innermost frame outside library internals (torch and torch_remat), which is
57
+ usually the model/kernel code that produced the tensor; fall back to the innermost frame
58
+ of any kind.
59
+ """
60
+ for frame in frames or []:
61
+ filename = frame.get("filename", "")
62
+ if "/torch/" not in filename and "/torch_remat/" not in filename:
63
+ base = filename.rsplit("/", 1)[-1]
64
+ return f"{base}:{frame.get('line')} {frame.get('name', '')}".rstrip()
65
+ if frames:
66
+ frame = frames[0]
67
+ return f"{frame.get('filename', '?').rsplit('/', 1)[-1]}:{frame.get('line')}"
68
+ return None
69
+
70
+
71
+ def memory_snapshot_annotate(snapshot: Mapping[str, Any] | None = None) -> Annotate:
72
+ """Return an :data:`Annotate` mapping a saved tensor's storage to its allocation site.
73
+
74
+ Each live allocator block carries its address and allocation backtrace, so a saved
75
+ tensor's storage pointer (which falls inside exactly one live block) resolves to the code
76
+ that allocated it. Pass the result as ``annotate=`` to
77
+ :func:`torch_remat.format_saved_tensors_report`.
78
+
79
+ Args:
80
+ snapshot: A CUDA memory snapshot (``torch.cuda.memory._snapshot()``); defaults to the
81
+ cached :func:`_memory_snapshot`. The block index is built once here and reused by
82
+ every per-tensor lookup, so a whole report costs a single snapshot fetch.
83
+ """
84
+
85
+ if snapshot is None:
86
+ snapshot = _memory_snapshot()
87
+
88
+ blocks: list[tuple[int, int, str]] = [] # (address, size, site), sorted by address
89
+ for segment in snapshot.get("segments", []):
90
+ for block in segment.get("blocks", []):
91
+ if block.get("state") != "active_allocated":
92
+ continue
93
+ site = _alloc_site_from_frames(block.get("frames"))
94
+ if site is not None:
95
+ blocks.append((block["address"], block["size"], site))
96
+ blocks.sort()
97
+ addrs = [address for address, _, _ in blocks]
98
+
99
+ def annotate(tensor: torch.Tensor) -> str | None:
100
+ try:
101
+ ptr = tensor.untyped_storage().data_ptr()
102
+ except Exception:
103
+ return None
104
+ index = bisect.bisect_right(addrs, ptr) - 1
105
+ if 0 <= index < len(blocks):
106
+ address, size, site = blocks[index]
107
+ if address <= ptr < address + size:
108
+ return site
109
+ return None
110
+
111
+ return annotate