nbxtensor 0.0.1__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 (291) hide show
  1. nbxtensor/THIRD_PARTY_NOTICES.md +287 -0
  2. nbxtensor/__init__.py +17 -0
  3. nbxtensor/backend/__init__.py +1 -0
  4. nbxtensor/backend/capabilities.py +207 -0
  5. nbxtensor/backend/cpu/__init__.py +1 -0
  6. nbxtensor/backend/cpu/blas.py +102 -0
  7. nbxtensor/backend/cpu/pool.py +204 -0
  8. nbxtensor/backend/cpu_backend.py +162 -0
  9. nbxtensor/backend/device_facts.py +105 -0
  10. nbxtensor/backend/device_files.py +129 -0
  11. nbxtensor/backend/device_names.py +18 -0
  12. nbxtensor/backend/devices/amd/cdna.yml +228 -0
  13. nbxtensor/backend/devices/amd/cdna2.yml +228 -0
  14. nbxtensor/backend/devices/amd/cdna3.yml +227 -0
  15. nbxtensor/backend/devices/apple/apple_m1.yml +266 -0
  16. nbxtensor/backend/devices/apple/apple_m1_max.yml +266 -0
  17. nbxtensor/backend/devices/apple/apple_m1_pro.yml +266 -0
  18. nbxtensor/backend/devices/apple/apple_m1_ultra.yml +266 -0
  19. nbxtensor/backend/devices/apple/apple_m2.yml +266 -0
  20. nbxtensor/backend/devices/apple/apple_m2_max.yml +266 -0
  21. nbxtensor/backend/devices/apple/apple_m2_pro.yml +266 -0
  22. nbxtensor/backend/devices/apple/apple_m2_ultra.yml +266 -0
  23. nbxtensor/backend/devices/apple/apple_m3.yml +266 -0
  24. nbxtensor/backend/devices/apple/apple_m3_max.yml +266 -0
  25. nbxtensor/backend/devices/apple/apple_m3_pro.yml +266 -0
  26. nbxtensor/backend/devices/apple/apple_m3_ultra.yml +266 -0
  27. nbxtensor/backend/devices/apple/apple_m4.yml +266 -0
  28. nbxtensor/backend/devices/apple/apple_m4_max.yml +266 -0
  29. nbxtensor/backend/devices/apple/apple_m4_pro.yml +308 -0
  30. nbxtensor/backend/devices/apple/apple_m5.yml +269 -0
  31. nbxtensor/backend/devices/apple/apple_m5_max.yml +269 -0
  32. nbxtensor/backend/devices/apple/apple_m5_pro.yml +269 -0
  33. nbxtensor/backend/devices/apple/apple_m5_ultra.yml +269 -0
  34. nbxtensor/backend/devices/apple/apple_m6.yml +269 -0
  35. nbxtensor/backend/devices/apple/apple_silicon.yml +263 -0
  36. nbxtensor/backend/devices/nvidia/ampere.yml +214 -0
  37. nbxtensor/backend/devices/nvidia/hopper.yml +213 -0
  38. nbxtensor/backend/devices/nvidia/volta.yml +411 -0
  39. nbxtensor/backend/faults.py +151 -0
  40. nbxtensor/backend/host_memory.py +185 -0
  41. nbxtensor/backend/launcher.py +2350 -0
  42. nbxtensor/backend/launcher_contract.py +429 -0
  43. nbxtensor/backend/metal_backend.py +661 -0
  44. nbxtensor/backend/metal_device.py +1274 -0
  45. nbxtensor/backend/process_memory.py +73 -0
  46. nbxtensor/backend/replay_graph.py +322 -0
  47. nbxtensor/backend/runtime.py +409 -0
  48. nbxtensor/backend/triton_ext_driver.py +735 -0
  49. nbxtensor/dtype/__init__.py +1 -0
  50. nbxtensor/dtype/convert.py +107 -0
  51. nbxtensor/dtype/dtypes.yml +70 -0
  52. nbxtensor/dtype/itemsize.py +166 -0
  53. nbxtensor/dtype/nbx_dtype.py +72 -0
  54. nbxtensor/dtype/operands.py +158 -0
  55. nbxtensor/dtype/outputs.py +64 -0
  56. nbxtensor/dtype/policy.py +155 -0
  57. nbxtensor/dtype/policy.yml +51 -0
  58. nbxtensor/dtype/promotion.py +112 -0
  59. nbxtensor/host.py +55 -0
  60. nbxtensor/io/__init__.py +1 -0
  61. nbxtensor/io/read_rate.py +197 -0
  62. nbxtensor/io/safetensors.py +314 -0
  63. nbxtensor/memory/__init__.py +9 -0
  64. nbxtensor/memory/allocator.py +1988 -0
  65. nbxtensor/memory/arena.py +75 -0
  66. nbxtensor/memory/device_memory.py +150 -0
  67. nbxtensor/memory/device_transfer.py +154 -0
  68. nbxtensor/memory/layout.py +43 -0
  69. nbxtensor/memory/memory_pool.py +191 -0
  70. nbxtensor/memory/metal.py +96 -0
  71. nbxtensor/memory/oom_advice.py +91 -0
  72. nbxtensor/memory/pointer_tables.py +323 -0
  73. nbxtensor/memory/pool.py +507 -0
  74. nbxtensor/memory/shadow.py +43 -0
  75. nbxtensor/memory/slab.py +273 -0
  76. nbxtensor/memory/trace_replay.py +122 -0
  77. nbxtensor/memory/traces/sana-video-2b-720p.16g.2026-11-02.tsv +13610 -0
  78. nbxtensor/memory/vmm.py +118 -0
  79. nbxtensor/ops/__init__.py +1 -0
  80. nbxtensor/ops/_autotune_policy.py +90 -0
  81. nbxtensor/ops/_common.py +32 -0
  82. nbxtensor/ops/_configs.py +1041 -0
  83. nbxtensor/ops/_cost.py +231 -0
  84. nbxtensor/ops/abs.py +17 -0
  85. nbxtensor/ops/adaptive_avg_pool2d.py +161 -0
  86. nbxtensor/ops/add.py +96 -0
  87. nbxtensor/ops/addcdiv.py +29 -0
  88. nbxtensor/ops/addcmul.py +29 -0
  89. nbxtensor/ops/addmv_op.py +64 -0
  90. nbxtensor/ops/addr.py +68 -0
  91. nbxtensor/ops/all_reduce.py +83 -0
  92. nbxtensor/ops/amax.py +40 -0
  93. nbxtensor/ops/any_reduce.py +83 -0
  94. nbxtensor/ops/arange_op.py +27 -0
  95. nbxtensor/ops/argmax.py +107 -0
  96. nbxtensor/ops/argmin.py +93 -0
  97. nbxtensor/ops/atan2.py +71 -0
  98. nbxtensor/ops/attention_wrappers.py +1648 -0
  99. nbxtensor/ops/autotune_bucket.py +137 -0
  100. nbxtensor/ops/autotune_cache.py +430 -0
  101. nbxtensor/ops/autotune_certified.py +1262 -0
  102. nbxtensor/ops/autotune_refusals.py +380 -0
  103. nbxtensor/ops/autotune_settings.py +58 -0
  104. nbxtensor/ops/avg_pool2d.py +73 -0
  105. nbxtensor/ops/baddbmm_op.py +132 -0
  106. nbxtensor/ops/batch_norm.py +163 -0
  107. nbxtensor/ops/binary_strided.py +196 -0
  108. nbxtensor/ops/bitwise_and.py +26 -0
  109. nbxtensor/ops/bitwise_left_shift.py +47 -0
  110. nbxtensor/ops/bitwise_not.py +24 -0
  111. nbxtensor/ops/bitwise_or.py +26 -0
  112. nbxtensor/ops/bitwise_right_shift.py +47 -0
  113. nbxtensor/ops/bucketize.py +37 -0
  114. nbxtensor/ops/cat_op.py +87 -0
  115. nbxtensor/ops/celu.py +20 -0
  116. nbxtensor/ops/clamp.py +24 -0
  117. nbxtensor/ops/clamp_min.py +22 -0
  118. nbxtensor/ops/conv2d.py +265 -0
  119. nbxtensor/ops/conv2d_m8n8k4.py +165 -0
  120. nbxtensor/ops/conv_depthwise2d.py +154 -0
  121. nbxtensor/ops/conv_transpose2d.py +148 -0
  122. nbxtensor/ops/conv_wrappers.py +1482 -0
  123. nbxtensor/ops/copy.py +18 -0
  124. nbxtensor/ops/copy_op.py +39 -0
  125. nbxtensor/ops/cos.py +17 -0
  126. nbxtensor/ops/count_nonzero.py +64 -0
  127. nbxtensor/ops/cross_entropy.py +67 -0
  128. nbxtensor/ops/cummax.py +109 -0
  129. nbxtensor/ops/cummin.py +105 -0
  130. nbxtensor/ops/cumsum.py +244 -0
  131. nbxtensor/ops/decode_attn_vec.py +162 -0
  132. nbxtensor/ops/depthwise_conv2d.py +132 -0
  133. nbxtensor/ops/dequant_gemv.py +190 -0
  134. nbxtensor/ops/dft.py +67 -0
  135. nbxtensor/ops/dispatch.py +1228 -0
  136. nbxtensor/ops/div.py +75 -0
  137. nbxtensor/ops/dot_op.py +67 -0
  138. nbxtensor/ops/dropout.py +23 -0
  139. nbxtensor/ops/dtype_convert.py +42 -0
  140. nbxtensor/ops/elementwise_launch.py +81 -0
  141. nbxtensor/ops/elementwise_wrappers.py +2315 -0
  142. nbxtensor/ops/elu.py +20 -0
  143. nbxtensor/ops/embedding.py +69 -0
  144. nbxtensor/ops/eq.py +32 -0
  145. nbxtensor/ops/erf.py +18 -0
  146. nbxtensor/ops/exp.py +17 -0
  147. nbxtensor/ops/exp2.py +18 -0
  148. nbxtensor/ops/fft_op.py +125 -0
  149. nbxtensor/ops/fill_op.py +20 -0
  150. nbxtensor/ops/flash_attention.py +307 -0
  151. nbxtensor/ops/flash_attention_m8n8k4.py +384 -0
  152. nbxtensor/ops/flash_decode.py +78 -0
  153. nbxtensor/ops/flip_op.py +60 -0
  154. nbxtensor/ops/floor.py +90 -0
  155. nbxtensor/ops/full_op.py +24 -0
  156. nbxtensor/ops/fused_moe.py +417 -0
  157. nbxtensor/ops/fused_moe_m8n8k4.py +99 -0
  158. nbxtensor/ops/fused_upsample_conv.py +413 -0
  159. nbxtensor/ops/gather_op.py +52 -0
  160. nbxtensor/ops/ge.py +32 -0
  161. nbxtensor/ops/gelu.py +31 -0
  162. nbxtensor/ops/gemv_vec.py +80 -0
  163. nbxtensor/ops/glu.py +43 -0
  164. nbxtensor/ops/grid_sampler.py +145 -0
  165. nbxtensor/ops/group_norm_tiled.py +67 -0
  166. nbxtensor/ops/groupnorm.py +123 -0
  167. nbxtensor/ops/gt.py +32 -0
  168. nbxtensor/ops/hardsigmoid.py +18 -0
  169. nbxtensor/ops/hardswish.py +19 -0
  170. nbxtensor/ops/im2col.py +57 -0
  171. nbxtensor/ops/index_add.py +95 -0
  172. nbxtensor/ops/index_put_op.py +92 -0
  173. nbxtensor/ops/index_select.py +121 -0
  174. nbxtensor/ops/index_wrappers.py +770 -0
  175. nbxtensor/ops/isfinite.py +20 -0
  176. nbxtensor/ops/isinf.py +20 -0
  177. nbxtensor/ops/isnan.py +18 -0
  178. nbxtensor/ops/launch_keys.py +1054 -0
  179. nbxtensor/ops/launch_observer.py +54 -0
  180. nbxtensor/ops/layernorm.py +71 -0
  181. nbxtensor/ops/le.py +35 -0
  182. nbxtensor/ops/leaky_relu.py +19 -0
  183. nbxtensor/ops/lerp.py +76 -0
  184. nbxtensor/ops/linalg_wrappers.py +1370 -0
  185. nbxtensor/ops/linspace_op.py +47 -0
  186. nbxtensor/ops/log.py +17 -0
  187. nbxtensor/ops/log_sigmoid.py +23 -0
  188. nbxtensor/ops/logical_and.py +26 -0
  189. nbxtensor/ops/logical_not.py +24 -0
  190. nbxtensor/ops/logical_or.py +26 -0
  191. nbxtensor/ops/logical_xor.py +37 -0
  192. nbxtensor/ops/loss_wrappers.py +282 -0
  193. nbxtensor/ops/lt.py +32 -0
  194. nbxtensor/ops/masked_fill.py +21 -0
  195. nbxtensor/ops/matmul.py +377 -0
  196. nbxtensor/ops/matmul_m8n8k4.py +235 -0
  197. nbxtensor/ops/max_pool2d.py +74 -0
  198. nbxtensor/ops/max_pool3d.py +93 -0
  199. nbxtensor/ops/max_reduce.py +91 -0
  200. nbxtensor/ops/maximum.py +18 -0
  201. nbxtensor/ops/mean.py +40 -0
  202. nbxtensor/ops/min_reduce.py +91 -0
  203. nbxtensor/ops/minimum.py +18 -0
  204. nbxtensor/ops/mish.py +22 -0
  205. nbxtensor/ops/mma_m8n8k4.py +152 -0
  206. nbxtensor/ops/moe_align.py +195 -0
  207. nbxtensor/ops/moe_decode_vec.py +374 -0
  208. nbxtensor/ops/moe_device_residency.py +141 -0
  209. nbxtensor/ops/moe_wrappers.py +345 -0
  210. nbxtensor/ops/mse_loss.py +79 -0
  211. nbxtensor/ops/mul.py +51 -0
  212. nbxtensor/ops/nan_to_num.py +23 -0
  213. nbxtensor/ops/ne.py +32 -0
  214. nbxtensor/ops/neg.py +17 -0
  215. nbxtensor/ops/nllloss.py +124 -0
  216. nbxtensor/ops/nonzero.py +33 -0
  217. nbxtensor/ops/nonzero_op.py +62 -0
  218. nbxtensor/ops/norm_wrappers.py +579 -0
  219. nbxtensor/ops/ones_op.py +20 -0
  220. nbxtensor/ops/oracles/__init__.py +6 -0
  221. nbxtensor/ops/oracles/conv2d_fp64.py +450 -0
  222. nbxtensor/ops/pad_op.py +159 -0
  223. nbxtensor/ops/paged_decode_attn.py +215 -0
  224. nbxtensor/ops/pixel_shuffle.py +144 -0
  225. nbxtensor/ops/pixel_unshuffle.py +72 -0
  226. nbxtensor/ops/pow.py +137 -0
  227. nbxtensor/ops/prod.py +81 -0
  228. nbxtensor/ops/rand_op.py +52 -0
  229. nbxtensor/ops/random_wrappers.py +366 -0
  230. nbxtensor/ops/reciprocal.py +17 -0
  231. nbxtensor/ops/reduction_wrappers.py +845 -0
  232. nbxtensor/ops/relu.py +18 -0
  233. nbxtensor/ops/remainder.py +46 -0
  234. nbxtensor/ops/repeat_interleave.py +48 -0
  235. nbxtensor/ops/repeat_op.py +132 -0
  236. nbxtensor/ops/residual_chain.py +374 -0
  237. nbxtensor/ops/rmsnorm.py +68 -0
  238. nbxtensor/ops/rope.py +142 -0
  239. nbxtensor/ops/rsqrt.py +17 -0
  240. nbxtensor/ops/scaled_softmax.py +96 -0
  241. nbxtensor/ops/scatter_op.py +139 -0
  242. nbxtensor/ops/screen_oracle.py +479 -0
  243. nbxtensor/ops/selu.py +22 -0
  244. nbxtensor/ops/shape_wrappers.py +1032 -0
  245. nbxtensor/ops/sigmoid.py +19 -0
  246. nbxtensor/ops/silu.py +27 -0
  247. nbxtensor/ops/sin.py +17 -0
  248. nbxtensor/ops/softmax.py +169 -0
  249. nbxtensor/ops/softplus.py +27 -0
  250. nbxtensor/ops/sort_op.py +304 -0
  251. nbxtensor/ops/spectral_wrappers.py +481 -0
  252. nbxtensor/ops/sqrt.py +17 -0
  253. nbxtensor/ops/stack_op.py +84 -0
  254. nbxtensor/ops/std.py +113 -0
  255. nbxtensor/ops/strided_copy.py +200 -0
  256. nbxtensor/ops/sub.py +35 -0
  257. nbxtensor/ops/sum.py +40 -0
  258. nbxtensor/ops/tan.py +19 -0
  259. nbxtensor/ops/tanh.py +21 -0
  260. nbxtensor/ops/threshold.py +19 -0
  261. nbxtensor/ops/tiling.yml +91 -0
  262. nbxtensor/ops/tiling_policy.py +43 -0
  263. nbxtensor/ops/tiling_sizes.py +370 -0
  264. nbxtensor/ops/topk.py +267 -0
  265. nbxtensor/ops/trace_op.py +64 -0
  266. nbxtensor/ops/tril.py +64 -0
  267. nbxtensor/ops/triu.py +62 -0
  268. nbxtensor/ops/unfold_op.py +37 -0
  269. nbxtensor/ops/unique_op.py +97 -0
  270. nbxtensor/ops/upsample_bilinear2d.py +106 -0
  271. nbxtensor/ops/upsample_nearest2d.py +52 -0
  272. nbxtensor/ops/upsample_nearest3d.py +61 -0
  273. nbxtensor/ops/var.py +136 -0
  274. nbxtensor/ops/vector_norm.py +216 -0
  275. nbxtensor/ops/weight_norm.py +104 -0
  276. nbxtensor/ops/where.py +39 -0
  277. nbxtensor/ops/wrappers.py +825 -0
  278. nbxtensor/ops/zeros_op.py +20 -0
  279. nbxtensor/quant/__init__.py +1 -0
  280. nbxtensor/quant/quantized_tensor.py +187 -0
  281. nbxtensor/random/__init__.py +1 -0
  282. nbxtensor/random/mt_randn.py +154 -0
  283. nbxtensor/random/rng_pin.py +59 -0
  284. nbxtensor/random/rng_stream.py +118 -0
  285. nbxtensor/random/seeded_draw.py +223 -0
  286. nbxtensor/shape/__init__.py +157 -0
  287. nbxtensor/tensor.py +1867 -0
  288. nbxtensor-0.0.1.dist-info/METADATA +379 -0
  289. nbxtensor-0.0.1.dist-info/RECORD +291 -0
  290. nbxtensor-0.0.1.dist-info/WHEEL +4 -0
  291. nbxtensor-0.0.1.dist-info/licenses/LICENSE +189 -0
@@ -0,0 +1,96 @@
1
+ """Apple Metal's memory primitives (`DeviceMemory`) — the Metal device behind the one account.
2
+
3
+ The account (`memory.allocator.DeviceAllocator`) counts every byte this process holds, refuses past the device's
4
+ budget and reports. This object only does what the account asks of the device, over the Metal runtime
5
+ (`backend.metal_device.MetalRuntime`): shared-storage buffers whose one address the CPU and the GPU both use, their
6
+ release once the GPU is done with them, the in-place binding of host pages, copies and fills in the one address
7
+ space. It keeps NO account of its own: until 2026-10-29 the runtime kept a second one (`_live_bytes`) beside the
8
+ account's, two figures of one fact.
9
+
10
+ - `budget()`: `MTLDevice.recommendedMaxWorkingSetSize`, Apple's own ceiling for the process on unified memory. Metal
11
+ hands out buffers past it, backed by memory the whole machine shares, until the system pages: the ACCOUNT refuses
12
+ past it (`supports("budget_refusal")`), with this backend's allocation-refused status (`ALLOC_REFUSED`).
13
+ - `mem_get_info()`: (budget less what the account holds, budget) — `MTLDevice.currentAllocatedSize` is cumulative and
14
+ never comes down (measured 2026-09-05), so the headroom is read from the account, never from the device.
15
+ - `free(ptr)`: the buffer is released once the command buffers in flight are done with it; the account counts it
16
+ freed at once.
17
+
18
+ Torch-free (R33).
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import ctypes
23
+ from typing import Tuple
24
+
25
+ from .device_memory import DeviceMemory
26
+
27
+ #: this backend's status for an allocation refused (the runtime's `_ERR_ALLOC`): the account returns it when the budget
28
+ #: refuses, so a refusal reads the same whichever of the two said no
29
+ ALLOC_REFUSED = 1
30
+
31
+ _PRIMITIVES = frozenset({"malloc", "free", "mem_get_info", "memset", "memcpy", "host_alloc", "host_free",
32
+ "set_device", "get_device", "device_count", "budget", "map_host", "unmap_host",
33
+ "budget_refusal"})
34
+
35
+
36
+ class MetalMemory(DeviceMemory):
37
+ """The memory primitives of the one Metal device of this process, over its runtime."""
38
+
39
+ __slots__ = ("_rt",)
40
+ ALLOC_REFUSED = ALLOC_REFUSED
41
+
42
+ def __init__(self, runtime):
43
+ self._rt = runtime
44
+
45
+ def supports(self, primitive):
46
+ return primitive in _PRIMITIVES
47
+
48
+ def malloc(self, nbytes):
49
+ p = ctypes.c_void_p()
50
+ rc = self._rt.malloc(ctypes.byref(p), ctypes.c_size_t(int(nbytes)))
51
+ return rc, (p.value or 0)
52
+
53
+ def free(self, ptr):
54
+ return self._rt.free(ctypes.c_void_p(int(ptr)))
55
+
56
+ def budget(self) -> Tuple[int, int]:
57
+ return 0, int(self._rt.budget_bytes())
58
+
59
+ def mem_get_info(self):
60
+ from .allocator import DeviceAllocator as DA
61
+ total = int(self._rt.budget_bytes())
62
+ held = DA._cuda_live_bytes.get(0, 0) + DA._host_pinned_live_bytes
63
+ return 0, max(0, total - held), total
64
+
65
+ def memset(self, ptr, value, nbytes):
66
+ return self._rt.memset(int(ptr), int(value), int(nbytes))
67
+
68
+ def memcpy(self, dst, src, nbytes, kind):
69
+ return self._rt.memcpy(int(dst), int(src), int(nbytes), int(kind))
70
+
71
+ def host_alloc(self, nbytes):
72
+ p = ctypes.c_void_p()
73
+ rc = self._rt.malloc_host(ctypes.byref(p), ctypes.c_size_t(int(nbytes)))
74
+ return rc, (p.value or 0)
75
+
76
+ def host_free(self, ptr):
77
+ return self._rt.free_host(ctypes.c_void_p(int(ptr)))
78
+
79
+ def set_device(self, index):
80
+ return self._rt.set_device(int(index))
81
+
82
+ def get_device(self):
83
+ d = ctypes.c_int()
84
+ rc = self._rt.get_device(ctypes.byref(d))
85
+ return rc, d.value
86
+
87
+ def device_count(self):
88
+ n = ctypes.c_int()
89
+ rc = self._rt.device_count(ctypes.byref(n))
90
+ return rc, n.value
91
+
92
+ def map_host(self, ptr, nbytes):
93
+ return self._rt.bind_mapped(int(ptr), int(nbytes))
94
+
95
+ def unmap_host(self, start):
96
+ self._rt.unbind_mapped(start)
@@ -0,0 +1,91 @@
1
+ """What would have made it fit.
2
+
3
+ Addition 4 of `docs/reference/adaptive-memory-a-runtime-controller.md`: a refusal
4
+ that only says "out of memory" costs the reader the whole diagnosis. The allocator
5
+ knows what it asked for and what the driver had; between those two numbers there
6
+ is a concrete answer — how many pieces this work would have to be cut into, and
7
+ how much larger a card would have taken it whole.
8
+
9
+ This module says only what the numbers support. It does not guess at a remedy it
10
+ cannot compute: with no shape it talks about bands and card size, and it mentions
11
+ a spatial extent only when it is given the shape to derive one from.
12
+
13
+ It lives under `kernels/` because both execution modes raise the same
14
+ `DeviceOOMError` and both need the same sentence — and because `kernels/` is
15
+ already free of torch, so the triton branch can import it without an R33
16
+ question.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import math
22
+ from typing import Optional, Sequence
23
+
24
+ _MB = 1024 * 1024
25
+
26
+
27
+ def _mb(n: int) -> str:
28
+ return f"{n / _MB:,.0f} MB"
29
+
30
+
31
+ def what_would_have_fit(oom, shape: Optional[Sequence] = None) -> str:
32
+ """One sentence naming what would have made the refused allocation fit.
33
+
34
+ `oom` is a `DeviceOOMError` carrying `requested` and `driver_free`. Returns
35
+ an empty string when it carries neither — a sentence built on a figure the
36
+ driver never gave would be a guess wearing the clothes of a measurement.
37
+
38
+ `shape` is the allocation's shape when the caller knows it. Its leading
39
+ dimension is the one a band split cuts, so it is the only one this reports
40
+ on; nothing here assumes NCHW or any other layout beyond "the first axis is
41
+ divisible", which is what band streaming actually needs.
42
+ """
43
+ requested = getattr(oom, "requested", None)
44
+ free = getattr(oom, "driver_free", None)
45
+ if not requested or free is None:
46
+ return ""
47
+
48
+ parts = []
49
+ shortfall = getattr(oom, "shortfall", None)
50
+ if shortfall:
51
+ parts.append(f"short by {_mb(shortfall)}")
52
+
53
+ if free > 0:
54
+ bands = math.ceil(requested / free)
55
+ # `free` is what the driver has RIGHT NOW, with this run's own live set
56
+ # already on the card, so a band of `requested / bands` is a size that
57
+ # fits beside what is already there — not a hypothetical on an empty card.
58
+ parts.append(
59
+ f"it would fit in {bands} bands of about {_mb(requested / bands)}")
60
+ if shape:
61
+ lead = shape[0] if len(shape) else None
62
+ if isinstance(lead, int) and lead >= bands > 1:
63
+ parts.append(
64
+ f"cutting the leading axis of {tuple(shape)} from {lead} to "
65
+ f"{lead // bands} per band")
66
+ else:
67
+ parts.append("the card reports nothing free: no band split can help here")
68
+
69
+ total = getattr(oom, "driver_total", None)
70
+ if total and requested > total:
71
+ parts.append(
72
+ f"and no band split alone reaches it — the request of {_mb(requested)} "
73
+ f"exceeds the card's entire {_mb(total)}")
74
+
75
+ return "; ".join(parts)
76
+
77
+
78
+ def annotate(message: str, cause, shape: Optional[Sequence] = None) -> str:
79
+ """Append the advice to an op-failure message when the cause is an OOM.
80
+
81
+ Any other cause passes through untouched: this must not turn an unrelated
82
+ failure into one that reads as a memory problem.
83
+ """
84
+ try:
85
+ from nbxtensor.memory.allocator import DeviceOOMError
86
+ except Exception: # pragma: no cover - import cycle safety
87
+ return message
88
+ if not isinstance(cause, DeviceOOMError):
89
+ return message
90
+ advice = what_would_have_fit(cause, shape)
91
+ return f"{message} | what would have fit: {advice}" if advice else message
@@ -0,0 +1,323 @@
1
+ """Device-address tables of a list of tensors: the fused MoE GEMMs read their experts through them (one [E] int64
2
+ table of absolute addresses per projection and device, no offset math), their fingerprinted LRU cache, and on Metal the
3
+ pinned scopes that keep those addresses valid for the table's whole cache life (moved from the engine's triton/moe.py,
4
+ M2). Torch-free.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from collections import defaultdict, OrderedDict
9
+
10
+ import numpy as np
11
+
12
+ import triton
13
+ import triton.language as tl
14
+
15
+ from nbxtensor.tensor import NBXTensor
16
+ from nbxtensor.memory.allocator import DeviceAllocator
17
+ from nbxtensor.backend.runtime import gpu_backend
18
+
19
+
20
+ # ============================================================================
21
+ # POINTER TABLE BUILDER — zero-copy, cached
22
+ # ============================================================================
23
+
24
+ # LRU cache of pointer tables keyed by a data_ptr fingerprint of every
25
+ # expert weight. Under zero3 pipelining weights may be freed and
26
+ # reallocated at the same virtual address, so a static cache_key
27
+ # (e.g. first expert's data_ptr) can resolve to a stale PtrTables and
28
+ # cause silent garbage or illegal memory accesses. Hashing ALL
29
+ # expert data_ptrs detects any change to the arena layout.
30
+ #
31
+ # Bounded by _PTR_CACHE_MAXSIZE entries — eviction is LRU (OrderedDict
32
+ # move_to_end on hit, popitem(last=False) on overflow). The cap keeps
33
+ # memory flat when running many blocks across devices during
34
+ # pipelining; 256 entries × 3 int64 tables × few hundred experts is
35
+ # still sub-megabyte.
36
+ _PTR_CACHE_MAXSIZE = 256
37
+ _ptr_cache: "OrderedDict[int, PtrTables]" = OrderedDict()
38
+
39
+
40
+ def _ptr_cache_fingerprint(
41
+ gate_weights, up_weights, down_weights, num_experts: int
42
+ ) -> int:
43
+ """Stable hash over every expert's data_ptr for all three projections.
44
+
45
+ Any weight swap (H2D, D2D, free-and-remalloc at the same address)
46
+ changes at least one data_ptr — and in the rare adversarial case
47
+ where the driver hands out identical addresses for a pair of
48
+ identically-sized buffers, we still hash all 3 × num_experts ptrs
49
+ so a single match isn't enough for a false cache hit.
50
+ """
51
+ vals = []
52
+ vals.append(num_experts)
53
+ for i in range(num_experts):
54
+ vals.append(gate_weights[i].data_ptr())
55
+ vals.append(up_weights[i].data_ptr())
56
+ vals.append(down_weights[i].data_ptr())
57
+ return hash(tuple(vals))
58
+
59
+
60
+ def _ptr_cache_get(fp: int):
61
+ """LRU lookup. Promotes the hit to the MRU end."""
62
+ tables = _ptr_cache.get(fp)
63
+ if tables is not None:
64
+ _ptr_cache.move_to_end(fp)
65
+ return tables
66
+
67
+
68
+ def _ptr_cache_put(fp: int, tables):
69
+ """Insert + evict the oldest entry if we're over the cap."""
70
+ _ptr_cache[fp] = tables
71
+ _ptr_cache.move_to_end(fp)
72
+ while len(_ptr_cache) > _PTR_CACHE_MAXSIZE:
73
+ _fp, evicted = _ptr_cache.popitem(last=False)
74
+ evicted.release_pins()
75
+
76
+
77
+ def release_pinned_tables() -> int:
78
+ """Drop every cached table that PINS (Metal), releasing its pins; returns how many. Called at the unload
79
+ boundary (`MemoryManager.unload_weights`), after the devices were synchronised.
80
+
81
+ A pinning table holds its expert tensors and one whole-allocation wrap per arena for as long as it lives,
82
+ and it lived until LRU eviction, 256 tables later: an unloaded component stayed pinned, its tensors not
83
+ freed and its arena still mapped. Under `layer_streaming` each unloaded segment stayed beside the next
84
+ (deepseek-moe-16b-chat on the Mac, 2026-10-04: swap 3.4 -> 10.2 GB in 30 s while the second of four
85
+ segments loaded, the guard killed the run). All of them go, not only the unloading component's: a table is
86
+ three small host arrays, rebuilt at the next call that misses, and a table that outlives ANY weight it
87
+ pins is the defect. A table that pins nothing (CUDA: integers only) is left where it is."""
88
+ pinned = [fp for fp, tables in _ptr_cache.items() if tables.pins]
89
+ for fp in pinned:
90
+ _ptr_cache.pop(fp).release_pins()
91
+ return len(pinned)
92
+
93
+
94
+ class PtrTables:
95
+ """Per-projection absolute pointer tables for zero-copy expert access.
96
+
97
+ Each table is [E] int64 on GPU: absolute data_ptr() of each expert.
98
+ The fused kernel loads ptrs[expert_id] and uses it directly as a pointer.
99
+ """
100
+ __slots__ = ('gate_ptrs', 'gate_stride_bk', 'gate_stride_bn',
101
+ 'up_ptrs', 'up_stride_bk', 'up_stride_bn',
102
+ 'down_ptrs', 'down_stride_bk', 'down_stride_bn',
103
+ 'device_experts', 'quantized', 'q_tables', 'pins')
104
+
105
+ def __init__(self):
106
+ self.gate_ptrs = {} # {device → NBXTensor[E] int64}
107
+ self.gate_stride_bk = {}
108
+ self.gate_stride_bn = {}
109
+ self.up_ptrs = {}
110
+ self.up_stride_bk = {}
111
+ self.up_stride_bn = {}
112
+ self.down_ptrs = {}
113
+ self.down_stride_bk = {}
114
+ self.down_stride_bn = {}
115
+ self.device_experts = {}
116
+ # int4-g128-asym experts: *_ptrs holds the qweight table and
117
+ # q_tables[proj][dev] = (sc_ptrs, mn_ptrs, stride_sg, stride_sn);
118
+ # the *_stride_bk/bn slots hold the PACKED int32 strides.
119
+ self.quantized = False
120
+ self.q_tables = {}
121
+ # Metal only: entered pinned_addresses scopes, one per device — the
122
+ # addresses in these tables are valid EXACTLY while these are held,
123
+ # so their lifetime is the table's cache lifetime (released on LRU
124
+ # eviction below), never a call's.
125
+ self.pins = []
126
+
127
+ def release_pins(self):
128
+ for pin in self.pins:
129
+ try:
130
+ pin.__exit__(None, None, None)
131
+ except Exception: # noqa: BLE001
132
+ pass
133
+ self.pins = []
134
+
135
+
136
+ @triton.jit
137
+ def _addr_capture_kernel(src_ptr, table_ptr, idx):
138
+ """Store the DEVICE address of src_ptr into table[idx].
139
+
140
+ The bitcast is the whole point: the launcher hands the kernel the address
141
+ a shader dereferences, and capturing it ON DEVICE is the one spelling that
142
+ is correct on every backend (verified bit-equal to the driver's own
143
+ address on triton_ext, #130, 2026-09-19). A host-side data_ptr() is the
144
+ CPU mapping on Metal, and a table of those reads as silent zeros.
145
+ """
146
+ tl.store(table_ptr + idx, tl.cast(src_ptr, tl.int64, bitcast=True))
147
+
148
+
149
+ def build_ptr_tables(gate_weights, up_weights, down_weights):
150
+ """Build per-projection absolute pointer tables.
151
+
152
+ Stores each expert's data_ptr() as int64 in a GPU tensor. No offset math.
153
+ Total GPU allocation: 3 × E × 8 bytes per device (~3KB for 128 experts).
154
+
155
+ On Metal the tables are built by DEVICE CAPTURE under a pinned scope
156
+ instead: the addresses a shader dereferences, kept alive for the table's
157
+ whole cache lifetime (PtrTables.pins, released on LRU eviction).
158
+ """
159
+ from nbxtensor.quant.quantized_tensor import QuantizedTensor
160
+
161
+ num_experts = len(gate_weights)
162
+ tables = PtrTables()
163
+ tables.quantized = isinstance(gate_weights[0], QuantizedTensor)
164
+ if tables.quantized:
165
+ # Uniformity contract: mixed dense/quantized expert lists are a
166
+ # broken build — refuse loudly (ZERO FALLBACK).
167
+ for proj_name, ws in (("gate", gate_weights), ("up", up_weights),
168
+ ("down", down_weights)):
169
+ if not all(isinstance(x, QuantizedTensor) for x in ws):
170
+ raise RuntimeError(
171
+ f"ZERO FALLBACK: mixed dense/quantized experts in "
172
+ f"'{proj_name}' projection — re-build the variant.")
173
+ else:
174
+ # The kernel takes ONE (stride_bk, stride_bn) pair per projection, read
175
+ # from the first expert: every expert of a projection must share it. A
176
+ # stacked slab's views do by construction (one slab, one geometry); a
177
+ # list mixing layouts would be read through the wrong strides in
178
+ # silence — refuse it by name.
179
+ for proj_name, ws in (("gate", gate_weights), ("up", up_weights),
180
+ ("down", down_weights)):
181
+ s0 = (tuple(ws[0].shape), ws[0].stride(0), ws[0].stride(1))
182
+ for eid in range(1, num_experts):
183
+ se = (tuple(ws[eid].shape), ws[eid].stride(0), ws[eid].stride(1))
184
+ if se != s0:
185
+ raise RuntimeError(
186
+ f"ZERO FALLBACK: MoE '{proj_name}' expert {eid} has "
187
+ f"(shape, strides) {se}, expert 0 {s0} — the grouped "
188
+ "GEMM reads every expert through one stride pair.")
189
+
190
+ by_device = defaultdict(list)
191
+ for i in range(num_experts):
192
+ by_device[gate_weights[i]._device_idx].append(i)
193
+ tables.device_experts = dict(by_device)
194
+
195
+ _metal = gpu_backend() == "metal"
196
+ if _metal and tables.quantized:
197
+ raise RuntimeError(
198
+ "ZERO FALLBACK: quantized (int4) expert tables are not proven on "
199
+ "Metal — the pinned-table contract was measured for the dense "
200
+ "bf16 grouped GEMM only. Adding the quantized path is proving "
201
+ "its triplet tables the same way, not assuming them.")
202
+
203
+ for dev, expert_ids in by_device.items():
204
+ DeviceAllocator.set_device(dev)
205
+
206
+ if _metal:
207
+ # Pin FIRST — the capture launches below store their wraps under
208
+ # the pin, which is what keeps the captured addresses mapped.
209
+ # Released when this table leaves the cache, never per call.
210
+ from nbxtensor.backend.triton_ext_driver import pinned_addresses
211
+ pin = pinned_addresses(*(
212
+ ws[eid]
213
+ for ws in (gate_weights, up_weights, down_weights)
214
+ for eid in expert_ids))
215
+ pin.__enter__()
216
+ tables.pins.append(pin)
217
+
218
+ # The pin's whole-allocation kept wrap is the ONE address
219
+ # authority: every MTLBuffer has its own GPU VA, so an address
220
+ # captured through a transient launch wrap dies with that wrap
221
+ # (zeros, nothing raised — measured 2026-09-21), and per-expert
222
+ # kept wraps blow Metal's alias budget (declined at ~480,
223
+ # granite layer 15). Host-side arithmetic on the pinned wrap's
224
+ # gpu_address needs no launch and one wrap per allocation.
225
+ from nbxtensor.backend.triton_ext_driver import pinned_gpu_address
226
+
227
+ def _capture_proj(weights):
228
+ ptrs = np.array(
229
+ [pinned_gpu_address(weights[eid].data_ptr())
230
+ for eid in expert_ids],
231
+ dtype=np.int64)
232
+ tab = NBXTensor.from_numpy(ptrs)
233
+ base = weights[expert_ids[0]]
234
+ return tab, base.stride(1), base.stride(0)
235
+
236
+ gp, gbk, gbn = _capture_proj(gate_weights)
237
+ tables.gate_ptrs[dev] = gp
238
+ tables.gate_stride_bk[dev] = gbk
239
+ tables.gate_stride_bn[dev] = gbn
240
+ up, ubk, ubn = _capture_proj(up_weights)
241
+ tables.up_ptrs[dev] = up
242
+ tables.up_stride_bk[dev] = ubk
243
+ tables.up_stride_bn[dev] = ubn
244
+ dp, dbk, dbn = _capture_proj(down_weights)
245
+ tables.down_ptrs[dev] = dp
246
+ tables.down_stride_bk[dev] = dbk
247
+ tables.down_stride_bn[dev] = dbn
248
+ continue
249
+
250
+ def _build_proj(weights):
251
+ # Absolute pointers as int64
252
+ if tables.quantized:
253
+ qw = np.array(
254
+ [weights[eid].qweight.data_ptr() for eid in expert_ids],
255
+ dtype=np.int64)
256
+ sc = np.array(
257
+ [weights[eid].scales.data_ptr() for eid in expert_ids],
258
+ dtype=np.int64)
259
+ mn = np.array(
260
+ [weights[eid].qmins.data_ptr() for eid in expert_ids],
261
+ dtype=np.int64)
262
+ base = weights[expert_ids[0]]
263
+ return ((NBXTensor.from_numpy(qw),
264
+ NBXTensor.from_numpy(sc),
265
+ NBXTensor.from_numpy(mn)),
266
+ base.qweight.stride(0), base.qweight.stride(1),
267
+ base.scales.stride(0), base.scales.stride(1))
268
+ ptrs = np.array(
269
+ [weights[eid].data_ptr() for eid in expert_ids],
270
+ dtype=np.int64)
271
+ ptr_table = NBXTensor.from_numpy(ptrs)
272
+ base = weights[expert_ids[0]]
273
+ return ptr_table, base.stride(1), base.stride(0)
274
+
275
+ if tables.quantized:
276
+ for name, ws in (("gate", gate_weights), ("up", up_weights),
277
+ ("down", down_weights)):
278
+ (qw_t, sc_t, mn_t), qk, qn, sg, sn = _build_proj(ws)
279
+ getattr(tables, f"{name}_ptrs")[dev] = qw_t
280
+ getattr(tables, f"{name}_stride_bk")[dev] = qk
281
+ getattr(tables, f"{name}_stride_bn")[dev] = qn
282
+ tables.q_tables.setdefault(name, {})[dev] = (
283
+ sc_t, mn_t, sg, sn)
284
+ continue
285
+
286
+ gp, gbk, gbn = _build_proj(gate_weights)
287
+ tables.gate_ptrs[dev] = gp
288
+ tables.gate_stride_bk[dev] = gbk
289
+ tables.gate_stride_bn[dev] = gbn
290
+
291
+ up, ubk, ubn = _build_proj(up_weights)
292
+ tables.up_ptrs[dev] = up
293
+ tables.up_stride_bk[dev] = ubk
294
+ tables.up_stride_bn[dev] = ubn
295
+
296
+ dp, dbk, dbn = _build_proj(down_weights)
297
+ tables.down_ptrs[dev] = dp
298
+ tables.down_stride_bk[dev] = dbk
299
+ tables.down_stride_bn[dev] = dbn
300
+
301
+ return tables
302
+
303
+
304
+ def _tables_for_call(gate_weights, up_weights, down_weights, num_experts: int,
305
+ per_call: bool, act_dev: int):
306
+ """The pointer tables of one call. Weights that live for this call only
307
+ (promoted from the host here or by the dispatcher) get a fresh table that
308
+ never enters the cache — their addresses are freed when the call returns,
309
+ and a later allocation reusing them would meet a stale entry. Resident
310
+ weights go through the fingerprinted LRU cache."""
311
+ if per_call:
312
+ tables = build_ptr_tables(gate_weights, up_weights, down_weights)
313
+ DeviceAllocator.set_device(act_dev)
314
+ DeviceAllocator.ensure_triton_device(act_dev)
315
+ return tables
316
+ fp = _ptr_cache_fingerprint(gate_weights, up_weights, down_weights, num_experts)
317
+ tables = _ptr_cache_get(fp)
318
+ if tables is None:
319
+ tables = build_ptr_tables(gate_weights, up_weights, down_weights)
320
+ _ptr_cache_put(fp, tables)
321
+ DeviceAllocator.set_device(act_dev)
322
+ DeviceAllocator.ensure_triton_device(act_dev)
323
+ return tables