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,735 @@
1
+ """NeuroBrix's launcher driver for triton-lang/triton-ext's AppleGPU backend.
2
+
3
+ Why this file exists, in one sentence: a driver implements ONE backend's launch
4
+ ABI, and triton-ext's is not the fork's.
5
+
6
+ The fork binds each scalar to its own Metal argument slot. triton-ext packs every
7
+ scalar into a SINGLE device buffer and binds it after the pointers — its own
8
+ `MetalLauncher` says so:
9
+
10
+ # Scalars pack into one device buffer, so IR param order is
11
+ # [pointers, packed_scalar_buf, system_values].
12
+
13
+ Launching one backend's kernel through the other's driver therefore lands the
14
+ first scalar and zeroes the rest, with nothing raised: every mask goes false,
15
+ every `tl.load` takes its `other`, and the `tl.store` is fully masked, so the
16
+ output buffer keeps the zeros it was allocated with. That is the exact-zero
17
+ signature measured on conv2d 1x1 across all 18 configs (see the campaign ledger,
18
+ `abi_scalars.py`: the kernel saw `HW,C,K = [4096, 0, 0]` for `[4096, 180, 180]`).
19
+
20
+ Two rules this file keeps:
21
+
22
+ * **We do not re-derive their ABI.** The scalar layout comes from triton-ext's own
23
+ `_compute_scalar_layout`/`_SCALAR_PACK_INFO`, so a change on their side is a
24
+ change here, not a silent divergence. We follow the line of Triton.
25
+ * **No torch.** triton-ext ships two dispatch runtimes and prefers the libtorch
26
+ one whenever torch is merely installed; R33 forbids that, so this driver pins
27
+ the torch-free `_NativeRuntime`. Upstream has no supported switch for it —
28
+ that is an owed upstream contribution, with `spike_native_runtime.py` as its
29
+ reproducer.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import ctypes
34
+ import struct
35
+ from typing import Any, List, NamedTuple, Optional, Sequence, Tuple
36
+
37
+ from nbxtensor.backend.launcher import Driver
38
+
39
+
40
+ def _ext():
41
+ """triton-ext's driver module, with the torch-free runtime pinned.
42
+
43
+ `_runtime()` chooses `_TorchRuntime` whenever torch can be imported and
44
+ offers no override, so the choice is made here before anything builds one.
45
+ """
46
+ from triton_apple_backend import driver as D
47
+ if not isinstance(getattr(D, "_RUNTIME", None), D._NativeRuntime):
48
+ D._RUNTIME = D._NativeRuntime()
49
+ return D
50
+
51
+
52
+ _SYNC_TIMING = __import__("os").environ.get("NBX_SYNC_TIMING", "0") == "1"
53
+ _SYNC_ACC = [0.0, 0.0, 0.0, 0] # drain, submit, synchronize, count
54
+ _SYNC_PER_KERNEL = {} # name -> [calls, seconds inside synchronize]
55
+ if _SYNC_TIMING: # pragma: no cover
56
+ import atexit as _ax
57
+
58
+ @_ax.register
59
+ def _report_sync():
60
+ d, s_, y, n = _SYNC_ACC
61
+ if not n:
62
+ return
63
+ print(f"[sync-timing] launches={n} drain={d:.3f}s submit={s_:.3f}s "
64
+ f"synchronize={y:.3f}s (per launch: drain {1000*d/n:.3f}ms "
65
+ f"submit {1000*s_/n:.3f}ms sync {1000*y/n:.3f}ms)", flush=True)
66
+ if not _SYNC_PER_KERNEL:
67
+ return
68
+ rows = sorted(_SYNC_PER_KERNEL.items(), key=lambda kv: -kv[1][1])
69
+ tot = sum(v[1] for v in _SYNC_PER_KERNEL.values()) or 1.0
70
+ print(f"[sync-timing] per operator, by time inside synchronize:", flush=True)
71
+ print(f"[sync-timing] {'kernel':<44} {'calls':>7} {'sync s':>9} "
72
+ f"{'ms/call':>9} {'share':>7}", flush=True)
73
+ for name, (c, t) in rows[:18]:
74
+ print(f"[sync-timing] {name[:44]:<44} {c:>7} {t:>9.3f} "
75
+ f"{1000*t/max(c,1):>9.3f} {100*t/tot:>6.1f}%", flush=True)
76
+
77
+
78
+ def _native():
79
+ from triton_apple_backend import metal_native
80
+ return metal_native
81
+
82
+
83
+ #: Our `_pack_param` kinds -> the width, in bytes, of the value it produced.
84
+ #: A float arrives as the bit pattern of its storage type and an int as itself,
85
+ #: so both are written as an unsigned field of this width. Re-packing through
86
+ #: `struct.pack('<f', ...)` would treat a bit pattern as a number.
87
+ _KIND_WIDTH = {
88
+ "bits16": 2, "bits32": 4, "bits64": 8,
89
+ "i1": 1, "i8": 1, "i16": 2, "i32": 4, "i64": 8,
90
+ "u1": 1, "u8": 1, "u16": 2, "u32": 4, "u64": 8,
91
+ }
92
+
93
+
94
+ class TritonExtDriver(Driver):
95
+ """Loads a metallib and launches it the way triton-ext's own launcher does."""
96
+
97
+ #: triton-ext emits a compiled `.metallib`, not text MSL (`compiler.py`
98
+ #: stages: "msl" then "metallib"). The fork emits the text.
99
+ artifact_kind = "metallib"
100
+
101
+ #: Their launcher builds its argument mask from the kernel SIGNATURE, so two
102
+ #: extra scratch pointers would trip its own arity check:
103
+ #: "flat arg count does not match signature keep-mask length".
104
+ wants_scratch_params = False
105
+
106
+ #: This driver binds a pointer by wrapping memory, and a wrap costs what it maps (~11.5 ms per GiB on M4 Pro,
107
+ #: 2026-10-04): it asks the launcher for each tensor's addressable extent so it wraps that, not the rest of the
108
+ #: allocation the tensor lives in.
109
+ wants_extents = True
110
+
111
+ _instance: Optional["TritonExtDriver"] = None
112
+
113
+ @classmethod
114
+ def instance(cls) -> "TritonExtDriver":
115
+ if cls._instance is None:
116
+ cls._instance = cls()
117
+ return cls._instance
118
+
119
+ # -- compile target ----------------------------------------------------
120
+ def target(self):
121
+ # The same target the fork's driver builds: the seam owns the NAME
122
+ # ("metal" for the fork, "mps" for triton-ext) and the device owns the
123
+ # arch. Building it here again would be a second answer to one question.
124
+ from nbxtensor.backend.metal_backend import metal_target
125
+ return metal_target()
126
+
127
+ def max_shared_memory_per_block(self) -> int:
128
+ """triton-ext's own threadgroup budget, by the name it actually uses.
129
+
130
+ This read `getattr(hw_constants, "MAX_THREADGROUP_MEMORY", 32768)` until
131
+ 2026-09-17. **That name does not exist in triton-ext** — the module
132
+ declares `TG_BUDGET_BYTES`, `WARP_SIZE`, `SG_FRAG_DIM`, `TARGET`,
133
+ `target_arch` and nothing else — so every call took the literal, and the
134
+ launcher pruned configurations against a number no backend had said.
135
+ The same class as a literal standing in for a runtime value, in the one
136
+ shape that hides best: a `getattr` default that is never not taken.
137
+
138
+ It was right by luck. Measured the day it was found, M4 Pro:
139
+
140
+ hw_constants.TG_BUDGET_BYTES 32768
141
+ MTLDevice.maxThreadgroupMemoryLength 32768
142
+
143
+ so the number does not move; only the authority for it does. The
144
+ backend binds the same constant into its own launch metadata
145
+ (`driver.py:238`, `"max_shared_mem": _TG_BUDGET_BYTES`), which is what
146
+ makes it the right thing to ask.
147
+
148
+ A backend that stops declaring it refuses here, and
149
+ `max_shared_memory_per_block()` in the launcher turns that into None —
150
+ "do not prune" — which is the safe answer. Inventing one is not.
151
+ """
152
+ from triton_apple_backend import hw_constants
153
+ budget = getattr(hw_constants, "TG_BUDGET_BYTES", None)
154
+ if budget is None:
155
+ raise RuntimeError(
156
+ "triton_apple_backend.hw_constants declares no TG_BUDGET_BYTES; "
157
+ "the threadgroup-memory ceiling is the backend's to state and "
158
+ "this driver will not invent one. Declared names: "
159
+ f"{sorted(n for n in dir(hw_constants) if not n.startswith('_'))}")
160
+ return int(budget)
161
+
162
+ def block_for(self, metadata):
163
+ """Threads per threadgroup, as their launcher computes it: `lx` is
164
+ `num_warps * 32`, and ly/lz are 1 — Metal has no second launch axis
165
+ here."""
166
+ return (32 * int(getattr(metadata, "num_warps", 4)), 1, 1)
167
+
168
+ # -- load --------------------------------------------------------------
169
+ def load(self, binary: bytes, name: str, shared: int):
170
+ module = _native().load_metallib(bytes(binary))
171
+ fn = module.get_function(name)
172
+ import os as _os
173
+ if _os.environ.get("NBX_EXT_DEBUG"):
174
+ print(f"[ext] load name={name!r} metallib={len(bytes(binary))}B "
175
+ f"fn={fn} max_threads="
176
+ f"{getattr(fn, 'max_total_threads_per_threadgroup', None)}", flush=True)
177
+ return fn
178
+
179
+ # -- launch ------------------------------------------------------------
180
+ def trailing_buffers(self, metadata):
181
+ """What triton-ext's emitter appends to a kernel's own parameters.
182
+
183
+ Its compiler writes two descriptor blocks into the emitted MSL and
184
+ carries them in the metadata (`compiler.py`: `metadata["print_layout"]`,
185
+ `metadata["assert_layout"]`, from `AGPU-PRINT-LAYOUT` / `AGPU-ASSERT-LAYOUT`).
186
+ When a block is present the kernel has ONE MORE buffer parameter than
187
+ its Triton signature declares, and triton-ext's own driver binds it
188
+ last, print before assert.
189
+
190
+ We bound neither until 2026-09-17, and `metal_native.packArguments`
191
+ binds the tuple it is handed at indices 0..n-1 without ever comparing
192
+ the count to what the kernel declares — so the slot simply stayed
193
+ unbound. Measured that day (`probe_ki19_driver_protections.py`): a
194
+ `tl.device_assert` that FAILS on every lane ran to completion and raised
195
+ nothing, while the same kernel's ordinary output was correct. A lost
196
+ assert is the worst shape of that: the guard is paid for and not
197
+ delivered, which is the same fault class as the conv-1x1 zeros.
198
+
199
+ Four of NeuroBrix's own kernels are compiled `@triton.jit(debug=True)`
200
+ precisely to keep such an assert (`embedding`, `index_select` x2,
201
+ `index_put`), so this is not a hypothetical parameter.
202
+
203
+ Returns None when the kernel declares neither block — the common case,
204
+ and then `launch` does exactly what it did before.
205
+ """
206
+ from triton_apple_backend.device_assert import parse_assert_layout
207
+ from triton_apple_backend.device_print import parse_print_layout
208
+ pl = parse_print_layout(getattr(metadata, "print_layout", None))
209
+ al = parse_assert_layout(getattr(metadata, "assert_layout", None))
210
+ # The two address flags triton-ext's compiler records (`compiler.py`:
211
+ # `metadata["exposes_addresses"]`, `metadata["reads_addresses"]`) and its
212
+ # own launcher passes to every call: a kernel that stores a pointer's
213
+ # bits exposes its buffers, and one that reads through such bits must
214
+ # name the exposed buffers to Metal, which keeps resident only what the
215
+ # encoder names. We passed neither until 2026-10-04.
216
+ exposes = bool(getattr(metadata, "exposes_addresses", False))
217
+ reads = bool(getattr(metadata, "reads_addresses", False))
218
+ if pl is None and al is None and not exposes and not reads:
219
+ return None
220
+ return _Trailing(pl, al, exposes, reads)
221
+
222
+ def launch(self, function, grid, block, shared: int, stream: int,
223
+ params: Sequence[Tuple[str, Any]], names=None, types=None,
224
+ trailing=None, extents=None) -> None:
225
+ if types is None:
226
+ raise RuntimeError(
227
+ "the triton-ext driver needs the Triton type of every launch "
228
+ "parameter: its scalars share one packed buffer whose field "
229
+ "offsets come from those types. Launching without them would "
230
+ "pack to the wrong offsets WITHOUT failing, which is the exact "
231
+ "silent-zero this driver exists to end.")
232
+
233
+ # The argument list must be exactly what the kernel declares. `zip`
234
+ # below stops at the shorter of the two, so a short list would bind
235
+ # what it could and launch — silently, with the tail unbound. CUDA has
236
+ # refused this since the launcher existed (`CudaDriver.launch` compares
237
+ # against the cubin's own count); this driver did not, and the
238
+ # launcher contract caught it the first time it was run against a
239
+ # driver that is not the archived fork's (2026-09-17).
240
+ if len(params) != len(types):
241
+ raise RuntimeError(
242
+ f"NeuroBrix triton-ext driver: {len(params)} launch parameters "
243
+ f"against {len(types)} declared types — refused, not launched. "
244
+ f"Binding the shorter of the two leaves the rest unbound and "
245
+ f"says nothing.")
246
+
247
+ D = _ext()
248
+ ptr_args: List[Any] = []
249
+ scalar_types: List[str] = []
250
+ scalar_vals: List[Tuple[str, int]] = []
251
+
252
+ extents = list(extents) if extents is not None else [None] * len(params)
253
+ for ((kind, value), ty), extent in zip(zip(params, types), extents):
254
+ if kind == "ptr":
255
+ ptr_args.append(_buffer_for(int(value), ty, extent))
256
+ else:
257
+ scalar_types.append(ty)
258
+ scalar_vals.append((kind, int(value)))
259
+
260
+ args: Tuple[Any, ...] = tuple(ptr_args)
261
+ if scalar_types:
262
+ total, offsets = D._compute_scalar_layout(scalar_types)
263
+ limit = getattr(D, "_SETBYTES_LIMIT", 4096)
264
+ if total > limit:
265
+ raise RuntimeError(
266
+ f"packed scalar args are {total} bytes, over Metal's "
267
+ f"{limit}-byte setBytes limit")
268
+ buf = bytearray(total)
269
+ for (kind, val), ty, off in zip(scalar_vals, scalar_types, offsets):
270
+ width = _KIND_WIDTH.get(kind)
271
+ if width is None:
272
+ raise RuntimeError(
273
+ f"the triton-ext driver cannot pack a parameter of kind "
274
+ f"{kind!r} (Triton type {ty!r})")
275
+ fmt = {1: "<B", 2: "<H", 4: "<I", 8: "<Q"}[width]
276
+ struct.pack_into(fmt, buf, off, val & ((1 << (8 * width)) - 1))
277
+ args = args + (bytes(buf),)
278
+
279
+ gx, gy, gz = grid
280
+ lx, ly, lz = block
281
+ import os as _os
282
+ if _os.environ.get("NBX_EXT_DEBUG"):
283
+ print(f"[ext] ptrs={len(ptr_args)} scalars={list(zip(scalar_types, scalar_vals))} "
284
+ f"packed={(args[-1].hex() if scalar_types else None)} "
285
+ f"threads={[gx*lx, gy*ly, gz*lz]} group_size={[lx, ly, lz]} "
286
+ f"grid={grid} block={block} shared={shared}", flush=True)
287
+ # The emitter's own trailing parameters, bound LAST and in its order
288
+ # (print, then assert), which is what `planKernelAbi` fixed. Both must
289
+ # start zeroed: each block's head word is a running count the kernel
290
+ # bumps.
291
+ print_buf = assert_buf = None
292
+ addr_flags = {}
293
+ if trailing is not None:
294
+ if trailing.exposes_addresses:
295
+ addr_flags["exposes_addresses"] = True
296
+ if trailing.reads_addresses:
297
+ addr_flags["reads_addresses"] = True
298
+ if trailing is not None:
299
+ rt = D._runtime() # D is the pinned torch-free runtime's module
300
+ if trailing.print_layout is not None:
301
+ print_buf = rt.zeros_i32(trailing.print_layout.nbytes // 4)
302
+ args = args + (print_buf,)
303
+ if trailing.assert_layout is not None:
304
+ assert_buf = rt.zeros_i32(trailing.assert_layout.nbytes // 4)
305
+ args = args + (assert_buf,)
306
+
307
+ # Two queues, one device, shared buffers. Metal orders command buffers
308
+ # within a queue; ACROSS queues nothing is ordered without an explicit
309
+ # event (Apple: a fence cannot synchronize untracked resources accessed
310
+ # from separate queues; MTLEvent/MTLSharedEvent is the mechanism).
311
+ #
312
+ # NeuroBrix enqueues its device-to-device copies as blits on ITS queue
313
+ # and returns to the host immediately — correct while the kernels shared
314
+ # that queue, which is what `metal_device._blit` says in as many words:
315
+ # "a blit on the same queue as the kernels is ordered by the GPU and
316
+ # waits for nothing". triton-ext breaks that premise: its kernels run on
317
+ # its own queue, so a blit that has not landed is invisible to them.
318
+ #
319
+ # Measured on swin2SR (M4 Pro, 2026-09-17): without this drain, the conv
320
+ # at (1,180,448,448)->64 read 63181 NaN / 356 Inf / absmax 3.39e+38 from
321
+ # an input that is `absmax 1.492` and finite in every ordered run, and
322
+ # the fp64 screen refused all 18 candidates. A host round trip inserted
323
+ # before the launch made the same run clean, which is what identified
324
+ # the ordering rather than the arithmetic.
325
+ with _launch_pool(addr_flags):
326
+ if _SYNC_TIMING:
327
+ import time as _t
328
+ _a = _t.perf_counter(); _nbx_queue_drain()
329
+ _b = _t.perf_counter()
330
+ function(*args, threads=[gx * lx, gy * ly, gz * lz], group_size=[lx, ly, lz], **addr_flags)
331
+ _c = _t.perf_counter(); _native().synchronize(); _d = _t.perf_counter()
332
+ _SYNC_ACC[0] += _b - _a; _SYNC_ACC[1] += _c - _b
333
+ _SYNC_ACC[2] += _d - _c; _SYNC_ACC[3] += 1
334
+ _kn = getattr(function, "name", None) or type(function).__name__
335
+ _row = _SYNC_PER_KERNEL.get(_kn)
336
+ if _row is None:
337
+ _SYNC_PER_KERNEL[_kn] = [1, _d - _c]
338
+ else:
339
+ _row[0] += 1; _row[1] += _d - _c
340
+ else:
341
+ _nbx_queue_drain()
342
+ function(*args, threads=[gx * lx, gy * ly, gz * lz], group_size=[lx, ly, lz], **addr_flags)
343
+ # And the other direction: the host (and NeuroBrix's own blits) must
344
+ # see what this kernel wrote.
345
+ _native().synchronize()
346
+
347
+ # Read what the kernel recorded. Prints first, so anything it printed is
348
+ # already out when a failed assert raises.
349
+ if print_buf is not None:
350
+ from triton_apple_backend.device_print import format_records
351
+ rt = D._runtime()
352
+ for line in format_records(trailing.print_layout, rt.as_u32(print_buf)):
353
+ print(line, flush=True)
354
+ if assert_buf is not None:
355
+ from triton_apple_backend.device_assert import check as _check_asserts
356
+ rt = D._runtime()
357
+ _check_asserts(trailing.assert_layout, rt.as_u32(assert_buf))
358
+
359
+
360
+ def _launch_pool(addr_flags):
361
+ """The scope a kernel call and its drain run in: an autorelease pool when the kernel reads addresses.
362
+
363
+ Such a kernel is launched with the exposed buffers named to Metal: triton-ext builds that list as
364
+ `[exposedBuffers() allObjects]`, an AUTORELEASED array, and a Python thread has no pool around the call, so
365
+ it is never released and it holds every exposed buffer. Ours is the pinned scope's wrap of a whole
366
+ allocation: one such launch kept the arena in the process for good (2 116 MB of a freed 2 GiB arena; with
367
+ the launch inside a pool, 69-78 MB; deepseek-moe streamed went 11 -> 20 GB of footprint with one segment in
368
+ the allocator's books, 2026-10-04). Every other launch keeps the path it had.
369
+ """
370
+ if addr_flags.get("reads_addresses"):
371
+ import objc
372
+ return objc.autorelease_pool()
373
+ import contextlib
374
+ return contextlib.nullcontext()
375
+
376
+
377
+ class _Trailing(NamedTuple):
378
+ """The emitter-declared buffers that follow a kernel's own parameters.
379
+
380
+ Held as the PARSED layouts rather than the raw text: parsing is a regex
381
+ walk over the whole emitted module, and it is a property of the
382
+ compilation, so it happens once in `trailing_buffers` and never on the
383
+ launch path.
384
+ """
385
+ print_layout: Any
386
+ assert_layout: Any
387
+ #: triton-ext's two address flags for this compilation, passed to the kernel call as its own launcher does.
388
+ exposes_addresses: bool = False
389
+ reads_addresses: bool = False
390
+
391
+
392
+ #: Binding census, printed at exit under NBX_EXT_STATS=1. An interior binding
393
+ #: takes its LENGTH from the allocator's range table rather than from an exact
394
+ #: pointer hit, which is the one place a stale entry could hand back a buffer
395
+ #: shorter than the tensor.
396
+ _STATS = {"exact": 0, "interior": 0, "extent_bound": 0}
397
+
398
+ import os as _os_stats
399
+ if _os_stats.environ.get("NBX_EXT_STATS"):
400
+ import atexit as _atexit
401
+ _atexit.register(lambda: print(
402
+ f"[ext-stats] pointer bindings: exact={_STATS['exact']} "
403
+ f"interior={_STATS['interior']}", flush=True))
404
+
405
+ #: Metal's zero-copy wrap requires page-aligned memory; Apple Silicon pages
406
+ #: are 16 KiB.
407
+ _PAGE = 16384
408
+
409
+ #: Triton pointer type -> numpy dtype. `wrap` reads the element type off the
410
+ #: object it is given; a bare memoryview arrives as dtype None, which is not a
411
+ #: buffer triton-ext can bind.
412
+ _PTR_NP = {
413
+ "fp16": "float16", "bf16": "uint16", "fp32": "float32", "fp64": "float64",
414
+ "i1": "bool", "i8": "int8", "i16": "int16", "i32": "int32", "i64": "int64",
415
+ "u8": "uint8", "u16": "uint16", "u32": "uint32", "u64": "uint64",
416
+ "fp8e4nv": "uint8", "fp8e5": "uint8", "fp8e4b15": "uint8",
417
+ }
418
+
419
+
420
+ #: Wraps pinned by an open `pinned_addresses` scope: (addr, np_dtype) -> the
421
+ #: MetalBuffer every launch inside the scope reuses for that pointer. Keeping
422
+ #: ONE wrap per (address, element type) is what keeps a captured GPU address
423
+ #: meaning the same storage from one launch to the next.
424
+ _PINNED_WRAPS: dict = {}
425
+
426
+ # Pinned wraps: ONE uint8 wrap of each WHOLE allocation containing a pinned
427
+ # tensor, held HERE for the scope's lifetime, its address read through the
428
+ # served triton-ext API (`MetalBuffer.gpu_address()`). What that buys is a
429
+ # PINNED LIFETIME: the wrap, and so its GPU virtual address,
430
+ # outlives the launch that made it. A kernel that reads through a
431
+ # pointer-table entry needs the covering wrap ALIVE, and this driver otherwise
432
+ # makes a fresh wrap per launch. Once that wrap is dropped, its captured
433
+ # address reads wrong bytes with nothing raised. Upstream documents exactly
434
+ # this: an address is valid only while its buffer lives (triton-ext
435
+ # address.gpu_address). It is not a residency fix. Re-measured 2026-09-24
436
+ # against PR #126's head 8b45b9aa: an ALIVE wrap left unbound reads correctly
437
+ # from a later command buffer at 1 KiB, at 64 MiB and after churn, with or
438
+ # without a useResource; a DROPPED wrap reads 0/256
439
+ # (docs/reference/owed-proofs.md, 2026-09-24). Until 2026-10-04 this called
440
+ # `metal_native.retain_resident`, a function of the retired residency commit
441
+ # that the served build never had: every mixture-of-experts model failed at
442
+ # its first band on Apple (granite-3.1-1b-a400m, "has no attribute
443
+ # 'retain_resident'"). Upstream's contract is the one followed now:
444
+ # `gpu_address()` records the buffer as exposed (weakly), and a kernel compiled
445
+ # as reading addresses names the exposed buffers to Metal at its launch
446
+ # (`reads_addresses`, carried by `_Trailing`). Measured 2026-09-21
447
+ # (granite): per-expert kept wraps also worked but Metal declined the ~480th
448
+ # GB-scale alias, so one wrap per allocation is the shape that scales.
449
+ # base -> [wrap, refcount, gpu_va].
450
+ _RESIDENT_WRAPS: dict = {}
451
+
452
+
453
+ def _resident_acquire(addr: int) -> int:
454
+ """Pin one wrap of the whole allocation containing `addr` for the scope's
455
+ lifetime (`_RESIDENT_WRAPS` holds it); returns base."""
456
+ import numpy as np
457
+ from nbxtensor.memory.allocator import DeviceAllocator
458
+
459
+ size = DeviceAllocator._cuda_ptr_size.get(addr)
460
+ base = addr
461
+ if size is None:
462
+ if _recorded_shadow(addr) is not None:
463
+ # A census shadow allocation, recorded as one: memory that exists nowhere, so
464
+ # nothing to wrap and nothing to hold. Recognised, not skipped: an address that
465
+ # nobody recorded still fails below, in a shadow as in a real run.
466
+ return None
467
+ base, size = _containing_allocation(addr)
468
+ ent = _RESIDENT_WRAPS.get(base)
469
+ if ent is not None:
470
+ ent[1] += 1
471
+ return base
472
+ raw = (ctypes.c_byte * size).from_address(base)
473
+ view = np.frombuffer(memoryview(raw), dtype=np.uint8)
474
+ # Inside a pool: `gpu_address()` records the buffer in triton-ext's weak table of exposed buffers, and an
475
+ # insert into a weak NSHashTable can load its live members, each load a retain + AUTORELEASE. A Python
476
+ # thread has no pool, so that reference was never dropped: about one pinned scope in fifteen kept its whole
477
+ # arena for good (48 rounds on a fresh 128 MiB arena, no kernel: +128 MB at rounds 15, 30 and 45;
478
+ # deepseek-moe streamed kept one 8.5 GB segment at its third pass, 2026-10-04).
479
+ import objc
480
+ with objc.autorelease_pool():
481
+ buf = _native().wrap(view)
482
+ gpu_va = int(buf.gpu_address()) # also records it as exposed, weakly
483
+ _RESIDENT_WRAPS[base] = [buf, 1, gpu_va]
484
+ return base
485
+
486
+
487
+ def pinned_gpu_address(addr: int) -> int:
488
+ """The address a SHADER may read at `addr`, valid while its pin is held.
489
+
490
+ Every MTLBuffer has its own GPU virtual address — a no-copy wrap of the
491
+ same memory is a DIFFERENT VA, which is why an address captured through a
492
+ transient launch wrap reads zeros once that wrap dies (measured,
493
+ probe_read_through 2026-09-21). The pin's whole-allocation kept wrap
494
+ is therefore the one address authority: its gpu_address plus the CPU
495
+ offset inside the allocation, computable on the host with no launch.
496
+ """
497
+ from nbxtensor.memory.allocator import DeviceAllocator
498
+ size = DeviceAllocator._cuda_ptr_size.get(addr)
499
+ base = addr
500
+ if size is None:
501
+ if _recorded_shadow(addr) is not None:
502
+ # A recorded shadow has no GPU address to give; the shadow's own address stands
503
+ # for it, exactly as every other address in a shadow addresses nothing.
504
+ return int(addr)
505
+ base, size = _containing_allocation(addr)
506
+ ent = _RESIDENT_WRAPS.get(base)
507
+ if ent is None:
508
+ raise RuntimeError(
509
+ f"pinned_gpu_address(0x{addr:x}): its allocation is not under any "
510
+ f"pinned_addresses scope — an address handed out without a pin "
511
+ f"would go stale silently, which is the defect this refuses")
512
+ return ent[2] + (addr - base)
513
+
514
+
515
+ def _recorded_shadow(addr: int):
516
+ """(base, nbytes) when `addr` is a shadow allocation RECORDED as one (`memory.shadow`), else None.
517
+ Never true in a real run: nothing is recorded there unless an application installed the shadow."""
518
+ from nbxtensor.memory.shadow import shadow_range
519
+ return shadow_range(addr)
520
+
521
+
522
+ def _resident_release(base) -> None:
523
+ if base is None: # a recorded shadow held nothing
524
+ return
525
+ ent = _RESIDENT_WRAPS.get(base)
526
+ if ent is None:
527
+ return
528
+ ent[1] -= 1
529
+ if ent[1] <= 0:
530
+ _RESIDENT_WRAPS.pop(base, None) # the wrap dies here; the weak exposed table drops it
531
+ #: How many open scopes pin each address. A wrap is dropped only when the last
532
+ #: scope holding its address exits.
533
+ _PIN_COUNTS: dict = {}
534
+
535
+
536
+ class pinned_addresses:
537
+ """Pin the Metal wraps behind these tensors, so an address a kernel CAPTURES
538
+ stays valid for every launch inside the scope.
539
+
540
+ The contract this makes enforceable: a kernel may store the bitcast of a
541
+ pointer it was handed (``tl.cast(ptr, tl.int64, bitcast=True)``) into a
542
+ table, and a later kernel inside the same scope may load that table entry
543
+ and read through it. That is how a mixture-of-experts band addresses its
544
+ per-expert weights.
545
+
546
+ Why a scope and not a convention. ``_buffer_for`` builds a NEW
547
+ ``metal_native.wrap`` for every launch, so without this an address captured
548
+ in one launch names a buffer that is RELEASED before the next launch runs.
549
+ What the read then returns is UNDEFINED, and both outcomes were measured:
550
+ 0/256 in one ordering (verify_wrap_lifetime.py, 2026-09-19) and, when Metal
551
+ happened to hand a re-wrap of the same region the same GPU address,
552
+ 256/256 that LOOK correct (2026-09-20, in test ordering). With one wrap
553
+ kept alive it is 256/256 by contract rather than by accident:
554
+
555
+ wrap created fresh per launch (as shipped) undefined — an accident
556
+ the same wrap kept alive 256/256, by contract
557
+
558
+ The scope holds two things, because the lifetime has two halves and both
559
+ were measured:
560
+
561
+ * **the wraps** — Metal keeps the GPU address mapped to this buffer for as
562
+ long as the MetalBuffer object lives;
563
+ * **the tensors** — so the engine's allocator cannot free and reissue the
564
+ storage underneath. On MPS the FIRST same-size allocation after a free
565
+ reissued the exact address, and the stale table then read the newcomer's
566
+ bytes with nothing raised (repro_130_mps_reissue.py). A stale table does
567
+ not fail loudly in either direction, which is why this is a scope in code
568
+ and not a caution in a comment.
569
+
570
+ Nests: an address pinned by two scopes stays pinned until both exit.
571
+ """
572
+
573
+ def __init__(self, *tensors):
574
+ if not tensors:
575
+ raise RuntimeError(
576
+ "pinned_addresses with nothing to pin would promise a lifetime "
577
+ "it does not hold; pass the tensors whose addresses kernels "
578
+ "will capture")
579
+ self._tensors = tuple(tensors) # the allocator half of the lifetime
580
+ self._addrs = []
581
+
582
+ def __enter__(self):
583
+ self._resident_bases = []
584
+ for t in self._tensors:
585
+ addr = int(t.data_ptr())
586
+ _PIN_COUNTS[addr] = _PIN_COUNTS.get(addr, 0) + 1
587
+ self._addrs.append(addr)
588
+ self._resident_bases.append(_resident_acquire(addr))
589
+ return self
590
+
591
+ def __exit__(self, exc_type, exc, tb):
592
+ for addr in self._addrs:
593
+ n = _PIN_COUNTS.get(addr, 0) - 1
594
+ if n > 0:
595
+ _PIN_COUNTS[addr] = n
596
+ continue
597
+ _PIN_COUNTS.pop(addr, None)
598
+ for key in [k for k in _PINNED_WRAPS if k[0] == addr]:
599
+ _PINNED_WRAPS.pop(key, None)
600
+ for base in getattr(self, "_resident_bases", ()):
601
+ _resident_release(base)
602
+ self._resident_bases = []
603
+ self._addrs.clear()
604
+ return False
605
+
606
+
607
+ def _buffer_for(addr: int, ty: str, extent: Optional[int] = None):
608
+ """Alias the NBXTensor allocation containing `addr` as a MetalBuffer.
609
+
610
+ `extent`: the bytes the launch's tensor can address from `addr` (the launcher's `_addressable_extent`), or None.
611
+ Given, the wrap covers that, rounded up to the page and never past the allocation — not the rest of the
612
+ allocation: a no-copy wrap costs what it maps, and a weight at the start of its multi-GB arena made every launch
613
+ on it map the whole remaining arena (10.9 ms at 1 GiB, 46.7 ms at 4 GiB for a 0.5 ms gemv; orpheus decoded at
614
+ 6.3 s per token, 2026-10-04). A pinned address keeps the whole-allocation span its scope's captured table needs.
615
+
616
+ The launcher reduces a pointer parameter to its integer address, so the
617
+ length has to come from the allocator, which already records it. A tensor
618
+ VIEW points into the middle of an allocation, so the base is looked up
619
+ before the size.
620
+
621
+ `ty` is the Triton pointer type ("*fp32"): the element type has to travel
622
+ with the address, because `wrap` takes it from the object it is handed and a
623
+ typeless buffer is not bindable.
624
+ """
625
+ import numpy as np
626
+
627
+ elem = ty.lstrip("*k") if ty else ""
628
+ np_dtype = _PTR_NP.get(elem)
629
+ if np_dtype is None:
630
+ raise RuntimeError(
631
+ f"the triton-ext driver has no numpy element type for pointer "
632
+ f"parameter of type {ty!r}; binding it typeless would not fail, it "
633
+ f"would bind a buffer the kernel reads as zeros")
634
+ if addr == 0:
635
+ raise RuntimeError(
636
+ "the triton-ext driver was handed a null pointer parameter; its "
637
+ "launcher has no slot for one")
638
+ from nbxtensor.memory.allocator import DeviceAllocator
639
+
640
+ size = DeviceAllocator._cuda_ptr_size.get(addr)
641
+ base = addr
642
+ if size is None:
643
+ base, size = _containing_allocation(addr)
644
+ _STATS["interior"] += 1
645
+ else:
646
+ _STATS["exact"] += 1
647
+ # An interior pointer — a view, or a tensor packed inside a larger buffer —
648
+ # binds by starting the Metal buffer AT that address and running to the end
649
+ # of the allocation. Binding the allocation's base instead would read the
650
+ # wrong elements silently, and `metal_native` has no offset binding
651
+ # (`metal_native.m`: every argument goes in with `setBuffer:...offset:0`).
652
+ #
653
+ # Metal decides whether a given address can be wrapped with no copy. The
654
+ # documented requirement is page alignment, but MEASURED on M4 Pro a
655
+ # non-page-aligned interior address inside an already-mapped region is
656
+ # accepted (repro_ext_no_offset_binding.py, case 3: offset 0x300, wrapped,
657
+ # 64768 bytes). So we do not pre-judge it: we ask, and turn Metal's own
658
+ # refusal into a named one. Guessing a binding is what this driver exists
659
+ # to stop; refusing a binding that would have worked is merely wrong.
660
+ offset = addr - base
661
+ # Floor the span to a whole number of elements. The allocation runs to the
662
+ # end of its block, which need not be a multiple of this tensor's element
663
+ # size — and `np.frombuffer` refuses a remainder with "buffer size must be a
664
+ # multiple of element size". Measured on Kokoro-82M, aten.upsample_linear1d
665
+ # at (1, 9, 76800). Flooring is safe: the kernel addresses its own tensor,
666
+ # which ends at or before the block's end, so no element it reads or writes
667
+ # is cut off.
668
+ itemsize = np.dtype(np_dtype).itemsize
669
+ span = ((size - offset) // itemsize) * itemsize
670
+ if span <= 0:
671
+ raise RuntimeError(
672
+ f"the triton-ext driver cannot bind pointer 0x{addr:x}: the "
673
+ f"allocation leaves {size - offset} bytes from it, less than one "
674
+ f"{np_dtype} element")
675
+ # A launch inside a `pinned_addresses` scope must reuse the scope's wrap:
676
+ # a fresh wrap would be a fresh GPU address, and the table the scope exists
677
+ # for would go stale mid-scope.
678
+ _pin_key = (addr, np_dtype)
679
+ _pinned = _PINNED_WRAPS.get(_pin_key)
680
+ if _pinned is not None:
681
+ return _pinned
682
+ if extent is not None and addr not in _PIN_COUNTS:
683
+ reach = -(-max(int(extent), 1) // _PAGE) * _PAGE # the tensor's extent, rounded up to the page
684
+ span = (min(span, reach) // itemsize) * itemsize
685
+ _STATS["extent_bound"] += 1
686
+ raw = (ctypes.c_byte * span).from_address(addr)
687
+ view = np.frombuffer(memoryview(raw), dtype=np_dtype)
688
+ try:
689
+ _buf = _native().wrap(view)
690
+ if addr in _PIN_COUNTS:
691
+ _PINNED_WRAPS[_pin_key] = _buf
692
+ return _buf
693
+ except Exception as exc:
694
+ raise RuntimeError(
695
+ f"the triton-ext driver could not bind pointer 0x{addr:x} "
696
+ f"(0x{offset:x} into an allocation at 0x{base:x}, {size - offset} "
697
+ f"bytes): Metal declined a zero-copy view of it. `metal_native` "
698
+ f"binds every buffer at offset 0, so there is no other correct "
699
+ f"binding here — binding the allocation base would read the wrong "
700
+ f"elements WITHOUT failing. Metal said: {exc}") from exc
701
+
702
+
703
+ def _containing_allocation(addr: int) -> Tuple[int, int]:
704
+ from nbxtensor.memory.allocator import DeviceAllocator
705
+ hit = DeviceAllocator.containing(addr)
706
+ if hit is not None:
707
+ return hit
708
+ raise RuntimeError(
709
+ f"the triton-ext driver cannot bind pointer 0x{addr:x}: the allocator "
710
+ f"does not record it, so its length is unknown and it cannot be wrapped "
711
+ f"as a Metal buffer")
712
+
713
+
714
+ def driver() -> TritonExtDriver:
715
+ return TritonExtDriver.instance()
716
+
717
+
718
+ def _nbx_queue_drain():
719
+ """Wait for NeuroBrix's own Metal queue before a foreign queue reads its
720
+ buffers. Coarse — a host wait — but correct; an MTLSharedEvent between the
721
+ two queues is the finer instrument and is owed if this costs measurably."""
722
+ from nbxtensor.backend.metal_device import runtime
723
+ rt = runtime()
724
+ # Skip the round trip when this queue has nothing that could be writing.
725
+ # `sync()` does `flush()` and then commits an EMPTY command buffer and waits
726
+ # on it — and that empty buffer queues behind triton-ext's work on a busy
727
+ # GPU, which is why it costs 0.458 ms per launch in a decode against
728
+ # 0.019 ms on an idle queue standalone. Measured over a 120-token TinyLlama
729
+ # decode, 4943 drains: 0 had a committed buffer pending, 1240 (25.1 %) had an
730
+ # open encoder, and 3703 (74.9 %) had NOTHING. Waiting for work that does not
731
+ # exist cannot order anything, so those 3703 are skipped.
732
+ if not rt.has_pending_gpu_writes():
733
+ return
734
+ rt.sync()
735
+