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,1648 @@
1
+ """Attention wrappers: softmax, scaled dot-product attention (math, chunked, flash, the matrix-unit flash), paged and split-KV decode. Cut out of wrappers.py by family (M2, 2026-10-29), unchanged; shared launch layer and run context stay in wrappers.py."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import triton
7
+ from nbxtensor.ops.autotune_bucket import bucket_of as _bucket_of
8
+ from nbxtensor.ops import launch_keys as _lk
9
+ from nbxtensor.tensor import NBXTensor
10
+ from nbxtensor.dtype.nbx_dtype import NBXDtype, dtype_size
11
+ from nbxtensor.memory.allocator import DeviceAllocator
12
+ from nbxtensor.backend.runtime import set_device
13
+ from nbxtensor.shape import MAX_NDIM as _NBX_MAX_NDIM, broadcast_shapes, max_linear_offset
14
+ from nbxtensor.dtype.itemsize import canonical_name
15
+ from nbxtensor.dtype.promotion import binary_result, float_math_input
16
+ from nbxtensor.dtype.nbx_dtype import parse_dtype
17
+ from nbxtensor.memory.allocator import DeviceOOMError
18
+ from nbxtensor.backend.capabilities import fa_min_tile as _fa_min_tile
19
+ from nbxtensor.backend.capabilities import reduce_tile_max as _reduce_tile_max
20
+ from nbxtensor.backend.faults import device_fault_buffer, device_fault_code_cached, fault_channel
21
+ from nbxtensor.tensor import _upload_int64_array as _nbx_upload_int64, _saturating_cast as _nbx_saturating_cast
22
+ from nbxtensor.ops._configs import sdpa_block_ceiling as _sdpa_block_ceiling
23
+ from nbxtensor.ops._configs import sdpa_launch_meta as _sdpa_launch_meta
24
+ from nbxtensor.ops._configs import matrix_unit as _matrix_unit, matrix_unit_mm_tile as _matrix_unit_mm_tile
25
+ from nbxtensor.ops._configs import largest_tile_within_smem as _largest_tile_within_smem
26
+ from nbxtensor.backend import launcher as _nbx_launcher
27
+ from nbxtensor.backend.launcher import install as _install_launcher
28
+ from nbxtensor.ops.relu import relu_forward_kernel
29
+ from nbxtensor.ops.silu import silu_forward_kernel
30
+ from nbxtensor.ops.gelu import gelu_forward_kernel
31
+ from nbxtensor.ops.sigmoid import sigmoid_forward_kernel
32
+ from nbxtensor.ops.tanh import tanh_forward_kernel
33
+ from nbxtensor.ops.hardsigmoid import hardsigmoid_forward_kernel
34
+ from nbxtensor.ops.hardswish import hardswish_forward_kernel
35
+ from nbxtensor.ops.leaky_relu import leaky_relu_forward_kernel
36
+ from nbxtensor.ops.elu import elu_forward_kernel
37
+ from nbxtensor.ops.mish import mish_forward_kernel
38
+ from nbxtensor.ops.selu import selu_forward_kernel
39
+ from nbxtensor.ops.neg import neg_forward_kernel
40
+ from nbxtensor.ops.exp import exp_forward_kernel
41
+ from nbxtensor.ops.sin import sin_forward_kernel
42
+ from nbxtensor.ops.cos import cos_forward_kernel
43
+ from nbxtensor.ops.rsqrt import rsqrt_forward_kernel
44
+ from nbxtensor.ops.sqrt import sqrt_forward_kernel
45
+ from nbxtensor.ops.abs import abs_forward_kernel
46
+ from nbxtensor.ops.log import log_forward_kernel
47
+ from nbxtensor.ops.reciprocal import reciprocal_forward_kernel
48
+ from nbxtensor.ops.pow import pow_forward_kernel
49
+ from nbxtensor.ops.clamp import clamp_forward_kernel
50
+ from nbxtensor.ops.erf import erf_forward_kernel
51
+ from nbxtensor.ops.copy import copy_forward_kernel
52
+ from nbxtensor.ops.add import add_forward_kernel, add_scalar_kernel, add_scalar_dev_kernel, add_bias_broadcast_kernel
53
+ from nbxtensor.ops.binary_strided import add_strided_nd_kernel, mul_strided_nd_kernel, sub_strided_nd_kernel, div_strided_nd_kernel, add_scalar_strided_nd_kernel, mul_scalar_strided_nd_kernel
54
+ from nbxtensor.ops.mul import mul_forward_kernel, mul_scalar_kernel, mul_scalar_dev_kernel
55
+ from nbxtensor.ops.div import div_forward_kernel, div_scalar_kernel, div_scalar_dev_kernel, int_div_kernel
56
+ from nbxtensor.ops.sub import sub_forward_kernel, rsub_forward_kernel
57
+ from nbxtensor.ops.where import where_forward_kernel
58
+ from nbxtensor.ops.maximum import maximum_forward_kernel
59
+ from nbxtensor.ops.minimum import minimum_forward_kernel
60
+ from nbxtensor.ops.masked_fill import masked_fill_forward_kernel
61
+ from nbxtensor.ops.gt import gt_forward_kernel, gt_scalar_kernel
62
+ from nbxtensor.ops.ge import ge_forward_kernel, ge_scalar_kernel
63
+ from nbxtensor.ops.lt import lt_forward_kernel, lt_scalar_kernel
64
+ from nbxtensor.ops.eq import eq_forward_kernel, eq_scalar_kernel
65
+ from nbxtensor.ops.ne import ne_forward_kernel, ne_scalar_kernel
66
+ from nbxtensor.ops.layernorm import layer_norm_forward_kernel
67
+ from nbxtensor.ops.rmsnorm import rms_norm_forward_kernel
68
+ from nbxtensor.ops.softmax import softmax_forward_kernel, softmax_forward_looped_kernel
69
+ from nbxtensor.ops.matmul import matmul_kernel, addmm_kernel
70
+ from nbxtensor.ops._autotune_policy import NBX_FORCE_FP32_ACCUM
71
+ from nbxtensor.ops.fused_moe import fused_moe_kernel, silu_and_mul_kernel, silu_mul_split_kernel
72
+ from nbxtensor.ops.dtype_convert import bf16_to_fp16_kernel
73
+ from nbxtensor.ops.embedding import embedding_kernel, EMBEDDING_OOB
74
+ from nbxtensor.ops.sum import sum_forward_kernel
75
+ from nbxtensor.ops.mean import mean_forward_kernel
76
+ from nbxtensor.ops.amax import amax_forward_kernel
77
+ from nbxtensor.ops.glu import glu_forward_kernel
78
+ from nbxtensor.ops.le import le_forward_kernel, le_scalar_kernel
79
+ from nbxtensor.ops.softplus import softplus_forward_kernel
80
+ from nbxtensor.ops.dropout import dropout_inference_kernel
81
+ from nbxtensor.ops.upsample_nearest2d import upsample_nearest2d_kernel
82
+ from nbxtensor.ops.groupnorm import group_norm_forward_kernel
83
+ from nbxtensor.ops.index_select import index_select_kernel, INDEX_SELECT_OOB
84
+ from nbxtensor.ops.triu import triu_kernel, triu_batch_kernel
85
+ from nbxtensor.ops.tril import tril_kernel, tril_batch_kernel
86
+ from nbxtensor.ops.argmax import argmax_kernel_1, argmax_kernel_2, argmax_kernel_inner
87
+ from nbxtensor.ops.argmin import argmin_kernel_1, argmin_kernel_2, argmin_kernel
88
+ from nbxtensor.ops.conv2d import conv2d_forward_kernel
89
+ from nbxtensor.ops.depthwise_conv2d import depthwise_conv2d_kernel
90
+ from nbxtensor.ops.batch_norm import batch_norm_forward_kernel
91
+ from nbxtensor.ops.cumsum import scan_part_sum_kernel, add_base_sum_kernel
92
+ from nbxtensor.ops.scatter_op import scatter_kernel, scatter_add_kernel, scatter_reduce_amax_kernel, scatter_reduce_amin_kernel
93
+ from nbxtensor.ops.gather_op import gather_kernel
94
+ from nbxtensor.ops.avg_pool2d import avg_pool2d_forward_kernel
95
+ from nbxtensor.ops.max_pool2d import max_pool2d_forward_kernel
96
+ from nbxtensor.ops.max_pool3d import max_pool3d_forward_kernel
97
+ from nbxtensor.ops.cross_entropy import cross_entropy_loss_forward_kernel
98
+ from nbxtensor.ops.exp2 import exp2_forward_kernel
99
+ from nbxtensor.ops.tan import tan_forward_kernel
100
+ from nbxtensor.ops.celu import celu_forward_kernel
101
+ from nbxtensor.ops.log_sigmoid import log_sigmoid_forward_kernel
102
+ from nbxtensor.ops.isfinite import isfinite_forward_kernel
103
+ from nbxtensor.ops.isinf import isinf_forward_kernel
104
+ from nbxtensor.ops.isnan import isnan_forward_kernel
105
+ from nbxtensor.ops.nan_to_num import nan_to_num_forward_kernel
106
+ from nbxtensor.ops.threshold import threshold_forward_kernel
107
+ from nbxtensor.ops.floor import floor_forward_kernel, ceil_forward_kernel, round_forward_kernel, trunc_forward_kernel
108
+ from nbxtensor.ops.addcdiv import addcdiv_forward_kernel
109
+ from nbxtensor.ops.addcmul import addcmul_forward_kernel
110
+ from nbxtensor.ops.all_reduce import all_kernel_mid, all_kernel_result, all_kernel_dim
111
+ from nbxtensor.ops.any_reduce import any_kernel_mid, any_kernel_result, any_kernel_dim
112
+ from nbxtensor.ops.bitwise_and import bitwise_and_forward_kernel
113
+ from nbxtensor.ops.bitwise_or import bitwise_or_forward_kernel
114
+ from nbxtensor.ops.bitwise_not import bitwise_not_forward_kernel
115
+ from nbxtensor.ops.logical_and import logical_and_forward_kernel
116
+ from nbxtensor.ops.logical_or import logical_or_forward_kernel
117
+ from nbxtensor.ops.logical_not import logical_not_forward_kernel
118
+ from nbxtensor.ops.lerp import lerp_tensor_forward_kernel, lerp_scalar_head_kernel, lerp_scalar_tail_kernel
119
+ from nbxtensor.ops.dot_op import dot_kernel_small, dot_kernel_partial, dot_kernel_reduce
120
+ from nbxtensor.ops.flip_op import flip_1d_kernel, flip_strided_kernel
121
+ from nbxtensor.ops.cat_op import cat_copy_kernel_4
122
+ from nbxtensor.ops.fill_op import fill_kernel
123
+ from nbxtensor.ops.arange_op import arange_kernel
124
+ from nbxtensor.ops.prod import prod_kernel_mid, prod_kernel_result, prod_kernel
125
+ from nbxtensor.ops.min_reduce import min_kernel_mid, min_kernel_result, min_kernel
126
+ from nbxtensor.ops.max_reduce import max_kernel_mid, max_kernel_result, max_kernel
127
+ from nbxtensor.ops.baddbmm_op import baddbmm_kernel
128
+ from nbxtensor.ops.addmv_op import addmv_kernel
129
+ from nbxtensor.ops.mse_loss import mse_loss_partial_kernel, mse_loss_reduce_kernel, mse_loss_none_kernel
130
+ from nbxtensor.ops.nllloss import nll_loss_forward_kernel
131
+ from nbxtensor.ops.std import std_map_kernel, std_reduce_kernel, std_dim_kernel
132
+ from nbxtensor.ops.var import var_kernel_1, var_kernel_2, var_welford_kernel
133
+ from nbxtensor.ops.index_add import index_add_gather_kernel
134
+ from nbxtensor.ops.index_put_op import index_put_kernel, INDEX_PUT_OOB
135
+ from nbxtensor.ops.sort_op import (radix_sort_histogram_kernel,
136
+ radix_sort_scatter_kernel,
137
+ radix_sort_tile_counts_kernel,
138
+ radix_sort_tile_prefix_kernel)
139
+ from nbxtensor.ops.rope import rope_forward_kernel
140
+ from nbxtensor.ops.pixel_shuffle import (
141
+ pixel_shuffle_kernel,
142
+ pixel_shuffle_broadcast_aware_kernel,
143
+ )
144
+ from nbxtensor.ops.pixel_unshuffle import pixel_unshuffle_kernel
145
+ from nbxtensor.ops.upsample_bilinear2d import upsample_bilinear2d_kernel
146
+ from nbxtensor.ops.adaptive_avg_pool2d import adaptive_avg_pool2d_kernel
147
+ from nbxtensor.ops.conv_depthwise2d import conv_depthwise2d_kernel
148
+ from nbxtensor.ops.scaled_softmax import scaled_softmax_kernel
149
+ from nbxtensor.ops.vector_norm import (
150
+ l2_norm_kernel, l1_norm_kernel, linf_norm_kernel, l0_norm_kernel, lp_norm_kernel,
151
+ l2_norm_pass1_kernel, l2_norm_pass2_kernel,
152
+ linf_norm_pass1_kernel, linf_norm_pass2_kernel,
153
+ )
154
+ from nbxtensor.ops.addr import addr_kernel
155
+ from nbxtensor.ops.logical_xor import logical_xor_kernel
156
+ from nbxtensor.ops.bitwise_left_shift import bitwise_left_shift_kernel
157
+ from nbxtensor.ops.bitwise_right_shift import bitwise_right_shift_kernel
158
+ from nbxtensor.ops.rand_op import rand_kernel, randn_kernel
159
+ from nbxtensor.ops.clamp_min import clamp_min_forward_kernel
160
+ from nbxtensor.ops.conv_transpose2d import conv_transpose2d_kernel
161
+ from nbxtensor.ops.cummax import scan_part_max_kernel
162
+ from nbxtensor.ops.cummin import scan_part_min_kernel
163
+ from nbxtensor.ops.count_nonzero import count_nonzero_kernel
164
+ from nbxtensor.ops.trace_op import trace_kernel
165
+ from nbxtensor.ops.linspace_op import linspace_kernel
166
+ from nbxtensor.ops.full_op import full_kernel
167
+ from nbxtensor.ops.stack_op import stack_copy_kernel
168
+ from nbxtensor.ops.repeat_op import repeat_1d_kernel
169
+ from nbxtensor.ops.upsample_nearest3d import upsample_nearest3d_kernel
170
+ from nbxtensor.ops.weight_norm import weight_norm_kernel_first, weight_norm_kernel_last
171
+ from nbxtensor.ops.repeat_interleave import repeat_interleave_tensor_kernel
172
+ from nbxtensor.ops.elementwise_launch import ew_meta as _ew_meta, ew_grid as _ew_grid
173
+ from nbxtensor.ops import tiling_sizes as _ts
174
+ import math
175
+ from nbxtensor.ops import elementwise_wrappers as _ew
176
+ from nbxtensor.ops import linalg_wrappers as _la
177
+ from nbxtensor.ops import wrappers as _core
178
+
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # Helper
182
+ # ---------------------------------------------------------------------------
183
+
184
+ _TL_DTYPE = {NBXDtype.float16: triton.language.float16, NBXDtype.bfloat16: triton.language.bfloat16,
185
+ NBXDtype.float32: triton.language.float32, NBXDtype.float64: triton.language.float64}
186
+
187
+
188
+ # ===========================================================================
189
+ # SOFTMAX WRAPPER
190
+ # ===========================================================================
191
+
192
+ # Above this feat size the single-tile kernel's next_power_of_2(feat) tile
193
+ # spills to CUDA local memory, whose pool the driver allocates DEVICE-WIDE
194
+ # at launch (frame × max resident threads — ~2 GiB measured at vocab
195
+ # 151936 on V100, D9 2026-07-27). Rows larger than the cap route to the
196
+ # bounded-tile looped kernel; rows at or under it keep the proven
197
+ # single-tile kernel byte-for-byte. Same capped-tile idiom as
198
+ # argmax_wrapper's TILE_N = min(next_power_of_2(N), 4096) (FlagGems).
199
+ _SOFTMAX_MAX_FUSED_FEAT = _lk.SOFTMAX_FUSED_MAX_FEAT # one value with the census (launch_keys)
200
+ def _launch_looped_softmax(x_2d, output_2d, batch_dim, feat_dim, log: bool):
201
+ """Launch the bounded-tile online softmax — one program per row; its tile and warps the certified configuration
202
+ of this key (chantier 1), the key the derived census forms (`launch_keys.softmax_launches`)."""
203
+ softmax_forward_looped_kernel[(batch_dim,)](
204
+ x_2d, output_2d,
205
+ feat_dim, _lk.bucket_of("M", batch_dim), _lk.softmax_feat_bucket(feat_dim),
206
+ x_2d.stride(0), x_2d.stride(1),
207
+ output_2d.stride(0), output_2d.stride(1),
208
+ log=log,
209
+ )
210
+
211
+
212
+ def softmax(x, dim: int = -1, half_to_float: bool = False) :
213
+ """Softmax over given dimension."""
214
+ dim = dim % x.ndim
215
+ # Reshape to [batch, feat] where feat = softmax dim
216
+ feat_dim = x.shape[dim]
217
+ batch_dim = x.numel() // feat_dim
218
+
219
+ # Move softmax dim to last, flatten
220
+ x_perm = x.movedim(dim, -1).contiguous()
221
+ x_2d = x_perm.view(batch_dim, feat_dim)
222
+ output_2d = NBXTensor.empty_like(x_2d)
223
+
224
+ set_device(x_2d)
225
+ if feat_dim > _SOFTMAX_MAX_FUSED_FEAT:
226
+ _launch_looped_softmax(x_2d, output_2d, batch_dim, feat_dim, log=False)
227
+ else:
228
+ # rows per program and warps: the certified configuration of this key (workstream 1), the row's bucket from
229
+ # the launcher's own `bucket_of` — the key the derived census forms (`launch_keys.softmax_launches`)
230
+ grid = lambda META: (triton.cdiv(batch_dim, META['BLOCK_SIZE_BATCH']),)
231
+ softmax_forward_kernel[grid](
232
+ x_2d, output_2d,
233
+ batch_dim, feat_dim, _lk.bucket_of("M", batch_dim), _lk.softmax_feat_bucket(feat_dim),
234
+ x_2d.stride(0), x_2d.stride(1),
235
+ output_2d.stride(0), output_2d.stride(1),
236
+ log=False,
237
+ )
238
+ return output_2d.view_as(x_perm).movedim(-1, dim)
239
+
240
+
241
+ def log_softmax(x, dim: int = -1) :
242
+ """Log softmax over given dimension."""
243
+ dim = dim % x.ndim
244
+ feat_dim = x.shape[dim]
245
+ batch_dim = x.numel() // feat_dim
246
+
247
+ x_perm = x.movedim(dim, -1).contiguous()
248
+ x_2d = x_perm.view(batch_dim, feat_dim)
249
+ output_2d = NBXTensor.empty_like(x_2d)
250
+
251
+ set_device(x_2d)
252
+ if feat_dim > _SOFTMAX_MAX_FUSED_FEAT:
253
+ _launch_looped_softmax(x_2d, output_2d, batch_dim, feat_dim, log=True)
254
+ else:
255
+ # rows per program and warps: the certified configuration of this key (workstream 1), the row's bucket from
256
+ # the launcher's own `bucket_of` — the key the derived census forms (`launch_keys.softmax_launches`)
257
+ grid = lambda META: (triton.cdiv(batch_dim, META['BLOCK_SIZE_BATCH']),)
258
+ softmax_forward_kernel[grid](
259
+ x_2d, output_2d,
260
+ batch_dim, feat_dim, _lk.bucket_of("M", batch_dim), _lk.softmax_feat_bucket(feat_dim),
261
+ x_2d.stride(0), x_2d.stride(1),
262
+ output_2d.stride(0), output_2d.stride(1),
263
+ log=True,
264
+ )
265
+ return output_2d.view_as(x_perm).movedim(-1, dim)
266
+
267
+
268
+ def _sdpa_math_scores_budget_bytes() -> int:
269
+ """Pre-Ampere scores-tensor budget for routing SDPA to the
270
+ deterministic `_math_attention` path (P-TRITON-MOE-DETERMINISM-
271
+ RESIDUAL, option B). Data-driven from
272
+ config/vendors/<vendor>/<arch>.yml `memory.sdpa_math_max_scores_bytes`
273
+ (R10/R23 — no hardcoded hardware param, no driver query in the hot
274
+ path; the value is read from the YAML keyed by the detected
275
+ profile). Returns 0 (→ pow2 head_dim never routes on size, i.e.
276
+ flash unchanged) when the profile or the key is unavailable, so an
277
+ unconfigured arch never silently changes the attention path."""
278
+ prof = _core.get_hardware_profile()
279
+ devices = getattr(prof, "devices", None) if prof is not None else None
280
+ if not devices:
281
+ return 0
282
+ try:
283
+ from nbxtensor.backend.device_files import describe as get_vendor_config # the device, both halves
284
+ # the DEVICE brand — NOT PrismProfile.vendor (that is the
285
+ # machine maker, e.g. "dell", and would mis-resolve the path
286
+ # config/vendors/<vendor>/<arch>.yml).
287
+ _brand = devices[0].brand
288
+ _vendor = getattr(_brand, "value", _brand)
289
+ cfg = get_vendor_config(_vendor, devices[0].architecture)
290
+ return int(cfg.get("memory", {}).get(
291
+ "sdpa_math_max_scores_bytes", 0))
292
+ except Exception:
293
+ return 0
294
+
295
+
296
+ def _sdpa_math_scores_device_fraction() -> float:
297
+ """Optional per-DEVICE cap on the math-route scores budget, as a
298
+ fraction of the EXECUTING device's memory. Data-driven from
299
+ config/vendors/<vendor>/<arch>.yml
300
+ `memory.sdpa_math_scores_device_fraction` (R23 — the 2026-08-31
301
+ xlong-prefill OOM proved the per-arch byte budget alone is wrong on
302
+ heterogeneous rigs: 2 GiB of scores fit a 32 GB card's headroom but
303
+ not a 16 GB card's). 0.0 (absent key) = no device cap, prior
304
+ behaviour byte-unchanged."""
305
+ prof = _core.get_hardware_profile()
306
+ devices = getattr(prof, "devices", None) if prof is not None else None
307
+ if not devices:
308
+ return 0.0
309
+ try:
310
+ from nbxtensor.backend.device_files import describe as get_vendor_config # the device, both halves
311
+ _brand = devices[0].brand
312
+ _vendor = getattr(_brand, "value", _brand)
313
+ cfg = get_vendor_config(_vendor, devices[0].architecture)
314
+ return float(cfg.get("memory", {}).get(
315
+ "sdpa_math_scores_device_fraction", 0.0))
316
+ except Exception:
317
+ return 0.0
318
+
319
+
320
+ def _sdpa_math_scores_budget_bytes_for(device_idx) -> int:
321
+ """Scores budget for the device that will execute this SDPA:
322
+ min(per-arch yml byte budget, device_fraction x THIS device's
323
+ memory_mb from the Prism profile — a static profile read, never a
324
+ driver query in the hot path). Absence (no fraction key, no
325
+ profile, unknown index) is handled by EXPLICIT conditions and
326
+ returns the plain per-arch budget; there is no broad except — a
327
+ real profile-read failure must surface, not silently restore the
328
+ 16G OOM this cap exists to prevent."""
329
+ base = _sdpa_math_scores_budget_bytes()
330
+ if not base:
331
+ return 0
332
+ frac = _sdpa_math_scores_device_fraction()
333
+ if not frac or device_idx is None:
334
+ return base
335
+ prof = _core.get_hardware_profile()
336
+ devices = getattr(prof, "devices", None) if prof is not None else None
337
+ if not devices:
338
+ return base
339
+ dev = next((d for d in devices
340
+ if getattr(d, "index", None) == int(device_idx)), None)
341
+ if dev is None:
342
+ return base
343
+ return _ts.sdpa_math_scores_budget(base, frac, dev.memory_mb)
344
+
345
+
346
+ def _sdpa_math_min_chunk_rows() -> int:
347
+ """Row alignment AND floor of the chunked deterministic math prefill,
348
+ from config/vendors/<vendor>/<arch>.yml `memory.sdpa_math_min_chunk_rows`
349
+ (the kernel's row block); 0 (absent key / no profile) = the chunked
350
+ route never fires, like `sdpa_math_max_chunks` (R23)."""
351
+ try:
352
+ return int(_core._arch_memory_param("sdpa_math_min_chunk_rows", 0))
353
+ except Exception:
354
+ return 0
355
+
356
+
357
+ def _sdpa_math_max_chunks() -> int:
358
+ """Chunk-count ceiling for the CHUNKED deterministic math prefill
359
+ (P-NONDET-LONG-ROW fix). Data-driven from
360
+ config/vendors/<vendor>/<arch>.yml `memory.sdpa_math_max_chunks`;
361
+ 0 (absent key / no profile) = the chunked route never fires, so an
362
+ unconfigured arch keeps its attention path byte-unchanged (R23).
363
+ The ceiling is the determinism tax bound: LLM-class prefills chunk
364
+ a handful of times under the 2 GiB scores budget (4k ctx → 2,
365
+ 16k → 16); video-scale prefills would chunk 30-500+ times at a
366
+ prohibitive cost and stay on their existing path, REGISTERED as
367
+ the residual non-deterministic class on this hardware."""
368
+ try:
369
+ return int(_core._arch_memory_param("sdpa_math_max_chunks", 0))
370
+ except Exception:
371
+ return 0
372
+
373
+
374
+ def _math_attention(q, k, v, attn_mask=None, is_causal=False, scale=None):
375
+ """Math-decomposed attention — deterministic for any head_dim.
376
+
377
+ Used for non-power-of-2 head_dim (PixArt 72, Sana 112) where the
378
+ flash kernel's masked-load path is non-deterministic on Volta SIMT,
379
+ and for asymmetric head_dim attention (MLA class — DeepSeek-V2
380
+ qk_head_dim 192 / v_head_dim 128), which the flash kernel cannot
381
+ express (single BLOCK_HEADDIM constexpr, q-shaped output).
382
+
383
+ Shapes: q [B, H, T_q, D], k [B, H_k, T_k, D], v [B, H_k, T_k, D_v]
384
+ (D_v may differ from D). Returns [B, H, T_q, D_v].
385
+
386
+ Algorithm:
387
+ scores = Q @ K^T * scale (bmm)
388
+ scores = scores + mask (additive bias when needed)
389
+ p = softmax(scores, dim=-1) (per-row softmax)
390
+ out = p @ V (bmm)
391
+
392
+ GQA: when nheads != nheads_k, K and V are repeated to match Q heads
393
+ via expand+reshape (zero-copy view). Same pattern flash uses
394
+ internally via GQA_GROUPS, but materialized here since math path
395
+ has no constexpr GQA hook.
396
+ """
397
+ import math as _math
398
+ B, H, T_q, D = q.shape
399
+ H_k = k.shape[1]
400
+ T_k = k.shape[2]
401
+ # Value head_dim is carried INDEPENDENTLY of q/k head_dim. MLA-class
402
+ # attention (DeepSeek-V2: qk_head_dim 192 = 128 nope + 64 rope,
403
+ # v_head_dim 128) is asymmetric — reusing q's D for the v reshape
404
+ # built a numel-mismatched view (NBXTensor.view re-strides without
405
+ # numel validation → silent out-of-bounds read) and returned a
406
+ # q-shaped [B, H, T_q, 192] output that the baked downstream views
407
+ # re-inferred into heads*192 = 3072 columns → aten.mm
408
+ # "Incompatible dimensions: 3072 vs 2048"
409
+ # (DeepSeek-Coder-V2-Lite aten.mm::3, P-TRITON-MLA).
410
+ D_v = v.shape[3]
411
+ # q @ k^T requires matching q/k head dims. Since NBXTensor.view /
412
+ # reshape never validate numel, a mismatch here would be a SILENT
413
+ # out-of-bounds read — fail loudly instead.
414
+ assert k.shape[3] == D, (
415
+ f"math attention: q head_dim {D} != k head_dim {k.shape[3]}")
416
+
417
+ if scale is None:
418
+ scale = 1.0 / _math.sqrt(D)
419
+
420
+ # GQA WITHOUT BROADCASTING K/V.
421
+ #
422
+ # The old form expanded K and V up to Q's head count and
423
+ # materialised the result — the whole KV cache, per layer, per
424
+ # token. Measured at a 4,164-token context that was 34 MB per call,
425
+ # 4.9 GB per decode step for K and the same again for V, and it is
426
+ # the dominant term in our context scaling: `strided_copy` fitted at
427
+ # 11.9 us per context token against `baddbmm`'s 3.8.
428
+ #
429
+ # The algebra never needed it. Q's heads can be grouped INSIDE each
430
+ # KV head instead: with H = H_k * groups the flat order of
431
+ # [B, H, T_q, D] is already b, h_k, g, t, d, so viewing it as
432
+ # [B*H_k, groups*T_q, D] is a pure view, and batched against
433
+ # [B*H_k, T_k, D] it computes exactly the same dot products.
434
+ #
435
+ # For plain MHA groups == 1, BH == B*H and M == T_q, so every
436
+ # expression below is literally the previous one — MHA is unchanged
437
+ # by construction, not by luck.
438
+ groups = H // H_k
439
+ assert H_k * groups == H, f"GQA: H={H} not a multiple of H_k={H_k}"
440
+ BH = B * H_k
441
+ M = groups * T_q
442
+
443
+ def _mask_to_grouped(m2d):
444
+ """[T_q, T_k] -> [BH, M, T_k], row (g, t) taking mask row t."""
445
+ if groups == 1:
446
+ return m2d.unsqueeze(0).expand(BH, M, T_k)
447
+ return (m2d.unsqueeze(0).expand(groups, T_q, T_k)
448
+ .reshape(M, T_k).unsqueeze(0).expand(BH, M, T_k))
449
+
450
+ # Reshape to 3D for bmm
451
+ q_3d = q.reshape(BH, M, D)
452
+ # Q @ K^T without materialising K^T.
453
+ #
454
+ # This used to be `k.transpose(-2,-1).contiguous().reshape(...)`,
455
+ # which copied the whole K tile on every layer of every decode step
456
+ # to serve a single-token query — on the canonical 30B row, 48 x
457
+ # [32, 256, 128] per token. `baddbmm_kernel` takes B's strides and
458
+ # can walk the transposed view directly. Measured on that exact
459
+ # shape: BIT-IDENTICAL result, 0.519 ms -> 0.122 ms (x4.26), which
460
+ # over 48 layers is ~19 ms per token.
461
+ #
462
+ # Order matters: reshape FIRST on the contiguous k (merging B and H,
463
+ # a pure view), THEN transpose. Transposing first and reshaping
464
+ # after would send `reshape` through its own materialisation and put
465
+ # the copy straight back.
466
+ # merge_dims keeps the (B, H) merge a PURE VIEW even when K/V are
467
+ # the bucketed cache's prefix-slice views (non-contiguous in the T
468
+ # dim — a plain reshape materialised them here, copying the whole
469
+ # padded K and V per layer per decode step; copies-at-source census
470
+ # 2026-08-23). Both bmms below walk B's strides.
471
+ k_3d_t = k.merge_dims(0, 1).transpose(-2, -1)
472
+ v_3d = v.merge_dims(0, 1)
473
+
474
+ # scores = q @ k^T (bmm returns fp32 on V100 via force_fp32)
475
+ scores = _la.bmm(q_3d, k_3d_t, allow_strided_b=True) # [BH, M, T_k]
476
+ scores = _ew.mul(scores, float(scale))
477
+
478
+ # Causal/explicit mask handling — additive bias on scores.
479
+ # Built via existing tril-based path; skipped for non-causal no-mask.
480
+ if is_causal and attn_mask is None:
481
+ device_idx = q._device_idx if hasattr(q, '_device_idx') else 0
482
+ bias = _ew._get_causal_bias(device_idx, T_q, T_k, scores.nbx_dtype)
483
+ # bias [T_q, T_k] -> [BH, M, T_k], grouped like the scores
484
+ scores = _ew.add(scores, _mask_to_grouped(bias))
485
+ elif attn_mask is not None:
486
+ # attn_mask may be [T_q, T_k], [B, T_q, T_k], or [B, H, T_q, T_k].
487
+ if attn_mask.ndim == 2:
488
+ mask_3d = _mask_to_grouped(attn_mask)
489
+ elif attn_mask.ndim == 3:
490
+ if attn_mask.shape[0] == 1 and B > 1:
491
+ attn_mask = attn_mask.expand(B, T_q, T_k)
492
+ mask_3d = (attn_mask.unsqueeze(1).unsqueeze(1)
493
+ .expand(B, H_k, groups, T_q, T_k).reshape(BH, M, T_k))
494
+ elif attn_mask.ndim == 4:
495
+ mask_3d = (attn_mask.expand(B, H, T_q, T_k)
496
+ .reshape(B, H_k, groups, T_q, T_k).reshape(BH, M, T_k))
497
+ else:
498
+ raise RuntimeError(f"unsupported attn_mask.ndim={attn_mask.ndim}")
499
+ if mask_3d.nbx_dtype != scores.nbx_dtype:
500
+ mask_3d = mask_3d.to(scores.nbx_dtype)
501
+ scores = _ew.add(scores, mask_3d.contiguous())
502
+
503
+ # softmax along K dim — kernel handles fp16/fp32 internally,
504
+ # returns same dtype as input. Score is fp32 from bmm so softmax
505
+ # output is fp32.
506
+ p = softmax(scores, dim=-1)
507
+ # Fully-masked-row guard — parity with PyTorch's fused SDPA backends
508
+ # (memory-efficient / flash), which NeuroBrix compiled mode uses as the
509
+ # numerical oracle. A query row whose every key is masked (additive bias
510
+ # = -inf on ALL keys — e.g. the Open-Sora HunyuanVAE mid-block attention
511
+ # passes an all-(-inf) bias) makes softmax(all -inf) = NaN in this
512
+ # plain-softmax math path, whereas the efficient backend emits 0 for such
513
+ # a row. Scrub NaN -> 0 so triton math attention matches compiled and the
514
+ # NaN does not blacken the whole VAE. In this fp32 score path (bmm is
515
+ # fp32-accumulated on V100, softmax of any finite row is finite) a NaN can
516
+ # ONLY originate from a fully-masked row, so this is inert — bit-identical
517
+ # — for every attention that has no fully-masked row. Mirror of the flash
518
+ # kernel's fully-masked-row guard in kernels/ops/flash_attention.py (the
519
+ # two SDPA paths, math and flash, stay symmetric under R30).
520
+ # The scores die at the softmax and the probabilities are cleaned in place: at most two [B*H, Tq, Tk] buffers
521
+ # live where four did (the scaled scores, the probabilities and their cleaned copy, all held to the function's
522
+ # end) — Wan2.2-I2V-A14B's cross-attention asked a third 731 MB block beside 1.3 GB of them on a V100-16GB
523
+ # (2026-10-28). Same kernels, same values.
524
+ del scores
525
+ p = _ew.nan_to_num_wrapper(p, nan=0.0, inplace=True)
526
+ # Cast back to value dtype for the second bmm
527
+ if p.nbx_dtype != v_3d.nbx_dtype:
528
+ p = p.to(v_3d.nbx_dtype)
529
+
530
+ # out = p @ v (bmm again returns fp32 on V100). Strided B: v_3d may
531
+ # be the merged bucketed-cache view (row pitch = buffer capacity).
532
+ out_3d = _la.bmm(p, v_3d, allow_strided_b=True) # [BH, M, D_v]
533
+ del p
534
+ if out_3d.nbx_dtype != q.nbx_dtype:
535
+ out_3d = out_3d.to(q.nbx_dtype)
536
+
537
+ return out_3d.reshape(B, H, T_q, D_v)
538
+
539
+
540
+ def _math_attention_chunked(q, k, v, attn_mask, is_causal, scale,
541
+ chunk_rows):
542
+ """Deterministic math attention for scores tensors OVER the budget —
543
+ the P-NONDET-LONG-ROW fix (2026-08-23).
544
+
545
+ The long-context prefill routed to the flash kernel (its full fp32
546
+ scores exceed `sdpa_math_max_scores_bytes`), and on Volta the flash
547
+ kernel's masked-load specialisation — which the exact D>=128 pad-
548
+ by-one detour lands in — is NON-DETERMINISTIC (kernel-level: 3
549
+ distinct outputs in 5 identical calls at the 4,164-token prefill
550
+ shape; end-to-end: the first replayed-step logits differed across
551
+ every pair of runs, and greedy near-ties downstream made the row's
552
+ output bimodal). Forcing math made 4/4 runs byte-identical at
553
+ every step.
554
+
555
+ This runs the SAME `_math_attention` (deterministic by
556
+ construction) over query-row chunks, so no chunk's scores tensor
557
+ exceeds the budget. Causality is preserved exactly: the full
558
+ causal bias is built once and each chunk receives its row slice as
559
+ an explicit additive mask — the same values `_math_attention`
560
+ would have added row-by-row unchunked.
561
+ """
562
+ B, H, T_q, _D = q.shape
563
+ T_k = k.shape[2]
564
+ D_v = v.shape[3]
565
+ device_idx = q._device_idx if hasattr(q, '_device_idx') else 0
566
+ out = NBXTensor.empty((B, H, T_q, D_v), dtype=q.nbx_dtype,
567
+ device=f"cuda:{device_idx}")
568
+ bias_full = None
569
+ if is_causal and attn_mask is None:
570
+ bias_full = _ew._get_causal_bias(device_idx, T_q, T_k,
571
+ NBXDtype.float32)
572
+ # ZERO FALLBACK: `chunk_rows` is the TilingEngine's size
573
+ # (`tiling_sizes.sdpa_chunk_rows`), fixed BEFORE the launch. An allocation
574
+ # the driver refuses is an error naming the key (op, shape, rows, bytes
575
+ # asked) and is re-raised — never a smaller retry, which would run a chunk
576
+ # count the plan and the launch key never saw.
577
+ q0 = 0
578
+ while q0 < T_q:
579
+ c = min(chunk_rows, T_q - q0)
580
+ q_c = q[:, :, q0:q0 + c, :].contiguous()
581
+ if bias_full is not None:
582
+ mask_c = bias_full[q0:q0 + c, :]
583
+ elif attn_mask is None:
584
+ mask_c = None
585
+ elif attn_mask.ndim == 2:
586
+ mask_c = attn_mask[q0:q0 + c, :]
587
+ elif attn_mask.ndim == 3:
588
+ mask_c = (attn_mask if attn_mask.shape[1] == 1
589
+ else attn_mask[:, q0:q0 + c, :])
590
+ else:
591
+ mask_c = (attn_mask if attn_mask.shape[2] == 1
592
+ else attn_mask[:, :, q0:q0 + c, :])
593
+ try:
594
+ o = _math_attention(q_c, k, v, attn_mask=mask_c,
595
+ is_causal=False, scale=scale)
596
+ except DeviceOOMError as _oom:
597
+ _asked = _ts.sdpa_scores_bytes(B, H, c, T_k)
598
+ raise DeviceOOMError(
599
+ f"chunked SDPA: q [{B}, {H}, {T_q}, {_D}] k/v [{B}, {k.shape[1]}, {T_k}, {D_v}], "
600
+ f"{chunk_rows} rows a chunk (tiling_sizes.sdpa_chunk_rows), chunk at row {q0}: "
601
+ f"{_asked} bytes of fp32 scores asked on cuda:{device_idx}, refused by the "
602
+ f"allocator ({_oom}). The row count is the plan's; it is not retried smaller.",
603
+ requested=_oom.requested, device_idx=_oom.device_idx, live=_oom.live,
604
+ pool_cached=_oom.pool_cached, pool_blocks=_oom.pool_blocks,
605
+ driver_free=_oom.driver_free, driver_total=_oom.driver_total) from _oom
606
+ out[:, :, q0:q0 + c, :] = o
607
+ q0 += c
608
+ return out
609
+
610
+
611
+ def paged_kv_append(new, pool, slot_mapping):
612
+ """Write new positions into a layer's paged pool (state engine step 2, ops/paged_decode_attn.py).
613
+
614
+ new [N_tok, H_kv, D] (any float dtype, last dim contiguous) · pool [num_pages, H_kv, page_tokens, D] · slot_mapping
615
+ [N_tok] int32 (core/state/pages.PageTable.slots: page * page_tokens + position in page). Cast to the pool's dtype on
616
+ store. Returns the pool."""
617
+ from nbxtensor.ops.paged_decode_attn import paged_kv_append_kernel
618
+ n_tok, h_kv, d = (int(x) for x in new.shape)
619
+ if tuple(pool.shape[1:2]) != (h_kv,) or int(pool.shape[3]) != d:
620
+ raise ValueError(f"paged_kv_append: new {list(new.shape)} does not fit the pool {list(pool.shape)}")
621
+ if slot_mapping.nbx_dtype != NBXDtype.int32 or int(slot_mapping.numel()) != n_tok:
622
+ raise ValueError(f"paged_kv_append: slot_mapping must be int32 [{n_tok}], got {slot_mapping.nbx_dtype} "
623
+ f"{list(slot_mapping.shape)}")
624
+ if new.stride(2) != 1 or pool.stride(3) != 1:
625
+ raise ValueError("paged_kv_append: the head dim must be contiguous in the new positions and in the pool")
626
+ if n_tok == 0:
627
+ return pool
628
+ set_device(pool)
629
+ paged_kv_append_kernel[(n_tok, h_kv)](
630
+ new, pool, slot_mapping, int(pool.shape[2]),
631
+ new.stride(0), new.stride(1),
632
+ pool.stride(0), pool.stride(1), pool.stride(2),
633
+ BLOCK_D=max(triton.next_power_of_2(d), _fa_min_tile()), D=d,
634
+ num_warps=1)
635
+ return pool
636
+
637
+
638
+ def paged_decode_attention(q, k_pool, v_pool, block_table, seq_lens, softmax_scale, max_seq_len, q_round=None):
639
+ """Decode attention (one query position) read through the page table — the vector split-KV kernel of
640
+ ops/decode_attn_vec.py on paged K / V, merged by the fixed-order `flash_decode_reduce_kernel`.
641
+
642
+ q [B, H, 1, D] contiguous · k_pool / v_pool [num_pages, H_kv, page_tokens, D] · block_table [B, max_pages] int32
643
+ (-1 past each sequence) · seq_lens [B] int32 · max_seq_len: the longest of seq_lens (sizes the splits; read by the
644
+ caller from its page tables, never from the device) — the position tile is the key's certified configuration among
645
+ PAGED_DECODE_TILE_CANDIDATES dividing page_tokens · q_round: the dtype Q is read in (the cache's), rounded on load
646
+ as `_decode_attn_vec` does; the output is written in it. Output [B, H, 1, D_v] in q_round, else q's dtype."""
647
+ from nbxtensor.ops.paged_decode_attn import (paged_decode_attn_split_kernel, PAGED_DECODE_TILE_CANDIDATES,
648
+ paged_decode_splits, PAGED_DECODE_SPLIT_ALIGN)
649
+ batch, nheads, tq, headdim = (int(x) for x in q.shape)
650
+ num_pages, nheads_k, page_tokens, d_k = (int(x) for x in k_pool.shape)
651
+ d_v = int(v_pool.shape[3])
652
+ if tq != 1 or d_k != headdim or nheads % nheads_k:
653
+ raise ValueError(f"paged_decode_attention: q {list(q.shape)} against the pool {list(k_pool.shape)}")
654
+ if page_tokens % PAGED_DECODE_SPLIT_ALIGN:
655
+ raise ValueError(f"ZERO FALLBACK: the page of {page_tokens} positions is not a whole number of the largest tile "
656
+ f"{PAGED_DECODE_SPLIT_ALIGN} ({PAGED_DECODE_TILE_CANDIDATES}) — a tile would straddle two pages")
657
+ for name, t in (("block_table", block_table), ("seq_lens", seq_lens)):
658
+ if t.nbx_dtype != NBXDtype.int32:
659
+ raise ValueError(f"paged_decode_attention: {name} must be int32, got {t.nbx_dtype}")
660
+ if int(block_table.shape[0]) != batch or int(seq_lens.numel()) != batch:
661
+ raise ValueError(f"paged_decode_attention: block_table {list(block_table.shape)} / seq_lens "
662
+ f"{list(seq_lens.shape)} for a batch of {batch}")
663
+ groups = nheads // nheads_k
664
+ bhq = batch * nheads
665
+ BLOCK_D = max(triton.next_power_of_2(max(headdim, d_v)), _fa_min_tile())
666
+ # The vector decode kernel's split shape (ops/paged_decode_attn.py), each split a whole number of tiles so a split
667
+ # never starts inside a tile.
668
+ longest = max(int(max_seq_len), 1)
669
+ split = paged_decode_splits(longest)
670
+
671
+ q2 = q.reshape(bhq, headdim)
672
+ dev = f"cuda:{q._device_idx}"
673
+ opart = NBXTensor.empty((bhq, split, d_v), dtype=NBXDtype.float32, device=dev)
674
+ mpart = NBXTensor.empty((bhq, split), dtype=NBXDtype.float32, device=dev)
675
+ lpart = NBXTensor.empty((bhq, split), dtype=NBXDtype.float32, device=dev)
676
+ out_dtype = q_round if q_round is not None else q.nbx_dtype
677
+ q_to = _TL_DTYPE[q_round] if q_round is not None else None
678
+ q_saturate = (q_round == NBXDtype.float16
679
+ and q.nbx_dtype in (NBXDtype.float32, NBXDtype.float64, NBXDtype.bfloat16))
680
+ out = NBXTensor.empty((bhq, 1, d_v), dtype=out_dtype, device=dev)
681
+ set_device(q)
682
+ paged_decode_attn_split_kernel[(bhq, split)](
683
+ q2, k_pool, v_pool, block_table, seq_lens,
684
+ opart, mpart, lpart,
685
+ batch, page_tokens, split, float(softmax_scale), nheads,
686
+ q2.stride(0),
687
+ k_pool.stride(0), k_pool.stride(1), k_pool.stride(2),
688
+ v_pool.stride(0), v_pool.stride(1), v_pool.stride(2),
689
+ block_table.stride(0),
690
+ opart.stride(0), opart.stride(1),
691
+ mpart.stride(0),
692
+ GQA_GROUPS=groups, BLOCK_D=BLOCK_D, D=headdim, D_V=d_v, SPLIT_ALIGN=PAGED_DECODE_SPLIT_ALIGN,
693
+ Q_TO=q_to, Q_SATURATE=q_saturate)
694
+ decode_reduce(opart, mpart, lpart, out)
695
+ return out.reshape(batch, nheads, 1, d_v)
696
+
697
+
698
+ def decode_reduce(opart, mpart, lpart, out):
699
+ """The fixed-order combine of a split decode (`flash_decode_reduce_kernel`), one program per query row viewed at
700
+ GROUPS=1: partials opart [rows, split, D_v] / mpart, lpart [rows, split] fp32 into out [rows, 1, D_v]; the warps
701
+ are the key's certified configuration (`launch_keys.decode_reduce_launch`)."""
702
+ from nbxtensor.ops.flash_decode import flash_decode_reduce_kernel
703
+ rows, split, d_v = (int(x) for x in opart.shape)
704
+ flash_decode_reduce_kernel[(rows,)](
705
+ opart, mpart, lpart, out,
706
+ opart.stride(0), opart.stride(1), d_v,
707
+ mpart.stride(0), mpart.stride(1),
708
+ out.stride(0), out.stride(1),
709
+ SPLIT=split, GROUPS=1, BLOCK_G=max(16, _fa_min_tile()),
710
+ BLOCK_D=max(triton.next_power_of_2(d_v), _fa_min_tile()), D=d_v)
711
+
712
+
713
+ def _decode_attn_vec(q, k, v, bias, softmax_scale,
714
+ batch, nheads, nheads_k, seqlen_k, headdim, q_round=None):
715
+ """Vector (SIMT) decode attention launch pair — see
716
+ ops/decode_attn_vec.py for the sourced structure (R16 2026-08-23:
717
+ FasterTransformer mmha / vLLM paged v2 / llama.cpp fattn-vec /
718
+ lightllm — no tl.dot on sm_70, one query row per program, split-KV
719
+ with the existing fixed-order reduce).
720
+
721
+ q [B, H, 1, D] fp16 CONTIGUOUS (wrapper entry) · k/v in native
722
+ cache layout, consumed BY THEIR OWN STRIDES (the bucketed cache's
723
+ prefix-slice views flow in unmaterialised — the strided-KV lot's
724
+ contract) · bias flat [T_k] or None.
725
+ """
726
+ from nbxtensor.ops.decode_attn_vec import decode_attn_vec_split_kernel, decode_vec_splits
727
+ groups = nheads // nheads_k
728
+ BHq = batch * nheads
729
+ D_v = v.shape[3]
730
+ # The floor is the backend capability, not 16: a dot tile dimension the
731
+ # attention lowering refuses is not a tile this path can use. Inert on
732
+ # cuda and hip, where it reads 16.
733
+ BLOCK_D = max(triton.next_power_of_2(max(headdim, D_v)), _fa_min_tile())
734
+ # The split (`decode_vec_splits`) is the algorithm's; the key block and the warps are each key's certified
735
+ # configuration.
736
+ split = decode_vec_splits(seqlen_k)
737
+ seg_len = int(triton.cdiv(seqlen_k, split))
738
+
739
+ q2 = q.reshape(BHq, headdim) # contiguous -> pure view
740
+ # `q_round`: the dtype the caller asked Q to be read in (the KV cache's);
741
+ # the kernel rounds Q to it on load and the output is written in it — the
742
+ # bytes of a cast copy of Q, without the copy.
743
+ out_dtype = q_round if q_round is not None else q.nbx_dtype
744
+ q_to = _TL_DTYPE[q_round] if q_round is not None else None
745
+ q_saturate = (q_round == NBXDtype.float16
746
+ and q.nbx_dtype in (NBXDtype.float32, NBXDtype.float64, NBXDtype.bfloat16))
747
+
748
+ dev = f"cuda:{q._device_idx}"
749
+ opart = NBXTensor.empty((BHq, split, D_v), dtype=NBXDtype.float32,
750
+ device=dev)
751
+ mpart = NBXTensor.empty((BHq, split), dtype=NBXDtype.float32,
752
+ device=dev)
753
+ lpart = NBXTensor.empty((BHq, split), dtype=NBXDtype.float32,
754
+ device=dev)
755
+ out = NBXTensor.empty((BHq, 1, D_v), dtype=out_dtype, device=dev)
756
+
757
+ set_device(q)
758
+ decode_attn_vec_split_kernel[(BHq, split)](
759
+ q2, k, v,
760
+ bias if bias is not None else q2, # dead pointer when HAS_BIAS=0
761
+ opart, mpart, lpart,
762
+ seqlen_k, seg_len, _lk.bucket_of("N", seqlen_k), float(softmax_scale),
763
+ q2.stride(0),
764
+ k.stride(1), k.stride(2),
765
+ v.stride(1), v.stride(2),
766
+ opart.stride(0), opart.stride(1),
767
+ mpart.stride(0),
768
+ GQA_GROUPS=groups, BLOCK_D=BLOCK_D,
769
+ D=headdim, D_V=D_v,
770
+ HAS_BIAS=bias is not None,
771
+ Q_TO=q_to, Q_SATURATE=q_saturate)
772
+ # Fixed-order deterministic combine — the EXISTING flash_decode
773
+ # reduce kernel viewed at GROUPS=1 (partials [BHq, split, 1, D_v]).
774
+ decode_reduce(opart, mpart, lpart, out)
775
+ return out.reshape(batch, nheads, 1, D_v)
776
+
777
+
778
+ def _flash_m8n8k4(q, k, v, bias, softmax_scale, gqa_groups, tile, has_bias, split=False):
779
+ """Flash forward on the m8n8k4 matrix unit (ops/flash_attention_m8n8k4.py); `bias` is the wrapper's
780
+ memory-resident [B, H, Sq, Sk] bias view (unit stride on the key axis), not read when `has_bias` is False (the
781
+ zero bias of a call with neither mask nor causality). `split`: the operands carried as hi + lo (the DtypeEngine's
782
+ "split" representation, `launch_keys.unit_flash`), `tile` then a `matrix_unit.fp32_split.flash` row whose
783
+ `block_k` head-dim columns the unit sums before each round-to-nearest add."""
784
+ from nbxtensor.ops.flash_attention_m8n8k4 import flash_attention_m8n8k4_kernel, head_parts
785
+ from nbxtensor.ops.mma_m8n8k4 import UNIT
786
+ from nbxtensor.dtype.operands import matrix_unit_split_scale # the library's operand rules
787
+ batch, nheads, seqlen_q, headdim = q.shape
788
+ seqlen_k = k.shape[2]
789
+ DA, DB = head_parts(headdim)
790
+ hi_exp, lo_shift = matrix_unit_split_scale(_matrix_unit())
791
+ ksa = min(tile["block_k"], DA) // UNIT["k"] if split else 1 # instructions per round-to-nearest add
792
+ ksb = min(tile["block_k"], DB) // UNIT["k"] if split and DB else 1
793
+ o = NBXTensor.empty_like(q)
794
+ # the LSE rows rounded to the largest query tile a candidate takes (the tile is the certified one of this key)
795
+ seqlen_q_rounded = math.ceil(seqlen_q / 128) * 128
796
+ lse = NBXTensor.empty((batch, nheads, seqlen_q_rounded), dtype=NBXDtype.float32,
797
+ device=f"cuda:{q._device_idx}" if hasattr(q, '_device_idx') else 'cuda')
798
+ set_device(q)
799
+ grid = lambda META: (triton.cdiv(seqlen_q, 16 * META["MB"]), batch * nheads)
800
+ flash_attention_m8n8k4_kernel[grid](
801
+ q, k, v, bias, o, lse, softmax_scale,
802
+ q.stride(0), q.stride(1), q.stride(2),
803
+ k.stride(0), k.stride(1), k.stride(2),
804
+ v.stride(0), v.stride(1), v.stride(2),
805
+ bias.stride(0), bias.stride(1), bias.stride(-2),
806
+ o.stride(0), o.stride(1), o.stride(2),
807
+ nheads, seqlen_q, seqlen_k, seqlen_q_rounded, headdim,
808
+ _lk.bucket_of("M", seqlen_q), _lk.bucket_of("N", seqlen_k), DA=DA, DB=DB, GQA_GROUPS=gqa_groups, HAS_BIAS=bool(has_bias),
809
+ SPLIT=bool(split), HI_EXP=hi_exp, LO_SHIFT=lo_shift, KSA=ksa, KSB=ksb)
810
+ return o
811
+
812
+
813
+ def scaled_dot_product_attention_wrapper(q, k, v, attn_mask=None,
814
+ dropout_p=0.0, is_causal=False,
815
+ scale=None, k_pre_transposed=None,
816
+ **kwargs):
817
+ """aten::scaled_dot_product_attention via Triton Flash Attention.
818
+
819
+ Args:
820
+ q: (batch, nheads, seqlen_q, headdim)
821
+ k: (batch, nheads_k, seqlen_k, headdim)
822
+ v: (batch, nheads_k, seqlen_k, headdim)
823
+ attn_mask: optional attention mask (bias)
824
+ dropout_p: dropout probability (ignored at inference)
825
+ is_causal: causal masking
826
+ scale: softmax scale (default: 1/sqrt(headdim))
827
+
828
+ Reference: Dao-AILab _flash_attn_forward signature and grid computation.
829
+ """
830
+ import math
831
+ from nbxtensor.ops.flash_attention import flash_attention_forward_kernel
832
+
833
+ set_device(q)
834
+ # A scale is a number: a numpy scalar (np.float64) carries a `.dtype`, and the autotuner — Triton's key and
835
+ # `autotune_cache.key_of` alike — appends the dtype of every argument that has one, so the kernel's key grew a
836
+ # 'float64' no certified entry can hold and the certified lookup refused it (the decode / flash cells, 2026-10-29)
837
+ if scale is not None:
838
+ scale = float(scale)
839
+
840
+ # Fix pre-transposed K from graph math decomposition path.
841
+ #
842
+ # PyTorch's SDPA math path does bmm(Q, K.transpose(-2,-1)). When the
843
+ # tracer captures this, the graph stores K already transposed to
844
+ # (batch, heads, headdim, seq). Our Flash Attention kernel expects K
845
+ # in standard (batch, heads, seq, headdim) format — same as Q and V.
846
+ #
847
+ # `k_pre_transposed` is the graph's answer, recorded at load by
848
+ # GraphExecutor._mark_sdpa_k_layout and passed down by the dispatcher.
849
+ # It exists because the shape test below cannot decide the square case:
850
+ # when the sequence length equals the head dimension both layouts have
851
+ # the same shape, and the test then declined to transpose and attention
852
+ # ran with K's axes crossed. Measured 2026-09-07 on Apple — TinyLlama,
853
+ # head_dim 64, a 64-token prompt: argmax 29892 at logit 6.41 where the
854
+ # float64 oracle says 3864 at 22.36, |delta| 23.27 across the vocabulary,
855
+ # with 63 and 65 tokens exact to 0.02. The correction is inert wherever
856
+ # the graph is unambiguous, which is every other length.
857
+ if k.ndim == 4 and q.ndim == 4:
858
+ if k_pre_transposed is not None:
859
+ if k_pre_transposed:
860
+ k = k.transpose(2, 3).contiguous()
861
+ elif (k.shape[2] == q.shape[3] and k.shape[3] == q.shape[2]
862
+ and k.shape[2] == q.shape[2]):
863
+ raise RuntimeError(
864
+ f"scaled_dot_product_attention: K is square "
865
+ f"{tuple(k.shape)} with seq_len == head_dim, so its layout "
866
+ f"cannot be read from its shape, and no k_pre_transposed was "
867
+ f"passed. Refusing to guess.")
868
+ elif (k.shape[2] == q.shape[3] # K's "seq" dim == Q's headdim
869
+ and k.shape[3] == q.shape[2]): # K's "dim" dim == Q's seqlen
870
+ k = k.transpose(2, 3).contiguous()
871
+
872
+ # Input shapes — ATen convention: (batch, heads, seq, dim)
873
+ batch = q.shape[0]
874
+ nheads = q.shape[1]
875
+ seqlen_q = q.shape[2]
876
+ headdim = q.shape[3]
877
+ seqlen_k = k.shape[2]
878
+ nheads_k = k.shape[1]
879
+
880
+ # Handle is_causal as tensor or bool
881
+ if hasattr(is_causal, 'item'):
882
+ is_causal = bool(is_causal.item()) if hasattr(is_causal, 'numel') and is_causal.numel() == 1 else bool(is_causal)
883
+ if hasattr(dropout_p, 'item'):
884
+ dropout_p = float(dropout_p.item()) if hasattr(dropout_p, 'numel') and dropout_p.numel() == 1 else float(dropout_p)
885
+
886
+ # When is_causal=True with an explicit attn_mask, drop the mask —
887
+ # the causal bias materializer below covers the causal semantics
888
+ # uniformly through memory.
889
+ if is_causal and attn_mask is not None:
890
+ attn_mask = None
891
+ # The former heuristic "2D square mask with seqlen_q == seqlen_k →
892
+ # assume causal tril" has been removed. The kernel is now strictly
893
+ # bias-driven (no IS_CAUSAL constexpr path), and any causal pattern
894
+ # arrives via memory through the cache below.
895
+
896
+ # Default scale
897
+ if scale is None:
898
+ softmax_scale = 1.0 / math.sqrt(headdim)
899
+ else:
900
+ softmax_scale = float(scale) if not isinstance(scale, float) else scale
901
+
902
+ # GQA is handled inside the kernel via GQA_GROUPS — no materialization.
903
+ # off_h_kv = off_h // GQA_GROUPS picks the right K/V head for each Q head,
904
+ # so K/V keep their native (b, nheads_k, s, d) layout. For plain MHA
905
+ # (nheads == nheads_k), gqa_groups=1 and off_h_kv == off_h (zero cost).
906
+ # Avoids the per-call expand+reshape+contiguous that copied K and V in
907
+ # full every decode step (~7 MB/step on TinyLlama, >>100 MB at 2k ctx).
908
+ gqa_groups = nheads // nheads_k
909
+ assert nheads_k * gqa_groups == nheads, (
910
+ f"nheads ({nheads}) must be a multiple of nheads_k ({nheads_k})")
911
+
912
+ # Q is materialised unconditionally (tiny at decode; graph-contiguous
913
+ # at prefill, so free). K and V are NOT: the bucketed KV cache hands
914
+ # out prefix-slice views `buffer[:, :, :padded, :]` that are
915
+ # non-contiguous in the T dim, and the unconditional `.contiguous()`
916
+ # here copied the ENTIRE padded K and V per layer per decode step —
917
+ # 24 MB/token at bucket 256, scaling with context (copies-at-source
918
+ # census, 2026-08-23). The math path (decode's route) now consumes
919
+ # the views directly (`merge_dims` + strided-B bmm); the flash paths
920
+ # materialise at their own entry below, exactly as before.
921
+ q = q.contiguous()
922
+
923
+ # ---- FLASH-DECODING (T_q == 1) ------------------------------------
924
+ #
925
+ # Decode used to fall through to the math budget below, which at
926
+ # T_q=1 always chose `_math_attention` — bmm/softmax/bmm with
927
+ # materialised scores, the largest single kernel of the captured
928
+ # step at a working context (9.3 ms/token at 4,164, linear in T_k).
929
+ #
930
+ # This routes single-query attention to the split-KV flash-decoding
931
+ # pair instead: the KV sequence is split across programs so the SMs
932
+ # fill even at batch 1, each split combines under online softmax,
933
+ # and a fixed-order second pass merges the splits — deterministic by
934
+ # construction (replay verifies plans byte-equal, so run-to-run
935
+ # variance would be disqualifying).
936
+ #
937
+ # Routing is correctness-preserving: any shape or mask this path
938
+ # does not model falls through to the existing paths unchanged. The
939
+ # decode mask from the bucketed KV cache is additive [1,1,1,T_k];
940
+ # anything not flattenable to [T_k] falls through.
941
+ # DEFAULT OFF, by measurement (2026-08-22): correct and deterministic
942
+ # (oracle green at D=127/128/129), but SLOWER than the math path on
943
+ # this card at real bucket sizes — paired, machine exclusive:
944
+ # 256-bucket 9.074 -> 8.167 tok/s (-10.0%)
945
+ # 4,352-bucket 7.568 -> 6.512 (-14.0%) best tuning 7.419
946
+ # 8,448-bucket 2.747 -> 2.841 (+3.4%, n=1)
947
+ # VERDICT CORRECTED (2026-08-23, supervisor): the math path is not
948
+ # "full", it is SLOW — at a 4,164 context it reads ~409 MB of KV in
949
+ # 9.3 ms = 44 GB/s = 5% of the 900 GB/s HBM. The split pair lost to
950
+ # it anyway (its own overheads are larger still at these buckets).
951
+ # RE-JUDGED under the pinned protocol (2026-08-23, bench_row.py,
952
+ # interleaved arms, n=5, published ffn-only artifact, at the 93%-GPU
953
+ # step where a GPU-side change is no longer masked by host time —
954
+ # HEAD 5cc7362):
955
+ # default cfg (BN=128): 4,164 ctx x0.560 (-44.0%) · short x0.754
956
+ # — BLOCK_N=128 is the pathology (the 128x128 double-buffered
957
+ # fp16 tile chokes Volta smem/occupancy: 2-6 ms/call isolated).
958
+ # best cfg (BN=64, SEG=512, from a 10-config isolated sweep):
959
+ # 4,164 ctx x0.857 (-14.3%) · short x0.968 (-3.2%), no overlap.
960
+ # The kernel REPLAYS and graph-captures fine (plan frozen, verified
961
+ # byte-equal), and its ROW OUTPUTS were byte-equal to the math
962
+ # path's (every armed rep reproduced the math arm's majority sha
963
+ # over 60 greedy tokens) — the loss is its own GPU time: ~0.47
964
+ # ms/call = ~22.7 ms/token vs the in-engine autotuned math band's
965
+ # ~14 ms. The
966
+ # isolated math figure (1.47 ms/call) was 5x SLOWER than in-engine
967
+ # — eager wrapper overhead + cold autotune; the FD isolated figures
968
+ # transferred. Third refusal, first two protocol-grade.
969
+ # THE BAR STANDS: ~0.5 ms per token at 4k context (the KV bytes at
970
+ # ~900 GB/s). This split-KV shape cannot reach it on sm_70 (fp32
971
+ # partials + reduce pass + 16-row G-tile carrying 4 real rows, no
972
+ # cp.async pipeline) — reaching the bar needs a different kernel,
973
+ # a named follow-up, not a tuning of this one.
974
+ import os as _os_fd
975
+ # NBX_DECODE_VEC three-state contract: "1" = ARMED here regardless
976
+ # of the math routing below (diagnostics + the oracle, which runs
977
+ # in profile-less processes where the budget is 0); unset = the
978
+ # kernel is the DEFAULT Tq=1 refinement INSIDE the math route
979
+ # below; "0" = kill switch everywhere.
980
+ # The KV cache asks for Q in the cache's dtype (`q_dtype_of_kv`): the vector
981
+ # decode kernel rounds Q to it on load (the stored cast's protected
982
+ # conversion, then the same fp32 math), so the per-layer cast copy of Q
983
+ # (TinyLlama: 22 a token) is gone on the decode route; every other route
984
+ # casts once, here below, where it diverges from the vec kernel.
985
+ q_round = None # an NBXDtype (`.dtype` is the Triton element type)
986
+ if kwargs.get("q_dtype_of_kv") and q._dtype != k._dtype:
987
+ q_round = k._dtype
988
+
989
+ def _q_cast(t):
990
+ return t.to(q_round) if (q_round is not None and t._dtype != q_round) else t
991
+
992
+ if (seqlen_q == 1 and q._device != "cpu"
993
+ and _os_fd.environ.get("NBX_DECODE_VEC", "") == "1"):
994
+ _dv_out = _ew._try_decode_vec(q, k, v, attn_mask, softmax_scale,
995
+ batch, nheads, nheads_k, seqlen_k,
996
+ headdim, q_round=q_round)
997
+ if _dv_out is not None:
998
+ return _dv_out
999
+
1000
+ # The kernels require matching operand dtypes; the fp32-stored GEMMs (Volta's store policy,
1001
+ # M<=4 decode) can leave q/k/v mixed (Sana: fp32 Q vs fp16 K; Allegro: fp32 V). If they
1002
+ # disagree, all three take the narrowest — the compiled engine's alignment (R30). For the
1003
+ # common case where all three match, this is a no-op — zero overhead.
1004
+ _qd, _kd, _vd, q_round = _lk.sdpa_operand_dtypes(q._dtype, k._dtype, v._dtype, q_round)
1005
+ if _qd != q._dtype or _kd != k._dtype or _vd != v._dtype:
1006
+ q, k, v = q.to(_qd), k.to(_kd), v.to(_vd)
1007
+
1008
+ # Deterministic-attention routing (P-TRITON-MOE-DETERMINISM-RESIDUAL,
1009
+ # decided on hardware and memory budget alone (option B), ZERO
1010
+ # model/family knowledge — R34 strict; the SDPA wrapper is a
1011
+ # universal runtime primitive).
1012
+ #
1013
+ # The Dao-AILab flash Triton kernel is non-deterministic on
1014
+ # pre-Ampere SIMT (Volta sm_70 / Turing sm_75):
1015
+ # - documented for the masked-load path (EVEN_HEADDIM=False,
1016
+ # head_dim < BLOCK_HEADDIM, e.g. PixArt 72 / Sana 112): "race
1017
+ # conditions on non-64/128 head dimensions"; 5 consecutive
1018
+ # calls yield 5 different outputs (DiT h=126,127 banding).
1019
+ # - EMPIRICALLY confirmed for power-of-2 head_dim=128 too
1020
+ # (Qwen3-30B-A3B GQA/fp32: §5.8 op-by-op full-hash differential
1021
+ # — pos 0..74 byte-identical across 3 runs, first divergence
1022
+ # SDPA::0 with bit-identical inputs; 3 runs → 3 distinct
1023
+ # outputs). So `_is_power_of_2(headdim)` was an INCOMPLETE
1024
+ # guard — pow2 hd=128 is not safe on Volta.
1025
+ #
1026
+ # `_math_attention` (Q@K^T→softmax→@V; plain bmm+softmax, no
1027
+ # online-softmax accumulation, no MMA reorder across K-tiles) is
1028
+ # deterministic by construction. Counterfactual proof:
1029
+ # NBX_FORCE_MATH_ATTENTION=1 → Qwen3-30B::triton 3 runs
1030
+ # BYTE-IDENTICAL. Cost: a [B*H,Tq,Tk] scores tensor — negligible
1031
+ # for LLM small-T, ~1 GiB+ for image-diffusion large-T.
1032
+ #
1033
+ # Routing (universal technical signals only):
1034
+ # - Ampere+ (sm_80+, has_native_bf16): flash unchanged — flash is
1035
+ # deterministic there (R23: zero non-Volta regression).
1036
+ # - non-pow2 head_dim: _math_attention, UNCONDITIONAL on ALL
1037
+ # hardware — EXACT prior behaviour preserved (the old guard was
1038
+ # `if not _is_power_of_2(headdim): return _math_attention`,
1039
+ # hardware-independent). R23 strict: zero non-Volta regression,
1040
+ # PixArt hd=72 / Sana hd=112 path byte-unchanged everywhere.
1041
+ # - pow2 head_dim + scores_bytes ≤ vendor-yml budget:
1042
+ # _math_attention — NEW (Qwen3 LLM fix), the ONLY added
1043
+ # routing. The hardware gate IS the data-driven per-arch
1044
+ # budget: only config/vendors/nvidia/volta.yml defines
1045
+ # `memory.sdpa_math_max_scores_bytes`; ampere.yml / hopper.yml
1046
+ # / cdna.yml have no such key → budget 0 → scores_bytes ≤ 0 is
1047
+ # False → flash unchanged (R23: flash is deterministic on
1048
+ # Ampere+; zero non-Volta regression, no ambiguous capability
1049
+ # proxy). Budget-gated so a pow2 + large-T tensor never OOMs:
1050
+ # it stays on flash, residual non-determinism documented +
1051
+ # trackable via NBX_OP_FINGERPRINT.
1052
+ # NBX_FORCE_MATH_ATTENTION=1 — retained diagnostic (same class as
1053
+ # NBX_DISABLE_AUTOTUNE / NBX_DUMP_TIDS): force the deterministic
1054
+ # path regardless of hardware/headdim.
1055
+ import os as _os_fma
1056
+ _use_math_forced = _os_fma.environ.get("NBX_FORCE_MATH_ATTENTION") == "1"
1057
+ headdim_v = v.shape[3]
1058
+ # The route is `launch_keys.sdpa_route` — the function the derived census keys with: math
1059
+ # when forced or when the value head dim differs; else by the fp32 scores' size against this
1060
+ # device's budget (a non-power-of-two head dim routes on `sdpa.non_pow2_head_scores_bytes`
1061
+ # when the arch declares no budget); over it, chunked when the rows fit the arch's ceiling; otherwise flash.
1062
+ _q_dev_idx = getattr(q, "_device_idx", None)
1063
+ _route, _chunk_rows = _lk.sdpa_route(
1064
+ batch, nheads, seqlen_q, seqlen_k, headdim, headdim_v,
1065
+ _sdpa_math_scores_budget_bytes_for(_q_dev_idx), _sdpa_math_min_chunk_rows(),
1066
+ _sdpa_math_max_chunks(), force_math=_use_math_forced,
1067
+ unit_flash=_lk.unit_flash_takes(headdim, q._dtype if q_round is None else q_round, k._dtype, v._dtype))
1068
+ _use_math = {"math": True, "chunked": "chunked", "flash": False}[_route]
1069
+ if _os_fma.environ.get("NBX_SDPA_ROUTE_DIAG") == "1":
1070
+ # Print once PER DISTINCT EXECUTING DEVICE, not once per process:
1071
+ # on heterogeneous rigs the budget is per-device, so a single line
1072
+ # (which lands on whichever device runs the first SDPA) cannot
1073
+ # show the routing on the other memory class. dev_idx is the
1074
+ # activation instrument — None here means the call site failed to
1075
+ # extract the device index and the per-device cap silently fell
1076
+ # back to the per-arch base budget.
1077
+ _diag_idx = getattr(q, "_device_idx", None)
1078
+ _seen = getattr(scaled_dot_product_attention_wrapper,
1079
+ "_route_diag_seen", None)
1080
+ if _seen is None:
1081
+ _seen = set()
1082
+ scaled_dot_product_attention_wrapper._route_diag_seen = _seen
1083
+ if _diag_idx not in _seen:
1084
+ _seen.add(_diag_idx)
1085
+ _pr = _core.get_hardware_profile()
1086
+ _dv = getattr(_pr, "devices", None) if _pr is not None else None
1087
+ print(f"[NBX_SDPA_ROUTE_DIAG] hd={headdim} hdv={headdim_v} pow2={_ew._is_power_of_2(headdim)} "
1088
+ f"B={batch} H={nheads} Tq={seqlen_q} Tk={seqlen_k} "
1089
+ f"scores_bytes={batch*nheads*seqlen_q*seqlen_k*4} "
1090
+ f"dev_idx={_diag_idx} qtype={type(q).__name__} "
1091
+ f"budget={_sdpa_math_scores_budget_bytes_for(_diag_idx)} "
1092
+ f"profile={'None' if _pr is None else getattr(_pr,'vendor','?')+'/'+(getattr(_dv[0],'architecture','?') if _dv else '?')} "
1093
+ f"use_math={_use_math}", flush=True)
1094
+ if _use_math == "chunked":
1095
+ return _math_attention_chunked(_q_cast(q), k, v, attn_mask, is_causal,
1096
+ softmax_scale, _chunk_rows)
1097
+ if _use_math:
1098
+ # ---- VECTOR (SIMT) DECODE ATTENTION — DEFAULT on the math
1099
+ # route at Tq == 1 (ADOPTED 2026-08-23, three-outcome verdict
1100
+ # under the locked pinned protocol, interleaved arms, n=5, no
1101
+ # overlap, outputs byte-identical to the math chain at all
1102
+ # three contexts):
1103
+ # short (256 bucket) 21.538 -> 22.759 tok/s (+5.7%)
1104
+ # 4,164-token context 17.033 -> 19.868 (+16.6%)
1105
+ # ~8,200-token context 13.043 -> 16.972 (+30.1%)
1106
+ # The gain grows with T, as the KV-streaming structure predicts
1107
+ # (see ops/decode_attn_vec.py for the sourced design). Scoped
1108
+ # INSIDE the math route: archs whose vendor yml defines no math
1109
+ # budget never reach here at pow2 dims (R23 — non-Volta
1110
+ # attention unchanged). NBX_FORCE_MATH_ATTENTION=1 keeps its
1111
+ # exact meaning (the pure bmm+softmax chain) and bypasses this.
1112
+ # NBX_DECODE_VEC=0 is the kill switch.
1113
+ if (seqlen_q == 1 and not _use_math_forced
1114
+ and _os_fd.environ.get("NBX_DECODE_VEC", "") != "0"):
1115
+ _dv_out = _ew._try_decode_vec(q, k, v, attn_mask, softmax_scale,
1116
+ batch, nheads, nheads_k,
1117
+ seqlen_k, headdim, q_round=q_round)
1118
+ if _dv_out is not None:
1119
+ return _dv_out
1120
+ return _math_attention(_q_cast(q), k, v, attn_mask=attn_mask,
1121
+ is_causal=is_causal, scale=softmax_scale)
1122
+
1123
+ # Flash path from here on: flat-indexed kernel — materialise the K/V
1124
+ # views that the math path above consumes strided (see the entry
1125
+ # comment). Contiguous tensors short-circuit at zero cost.
1126
+ q = _q_cast(q)
1127
+ k = k.contiguous()
1128
+ v = v.contiguous()
1129
+
1130
+ # ---- P-FLASH-D128-CORRECTNESS: exact detour around a wrong kernel ----
1131
+ #
1132
+ # The Triton flash kernel returns WRONG answers when headdim is a
1133
+ # power of two >= 128 — measured against a float64 reference:
1134
+ #
1135
+ # headdim 127 -> 5.0e-04 headdim 128 -> 4.2e-01 (208x bound)
1136
+ # headdim 129 -> 3.1e-04 headdim 256 -> 4.1e-01
1137
+ #
1138
+ # 127 and 128 compile the SAME kernel source with the SAME tile and
1139
+ # the SAME block sizes; the divergence is in Triton's codegen for
1140
+ # that specialisation, and it is a known upstream class (Dao-AILab's
1141
+ # own flash_attn_triton.py: "Triton bug ... we get the wrong
1142
+ # output!", tested only on A100 — we run sm_70). Root cause needs a
1143
+ # version bisect / PTX diff and is tracked separately; it must not
1144
+ # keep a wrong kernel in production in the meantime.
1145
+ #
1146
+ # The detour is ZERO-PADDING the head dimension by one. Padded
1147
+ # components contribute exactly nothing to a dot product, so
1148
+ # Q·K is unchanged, the softmax is unchanged, and the extra output
1149
+ # columns are zero and discarded: the algebra is EXACT, not
1150
+ # approximate. Measured 3.35e-01 -> 5.95e-04, i.e. inside the bound
1151
+ # and equal to the math path's own accuracy.
1152
+ #
1153
+ # NOTE ON THE SCALE, which is the trap here: `softmax_scale` is
1154
+ # computed from the TRUE headdim above and is passed through
1155
+ # unchanged. Letting the kernel derive 1/sqrt(headdim) from the
1156
+ # padded value would silently rescale every score.
1157
+ #
1158
+ # A half-and-half split of the head dimension was evaluated first and
1159
+ # is NOT viable: the softmax needs the COMPLETE score, so two
1160
+ # 64-wide flash calls produce softmaxes of half dot products at the
1161
+ # wrong scale, which no later operation can recombine. Splitting only
1162
+ # works if the scores are materialised first, which is the math path.
1163
+ # The condition is `launch_keys.flash_headdim_detour` — the function the derived census
1164
+ # re-routes the padded call with, so the keys it derives are this detour's.
1165
+ _hd_broken = _lk.flash_headdim_detour(
1166
+ headdim, _os_fd.environ.get("NBX_D128_DETOUR", "1") != "0") != headdim
1167
+ # NBX_D128_DETOUR=0 disables the zero-pad detour — DIAGNOSTIC ONLY:
1168
+ # it re-exposes the wrong kernel. Exists to measure the detour's true
1169
+ # cost (TTFT at long prefill) and for the day the upstream codegen
1170
+ # defect is fixed, when the correctness oracle's _KNOWN_BROKEN
1171
+ # mechanism will demand this whole block be removed.
1172
+ if _hd_broken:
1173
+ _pad = _lk.flash_headdim_detour(headdim) - headdim
1174
+ def _pad_hd(t):
1175
+ z = NBXTensor.zeros(tuple(t.shape[:-1]) + (_pad,),
1176
+ dtype=t._dtype, device=f"cuda:{t._device_idx}")
1177
+ return NBXTensor.cat([t, z], dim=-1).contiguous()
1178
+ _dv = v.shape[-1]
1179
+ _kw = {k_: v_ for k_, v_ in kwargs.items() if k_ != "k_pre_transposed"}
1180
+ out = scaled_dot_product_attention_wrapper(
1181
+ _pad_hd(q), _pad_hd(k), _pad_hd(v), attn_mask=attn_mask,
1182
+ dropout_p=dropout_p, is_causal=is_causal,
1183
+ scale=softmax_scale,
1184
+ # K was normalised above; saying so keeps the inner call from
1185
+ # re-reading a layout off a shape it can no longer interpret.
1186
+ k_pre_transposed=False, **_kw)
1187
+ return out.narrow(-1, 0, _dv).contiguous()
1188
+
1189
+ # Adaptive BLOCK_M (Phase 1): decode-path seqlen_q is typically 1–4. Using
1190
+ # BLOCK_M=128 wastes >99% of the Q tile. 16 is the floor enforced by
1191
+ # tl.dot (TensorCore tile minimum) — below that the kernel crashes.
1192
+ # Block sizes — adapted from Dao-AILab defaults.
1193
+ # Reduce BLOCK_M for large headdim to stay within shared memory limits.
1194
+ # V100 has 96KB shared memory. With headdim=256: 128×256×2×3 = 192KB > 96KB.
1195
+ BLOCK_HEADDIM = max(triton.next_power_of_2(headdim), 16)
1196
+ # The floor is a BACKEND CAPABILITY, not 16. A backend whose generic
1197
+ # attention lowering mis-computes below 32 refuses the kernel, and a
1198
+ # refusal wastes the whole model where a padded tile only wastes half a Q
1199
+ # tile at seqlen_q=1. On cuda and hip this reads 16 and nothing moves.
1200
+ _fa_floor = _fa_min_tile()
1201
+ if seqlen_q <= 16:
1202
+ BLOCK_M = max(16, _fa_floor)
1203
+ BLOCK_N = 64 if BLOCK_HEADDIM < 128 else (64 if BLOCK_HEADDIM < 256 else 32)
1204
+ elif BLOCK_HEADDIM >= 512:
1205
+ # PixArt VAE on V100. (32,32) for h=512 needs 131KB SMEM > 96KB
1206
+ # opt-in. (16,16) measured 49KB → fits with margin. Inlined for
1207
+ # zero Python overhead vs Layer 6's data-driven approach.
1208
+ BLOCK_M = 16
1209
+ BLOCK_N = 16
1210
+ elif BLOCK_HEADDIM >= 256:
1211
+ BLOCK_M = 32
1212
+ BLOCK_N = 32
1213
+ elif BLOCK_HEADDIM >= 128:
1214
+ BLOCK_M = 64
1215
+ BLOCK_N = 64
1216
+ else:
1217
+ # head_dim < 128 (the common case: hd=64 LLMs, hd=64 video DiTs).
1218
+ # BLOCK_M=128 is the Dao A100 default; on pre-Ampere (Volta sm_70 /
1219
+ # Turing sm_75: 96 KB SMEM, fewer registers) a 128-row Q tile spills
1220
+ # registers and serialises — arch-gate to a smaller tile. Data-driven
1221
+ # via the hardware capability flag (same single source of truth as
1222
+ # the dtype-protection path); Ampere+ keeps 128. NBX_FLASH_BLOCK_M_VOLTA
1223
+ # overrides the Volta value (diagnostic / per-arch tuning).
1224
+ if not _core._NBX_HAS_NATIVE_BF16:
1225
+ # Measured on V100 (sm_70), seq=8192 hd=64: BLOCK_M 128 -> 9.2 s,
1226
+ # 64 -> 6.1 s, 32 -> 0.42 s. The 128-row Q tile spills registers to
1227
+ # local memory (DRAM); 64 cuts that ~1.5x and stays NUMERICALLY
1228
+ # CORRECT (CogVideoX-2b/5b triton produce a coherent fox). 32 is
1229
+ # ~14x faster but WRONG — it diverges (max|diff| 0.035 vs torch
1230
+ # SDPA, vs 0.0002 at 64/128) and the error compounds across the
1231
+ # diffusion loop into pure noise. So 64 is the floor of
1232
+ # correctness on Volta. NBX_FLASH_BLOCK_M_VOLTA overrides (do not
1233
+ # set below 64 for accuracy-sensitive / iterative models).
1234
+ BLOCK_M = int(os.environ.get("NBX_FLASH_BLOCK_M_VOLTA", "64"))
1235
+ else:
1236
+ BLOCK_M = 128
1237
+ BLOCK_N = 64
1238
+
1239
+ # Clamp to what the HARDWARE PROFILE allows for this shape.
1240
+ #
1241
+ # The cascade above proposes; the profile is the ceiling. It stays a
1242
+ # clamp and never an instruction because the cascade carries measurements
1243
+ # the profiles do not — BLOCK_M=64 is the floor of CORRECTNESS on Volta
1244
+ # at head_dim<128, where volta.yml declares 128 — and a ceiling cannot
1245
+ # undo that.
1246
+ #
1247
+ # Why it is needed at all: every branch above decides on
1248
+ # `_NBX_HAS_NATIVE_BF16`, which separates Volta from Ampere and means
1249
+ # nothing off CUDA. On an Apple GPU the cascade therefore took the
1250
+ # Ampere branch and asked for a 128-row Q tile — four times the 32 that
1251
+ # apple_silicon.yml has declared since it was written, over the 32 KB
1252
+ # threadgroup budget in the same file, and past what the Metal backend
1253
+ # will lower at all. The profile had the answer; nothing read it.
1254
+ #
1255
+ # MEASURED 2026-09-06: no NVIDIA tile changes (min(64,128)=64 on Volta,
1256
+ # min(128,128)=128 on Hopper), so the CUDA rows are untouched.
1257
+ _ceil_m, _ceil_n = _sdpa_block_ceiling(seqlen_q, headdim)
1258
+ if _ceil_m:
1259
+ BLOCK_M = min(BLOCK_M, int(_ceil_m))
1260
+ if _ceil_n:
1261
+ BLOCK_N = min(BLOCK_N, int(_ceil_n))
1262
+ # The ceiling clamps, the floor holds — and the floor is applied LAST.
1263
+ #
1264
+ # The ceiling is a RESOURCE bound read from the profile; the floor is a
1265
+ # CORRECTNESS bound read from the backend, below which the attention
1266
+ # lowering refuses (or, worse, would mis-compute). min(32, 16) = 16 is
1267
+ # exactly what happened on whisper's decode probe: the wrapper's floor
1268
+ # raised BLOCK_M to 32 and the Apple profile's `block_m: 16` decode row
1269
+ # clamped it straight back down into the refusal. Below the floor there
1270
+ # is nothing to run, so a profile row under it is a CONFLICT — obeyed
1271
+ # nowhere, and said once, because a profile that promises a tile the
1272
+ # backend refuses should be heard about and fixed, not silently outrun.
1273
+ if BLOCK_M < _fa_floor or BLOCK_N < _fa_floor:
1274
+ _clamped = (BLOCK_M, BLOCK_N)
1275
+ BLOCK_M = max(BLOCK_M, _fa_floor)
1276
+ BLOCK_N = max(BLOCK_N, _fa_floor)
1277
+ _core._announce_floor_over_ceiling(_clamped, (BLOCK_M, BLOCK_N), _fa_floor)
1278
+
1279
+ # Bias handling — always memory-resident.
1280
+ # The kernel only accepts BIAS_TYPE in {"vector", "matrix"} and the
1281
+ # IS_CAUSAL constexpr is gone. Every tensor reaching tl.dot must
1282
+ # arrive via tl.load to guarantee Triton compiles a single MMA path.
1283
+ # Three sub-cases below all converge to BIAS_TYPE="matrix" with a
1284
+ # memory-resident bias.
1285
+ device_idx = q._device_idx if hasattr(q, '_device_idx') else 0
1286
+ _bias_read = attn_mask is not None or bool(is_causal) # else the zero base below: nothing to add
1287
+ if attn_mask is not None:
1288
+ if attn_mask.ndim == 2:
1289
+ attn_mask = attn_mask.unsqueeze(0).unsqueeze(0).expand(
1290
+ batch, nheads, seqlen_q, seqlen_k
1291
+ )
1292
+ elif attn_mask.ndim == 3:
1293
+ if attn_mask.shape[0] == 1 and batch > 1:
1294
+ attn_mask = attn_mask.expand(batch, seqlen_q, seqlen_k)
1295
+ attn_mask = attn_mask.unsqueeze(1).expand(
1296
+ batch, nheads, seqlen_q, seqlen_k
1297
+ )
1298
+ elif attn_mask.ndim == 4:
1299
+ attn_mask = attn_mask.expand(batch, nheads, seqlen_q, seqlen_k)
1300
+ # Pass the bias as a BROADCAST view: keep stride-0 on every axis the
1301
+ # mask is constant over (H and/or the query axis for a key-padding
1302
+ # mask). The kernel reads the bias through its strides, so a key-pad
1303
+ # mask stays a few-MB [.., Sk] buffer (stride_bm=0) instead of a
1304
+ # materialized [B,H,Sq,Sk] matrix — for video self-attention that
1305
+ # matrix is tens of GB (Mochi [2,24,~16k,~16k] ≈ 25 GB). The previous
1306
+ # .contiguous() on a [B,1,1,Sk] mask kept unit-size H/Sq dims with
1307
+ # NON-ZERO contiguous strides (=Sk), which the kernel then indexed
1308
+ # PAST at off_h<nheads / offs_m<seqlen_q → out-of-bounds illegal
1309
+ # access (Mochi triton "error 700"). The bias is still memory-resident
1310
+ # (a real tl.load, bit-equivalent — only the strides change). Only
1311
+ # materialize if the key axis is not unit-stride (kernel reads offs_n
1312
+ # with implicit stride 1).
1313
+ bias = attn_mask if attn_mask.stride(-1) == 1 else attn_mask.contiguous()
1314
+ elif is_causal:
1315
+ causal_base = _ew._get_causal_bias(device_idx, seqlen_q, seqlen_k, q.dtype)
1316
+ bias = causal_base.unsqueeze(0).unsqueeze(0).expand(
1317
+ batch, nheads, seqlen_q, seqlen_k
1318
+ )
1319
+ else:
1320
+ zero_base = _ew._get_zero_bias(device_idx, seqlen_q, seqlen_k, q.dtype)
1321
+ bias = zero_base.unsqueeze(0).unsqueeze(0).expand(
1322
+ batch, nheads, seqlen_q, seqlen_k
1323
+ )
1324
+ bias_type = "matrix"
1325
+ # Causal semantics now live entirely in the materialized bias above —
1326
+ # the kernel's IS_CAUSAL path has been removed. Force False here so
1327
+ # any caller-provided is_causal flag does not reach the kernel
1328
+ # signature (it no longer accepts one anyway, but we keep the local
1329
+ # variable for clarity).
1330
+ is_causal = False
1331
+
1332
+ # The arch's matrix unit, where the profile declares one Triton does not lower `tl.dot` onto
1333
+ # (`matrix_unit`, volta.yml): the flash kernel built on that unit, for the operands the DtypeEngine carries
1334
+ # on it (its operand dtype natively, an `fp32_split` operand as hi + lo) and the head dims its measured rows
1335
+ # cover. Same contract as the kernel below (bias, GQA, LSE, tails).
1336
+ # Taken before the tl.dot kernel's own output, LSE and tile buffers are allocated.
1337
+ _mu_flash = _lk.unit_flash(headdim, q._dtype, k._dtype, v._dtype)
1338
+ if _mu_flash is not None:
1339
+ o = _flash_m8n8k4(q, k, v, bias, softmax_scale, gqa_groups, _mu_flash[1], _bias_read,
1340
+ split=_mu_flash[0] == "split")
1341
+ if attn_mask is not None:
1342
+ o = _ew.nan_to_num_wrapper(o, nan=0.0, posinf=0.0, neginf=0.0)
1343
+ return o
1344
+
1345
+ # The tl.dot flash with its tile CERTIFIED per key (workstream 1, roadmap C3) where the profile declares no hand
1346
+ # table of tiles (`launch_keys.flash_tiles_certified`): the autotuned handle of the same kernel, keyed on the
1347
+ # buckets of the query rows and the keys and on the head dim (`launch_keys.flash_launches`, the census's key);
1348
+ # the log-sum-exp and its scratch are sized for the tallest candidate tile, so every candidate's rows fit.
1349
+ if _lk.flash_tiles_certified():
1350
+ from nbxtensor.ops.flash_attention import flash_attention_certified_kernel, flash_configs
1351
+ o = NBXTensor.empty_like(q)
1352
+ _rows_top = max(c.kwargs["BLOCK_M"] for c in flash_configs())
1353
+ seqlen_q_rounded = math.ceil(seqlen_q / _rows_top) * _rows_top
1354
+ _dev = f"cuda:{q._device_idx}" if hasattr(q, "_device_idx") else "cuda"
1355
+ lse = NBXTensor.empty((batch, nheads, seqlen_q_rounded), dtype=NBXDtype.float32, device=_dev)
1356
+ tmp = NBXTensor.empty((batch, nheads, seqlen_q_rounded), dtype=NBXDtype.float32, device=_dev)
1357
+ set_device(q)
1358
+ flash_attention_certified_kernel[lambda META: (triton.cdiv(seqlen_q, META["BLOCK_M"]), batch * nheads)](
1359
+ q, k, v, bias,
1360
+ o, lse, tmp,
1361
+ softmax_scale,
1362
+ q.stride(0), q.stride(1), q.stride(2),
1363
+ k.stride(0), k.stride(1), k.stride(2),
1364
+ v.stride(0), v.stride(1), v.stride(2),
1365
+ bias.stride(0), bias.stride(1), bias.stride(-2),
1366
+ o.stride(0), o.stride(1), o.stride(2),
1367
+ nheads, seqlen_q, seqlen_k, seqlen_q_rounded, headdim,
1368
+ _bucket_of("M", seqlen_q), _bucket_of("N", seqlen_k),
1369
+ BIAS_TYPE=bias_type,
1370
+ BLOCK_HEADDIM=BLOCK_HEADDIM,
1371
+ GQA_GROUPS=gqa_groups,
1372
+ )
1373
+ if attn_mask is not None:
1374
+ o = _ew.nan_to_num_wrapper(o, nan=0.0, posinf=0.0, neginf=0.0)
1375
+ return o
1376
+
1377
+ # Output allocation. seqlen_q_rounded must align with actual BLOCK_M.
1378
+ o = NBXTensor.empty_like(q)
1379
+ seqlen_q_rounded = math.ceil(seqlen_q / BLOCK_M) * BLOCK_M
1380
+ lse = NBXTensor.empty((batch, nheads, seqlen_q_rounded), dtype=NBXDtype.float32,
1381
+ device=f"cuda:{q._device_idx}" if hasattr(q, '_device_idx') else 'cuda')
1382
+ tmp = NBXTensor.empty((batch, nheads, seqlen_q_rounded), dtype=NBXDtype.float32,
1383
+ device=f"cuda:{q._device_idx}" if hasattr(q, '_device_idx') else 'cuda')
1384
+
1385
+ grid = (triton.cdiv(seqlen_q, BLOCK_M), batch * nheads)
1386
+
1387
+ # Launch meta (num_warps / num_stages). Triton auto-selects when not
1388
+ # given; on Volta the auto-pick for a small Q tile under-parallelises the
1389
+ # softmax reduction (the BLOCK_M=32 wrongness probe). NBX_FLASH_NUM_WARPS
1390
+ # / NBX_FLASH_NUM_STAGES let us pin them for diagnosis / per-arch tuning.
1391
+ _flash_launch_meta = {}
1392
+ # The arch profile's row for this shape: its stated warps (a row without them is an error), and on an FMA-path row (Volta/Turing
1393
+ # under Triton >= 3.3) the head-dim chunk the QK^T contraction is done in. No chunk: held Q.
1394
+ _fa_meta = _sdpa_launch_meta(seqlen_q, headdim)
1395
+ QK_CHUNK = min(_fa_meta["qk_chunk"], BLOCK_HEADDIM) if "qk_chunk" in _fa_meta else 0
1396
+ if _fa_meta:
1397
+ _flash_launch_meta["num_warps"] = _fa_meta["num_warps"]
1398
+ _fnw = os.environ.get("NBX_FLASH_NUM_WARPS")
1399
+ if _fnw:
1400
+ _flash_launch_meta["num_warps"] = int(_fnw)
1401
+ _fns = os.environ.get("NBX_FLASH_NUM_STAGES")
1402
+ if _fns:
1403
+ _flash_launch_meta["num_stages"] = int(_fns)
1404
+
1405
+ # Ensure CUDA runtime is on the correct device before kernel launch
1406
+ set_device(q)
1407
+
1408
+ # The tile is offered only if the DEVICE says it can hold it.
1409
+ #
1410
+ # Everything above proposes on `_NBX_HAS_NATIVE_BF16`, which separates Volta
1411
+ # from Ampere and says nothing else — so sm_86 takes the branch written for
1412
+ # sm_80 and asks a 128-row Q tile of a card declaring 99 KB where sm_80
1413
+ # declares 163. That is what stopped every language model on an A40: 164 352
1414
+ # bytes requested against 101 376 available, the same number in both Triton
1415
+ # modes and unmoved by sequence length. The clamp above could not catch it
1416
+ # either: `sdpa_thresholds` exists only in the seven profiles we ship, so for
1417
+ # any card whose profile we do not ship it returns (None, None) and clamps
1418
+ # nothing.
1419
+ #
1420
+ # The cost is READ FROM THE COMPILER, never estimated: `prepare()` returns
1421
+ # the shared memory Triton computed for this exact specialisation. An
1422
+ # analytic formula is a guess — one written while diagnosing this said
1423
+ # 112 KB for a Volta tile that runs in 96, and pruning on that would have
1424
+ # deleted a configuration that works. One compile per candidate, which the
1425
+ # autotuner already pays, and a dict lookup on every launch after.
1426
+ _smem_budget = _nbx_launcher.max_shared_memory_per_block()
1427
+ if _smem_budget is not None:
1428
+ _probe_args = (
1429
+ q, k, v, bias, o, lse, tmp, softmax_scale,
1430
+ q.stride(0), q.stride(1), q.stride(2),
1431
+ k.stride(0), k.stride(1), k.stride(2),
1432
+ v.stride(0), v.stride(1), v.stride(2),
1433
+ bias.stride(0), bias.stride(1), bias.stride(-2),
1434
+ o.stride(0), o.stride(1), o.stride(2),
1435
+ nheads, seqlen_q, seqlen_k, seqlen_q_rounded, headdim,
1436
+ seqlen_q // 32, seqlen_k // 32,
1437
+ )
1438
+ _probe_kw = dict(BIAS_TYPE=bias_type, BLOCK_HEADDIM=BLOCK_HEADDIM,
1439
+ GQA_GROUPS=gqa_groups, QK_CHUNK=QK_CHUNK, **_flash_launch_meta)
1440
+
1441
+ # `prepare` binds a JITFunction; the kernel is wrapped in
1442
+ # @triton.heuristics, whose only job is to add constexprs. Unwrap to
1443
+ # the function and apply those heuristics exactly as Heuristics.run
1444
+ # does, so the probe compiles the SAME specialisation the launch will.
1445
+ _inner, _heur = flash_attention_forward_kernel, {}
1446
+ while hasattr(_inner, "values") and hasattr(_inner, "fn"):
1447
+ _heur.update(_inner.values)
1448
+ _inner = _inner.fn
1449
+
1450
+ def _tile_cost(_t):
1451
+ _kw = dict(_probe_kw, BLOCK_M=_t[0], BLOCK_N=_t[1])
1452
+ if _heur:
1453
+ _named = dict(zip(_inner.arg_names, _probe_args))
1454
+ for _v, _h in _heur.items():
1455
+ _kw[_v] = _h({**_named, **_kw})
1456
+ _prep, _ = _nbx_launcher.prepare(_inner, _probe_args, _kw)
1457
+ return int(_prep.shared)
1458
+
1459
+ # The shrink stops at the backend's floor, not at 16: a candidate the
1460
+ # backend refuses is not a candidate.
1461
+ _cands, _m = [(BLOCK_M, BLOCK_N)], BLOCK_M
1462
+ while _m > max(16, _fa_min_tile()):
1463
+ _m //= 2
1464
+ _cands.append((_m, BLOCK_N))
1465
+ try:
1466
+ _sel = _largest_tile_within_smem(_cands, _tile_cost, _smem_budget)
1467
+ except Exception as _e:
1468
+ # A probe that cannot run changes nothing — but it SAYS SO. A guard
1469
+ # that degrades in silence is the defect this whole change is about.
1470
+ print(f"[flash] shared-memory probe unavailable ({type(_e).__name__}: "
1471
+ f"{_e}); the tile is not checked against the device", flush=True)
1472
+ _sel = (BLOCK_M, BLOCK_N)
1473
+ if _sel != (BLOCK_M, BLOCK_N):
1474
+ print(f"[flash] tile {(BLOCK_M, BLOCK_N)} needs "
1475
+ f"{_tile_cost((BLOCK_M, BLOCK_N))} bytes of shared memory and this "
1476
+ f"device declares {_smem_budget} — using {_sel}", flush=True)
1477
+ BLOCK_M, BLOCK_N = _sel
1478
+ # Everything downstream that was derived from the proposed tile.
1479
+ seqlen_q_rounded = math.ceil(seqlen_q / BLOCK_M) * BLOCK_M
1480
+ _dev = f"cuda:{q._device_idx}" if hasattr(q, "_device_idx") else "cuda"
1481
+ lse = NBXTensor.empty((batch, nheads, seqlen_q_rounded),
1482
+ dtype=NBXDtype.float32, device=_dev)
1483
+ tmp = NBXTensor.empty((batch, nheads, seqlen_q_rounded),
1484
+ dtype=NBXDtype.float32, device=_dev)
1485
+ grid = (triton.cdiv(seqlen_q, BLOCK_M), batch * nheads)
1486
+
1487
+ flash_attention_forward_kernel[grid](
1488
+ q, k, v, bias,
1489
+ o, lse, tmp,
1490
+ softmax_scale,
1491
+ q.stride(0), q.stride(1), q.stride(2),
1492
+ k.stride(0), k.stride(1), k.stride(2),
1493
+ v.stride(0), v.stride(1), v.stride(2),
1494
+ bias.stride(0), bias.stride(1), bias.stride(-2),
1495
+ o.stride(0), o.stride(1), o.stride(2),
1496
+ nheads, seqlen_q, seqlen_k, seqlen_q_rounded, headdim,
1497
+ seqlen_q // 32, seqlen_k // 32, # cache keys
1498
+ BIAS_TYPE=bias_type,
1499
+ BLOCK_HEADDIM=BLOCK_HEADDIM,
1500
+ BLOCK_M=BLOCK_M,
1501
+ BLOCK_N=BLOCK_N,
1502
+ GQA_GROUPS=gqa_groups,
1503
+ QK_CHUNK=QK_CHUNK,
1504
+ **_flash_launch_meta,
1505
+ )
1506
+ # Fully-masked-row guard at the SDPA-op level — parity with PyTorch's fused
1507
+ # SDPA backends (which NeuroBrix compiled mode uses as the numerical oracle).
1508
+ # A query row whose every key is masked (an additive attn_mask of -inf on ALL
1509
+ # keys — e.g. the Open-Sora-v2 VAE mid-block self-attention) has no finite
1510
+ # score, so the online-softmax flash kernel emits NaN/-inf for that row while
1511
+ # the efficient backend emits 0. Scrub it -> 0 HERE at the wrapper, NOT inside
1512
+ # the kernel: editing the @triton.jit body perturbs Triton register
1513
+ # allocation and changes the bit-exact result of EVERY attention (measured:
1514
+ # TinyLlama greedy output drifted with an in-kernel guard, byte-identical with
1515
+ # this one). Only an explicit additive mask can fully-mask a row (causal /
1516
+ # no-mask attention never does), so gate on attn_mask: inert (skipped) for
1517
+ # causal + plain self-attention, and a value-identity (nan_to_num on a finite
1518
+ # tensor) for any masked attention with no fully-masked row. posinf/neginf ->
1519
+ # 0 so a fully-masked row that resolves to -inf rather than NaN is also
1520
+ # zeroed. Behaviour-symmetric (R30) with the math path guard in
1521
+ # _math_attention.
1522
+ if attn_mask is not None:
1523
+ o = _ew.nan_to_num_wrapper(o, nan=0.0, posinf=0.0, neginf=0.0)
1524
+ return o
1525
+
1526
+
1527
+ # ---------------------------------------------------------------------------
1528
+ # The cost the math attention declares (M3, docs/nbxtensor/COST.md): its buffers in its order, read from the decisions
1529
+ # it takes (the GEMM family's `linalg_wrappers.bmm_cost`, the element-wise and softmax declarations of `_cost`). A
1530
+ # causal bias is the wrapper's cache (`_get_causal_bias`, built once per shape and kept): not this call's.
1531
+ # ---------------------------------------------------------------------------
1532
+
1533
+ def math_attention_cost(q, k, v, mask=None, is_causal=False, native_bf16=None, force_accum=None,
1534
+ contract_half=None, unit=None):
1535
+ """`_math_attention` on q [B, H, Tq, D], k [B, Hk, Tk, D], v [B, Hk, Tk, Dv] and an optional additive `mask`
1536
+ (`_cost.Spec`s; q as the wrapper hands it, contiguous): the scores' product (`bmm_cost`, k walked by its strides),
1537
+ the scale (a new buffer, the product dropped), the bias added (a mask broadcast to the grouped scores and made
1538
+ dense at the scores' dtype, then the sum; the causal bias is the cached one), the softmax (`_cost.softmax`), the
1539
+ scores dropped, the probabilities cleaned in place, cast to the value's dtype when they differ, the output's
1540
+ product (`bmm_cost`), the probabilities dropped, the output cast to q's dtype when they differ."""
1541
+ from nbxtensor.ops import _cost as C
1542
+ from nbxtensor.ops import linalg_wrappers as _lw
1543
+ from nbxtensor.shape import contiguous_strides
1544
+ B, H, Tq, D = (int(d) for d in q.shape)
1545
+ Hk, Tk, Dv = int(k.shape[1]), int(k.shape[2]), int(v.shape[3])
1546
+ groups = H // Hk
1547
+ BH, M = B * Hk, groups * Tq
1548
+ st = C._Steps()
1549
+ held = {}
1550
+
1551
+ def bind(name, index):
1552
+ old = held.get(name)
1553
+ held[name] = index
1554
+ if old is not None and old not in held.values():
1555
+ st.free(old)
1556
+
1557
+ def drop(name):
1558
+ old = held.pop(name, None)
1559
+ if old is not None and old not in held.values():
1560
+ st.free(old)
1561
+
1562
+ facts = dict(native_bf16=native_bf16, force_accum=force_accum, contract_half=contract_half, unit=unit)
1563
+ q3 = C.Spec((BH, M, D), contiguous_strides((BH, M, D)), q.dtype)
1564
+ ks = k.strides
1565
+ k3t = C.Spec((BH, D, Tk), (ks[1], ks[3], ks[2]), k.dtype) # merged (B, Hk), transposed: a view
1566
+ s_cost = _lw.bmm_cost(q3, k3t, allow_strided_b=True, **facts)
1567
+ scores_dt = next(b for b in s_cost.buffers if b.returned)
1568
+ sdt = None
1569
+ for name in ("float32", "float16", "bfloat16", "float64"):
1570
+ if scores_dt.nbytes == BH * M * Tk * dtype_size(NBXDtype[name]):
1571
+ sdt = name
1572
+ break
1573
+ out_idx = st.nest(s_cost, "scores")
1574
+ bind("scores", out_idx[0])
1575
+ sbytes = BH * M * Tk * dtype_size(NBXDtype[sdt])
1576
+ bind("scores", st.alloc("scores scaled", sbytes))
1577
+ if mask is not None and not is_causal:
1578
+ m = st.alloc("mask made dense at the scores' dtype", sbytes)
1579
+ bind("scores", st.alloc("scores with the mask", sbytes))
1580
+ st.free(m)
1581
+ elif is_causal and mask is None:
1582
+ bind("scores", st.alloc("scores with the causal bias", sbytes))
1583
+ p_cost = C.softmax(C.Spec.contiguous((BH, M, Tk), sdt), -1)
1584
+ p_idx = st.nest(p_cost, "softmax")
1585
+ bind("p", p_idx[0])
1586
+ drop("scores")
1587
+ pdt = sdt
1588
+ if pdt != v.dtype:
1589
+ bind("p", st.alloc("probabilities cast to the value's dtype", BH * M * Tk * dtype_size(NBXDtype[v.dtype])))
1590
+ pdt = v.dtype
1591
+ vs = v.strides
1592
+ v3 = C.Spec((BH, Tk, Dv), (vs[1], vs[2], vs[3]), v.dtype)
1593
+ o_cost = _lw.bmm_cost(C.Spec.contiguous((BH, M, Tk), pdt), v3, allow_strided_b=True, **facts)
1594
+ o_idx = st.nest(o_cost, "output")
1595
+ bind("out", o_idx[0])
1596
+ drop("p")
1597
+ odt = next(b for b in o_cost.buffers if b.returned)
1598
+ if odt.nbytes != BH * M * Dv * dtype_size(NBXDtype[q.dtype]):
1599
+ bind("out", st.alloc("output cast to q's dtype", BH * M * Dv * dtype_size(NBXDtype[q.dtype])))
1600
+ out = held.pop("out")
1601
+ st.buffers[out][4] = True
1602
+ st.buffers[out][3] = None
1603
+ return st.done(2 * B * H * Tq * Tk * (D + Dv))
1604
+
1605
+
1606
+ def chunked_math_attention_cost(q, k, v, chunk_rows: int, mask=None, is_causal=False, native_bf16=None,
1607
+ force_accum=None, contract_half=None, unit=None):
1608
+ """`_math_attention_chunked`: the whole output at q's dtype, then per chunk of `chunk_rows` query rows (the last
1609
+ one shorter) the chunk's query made dense and its math attention (`math_attention_cost`, the chunk's mask rows),
1610
+ the chunk's result copied into the output. Python's lifetimes, as the wrapper has them: `q_c` and `o` are rebound,
1611
+ not dropped, so the next chunk's query is made while the previous one is alive, and the previous chunk's result
1612
+ lives until the next chunk's attention has RETURNED — the dearest chunk is the second, not the first (measured: one
1613
+ chunk's result short, 16 384 elements on the test's case)."""
1614
+ from nbxtensor.ops import _cost as C
1615
+ B, H, Tq, D = (int(d) for d in q.shape)
1616
+ Dv = int(v.shape[3])
1617
+ st = C._Steps()
1618
+ st.alloc("output", B * H * Tq * Dv * dtype_size(NBXDtype[q.dtype]), returned=True)
1619
+ rows, q0 = [], 0
1620
+ while q0 < Tq:
1621
+ rows.append(min(int(chunk_rows), Tq - q0))
1622
+ q0 += rows[-1]
1623
+ prev_q, prev_o = None, []
1624
+ for n, c in enumerate(rows):
1625
+ start = sum(rows[:n])
1626
+ # q[:, :, q0:q0 + c, :].contiguous(): a copy unless the slice already lies row-major
1627
+ sliced = C.Spec(tuple((B, H, c, D)), tuple(q.strides), q.dtype)
1628
+ qi = C._contiguous_copy(st, sliced, f"chunk {n}'s query made dense")
1629
+ if prev_q is not None:
1630
+ st.free(prev_q) # the previous q_c, rebound
1631
+ prev_q = qi
1632
+ qc = C.Spec.contiguous((B, H, c, D), q.dtype)
1633
+ if is_causal and mask is None:
1634
+ # the cached causal bias, built once in float32, handed to each chunk as its rows: an explicit mask
1635
+ m = C.Spec.contiguous((c, int(k.shape[2])), "float32")
1636
+ else:
1637
+ m = None if mask is None else C.Spec.contiguous(tuple(mask.shape[:-2]) + (c, int(mask.shape[-1])),
1638
+ mask.dtype)
1639
+ outs = st.nest(math_attention_cost(qc, k, v, m, False, native_bf16, force_accum, contract_half, unit),
1640
+ f"chunk {n} (rows {start}..{start + c})")
1641
+ for i in prev_o:
1642
+ st.free(i) # the previous chunk's result, rebound once this one returned
1643
+ prev_o = outs
1644
+ for i in prev_o:
1645
+ st.free(i)
1646
+ if prev_q is not None:
1647
+ st.free(prev_q)
1648
+ return st.done(2 * B * H * Tq * int(k.shape[2]) * (D + Dv))