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.
- nbxtensor/THIRD_PARTY_NOTICES.md +287 -0
- nbxtensor/__init__.py +17 -0
- nbxtensor/backend/__init__.py +1 -0
- nbxtensor/backend/capabilities.py +207 -0
- nbxtensor/backend/cpu/__init__.py +1 -0
- nbxtensor/backend/cpu/blas.py +102 -0
- nbxtensor/backend/cpu/pool.py +204 -0
- nbxtensor/backend/cpu_backend.py +162 -0
- nbxtensor/backend/device_facts.py +105 -0
- nbxtensor/backend/device_files.py +129 -0
- nbxtensor/backend/device_names.py +18 -0
- nbxtensor/backend/devices/amd/cdna.yml +228 -0
- nbxtensor/backend/devices/amd/cdna2.yml +228 -0
- nbxtensor/backend/devices/amd/cdna3.yml +227 -0
- nbxtensor/backend/devices/apple/apple_m1.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m1_max.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m1_pro.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m1_ultra.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m2.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m2_max.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m2_pro.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m2_ultra.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m3.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m3_max.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m3_pro.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m3_ultra.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m4.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m4_max.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m4_pro.yml +308 -0
- nbxtensor/backend/devices/apple/apple_m5.yml +269 -0
- nbxtensor/backend/devices/apple/apple_m5_max.yml +269 -0
- nbxtensor/backend/devices/apple/apple_m5_pro.yml +269 -0
- nbxtensor/backend/devices/apple/apple_m5_ultra.yml +269 -0
- nbxtensor/backend/devices/apple/apple_m6.yml +269 -0
- nbxtensor/backend/devices/apple/apple_silicon.yml +263 -0
- nbxtensor/backend/devices/nvidia/ampere.yml +214 -0
- nbxtensor/backend/devices/nvidia/hopper.yml +213 -0
- nbxtensor/backend/devices/nvidia/volta.yml +411 -0
- nbxtensor/backend/faults.py +151 -0
- nbxtensor/backend/host_memory.py +185 -0
- nbxtensor/backend/launcher.py +2350 -0
- nbxtensor/backend/launcher_contract.py +429 -0
- nbxtensor/backend/metal_backend.py +661 -0
- nbxtensor/backend/metal_device.py +1274 -0
- nbxtensor/backend/process_memory.py +73 -0
- nbxtensor/backend/replay_graph.py +322 -0
- nbxtensor/backend/runtime.py +409 -0
- nbxtensor/backend/triton_ext_driver.py +735 -0
- nbxtensor/dtype/__init__.py +1 -0
- nbxtensor/dtype/convert.py +107 -0
- nbxtensor/dtype/dtypes.yml +70 -0
- nbxtensor/dtype/itemsize.py +166 -0
- nbxtensor/dtype/nbx_dtype.py +72 -0
- nbxtensor/dtype/operands.py +158 -0
- nbxtensor/dtype/outputs.py +64 -0
- nbxtensor/dtype/policy.py +155 -0
- nbxtensor/dtype/policy.yml +51 -0
- nbxtensor/dtype/promotion.py +112 -0
- nbxtensor/host.py +55 -0
- nbxtensor/io/__init__.py +1 -0
- nbxtensor/io/read_rate.py +197 -0
- nbxtensor/io/safetensors.py +314 -0
- nbxtensor/memory/__init__.py +9 -0
- nbxtensor/memory/allocator.py +1988 -0
- nbxtensor/memory/arena.py +75 -0
- nbxtensor/memory/device_memory.py +150 -0
- nbxtensor/memory/device_transfer.py +154 -0
- nbxtensor/memory/layout.py +43 -0
- nbxtensor/memory/memory_pool.py +191 -0
- nbxtensor/memory/metal.py +96 -0
- nbxtensor/memory/oom_advice.py +91 -0
- nbxtensor/memory/pointer_tables.py +323 -0
- nbxtensor/memory/pool.py +507 -0
- nbxtensor/memory/shadow.py +43 -0
- nbxtensor/memory/slab.py +273 -0
- nbxtensor/memory/trace_replay.py +122 -0
- nbxtensor/memory/traces/sana-video-2b-720p.16g.2026-11-02.tsv +13610 -0
- nbxtensor/memory/vmm.py +118 -0
- nbxtensor/ops/__init__.py +1 -0
- nbxtensor/ops/_autotune_policy.py +90 -0
- nbxtensor/ops/_common.py +32 -0
- nbxtensor/ops/_configs.py +1041 -0
- nbxtensor/ops/_cost.py +231 -0
- nbxtensor/ops/abs.py +17 -0
- nbxtensor/ops/adaptive_avg_pool2d.py +161 -0
- nbxtensor/ops/add.py +96 -0
- nbxtensor/ops/addcdiv.py +29 -0
- nbxtensor/ops/addcmul.py +29 -0
- nbxtensor/ops/addmv_op.py +64 -0
- nbxtensor/ops/addr.py +68 -0
- nbxtensor/ops/all_reduce.py +83 -0
- nbxtensor/ops/amax.py +40 -0
- nbxtensor/ops/any_reduce.py +83 -0
- nbxtensor/ops/arange_op.py +27 -0
- nbxtensor/ops/argmax.py +107 -0
- nbxtensor/ops/argmin.py +93 -0
- nbxtensor/ops/atan2.py +71 -0
- nbxtensor/ops/attention_wrappers.py +1648 -0
- nbxtensor/ops/autotune_bucket.py +137 -0
- nbxtensor/ops/autotune_cache.py +430 -0
- nbxtensor/ops/autotune_certified.py +1262 -0
- nbxtensor/ops/autotune_refusals.py +380 -0
- nbxtensor/ops/autotune_settings.py +58 -0
- nbxtensor/ops/avg_pool2d.py +73 -0
- nbxtensor/ops/baddbmm_op.py +132 -0
- nbxtensor/ops/batch_norm.py +163 -0
- nbxtensor/ops/binary_strided.py +196 -0
- nbxtensor/ops/bitwise_and.py +26 -0
- nbxtensor/ops/bitwise_left_shift.py +47 -0
- nbxtensor/ops/bitwise_not.py +24 -0
- nbxtensor/ops/bitwise_or.py +26 -0
- nbxtensor/ops/bitwise_right_shift.py +47 -0
- nbxtensor/ops/bucketize.py +37 -0
- nbxtensor/ops/cat_op.py +87 -0
- nbxtensor/ops/celu.py +20 -0
- nbxtensor/ops/clamp.py +24 -0
- nbxtensor/ops/clamp_min.py +22 -0
- nbxtensor/ops/conv2d.py +265 -0
- nbxtensor/ops/conv2d_m8n8k4.py +165 -0
- nbxtensor/ops/conv_depthwise2d.py +154 -0
- nbxtensor/ops/conv_transpose2d.py +148 -0
- nbxtensor/ops/conv_wrappers.py +1482 -0
- nbxtensor/ops/copy.py +18 -0
- nbxtensor/ops/copy_op.py +39 -0
- nbxtensor/ops/cos.py +17 -0
- nbxtensor/ops/count_nonzero.py +64 -0
- nbxtensor/ops/cross_entropy.py +67 -0
- nbxtensor/ops/cummax.py +109 -0
- nbxtensor/ops/cummin.py +105 -0
- nbxtensor/ops/cumsum.py +244 -0
- nbxtensor/ops/decode_attn_vec.py +162 -0
- nbxtensor/ops/depthwise_conv2d.py +132 -0
- nbxtensor/ops/dequant_gemv.py +190 -0
- nbxtensor/ops/dft.py +67 -0
- nbxtensor/ops/dispatch.py +1228 -0
- nbxtensor/ops/div.py +75 -0
- nbxtensor/ops/dot_op.py +67 -0
- nbxtensor/ops/dropout.py +23 -0
- nbxtensor/ops/dtype_convert.py +42 -0
- nbxtensor/ops/elementwise_launch.py +81 -0
- nbxtensor/ops/elementwise_wrappers.py +2315 -0
- nbxtensor/ops/elu.py +20 -0
- nbxtensor/ops/embedding.py +69 -0
- nbxtensor/ops/eq.py +32 -0
- nbxtensor/ops/erf.py +18 -0
- nbxtensor/ops/exp.py +17 -0
- nbxtensor/ops/exp2.py +18 -0
- nbxtensor/ops/fft_op.py +125 -0
- nbxtensor/ops/fill_op.py +20 -0
- nbxtensor/ops/flash_attention.py +307 -0
- nbxtensor/ops/flash_attention_m8n8k4.py +384 -0
- nbxtensor/ops/flash_decode.py +78 -0
- nbxtensor/ops/flip_op.py +60 -0
- nbxtensor/ops/floor.py +90 -0
- nbxtensor/ops/full_op.py +24 -0
- nbxtensor/ops/fused_moe.py +417 -0
- nbxtensor/ops/fused_moe_m8n8k4.py +99 -0
- nbxtensor/ops/fused_upsample_conv.py +413 -0
- nbxtensor/ops/gather_op.py +52 -0
- nbxtensor/ops/ge.py +32 -0
- nbxtensor/ops/gelu.py +31 -0
- nbxtensor/ops/gemv_vec.py +80 -0
- nbxtensor/ops/glu.py +43 -0
- nbxtensor/ops/grid_sampler.py +145 -0
- nbxtensor/ops/group_norm_tiled.py +67 -0
- nbxtensor/ops/groupnorm.py +123 -0
- nbxtensor/ops/gt.py +32 -0
- nbxtensor/ops/hardsigmoid.py +18 -0
- nbxtensor/ops/hardswish.py +19 -0
- nbxtensor/ops/im2col.py +57 -0
- nbxtensor/ops/index_add.py +95 -0
- nbxtensor/ops/index_put_op.py +92 -0
- nbxtensor/ops/index_select.py +121 -0
- nbxtensor/ops/index_wrappers.py +770 -0
- nbxtensor/ops/isfinite.py +20 -0
- nbxtensor/ops/isinf.py +20 -0
- nbxtensor/ops/isnan.py +18 -0
- nbxtensor/ops/launch_keys.py +1054 -0
- nbxtensor/ops/launch_observer.py +54 -0
- nbxtensor/ops/layernorm.py +71 -0
- nbxtensor/ops/le.py +35 -0
- nbxtensor/ops/leaky_relu.py +19 -0
- nbxtensor/ops/lerp.py +76 -0
- nbxtensor/ops/linalg_wrappers.py +1370 -0
- nbxtensor/ops/linspace_op.py +47 -0
- nbxtensor/ops/log.py +17 -0
- nbxtensor/ops/log_sigmoid.py +23 -0
- nbxtensor/ops/logical_and.py +26 -0
- nbxtensor/ops/logical_not.py +24 -0
- nbxtensor/ops/logical_or.py +26 -0
- nbxtensor/ops/logical_xor.py +37 -0
- nbxtensor/ops/loss_wrappers.py +282 -0
- nbxtensor/ops/lt.py +32 -0
- nbxtensor/ops/masked_fill.py +21 -0
- nbxtensor/ops/matmul.py +377 -0
- nbxtensor/ops/matmul_m8n8k4.py +235 -0
- nbxtensor/ops/max_pool2d.py +74 -0
- nbxtensor/ops/max_pool3d.py +93 -0
- nbxtensor/ops/max_reduce.py +91 -0
- nbxtensor/ops/maximum.py +18 -0
- nbxtensor/ops/mean.py +40 -0
- nbxtensor/ops/min_reduce.py +91 -0
- nbxtensor/ops/minimum.py +18 -0
- nbxtensor/ops/mish.py +22 -0
- nbxtensor/ops/mma_m8n8k4.py +152 -0
- nbxtensor/ops/moe_align.py +195 -0
- nbxtensor/ops/moe_decode_vec.py +374 -0
- nbxtensor/ops/moe_device_residency.py +141 -0
- nbxtensor/ops/moe_wrappers.py +345 -0
- nbxtensor/ops/mse_loss.py +79 -0
- nbxtensor/ops/mul.py +51 -0
- nbxtensor/ops/nan_to_num.py +23 -0
- nbxtensor/ops/ne.py +32 -0
- nbxtensor/ops/neg.py +17 -0
- nbxtensor/ops/nllloss.py +124 -0
- nbxtensor/ops/nonzero.py +33 -0
- nbxtensor/ops/nonzero_op.py +62 -0
- nbxtensor/ops/norm_wrappers.py +579 -0
- nbxtensor/ops/ones_op.py +20 -0
- nbxtensor/ops/oracles/__init__.py +6 -0
- nbxtensor/ops/oracles/conv2d_fp64.py +450 -0
- nbxtensor/ops/pad_op.py +159 -0
- nbxtensor/ops/paged_decode_attn.py +215 -0
- nbxtensor/ops/pixel_shuffle.py +144 -0
- nbxtensor/ops/pixel_unshuffle.py +72 -0
- nbxtensor/ops/pow.py +137 -0
- nbxtensor/ops/prod.py +81 -0
- nbxtensor/ops/rand_op.py +52 -0
- nbxtensor/ops/random_wrappers.py +366 -0
- nbxtensor/ops/reciprocal.py +17 -0
- nbxtensor/ops/reduction_wrappers.py +845 -0
- nbxtensor/ops/relu.py +18 -0
- nbxtensor/ops/remainder.py +46 -0
- nbxtensor/ops/repeat_interleave.py +48 -0
- nbxtensor/ops/repeat_op.py +132 -0
- nbxtensor/ops/residual_chain.py +374 -0
- nbxtensor/ops/rmsnorm.py +68 -0
- nbxtensor/ops/rope.py +142 -0
- nbxtensor/ops/rsqrt.py +17 -0
- nbxtensor/ops/scaled_softmax.py +96 -0
- nbxtensor/ops/scatter_op.py +139 -0
- nbxtensor/ops/screen_oracle.py +479 -0
- nbxtensor/ops/selu.py +22 -0
- nbxtensor/ops/shape_wrappers.py +1032 -0
- nbxtensor/ops/sigmoid.py +19 -0
- nbxtensor/ops/silu.py +27 -0
- nbxtensor/ops/sin.py +17 -0
- nbxtensor/ops/softmax.py +169 -0
- nbxtensor/ops/softplus.py +27 -0
- nbxtensor/ops/sort_op.py +304 -0
- nbxtensor/ops/spectral_wrappers.py +481 -0
- nbxtensor/ops/sqrt.py +17 -0
- nbxtensor/ops/stack_op.py +84 -0
- nbxtensor/ops/std.py +113 -0
- nbxtensor/ops/strided_copy.py +200 -0
- nbxtensor/ops/sub.py +35 -0
- nbxtensor/ops/sum.py +40 -0
- nbxtensor/ops/tan.py +19 -0
- nbxtensor/ops/tanh.py +21 -0
- nbxtensor/ops/threshold.py +19 -0
- nbxtensor/ops/tiling.yml +91 -0
- nbxtensor/ops/tiling_policy.py +43 -0
- nbxtensor/ops/tiling_sizes.py +370 -0
- nbxtensor/ops/topk.py +267 -0
- nbxtensor/ops/trace_op.py +64 -0
- nbxtensor/ops/tril.py +64 -0
- nbxtensor/ops/triu.py +62 -0
- nbxtensor/ops/unfold_op.py +37 -0
- nbxtensor/ops/unique_op.py +97 -0
- nbxtensor/ops/upsample_bilinear2d.py +106 -0
- nbxtensor/ops/upsample_nearest2d.py +52 -0
- nbxtensor/ops/upsample_nearest3d.py +61 -0
- nbxtensor/ops/var.py +136 -0
- nbxtensor/ops/vector_norm.py +216 -0
- nbxtensor/ops/weight_norm.py +104 -0
- nbxtensor/ops/where.py +39 -0
- nbxtensor/ops/wrappers.py +825 -0
- nbxtensor/ops/zeros_op.py +20 -0
- nbxtensor/quant/__init__.py +1 -0
- nbxtensor/quant/quantized_tensor.py +187 -0
- nbxtensor/random/__init__.py +1 -0
- nbxtensor/random/mt_randn.py +154 -0
- nbxtensor/random/rng_pin.py +59 -0
- nbxtensor/random/rng_stream.py +118 -0
- nbxtensor/random/seeded_draw.py +223 -0
- nbxtensor/shape/__init__.py +157 -0
- nbxtensor/tensor.py +1867 -0
- nbxtensor-0.0.1.dist-info/METADATA +379 -0
- nbxtensor-0.0.1.dist-info/RECORD +291 -0
- nbxtensor-0.0.1.dist-info/WHEEL +4 -0
- 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))
|