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.
Files changed (40) hide show
  1. sparse_delta_memory/__init__.py +13 -0
  2. sparse_delta_memory/backward.py +367 -0
  3. sparse_delta_memory/cache.py +70 -0
  4. sparse_delta_memory/cuda/__init__.py +3 -0
  5. sparse_delta_memory/cuda/_build.py +254 -0
  6. sparse_delta_memory/cuda/_device.py +158 -0
  7. sparse_delta_memory/cuda/_devmemo.py +64 -0
  8. sparse_delta_memory/cuda/_image.py +128 -0
  9. sparse_delta_memory/cuda/bwd_steps_cuda.py +111 -0
  10. sparse_delta_memory/cuda/gather_cuda.py +796 -0
  11. sparse_delta_memory/cuda/gather_ns_cuda.py +134 -0
  12. sparse_delta_memory/cuda/gx_gds_cuda.py +1297 -0
  13. sparse_delta_memory/cuda/gx_gemm_cuda.py +857 -0
  14. sparse_delta_memory/cuda/gx_pa2_cuda.py +1320 -0
  15. sparse_delta_memory/cuda/mover_da2_cuda.py +1802 -0
  16. sparse_delta_memory/cuda/mover_fx_cuda.py +2229 -0
  17. sparse_delta_memory/cuda/pa_dc_cuda.py +1500 -0
  18. sparse_delta_memory/cuda/phase1_bwd_cuda.py +852 -0
  19. sparse_delta_memory/cuda/phase1_common.cuh +181 -0
  20. sparse_delta_memory/cuda/phase1_cuda.py +3801 -0
  21. sparse_delta_memory/cuda/product_key_topk_anyf_cuda.py +668 -0
  22. sparse_delta_memory/cuda/product_key_topk_cuda.py +735 -0
  23. sparse_delta_memory/cuda/px_qrec_cuda.py +167 -0
  24. sparse_delta_memory/cuda/sdm_copy.cuh +90 -0
  25. sparse_delta_memory/cuda/sdm_plancap.cuh +150 -0
  26. sparse_delta_memory/cuda_graph.py +605 -0
  27. sparse_delta_memory/decode.py +140 -0
  28. sparse_delta_memory/forward.py +464 -0
  29. sparse_delta_memory/layer.py +663 -0
  30. sparse_delta_memory/memory_ops.py +1069 -0
  31. sparse_delta_memory/memory_stats.py +221 -0
  32. sparse_delta_memory/movers.py +198 -0
  33. sparse_delta_memory/phase2_kernels.py +267 -0
  34. sparse_delta_memory/product_key_topk.py +113 -0
  35. sparse_delta_memory/wy_solve.py +273 -0
  36. sparse_delta_memory-2.0.0.dist-info/METADATA +482 -0
  37. sparse_delta_memory-2.0.0.dist-info/RECORD +40 -0
  38. sparse_delta_memory-2.0.0.dist-info/WHEEL +5 -0
  39. sparse_delta_memory-2.0.0.dist-info/licenses/LICENSE +407 -0
  40. 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:]))