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