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,429 @@
|
|
|
1
|
+
"""What a device driver must present to the engine's kernel launcher.
|
|
2
|
+
|
|
3
|
+
The launcher itself is not here. It is an engine component, vendor-agnostic,
|
|
4
|
+
and it belongs in the dispatch layer with CUDA as its first client. This
|
|
5
|
+
module is the **contract** that launcher will program against: the smallest
|
|
6
|
+
surface a driver has to expose for the launcher to compile a kernel once and
|
|
7
|
+
dispatch it many times, on any backend.
|
|
8
|
+
|
|
9
|
+
It exists because the Metal port had to build a driver anyway — upstream
|
|
10
|
+
Triton's own launcher imports torch inside its argument binder, on every
|
|
11
|
+
backend, and R33 does not have an Apple exception — and building it made the
|
|
12
|
+
surface concrete. `triton/metal_driver.py` is the first implementation and it
|
|
13
|
+
passes the checker below. A CUDA implementation has to pass the same checker,
|
|
14
|
+
unchanged, or the launcher would be programming against Metal's habits rather
|
|
15
|
+
than against an interface.
|
|
16
|
+
|
|
17
|
+
## The surface, in one paragraph
|
|
18
|
+
|
|
19
|
+
A driver **compiles** a `@triton.jit` function plus a signature and a set of
|
|
20
|
+
`tl.constexpr` values into a `CompiledKernel`. That object carries the
|
|
21
|
+
compiled artifact, the argument binding the launcher must fill, the
|
|
22
|
+
threadgroup/block width, and the shared-memory request. The launcher then
|
|
23
|
+
**launches** it with a grid and a flat argument list, where every device
|
|
24
|
+
buffer is passed as an **integer pointer** and every scalar as a typed Python
|
|
25
|
+
value. Ordering is expressed with **streams** and **events**, both opaque
|
|
26
|
+
integer handles.
|
|
27
|
+
|
|
28
|
+
Nothing in that paragraph is Metal-shaped, and that is the point.
|
|
29
|
+
|
|
30
|
+
## What the contract deliberately does NOT include
|
|
31
|
+
|
|
32
|
+
* **Tensors.** The launcher passes pointer integers, not container objects. A
|
|
33
|
+
driver never learns what an NBXTensor is, and NBXTensor never learns what a
|
|
34
|
+
backend is; the allocator already owns that seam
|
|
35
|
+
(`kernels/nbx_tensor.py::DeviceAllocator`).
|
|
36
|
+
* **Autotuning, caching, heuristics.** Choosing `BLOCK_SIZE` is the engine's
|
|
37
|
+
business and it is the same decision on every backend. A driver is asked to
|
|
38
|
+
compile what it is given.
|
|
39
|
+
* **A device abstraction.** Allocation, copies and device queries are the
|
|
40
|
+
allocator's, not the driver's. The driver receives addresses that already
|
|
41
|
+
exist.
|
|
42
|
+
* **torch.** On any backend. A driver that needs torch to bind an argument
|
|
43
|
+
has put the framework back in the execution path, which is the thing R33
|
|
44
|
+
forbids and the reason this contract was written at all.
|
|
45
|
+
|
|
46
|
+
R33 preserved — no torch, at the boundary included.
|
|
47
|
+
R34 preserved — nothing here is keyed on a model name.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
from dataclasses import dataclass
|
|
53
|
+
from typing import Any, Mapping, Protocol, Sequence, runtime_checkable
|
|
54
|
+
|
|
55
|
+
#: Scalar kinds a launcher may be asked to pack. A driver must accept every
|
|
56
|
+
#: one of these spellings, because they are Triton's own signature spellings
|
|
57
|
+
#: and the launcher passes them straight through.
|
|
58
|
+
SCALAR_DTYPES = ("i1", "i8", "i16", "i32", "i64",
|
|
59
|
+
"u8", "u16", "u32", "u64",
|
|
60
|
+
"fp16", "bf16", "fp32", "fp64")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class ArgSlot:
|
|
65
|
+
"""One argument the launcher must supply, in binding order.
|
|
66
|
+
|
|
67
|
+
`index` is the driver's own binding index, not the position in the Python
|
|
68
|
+
call: a backend may reserve indices or reorder, and the launcher must not
|
|
69
|
+
have to know which. It fills the slots in the order they are given.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
index: int
|
|
73
|
+
name: str
|
|
74
|
+
is_pointer: bool
|
|
75
|
+
dtype: str
|
|
76
|
+
|
|
77
|
+
def __post_init__(self):
|
|
78
|
+
if not self.is_pointer and self.dtype not in SCALAR_DTYPES:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"{self.name}: {self.dtype!r} is not a scalar dtype the "
|
|
81
|
+
f"launcher can pack; expected one of {SCALAR_DTYPES}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@runtime_checkable
|
|
85
|
+
class CompiledKernel(Protocol):
|
|
86
|
+
"""The result of compiling once, launched many times."""
|
|
87
|
+
|
|
88
|
+
#: The kernel's entry-point name, as the backend knows it.
|
|
89
|
+
name: str
|
|
90
|
+
|
|
91
|
+
#: The compiled artifact, opaque to the launcher, in whatever form this
|
|
92
|
+
#: backend reloads without recompiling: a cubin, a metallib, an MSL
|
|
93
|
+
#: source. The launcher only ever caches and hands it back.
|
|
94
|
+
binary: bytes
|
|
95
|
+
|
|
96
|
+
#: What `binary` is — "cubin", "metallib", "msl", "hsaco". Declared so a
|
|
97
|
+
#: cache can refuse an artifact built for another backend instead of
|
|
98
|
+
#: loading it and failing later.
|
|
99
|
+
binary_kind: str
|
|
100
|
+
|
|
101
|
+
#: Threads per block / threadgroup.
|
|
102
|
+
block_size: int
|
|
103
|
+
|
|
104
|
+
#: Bytes of shared / threadgroup memory the kernel requests.
|
|
105
|
+
shared_memory: int
|
|
106
|
+
|
|
107
|
+
#: The `tl.constexpr` values this kernel was compiled with. Part of the
|
|
108
|
+
#: identity of the compilation, so a cache keyed on the source alone is
|
|
109
|
+
#: wrong and this makes that visible.
|
|
110
|
+
constexprs: Mapping[str, Any]
|
|
111
|
+
|
|
112
|
+
#: The specialization markers this kernel was compiled with — the same
|
|
113
|
+
#: mapping `compile()` was given. Also part of the compilation's
|
|
114
|
+
#: identity: the same source and the same constexprs, compiled once
|
|
115
|
+
#: claiming 16-byte alignment and once not, are two different kernels,
|
|
116
|
+
#: and handing one back for the other is silently wrong numbers.
|
|
117
|
+
specialization: Mapping[str, str]
|
|
118
|
+
|
|
119
|
+
#: The arguments the launcher must supply, in binding order.
|
|
120
|
+
binding: Sequence[ArgSlot]
|
|
121
|
+
|
|
122
|
+
def launch(self, grid: Sequence[int], args: Sequence[Any],
|
|
123
|
+
stream: int = 0) -> None:
|
|
124
|
+
"""Dispatch `grid` blocks.
|
|
125
|
+
|
|
126
|
+
`args` is positional and matches `binding` one for one. A slot with
|
|
127
|
+
`is_pointer` takes an **integer device address**; every other slot
|
|
128
|
+
takes a Python int or float, which the driver packs to `dtype`.
|
|
129
|
+
|
|
130
|
+
`stream` is 0 for the default stream, or a handle from
|
|
131
|
+
`create_stream`. The call returns once the work is enqueued; ordering
|
|
132
|
+
against other work is the caller's, expressed with events.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@runtime_checkable
|
|
137
|
+
class LauncherDriver(Protocol):
|
|
138
|
+
"""What the launcher needs from a backend, and nothing more."""
|
|
139
|
+
|
|
140
|
+
#: "cuda", "hip", "metal". The launcher uses it for diagnostics and for
|
|
141
|
+
#: refusing a cached artifact from another backend, never to branch on.
|
|
142
|
+
backend: str
|
|
143
|
+
|
|
144
|
+
def compile(self, jit_fn, signature: Mapping[str, str],
|
|
145
|
+
constexprs: Mapping[str, Any],
|
|
146
|
+
num_warps: int = 4,
|
|
147
|
+
specialization: Mapping[str, str] | None = None,
|
|
148
|
+
num_stages: int | None = None,
|
|
149
|
+
) -> CompiledKernel:
|
|
150
|
+
"""Compile one `@triton.jit` function.
|
|
151
|
+
|
|
152
|
+
`signature` maps parameter name to a Triton signature string —
|
|
153
|
+
`"*fp32"` for a pointer, `"i32"` / `"fp32"` for a scalar, and
|
|
154
|
+
`"constexpr"` for a compile-time parameter whose value is in
|
|
155
|
+
`constexprs`. Raises rather than returning a partial result.
|
|
156
|
+
|
|
157
|
+
`specialization` maps parameter name to Triton's per-argument marker:
|
|
158
|
+
`"D"` when the argument is a multiple of 16 (a pointer's address, an
|
|
159
|
+
integer's value), `""` when it is not, absent when the argument takes
|
|
160
|
+
no marker at all. It is NOT decoration — the compiler turns "D" into
|
|
161
|
+
a divisibility attribute and vectorizes loads and stores on it, so a
|
|
162
|
+
driver that ignores this argument compiles a different kernel from
|
|
163
|
+
the one the launcher asked for, at the same shape, with the same
|
|
164
|
+
arguments. Measured on Metal 2026-09-05: dropping it changed
|
|
165
|
+
rms_norm's fp16 output at two of four shapes while leaving fp32
|
|
166
|
+
untouched.
|
|
167
|
+
|
|
168
|
+
A driver is free to have no use for it, but must then say so
|
|
169
|
+
explicitly rather than accept and discard it.
|
|
170
|
+
|
|
171
|
+
`num_stages` is the software-pipelining depth, which an autotuner
|
|
172
|
+
config carries alongside `num_warps`. Measured on Metal 2026-09-05:
|
|
173
|
+
it changes nothing in the emitted MSL, because that lowerer does not
|
|
174
|
+
pipeline — but it changes CUDA's generated code substantially, so the
|
|
175
|
+
launcher passes it and each backend decides. Dropping it in the
|
|
176
|
+
launcher would silently discard the autotuner's choice on the first
|
|
177
|
+
backend that reads it.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
# -- ordering ------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def create_stream(self) -> int:
|
|
183
|
+
"""A new stream. Never returns 0, which is the default stream."""
|
|
184
|
+
|
|
185
|
+
def destroy_stream(self, stream: int) -> None:
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
def synchronize_stream(self, stream: int) -> None:
|
|
189
|
+
"""Block until everything on `stream` has retired."""
|
|
190
|
+
|
|
191
|
+
def create_event(self, timing: bool = False) -> int:
|
|
192
|
+
"""An event handle. `timing=False` is an ordering handle and must
|
|
193
|
+
REFUSE to be read as a stopwatch — see `elapsed_ms`."""
|
|
194
|
+
|
|
195
|
+
def destroy_event(self, event: int) -> None:
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
def record_event(self, event: int, stream: int = 0) -> None:
|
|
199
|
+
...
|
|
200
|
+
|
|
201
|
+
def synchronize_event(self, event: int) -> None:
|
|
202
|
+
"""Block until `event` has been reached."""
|
|
203
|
+
|
|
204
|
+
def wait_event(self, stream: int, event: int) -> None:
|
|
205
|
+
"""Make `stream` wait for `event` WITHOUT blocking the host."""
|
|
206
|
+
|
|
207
|
+
def elapsed_ms(self, start: int, end: int) -> float:
|
|
208
|
+
"""Milliseconds between two recorded timing events.
|
|
209
|
+
|
|
210
|
+
Must raise for an event created with `timing=False`: an ordering
|
|
211
|
+
handle is not a clock, and returning a plausible number instead of
|
|
212
|
+
refusing is the failure this method exists to prevent.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# The checker
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
def verify_driver_contract(driver, jit_fn, signature, constexprs,
|
|
221
|
+
make_buffer, read_buffer, free_buffer,
|
|
222
|
+
grid, args_builder, expected):
|
|
223
|
+
"""Exercise a driver against the contract. Returns a list of failures.
|
|
224
|
+
|
|
225
|
+
Written as a plain function rather than a test so a backend can run it
|
|
226
|
+
from anywhere — a CI job, a `doctor` subcommand, a notebook on a machine
|
|
227
|
+
none of us has. `tests/unit/triton/test_launcher_contract.py` calls it
|
|
228
|
+
for Metal.
|
|
229
|
+
|
|
230
|
+
It exercises the driver **exactly as `kernels/launcher.py` does**: Triton
|
|
231
|
+
compiles for the target the driver names, the driver loads the artifact
|
|
232
|
+
it declared, and the driver launches it with the launcher's own
|
|
233
|
+
`(kind, value)` parameter list AND everything that travels beside it —
|
|
234
|
+
`names`, `types`, and the `trailing` buffers the driver itself declared
|
|
235
|
+
from the compile metadata. A checker that used a private path of its own
|
|
236
|
+
would certify a driver the engine cannot actually drive; until 2026-09-17
|
|
237
|
+
this one omitted all three, so a driver that needs any of them could not be
|
|
238
|
+
certified here at all.
|
|
239
|
+
|
|
240
|
+
The callables are the caller's, because allocation is the allocator's job
|
|
241
|
+
and this module must not grow one:
|
|
242
|
+
|
|
243
|
+
* `make_buffer(nbytes) -> int` — a device address.
|
|
244
|
+
* `read_buffer(addr, nbytes) -> bytes`
|
|
245
|
+
* `free_buffer(addr) -> None`
|
|
246
|
+
* `args_builder(pointers) -> list` — the flat argument list, given the
|
|
247
|
+
pointers it asked for by allocating them.
|
|
248
|
+
"""
|
|
249
|
+
from nbxtensor.backend.launcher import _pack_param
|
|
250
|
+
|
|
251
|
+
failures = []
|
|
252
|
+
|
|
253
|
+
def check(condition, message):
|
|
254
|
+
if not condition:
|
|
255
|
+
failures.append(message)
|
|
256
|
+
return condition
|
|
257
|
+
|
|
258
|
+
# -- the declared surface ------------------------------------------------
|
|
259
|
+
check(isinstance(getattr(driver, "artifact_kind", None), str)
|
|
260
|
+
and driver.artifact_kind,
|
|
261
|
+
"a driver must declare `artifact_kind` — the key of its artifact "
|
|
262
|
+
"in CompiledKernel.asm — so a cache cannot hand one backend "
|
|
263
|
+
"another's binary")
|
|
264
|
+
check(isinstance(getattr(driver, "wants_scratch_params", None), bool),
|
|
265
|
+
"a driver must declare `wants_scratch_params`: whether its launch "
|
|
266
|
+
"ABI carries Triton's two trailing scratch pointers")
|
|
267
|
+
|
|
268
|
+
target = driver.target()
|
|
269
|
+
check(isinstance(getattr(target, "backend", None), str) and target.backend,
|
|
270
|
+
f"target() must name a backend, got {target!r}")
|
|
271
|
+
check(int(getattr(target, "warp_size", 0)) > 0,
|
|
272
|
+
f"target() must give a positive warp size, got {target!r}")
|
|
273
|
+
|
|
274
|
+
# -- compile, load, launch: the launcher's own three steps ---------------
|
|
275
|
+
import triton
|
|
276
|
+
from triton.compiler.compiler import ASTSource
|
|
277
|
+
|
|
278
|
+
compiled = triton.compile(
|
|
279
|
+
ASTSource(fn=jit_fn, signature=signature, constexprs=constexprs),
|
|
280
|
+
target=target, options={"num_warps": 4})
|
|
281
|
+
metadata = compiled.metadata
|
|
282
|
+
|
|
283
|
+
artifact = compiled.asm.get(driver.artifact_kind)
|
|
284
|
+
if not check(artifact is not None,
|
|
285
|
+
f"the compiler produced {sorted(compiled.asm)} and the "
|
|
286
|
+
f"driver takes {driver.artifact_kind!r}: nothing to load"):
|
|
287
|
+
return failures
|
|
288
|
+
|
|
289
|
+
block = driver.block_for(metadata)
|
|
290
|
+
check(isinstance(block, tuple) and len(block) == 3
|
|
291
|
+
and all(isinstance(b, int) and b > 0 for b in block),
|
|
292
|
+
f"block_for() must return a 3-tuple of positive ints, got {block!r}")
|
|
293
|
+
|
|
294
|
+
function = driver.load(artifact, metadata.name, metadata.shared)
|
|
295
|
+
check(function is not None, "load() returned nothing")
|
|
296
|
+
|
|
297
|
+
# -- the launch produces the right bytes, at a grid wider than one block -
|
|
298
|
+
pointers = [make_buffer(n) for n in args_builder.buffer_sizes]
|
|
299
|
+
try:
|
|
300
|
+
values = args_builder(pointers)
|
|
301
|
+
ordered = [(name, ty) for name, ty in signature.items()
|
|
302
|
+
if ty != "constexpr"]
|
|
303
|
+
if not check(len(ordered) == len(values),
|
|
304
|
+
f"the signature has {len(ordered)} runtime parameters "
|
|
305
|
+
f"and args_builder gave {len(values)}"):
|
|
306
|
+
return failures
|
|
307
|
+
params = [_pack_param(ty, v) for (_n, ty), v in zip(ordered, values)]
|
|
308
|
+
if driver.wants_scratch_params:
|
|
309
|
+
params = params + [("ptr", 0), ("ptr", 0)]
|
|
310
|
+
|
|
311
|
+
# Everything `kernels/launcher.py` hands a driver, because that is what
|
|
312
|
+
# this function claims to do. Omitting any of it certifies a driver
|
|
313
|
+
# against a call the engine never makes: a backend that packs its
|
|
314
|
+
# scalars into one buffer needs `types` to place the fields, one that
|
|
315
|
+
# binds by name needs `names`, and one whose emitter appends its own
|
|
316
|
+
# buffers needs `trailing` — without which the checker would bless a
|
|
317
|
+
# launch that binds fewer buffers than the kernel declares.
|
|
318
|
+
#
|
|
319
|
+
# `names` and `types` cover the SIGNATURE's runtime parameters and stop
|
|
320
|
+
# there. The launcher appends the two scratch pointers to `params` and
|
|
321
|
+
# to nothing else, so this must not "helpfully" extend them either: the
|
|
322
|
+
# point is to make the launcher's call, divergences included.
|
|
323
|
+
names = [name for name, _ty in ordered]
|
|
324
|
+
types = [ty for _name, ty in ordered]
|
|
325
|
+
trailing = driver.trailing_buffers(metadata)
|
|
326
|
+
|
|
327
|
+
# THREE extents, because that is what the launcher hands a driver:
|
|
328
|
+
# `launch()` pads with `+ (1,) * (3 - len(grid))` before the call. This
|
|
329
|
+
# checker passed the caller's grid through unpadded, and a driver that
|
|
330
|
+
# unpacks `gx, gy, gz = grid` — as triton-ext's does — raised
|
|
331
|
+
# ValueError on an ordinary one-dimensional launch. The archived fork's
|
|
332
|
+
# driver tolerated a short tuple, so the gap sat here unseen until the
|
|
333
|
+
# backend changed.
|
|
334
|
+
padded = tuple(int(g) for g in grid) + (1,) * (3 - len(grid))
|
|
335
|
+
|
|
336
|
+
def _launch(p):
|
|
337
|
+
driver.launch(function, padded, block, metadata.shared, 0, p,
|
|
338
|
+
names=names, types=types, trailing=trailing)
|
|
339
|
+
|
|
340
|
+
_launch(params)
|
|
341
|
+
got = read_buffer(pointers[args_builder.output_index],
|
|
342
|
+
args_builder.output_bytes)
|
|
343
|
+
check(got == expected,
|
|
344
|
+
"the launch produced the wrong bytes. The grid is deliberately "
|
|
345
|
+
"wider than one block: a dropped program offset passes a "
|
|
346
|
+
"single-block launch and fails here.")
|
|
347
|
+
|
|
348
|
+
# A wrong-length argument list must be refused, not padded.
|
|
349
|
+
try:
|
|
350
|
+
_launch(params[:-1])
|
|
351
|
+
failures.append("launch() accepted an argument list of the wrong "
|
|
352
|
+
"length instead of refusing it")
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
# A pointer the allocator never handed out must be refused. This is
|
|
357
|
+
# the ownership rule: the engine owns device memory, and a launch
|
|
358
|
+
# that reads an address from anywhere else is undefined behaviour
|
|
359
|
+
# the driver is in the best position to catch.
|
|
360
|
+
foreign = list(params)
|
|
361
|
+
for i, (kind, _v) in enumerate(foreign):
|
|
362
|
+
if kind == "ptr":
|
|
363
|
+
foreign[i] = ("ptr", 0x1000)
|
|
364
|
+
break
|
|
365
|
+
try:
|
|
366
|
+
_launch(foreign)
|
|
367
|
+
failures.append("launch() accepted a device address the "
|
|
368
|
+
"allocator never handed out")
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
finally:
|
|
372
|
+
for address in pointers:
|
|
373
|
+
free_buffer(address)
|
|
374
|
+
|
|
375
|
+
# -- events --------------------------------------------------------------
|
|
376
|
+
# A DIFFERENT PROTOCOL, and not every backend puts it on the driver. The
|
|
377
|
+
# launcher calls none of these — it uses `load`, `launch`, `block_for`,
|
|
378
|
+
# `target`, `artifact_kind`, `wants_scratch_params`, `trailing_buffers` and
|
|
379
|
+
# `max_shared_memory_per_block`, and nothing else. On Apple the streams and
|
|
380
|
+
# events belong to the ALLOCATOR (`kernels/metal_device.py`), so
|
|
381
|
+
# `TritonExtDriver` implements the launch half only. Measured 2026-09-17,
|
|
382
|
+
# when archiving the fork replaced a driver that carried both halves on one
|
|
383
|
+
# object and hid the distinction.
|
|
384
|
+
#
|
|
385
|
+
# A driver that does not claim this half is NOT failed here: certifying it
|
|
386
|
+
# against an API the engine never calls would be this checker inventing a
|
|
387
|
+
# contract, which is the fault it exists to prevent. It is SAID instead, so
|
|
388
|
+
# the gap stays visible.
|
|
389
|
+
_ordering_api = ("create_event", "record_event", "synchronize_event",
|
|
390
|
+
"create_stream", "wait_event", "synchronize_stream",
|
|
391
|
+
"destroy_event", "destroy_stream", "elapsed_ms")
|
|
392
|
+
_absent = [m for m in _ordering_api if not hasattr(driver, m)]
|
|
393
|
+
if _absent:
|
|
394
|
+
print(f" note: {driver.__class__.__name__} implements the LAUNCH half "
|
|
395
|
+
f"of this contract and not the ordering half "
|
|
396
|
+
f"({', '.join(_absent)}); on this backend streams and events "
|
|
397
|
+
f"belong to the allocator. Not certified here, not a failure.",
|
|
398
|
+
flush=True)
|
|
399
|
+
return failures
|
|
400
|
+
|
|
401
|
+
ordering = driver.create_event(timing=False)
|
|
402
|
+
timing_a = driver.create_event(timing=True)
|
|
403
|
+
timing_b = driver.create_event(timing=True)
|
|
404
|
+
try:
|
|
405
|
+
driver.record_event(ordering, 0)
|
|
406
|
+
driver.synchronize_event(ordering)
|
|
407
|
+
second = driver.create_stream()
|
|
408
|
+
driver.wait_event(second, ordering)
|
|
409
|
+
driver.synchronize_stream(second)
|
|
410
|
+
driver.destroy_stream(second)
|
|
411
|
+
|
|
412
|
+
driver.record_event(timing_a, 0)
|
|
413
|
+
driver.record_event(timing_b, 0)
|
|
414
|
+
driver.synchronize_event(timing_b)
|
|
415
|
+
interval = driver.elapsed_ms(timing_a, timing_b)
|
|
416
|
+
check(isinstance(interval, float) and interval >= 0.0,
|
|
417
|
+
f"elapsed_ms must return a non-negative float, got {interval!r}")
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
driver.elapsed_ms(ordering, ordering)
|
|
421
|
+
failures.append("elapsed_ms returned a number for an ordering "
|
|
422
|
+
"event; an ordering handle is not a clock")
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
425
|
+
finally:
|
|
426
|
+
for e in (ordering, timing_a, timing_b):
|
|
427
|
+
driver.destroy_event(e)
|
|
428
|
+
|
|
429
|
+
return failures
|