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,429 @@
1
+ """What a device driver must present to the engine's kernel launcher.
2
+
3
+ The launcher itself is not here. It is an engine component, vendor-agnostic,
4
+ and it belongs in the dispatch layer with CUDA as its first client. This
5
+ module is the **contract** that launcher will program against: the smallest
6
+ surface a driver has to expose for the launcher to compile a kernel once and
7
+ dispatch it many times, on any backend.
8
+
9
+ It exists because the Metal port had to build a driver anyway — upstream
10
+ Triton's own launcher imports torch inside its argument binder, on every
11
+ backend, and R33 does not have an Apple exception — and building it made the
12
+ surface concrete. `triton/metal_driver.py` is the first implementation and it
13
+ passes the checker below. A CUDA implementation has to pass the same checker,
14
+ unchanged, or the launcher would be programming against Metal's habits rather
15
+ than against an interface.
16
+
17
+ ## The surface, in one paragraph
18
+
19
+ A driver **compiles** a `@triton.jit` function plus a signature and a set of
20
+ `tl.constexpr` values into a `CompiledKernel`. That object carries the
21
+ compiled artifact, the argument binding the launcher must fill, the
22
+ threadgroup/block width, and the shared-memory request. The launcher then
23
+ **launches** it with a grid and a flat argument list, where every device
24
+ buffer is passed as an **integer pointer** and every scalar as a typed Python
25
+ value. Ordering is expressed with **streams** and **events**, both opaque
26
+ integer handles.
27
+
28
+ Nothing in that paragraph is Metal-shaped, and that is the point.
29
+
30
+ ## What the contract deliberately does NOT include
31
+
32
+ * **Tensors.** The launcher passes pointer integers, not container objects. A
33
+ driver never learns what an NBXTensor is, and NBXTensor never learns what a
34
+ backend is; the allocator already owns that seam
35
+ (`kernels/nbx_tensor.py::DeviceAllocator`).
36
+ * **Autotuning, caching, heuristics.** Choosing `BLOCK_SIZE` is the engine's
37
+ business and it is the same decision on every backend. A driver is asked to
38
+ compile what it is given.
39
+ * **A device abstraction.** Allocation, copies and device queries are the
40
+ allocator's, not the driver's. The driver receives addresses that already
41
+ exist.
42
+ * **torch.** On any backend. A driver that needs torch to bind an argument
43
+ has put the framework back in the execution path, which is the thing R33
44
+ forbids and the reason this contract was written at all.
45
+
46
+ R33 preserved — no torch, at the boundary included.
47
+ R34 preserved — nothing here is keyed on a model name.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ from dataclasses import dataclass
53
+ from typing import Any, Mapping, Protocol, Sequence, runtime_checkable
54
+
55
+ #: Scalar kinds a launcher may be asked to pack. A driver must accept every
56
+ #: one of these spellings, because they are Triton's own signature spellings
57
+ #: and the launcher passes them straight through.
58
+ SCALAR_DTYPES = ("i1", "i8", "i16", "i32", "i64",
59
+ "u8", "u16", "u32", "u64",
60
+ "fp16", "bf16", "fp32", "fp64")
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class ArgSlot:
65
+ """One argument the launcher must supply, in binding order.
66
+
67
+ `index` is the driver's own binding index, not the position in the Python
68
+ call: a backend may reserve indices or reorder, and the launcher must not
69
+ have to know which. It fills the slots in the order they are given.
70
+ """
71
+
72
+ index: int
73
+ name: str
74
+ is_pointer: bool
75
+ dtype: str
76
+
77
+ def __post_init__(self):
78
+ if not self.is_pointer and self.dtype not in SCALAR_DTYPES:
79
+ raise ValueError(
80
+ f"{self.name}: {self.dtype!r} is not a scalar dtype the "
81
+ f"launcher can pack; expected one of {SCALAR_DTYPES}")
82
+
83
+
84
+ @runtime_checkable
85
+ class CompiledKernel(Protocol):
86
+ """The result of compiling once, launched many times."""
87
+
88
+ #: The kernel's entry-point name, as the backend knows it.
89
+ name: str
90
+
91
+ #: The compiled artifact, opaque to the launcher, in whatever form this
92
+ #: backend reloads without recompiling: a cubin, a metallib, an MSL
93
+ #: source. The launcher only ever caches and hands it back.
94
+ binary: bytes
95
+
96
+ #: What `binary` is — "cubin", "metallib", "msl", "hsaco". Declared so a
97
+ #: cache can refuse an artifact built for another backend instead of
98
+ #: loading it and failing later.
99
+ binary_kind: str
100
+
101
+ #: Threads per block / threadgroup.
102
+ block_size: int
103
+
104
+ #: Bytes of shared / threadgroup memory the kernel requests.
105
+ shared_memory: int
106
+
107
+ #: The `tl.constexpr` values this kernel was compiled with. Part of the
108
+ #: identity of the compilation, so a cache keyed on the source alone is
109
+ #: wrong and this makes that visible.
110
+ constexprs: Mapping[str, Any]
111
+
112
+ #: The specialization markers this kernel was compiled with — the same
113
+ #: mapping `compile()` was given. Also part of the compilation's
114
+ #: identity: the same source and the same constexprs, compiled once
115
+ #: claiming 16-byte alignment and once not, are two different kernels,
116
+ #: and handing one back for the other is silently wrong numbers.
117
+ specialization: Mapping[str, str]
118
+
119
+ #: The arguments the launcher must supply, in binding order.
120
+ binding: Sequence[ArgSlot]
121
+
122
+ def launch(self, grid: Sequence[int], args: Sequence[Any],
123
+ stream: int = 0) -> None:
124
+ """Dispatch `grid` blocks.
125
+
126
+ `args` is positional and matches `binding` one for one. A slot with
127
+ `is_pointer` takes an **integer device address**; every other slot
128
+ takes a Python int or float, which the driver packs to `dtype`.
129
+
130
+ `stream` is 0 for the default stream, or a handle from
131
+ `create_stream`. The call returns once the work is enqueued; ordering
132
+ against other work is the caller's, expressed with events.
133
+ """
134
+
135
+
136
+ @runtime_checkable
137
+ class LauncherDriver(Protocol):
138
+ """What the launcher needs from a backend, and nothing more."""
139
+
140
+ #: "cuda", "hip", "metal". The launcher uses it for diagnostics and for
141
+ #: refusing a cached artifact from another backend, never to branch on.
142
+ backend: str
143
+
144
+ def compile(self, jit_fn, signature: Mapping[str, str],
145
+ constexprs: Mapping[str, Any],
146
+ num_warps: int = 4,
147
+ specialization: Mapping[str, str] | None = None,
148
+ num_stages: int | None = None,
149
+ ) -> CompiledKernel:
150
+ """Compile one `@triton.jit` function.
151
+
152
+ `signature` maps parameter name to a Triton signature string —
153
+ `"*fp32"` for a pointer, `"i32"` / `"fp32"` for a scalar, and
154
+ `"constexpr"` for a compile-time parameter whose value is in
155
+ `constexprs`. Raises rather than returning a partial result.
156
+
157
+ `specialization` maps parameter name to Triton's per-argument marker:
158
+ `"D"` when the argument is a multiple of 16 (a pointer's address, an
159
+ integer's value), `""` when it is not, absent when the argument takes
160
+ no marker at all. It is NOT decoration — the compiler turns "D" into
161
+ a divisibility attribute and vectorizes loads and stores on it, so a
162
+ driver that ignores this argument compiles a different kernel from
163
+ the one the launcher asked for, at the same shape, with the same
164
+ arguments. Measured on Metal 2026-09-05: dropping it changed
165
+ rms_norm's fp16 output at two of four shapes while leaving fp32
166
+ untouched.
167
+
168
+ A driver is free to have no use for it, but must then say so
169
+ explicitly rather than accept and discard it.
170
+
171
+ `num_stages` is the software-pipelining depth, which an autotuner
172
+ config carries alongside `num_warps`. Measured on Metal 2026-09-05:
173
+ it changes nothing in the emitted MSL, because that lowerer does not
174
+ pipeline — but it changes CUDA's generated code substantially, so the
175
+ launcher passes it and each backend decides. Dropping it in the
176
+ launcher would silently discard the autotuner's choice on the first
177
+ backend that reads it.
178
+ """
179
+
180
+ # -- ordering ------------------------------------------------------------
181
+
182
+ def create_stream(self) -> int:
183
+ """A new stream. Never returns 0, which is the default stream."""
184
+
185
+ def destroy_stream(self, stream: int) -> None:
186
+ ...
187
+
188
+ def synchronize_stream(self, stream: int) -> None:
189
+ """Block until everything on `stream` has retired."""
190
+
191
+ def create_event(self, timing: bool = False) -> int:
192
+ """An event handle. `timing=False` is an ordering handle and must
193
+ REFUSE to be read as a stopwatch — see `elapsed_ms`."""
194
+
195
+ def destroy_event(self, event: int) -> None:
196
+ ...
197
+
198
+ def record_event(self, event: int, stream: int = 0) -> None:
199
+ ...
200
+
201
+ def synchronize_event(self, event: int) -> None:
202
+ """Block until `event` has been reached."""
203
+
204
+ def wait_event(self, stream: int, event: int) -> None:
205
+ """Make `stream` wait for `event` WITHOUT blocking the host."""
206
+
207
+ def elapsed_ms(self, start: int, end: int) -> float:
208
+ """Milliseconds between two recorded timing events.
209
+
210
+ Must raise for an event created with `timing=False`: an ordering
211
+ handle is not a clock, and returning a plausible number instead of
212
+ refusing is the failure this method exists to prevent.
213
+ """
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # The checker
218
+ # ---------------------------------------------------------------------------
219
+
220
+ def verify_driver_contract(driver, jit_fn, signature, constexprs,
221
+ make_buffer, read_buffer, free_buffer,
222
+ grid, args_builder, expected):
223
+ """Exercise a driver against the contract. Returns a list of failures.
224
+
225
+ Written as a plain function rather than a test so a backend can run it
226
+ from anywhere — a CI job, a `doctor` subcommand, a notebook on a machine
227
+ none of us has. `tests/unit/triton/test_launcher_contract.py` calls it
228
+ for Metal.
229
+
230
+ It exercises the driver **exactly as `kernels/launcher.py` does**: Triton
231
+ compiles for the target the driver names, the driver loads the artifact
232
+ it declared, and the driver launches it with the launcher's own
233
+ `(kind, value)` parameter list AND everything that travels beside it —
234
+ `names`, `types`, and the `trailing` buffers the driver itself declared
235
+ from the compile metadata. A checker that used a private path of its own
236
+ would certify a driver the engine cannot actually drive; until 2026-09-17
237
+ this one omitted all three, so a driver that needs any of them could not be
238
+ certified here at all.
239
+
240
+ The callables are the caller's, because allocation is the allocator's job
241
+ and this module must not grow one:
242
+
243
+ * `make_buffer(nbytes) -> int` — a device address.
244
+ * `read_buffer(addr, nbytes) -> bytes`
245
+ * `free_buffer(addr) -> None`
246
+ * `args_builder(pointers) -> list` — the flat argument list, given the
247
+ pointers it asked for by allocating them.
248
+ """
249
+ from nbxtensor.backend.launcher import _pack_param
250
+
251
+ failures = []
252
+
253
+ def check(condition, message):
254
+ if not condition:
255
+ failures.append(message)
256
+ return condition
257
+
258
+ # -- the declared surface ------------------------------------------------
259
+ check(isinstance(getattr(driver, "artifact_kind", None), str)
260
+ and driver.artifact_kind,
261
+ "a driver must declare `artifact_kind` — the key of its artifact "
262
+ "in CompiledKernel.asm — so a cache cannot hand one backend "
263
+ "another's binary")
264
+ check(isinstance(getattr(driver, "wants_scratch_params", None), bool),
265
+ "a driver must declare `wants_scratch_params`: whether its launch "
266
+ "ABI carries Triton's two trailing scratch pointers")
267
+
268
+ target = driver.target()
269
+ check(isinstance(getattr(target, "backend", None), str) and target.backend,
270
+ f"target() must name a backend, got {target!r}")
271
+ check(int(getattr(target, "warp_size", 0)) > 0,
272
+ f"target() must give a positive warp size, got {target!r}")
273
+
274
+ # -- compile, load, launch: the launcher's own three steps ---------------
275
+ import triton
276
+ from triton.compiler.compiler import ASTSource
277
+
278
+ compiled = triton.compile(
279
+ ASTSource(fn=jit_fn, signature=signature, constexprs=constexprs),
280
+ target=target, options={"num_warps": 4})
281
+ metadata = compiled.metadata
282
+
283
+ artifact = compiled.asm.get(driver.artifact_kind)
284
+ if not check(artifact is not None,
285
+ f"the compiler produced {sorted(compiled.asm)} and the "
286
+ f"driver takes {driver.artifact_kind!r}: nothing to load"):
287
+ return failures
288
+
289
+ block = driver.block_for(metadata)
290
+ check(isinstance(block, tuple) and len(block) == 3
291
+ and all(isinstance(b, int) and b > 0 for b in block),
292
+ f"block_for() must return a 3-tuple of positive ints, got {block!r}")
293
+
294
+ function = driver.load(artifact, metadata.name, metadata.shared)
295
+ check(function is not None, "load() returned nothing")
296
+
297
+ # -- the launch produces the right bytes, at a grid wider than one block -
298
+ pointers = [make_buffer(n) for n in args_builder.buffer_sizes]
299
+ try:
300
+ values = args_builder(pointers)
301
+ ordered = [(name, ty) for name, ty in signature.items()
302
+ if ty != "constexpr"]
303
+ if not check(len(ordered) == len(values),
304
+ f"the signature has {len(ordered)} runtime parameters "
305
+ f"and args_builder gave {len(values)}"):
306
+ return failures
307
+ params = [_pack_param(ty, v) for (_n, ty), v in zip(ordered, values)]
308
+ if driver.wants_scratch_params:
309
+ params = params + [("ptr", 0), ("ptr", 0)]
310
+
311
+ # Everything `kernels/launcher.py` hands a driver, because that is what
312
+ # this function claims to do. Omitting any of it certifies a driver
313
+ # against a call the engine never makes: a backend that packs its
314
+ # scalars into one buffer needs `types` to place the fields, one that
315
+ # binds by name needs `names`, and one whose emitter appends its own
316
+ # buffers needs `trailing` — without which the checker would bless a
317
+ # launch that binds fewer buffers than the kernel declares.
318
+ #
319
+ # `names` and `types` cover the SIGNATURE's runtime parameters and stop
320
+ # there. The launcher appends the two scratch pointers to `params` and
321
+ # to nothing else, so this must not "helpfully" extend them either: the
322
+ # point is to make the launcher's call, divergences included.
323
+ names = [name for name, _ty in ordered]
324
+ types = [ty for _name, ty in ordered]
325
+ trailing = driver.trailing_buffers(metadata)
326
+
327
+ # THREE extents, because that is what the launcher hands a driver:
328
+ # `launch()` pads with `+ (1,) * (3 - len(grid))` before the call. This
329
+ # checker passed the caller's grid through unpadded, and a driver that
330
+ # unpacks `gx, gy, gz = grid` — as triton-ext's does — raised
331
+ # ValueError on an ordinary one-dimensional launch. The archived fork's
332
+ # driver tolerated a short tuple, so the gap sat here unseen until the
333
+ # backend changed.
334
+ padded = tuple(int(g) for g in grid) + (1,) * (3 - len(grid))
335
+
336
+ def _launch(p):
337
+ driver.launch(function, padded, block, metadata.shared, 0, p,
338
+ names=names, types=types, trailing=trailing)
339
+
340
+ _launch(params)
341
+ got = read_buffer(pointers[args_builder.output_index],
342
+ args_builder.output_bytes)
343
+ check(got == expected,
344
+ "the launch produced the wrong bytes. The grid is deliberately "
345
+ "wider than one block: a dropped program offset passes a "
346
+ "single-block launch and fails here.")
347
+
348
+ # A wrong-length argument list must be refused, not padded.
349
+ try:
350
+ _launch(params[:-1])
351
+ failures.append("launch() accepted an argument list of the wrong "
352
+ "length instead of refusing it")
353
+ except Exception:
354
+ pass
355
+
356
+ # A pointer the allocator never handed out must be refused. This is
357
+ # the ownership rule: the engine owns device memory, and a launch
358
+ # that reads an address from anywhere else is undefined behaviour
359
+ # the driver is in the best position to catch.
360
+ foreign = list(params)
361
+ for i, (kind, _v) in enumerate(foreign):
362
+ if kind == "ptr":
363
+ foreign[i] = ("ptr", 0x1000)
364
+ break
365
+ try:
366
+ _launch(foreign)
367
+ failures.append("launch() accepted a device address the "
368
+ "allocator never handed out")
369
+ except Exception:
370
+ pass
371
+ finally:
372
+ for address in pointers:
373
+ free_buffer(address)
374
+
375
+ # -- events --------------------------------------------------------------
376
+ # A DIFFERENT PROTOCOL, and not every backend puts it on the driver. The
377
+ # launcher calls none of these — it uses `load`, `launch`, `block_for`,
378
+ # `target`, `artifact_kind`, `wants_scratch_params`, `trailing_buffers` and
379
+ # `max_shared_memory_per_block`, and nothing else. On Apple the streams and
380
+ # events belong to the ALLOCATOR (`kernels/metal_device.py`), so
381
+ # `TritonExtDriver` implements the launch half only. Measured 2026-09-17,
382
+ # when archiving the fork replaced a driver that carried both halves on one
383
+ # object and hid the distinction.
384
+ #
385
+ # A driver that does not claim this half is NOT failed here: certifying it
386
+ # against an API the engine never calls would be this checker inventing a
387
+ # contract, which is the fault it exists to prevent. It is SAID instead, so
388
+ # the gap stays visible.
389
+ _ordering_api = ("create_event", "record_event", "synchronize_event",
390
+ "create_stream", "wait_event", "synchronize_stream",
391
+ "destroy_event", "destroy_stream", "elapsed_ms")
392
+ _absent = [m for m in _ordering_api if not hasattr(driver, m)]
393
+ if _absent:
394
+ print(f" note: {driver.__class__.__name__} implements the LAUNCH half "
395
+ f"of this contract and not the ordering half "
396
+ f"({', '.join(_absent)}); on this backend streams and events "
397
+ f"belong to the allocator. Not certified here, not a failure.",
398
+ flush=True)
399
+ return failures
400
+
401
+ ordering = driver.create_event(timing=False)
402
+ timing_a = driver.create_event(timing=True)
403
+ timing_b = driver.create_event(timing=True)
404
+ try:
405
+ driver.record_event(ordering, 0)
406
+ driver.synchronize_event(ordering)
407
+ second = driver.create_stream()
408
+ driver.wait_event(second, ordering)
409
+ driver.synchronize_stream(second)
410
+ driver.destroy_stream(second)
411
+
412
+ driver.record_event(timing_a, 0)
413
+ driver.record_event(timing_b, 0)
414
+ driver.synchronize_event(timing_b)
415
+ interval = driver.elapsed_ms(timing_a, timing_b)
416
+ check(isinstance(interval, float) and interval >= 0.0,
417
+ f"elapsed_ms must return a non-negative float, got {interval!r}")
418
+
419
+ try:
420
+ driver.elapsed_ms(ordering, ordering)
421
+ failures.append("elapsed_ms returned a number for an ordering "
422
+ "event; an ordering handle is not a clock")
423
+ except Exception:
424
+ pass
425
+ finally:
426
+ for e in (ordering, timing_a, timing_b):
427
+ driver.destroy_event(e)
428
+
429
+ return failures