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,254 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """_build: lazy, content-addressed JIT builds of the package's CUDA extensions.
3
+
4
+ The build directory and the imported module are keyed only by the extension name. A
5
+ process that loses the build lock imports whatever shared object the winner produced
6
+ without checking that it came from the same sources. Every extension here is therefore
7
+ named ``<base>_<sha256(build inputs)[:16]>``, where the build inputs are the sources,
8
+ the headers, the exported bindings, all compiler flags, the torch/CUDA/Python versions,
9
+ the CUDA toolkit location and the target GPU architectures. Identical inputs share one
10
+ cache entry; anything else gets its own directory, so a stale ``.so`` is never reused
11
+ and two source trees can share one ``TORCH_EXTENSIONS_DIR``.
12
+
13
+ Each entry of ``cuda_sources`` is its own translation unit, so ninja compiles them in
14
+ parallel (``MAX_JOBS`` caps the job count). ``headers`` are written next to the sources,
15
+ so any unit can ``#include "<name>"``. The toolkit's CCCL directory (CUB, Thrust,
16
+ libcu++) is on the include path.
17
+
18
+ Builds happen on the first call, never at import. Call each op once eagerly (for
19
+ example in a warmup step) before capturing a CUDA graph so that no compilation is
20
+ triggered during capture."""
21
+
22
+ from __future__ import annotations
23
+
24
+ import hashlib
25
+ import json
26
+ import os
27
+ import shutil
28
+ import sys
29
+ import tempfile
30
+ import threading
31
+ import warnings
32
+ from collections.abc import Mapping, Sequence
33
+ from types import ModuleType
34
+
35
+ import torch
36
+ from torch.utils import cpp_extension
37
+
38
+ from . import _image
39
+
40
+ __all__ = ["load_cuda_extension"]
41
+
42
+ _LOCK = threading.Lock()
43
+
44
+
45
+ def _as_tuple(value: str | Sequence[str]) -> tuple[str, ...]:
46
+ return (value,) if isinstance(value, str) else tuple(value)
47
+
48
+
49
+ def _require_toolchain() -> str:
50
+ """Return CUDA_HOME, or raise a readable error if the toolkit is unusable."""
51
+ if not torch.cuda.is_available() or torch.version.cuda is None:
52
+ raise RuntimeError(
53
+ "The SDM CUDA kernels need a CUDA build of PyTorch and a visible GPU."
54
+ )
55
+ cuda_home = cpp_extension.CUDA_HOME
56
+ if cuda_home is None:
57
+ raise RuntimeError(
58
+ "The SDM CUDA kernels are JIT-compiled and need the CUDA toolkit, but "
59
+ "PyTorch could not locate it. Set CUDA_HOME to a toolkit that matches "
60
+ f"torch.version.cuda={torch.version.cuda} (it must contain bin/nvcc)."
61
+ )
62
+ nvcc = os.environ.get("PYTORCH_NVCC") or os.path.join(cuda_home, "bin", "nvcc")
63
+ if not (os.path.exists(nvcc) or shutil.which(nvcc)):
64
+ raise RuntimeError(
65
+ f"nvcc was not found at {nvcc!r}. Set CUDA_HOME to a full CUDA toolkit "
66
+ f"matching torch.version.cuda={torch.version.cuda}."
67
+ )
68
+ return cuda_home
69
+
70
+
71
+ def toolkit() -> tuple:
72
+ """The toolkit an extension build uses (cpp_extension.CUDA_HOME, PYTORCH_NVCC): part of _image's build environment."""
73
+ return cpp_extension.CUDA_HOME, os.environ.get("PYTORCH_NVCC")
74
+
75
+
76
+ def _cccl_include_paths(cuda_home: str) -> tuple[str, ...]:
77
+ """CUDA 13 moved CUB/Thrust/libcu++ to ``include/cccl``; older toolkits keep them
78
+ directly under ``include``, which PyTorch already adds."""
79
+ path = os.path.join(cuda_home, "include", "cccl")
80
+ return (path,) if os.path.isdir(path) else ()
81
+
82
+
83
+ def _target_architectures() -> dict[str, object]:
84
+ """The inputs PyTorch uses to choose the -gencode flags."""
85
+ capabilities = sorted(
86
+ {torch.cuda.get_device_capability(i) for i in range(torch.cuda.device_count())}
87
+ )
88
+ return {
89
+ "TORCH_CUDA_ARCH_LIST": os.environ.get("TORCH_CUDA_ARCH_LIST"),
90
+ "capabilities": [list(c) for c in capabilities],
91
+ }
92
+
93
+
94
+ def _header_items(headers: Mapping[str, str] | None) -> tuple[tuple[str, str], ...]:
95
+ items = tuple(sorted((headers or {}).items()))
96
+ for file_name, _ in items:
97
+ if not file_name or os.path.basename(file_name) != file_name:
98
+ raise ValueError(f"header names must be plain file names, got {file_name!r}")
99
+ return items
100
+
101
+
102
+ def _extension_name(
103
+ base: str,
104
+ *,
105
+ cpp_sources: str | Sequence[str],
106
+ cuda_sources: str | Sequence[str],
107
+ functions: Sequence[str],
108
+ headers: Mapping[str, str] | None = None,
109
+ extra_cflags: Sequence[str] = (),
110
+ extra_cuda_cflags: Sequence[str] = (),
111
+ extra_ldflags: Sequence[str] = (),
112
+ extra_include_paths: Sequence[str] = (),
113
+ ) -> str:
114
+ """Return ``<base>_<16 hex digits>`` identifying one exact build."""
115
+ if not base.isidentifier():
116
+ raise ValueError(f"extension base name must be an identifier, got {base!r}")
117
+ cuda_home = _require_toolchain()
118
+ identity = {
119
+ "sources": {"cpp": _as_tuple(cpp_sources), "cuda": _as_tuple(cuda_sources)},
120
+ "headers": _header_items(headers),
121
+ "functions": tuple(functions),
122
+ "flags": {
123
+ "cflags": tuple(extra_cflags),
124
+ "cuda_cflags": tuple(extra_cuda_cflags),
125
+ "ldflags": tuple(extra_ldflags),
126
+ "include_paths": tuple(extra_include_paths),
127
+ },
128
+ "torch": torch.__version__,
129
+ "torch_cuda": torch.version.cuda,
130
+ "python": sys.implementation.cache_tag,
131
+ "cuda_home": os.path.realpath(cuda_home),
132
+ "arch": _target_architectures(),
133
+ }
134
+ nvcc_env = {k: os.environ[k] for k in ("NVCC_APPEND_FLAGS", "NVCC_PREPEND_FLAGS") if os.environ.get(k)}
135
+ if nvcc_env: # nvcc's environment flags shape the targets: a build under them is its own build (_image)
136
+ identity["nvcc_env"] = nvcc_env
137
+ payload = json.dumps(identity, sort_keys=True, separators=(",", ":"))
138
+ return f"{base}_{hashlib.sha256(payload.encode('utf-8')).hexdigest()[:16]}"
139
+
140
+
141
+ def _warn_if_locked(build_dir: str) -> None:
142
+ """A build killed mid-compile leaves a lock file that makes later loads wait forever."""
143
+ lock = os.path.join(build_dir, "lock")
144
+ if os.path.exists(lock):
145
+ warnings.warn(
146
+ f"{lock} exists. If no other process is currently building this "
147
+ "extension, delete that file; otherwise this load will wait for it.",
148
+ stacklevel=3,
149
+ )
150
+
151
+
152
+ def _write_if_changed(path: str, text: str) -> str:
153
+ """Write atomically, and leave an identical file untouched so ninja skips it."""
154
+ try:
155
+ with open(path, encoding="utf-8") as f:
156
+ if f.read() == text:
157
+ return path
158
+ except FileNotFoundError:
159
+ pass
160
+ fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix=".tmp_")
161
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
162
+ f.write(text)
163
+ os.replace(tmp, path)
164
+ return path
165
+
166
+
167
+ def _bindings_source(cpp_sources: tuple[str, ...], functions: tuple[str, ...]) -> str:
168
+ """The C++ unit holding the declarations and the pybind11 module."""
169
+ defs = [f' m.def("{f}", torch::wrap_pybind_function({f}), "{f}");' for f in functions]
170
+ return "\n".join(
171
+ ["#include <torch/extension.h>", *cpp_sources,
172
+ "PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {", *defs, "}", ""]
173
+ )
174
+
175
+
176
+ def load_cuda_extension(
177
+ name: str,
178
+ *,
179
+ cpp_sources: str | Sequence[str],
180
+ cuda_sources: str | Sequence[str],
181
+ functions: Sequence[str],
182
+ headers: Mapping[str, str] | None = None,
183
+ extra_cflags: Sequence[str] = (),
184
+ extra_cuda_cflags: Sequence[str] = (),
185
+ extra_ldflags: Sequence[str] = (),
186
+ extra_include_paths: Sequence[str] = (),
187
+ verbose: bool = False,
188
+ ) -> ModuleType:
189
+ """Build (on first use) and import a JIT-compiled CUDA extension.
190
+
191
+ Args:
192
+ name: identifier prefix of the extension; the content hash is appended.
193
+ cpp_sources: C++ declarations of the bound functions.
194
+ cuda_sources: CUDA translation unit(s); each string is compiled separately.
195
+ functions: names to expose through pybind11; each must be declared in
196
+ ``cpp_sources``. The loaded module is checked to export all of them.
197
+ headers: ``{file name: text}`` of headers the units include as
198
+ ``#include "<file name>"``.
199
+ extra_cflags / extra_cuda_cflags / extra_ldflags / extra_include_paths:
200
+ forwarded to the compiler and linker and included in the content hash.
201
+ verbose: forwarded to ``torch.utils.cpp_extension.load``.
202
+
203
+ Returns the imported module. Callers cache it: each call hashes the inputs and loads
204
+ the extension.
205
+ """
206
+ cpp_sources, cuda_sources = _as_tuple(cpp_sources), _as_tuple(cuda_sources)
207
+ functions = tuple(functions)
208
+ header_items = _header_items(headers)
209
+ if not functions:
210
+ raise ValueError("load_cuda_extension needs at least one bound function")
211
+ if not cuda_sources:
212
+ raise ValueError("load_cuda_extension needs at least one CUDA source")
213
+ full_name = _extension_name(
214
+ name,
215
+ cpp_sources=cpp_sources,
216
+ cuda_sources=cuda_sources,
217
+ functions=functions,
218
+ headers=headers,
219
+ extra_cflags=extra_cflags,
220
+ extra_cuda_cflags=extra_cuda_cflags,
221
+ extra_ldflags=extra_ldflags,
222
+ extra_include_paths=extra_include_paths,
223
+ )
224
+ with _LOCK:
225
+ build_dir = cpp_extension._get_build_directory(full_name, verbose)
226
+ _warn_if_locked(build_dir)
227
+ for file_name, text in header_items:
228
+ _write_if_changed(os.path.join(build_dir, file_name), text)
229
+ sources = [_write_if_changed(os.path.join(build_dir, "main.cpp"),
230
+ _bindings_source(cpp_sources, functions))]
231
+ sources += [_write_if_changed(os.path.join(build_dir, f"unit{i}.cu"), text)
232
+ for i, text in enumerate(cuda_sources)]
233
+ module = cpp_extension.load(
234
+ name=full_name,
235
+ sources=sources,
236
+ extra_cflags=list(extra_cflags),
237
+ extra_cuda_cflags=list(extra_cuda_cflags),
238
+ extra_ldflags=list(extra_ldflags),
239
+ extra_include_paths=[*_cccl_include_paths(cpp_extension.CUDA_HOME),
240
+ *extra_include_paths],
241
+ build_directory=build_dir,
242
+ verbose=verbose,
243
+ with_cuda=True,
244
+ )
245
+ missing = [f for f in functions if not callable(getattr(module, f, None))]
246
+ if missing:
247
+ raise RuntimeError(
248
+ f"CUDA extension {full_name!r} loaded from "
249
+ f"{getattr(module, '__file__', None)!r} does not export {missing}. "
250
+ "The cached build is incomplete; delete its directory and retry."
251
+ )
252
+ if name != _image.PROBE_BASE:
253
+ _image.require_supported_image() # before any kernel of the extension runs
254
+ return module
@@ -0,0 +1,158 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """_device: the routing predicate (backend(): which kernels and plans serve a device) and the device properties that the
3
+ launch-shape rules scale with: SM count and L2 size, cached per device index.
4
+
5
+ The launch-shape rules were tuned on GB300 (152 SMs, 135528448-byte L2), where they reduce to the tuned constants. They
6
+ choose launch shapes only, never the arithmetic, so no output depends on them."""
7
+
8
+ import contextlib
9
+ import functools
10
+ import os
11
+ import threading
12
+
13
+ import torch
14
+
15
+
16
+ def _index(device) -> int:
17
+ """CUDA device index of `device` (None, an int, a string or a torch.device; unindexed = the current device)."""
18
+ if device is None:
19
+ return torch.cuda.current_device()
20
+ if isinstance(device, int):
21
+ return device
22
+ d = device if isinstance(device, torch.device) else torch.device(device)
23
+ return d.index if d.index is not None else torch.cuda.current_device()
24
+
25
+
26
+ @functools.lru_cache(maxsize=None)
27
+ def _props(index: int):
28
+ p = torch.cuda.get_device_properties(index)
29
+ return int(p.multi_processor_count), int(p.L2_cache_size)
30
+
31
+
32
+ def num_sms(device=None) -> int:
33
+ """Streaming multiprocessors of `device` (default: the current device)."""
34
+ return _FORCED[2] if _FORCED else _props(_index(device))[0]
35
+
36
+
37
+ def l2_bytes(device=None) -> int:
38
+ """L2 cache size in bytes of `device` (default: the current device)."""
39
+ return _FORCED[3] if _FORCED and _FORCED[3] is not None else _props(_index(device))[1]
40
+
41
+
42
+ def prev_pow2(x: int) -> int:
43
+ """Largest power of two <= x (x >= 1)."""
44
+ return 1 << (int(x).bit_length() - 1)
45
+
46
+
47
+ # The ONE routing predicate (the supported hardware matrix; no knob). "modern": H100 / B200 / GB300 (compute capability
48
+ # 9.0 / 10.0 / 10.3 with at least 232,448 bytes of opt-in shared memory per block): the fast kernels as tuned (TMA, PDL,
49
+ # clusters, hot/cold Phase 1). "a100": A100 (8.0, 166,912 bytes): the same kernels through the portable (cp.async) side of
50
+ # their arch guards, no PDL, no clusters, the ordered Phase 1, A100 tile plans. "rtx": RTX 30 / 40 / 50 (8.6 / 8.9 / 12.0,
51
+ # 101,376 bytes): the same portable code at RTX limits (a native sm_120 build compiles the same device branches as sm_86
52
+ # / 89: sdm_copy.cuh). Every other target (below 8.0, or a capability or shared-memory limit not listed): one clear error
53
+ # naming the supported GPUs. Read from torch's device properties only, so a test that patches them forces a row.
54
+ MODERN_CAPS = ((9, 0), (10, 0), (10, 3))
55
+ MODERN_SMEM = 232448
56
+ A100_CAPS, A100_SMEM = ((8, 0),), 166912
57
+ RTX_CAPS, RTX_SMEM = ((8, 6), (8, 9), (12, 0)), 101376
58
+ UNSUPPORTED = ("sparse_delta_memory supports NVIDIA H100 / B200 / GB300 (compute capability 9.0 / 10.0 / 10.3), A100 (8.0) "
59
+ "and RTX 30 / 40 / 50 (8.6 / 8.9 / 12.0); device {i} ({name}) has compute capability {major}.{minor} with "
60
+ "{smem} bytes of opt-in shared memory per block")
61
+
62
+ # The device-side mirror of backend() lives in sdm_copy.cuh (SDM_PORTABLE_ARCH: every device pass but sm_90 / 100 / 103),
63
+ # with the portable copy layer; kernels that guard on the architecture include it (headers()).
64
+ _COPY_HEADER = "sdm_copy.cuh"
65
+ _PLAN_HEADER = "sdm_plancap.cuh" # host-side launch capture of the backward's step plans (bwd_steps_cuda)
66
+
67
+
68
+ @functools.cache
69
+ def headers() -> dict:
70
+ """{file name: text} of the shared headers for load_cuda_extension(headers=...)."""
71
+ out = {}
72
+ for name in (_COPY_HEADER, _PLAN_HEADER):
73
+ with open(os.path.join(os.path.dirname(__file__), name), encoding="utf-8") as f:
74
+ out[name] = f.read()
75
+ return out
76
+
77
+
78
+ def capability(device=None) -> tuple:
79
+ """Compute capability (major, minor) of `device`."""
80
+ return _FORCED[0] if _FORCED else tuple(torch.cuda.get_device_capability(_index(device)))
81
+
82
+
83
+ def _optin(device=None) -> int:
84
+ """The device's own opt-in shared memory per block (bytes): the classification input of backend()."""
85
+ return _FORCED[1] if _FORCED else int(torch.cuda.get_device_properties(_index(device)).shared_memory_per_block_optin)
86
+
87
+
88
+ def backend(device=None) -> str:
89
+ """'modern' | 'a100' | 'rtx' for `device` (see MODERN_CAPS); any other target raises RuntimeError(UNSUPPORTED)."""
90
+ i = _index(device)
91
+ cc, optin = capability(i), _optin(i)
92
+ if cc in MODERN_CAPS and optin >= MODERN_SMEM:
93
+ return "modern"
94
+ if cc in A100_CAPS and optin == A100_SMEM:
95
+ return "a100"
96
+ if cc in RTX_CAPS and optin == RTX_SMEM:
97
+ return "rtx"
98
+ raise RuntimeError(UNSUPPORTED.format(i=i, name=torch.cuda.get_device_name(i), major=cc[0], minor=cc[1], smem=optin))
99
+
100
+
101
+ def cuda_backend(device=None) -> bool:
102
+ """The package's CUDA kernels serve `device`: True on every backend (elsewhere, backend()'s clear error)."""
103
+ return bool(backend(device))
104
+
105
+
106
+ def pdl_ok(device=None) -> bool:
107
+ """Programmatic dependent launch may be requested on `device`: the modern backend only."""
108
+ return backend(device) == "modern"
109
+
110
+
111
+ def tbind(kernel, grid, ref):
112
+ """kernel[grid] for an SDM Triton launch (`ref`: one of its CUDA operands)."""
113
+ return kernel[grid]
114
+
115
+
116
+ # Shared memory per block every plan of a class assumes (modern: 232,448 on any modern part; A100 / RTX exact).
117
+ CLASS_SMEM = {"modern": MODERN_SMEM, "a100": A100_SMEM, "rtx": RTX_SMEM}
118
+
119
+
120
+ def smem_cap(device=None) -> int:
121
+ """Opt-in shared memory per block the plans of `device`'s class use (bytes; CLASS_SMEM)."""
122
+ return CLASS_SMEM[backend(device)]
123
+
124
+
125
+ def smem_per_sm(device=None) -> int:
126
+ """Shared memory per SM of `device`'s class (bytes): smem_cap() plus the 1 KB reserved per block."""
127
+ return smem_cap(device) + RESERVED_SMEM
128
+
129
+
130
+ RESERVED_SMEM = 1024 # shared memory the system reserves per resident block (sm_80+)
131
+
132
+ # _force_row overrides capability(), _optin(), num_sms() and l2_bytes() for every device (process-wide; contexts nest).
133
+ _FORCED = None
134
+ _FORCE_STACK = []
135
+ _FORCE_LOCK = threading.Lock()
136
+
137
+
138
+ @contextlib.contextmanager
139
+ def _force_row(capability, smem_optin, sm_count, l2_bytes=None):
140
+ """TEST ONLY: while active, every device reports this capability, opt-in shared memory, SM count and (given) L2, so a
141
+ test reproduces a hardware-matrix row on another GPU. Contexts must nest (an exit out of order raises)."""
142
+ global _FORCED
143
+ entry = (tuple(int(c) for c in capability), int(smem_optin), int(sm_count),
144
+ None if l2_bytes is None else int(l2_bytes))
145
+ token = object()
146
+ with _FORCE_LOCK:
147
+ _FORCE_STACK.append((token, entry))
148
+ _FORCED = entry
149
+ top = True
150
+ try:
151
+ yield
152
+ finally:
153
+ with _FORCE_LOCK:
154
+ top = bool(_FORCE_STACK) and _FORCE_STACK[-1][0] is token
155
+ _FORCE_STACK[:] = [e for e in _FORCE_STACK if e[0] is not token]
156
+ _FORCED = _FORCE_STACK[-1][1] if _FORCE_STACK else None
157
+ if not top: # outside the finally block: an exception in flight (e.g. ImageRefusal) is never replaced
158
+ raise RuntimeError("_force_row: forcing contexts must nest (this one exited out of order)")
@@ -0,0 +1,64 @@
1
+ """Memoized device-class reads: _device's backend() and capability() serve exactly the originals' values (or raise their
2
+ exceptions, never cached) from one bounded memo keyed by (read, device ordinal, forced test row), so a call makes no
3
+ device query after the device's first one."""
4
+ from . import _device as _dev_mod
5
+ from ._device import CLASS_SMEM, RESERVED_SMEM
6
+
7
+ _DEVCLS: dict = {} # ("backend" | "capability", device ordinal, _device._FORCED) -> the original's value (successful reads only)
8
+
9
+
10
+ def _memo(read, idx, compute):
11
+ """compute(idx) memoized under (read, idx, _device._FORCED); an exception propagates and is never cached."""
12
+ hit = _DEVCLS.get((read, idx, _dev_mod._FORCED))
13
+ if hit is None:
14
+ with _dev_mod._FORCE_LOCK:
15
+ key = (read, idx, _dev_mod._FORCED)
16
+ hit = _DEVCLS.get(key)
17
+ if hit is None:
18
+ hit = compute(idx)
19
+ if len(_DEVCLS) >= 64:
20
+ _DEVCLS.clear()
21
+ _DEVCLS[key] = hit
22
+ return hit
23
+
24
+
25
+ def _raw_capability(idx) -> tuple:
26
+ return tuple(_dev_mod.capability(idx))
27
+
28
+
29
+ def backend(device=None) -> str:
30
+ """_device.backend(device), memoized."""
31
+ return _memo("backend", _dev_mod._index(device), _dev_mod.backend)
32
+
33
+
34
+ def capability(device=None) -> tuple:
35
+ """_device.capability(device) on every row, supported or not (a forced row's directly), memoized."""
36
+ forced = _dev_mod._FORCED
37
+ if forced:
38
+ return forced[0]
39
+ return _memo("capability", _dev_mod._index(device), _raw_capability)
40
+
41
+
42
+ def cuda_backend(device=None) -> bool:
43
+ """_device.cuda_backend(device), memoized."""
44
+ return bool(backend(device))
45
+
46
+
47
+ def pdl_ok(device=None) -> bool:
48
+ """_device.pdl_ok(device), memoized."""
49
+ return backend(device) == "modern"
50
+
51
+
52
+ def smem_cap(device=None) -> int:
53
+ """_device.smem_cap(device), memoized."""
54
+ return CLASS_SMEM[backend(device)]
55
+
56
+
57
+ def smem_per_sm(device=None) -> int:
58
+ """_device.smem_per_sm(device), memoized."""
59
+ return smem_cap(device) + RESERVED_SMEM
60
+
61
+
62
+ def sm90(device=None) -> bool:
63
+ """`device` is an sm_90 part (H100): capability(device) == (9, 0), memoized."""
64
+ return capability(device) == (9, 0)
@@ -0,0 +1,128 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ """_image: PTX-only builds for older GPUs are refused on Hopper / Blackwell GPUs, at every CUDA extension load
3
+ (_build.load_cuda_extension), before any kernel of the extension runs. The modern routes (H100 / B200 / GB300) use device
4
+ features only the modern passes compile (sdm_copy.cuh); the image the runtime loaded for a probe kernel, built like every
5
+ extension, must be one (cudaFuncGetAttributes().ptxVersion). ImageRefusal is a BaseException: no `except Exception`
6
+ fallback catches it. An RTX-class device warns once instead."""
7
+
8
+ import contextlib
9
+ import os
10
+ import threading
11
+ import warnings
12
+
13
+ import torch
14
+
15
+
16
+ class UntestedHardwareWarning(UserWarning):
17
+ """sparse_delta_memory runs on a GPU class this release has not tested (RTX: compute capability 8.6 / 8.9 / 12.0): the
18
+ results are unverified. Issued once per process per device, when the first CUDA extension loads."""
19
+
20
+
21
+ UNTESTED_MESSAGE = ("sparse_delta_memory {version} has not been tested on RTX-class GPUs: device {index} ({name}, compute "
22
+ "capability {major}.{minor}); results are unverified. Tested: A100 / H100 / B200 / GB300 (compute "
23
+ "capability 8.0 / 9.0 / 10.0 / 10.3).")
24
+
25
+
26
+ class ImageRefusal(BaseException):
27
+ """The package refuses to run the CUDA image it loaded (sparse_delta_memory.cuda._image). A BaseException, so that no
28
+ `except Exception` fallback reroutes it: catch it by name. The message names the image, the device and the fix."""
29
+
30
+
31
+ MODERN_PTX = (90, 100, 103) # ptxVersion of the device passes without SDM_PORTABLE_ARCH (sdm_copy.cuh)
32
+ ENV_KEYS = ("TORCH_CUDA_ARCH_LIST", "CUDA_FORCE_PTX_JIT", "CUDA_DISABLE_PTX_JIT", "NVCC_APPEND_FLAGS", "NVCC_PREPEND_FLAGS")
33
+ PROBE_BASE = "sdm_image_probe"
34
+ MESSAGE = ("sparse_delta_memory: PTX-only builds for older GPUs are refused on Hopper / Blackwell GPUs. The CUDA image "
35
+ "loaded on device {index} ({name}, compute capability {major}.{minor}) was compiled for compute_{ptx} "
36
+ "(cudaFuncGetAttributes ptxVersion {ptx}; {env}). Rebuild with TORCH_CUDA_ARCH_LIST including your GPU "
37
+ "(9.0 for H100, 10.0 for B200, 10.3 for GB300), or unset it, and start a new process.")
38
+
39
+ _PROBE = {} # build environment -> the probe extension built under it
40
+ _PTX = {} # (compute capability, build environment) -> the probe's loaded ptxVersion
41
+ _OK = set() # (device ordinal, forced test row, build environment) checked
42
+ _WARNED = set() # (device ordinal, forced test row) given the RTX warning
43
+ _TLS = threading.local() # .depth: _unchecked_build_env in this thread (test only)
44
+
45
+
46
+ def build_env() -> tuple:
47
+ """What the probe's and every extension's images depend on: ENV_KEYS and the toolkit (_build.toolkit)."""
48
+ from ._build import toolkit
49
+ return (*(os.environ.get(k) for k in ENV_KEYS), *toolkit())
50
+
51
+
52
+ def env_summary() -> str:
53
+ return ", ".join(f"{k}={os.environ.get(k)!r}" for k in ENV_KEYS)
54
+
55
+
56
+ _PROBE_CPP = "int64_t sdm_image_ptx();"
57
+ _PROBE_SOURCE = r"""
58
+ #include <torch/extension.h>
59
+ #include <cuda_runtime.h>
60
+ __global__ void sdm_image_probe_kernel(int* p) { if (p != nullptr) *p = 0; }
61
+ int64_t sdm_image_ptx() {
62
+ cudaFuncAttributes attr{};
63
+ const cudaError_t err = cudaFuncGetAttributes(&attr, reinterpret_cast<const void*>(sdm_image_probe_kernel));
64
+ if (err != cudaSuccess) {
65
+ (void)cudaGetLastError();
66
+ TORCH_CHECK(false, "sdm_image_ptx: cudaFuncGetAttributes failed: ", cudaGetErrorString(err));
67
+ }
68
+ return static_cast<int64_t>(attr.ptxVersion);
69
+ }
70
+ """
71
+
72
+
73
+ def probe_ptx(index: int, env: tuple) -> int:
74
+ """The ptxVersion of the image the runtime loads on device `index` for the probe kernel built under build environment
75
+ `env` (as for every extension built under it), once per (compute capability, environment)."""
76
+ real = tuple(int(c) for c in torch.cuda.get_device_capability(index))
77
+ if (real, env) not in _PTX:
78
+ if env not in _PROBE:
79
+ from ._build import load_cuda_extension
80
+ _PROBE[env] = load_cuda_extension(PROBE_BASE, cpp_sources=_PROBE_CPP, cuda_sources=_PROBE_SOURCE,
81
+ functions=("sdm_image_ptx",))
82
+ with torch.cuda.device(index):
83
+ _PTX[real, env] = int(_PROBE[env].sdm_image_ptx())
84
+ return _PTX[real, env]
85
+
86
+
87
+ def require_supported_image(device=None) -> None:
88
+ """Refuses (ImageRefusal) a device whose routes are the modern ones (a forced test row included) while the image the
89
+ runtime loads there is not a modern pass; an RTX-class device warns once. `device` None = every visible device, the
90
+ current one first. Run by _build after every extension load; memoized per (device, forced row, build environment: an
91
+ extension built under another environment is checked again, with a probe built under it), refusals never."""
92
+ if getattr(_TLS, "depth", 0):
93
+ return
94
+ from . import _device
95
+ env = build_env()
96
+ if device is None:
97
+ cur = torch.cuda.current_device()
98
+ devices = [cur] + [j for j in range(torch.cuda.device_count()) if j != cur]
99
+ else:
100
+ devices = [_device._index(device)]
101
+ for i in devices:
102
+ key = (i, _device._FORCED, env)
103
+ if key in _OK:
104
+ continue
105
+ routes = _device.backend(i)
106
+ if routes == "modern" and probe_ptx(i, env) not in MODERN_PTX:
107
+ major, minor = (int(c) for c in torch.cuda.get_device_capability(i))
108
+ raise ImageRefusal(MESSAGE.format(index=i, name=torch.cuda.get_device_name(i), major=major, minor=minor,
109
+ ptx=probe_ptx(i, env), env=env_summary()))
110
+ if routes == "rtx" and key[:2] not in _WARNED:
111
+ from .. import __version__
112
+ cc = tuple(_device.capability(i))
113
+ warnings.warn(UNTESTED_MESSAGE.format(version=__version__, index=i, name=torch.cuda.get_device_name(i),
114
+ major=cc[0], minor=cc[1]), UntestedHardwareWarning, stacklevel=2)
115
+ _WARNED.add(key[:2])
116
+ _OK.add(key)
117
+
118
+
119
+ @contextlib.contextmanager
120
+ def _unchecked_build_env():
121
+ """TEST ONLY: every visible device is checked first; inside, this thread's builds (a test's own module for another
122
+ target list, called directly, never through the package's routes) are not checked."""
123
+ require_supported_image()
124
+ _TLS.depth = getattr(_TLS, "depth", 0) + 1
125
+ try:
126
+ yield
127
+ finally:
128
+ _TLS.depth -= 1