sparse-delta-memory 2.0.0__py3-none-any.whl
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.
- sparse_delta_memory/__init__.py +13 -0
- sparse_delta_memory/backward.py +367 -0
- sparse_delta_memory/cache.py +70 -0
- sparse_delta_memory/cuda/__init__.py +3 -0
- sparse_delta_memory/cuda/_build.py +254 -0
- sparse_delta_memory/cuda/_device.py +158 -0
- sparse_delta_memory/cuda/_devmemo.py +64 -0
- sparse_delta_memory/cuda/_image.py +128 -0
- sparse_delta_memory/cuda/bwd_steps_cuda.py +111 -0
- sparse_delta_memory/cuda/gather_cuda.py +796 -0
- sparse_delta_memory/cuda/gather_ns_cuda.py +134 -0
- sparse_delta_memory/cuda/gx_gds_cuda.py +1297 -0
- sparse_delta_memory/cuda/gx_gemm_cuda.py +857 -0
- sparse_delta_memory/cuda/gx_pa2_cuda.py +1320 -0
- sparse_delta_memory/cuda/mover_da2_cuda.py +1802 -0
- sparse_delta_memory/cuda/mover_fx_cuda.py +2229 -0
- sparse_delta_memory/cuda/pa_dc_cuda.py +1500 -0
- sparse_delta_memory/cuda/phase1_bwd_cuda.py +852 -0
- sparse_delta_memory/cuda/phase1_common.cuh +181 -0
- sparse_delta_memory/cuda/phase1_cuda.py +3801 -0
- sparse_delta_memory/cuda/product_key_topk_anyf_cuda.py +668 -0
- sparse_delta_memory/cuda/product_key_topk_cuda.py +735 -0
- sparse_delta_memory/cuda/px_qrec_cuda.py +167 -0
- sparse_delta_memory/cuda/sdm_copy.cuh +90 -0
- sparse_delta_memory/cuda/sdm_plancap.cuh +150 -0
- sparse_delta_memory/cuda_graph.py +605 -0
- sparse_delta_memory/decode.py +140 -0
- sparse_delta_memory/forward.py +464 -0
- sparse_delta_memory/layer.py +663 -0
- sparse_delta_memory/memory_ops.py +1069 -0
- sparse_delta_memory/memory_stats.py +221 -0
- sparse_delta_memory/movers.py +198 -0
- sparse_delta_memory/phase2_kernels.py +267 -0
- sparse_delta_memory/product_key_topk.py +113 -0
- sparse_delta_memory/wy_solve.py +273 -0
- sparse_delta_memory-2.0.0.dist-info/METADATA +482 -0
- sparse_delta_memory-2.0.0.dist-info/RECORD +40 -0
- sparse_delta_memory-2.0.0.dist-info/WHEEL +5 -0
- sparse_delta_memory-2.0.0.dist-info/licenses/LICENSE +407 -0
- sparse_delta_memory-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
"""CUDA-graph execution of the SDM write/read (``SparseDeltaMemoryArgs.cuda_graph``).
|
|
3
|
+
|
|
4
|
+
A layer replays two graphs, captured once per entry: the forward (batch expand of the initial bank, then
|
|
5
|
+
``SDMWriteRead.forward``), captured at the entry's first call, and the backward (``SDMWriteRead.backward``, then the
|
|
6
|
+
batch reduction of the bank gradient), captured at its first backward. Projections, top-k and gates stay eager.
|
|
7
|
+
|
|
8
|
+
Contract:
|
|
9
|
+
* One captured key (shapes, dtypes, op scalars, the TF32 matmul setting) per layer: that of
|
|
10
|
+
its first call that may need a backward. A call with another key runs eager, and so does
|
|
11
|
+
every call of a layer whose capture failed (or whose first backward ran out of memory);
|
|
12
|
+
each case logs one warning per layer. ``reset_cuda_graphs()`` drops the graphs (call it
|
|
13
|
+
between steps) so that the next call captures again. Graphs are never re-captured on
|
|
14
|
+
their own.
|
|
15
|
+
* Inside ``suspend_cuda_graphs()`` or a CUDA-graph capture every call runs eager.
|
|
16
|
+
* A differentiable call saves the same tensors for backward on either path: a token, the
|
|
17
|
+
bank parameter and the inputs. An eager call (``_EagerSaved``) runs its forward again in
|
|
18
|
+
the backward. So under non-reentrant checkpointing the original forward and its
|
|
19
|
+
recompute may take different paths.
|
|
20
|
+
* The graphs, their static buffers and the state of the call they last replayed form an entry. Under non-reentrant
|
|
21
|
+
activation checkpointing (``_region_entries``) the layers of one key in different regions share an entry (each
|
|
22
|
+
backward follows its own recompute); layers of one key in one region take distinct entries ("slots"); a layer
|
|
23
|
+
outside any checkpoint keeps its own.
|
|
24
|
+
* Each forward replay issues a token (a non-reentrant recompute substitutes its own) and makes its call the owner of
|
|
25
|
+
the entry. A backward that does not own its entry first replays its forward from the saved tensors: the same
|
|
26
|
+
result, at the cost of one more forward.
|
|
27
|
+
* Forward graphs share one memory pool per device and capture stream and are captured in call order. Replaying one
|
|
28
|
+
may reuse blocks that hold the state of entries captured after it, so the replay invalidates their tokens; a replay
|
|
29
|
+
on another stream is ordered after the work queued on the pool's stream and before the work queued there next, and
|
|
30
|
+
threads run their calls on the pool's graphs one at a time (``_Pool.lock``). Backward graphs use the pool of their
|
|
31
|
+
forward graph; forward graphs captured after the first backward graph of the shared pool get a private pool.
|
|
32
|
+
* An entry lives while a layer or a pending backward refers to it: until ``reset_cuda_graphs()``, the death of its
|
|
33
|
+
layers, or an out-of-memory first backward, which releases every entry of its pool."""
|
|
34
|
+
|
|
35
|
+
import contextlib
|
|
36
|
+
import itertools
|
|
37
|
+
import logging
|
|
38
|
+
import threading
|
|
39
|
+
import weakref
|
|
40
|
+
from collections import Counter
|
|
41
|
+
|
|
42
|
+
import torch
|
|
43
|
+
from torch.autograd.function import once_differentiable
|
|
44
|
+
from torch.utils._python_dispatch import _disable_current_modes
|
|
45
|
+
|
|
46
|
+
from .cache import SDMLayerState
|
|
47
|
+
from .memory_ops import (SDMWriteRead, UnsupportedConfigurationError, _layout, expand_memory_batch,
|
|
48
|
+
refuse_non_native, refuse_quant_tail, sdm_write_read, triton_sum_dim0)
|
|
49
|
+
from .phase2_kernels import SNAPSHOT_QUANT_MODES
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
_tokens = itertools.count(1)
|
|
54
|
+
_pools: dict = {}
|
|
55
|
+
_pools_lock = threading.Lock()
|
|
56
|
+
# The shared entries by (pool, key, slot), held weakly: by their layers and pending backwards.
|
|
57
|
+
_shared: "weakref.WeakValueDictionary" = weakref.WeakValueDictionary()
|
|
58
|
+
_shared_lock = threading.Lock() # every access to _shared (pools insert concurrently)
|
|
59
|
+
_runners: "weakref.WeakSet[CudaGraphRunner]" = weakref.WeakSet()
|
|
60
|
+
_suspended = 0
|
|
61
|
+
# Per thread: checkpoint region pack hook -> the entries its calls used (weak).
|
|
62
|
+
_local = threading.local()
|
|
63
|
+
# One CUDA-graph capture at a time in the process (torch.cuda.graph), whatever the pool.
|
|
64
|
+
_capture_lock = threading.Lock()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def reset_cuda_graphs() -> None:
|
|
68
|
+
"""Drop every captured graph, e.g. between steps after a permanent shape change: the next
|
|
69
|
+
call of each layer captures again (also in a layer whose capture failed)."""
|
|
70
|
+
for runner in list(_runners):
|
|
71
|
+
runner.entry = None
|
|
72
|
+
runner.capture_failed = False
|
|
73
|
+
with _pools_lock:
|
|
74
|
+
_pools.clear()
|
|
75
|
+
with _shared_lock:
|
|
76
|
+
_shared.clear()
|
|
77
|
+
_local.__dict__.clear()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@contextlib.contextmanager
|
|
81
|
+
def suspend_cuda_graphs():
|
|
82
|
+
"""Run the write/read of every layer eager inside the block, e.g. a probe at another
|
|
83
|
+
shape. The backward of those calls may run outside the block."""
|
|
84
|
+
global _suspended
|
|
85
|
+
_suspended += 1
|
|
86
|
+
try:
|
|
87
|
+
yield
|
|
88
|
+
finally:
|
|
89
|
+
_suspended -= 1
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def cuda_graphs_suspended() -> bool:
|
|
93
|
+
return _suspended > 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@contextlib.contextmanager
|
|
97
|
+
def _untracked():
|
|
98
|
+
# Selective activation checkpointing caches op outputs in call order, so a recompute must
|
|
99
|
+
# issue the ops of the original forward. The first graphed call also warms up and
|
|
100
|
+
# captures; hiding every op of the runner keeps both sequences empty.
|
|
101
|
+
with _disable_current_modes(), torch.no_grad():
|
|
102
|
+
yield
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _region_entries():
|
|
106
|
+
"""The entries used by the graphed calls of the innermost non-reentrant checkpoint region (its saved-tensors pack hook,
|
|
107
|
+
a private torch API), or None outside one or on any error."""
|
|
108
|
+
try:
|
|
109
|
+
hooks = _top_hooks(False)
|
|
110
|
+
pack = hooks[0] if hooks else None
|
|
111
|
+
if not (getattr(pack, "__module__", None) == "torch.utils.checkpoint"
|
|
112
|
+
and str(getattr(pack, "__qualname__", "")).startswith("_checkpoint_hook.")):
|
|
113
|
+
return None
|
|
114
|
+
regions = getattr(_local, "regions", None)
|
|
115
|
+
if regions is None:
|
|
116
|
+
regions = _local.regions = weakref.WeakKeyDictionary()
|
|
117
|
+
used = regions.get(pack)
|
|
118
|
+
if used is None:
|
|
119
|
+
used = regions[pack] = weakref.WeakSet()
|
|
120
|
+
return used
|
|
121
|
+
except Exception: # noqa: BLE001 - another torch: entries of their own
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
_top_hooks = getattr(torch._C._autograd, "_top_saved_tensors_default_hooks", None)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _capture(fn, pool):
|
|
129
|
+
graph = torch.cuda.CUDAGraph()
|
|
130
|
+
stream = torch.cuda.current_stream()
|
|
131
|
+
try:
|
|
132
|
+
with _capture_lock, _untracked(), torch.cuda.graph(graph, pool=pool,
|
|
133
|
+
capture_error_mode="thread_local"):
|
|
134
|
+
out = fn()
|
|
135
|
+
finally:
|
|
136
|
+
# a failed capture_end() skips torch.cuda.graph's stream restore
|
|
137
|
+
torch.cuda.set_stream(stream)
|
|
138
|
+
return graph, out
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _failure(exc: BaseException) -> str:
|
|
142
|
+
return f"{type(exc).__name__}: {exc}".splitlines()[0][:300]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _Ctx:
|
|
146
|
+
"""Stand-in autograd ctx: SDMWriteRead uses only save_for_backward, saved_tensors and
|
|
147
|
+
plain attributes. The runner takes several backwards on one such ctx: the eager warm-up
|
|
148
|
+
of the first backward (whose gradients the first backward returns), the capture right
|
|
149
|
+
after it (which records the kernels without running them), and eager fallbacks that run
|
|
150
|
+
after the call's forward was replayed. SDMWriteRead's second-backward guard therefore
|
|
151
|
+
does not apply to it."""
|
|
152
|
+
|
|
153
|
+
repeated_backward_ok = True
|
|
154
|
+
|
|
155
|
+
def save_for_backward(self, *tensors):
|
|
156
|
+
self.saved_tensors = tensors
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class _Pool:
|
|
160
|
+
def __init__(self, stream):
|
|
161
|
+
self.handle = torch.cuda.graph_pool_handle()
|
|
162
|
+
self.stream = stream # the capture stream
|
|
163
|
+
self.forward = [] # weak references to the shared forward entries, in capture order
|
|
164
|
+
self.has_backward = False
|
|
165
|
+
self.lock = threading.RLock() # serializes the threads' calls on the pool's graphs
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _stream_pool(device):
|
|
169
|
+
"""The shared pool of ``device`` and its current stream (one stream: replays never overlap)."""
|
|
170
|
+
stream = torch.cuda.current_stream(device)
|
|
171
|
+
with _pools_lock:
|
|
172
|
+
pool = _pools.get((device.index, stream.stream_id))
|
|
173
|
+
if pool is None:
|
|
174
|
+
pool = _pools[(device.index, stream.stream_id)] = _Pool(stream)
|
|
175
|
+
return pool
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@contextlib.contextmanager
|
|
179
|
+
def _ordered(pool):
|
|
180
|
+
"""Runs the block alone among the threads' blocks on ``pool`` and orders it, when it runs
|
|
181
|
+
on another stream than the pool's, after the work queued on the pool's stream and before
|
|
182
|
+
the work queued there next: graphs that share the pool never overlap."""
|
|
183
|
+
with pool.lock:
|
|
184
|
+
current = torch.cuda.current_stream(pool.stream.device)
|
|
185
|
+
if current == pool.stream:
|
|
186
|
+
yield
|
|
187
|
+
return
|
|
188
|
+
current.wait_stream(pool.stream)
|
|
189
|
+
try:
|
|
190
|
+
yield
|
|
191
|
+
finally:
|
|
192
|
+
pool.stream.wait_stream(current)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _write_read(param, bank_shape, dtype, inputs, scalars, training=False):
|
|
196
|
+
"""Readings of the eager op from the initial bank (B copies of ``param``, or zeros), and
|
|
197
|
+
the final bank."""
|
|
198
|
+
if param is None:
|
|
199
|
+
bank = torch.zeros(bank_shape, device=inputs[0].device, dtype=dtype)
|
|
200
|
+
else:
|
|
201
|
+
bank = expand_memory_batch(param, bank_shape[0] // len(param))
|
|
202
|
+
chunk, slots, parallel, quant, *rest = scalars # rest: (grad_bank_fp32,), part of the graph key
|
|
203
|
+
if training and quant in SNAPSHOT_QUANT_MODES: # the no-grad op's training refusals (else: the op's ValueError)
|
|
204
|
+
lay = _layout(bank, *inputs, chunk, slots, parallel) # (SDMWriteRead.forward's checks, in its order)
|
|
205
|
+
rows = lay.P * lay.Nt * lay.C * lay.W
|
|
206
|
+
refuse_quant_tail(dtype, lay.hd, quant, rows, device=bank.device)
|
|
207
|
+
refuse_non_native(bank, lay.C, quant, rows, needs_backward=True, grad_bank_fp32=bool(rest[0]) if rest else False)
|
|
208
|
+
readings = sdm_write_read(bank, *inputs, chunk_size=chunk, num_memory_slots=slots,
|
|
209
|
+
num_parallel=parallel, snapshot_quant=quant,
|
|
210
|
+
grad_bank_fp32=bool(rest[0]) if rest else False)
|
|
211
|
+
return readings, bank
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _recomputed_grads(call, param, inputs, grad, needs):
|
|
215
|
+
"""Gradients for (param, *inputs) of the readings, by running the forward again with
|
|
216
|
+
autograd; ``needs`` flags the ones to compute."""
|
|
217
|
+
leaves = [None if t is None else t.detach().requires_grad_(n)
|
|
218
|
+
for t, n in zip((param, *inputs), needs)]
|
|
219
|
+
with torch.enable_grad():
|
|
220
|
+
readings, _ = _write_read(leaves[0], call[0], call[1], leaves[1:], call[2])
|
|
221
|
+
want = [t for t in leaves if t is not None and t.requires_grad]
|
|
222
|
+
got = iter(torch.autograd.grad(readings, want, grad, allow_unused=True) if want else ())
|
|
223
|
+
return tuple(next(got) if t is not None and t.requires_grad else None for t in leaves)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class _EagerSaved(torch.autograd.Function):
|
|
227
|
+
"""The eager op for a differentiable call that the graphs do not serve. It saves what a
|
|
228
|
+
graphed call saves (a token, the bank parameter and the inputs) and keeps no other state:
|
|
229
|
+
the backward runs the forward again with autograd."""
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def forward(ctx, call, param, *inputs):
|
|
233
|
+
with _untracked():
|
|
234
|
+
readings, bank = _write_read(param, call[0], call[1], inputs, call[2], training=True)
|
|
235
|
+
ctx.call = call
|
|
236
|
+
ctx.save_for_backward(torch.tensor(0), param, *inputs)
|
|
237
|
+
ctx.mark_non_differentiable(bank)
|
|
238
|
+
return readings, bank
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
@once_differentiable
|
|
242
|
+
def backward(ctx, grad, _grad_bank):
|
|
243
|
+
_, param, *inputs = ctx.saved_tensors
|
|
244
|
+
return (None, *_recomputed_grads(ctx.call, param, inputs, grad,
|
|
245
|
+
ctx.needs_input_grad[1:]))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class _Entry:
|
|
249
|
+
"""Static buffers, graphs and state of one captured key, used by the layers of one slot."""
|
|
250
|
+
|
|
251
|
+
def __init__(self, runner, key, call, pool, handle, static_param, static_in, ctx, forward,
|
|
252
|
+
bank, readings):
|
|
253
|
+
self.runners = weakref.WeakSet((runner,)) # the layers that use it
|
|
254
|
+
self.key, self.call, self.pool, self.handle = key, call, pool, handle
|
|
255
|
+
self.static_param, self.static_in, self.ctx = static_param, static_in, ctx
|
|
256
|
+
self.forward, self.bank, self.readings = forward, bank, readings
|
|
257
|
+
self.grad_in = torch.empty_like(self.readings, memory_format=torch.contiguous_format)
|
|
258
|
+
self.backward = self.grad_out = None # captured on the first backward
|
|
259
|
+
self.backward_eager = False # set when that capture fails
|
|
260
|
+
self.backward_tf32 = None # torch.backends.cuda.matmul.allow_tf32 at that capture
|
|
261
|
+
self.index = None # position in pool.forward when the forward graph is shared
|
|
262
|
+
self.slot = None # its slot when shared by layers in checkpoint regions (_acquire)
|
|
263
|
+
self.token = None # the owner: token of the replay whose state is live
|
|
264
|
+
self.released = False
|
|
265
|
+
|
|
266
|
+
def release(self):
|
|
267
|
+
"""Drops the graphs and static buffers; later backwards of this entry run eager."""
|
|
268
|
+
self.released, self.token = True, None
|
|
269
|
+
self.forward = self.backward = self.ctx = self.bank = self.readings = None
|
|
270
|
+
self.static_param = self.static_in = self.grad_in = self.grad_out = None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
_STALE = ("SparseDeltaMemory(cuda_graph=True): the final bank of this replayed call was overwritten "
|
|
274
|
+
"before its state was first read (by a later forward of a layer sharing its CUDA graphs, "
|
|
275
|
+
"of this layer or of a layer captured before it, or by its backward). Read "
|
|
276
|
+
"new_cache.memory right after the call and keep that copy (or a clone), or run the "
|
|
277
|
+
"call under suspend_cuda_graphs().")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ReplayedState(SDMLayerState):
|
|
281
|
+
"""A replayed call's ``new_cache``: the first read of ``memory`` copies the call's final bank (never the static one);
|
|
282
|
+
it raises once a later replay that may write the bank, or the backward, has run."""
|
|
283
|
+
|
|
284
|
+
def __init__(self, entry, token, seq_len=0):
|
|
285
|
+
self._entry, self._token, self._copy = weakref.ref(entry), token, None
|
|
286
|
+
self._cache_len = seq_len
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def memory(self):
|
|
290
|
+
if self._copy is None:
|
|
291
|
+
e = self._entry() if self._entry is not None else None
|
|
292
|
+
if e is None or e.released:
|
|
293
|
+
raise RuntimeError(_STALE)
|
|
294
|
+
with _ordered(e.pool):
|
|
295
|
+
if e.token != self._token or e.released:
|
|
296
|
+
raise RuntimeError(_STALE)
|
|
297
|
+
self._copy = e.bank.clone()
|
|
298
|
+
self._entry = None
|
|
299
|
+
return self._copy
|
|
300
|
+
|
|
301
|
+
@memory.setter
|
|
302
|
+
def memory(self, value):
|
|
303
|
+
self._copy, self._entry = value, None
|
|
304
|
+
|
|
305
|
+
def update_(self, new_memory, seq_len=1):
|
|
306
|
+
self.memory = new_memory.detach() # without reading the replaced bank
|
|
307
|
+
self._cache_len += seq_len
|
|
308
|
+
return self
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class CudaGraphRunner:
|
|
312
|
+
"""Runs the write/read of one layer from CUDA graphs. ``stats`` counts captures, replays,
|
|
313
|
+
backward-time forward replays (``stale_replay``) and eager fallbacks (``eager_*``)."""
|
|
314
|
+
|
|
315
|
+
def __init__(self, name: str = "SparseDeltaMemory"):
|
|
316
|
+
self.name = name
|
|
317
|
+
self.entry = None
|
|
318
|
+
self.capture_failed = False
|
|
319
|
+
self.stats = Counter()
|
|
320
|
+
self._warned = set()
|
|
321
|
+
_runners.add(self)
|
|
322
|
+
|
|
323
|
+
def __deepcopy__(self, memo):
|
|
324
|
+
# Graphs cannot be copied; the copy captures its own.
|
|
325
|
+
return CudaGraphRunner(self.name)
|
|
326
|
+
|
|
327
|
+
def __reduce__(self):
|
|
328
|
+
# Nor pickled (e.g. torch.save of a whole module); the restored layer captures its own.
|
|
329
|
+
return CudaGraphRunner, (self.name,)
|
|
330
|
+
|
|
331
|
+
def note(self, reason: str, message: str) -> None:
|
|
332
|
+
"""Counts ``reason`` and logs ``message`` the first time ``reason`` occurs here."""
|
|
333
|
+
self.stats[reason] += 1
|
|
334
|
+
if reason not in self._warned:
|
|
335
|
+
self._warned.add(reason)
|
|
336
|
+
logger.warning("%s(cuda_graph=True): %s", self.name, message)
|
|
337
|
+
|
|
338
|
+
def __call__(self, param, bank_shape, dtype, inputs, scalars, seq_len=None):
|
|
339
|
+
"""Readings and final bank of ``SDMWriteRead.apply(bank, *inputs, *scalars)`` for a
|
|
340
|
+
call that may need a backward: replayed from the graphs, or ``eager`` (another key
|
|
341
|
+
than the captured one, or a failed capture).
|
|
342
|
+
|
|
343
|
+
The bank of shape ``bank_shape`` = [B * S, D] and dtype ``dtype`` starts as B copies
|
|
344
|
+
of ``param`` [S, D], or as zeros when ``param`` is None. A replayed call returns the
|
|
345
|
+
static bank: it holds the final state until the backward rewinds it or a forward
|
|
346
|
+
replay of a layer sharing the entry, or of an entry captured before it, overwrites it.
|
|
347
|
+
With ``seq_len`` (the layer's calls), the second output is the call's state instead:
|
|
348
|
+
a ``ReplayedState`` for a replayed call, which never hands out the static bank.
|
|
349
|
+
"""
|
|
350
|
+
if self.capture_failed:
|
|
351
|
+
self.stats["eager_capture_failed"] += 1
|
|
352
|
+
return self._eager_out(seq_len, param, bank_shape, dtype, inputs, scalars)
|
|
353
|
+
key = (None if param is None else (param.shape, param.dtype),
|
|
354
|
+
tuple(bank_shape), dtype,
|
|
355
|
+
tuple((x.shape, x.dtype, x.device) for x in inputs),
|
|
356
|
+
tuple(scalars), torch.backends.cuda.matmul.allow_tf32)
|
|
357
|
+
e = self.entry
|
|
358
|
+
if e is None:
|
|
359
|
+
with _untracked():
|
|
360
|
+
e = self._acquire(key, param, bank_shape, dtype, inputs, scalars)
|
|
361
|
+
if e is None:
|
|
362
|
+
return self._eager_out(seq_len, param, bank_shape, dtype, inputs, scalars)
|
|
363
|
+
self.entry = e
|
|
364
|
+
elif e.key != key:
|
|
365
|
+
self.note("eager_shape",
|
|
366
|
+
"a call that differs from the captured one (shapes, dtypes, op settings "
|
|
367
|
+
"or torch.backends.cuda.matmul.allow_tf32) runs eager. After a permanent "
|
|
368
|
+
"change, call cuda_graph.reset_cuda_graphs() between steps to capture "
|
|
369
|
+
"the new call.")
|
|
370
|
+
return self._eager_out(seq_len, param, bank_shape, dtype, inputs, scalars)
|
|
371
|
+
elif e.slot is not None:
|
|
372
|
+
used = _region_entries()
|
|
373
|
+
if used is not None: # the region's later new layers take other slots
|
|
374
|
+
used.add(e)
|
|
375
|
+
with e.pool.lock:
|
|
376
|
+
if not e.released: # (another thread's out-of-memory backward releases it)
|
|
377
|
+
token = []
|
|
378
|
+
readings = _Graphed.apply(self, e, token, param, *inputs)
|
|
379
|
+
return readings, (e.bank if seq_len is None else
|
|
380
|
+
ReplayedState(e, token[0], seq_len))
|
|
381
|
+
self.stats["eager_released"] += 1
|
|
382
|
+
return self._eager_out(seq_len, param, bank_shape, dtype, inputs, scalars)
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def eager(param, bank_shape, dtype, inputs, scalars):
|
|
386
|
+
"""``__call__`` on the eager path (``_EagerSaved``)."""
|
|
387
|
+
return _EagerSaved.apply((tuple(bank_shape), dtype, tuple(scalars)), param, *inputs)
|
|
388
|
+
|
|
389
|
+
def _eager_out(self, seq_len, *call):
|
|
390
|
+
readings, bank = self.eager(*call)
|
|
391
|
+
return readings, (bank if seq_len is None else SDMLayerState(memory=bank, seq_len=seq_len))
|
|
392
|
+
|
|
393
|
+
def _acquire(self, key, param, bank_shape, dtype, inputs, scalars):
|
|
394
|
+
"""The entry of this layer at its first graphed call: in a checkpoint region, the shared entry of the first slot of
|
|
395
|
+
``key`` no other call of the region uses; elsewhere its own. None when the capture fails."""
|
|
396
|
+
pool = _stream_pool(inputs[0].device)
|
|
397
|
+
used = _region_entries()
|
|
398
|
+
with pool.lock:
|
|
399
|
+
if used is None:
|
|
400
|
+
return self._capture_forward(key, pool, param, bank_shape, dtype, inputs, scalars)
|
|
401
|
+
slot = 0
|
|
402
|
+
with _shared_lock:
|
|
403
|
+
while (e := _shared.get((pool, key, slot))) is not None and e in used:
|
|
404
|
+
slot += 1
|
|
405
|
+
if e is None: # the pool's lock keeps the slot free during the capture
|
|
406
|
+
e = self._capture_forward(key, pool, param, bank_shape, dtype, inputs, scalars)
|
|
407
|
+
if e is None:
|
|
408
|
+
return None
|
|
409
|
+
e.slot = slot
|
|
410
|
+
with _shared_lock:
|
|
411
|
+
_shared[(pool, key, slot)] = e
|
|
412
|
+
else:
|
|
413
|
+
e.runners.add(self)
|
|
414
|
+
self.stats["forward_shared"] += 1
|
|
415
|
+
used.add(e)
|
|
416
|
+
return e
|
|
417
|
+
|
|
418
|
+
def _capture_forward(self, key, pool, param, bank_shape, dtype, inputs, scalars):
|
|
419
|
+
device = inputs[0].device
|
|
420
|
+
contiguous = torch.contiguous_format
|
|
421
|
+
|
|
422
|
+
def failed(exc):
|
|
423
|
+
self.capture_failed = True
|
|
424
|
+
self.note("eager_capture_failed",
|
|
425
|
+
f"capturing the write/read failed ({_failure(exc)}); this layer runs "
|
|
426
|
+
"eager until cuda_graph.reset_cuda_graphs().")
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
# The static copies, and an eager call on them that builds extensions, compiles
|
|
430
|
+
# and autotunes kernels and creates lazy state outside the capture. Running out
|
|
431
|
+
# of memory here falls back to eager, which needs less; any other error is the
|
|
432
|
+
# call's own (the eager path raises it too).
|
|
433
|
+
static_param = (None if param is None else
|
|
434
|
+
param.detach().to(dtype=dtype, memory_format=contiguous, copy=True))
|
|
435
|
+
static_in = tuple(x.detach().clone(memory_format=contiguous) for x in inputs)
|
|
436
|
+
|
|
437
|
+
def forward(ctx):
|
|
438
|
+
if static_param is None:
|
|
439
|
+
bank = torch.zeros(bank_shape, device=device, dtype=dtype)
|
|
440
|
+
else:
|
|
441
|
+
bank = expand_memory_batch(static_param, bank_shape[0] // len(static_param))
|
|
442
|
+
return bank, SDMWriteRead.forward(ctx, bank, *static_in, *scalars)
|
|
443
|
+
|
|
444
|
+
forward(_Ctx())
|
|
445
|
+
except torch.cuda.OutOfMemoryError as exc:
|
|
446
|
+
failed(exc)
|
|
447
|
+
return None
|
|
448
|
+
shared = not pool.has_backward
|
|
449
|
+
handle = pool.handle if shared else torch.cuda.graph_pool_handle()
|
|
450
|
+
ctx = _Ctx()
|
|
451
|
+
try:
|
|
452
|
+
graph, (bank, readings) = _capture(lambda: forward(ctx), handle)
|
|
453
|
+
e = _Entry(self, key, (tuple(bank_shape), dtype, tuple(scalars)), pool, handle,
|
|
454
|
+
static_param, static_in, ctx, graph, bank, readings)
|
|
455
|
+
except UnsupportedConfigurationError: # a refusal is the call's own error, never a capture failure
|
|
456
|
+
raise
|
|
457
|
+
except Exception as exc: # any failure of the capture itself falls back to eager
|
|
458
|
+
failed(exc)
|
|
459
|
+
return None
|
|
460
|
+
if shared:
|
|
461
|
+
e.index = len(pool.forward)
|
|
462
|
+
pool.forward.append(weakref.ref(e))
|
|
463
|
+
self.stats["forward_capture"] += 1
|
|
464
|
+
return e
|
|
465
|
+
|
|
466
|
+
def _replay_forward(self, e, param, inputs):
|
|
467
|
+
if param is not None:
|
|
468
|
+
e.static_param.copy_(param)
|
|
469
|
+
for static, x in zip(e.static_in, inputs):
|
|
470
|
+
static.copy_(x)
|
|
471
|
+
e.forward.replay()
|
|
472
|
+
e.token = next(_tokens)
|
|
473
|
+
if e.index is not None:
|
|
474
|
+
for ref in e.pool.forward[e.index + 1:]:
|
|
475
|
+
later = ref()
|
|
476
|
+
if later is not None:
|
|
477
|
+
later.token = None
|
|
478
|
+
return e.token
|
|
479
|
+
|
|
480
|
+
def _backward_body(self, e):
|
|
481
|
+
grads = SDMWriteRead.backward(e.ctx, e.grad_in)
|
|
482
|
+
grad_param = None
|
|
483
|
+
if e.static_param is not None:
|
|
484
|
+
g = grads[0]
|
|
485
|
+
grad_param = triton_sum_dim0(g.view(-1, len(e.static_param), g.shape[1]))
|
|
486
|
+
return (grad_param, *grads[1:1 + len(e.static_in)])
|
|
487
|
+
|
|
488
|
+
def _backward(self, e, token, grad, param, inputs, needs):
|
|
489
|
+
abandoned = False
|
|
490
|
+
with _ordered(e.pool):
|
|
491
|
+
if not e.released:
|
|
492
|
+
if e.token != token:
|
|
493
|
+
self.note("stale_replay",
|
|
494
|
+
"the graph state of a forward was overwritten before its backward "
|
|
495
|
+
"(a forward of this layer or of a layer sharing its graphs, or of a "
|
|
496
|
+
"layer captured before it, ran in between, or the backward runs "
|
|
497
|
+
"twice), so the backward replays that forward first. Each such "
|
|
498
|
+
"backward costs one more forward.")
|
|
499
|
+
with _untracked():
|
|
500
|
+
self._replay_forward(e, param, inputs)
|
|
501
|
+
e.token = None # the backward rewinds the bank in place
|
|
502
|
+
e.grad_in.copy_(grad)
|
|
503
|
+
grads = self._backward_graphs(e)
|
|
504
|
+
if grads is not None:
|
|
505
|
+
# Never hand out graph memory: later replays overwrite it, and autograd
|
|
506
|
+
# may keep an incoming gradient as .grad.
|
|
507
|
+
return tuple(None if t is None else t.clone() for t in grads)
|
|
508
|
+
self._abandon(e)
|
|
509
|
+
abandoned = True
|
|
510
|
+
if abandoned:
|
|
511
|
+
torch.cuda.empty_cache()
|
|
512
|
+
else: # released by an earlier out-of-memory backward
|
|
513
|
+
self.stats["eager_backward"] += 1
|
|
514
|
+
return _recomputed_grads(e.call, param, inputs, grad, needs)
|
|
515
|
+
|
|
516
|
+
def _abandon(self, e):
|
|
517
|
+
"""The first backward ran out of memory (called under the pool's lock). Drop the graphs
|
|
518
|
+
and buffers of the entry and of every entry sharing its memory pool, so that the pool
|
|
519
|
+
can be freed: the layers that use them run eager from now on (pending backwards from
|
|
520
|
+
their saved inputs). The caller then computes its gradients eagerly."""
|
|
521
|
+
# join this backward's stream before the pool's buffers are freed
|
|
522
|
+
e.pool.stream.wait_stream(torch.cuda.current_stream(e.pool.stream.device))
|
|
523
|
+
entries = [e]
|
|
524
|
+
if e.index is not None:
|
|
525
|
+
entries += [ent for ent in (ref() for ref in e.pool.forward)
|
|
526
|
+
if ent is not None and ent is not e]
|
|
527
|
+
with _pools_lock:
|
|
528
|
+
for key, pool in list(_pools.items()):
|
|
529
|
+
if pool is e.pool:
|
|
530
|
+
del _pools[key]
|
|
531
|
+
with _shared_lock:
|
|
532
|
+
for key, ent in list(_shared.items()):
|
|
533
|
+
if any(ent is x for x in entries):
|
|
534
|
+
del _shared[key]
|
|
535
|
+
for ent in entries:
|
|
536
|
+
runners = list(ent.runners)
|
|
537
|
+
ent.release()
|
|
538
|
+
for runner in runners:
|
|
539
|
+
runner.capture_failed = True
|
|
540
|
+
if runner.entry is ent:
|
|
541
|
+
runner.entry = None
|
|
542
|
+
runner.note("eager_capture_failed",
|
|
543
|
+
"the first backward of this layer, or of a layer that shares its "
|
|
544
|
+
"graphs or their memory, ran out of memory; this layer runs eager "
|
|
545
|
+
"until cuda_graph.reset_cuda_graphs().")
|
|
546
|
+
|
|
547
|
+
def _backward_graphs(self, e):
|
|
548
|
+
"""The backward outputs on the static state, or None when the first backward runs out
|
|
549
|
+
of memory."""
|
|
550
|
+
tf32 = torch.backends.cuda.matmul.allow_tf32
|
|
551
|
+
if e.backward is None and not e.backward_eager:
|
|
552
|
+
# First backward: eager on the static state (the exact result of this call, and
|
|
553
|
+
# the kernels get compiled), then capture.
|
|
554
|
+
try:
|
|
555
|
+
grads = self._backward_body(e)
|
|
556
|
+
except torch.cuda.OutOfMemoryError:
|
|
557
|
+
return None
|
|
558
|
+
try:
|
|
559
|
+
e.backward, e.grad_out = _capture(lambda: self._backward_body(e), e.handle)
|
|
560
|
+
except Exception as exc: # keep the forward graph; the backward runs eager
|
|
561
|
+
e.backward_eager = True
|
|
562
|
+
self.note("eager_backward",
|
|
563
|
+
f"capturing the backward failed ({_failure(exc)}); it runs eager.")
|
|
564
|
+
else:
|
|
565
|
+
e.backward_tf32 = tf32
|
|
566
|
+
if e.index is not None:
|
|
567
|
+
e.pool.has_backward = True
|
|
568
|
+
self.stats["backward_capture"] += 1
|
|
569
|
+
elif e.backward_eager or e.backward_tf32 != tf32:
|
|
570
|
+
# Eager on the static state: the capture failed, or the matmul TF32 setting
|
|
571
|
+
# differs from that of the capture.
|
|
572
|
+
if not e.backward_eager:
|
|
573
|
+
self.note("eager_backward_tf32",
|
|
574
|
+
"torch.backends.cuda.matmul.allow_tf32 differs from that of the "
|
|
575
|
+
"backward capture; the backward runs eager.")
|
|
576
|
+
grads = self._backward_body(e)
|
|
577
|
+
self.stats["eager_backward"] += 1
|
|
578
|
+
else:
|
|
579
|
+
e.backward.replay()
|
|
580
|
+
grads = e.grad_out
|
|
581
|
+
self.stats["backward_replay"] += 1
|
|
582
|
+
return grads
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class _Graphed(torch.autograd.Function):
|
|
586
|
+
@staticmethod
|
|
587
|
+
def forward(ctx, runner, entry, box, param, *inputs):
|
|
588
|
+
with _untracked(), _ordered(entry.pool):
|
|
589
|
+
token = runner._replay_forward(entry, param, inputs)
|
|
590
|
+
readings = entry.readings.clone()
|
|
591
|
+
box.append(token) # the caller's ReplayedState
|
|
592
|
+
runner.stats["forward_replay"] += 1
|
|
593
|
+
ctx.runner, ctx.entry = runner, entry
|
|
594
|
+
# The token is a tensor, so that a non-reentrant checkpoint recompute substitutes its
|
|
595
|
+
# own. The bank parameter and inputs let a backward whose state was overwritten
|
|
596
|
+
# replay this forward.
|
|
597
|
+
ctx.save_for_backward(torch.tensor(token), param, *inputs)
|
|
598
|
+
return readings
|
|
599
|
+
|
|
600
|
+
@staticmethod
|
|
601
|
+
@once_differentiable
|
|
602
|
+
def backward(ctx, grad):
|
|
603
|
+
token, param, *inputs = ctx.saved_tensors # unpacking first runs any pending recompute
|
|
604
|
+
return (None, None, None, *ctx.runner._backward(ctx.entry, token.item(), grad, param,
|
|
605
|
+
inputs, ctx.needs_input_grad[3:]))
|