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,1041 @@
1
+ """Shared autotune configurations for Triton kernels.
2
+
3
+ V100 safety: _safe_num_stages() clamps num_stages to 2 on pre-Ampere GPUs.
4
+ V100 (sm_70) lacks cp.async — Triton's software pipelining with num_stages > 2
5
+ emits async copy instructions that cause CUDA_ERROR_MISALIGNED_ADDRESS on Volta.
6
+ """
7
+
8
+ import inspect
9
+ import sys
10
+ import warnings
11
+ from typing import Dict, List, Optional
12
+
13
+ from pathlib import Path
14
+
15
+ import triton
16
+ from triton import next_power_of_2
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Autotune compatibility across Triton versions
21
+ # ---------------------------------------------------------------------------
22
+
23
+ _AUTOTUNE_DROPPED: set = set()
24
+
25
+
26
+ def nbx_autotune(*args, **kwargs):
27
+ """`triton.autotune`, minus any keyword the installed Triton lacks.
28
+
29
+ NeuroBrix passes `cache_results=True` so autotune results persist to
30
+ disk instead of being re-measured every process. That keyword arrived
31
+ in a later Triton than the one `pip install torch --index-url .../cu121`
32
+ resolves (Triton 3.1 for torch 2.5), and passing it there raises
33
+
34
+ TypeError: autotune() got an unexpected keyword argument 'cache_results'
35
+
36
+ at the FIRST kernel launch — which made a clean install of NeuroBrix
37
+ unable to run a single model on any GPU whose torch build ships an
38
+ older Triton. That is what this shim exists to stop.
39
+
40
+ The degradation is real but bounded, and it is announced rather than
41
+ hidden: `cache_results` is a caching optimisation, so dropping it costs
42
+ one autotune sweep per process and changes no result. Anything that
43
+ would change a RESULT must never be dropped this way — it would be a
44
+ silent fallback, which this engine does not do.
45
+ """
46
+ import triton
47
+
48
+ try:
49
+ supported = set(inspect.signature(triton.autotune).parameters)
50
+ except (TypeError, ValueError):
51
+ return triton.autotune(*args, **kwargs)
52
+ # Triton's own on-disk autotune cache (`cache_results`) keys its files by
53
+ # `make_backend(driver.active.get_current_target()).hash()` — the driver
54
+ # probe that imports torch (R33, universal since 2026-09-05). The engine
55
+ # persists its sweeps itself (triton/autotune_cache.py seeds the tuner's
56
+ # cache from ~/.neurobrix/replay_cache), so the upstream file cache is
57
+ # dropped here: same results, one persistence, no probe.
58
+ kwargs.pop("cache_results", None)
59
+ if "do_bench" in supported and "do_bench" not in kwargs:
60
+ # The sweep's timing through the engine's runtime (CUDA events via
61
+ # the allocator), not Triton's benchmarker, which asks torch for its
62
+ # events and its flush buffer (R33, universal since 2026-09-05).
63
+ from nbxtensor.backend.launcher import do_bench as _nbx_do_bench
64
+ kwargs["do_bench"] = _nbx_do_bench
65
+
66
+ unsupported = {k for k in kwargs if k not in supported}
67
+ if unsupported:
68
+ for name in sorted(unsupported - _AUTOTUNE_DROPPED):
69
+ _AUTOTUNE_DROPPED.add(name)
70
+ warnings.warn(
71
+ f"Triton {getattr(triton, '__version__', '?')} does not support "
72
+ f"@triton.autotune({name}=...); continuing without it. "
73
+ f"Autotune results will be re-measured once per process instead "
74
+ f"of persisted to disk (slower first launch, identical results). "
75
+ f"Triton 3.6+ restores it.",
76
+ RuntimeWarning,
77
+ stacklevel=2,
78
+ )
79
+ kwargs = {k: v for k, v in kwargs.items() if k not in unsupported}
80
+
81
+ decorator = triton.autotune(*args, **kwargs)
82
+
83
+ def announce_then_decorate(fn):
84
+ return _announce_first_sweep(decorator(fn))
85
+
86
+ return announce_then_decorate
87
+
88
+
89
+ # Said once per process, the first time the autotuner actually measures.
90
+ _SWEEP_ANNOUNCED = [False]
91
+ _SEEDED = [False]
92
+
93
+ _SWEEP_NOTICE = (
94
+ "[neurobrix] First run for these tensor shapes on this machine: measuring "
95
+ "kernel configurations.\n"
96
+ "[neurobrix] This happens ONCE per shape per machine and is kept in the engine's "
97
+ "artifact (~/.neurobrix/replay_cache/autotune_configs_<arch>.json).\n"
98
+ "[neurobrix] Later runs skip it entirely — measured 254.9 s -> 7.9 s on "
99
+ "whisper-large-v3-turbo, 15.5 s -> 7.4 s on TinyLlama-1.1B.\n"
100
+ )
101
+
102
+
103
+ def _announce_first_sweep(tuned):
104
+ """Say, once, that the engine is tuning rather than hung.
105
+
106
+ A first `whisper-large-v3-turbo` transcription spends **247 seconds**
107
+ autotuning before it produces a word, and said nothing at all while it did
108
+ — measured 2026-09-03,
109
+ `validation_outputs/audio_launch_census_2026_09_03/VERDICT.md`. The second
110
+ run of the same command takes 7.9 s. A user whose first run looks hung for
111
+ four minutes concludes the engine is broken, and they are not being
112
+ unreasonable: nothing on screen distinguished it from a hang.
113
+
114
+ The cost is not a defect — it buys ~12 % throughput per the autotune policy
115
+ and it is paid once per shape per machine. Being silent about it was.
116
+
117
+ The wrapper REMOVES ITSELF once it has spoken, restoring the class method,
118
+ so the warm path (the overwhelming majority of launches) pays nothing for
119
+ an announcement that has already happened.
120
+ """
121
+ cache = getattr(tuned, "cache", None)
122
+ if cache is None: # not an Autotuner — nothing to watch
123
+ return tuned
124
+ launch = tuned.run
125
+ # A dry run (an application's launch observer, active: NeuroBrix's census shadow) keeps this wrapper — it is the
126
+ # one door where the key is formed and recorded — and drops what stands behind it: no bench, no launch, no
127
+ # device. The observer is read at every call, so one installed after this kernel was defined is the one consulted.
128
+ from nbxtensor.ops.launch_observer import launch_observer
129
+
130
+ def run_with_notice(*args, **kwargs):
131
+ _census = launch_observer() # the application's launch observer (dry run), if any
132
+ original = _census.shadow_run if _census.active() else launch
133
+ # The engine's artifact is the ONLY persistence of a sweep since
134
+ # 2026-09-05 (upstream's on-disk cache was dropped: its key was a
135
+ # torch-importing driver probe): seed once, capture every growth.
136
+ from nbxtensor.ops import autotune_cache as _atc
137
+ from nbxtensor.ops import autotune_certified as _cert_mode
138
+ # A CONFIRMATION run (--certified-only) serves the certified directory and nothing else:
139
+ # the local replay cache holds earlier SWEEPS, so it is never seeded here (a gate that
140
+ # read it looked "served" while 39 % of its key uses were replayed sweeps — the Mac,
141
+ # 2026-09-28). A census shadow is not a confirmation run and keeps its own rules.
142
+ _confirm = _cert_mode.certified_only() and not _census.active()
143
+ if not _SEEDED[0]:
144
+ _SEEDED[0] = True
145
+ if not _confirm:
146
+ try:
147
+ _n = _atc.seed()
148
+ _cert_mode.note_local(_n)
149
+ _cert_mode.override_seeded() # the directory first, even on a warm machine
150
+ except Exception: # the replay cache is an optimisation, never a failure source
151
+ pass
152
+ # The certified directory (owner directive 2026-09-06, the engine
153
+ # component): a shape the directory certifies for the profile in
154
+ # force, this kernel and its dtype is applied here — no sweep, no
155
+ # screen. A shape it does not know sweeps at runtime with the
156
+ # consensus screen, is said in clear, and lands in the local replay
157
+ # cache, never in the directory.
158
+ try:
159
+ key = _atc.key_of(tuned, args, kwargs)
160
+ except Exception as exc:
161
+ # A key that cannot be formed cannot be looked up: in a confirmation run that is a refusal,
162
+ # never the autotuner's sweep behind a skipped lookup (the certified-only door, 2026-11-03)
163
+ if _confirm:
164
+ from nbxtensor.ops import autotune_certified as _cert
165
+ qual = _atc._qual_of(tuned) or getattr(getattr(tuned, "base_fn", None), "__name__", "?")
166
+ raise _cert.KeyNotCertified(f"CERTIFIED-ONLY: the launch key of {qual} cannot be formed "
167
+ f"({type(exc).__name__}: {exc}) — nothing to look up, never a sweep") from exc
168
+ key = None
169
+ # THE KEY RECORD (2026-09-21, the census doctrine): every key this call forms — served
170
+ # from the directory, swept, or replayed — written as the certifier reads it, one door
171
+ # for the live proof target and for the shadow census. NBX_KEY_RECORD=<path>.
172
+ if key is not None:
173
+ _census.record(tuned, key) # every launch offers its key; the observer of this call
174
+ if key is not None and key not in cache:
175
+ from nbxtensor.ops import autotune_certified as _cert
176
+ qual = _atc._qual_of(tuned) or getattr(getattr(tuned, "base_fn", None), "__name__", "?")
177
+ try:
178
+ # the card this launch executes on: its tensor's device, read in
179
+ # the Prism profile — an entry serves only the memory class it
180
+ # covered (register 56)
181
+ mcls = _cert.executing_memory_gb(args)
182
+ applied = _cert.apply(qual, tuned, key, memory_gb=mcls)
183
+ if not applied:
184
+ # an operand widened on load is keyed by the dtype it is computed in
185
+ twin = _cert.computed_key(tuned, key, kwargs)
186
+ applied = bool(twin) and _cert.apply(qual, tuned, key, lookup_key=twin, memory_gb=mcls)
187
+ except Exception as exc: # the directory is an optimisation, never a failure source —
188
+ if _confirm: # except in a confirmation run, where it is the only source
189
+ raise _cert.KeyNotCertified(f"CERTIFIED-ONLY: the certified lookup failed for {qual} "
190
+ f"at {key}: {exc}") from exc
191
+ print(f"[autotune] certified lookup failed for {qual}: {exc}", flush=True)
192
+ applied = False
193
+ if applied:
194
+ _cert.served_evicted(qual, key) # a seed evicted at seed time, now served certified for this card
195
+ elif _cert.reseed_evicted(qual, tuned, key):
196
+ applied = True # the machine's own earlier sweep, put back for this card
197
+ if not applied:
198
+ if _confirm:
199
+ _cert.refuse_missing(qual, tuned, key) # raises KeyNotCertified: never a sweep
200
+ _cert.announce_missing(qual, tuned, key)
201
+ before = len(cache)
202
+ try:
203
+ result = original(*args, **kwargs)
204
+ except Exception as exc:
205
+ refused = every_candidate_refused(tuned, key, exc)
206
+ if refused is not None:
207
+ raise refused from exc
208
+ raise
209
+ if len(cache) > before:
210
+ if _census.active():
211
+ # THE DOOR: a shadow benches nothing, so a grown tuner cache here means a
212
+ # kernel was defined before the census installed and ran a bench on events
213
+ # that answer 0 ms — its config must never reach the machine's live replay
214
+ # cache. Refused, never captured.
215
+ raise RuntimeError(
216
+ "census shadow: the autotuner cache grew during a shadow run — a kernel "
217
+ f"defined before kernels/census.py installed ({_atc._qual_of(tuned)!r}); "
218
+ "nothing is captured. Install the census before the engine imports.")
219
+ if key is not None:
220
+ try: # the bench this call just ran: best, second-best, margin
221
+ _atc.note_timings(tuned, key, getattr(tuned, "configs_timings", None))
222
+ except Exception:
223
+ pass
224
+ if not _SWEEP_ANNOUNCED[0]:
225
+ _SWEEP_ANNOUNCED[0] = True
226
+ print(_SWEEP_NOTICE, file=sys.stderr, end="")
227
+ try:
228
+ _atc.capture()
229
+ except Exception:
230
+ pass
231
+ return result
232
+
233
+ tuned.run = run_with_notice
234
+ return tuned
235
+
236
+
237
+ def every_candidate_refused(tuned, key, exc: BaseException):
238
+ """The named refusal to raise when the sweep just run scored EVERY candidate infinite (each refused by the backend:
239
+ out of shared memory, a compile assertion, ...) and the launch of the one Triton then picked failed — or None when
240
+ the failure is something else. Triton takes `min` over all-infinite timings and launches an arbitrary config, whose
241
+ own error names that one config and hides that none runs here (the matrix-unit split flash at head dim 192 on a
242
+ V100: each tile over the 96 KB of shared memory once compiled, 2026-10-30)."""
243
+ from nbxtensor.ops.autotune_refusals import all_refused, _is_backend_refusal
244
+ from triton.runtime.errors import OutOfResources, PTXASError
245
+ from triton.compiler.errors import CompileTimeAssertionFailure
246
+ # the classes Triton's own bench scores infinite (autotuner._bench), and the Metal seam's refusals
247
+ refusal = isinstance(exc, (OutOfResources, CompileTimeAssertionFailure, PTXASError)) or _is_backend_refusal(exc)
248
+ name = getattr(getattr(tuned, "base_fn", None), "__name__", None) or getattr(tuned, "__name__", "?")
249
+ timings = getattr(tuned, "configs_timings", None)
250
+ if timings and all_refused(timings):
251
+ tried = f"{len(timings)} of {len(timings)}"
252
+ elif refusal and len(getattr(tuned, "configs", None) or []) >= 1 and not timings:
253
+ # Triton benches nothing when the prune leaves one config: it launches it, and that one was refused
254
+ tried = "the only one the prune kept"
255
+ else:
256
+ return None
257
+ return RuntimeError(f"autotune: every candidate of {name} at key {key} was refused by the backend "
258
+ f"({tried}; the launch failed with {type(exc).__name__}: {exc}) — no configuration of this "
259
+ f"kernel runs on this device at this key")
260
+
261
+
262
+ def _safe_num_stages(n: int) -> int:
263
+ """Clamp num_stages to the executing architecture's pipelining budget.
264
+
265
+ The budget is a HARDWARE CAPABILITY and is read from
266
+ `config/vendors/<vendor>/<arch>.yml` `pipelining.max_num_stages`
267
+ (R23/R24) — never decided in code, never queried from the driver in a
268
+ hot path. Volta's 2 is measured: sm_70 has no cp.async, and Triton's
269
+ pipelining above 2 stages emits async copies that fault with
270
+ CUDA_ERROR_MISALIGNED_ADDRESS in any kernel using tl.dot. Ampere and
271
+ Hopper carry a cap above the widest config space, so it never binds.
272
+ The CDNA profiles carry a conservative 2 pending first light.
273
+
274
+ Fallback path — a bare process with no hardware profile (unit tests):
275
+ the driver is the only signal available. Note that
276
+ `get_device_capability()` on ROCm returns the *gfx* version
277
+ (gfx90a -> (9, 0)), which is not an NVIDIA compute capability and must
278
+ never be compared against 8 — doing so silently cleared unvalidated
279
+ pipelining on every CDNA card. ROCm is clamped explicitly instead.
280
+ """
281
+ try:
282
+ from nbxtensor.ops.wrappers import _arch_param, get_hardware_profile
283
+ if get_hardware_profile() is not None:
284
+ cap = _arch_param("pipelining", "max_num_stages", None)
285
+ if cap:
286
+ return min(n, int(cap))
287
+ except Exception:
288
+ pass
289
+
290
+ # Ask the TRITON target, not torch. This module is imported by every
291
+ # triton kernel (rmsnorm, softmax, conv2d, matmul, baddbmm,
292
+ # depthwise_conv2d), so a module-level `import torch` here pulled torch
293
+ # into the process on the branch that exists to contain none — the widest
294
+ # torch surface under kernels/ (R33 inventory, 2026-09-03). The same
295
+ # question is answerable from the Triton runtime, which is the documented
296
+ # bootstrap mechanism already used by `matmul._detect_arch_configs`.
297
+ try:
298
+ from nbxtensor.backend.launcher import target as _nbx_target # engine data, no driver probe (R33)
299
+ arch = _nbx_target().arch
300
+ except Exception:
301
+ return n # no runtime to ask: change nothing
302
+ if isinstance(arch, int):
303
+ # NVIDIA: Triton reports compute capability x 10 (sm_70 -> 70).
304
+ return n if arch >= 80 else min(n, 2)
305
+ # AMD ("gfx90a") and any other string-arch target: conservative, since
306
+ # cp.async does not exist there and their pipelining is unvalidated.
307
+ return min(n, 2)
308
+
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # SHARED-MEMORY BUDGET — the physical limit on which autotune tiles can run
312
+ # ---------------------------------------------------------------------------
313
+ #
314
+ # Every vendor profile has declared `memory.max_shared_memory_per_block` since
315
+ # the profiles were written, and until 2026-09-03 NOTHING read it. The autotune
316
+ # config space was instead chosen by an architecture NAME, with the reasoning
317
+ # that anything unrecognised should take "the Volta subset, which fits the
318
+ # smallest budget". That reasoning is false on Apple: Volta declares 96 KB and
319
+ # an Apple GPU has 32 KB, so a BM=64/BN=128/BK=64 tile at 3 stages wants 72 KB
320
+ # and cannot run there at all. The same silence sent every CDNA card into the
321
+ # Volta space by coincidence rather than by decision, which the arch-selection
322
+ # docstring already flagged as a first-light task.
323
+ #
324
+ # The budget is a hardware parameter, so it comes from the profile (R23/R24).
325
+ # Resolution is data, not a mapping table: each profile declares a
326
+ # `compute_capability` in exactly the form the Triton target reports it
327
+ # ("7.0" / "gfx90a" / "apple9"), so the right file is found by matching, and a
328
+ # new architecture is a new YAML rather than a new branch.
329
+
330
+
331
+ def smem_bytes_for_config(config, dtype_bytes: int = 2) -> int:
332
+ """Shared memory one matmul-class tile needs, in bytes.
333
+
334
+ A blocked matmul stages an `[BLOCK_M, BLOCK_K]` slab of A and a
335
+ `[BLOCK_K, BLOCK_N]` slab of B per pipeline stage. That working set is what
336
+ saturates and spills — the measured Phase 1.5 collapse to 98-145 ms on
337
+ Volta was this quantity exceeding 96 KB.
338
+
339
+ Returns 0 for a config that names no tile (element-wise spaces), which the
340
+ filter reads as "no constraint to check".
341
+ """
342
+ kw = getattr(config, "kwargs", {}) or {}
343
+ m, n, k = kw.get("BLOCK_M"), kw.get("BLOCK_N"), kw.get("BLOCK_K")
344
+ if not (m and n and k):
345
+ return 0
346
+ stages = max(1, int(getattr(config, "num_stages", 1) or 1))
347
+ return (m * k + k * n) * dtype_bytes * stages
348
+
349
+
350
+ def _profiles_for(wanted: str):
351
+ """Match the hardware profiles against one arch string.
352
+
353
+ Lifted verbatim out of `arch_smem_budget` on 2026-09-17 so that the
354
+ profile can be resolved from an arch ALONE. It used to be reachable
355
+ only through `launcher.target()`, which also builds the selected Metal
356
+ backend's NAME — and the Metal seam reads its own declaration out of
357
+ the profile. Profile needs target, target needs backend, backend needs
358
+ profile: on a cold process the declaration therefore could not be read,
359
+ and the seam fell through to 'the single installed backend' and
360
+ INFERRED what it was supposed to be told. It agreed with the
361
+ declaration only while exactly one backend was installed.
362
+
363
+ The cycle was spurious: a profile is matched on the ARCH, and the arch
364
+ is the device's, not the backend's.
365
+ """
366
+ import yaml
367
+
368
+ # NVIDIA reports capability x 10 (sm_70 -> 70); the profiles spell it
369
+ # "7.0". AMD and Apple report the profile's string form directly.
370
+
371
+ from nbxtensor.backend import device_files # the library's device files, merged with the application's
372
+
373
+ exact, same_family = None, None
374
+ for vendor, name, path in device_files.names():
375
+ try:
376
+ cfg = device_files.merged(vendor, name) # a key written in both halves is refused, never skipped
377
+ except yaml.YAMLError: # pragma: no cover — an unparsable file describes nothing
378
+ continue
379
+ declared = str(cfg.get("compute_capability", "")).strip().lower()
380
+ if not declared:
381
+ continue
382
+ budget = (cfg.get("memory") or {}).get("max_shared_memory_per_block")
383
+ if not budget:
384
+ continue
385
+ _remember_profile(declared, wanted, cfg, path)
386
+ if declared == wanted:
387
+ exact = int(budget)
388
+ elif (declared.split(".")[0] == wanted.split(".")[0]
389
+ and "." in declared and "." in wanted):
390
+ # Same NVIDIA capability MAJOR. Only three NVIDIA profiles exist
391
+ # — 7.0, 8.0, 9.0 — so an exact match covers V100, A100 and H100
392
+ # and nothing else: a T4 (7.5), an A10 or 3090 (8.6) and an L4 or
393
+ # 4090 (8.9) would resolve to no profile and get no filtering at
394
+ # all. Falling back to the major mirrors what Prism's runtime
395
+ # detection already does (`_NVIDIA_ARCH_MAP`: "8" -> ampere), and
396
+ # the two MUST agree — a card whose runtime profile is ampere
397
+ # while its autotune space was chosen as if no profile existed is
398
+ # a disagreement inside the engine about its own hardware.
399
+ #
400
+ # That map is not imported here on purpose: `autodetect` pulls
401
+ # torch, and this module is imported by every Triton kernel (R33).
402
+ # Matching majors reads the same fact out of the profiles instead
403
+ # of copying the table.
404
+ same_family = int(budget)
405
+ elif any(wanted.startswith(prefix) for prefix in
406
+ (str(m).strip().lower()
407
+ for m in (cfg.get("compute_capability_matches") or []))):
408
+ # A profile may also declare the target strings it covers, as
409
+ # prefixes. Needed because not every backend reports a family:
410
+ # the Triton Metal target reports the Metal DEVICE NAME
411
+ # ("apple-m4-pro"), so no Apple profile could ever match on
412
+ # `compute_capability` alone and the budget resolved to None on
413
+ # every real Mac — the 32 KB filter silently not applying.
414
+ #
415
+ # It stays DATA: the prefixes live in the profile beside the
416
+ # budget they select, so a new chip family is a YAML line rather
417
+ # than a branch here. Profiles that declare none are unaffected,
418
+ # which is every NVIDIA and AMD profile — their exact and
419
+ # major-family matches are reached before this and are unchanged.
420
+ same_family = int(budget)
421
+ if exact is None and same_family is not None:
422
+ _announce_family_fallback(wanted)
423
+ return exact if exact is not None else same_family
424
+
425
+
426
+ def vendor_profile_for_arch(arch) -> dict:
427
+ """The hardware profile for an arch string, resolved WITHOUT a Triton
428
+ target — so a caller the target itself depends on can still read it.
429
+
430
+ The Metal seam is that caller: it must read `metal_backend:` out of the
431
+ profile, and building the target asks the seam which backend is in
432
+ force. Added 2026-09-17 with `_profiles_for`, which see.
433
+ """
434
+ _profiles_for(str(arch).strip().lower())
435
+ return dict(_ACTIVE_PROFILE)
436
+
437
+
438
+ def fits_shared_memory(nbytes: int) -> bool:
439
+ """Whether a block of `nbytes` a kernel stages in shared (threadgroup) memory fits the device's
440
+ `memory.max_shared_memory_per_block` (the profile in force): the prune of a config space whose widest block a
441
+ cross-warp reduction stages there. A config past it is not only slower: Metal's emitter aborts the process
442
+ ("threadgroup memory past the hardware limit", 2026-11-04), so it is pruned before any compile. A profile that
443
+ declares no budget is refused by name."""
444
+ budget = arch_smem_budget()
445
+ if budget is None:
446
+ raise RuntimeError("ZERO FALLBACK: a config space is pruned by memory.max_shared_memory_per_block, which the "
447
+ "hardware profile in force does not declare")
448
+ return int(nbytes) <= int(budget)
449
+
450
+
451
+ def arch_smem_budget() -> Optional[int]:
452
+ """`memory.max_shared_memory_per_block` for the executing hardware.
453
+
454
+ Read from the vendor YAML, never from the driver — the driver is asked
455
+ only WHICH profile applies, which is identification and not a hardware
456
+ parameter. Returns None when no profile matches, and the caller must then
457
+ leave the config space alone rather than guess a budget: filtering on an
458
+ invented number would silently delete working configs.
459
+ """
460
+ # yaml FIRST, before anything touches the launcher. Measured 2026-09-16 on
461
+ # M4 Pro: resolving the target imports the Metal backend's compiler, which is
462
+ # re-entrant with Triton's backend discovery ("Found 0 concrete subclasses of
463
+ # BaseBackend in triton_msl.backend.compiler") and leaves partially
464
+ # (that string is quoted from the 2026-09-16 measurement, on the fork that
465
+ # was archived on 2026-09-17; the ORDERING it describes is a property of
466
+ # resolving the target, not of that backend, and still holds)
467
+ # initialised modules behind; a LATER `import yaml` then dies inside the
468
+ # libyaml C extension with "partially initialized module 'yaml' has no
469
+ # attribute 'error'". That exception is not an ImportError, so it escaped the
470
+ # guard below, `arch_smem_budget` returned nothing and `_ACTIVE_PROFILE` was
471
+ # CACHED EMPTY — the Apple profile's `metal_backend`,
472
+ # `autotune_screen_max_bytes` and smem budget silently unread, and
473
+ # `selected_metal_backend()` inferring the backend instead of reading the
474
+ # declaration. Importing yaml first removes the order dependency outright.
475
+ try:
476
+ import yaml
477
+ except ImportError: # pragma: no cover
478
+ return None
479
+
480
+ try:
481
+ from nbxtensor.backend.launcher import target as _nbx_target # engine data, no driver probe (R33)
482
+ arch = _nbx_target().arch
483
+ except Exception as exc:
484
+ # A machine with no Triton target has no profile, and None says so.
485
+ # But a DELIBERATE refusal is not that, and swallowing it here is how a
486
+ # loud correct stop became a quiet wrong answer: measured 2026-09-17,
487
+ # the moment the Apple profile declared a backend this venv did not
488
+ # have, the Metal seam's refusal was caught here, the whole profile
489
+ # resolved EMPTY and was cached empty — smem budget, screen bytes and
490
+ # the declared backend all silently unread — and the seam's next
491
+ # refusal then reported "the profile declares none" about a profile
492
+ # that declares one. A circle of silence, each half explaining the
493
+ # other.
494
+ #
495
+ # Asked BY TYPE, never by message: the seam names its refusal
496
+ # (`metal_backend.BackendSelectionRefused`) precisely so this does not
497
+ # become text matching. Inert on CUDA and on any machine without a
498
+ # Metal backend — the import below fails there and the old behaviour
499
+ # stands exactly as it was.
500
+ try:
501
+ from nbxtensor.backend.metal_backend import BackendSelectionRefused
502
+ except Exception: # pragma: no cover
503
+ return None
504
+ if isinstance(exc, BackendSelectionRefused):
505
+ raise
506
+ return None
507
+
508
+ wanted = (f"{arch // 10}.{arch % 10}" if isinstance(arch, int)
509
+ else str(arch).strip().lower())
510
+ return _profiles_for(wanted)
511
+
512
+
513
+ #: (target, profile) pairs already announced, so the notice is said once per
514
+ #: process and not once per kernel compile.
515
+ _FALLBACK_ANNOUNCED: set = set()
516
+
517
+
518
+ def _announce_family_fallback(wanted: str) -> None:
519
+ """Say, out loud and once, that this chip has no profile of its own.
520
+
521
+ An unknown chip must RUN — correctly if not optimally — because refusing
522
+ would contradict a cascade that never refuses. But running on a family
523
+ profile is a different fact from running on the chip's own, and the
524
+ difference shows up as performance nobody can explain later. It is said
525
+ here rather than left to be inferred from a number.
526
+ """
527
+ import sys
528
+
529
+ profile = _ACTIVE_PROFILE.get("_profile")
530
+ key = (wanted, profile)
531
+ if profile is None or key in _FALLBACK_ANNOUNCED:
532
+ return
533
+ _FALLBACK_ANNOUNCED.add(key)
534
+ print(f"[PROFILE] {wanted!r} has no profile of its own; falling back to "
535
+ f"{profile!r}, which declares a prefix that covers it. The engine "
536
+ f"will run correctly, not optimally — a per-variant profile is what "
537
+ f"makes it optimal.", file=sys.stderr, flush=True)
538
+
539
+
540
+ #: The vendor profile the last `arch_smem_budget()` resolution matched, by
541
+ #: the same exact / same-family / declared-prefix rules. Kept beside that
542
+ #: function rather than re-resolved so the engine cannot end up reading its
543
+ #: shared-memory budget from one profile and its attention tiles from another.
544
+ _ACTIVE_PROFILE: dict = {}
545
+
546
+
547
+ def _remember_profile(declared: str, wanted: str, cfg: dict, path=None) -> None:
548
+ """Record the best profile seen so far, exact match winning. The profile's
549
+ NAME is its file's — `<vendor>/<profile>.yml` — kept as `_vendor` and
550
+ `_profile`, the only names the certified autotune directory is allowed to
551
+ use (nothing there names a backend)."""
552
+ names = {"_vendor": path.parent.name, "_profile": path.stem} if path is not None else {}
553
+ if declared == wanted:
554
+ _ACTIVE_PROFILE.clear()
555
+ _ACTIVE_PROFILE.update(cfg)
556
+ _ACTIVE_PROFILE.update(names)
557
+ _ACTIVE_PROFILE["_exact"] = True
558
+ elif not _ACTIVE_PROFILE.get("_exact"):
559
+ matches = [str(m).strip().lower()
560
+ for m in (cfg.get("compute_capability_matches") or [])]
561
+ same_major = (declared.split(".")[0] == wanted.split(".")[0]
562
+ and "." in declared and "." in wanted)
563
+ if same_major or any(wanted.startswith(m) for m in matches):
564
+ _ACTIVE_PROFILE.clear()
565
+ _ACTIVE_PROFILE.update(cfg)
566
+ _ACTIVE_PROFILE.update(names)
567
+
568
+
569
+ def active_vendor_profile() -> dict:
570
+ """The hardware profile in force, or `{}` when none matched."""
571
+ if not _ACTIVE_PROFILE:
572
+ arch_smem_budget() # resolves and records, cached upstream
573
+ return dict(_ACTIVE_PROFILE)
574
+
575
+
576
+ def sdpa_block_ceiling(seqlen_q: int, head_dim: int):
577
+ """`(block_m, block_n)` the hardware profile allows here, or `(None, None)`.
578
+
579
+ A CEILING, not an instruction. The wrapper's own cascade stays the
580
+ proposer — it carries measurements the profiles do not, notably that
581
+ BLOCK_M=64 is the floor of CORRECTNESS on Volta at head_dim<128 while
582
+ volta.yml declares 128 — and this only clamps what it proposes.
583
+
584
+ Why it exists: the cascade decides between a 128-row and a 64-row Q tile
585
+ on `_NBX_HAS_NATIVE_BF16`, which distinguishes Volta from Ampere and
586
+ means nothing off CUDA. On an Apple GPU it therefore took the Ampere
587
+ branch and asked for BLOCK_M=128 — four times what
588
+ `apple_silicon.yml` declares (32) for this shape, and more than the
589
+ Metal backend will lower at all. The profile has said 32 since the file
590
+ was written; nothing read it.
591
+
592
+ Clamping rather than replacing is what keeps CUDA byte-identical:
593
+ min(64, 128) is Volta's measured 64, min(128, 128) is Hopper's 128.
594
+ Measured 2026-09-06: no NVIDIA tile changes.
595
+
596
+ Thresholds are read in file order, first match wins, exactly as the
597
+ profiles are written (decode rows before prefill rows).
598
+ """
599
+ profile = active_vendor_profile()
600
+ for row in profile.get("sdpa_thresholds") or []:
601
+ if "seqlen_q_le" in row and seqlen_q > row["seqlen_q_le"]:
602
+ continue
603
+ if "head_dim_ge" in row and head_dim < row["head_dim_ge"]:
604
+ continue
605
+ if "head_dim_lt" in row and head_dim >= row["head_dim_lt"]:
606
+ continue
607
+ return row.get("block_m"), row.get("block_n")
608
+ return None, None
609
+
610
+
611
+ def sdpa_launch_meta(seqlen_q: int, head_dim: int) -> dict:
612
+ """The flash launch's `num_warps`, and its `qk_chunk` where the row states one (the row of an arch whose
613
+ dot lowers to scalar FMA), from the hardware profile row matching this shape; `{}` when no row matches.
614
+ Same row order and match rule as `sdpa_block_ceiling`. A matching row that states no `num_warps` is an
615
+ error naming it: the launch never takes Triton's hidden default (supervisor 2026-10-08 00:05).
616
+
617
+ Until 2026-10-08 `num_warps` was read only beside `qk_chunk`: every other row's stated warps were
618
+ ignored, and the M4 Pro's head_dim-128 attention ran at 4 warps where 8 is 41-43 % faster."""
619
+ profile = active_vendor_profile()
620
+ for i, row in enumerate(profile.get("sdpa_thresholds") or []):
621
+ if "seqlen_q_le" in row and seqlen_q > row["seqlen_q_le"]:
622
+ continue
623
+ if "head_dim_ge" in row and head_dim < row["head_dim_ge"]:
624
+ continue
625
+ if "head_dim_lt" in row and head_dim >= row["head_dim_lt"]:
626
+ continue
627
+ if "num_warps" not in row:
628
+ raise ValueError(f"sdpa_thresholds row {i} {row} matches seqlen_q={seqlen_q} head_dim={head_dim} "
629
+ f"but states no num_warps; every row states the warps its flash launch runs at")
630
+ return {k: int(row[k]) for k in ("num_warps", "qk_chunk") if k in row}
631
+ return {}
632
+
633
+
634
+ def matrix_unit(vendor_profile: Optional[dict] = None) -> dict:
635
+ """The matrix unit the hardware profile declares for kernels Triton cannot lower onto it (`matrix_unit`:
636
+ shape, operand and accumulator dtypes, measured tiles); `{}` when the profile declares none. A declared
637
+ unit this engine has no kernel for, dtypes its instruction does not compute in, or a flash row whose tile
638
+ the kernel cannot lay out is refused by name: a declared capability is never silently left unused.
639
+ `vendor_profile`: the profile to read (Prism, planning for a device that is not this process's); the
640
+ profile in force when None (the wrappers)."""
641
+ profile = active_vendor_profile() if vendor_profile is None else vendor_profile
642
+ mu = dict(profile.get("matrix_unit") or {})
643
+ if not mu:
644
+ return mu
645
+ from nbxtensor.ops.mma_m8n8k4 import UNIT
646
+ if mu.get("shape") != UNIT["shape"]:
647
+ raise ValueError(f"hardware profile: matrix_unit.shape {mu.get('shape')!r} has no kernel "
648
+ f"(this engine builds {UNIT['shape']!r})")
649
+ for f in ("operand_dtype", "accumulator_dtype"):
650
+ if mu.get(f) != UNIT[f]:
651
+ raise ValueError(f"hardware profile: matrix_unit.{f} {mu.get(f)!r} is not what "
652
+ f"{UNIT['shape']} computes in ({UNIT[f]!r})")
653
+ for row in mu.get("mm") or []:
654
+ for f in ("block_m", "block_n"):
655
+ t = int(row[f])
656
+ if t < 16 or t % 16 or (t // 16) & (t // 16 - 1):
657
+ raise ValueError(f"hardware profile: matrix_unit.mm {f} {t} is not 16 x a power of two")
658
+ split = mu.get("fp32_split") or {}
659
+ smem = (profile.get("memory") or {}).get("max_shared_memory_per_block") # the read profile's own budget
660
+ from nbxtensor.ops.flash_attention_m8n8k4 import head_parts, staged_shared_bytes
661
+ for sec, rows in (("flash", mu.get("flash") or []), ("fp32_split.flash", split.get("flash") or [])):
662
+ for row in rows:
663
+ for f in ("block_m", "block_n"):
664
+ t = int(row[f])
665
+ if t < 16 or t % 16 or (t // 16) & (t // 16 - 1):
666
+ raise ValueError(f"hardware profile: matrix_unit.{sec} {f} {t} is not 16 x a power of two")
667
+ wm = int(row["warps_m"])
668
+ if wm < 1 or wm & (wm - 1) or (int(row["block_m"]) // 16) % wm:
669
+ raise ValueError(f"hardware profile: matrix_unit.{sec} warps_m {wm} does not divide the "
670
+ f"{row['block_m']}-row tile's 16-row bands")
671
+ if "head_dim_le" not in row:
672
+ raise ValueError(f"hardware profile: matrix_unit.{sec} row {row!r} names no head_dim_le — the head "
673
+ "dim its shared memory is priced at")
674
+ if smem is not None:
675
+ # THE DOOR: the tiles the kernel stages by name (ops/flash_attention_m8n8k4.staged_shared_bytes) at
676
+ # the row's widest head dim, against the profile's budget. A wider row used to be refused by the
677
+ # compiler at the first launch that reached it — a census of "not this time"; here it never loads.
678
+ need = staged_shared_bytes(int(row["block_m"]), int(row["block_n"]),
679
+ *head_parts(int(row["head_dim_le"])), split=(sec != "flash"))
680
+ if need > int(smem):
681
+ raise ValueError(f"hardware profile: matrix_unit.{sec} row head_dim_le {row['head_dim_le']} "
682
+ f"{row['block_m']}x{row['block_n']} stages {need} bytes of shared memory, over "
683
+ f"memory.max_shared_memory_per_block {smem}")
684
+ for row in split.get("flash") or []:
685
+ # the head-dim columns the unit sums (rounding toward zero) before each round-to-nearest add of S
686
+ if "block_k" not in row:
687
+ raise ValueError(f"hardware profile: matrix_unit.fp32_split.flash row head_dim_le {row.get('head_dim_le')} "
688
+ "names no block_k (the head-dim columns the unit sums before each round-to-nearest add)")
689
+ bk = int(row["block_k"])
690
+ if bk < UNIT["k"] or bk & (bk - 1):
691
+ raise ValueError(f"hardware profile: matrix_unit.fp32_split.flash block_k {bk} is not a power of two "
692
+ f">= {UNIT['k']} (the unit's k)")
693
+ layout = (profile.get("conv") or {}).get("weight_layout")
694
+ if layout not in (None, "KCRS"):
695
+ # ops/conv2d_m8n8k4.py reads a filter as one contiguous (C, R, S) run (w.stride(0) alone): a weight the
696
+ # loader relaid channels-innermost would be read in the wrong order. Refused here, where the two
697
+ # declarations meet, until that kernel takes the weight's four strides.
698
+ raise ValueError(f"hardware profile: matrix_unit with conv.weight_layout {layout!r} — the {UNIT['shape']} "
699
+ f"convolution reads KCRS weights only")
700
+ if split and (not split.get("mm") or not split.get("operands")):
701
+ raise ValueError("hardware profile: matrix_unit.fp32_split declares no mm tiles or no operands")
702
+ for row in split.get("mm") or []:
703
+ for f in ("block_m", "block_n"):
704
+ t = int(row[f])
705
+ if t < 16 or t % 16 or (t // 16) & (t // 16 - 1):
706
+ raise ValueError(f"hardware profile: matrix_unit.fp32_split.mm {f} {t} is not 16 x a power of two")
707
+ for row in (mu.get("mm") or []) + (split.get("mm") or []):
708
+ bk, wm, wn = int(row["block_k"]), int(row["warps_m"]), int(row["warps_n"])
709
+ if bk < 8 or bk & (bk - 1):
710
+ raise ValueError(f"hardware profile: matrix_unit.mm block_k {bk} is not a power of two >= 8")
711
+ if (int(row["block_m"]) // 16) % wm or (int(row["block_n"]) // 16) % wn:
712
+ raise ValueError(f"hardware profile: matrix_unit.mm warps {wm}x{wn} do not divide the "
713
+ f"{row['block_m']}x{row['block_n']} tile's 16-blocks")
714
+ int(row["group_m"])
715
+ return mu
716
+
717
+
718
+ def matrix_unit_flash_tile(head_dim: int, unit: Optional[dict] = None) -> Optional[dict]:
719
+ """The first `flash` row of `unit` (the profile's `matrix_unit`, or its `fp32_split`) holding `head_dim`
720
+ (every row names its `head_dim_le`: `matrix_unit()` refuses one that does not), or None (no unit declared, or a
721
+ head dim beyond its measured rows: the kernel keeps tl.dot). `block_k` is the split rows' field alone."""
722
+ for row in (matrix_unit() if unit is None else unit).get("flash") or []:
723
+ if head_dim > row["head_dim_le"]:
724
+ continue
725
+ return {f: int(row[f]) for f in ("block_m", "block_n", "warps_m", "block_k") if f in row}
726
+ return None
727
+
728
+
729
+ def matrix_unit_mm_tile(M: int, N: int, unit: Optional[dict] = None) -> Optional[dict]:
730
+ """The first `matrix_unit.mm` row whose tile fits inside an M x N output (rows are listed largest
731
+ first), else the last row; None when the profile declares no mm rows (the GEMM keeps tl.dot)."""
732
+ rows = (matrix_unit() if unit is None else unit).get("mm") or []
733
+ for row in rows:
734
+ if int(row["block_m"]) <= M and int(row["block_n"]) <= N:
735
+ return {k: int(v) for k, v in row.items()}
736
+ return {k: int(v) for k, v in rows[-1].items()} if rows else None
737
+
738
+
739
+ def largest_tile_within_smem(candidates, cost, budget: Optional[int]):
740
+ """The first candidate the hardware can hold, or the smallest if none fits.
741
+
742
+ `candidates` are ordered largest-first, as the caller wants them. `cost` is
743
+ a callable returning the shared memory a candidate needs, in bytes; it is
744
+ INJECTED rather than computed here because an analytic formula for a Triton
745
+ kernel's shared memory is a guess, and the note on
746
+ `configs_within_smem_budget` applies with the same force: pruning on a
747
+ guessed budget silently deletes configurations that work.
748
+
749
+ `budget=None` means the hardware did not say. The caller's proposal is then
750
+ returned untouched — the same rule, for the same reason.
751
+
752
+ Why this exists. A user running 0.5.3 on two A40s (sm_86) could not start a
753
+ single language model on the Triton branch: the flash kernel asked for
754
+ 164 352 bytes of shared memory per block against a limit of 101 376, the
755
+ same number in both Triton modes and unmoved by sequence length. The tile
756
+ was chosen at `wrappers.py` on `_NBX_HAS_NATIVE_BF16`, a flag that
757
+ separates Volta from Ampere and nothing else, so sm_86 took the branch
758
+ written for sm_80 — 163 KB of shared memory against its own 99. The engine
759
+ had already written `compute_capability: '8.6'` into the profile it
760
+ generated by itself for that card. The information was present and unused.
761
+
762
+ Nothing is ever refused into an empty selection: when no candidate fits,
763
+ the smallest is returned. A tile that spills is measurable and reports
764
+ itself; a selection with nothing in it is a crash at launch.
765
+ """
766
+ ordered = list(candidates)
767
+ if not ordered:
768
+ raise ValueError("largest_tile_within_smem: no candidate offered")
769
+ if budget is None:
770
+ return ordered[0]
771
+ for cand in ordered:
772
+ if cost(cand) <= budget:
773
+ return cand
774
+ return min(ordered, key=cost)
775
+
776
+
777
+ def configs_within_smem_budget(configs, budget: Optional[int],
778
+ dtype_bytes: int = 2):
779
+ """Drop the tiles that cannot physically fit in `budget` bytes.
780
+
781
+ `budget=None` (no profile matched) returns the space untouched: removing
782
+ configs on a guessed budget is worse than exploring a few that spill,
783
+ because the autotuner measures spilling configs and rejects them, whereas
784
+ a config that was never offered can never be chosen.
785
+
786
+ The smallest tile is always kept even if it exceeds the budget, so the
787
+ space is never emptied — an empty autotune list is an import-time crash,
788
+ and a wrong-but-present config is a slow kernel.
789
+ """
790
+ if not budget:
791
+ return configs
792
+ fitting = [c for c in configs
793
+ if smem_bytes_for_config(c, dtype_bytes) <= budget]
794
+ if fitting:
795
+ return fitting
796
+ return [min(configs, key=lambda c: smem_bytes_for_config(c, dtype_bytes))]
797
+
798
+
799
+ def element_wise_configs() -> List[triton.Config]:
800
+ """Autotune configs for element-wise (1D) kernels."""
801
+ return [
802
+ triton.Config({'BLOCK_SIZE': 64}, num_warps=2),
803
+ triton.Config({'BLOCK_SIZE': 128}, num_warps=2),
804
+ triton.Config({'BLOCK_SIZE': 256}, num_warps=4),
805
+ triton.Config({'BLOCK_SIZE': 512}, num_warps=4),
806
+ triton.Config({'BLOCK_SIZE': 1024}, num_warps=4),
807
+ ]
808
+
809
+
810
+ def warps_configs() -> List[triton.Config]:
811
+ """Autotune configs sweeping warp counts (for row-wise kernels)."""
812
+ return [triton.Config({}, num_warps=2**i) for i in range(6)]
813
+
814
+
815
+ #: The widest feature tile one program of a row reduction holds at once. A reduction that sized its
816
+ #: tile to the whole row (`next_power_of_2(feat_dim)`) kept every element in registers: at 4 warps a
817
+ #: 224 640-element row is 2 048 fp32 values per thread, which spill to local memory — and the driver
818
+ #: reserves local memory for EVERY thread the card can hold, outside any allocator: +2 062 MB on a
819
+ #: V100 for one such sum (op-level probe, 2026-10-03), +4.3 GB at Wan2.1-I2V's CFG batch, the memory
820
+ #: its 480x832 run lacked at SDPA::0. 4 096 elements is 32 fp32 values per thread at 4 warps — inside
821
+ #: the register file on every NVIDIA and AMD part — and the cap `batch_norm`'s spatial tile already
822
+ #: uses. A row wider than one tile is walked in tiles; a row that fits one tile is reduced exactly as
823
+ #: before (one tile, no added term), so its bytes do not move.
824
+ REDUCTION_FEAT_TILE = 4096
825
+
826
+
827
+ def reduction_feat_tile(args: Dict) -> int:
828
+ """The feature tile of a looped row reduction: the row, rounded up, at most one tile."""
829
+ return min(next_power_of_2(args['feat_dim']), REDUCTION_FEAT_TILE)
830
+
831
+
832
+ def row_program_configs() -> List[triton.Config]:
833
+ """The candidates the certifier proves for a row kernel holding its whole padded row (softmax, layer norm, RMS
834
+ norm — certified per key since 2026-10-19 instead of a hand table): rows per program x warps. The configuration such a kernel ran before
835
+ it was certified is among them (`batch_block_heuristic`: 1 row for a row of 64 or more, up to 128 rows for a short
836
+ one; 4 warps) — a certified winner equal to it changes no bit. Rows per program partition independent rows and
837
+ change no bit; the warps split a row's reduction and can."""
838
+ return [triton.Config({'BLOCK_SIZE_BATCH': rows}, num_warps=warps)
839
+ for rows in (1, 2, 4, 8, 16, 32, 64, 128) for warps in (1, 2, 4, 8)]
840
+
841
+
842
+ def fits_thread_registers(elements: int, num_warps: int) -> bool:
843
+ """Whether a program holding `elements` (rows x padded row) over `num_warps` warps keeps at most the profile's
844
+ `memory.max_registers_per_thread` elements per thread (an element a register, as the block bound counts it). A
845
+ profile that declares no per-thread bound (Apple: its lowering's own bound is `reduced_extent_max`) bounds
846
+ nothing here. Measured 2026-10-26: one warp over a 16 384-wide fp16 row (512 elements a thread) compiled for
847
+ over 30 minutes in LLVM and stalled the certification of the fp16 contract's softmax keys."""
848
+ from nbxtensor.ops.wrappers import _arch_param
849
+ per = _arch_param("memory", "max_registers_per_thread", None)
850
+ if per is None:
851
+ return True
852
+ warp = _arch_param("memory", "warp_size", None)
853
+ if warp is None:
854
+ raise RuntimeError("ZERO FALLBACK: the profile declares memory.max_registers_per_thread but no memory.warp_size")
855
+ return int(elements) <= int(per) * int(warp) * int(num_warps)
856
+
857
+
858
+ def prune_row_programs(configs, named_args, **kwargs):
859
+ """A program holds rows x the padded row: one row whatever its length, more only while the tile stays within the
860
+ backend's reduce-tile cap (`nbx_tensor.reduce_tile_max`, where the lowering refuses beyond) AND within the
861
+ registers of one block (`memory.max_registers_per_block` of the profile: an fp32 element a register) — a tile past
862
+ them spills and its compile alone ran tens of minutes per config (the first norm certification, 2026-10-22: no key
863
+ done in 94 min on two cards); never more rows than exist."""
864
+ from nbxtensor.backend.capabilities import reduce_tile_max
865
+ from nbxtensor.ops.wrappers import _arch_param
866
+ regs = _arch_param("memory", "max_registers_per_block", None)
867
+ if regs is None:
868
+ raise RuntimeError("ZERO FALLBACK: the hardware profile declares no memory.max_registers_per_block — the row "
869
+ "programs' tile has no bound")
870
+ feat = triton.next_power_of_2(int(named_args['feat_dim']))
871
+ rows = int(named_args['batch_dim'])
872
+ cap = min(reduce_tile_max(), int(regs))
873
+ keep = [c for c in configs
874
+ if c.kwargs['BLOCK_SIZE_BATCH'] * feat <= max(cap, feat)
875
+ and c.kwargs['BLOCK_SIZE_BATCH'] <= max(1, rows)
876
+ and fits_thread_registers(c.kwargs['BLOCK_SIZE_BATCH'] * feat, c.num_warps)]
877
+ return keep or [max(configs, key=lambda c: c.num_warps)]
878
+
879
+
880
+ def gemv_vec_configs() -> List[triton.Config]:
881
+ """The candidates the certifier proves for the SIMT GEMV (`gemv_vec_kernel`, the decode projection of every
882
+ language model — workstream 1, small-M GEMM): output rows per program x K chunk x warps x stages. The tiles it
883
+ ran before it was certified are among them (8 x 256, 4 warps, 1 stage; Apple's 8 x 128). Every config reduces all
884
+ of K inside one program in a fixed chunk order — no split-K, no atomics: the chunk width moves the summation
885
+ order and so can move bits, rows per program cannot."""
886
+ return [triton.Config({'BLOCK_N': bn, 'BLOCK_K': bk}, num_warps=w, num_stages=st)
887
+ for bn in (4, 8, 16, 32) for bk in (128, 256, 512, 1024) for w in (2, 4, 8) for st in (1, 2)]
888
+
889
+
890
+ def prune_gemv_vec(configs, named_args, **kwargs):
891
+ """A program's tile is BLOCK_N x BLOCK_K: at most the backend's reduce-tile cap (`nbx_tensor.reduce_tile_max`,
892
+ where its lowering refuses beyond — Metal's 1024-thread threadgroup) and, on a backend that stages the reduce one
893
+ element per thread, a K chunk within the program's threads (`nbx_tensor.reduced_extent_max`); never a chunk wider
894
+ than K's next power of two nor more rows than N's but the smallest candidate."""
895
+ from nbxtensor.backend.capabilities import reduce_tile_max, reduced_extent_max
896
+ cap = reduce_tile_max()
897
+ k = triton.next_power_of_2(int(named_args['K']))
898
+ n = triton.next_power_of_2(int(named_args['N']))
899
+ bk_min = min(c.kwargs['BLOCK_K'] for c in configs)
900
+ bn_min = min(c.kwargs['BLOCK_N'] for c in configs)
901
+ keep = [c for c in configs
902
+ if c.kwargs['BLOCK_N'] * c.kwargs['BLOCK_K'] <= cap
903
+ and c.kwargs['BLOCK_K'] <= max(k, bk_min) and c.kwargs['BLOCK_N'] <= max(n, bn_min)]
904
+ keep = [c for c in keep if reduced_extent_max(c.num_warps) is None
905
+ or c.kwargs['BLOCK_K'] <= reduced_extent_max(c.num_warps)]
906
+ if not keep:
907
+ raise ValueError(f"ZERO FALLBACK: no GEMV tile fits the backend's reduce tile of {cap} (N={named_args['N']}, "
908
+ f"K={named_args['K']})")
909
+ return keep
910
+
911
+
912
+ def addmv_configs() -> List[triton.Config]:
913
+ """The candidates the certifier proves for `addmv_kernel` (the decode projection WITH a bias, `addmm` at M <= 4):
914
+ output rows x reduction tile x warps. The kernel keeps a BLOCK_N x BLOCK_M fp32 accumulator and reduces it after
915
+ the loop. The tiles it ran before (64 x 256, 4 warps; Apple's 8 x 128 at 32 warps) are among them."""
916
+ return [triton.Config({'BLOCK_N': bn, 'BLOCK_M': bm}, num_warps=w)
917
+ for bn in (8, 16, 32, 64) for bm in (128, 256, 512) for w in (4, 8, 32)]
918
+
919
+
920
+ def prune_addmv(configs, named_args, **kwargs):
921
+ """The accumulator tile BLOCK_N x BLOCK_M within the backend's reduce-tile cap (`reduce_tile_max`), never a tile
922
+ wider than M's next power of two nor more rows than N's but the smallest candidate; on a backend that stages the
923
+ reduce one element per thread, a reduction tile within the program's threads (`nbx_tensor.reduced_extent_max`;
924
+ Metal, measured 2026-09-07: 64 x 256 against a 128-thread group refused)."""
925
+ from nbxtensor.backend.capabilities import reduce_tile_max, reduced_extent_max
926
+ cap = reduce_tile_max()
927
+ m = triton.next_power_of_2(int(named_args['M']))
928
+ n = triton.next_power_of_2(int(named_args['N']))
929
+ bm_min = min(c.kwargs['BLOCK_M'] for c in configs)
930
+ bn_min = min(c.kwargs['BLOCK_N'] for c in configs)
931
+ keep = [c for c in configs
932
+ if c.kwargs['BLOCK_N'] * c.kwargs['BLOCK_M'] <= cap
933
+ and c.kwargs['BLOCK_M'] <= max(m, bm_min) and c.kwargs['BLOCK_N'] <= max(n, bn_min)]
934
+ keep = [c for c in keep if reduced_extent_max(c.num_warps) is None
935
+ or c.kwargs['BLOCK_M'] <= reduced_extent_max(c.num_warps)]
936
+ if not keep:
937
+ raise ValueError(f"ZERO FALLBACK: no addmv tile fits the backend's reduce tile of {cap} (N={named_args['N']}, "
938
+ f"M={named_args['M']})")
939
+ return keep
940
+
941
+
942
+ def row_reduce_configs() -> List[triton.Config]:
943
+ """The candidates the certifier proves for a looped row reduction (sum, mean, amax — chantier 1): rows per program x
944
+ the feature tile the row is walked by x warps. What such a kernel ran before it was certified is among them
945
+ (`batch_block_heuristic` rows, `reduction_feat_tile` tile, 4 warps)."""
946
+ return [triton.Config({'BLOCK_SIZE_BATCH': rows, 'BLOCK_SIZE_FEAT': tile}, num_warps=warps)
947
+ for rows in (1, 2, 4, 8, 16, 32, 64, 128) for tile in (64, 256, 1024, 4096) for warps in (2, 4, 8)]
948
+
949
+
950
+ def prune_row_reduce(configs, named_args, **kwargs):
951
+ """No more rows than exist, no tile wider than the padded row (it only masks), and a rows x tile block within the
952
+ backend's reduce-tile cap (`nbx_tensor.reduce_tile_max`), the registers of one block (`memory.max_registers_per_
953
+ block`: an fp32 element a register — a 128 x 4 096 block spilled and ptxas took minutes per candidate, the c1rest
954
+ certification, 2026-10-23 17:57) and the per-thread bound (`reduced_extent_max`)."""
955
+ from nbxtensor.backend.capabilities import reduce_tile_max, reduced_extent_max
956
+ from nbxtensor.ops.wrappers import _arch_param
957
+ regs = _arch_param("memory", "max_registers_per_block", None)
958
+ if regs is None:
959
+ raise RuntimeError("ZERO FALLBACK: the hardware profile declares no memory.max_registers_per_block — the row "
960
+ "reductions' block has no bound")
961
+ rows, feat = int(named_args['batch_dim']), next_power_of_2(max(1, int(named_args['feat_dim'])))
962
+ cap = min(reduce_tile_max(), int(regs))
963
+ keep = []
964
+ for c in configs:
965
+ r, t = c.kwargs['BLOCK_SIZE_BATCH'], c.kwargs['BLOCK_SIZE_FEAT']
966
+ per = reduced_extent_max(c.num_warps)
967
+ if r <= max(1, rows) and t <= max(64, feat) and r * t <= cap and (per is None or t <= per):
968
+ keep.append(c)
969
+ return keep or [min(configs, key=lambda c: c.kwargs['BLOCK_SIZE_BATCH'] * c.kwargs['BLOCK_SIZE_FEAT'])]
970
+
971
+
972
+ def batch_block_heuristic(args: Dict) -> int:
973
+ """Heuristic for batch block size in softmax/norm kernels.
974
+
975
+ For small feature dims (< 64) and large batch, processes multiple rows
976
+ per program for efficiency.
977
+ """
978
+ return (min(max(1, next_power_of_2(args['batch_dim'] // 2 ** 10)), 128)
979
+ if args['feat_dim'] < 64 else 1)
980
+
981
+
982
+ def reduction_configs() -> List[triton.Config]:
983
+ """Autotune configs for row-wise reduction kernels (BLOCK_M x BLOCK_N)."""
984
+ return [
985
+ triton.Config({'BLOCK_M': 8, 'BLOCK_N': 1024}, num_warps=8),
986
+ triton.Config({'BLOCK_M': 8, 'BLOCK_N': 2048}, num_warps=8),
987
+ triton.Config({'BLOCK_M': 16, 'BLOCK_N': 512}, num_warps=4),
988
+ triton.Config({'BLOCK_M': 16, 'BLOCK_N': 1024}, num_warps=8),
989
+ triton.Config({'BLOCK_M': 32, 'BLOCK_N': 256}, num_warps=4),
990
+ triton.Config({'BLOCK_M': 32, 'BLOCK_N': 512}, num_warps=4),
991
+ ]
992
+
993
+
994
+ def matmul_configs() -> List[triton.Config]:
995
+ """Autotune configs for matmul kernels (CUDA).
996
+
997
+ num_stages clamped to 2 on V100 via _safe_num_stages().
998
+ """
999
+ return [
1000
+ triton.Config({'BLOCK_M': 128, 'BLOCK_N': 256, 'BLOCK_K': 64, 'GROUP_M': 8},
1001
+ num_stages=_safe_num_stages(3), num_warps=8),
1002
+ triton.Config({'BLOCK_M': 64, 'BLOCK_N': 256, 'BLOCK_K': 32, 'GROUP_M': 8},
1003
+ num_stages=_safe_num_stages(4), num_warps=4),
1004
+ triton.Config({'BLOCK_M': 128, 'BLOCK_N': 128, 'BLOCK_K': 32, 'GROUP_M': 8},
1005
+ num_stages=_safe_num_stages(4), num_warps=4),
1006
+ triton.Config({'BLOCK_M': 128, 'BLOCK_N': 64, 'BLOCK_K': 32, 'GROUP_M': 8},
1007
+ num_stages=_safe_num_stages(4), num_warps=4),
1008
+ triton.Config({'BLOCK_M': 64, 'BLOCK_N': 128, 'BLOCK_K': 32, 'GROUP_M': 8},
1009
+ num_stages=_safe_num_stages(4), num_warps=4),
1010
+ triton.Config({'BLOCK_M': 128, 'BLOCK_N': 32, 'BLOCK_K': 32, 'GROUP_M': 8},
1011
+ num_stages=_safe_num_stages(4), num_warps=4),
1012
+ triton.Config({'BLOCK_M': 64, 'BLOCK_N': 32, 'BLOCK_K': 32, 'GROUP_M': 8},
1013
+ num_stages=_safe_num_stages(5), num_warps=2),
1014
+ triton.Config({'BLOCK_M': 32, 'BLOCK_N': 64, 'BLOCK_K': 32, 'GROUP_M': 8},
1015
+ num_stages=_safe_num_stages(5), num_warps=2),
1016
+ ]
1017
+
1018
+
1019
+ # --- R33: identify the backend before any kernel module asks Triton --------
1020
+ #
1021
+ # `matmul.py` calls `triton.runtime.driver.active.get_current_target()` at
1022
+ # MODULE IMPORT to choose its autotune space. That question makes Triton probe
1023
+ # every registered backend, and upstream's AMD probe runs `import torch`
1024
+ # inside its own `is_active()` — so importing a kernel module pulled torch
1025
+ # into the process, on a CUDA box as much as on a Mac.
1026
+ #
1027
+ # `gpu_backend()` answers the same question with three torch-free
1028
+ # probes and records the result in `TRITON_DEFAULT_BACKEND`, after which
1029
+ # Triton resolves straight to that backend and probes nothing else. Doing it
1030
+ # here, in the module every kernel imports before it asks anything, is what
1031
+ # makes the ordering reliable rather than accidental.
1032
+ #
1033
+ # Wrapped: on a machine with no GPU at all this raises, and a kernel module
1034
+ # must still be importable — the refusal belongs at the call site, not at
1035
+ # import.
1036
+ try: # pragma: no cover - env-dependent
1037
+ from nbxtensor.backend.runtime import gpu_backend as _r33_pin_backend
1038
+
1039
+ _r33_pin_backend()
1040
+ except Exception:
1041
+ pass