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,380 @@
1
+ """A backend refusal for one candidate config costs that config, not the run.
2
+
3
+ Triton's autotuner already has this semantics: `_bench` catches
4
+ `OutOfResources`, `CompileTimeAssertionFailure` and `PTXASError`, scores that
5
+ config `inf`, and carries on. A backend refusal says the same thing -- this
6
+ config cannot be compiled here -- but a backend's refusal type descends from
7
+ `RuntimeError` rather than `TritonError`, so nothing catches it and it ends
8
+ the run.
9
+
10
+ This module names NO backend and NO vendor: it asks the Metal seam
11
+ (`triton.metal_backend`) whether an exception is a backend refusal. The seam is
12
+ the only place an implementation is named, because this file is shared -- the
13
+ CUDA rack runs it too, where the answer is simply False.
14
+
15
+ Measured 2026-09-12: hat-s-x4 and real-esrgan-x2 both died inside the sweep on
16
+
17
+ Refusing a tt.dot with tile dim 128 (> 64) on the generic per-thread path
18
+
19
+ while `conv2d_forward_kernel` declares eighteen configs of which eleven have
20
+ every dimension at 64 or below. Eleven servable choices, and the run died on
21
+ the first of the seven that refuse.
22
+
23
+ The refusal itself is right and stays: that path is validated only to 64 and
24
+ says so. What this changes is where it lands.
25
+
26
+ Two rules, and the second is what keeps an exclusion from becoming a silent
27
+ narrowing:
28
+
29
+ * only a REFUSAL is excluded. An out-of-memory, a kernel bug, a driver
30
+ fault must end the run exactly as before -- scoring those `inf` would turn
31
+ every failure into a shape that is merely slower.
32
+ * the exclusion is SAID, once per reason. A tuner that quietly drops configs
33
+ leaves a shape slow for a cause nobody can find.
34
+ """
35
+ from __future__ import annotations
36
+
37
+ from typing import Callable, Dict, Iterable, List
38
+
39
+ #: Reasons already announced this process, so a sweep of eighteen configs
40
+ #: prints one line rather than eighteen identical ones.
41
+ _ANNOUNCED: set = set()
42
+
43
+ _INF: List[float] = [float("inf"), float("inf"), float("inf")]
44
+
45
+
46
+ def reset_announcements() -> None:
47
+ """Forget what has been said. For tests, which must not depend on order.
48
+
49
+ The "once per process" above is deliberate product behaviour and stays;
50
+ without a reset it also makes the behaviour order-dependent, so the second
51
+ test to use a reason sees nothing and cannot tell that from a bug.
52
+ """
53
+ _ANNOUNCED.clear()
54
+
55
+
56
+ def _is_backend_refusal(exc: BaseException) -> bool:
57
+ """Is this the backend saying it cannot compile this config correctly?
58
+
59
+ Asked by class, not by message: matching on the text would make every
60
+ reworded refusal a run-ending error again, silently.
61
+
62
+ This SHARED module names NO backend vendor: it asks the Metal seam
63
+ (`triton.metal_backend.is_backend_refusal`), which knows the selectable
64
+ backends (the bledden fork, triton-ext). On a machine with no Metal backend
65
+ — the Dell on CUDA — the seam returns False, exactly as this did when it
66
+ imported one vendor by name. One seam decides which backend runs; the engine
67
+ does not branch to a vendor, here or anywhere else.
68
+ """
69
+ try:
70
+ from nbxtensor.backend.metal_backend import is_backend_refusal
71
+ except Exception: # the seam is not importable here
72
+ return False
73
+ return is_backend_refusal(exc)
74
+
75
+
76
+ def exclude_refused_configs(bench: Callable, say: Callable[[str], None] | None = None):
77
+ """Wrap a per-config benchmark so a refusal scores `inf` instead of raising."""
78
+ def _say(line: str) -> None:
79
+ if say is not None:
80
+ say(line)
81
+ else:
82
+ print(line, flush=True)
83
+
84
+ def _run(*args, **kwargs):
85
+ try:
86
+ return bench(*args, **kwargs)
87
+ except BaseException as exc:
88
+ if not _is_backend_refusal(exc):
89
+ raise
90
+ reason = str(exc).strip().splitlines()[0][:160]
91
+ if reason not in _ANNOUNCED:
92
+ _ANNOUNCED.add(reason)
93
+ _say(f"[AUTOTUNE_REFUSED] a candidate config is not servable on "
94
+ f"this backend and was excluded from the sweep: {reason}")
95
+ return list(_INF)
96
+ return _run
97
+
98
+
99
+ def all_refused(timings: Dict) -> bool:
100
+ """Did EVERY config score infinite?
101
+
102
+ `min` over all-inf returns an arbitrary config, which then refuses at
103
+ launch with a message about that one config -- hiding that every config
104
+ refused. A caller that can ask this can say the true thing instead.
105
+ """
106
+ values: Iterable = timings.values() if hasattr(timings, "values") else timings
107
+ values = list(values)
108
+ if not values:
109
+ return False
110
+ return all(all(x == float("inf") for x in (v if isinstance(v, (list, tuple)) else [v]))
111
+ for v in values)
112
+
113
+
114
+ # ── the per-candidate time budget ──────────────────────────────────────────
115
+ #
116
+ # A third species of candidate, measured 2026-09-12 on a bf16 matmul sweep at
117
+ # M=1500: neither refused nor wrong, but PATHOLOGICALLY SLOW -- one launch of
118
+ # 2.5 minutes, which the five-launch estimate multiplied past twelve. Neither
119
+ # the refusal exclusion above nor the oracle sees it; only a campaign-level
120
+ # timeout reaped it, killing the whole run without naming the candidate.
121
+ #
122
+ # The budget is DERIVED, never a constant:
123
+ # * first candidate of a sweep: R0 x the fp64 oracle's own CPU time for the
124
+ # key it just computed (the one number measured before any candidate) --
125
+ # a GPU configuration slower than the CPU float64 of the same mathematics
126
+ # is not a configuration;
127
+ # * later candidates: R x the fastest single-launch time completed so far
128
+ # in the same sweep (the winner sets the scale).
129
+ #
130
+ # The ratios themselves come from measurement, and until they are measured
131
+ # they are None and the watchdog is DISARMED -- it must never guess. When the
132
+ # measurement lands, its numbers are written beside the values.
133
+ _BUDGET_RATIOS = {"first_vs_oracle_cpu": None, "later_vs_best": None}
134
+
135
+ #: The most recent oracle CPU time, in ms. The screen computes the oracle for
136
+ #: a key immediately before Triton benches that key's candidates, and sweeps
137
+ #: are serialised in-process, so "most recent" IS this sweep's key. That
138
+ #: serialisation is an assumption this module states rather than hides; a
139
+ #: parallel sweep would need the key joined explicitly.
140
+ _SWEEP: dict = {"best_ms": None, "budget_ms": None, "token": None}
141
+
142
+
143
+ def _key_for_cache(at, args, kwargs):
144
+ """The key exactly as `Autotuner.run` files it in `at.cache`, so the
145
+ unmeasured mark lands on the same identity `capture()` will walk."""
146
+ try:
147
+ from nbxtensor.ops import autotune_cache as atc
148
+ return tuple(atc.key_of(at, args, kwargs))
149
+ except Exception: # noqa: BLE001
150
+ return None
151
+
152
+
153
+ def begin_sweep() -> None:
154
+ """Reset the per-sweep state. Called when a new key's bench begins."""
155
+ _SWEEP["best_ms"] = None
156
+ _SWEEP["budget_ms"] = None
157
+ _SWEEP["swap0_mb"] = None
158
+ _SWEEP["swap_said"] = False
159
+
160
+
161
+ def note_sweep_swap_baseline() -> None:
162
+ """Snapshot swap usage at the sweep's start, from our own authority."""
163
+ try:
164
+ from nbxtensor.backend.host_memory import memory_state # the library's own reading of the host
165
+ _SWEEP["swap0_mb"] = memory_state().swap_used_mb
166
+ except Exception: # noqa: BLE001
167
+ _SWEEP["swap0_mb"] = None
168
+
169
+
170
+ def sweep_grew_the_swap() -> int:
171
+ """MB of swap growth since the sweep's baseline, 0 when unknown or quiet.
172
+
173
+ The threshold is ZERO -- the identity, not a tuned constant -- because the
174
+ costs are asymmetric: a false mark (a daemon moved the swap) only skips
175
+ persistence and the key re-sweeps another day, while a missed mark
176
+ persists a choice timed against the swap, which keeps deciding on machines
177
+ and days it knows nothing about. Reproduced 2026-09-13: a 5.9 GB sweep
178
+ passed the pre-gate at 6.5 GB available, saturated the machine, and the
179
+ FOLLOWING key crawled in `waitUntilCompleted` with swap at 7621/8192 MB --
180
+ the round-9 shape, whose own arguments were 11 MB and innocent.
181
+ """
182
+ base = _SWEEP.get("swap0_mb")
183
+ if base is None:
184
+ return 0
185
+ try:
186
+ from nbxtensor.backend import device_facts as _facts, host_memory as _hm # the library's own readings
187
+ host_shares_memory_with_device, memory_state = _facts.host_shares_memory_with_device, _hm.memory_state
188
+ if not host_shares_memory_with_device():
189
+ return 0 # discrete memory: host swap is not the bench
190
+ now = memory_state().swap_used_mb
191
+ except Exception: # noqa: BLE001
192
+ return 0
193
+ if now is None:
194
+ return 0
195
+ return max(0, int(now) - int(base))
196
+
197
+
198
+ def mark_sweep_unmeasured(at, key, grew_mb: int, say=None) -> None:
199
+ """Mark this sweep's choice as timed against the swap, and SAY it once."""
200
+ from nbxtensor.ops import autotune_cache as atc
201
+
202
+ atc.mark_unmeasured(at, key)
203
+ if not _SWEEP.get("swap_said"):
204
+ _SWEEP["swap_said"] = True
205
+ line = (f"[AUTOTUNE_SWAP] the swap grew by {grew_mb} MB during this "
206
+ f"sweep: its timings measured the swap, not the kernels. The "
207
+ f"choice stands for this run and will NOT be persisted.")
208
+ (say or (lambda l: print(l, flush=True)))(line)
209
+
210
+
211
+ def current_budget_ms():
212
+ """The budget the bench should enforce for the CURRENT candidate, or None.
213
+
214
+ Read by the launcher's `do_bench` when its caller passed none: Triton's
215
+ `_bench -> self.do_bench(kernel_call, quantiles=...)` chain is not ours to
216
+ re-sign, so the wrapper publishes here and the bench consults it. Explicit
217
+ coupling, documented at both ends.
218
+ """
219
+ return _SWEEP.get("budget_ms")
220
+
221
+
222
+ def _compute_budget():
223
+ later = _BUDGET_RATIOS["later_vs_best"]
224
+ first = _BUDGET_RATIOS["first_vs_oracle_cpu"]
225
+ if _SWEEP["best_ms"] is not None:
226
+ return None if later is None else later * _SWEEP["best_ms"]
227
+ if first is None:
228
+ return None
229
+ try:
230
+ from nbxtensor.ops.screen_oracle import _ORACLE_MS
231
+ if _ORACLE_MS:
232
+ return first * next(reversed(_ORACLE_MS.values()))
233
+ except Exception: # noqa: BLE001
234
+ pass
235
+ return None
236
+
237
+
238
+ def note_candidate_time(single_launch_ms: float) -> None:
239
+ """Feed a completed candidate's probe time back into the sweep state."""
240
+ best = _SWEEP.get("best_ms")
241
+ if best is None or single_launch_ms < best:
242
+ _SWEEP["best_ms"] = float(single_launch_ms)
243
+
244
+
245
+ def exclude_slow_candidates(bench, say=None):
246
+ """Wrap a per-config benchmark so an over-budget candidate scores `inf`.
247
+
248
+ Same shape as `exclude_refused_configs`, same two rules: only the
249
+ dedicated exception is caught -- everything else propagates -- and the
250
+ exclusion is SAID with both of its numbers, because a candidate scored
251
+ out for time without its time is a silent narrowing.
252
+ """
253
+ def _say(line):
254
+ (say or (lambda l: print(l, flush=True)))(line)
255
+
256
+ def _run(*args, **kwargs):
257
+ from nbxtensor.backend.launcher import CandidateOverTimeBudget
258
+
259
+ _SWEEP["budget_ms"] = _compute_budget()
260
+ try:
261
+ out = bench(*args, **kwargs)
262
+ except CandidateOverTimeBudget as exc:
263
+ _say(f"[AUTOTUNE_SLOW] a candidate took {exc.took_ms:.1f} ms for "
264
+ f"ONE launch against a budget of {exc.budget_ms:.1f} ms "
265
+ f"derived from this sweep's own measurements; excluded from "
266
+ f"the sweep, which continues")
267
+ return list(_INF)
268
+ finally:
269
+ _SWEEP["budget_ms"] = None
270
+ return out
271
+ return _run
272
+
273
+
274
+ # ── the per-config memory footprint door ───────────────────────────────────
275
+ #
276
+ # `bench_would_swap` (launcher) refuses to bench a KEY whose ARGUMENTS alone
277
+ # exceed available memory. But a sweep tries CONFIGS, and a config allocates
278
+ # more than the arguments: Triton's L2-flush bench scratch (a 256 MiB torch
279
+ # buffer per never-seen key) plus the compiled kernel's own global/profile
280
+ # scratch. A config whose full footprint exceeds what remains is caught only
281
+ # AFTER it has run and grown the swap. This door estimates the footprint and
282
+ # excludes such a config BEFORE the spend -- the threadgroup-budget door
283
+ # (codegen refuses an over-budget tile) transposed to unified memory.
284
+ #
285
+ # The limit is read from `core.host_memory` -- the SAME source Prism plans
286
+ # against, never `Pages free` -- and live, because pressure is a state of the
287
+ # moment. The estimate is a LOWER bound made honest by a margin: undercounting
288
+ # would let a swapping config through, so unknown scratch is treated as its
289
+ # declared size and a safety margin is added, never subtracted.
290
+
291
+ #: Triton's L2-flush bench buffer, allocated once per never-seen key.
292
+ _BENCH_SCRATCH_BYTES = 256 * 1024 * 1024
293
+ #: A fraction of available kept free so the estimate's own slack cannot tip
294
+ #: the machine over. Not a tuned magic number -- a headroom, stated.
295
+ _FOOTPRINT_MARGIN = 0.10
296
+
297
+
298
+ def config_footprint_fits(arg_bytes: int, config_scratch_bytes: int = 0,
299
+ available_mb: "float | None" = None):
300
+ """(fits, available_mb, footprint_mb) for one candidate config.
301
+
302
+ footprint = arguments + the kernel's own scratch + the bench L2 buffer.
303
+ Compared to available memory less a margin. `available_mb` defaults to the
304
+ live `core.host_memory` reading (Prism's source). An unreadable platform
305
+ (None) fits everything and says nothing -- it adds no policy of its own.
306
+ """
307
+ if available_mb is None:
308
+ try:
309
+ from nbxtensor.backend import host_memory as _hm # the library's own reading of the host
310
+ memory_state = _hm.memory_state
311
+ available_mb = memory_state().available_mb
312
+ except Exception: # noqa: BLE001
313
+ available_mb = None
314
+ if available_mb is None:
315
+ return True, None, None
316
+ footprint = int(arg_bytes) + int(config_scratch_bytes) + _BENCH_SCRATCH_BYTES
317
+ usable = available_mb * (1.0 - _FOOTPRINT_MARGIN) * 2 ** 20
318
+ return footprint <= usable, available_mb, footprint / 2 ** 20
319
+
320
+
321
+ def exclude_over_footprint(bench, arg_bytes, config_scratch_bytes=0, say=None):
322
+ """Wrap a per-config bench so an over-footprint config scores `inf` without
323
+ running -- excluded BEFORE the spend, and SAID with its numbers."""
324
+ def _say(line):
325
+ (say or (lambda l: print(l, flush=True)))(line)
326
+
327
+ def _run(*args, **kwargs):
328
+ fits, avail, fp_mb = config_footprint_fits(arg_bytes, config_scratch_bytes)
329
+ if not fits:
330
+ _say(f"[AUTOTUNE_FOOTPRINT] a candidate config needs ~{fp_mb:.0f} MB "
331
+ f"(args + scratch + bench buffer) against {avail:.0f} MB "
332
+ f"available; excluded BEFORE running it, so the sweep neither "
333
+ f"swaps nor measures the swap. The sweep continues.")
334
+ return list(_INF)
335
+ return bench(*args, **kwargs)
336
+ return _run
337
+
338
+
339
+ def install() -> bool:
340
+ """Wrap `Autotuner._bench` so a refused config is excluded, once.
341
+
342
+ Installed by the launcher beside the screen oracle. Returns False when the
343
+ autotuner is not importable, so a caller can say so rather than assume it
344
+ took.
345
+ """
346
+ try:
347
+ from triton.runtime.autotuner import Autotuner
348
+ except Exception:
349
+ return False
350
+ if getattr(Autotuner._bench, "_nbx_excludes_refusals", False):
351
+ return True
352
+ original = Autotuner._bench
353
+
354
+ def _bench(self, *args, config=None, **kwargs):
355
+ # A sweep boundary is a change of (tuner, live-arg key): Triton
356
+ # benches one key's candidates consecutively, so the first _bench of
357
+ # a new pair is the first candidate of a new sweep. Detected HERE
358
+ # because nothing upstream of _bench is ours to hook -- and without
359
+ # this call, `begin_sweep` would be machinery with no caller, the
360
+ # register's own vacuous form.
361
+ try:
362
+ from nbxtensor.ops import autotune_cache as _atc
363
+ self.nargs = dict(zip(self.arg_names, args))
364
+ _token = (id(self), str(_atc.key_of(self, args, kwargs)))
365
+ except Exception: # noqa: BLE001
366
+ _token = (id(self), None)
367
+ if _SWEEP.get("token") != _token:
368
+ begin_sweep()
369
+ _SWEEP["token"] = _token
370
+ note_sweep_swap_baseline()
371
+ _out = exclude_slow_candidates(exclude_refused_configs(
372
+ lambda: original(self, *args, config=config, **kwargs)))()
373
+ _grew = sweep_grew_the_swap()
374
+ if _grew:
375
+ mark_sweep_unmeasured(self, _key_for_cache(self, args, kwargs), _grew)
376
+ return _out
377
+
378
+ _bench._nbx_excludes_refusals = True
379
+ Autotuner._bench = _bench
380
+ return True
@@ -0,0 +1,58 @@
1
+ """The autotune's two settings an application may set, in a module that loads nothing but the standard library — so
2
+ an application can set them when it starts without importing the autotune itself.
3
+
4
+ - `set_store(path)`: the directory of proven kernel settings (a path, or a callable returning one) (None: the library's own, the user's cache,
5
+ `backend.device_facts.certified_dir`); `directory()` the one in force.
6
+ - `set_miss_explainer(fn)`: `fn(kernel_qual, key, memory_gb) -> str`, an application's account of a key a confirmation
7
+ run misses (where it expected that key to come from), appended to the refusal; None removes it.
8
+
9
+ `ops.autotune_certified` reads both at call time.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Callable, Dict, Optional
16
+
17
+ __all__ = ["set_store", "directory", "set_miss_explainer", "miss_explainer"]
18
+
19
+ _STORE: Dict[str, object] = {} # {"dir": a Path, or a callable returning one} once a caller set its store
20
+ _MISS_EXPLAINER: Optional[Callable] = None
21
+
22
+
23
+ def set_store(path) -> None:
24
+ """Set the directory of proven kernel settings: a path, or a callable returning one (read each time the store is
25
+ asked — an application can set it when it starts without resolving it then); `None` restores the library's own,
26
+ the user's cache (`backend.device_facts.certified_dir`)."""
27
+ if path is None:
28
+ _STORE.clear()
29
+ else:
30
+ _STORE["dir"] = path if callable(path) else Path(path)
31
+
32
+
33
+ def directory() -> Path:
34
+ """The store in force: NEUROBRIX_AUTOTUNE_CERTIFIED_DIR when set (tests, a contributor's draft), else the one a
35
+ caller set (`set_store`), else the library's own in the user's cache."""
36
+ override = os.environ.get("NEUROBRIX_AUTOTUNE_CERTIFIED_DIR")
37
+ if override:
38
+ return Path(override)
39
+ if "dir" in _STORE:
40
+ where = _STORE["dir"]
41
+ return Path(where()) if callable(where) else where
42
+ from nbxtensor.backend.device_facts import certified_dir
43
+ return certified_dir()
44
+
45
+
46
+ def set_miss_explainer(explain) -> None:
47
+ """Install `explain(kernel_qual, key, memory_gb) -> str`: where the application expected a key it finds uncertified
48
+ to come from, appended to the refusal; None removes it. Optional: without one, the refusal says what the store holds
49
+ and nothing more."""
50
+ global _MISS_EXPLAINER
51
+ if explain is not None and not callable(explain):
52
+ raise TypeError("set_miss_explainer: the explainer must be callable (kernel_qual, key, memory_gb) -> str")
53
+ _MISS_EXPLAINER = explain
54
+
55
+
56
+ def miss_explainer() -> Optional[Callable]:
57
+ """The explainer in force, or None."""
58
+ return _MISS_EXPLAINER
@@ -0,0 +1,73 @@
1
+ """Average pooling 2D — pure @triton.jit kernel.
2
+
3
+ Extracted from FlagGems avg_pool2d.py. Forward only (inference).
4
+ 2D blocked approach with proper padding, dilation, count_include_pad support.
5
+ """
6
+
7
+ import triton
8
+ import triton.language as tl
9
+
10
+
11
+ @triton.jit
12
+ def avg_pool2d_forward_kernel(
13
+ input_ptr,
14
+ output_ptr,
15
+ in_stride_n, in_stride_c, in_stride_h, in_stride_w,
16
+ in_c, in_h, in_w,
17
+ out_h, out_w,
18
+ kernel_h: tl.constexpr,
19
+ kernel_w: tl.constexpr,
20
+ stride_h: tl.constexpr,
21
+ stride_w: tl.constexpr,
22
+ padding_h: tl.constexpr,
23
+ padding_w: tl.constexpr,
24
+ dilation_h: tl.constexpr,
25
+ dilation_w: tl.constexpr,
26
+ COUNT_INCLUDE_PAD: tl.constexpr,
27
+ divisor_override,
28
+ BLOCK_H: tl.constexpr,
29
+ BLOCK_W: tl.constexpr,
30
+ ):
31
+ pid_nc = tl.program_id(0).to(tl.int64)
32
+ pid_hw = tl.program_id(1).to(tl.int64)
33
+ num_w_blocks = tl.cdiv(out_w, BLOCK_W)
34
+ h_block_idx = pid_hw // num_w_blocks
35
+ w_block_idx = pid_hw % num_w_blocks
36
+ n_idx = pid_nc // in_c
37
+ c_idx = pid_nc % in_c
38
+
39
+ h_out_offsets = h_block_idx * BLOCK_H + tl.arange(0, BLOCK_H)
40
+ w_out_offsets = w_block_idx * BLOCK_W + tl.arange(0, BLOCK_W)
41
+
42
+ sum_acc = tl.zeros((BLOCK_H, BLOCK_W), dtype=tl.float32)
43
+ count_acc = tl.zeros((BLOCK_H, BLOCK_W), dtype=tl.int32)
44
+
45
+ input_base_ptr = input_ptr + n_idx * in_stride_n + c_idx * in_stride_c
46
+
47
+ for kh in range(0, kernel_h):
48
+ for kw in range(0, kernel_w):
49
+ h_in = h_out_offsets[:, None] * stride_h - padding_h + kh * dilation_h
50
+ w_in = w_out_offsets[None, :] * stride_w - padding_w + kw * dilation_w
51
+ in_mask = (h_in >= 0) & (h_in < in_h) & (w_in >= 0) & (w_in < in_w)
52
+
53
+ input_offset = h_in * in_stride_h + w_in * in_stride_w
54
+ current_val = tl.load(input_base_ptr + input_offset, mask=in_mask, other=0.0)
55
+
56
+ sum_acc += tl.where(in_mask, current_val, 0.0)
57
+ count_acc += in_mask.to(tl.int32)
58
+
59
+ if divisor_override != 0:
60
+ divisor = tl.full((BLOCK_H, BLOCK_W), divisor_override, dtype=tl.float32)
61
+ elif COUNT_INCLUDE_PAD:
62
+ divisor = tl.full((BLOCK_H, BLOCK_W), kernel_h * kernel_w, dtype=tl.float32)
63
+ else:
64
+ divisor = count_acc.to(tl.float32)
65
+
66
+ output_vals = tl.where(divisor != 0, sum_acc / divisor, 0.0)
67
+
68
+ out_base_ptr = output_ptr + pid_nc * out_h * out_w
69
+ output_block_ptr = (
70
+ out_base_ptr + h_out_offsets[:, None] * out_w + w_out_offsets[None, :]
71
+ )
72
+ out_mask = (h_out_offsets[:, None] < out_h) & (w_out_offsets[None, :] < out_w)
73
+ tl.store(output_block_ptr, output_vals.to(output_ptr.type.element_ty), mask=out_mask)
@@ -0,0 +1,132 @@
1
+ """Batched matmul with optional bias (baddbmm/bmm) — pure @triton.jit kernel.
2
+
3
+ Originally ported from FlagGems baddbmm (Apache-2.0 license); body aligned
4
+ on kernels/ops/matmul.py:matmul_kernel (P-KERNEL-LAUNCH-HYGIENE B1,
5
+ 2026-07) so the SAME batched kernel serves both:
6
+
7
+ - aten::baddbmm (HAS_BIAS=True): out = beta * bias + alpha * (b1 @ b2)
8
+ - aten::bmm (HAS_BIAS=False): out = A @ B, one 3D-grid launch for the
9
+ whole batch instead of the former per-batch (and per-row in decode)
10
+ Python launch loops in wrappers.bmm.
11
+
12
+ batch1 is [B, M, K], batch2 is [B, K, N], bias broadcastable to [B, M, N].
13
+ Grid: (cdiv(M, BLOCK_M) * cdiv(N, BLOCK_N), 1, B). Accumulates in fp32.
14
+
15
+ IEEE_PRECISION / PROMOTE_B carry the exact matmul_kernel semantics (see its
16
+ docstring): ieee dot for fp32-promoted inputs on pre-Ampere, in-register
17
+ fp16→fp32 b-tile promotion for the mixed activation/weight case. The K-loop
18
+ body (3-arg fused `tl.dot`, %-wrap row/col offsets, K-tail masks) is a
19
+ line-for-line mirror of matmul_kernel so a batched launch is value-identical
20
+ to the former per-batch matmul_kernel loop under the same autotune config.
21
+
22
+ Phase 1.5 (2026-05): @triton.autotune ENABLED with the same Volta-viable
23
+ configs as kernels/ops/matmul.py. See CLAUDE.md "Autotune policy" section
24
+ for the doctrinal exception.
25
+ """
26
+
27
+ import triton
28
+
29
+ from nbxtensor.ops._configs import nbx_autotune
30
+ import triton.language as tl
31
+
32
+ from nbxtensor.ops.matmul import _MATMUL_AUTOTUNE_CONFIGS
33
+
34
+
35
+ @nbx_autotune(configs=_MATMUL_AUTOTUNE_CONFIGS,
36
+ key=['M_BUCKET', 'N_BUCKET', 'K_BUCKET', 'IEEE_PRECISION', 'PROMOTE_B',
37
+ 'HAS_BIAS'],
38
+ cache_results=True)
39
+ @triton.jit
40
+ def baddbmm_kernel(
41
+ A_ptr,
42
+ B_ptr,
43
+ out_ptr,
44
+ bias_ptr,
45
+ alpha,
46
+ beta,
47
+ M, N, K, M_BUCKET, N_BUCKET, K_BUCKET,
48
+ stride_ab, stride_am, stride_ak,
49
+ stride_bb, stride_bk, stride_bn,
50
+ stride_ob, stride_om, stride_on,
51
+ bias_batch_stride,
52
+ bias_m_stride,
53
+ bias_n_stride,
54
+ IEEE_PRECISION: tl.constexpr = False,
55
+ PROMOTE_B: tl.constexpr = False,
56
+ HAS_BIAS: tl.constexpr = True,
57
+ BLOCK_M: tl.constexpr = 64,
58
+ BLOCK_N: tl.constexpr = 64,
59
+ BLOCK_K: tl.constexpr = 32,
60
+ GROUP_M: tl.constexpr = 8,
61
+ ):
62
+ """out[b] = beta*bias[b] + alpha*(A[b] @ B[b]) (HAS_BIAS=True)
63
+ out[b] = A[b] @ B[b] (HAS_BIAS=False)
64
+
65
+ Grid: (cdiv(M, BLOCK_M) * cdiv(N, BLOCK_N), 1, batch)
66
+ Accumulates in fp32 for numerical stability; in-register cast to the
67
+ output pointer's dtype before store (matmul_kernel pattern).
68
+ """
69
+ # Batch offset
70
+ pid_b = tl.program_id(2).to(tl.int64) # a batch offset times a stride wraps past 2^31 elements in int32
71
+ A_ptr += pid_b * stride_ab
72
+ B_ptr += pid_b * stride_bb
73
+ out_ptr += pid_b * stride_ob
74
+ if HAS_BIAS:
75
+ bias_ptr += pid_b * bias_batch_stride
76
+
77
+ # tl.assume integer-analyzer hints (matmul_kernel pattern). Batch and
78
+ # bias strides are excluded: they are legitimately 0 for broadcast.
79
+ tl.assume(stride_am > 0); tl.assume(stride_ak > 0)
80
+ tl.assume(stride_bk > 0); tl.assume(stride_bn > 0)
81
+ tl.assume(stride_om > 0); tl.assume(stride_on > 0)
82
+
83
+ # 2D tile indexing with grouping for L2 locality
84
+ pid = tl.program_id(0).to(tl.int64)
85
+ num_pid_m = tl.cdiv(M, BLOCK_M)
86
+ num_pid_n = tl.cdiv(N, BLOCK_N)
87
+ num_pid_in_group = GROUP_M * num_pid_n
88
+ group_id = pid // num_pid_in_group
89
+ first_pid_m = group_id * GROUP_M
90
+ group_size_m = min(num_pid_m - first_pid_m, GROUP_M)
91
+ pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
92
+ pid_n = (pid % num_pid_in_group) // group_size_m
93
+
94
+ offs_am = (pid_m * BLOCK_M + tl.arange(0, BLOCK_M)) % M
95
+ offs_bn = (pid_n * BLOCK_N + tl.arange(0, BLOCK_N)) % N
96
+ offs_k = tl.arange(0, BLOCK_K)
97
+ a_ptrs = A_ptr + (offs_am[:, None].to(tl.int64) * stride_am + offs_k[None, :] * stride_ak)
98
+ b_ptrs = B_ptr + (offs_k[:, None] * stride_bk + offs_bn[None, :].to(tl.int64) * stride_bn)
99
+
100
+ # Accumulate matmul in fp32 — line-for-line matmul_kernel K loop.
101
+ accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
102
+ for k in range(0, tl.cdiv(K, BLOCK_K)):
103
+ a = tl.load(a_ptrs, mask=offs_k[None, :] < K - k * BLOCK_K, other=0.0)
104
+ b = tl.load(b_ptrs, mask=offs_k[:, None] < K - k * BLOCK_K, other=0.0)
105
+ if PROMOTE_B:
106
+ b = b.to(a.dtype)
107
+ # 3-arg HMMA-FMA fused form (tutorial pattern) — same as
108
+ # matmul_kernel so the bmm path stays bit-identical to the former
109
+ # per-batch loop. IEEE_PRECISION supersedes the historical
110
+ # `allow_tf32=False` (equivalent lowering for fp32 inputs).
111
+ if IEEE_PRECISION:
112
+ accumulator = tl.dot(a, b, accumulator, input_precision="ieee")
113
+ else:
114
+ accumulator = tl.dot(a, b, accumulator)
115
+ a_ptrs += BLOCK_K * stride_ak
116
+ b_ptrs += BLOCK_K * stride_bk
117
+
118
+ offs_cm = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
119
+ offs_cn = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
120
+ out_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
121
+
122
+ if HAS_BIAS:
123
+ # Load bias and apply alpha/beta
124
+ bias_ptrs = (bias_ptr + offs_cm[:, None].to(tl.int64) * bias_m_stride
125
+ + offs_cn[None, :].to(tl.int64) * bias_n_stride)
126
+ bi = tl.load(bias_ptrs, mask=out_mask, other=0.0)
127
+ accumulator = accumulator * alpha + bi * beta
128
+
129
+ # In-kernel cast accum → output dtype (matmul_kernel pattern).
130
+ c = accumulator.to(out_ptr.dtype.element_ty)
131
+ o_ptrs = out_ptr + offs_cm[:, None].to(tl.int64) * stride_om + offs_cn[None, :].to(tl.int64) * stride_on
132
+ tl.store(o_ptrs, c, mask=out_mask)