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,137 @@
|
|
|
1
|
+
"""The bucket of a request-dependent dimension in an autotune key.
|
|
2
|
+
|
|
3
|
+
The owner's decision (2026-09-21): request-dependent dimensions — a prefill's token count,
|
|
4
|
+
a decode's key length, a convolution's spatial extents and batch — are BUCKETED in the
|
|
5
|
+
launcher's key. The bucket selects the configuration; the kernel still runs the true size
|
|
6
|
+
with its masks. Without it a census can only cover the requests it is shown, a 57-token
|
|
7
|
+
prompt meets a key nobody certified, and zero miss at verification can never be true.
|
|
8
|
+
|
|
9
|
+
The ladder is DATA, on the vendor profile (`autotune.buckets`), chosen by measurement the
|
|
10
|
+
way the tile unit was (`tools/bucket_loss.py`, owed-proofs 2026-09-21): on Volta, both
|
|
11
|
+
memory classes, matmul M and the SDPA scores' batched GEMM — exact under 64 (the batched
|
|
12
|
+
GEMM's optimum moves with every size there: up to 16.7 % lost inside a 16-wide bucket),
|
|
13
|
+
then 16-step to 256, 32-step to 1024, 128-step to 8192, 512-step beyond: 0.0 % median and
|
|
14
|
+
maximum loss against the per-size optimum, where powers of two lost up to 26.9 %.
|
|
15
|
+
|
|
16
|
+
A profile that is BOUND but declares no ladder is REFUSED, not quietly bucketed at step 1.
|
|
17
|
+
Answering "exact" there is a silent degradation dressed as a default: it produces a key
|
|
18
|
+
explosion with no error, and it hid for a day that the ladder had been added to
|
|
19
|
+
nvidia/volta.yml alone while all twenty-six other profiles carried none (found 2026-09-22 by
|
|
20
|
+
the schema gate, not by a run). An UNBOUND profile — `{}`, no target matched, the census
|
|
21
|
+
behind its door before `install()` — is a different condition and still keys exact; that one
|
|
22
|
+
is covered by binding the shadow to its profile, not by a refusal here.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
27
|
+
|
|
28
|
+
#: A ladder is a list of [upper_bound, step] rows in ascending bound order: a value v takes
|
|
29
|
+
#: the smallest multiple of `step` >= v within the first row whose bound is >= v. A step of
|
|
30
|
+
#: 1 is "exact". The last row's bound may be null (open).
|
|
31
|
+
Ladder = Sequence[Tuple[Optional[int], int]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_PARSED: Dict[str, Ladder] = {}
|
|
35
|
+
_BY_ID: Dict[int, Tuple[Any, Ladder]] = {}
|
|
36
|
+
|
|
37
|
+
#: Dimensions that are NOT a tensor dimension and never take the profile's `default` ladder: an element-wise launch's
|
|
38
|
+
#: element count is classed by the launch-overhead and bandwidth regimes (four classes on volta and apple_m4_pro), and
|
|
39
|
+
#: answered by the dimension ladder it became 27 868 classes on apple_m4_pro (the Mac's census, 2026-10-24 12:26) —
|
|
40
|
+
#: five days of certification for one launch tile. A profile without its own ladder for one of these is refused.
|
|
41
|
+
NO_DEFAULT = frozenset({"elements"})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_ladder(rows: Any) -> Ladder:
|
|
45
|
+
"""Parsed once per distinct row list: the key of every launch reads the ladder (32 616
|
|
46
|
+
parses in one chatterbox shadow, 2026-09-22)."""
|
|
47
|
+
# the profile hands the same row list on every call: answered by its identity first (the `repr` key below, built
|
|
48
|
+
# per call, was itself the cost — 2 of 40 decode-step samples of Qwen3-30B-A3B, 2026-10-28); the list is kept so
|
|
49
|
+
# its id is never reused by another
|
|
50
|
+
hit = _BY_ID.get(id(rows))
|
|
51
|
+
if hit is not None and hit[0] is rows:
|
|
52
|
+
return hit[1]
|
|
53
|
+
memo = repr(rows)
|
|
54
|
+
got = _PARSED.get(memo)
|
|
55
|
+
if got is None:
|
|
56
|
+
got = _PARSED[memo] = _parse_ladder(rows)
|
|
57
|
+
_BY_ID[id(rows)] = (rows, got)
|
|
58
|
+
return got
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_ladder(rows: Any) -> Ladder:
|
|
62
|
+
out: List[Tuple[Optional[int], int]] = []
|
|
63
|
+
for r in rows or []:
|
|
64
|
+
if isinstance(r, dict):
|
|
65
|
+
bound, step = r.get("up_to"), r.get("step")
|
|
66
|
+
else:
|
|
67
|
+
bound, step = r[0], r[1]
|
|
68
|
+
if step is None or int(step) < 1:
|
|
69
|
+
raise ValueError(f"ZERO FALLBACK: a bucket ladder row needs a step >= 1: {r!r}")
|
|
70
|
+
out.append((None if bound is None else int(bound), int(step)))
|
|
71
|
+
for i in range(1, len(out)):
|
|
72
|
+
if out[i - 1][0] is None or (out[i][0] is not None and out[i][0] <= out[i - 1][0]):
|
|
73
|
+
raise ValueError(f"ZERO FALLBACK: bucket ladder bounds must ascend and only the last may be open: {rows!r}")
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def bucket(value: int, ladder: Ladder) -> int:
|
|
78
|
+
"""The bucket's TOP for `value`: the smallest multiple of the row's step that is >= value."""
|
|
79
|
+
v = int(value)
|
|
80
|
+
if v <= 0:
|
|
81
|
+
return v
|
|
82
|
+
for bound, step in ladder:
|
|
83
|
+
if bound is None or v <= bound:
|
|
84
|
+
return -(-v // step) * step
|
|
85
|
+
bound, step = ladder[-1]
|
|
86
|
+
return -(-v // step) * step
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def ladder_for(dim: str, profile: Optional[Dict[str, Any]] = None) -> Ladder:
|
|
90
|
+
"""The profile's ladder for `dim` ("M", "N", "batch", "height", "width"), else exact."""
|
|
91
|
+
if profile is None:
|
|
92
|
+
from nbxtensor.ops._configs import active_vendor_profile
|
|
93
|
+
profile = active_vendor_profile()
|
|
94
|
+
bound = bool(profile)
|
|
95
|
+
spec = ((profile or {}).get("autotune") or {}).get("buckets") or {}
|
|
96
|
+
rows = spec.get(dim) if isinstance(spec, dict) else None
|
|
97
|
+
if rows is None and dim in NO_DEFAULT and bound:
|
|
98
|
+
raise RuntimeError(
|
|
99
|
+
f"ZERO FALLBACK: the bound hardware profile {(profile or {}).get('architecture') or '<unnamed>'!r} declares "
|
|
100
|
+
f"no autotune.buckets.{dim} ladder, and {dim!r} never takes the `default` dimension ladder (an element "
|
|
101
|
+
f"count's classes are its launch-overhead and bandwidth regimes, not dimension steps). Add the measured "
|
|
102
|
+
f"ladder, with its source, to its vendor YAML (see nvidia/volta.yml).")
|
|
103
|
+
if rows is None:
|
|
104
|
+
rows = spec.get("default") if isinstance(spec, dict) else None
|
|
105
|
+
if rows:
|
|
106
|
+
return parse_ladder(rows)
|
|
107
|
+
if bound:
|
|
108
|
+
# ZERO FALLBACK: a real profile that forgot the ladder is a defect in the profile,
|
|
109
|
+
# and keying exact would hide it behind a plausible-looking answer.
|
|
110
|
+
raise RuntimeError(
|
|
111
|
+
"ZERO FALLBACK: the bound hardware profile "
|
|
112
|
+
f"{(profile or {}).get('architecture') or '<unnamed>'!r} declares no "
|
|
113
|
+
f"autotune.buckets ladder, so dimension {dim!r} has no bucket.\n"
|
|
114
|
+
" Add `autotune.buckets.default` to its vendor YAML (see nvidia/volta.yml), "
|
|
115
|
+
"or the census keys this dimension EXACTLY and every request length becomes "
|
|
116
|
+
"its own key.\n"
|
|
117
|
+
" A step of 1 is a legitimate ladder and must be written down as one."
|
|
118
|
+
)
|
|
119
|
+
return [(None, 1)] # no target bound: the census's own door, unchanged
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def class_floor(dim: str, top: int, profile: Optional[Dict[str, Any]] = None) -> int:
|
|
123
|
+
"""The fewest `dim` that form the class whose top is `top` (`bucket_of`'s inverse floor): the row holding `top`,
|
|
124
|
+
one past the class below it (its top minus the row's step) or past the previous row's bound."""
|
|
125
|
+
t = int(top)
|
|
126
|
+
if t <= 0:
|
|
127
|
+
return t
|
|
128
|
+
prev = 0
|
|
129
|
+
for bound, step in ladder_for(dim, profile):
|
|
130
|
+
if bound is None or t <= bound:
|
|
131
|
+
return max(prev + 1, t - step + 1)
|
|
132
|
+
prev = bound
|
|
133
|
+
return max(prev + 1, t - ladder_for(dim, profile)[-1][1] + 1)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def bucket_of(dim: str, value: int, profile: Optional[Dict[str, Any]] = None) -> int:
|
|
137
|
+
return bucket(value, ladder_for(dim, profile))
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""E2-full — persistable autotune-config artifact (scoping §Phase 4a.4).
|
|
2
|
+
|
|
3
|
+
Triton's own disk cache (`cache_results=True`) persists autotune
|
|
4
|
+
selections keyed by SOURCE HASH — any kernel-file edit invalidates it
|
|
5
|
+
and the next run re-benches (the battery kernel-edit false-RED class,
|
|
6
|
+
and a run-to-run timing-variance surface). This module captures the
|
|
7
|
+
SELECTED configs keyed by OUR fingerprint — (kernel qualname,
|
|
8
|
+
autotune-key literal, arch) — and seeds them back into the Autotuner
|
|
9
|
+
in-memory caches, immune to source-hash invalidation:
|
|
10
|
+
|
|
11
|
+
capture() after a warmed pass (the replay recording pass), walk the
|
|
12
|
+
five sanctioned Autotuner kernels (mm/bmm/addmm/conv2d/
|
|
13
|
+
depthwise — the Phase 1.5 scope) and merge their selected
|
|
14
|
+
configs into the artifact.
|
|
15
|
+
seed() before execution, insert stored configs into each
|
|
16
|
+
Autotuner's cache: a hit means run() never benches.
|
|
17
|
+
|
|
18
|
+
R33: imports triton + stdlib only. The artifact is an optimization,
|
|
19
|
+
never a failure source — every I/O error degrades to "no seed".
|
|
20
|
+
Storage: ~/.neurobrix/replay_cache/autotune_configs_<arch>.json.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
from typing import Dict, Iterator, Optional, Tuple
|
|
27
|
+
|
|
28
|
+
# The machine's replay cache (the producer's accumulation across models, and
|
|
29
|
+
# the correctness screen's exclusions). NEUROBRIX_REPLAY_CACHE relocates it —
|
|
30
|
+
# a gate that needs a truly cold sweep per arm gives every arm its own, else
|
|
31
|
+
# sweep mode seeds the autotuners from here and no arm sweeps (2026-09-06:
|
|
32
|
+
# a screened arm reported "checked 0 key(s)" for exactly this reason).
|
|
33
|
+
def _dir() -> str:
|
|
34
|
+
"""Where the local replay cache lives, read WHEN USED.
|
|
35
|
+
|
|
36
|
+
This was a module global computed at import time. A value frozen at import
|
|
37
|
+
from an environment variable is the same class of defect as a literal
|
|
38
|
+
standing in for a runtime value: whoever imports the module first decides it
|
|
39
|
+
for the whole process. Measured 2026-09-17 — a test redirecting
|
|
40
|
+
NEUROBRIX_REPLAY_CACHE to a tmp path imported this module inside its own
|
|
41
|
+
body, froze the tmp path, and a later test in the same process asserting on
|
|
42
|
+
the real directory failed. monkeypatch restored the env; it could not
|
|
43
|
+
restore a constant already computed from it.
|
|
44
|
+
"""
|
|
45
|
+
return (os.environ.get("NEUROBRIX_REPLAY_CACHE")
|
|
46
|
+
or os.path.join(os.path.expanduser("~"), ".neurobrix", "replay_cache"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
#: The directory as it stood AT IMPORT. Kept as a real attribute because
|
|
50
|
+
#: `importlib.reload` is how the relocation test observes it, and a module
|
|
51
|
+
#: __getattr__ does not survive that cleanly. Everything INSIDE this module
|
|
52
|
+
#: calls `_dir()` instead, so a process that changes the environment after
|
|
53
|
+
#: import writes where the environment now says — which is what a test
|
|
54
|
+
#: redirecting it to a tmp path needs, and what a later test reading the real
|
|
55
|
+
#: directory needs too.
|
|
56
|
+
_DIR = _dir()
|
|
57
|
+
|
|
58
|
+
# The sanctioned autotune surface (Phase 1.5 doctrine: mm/bmm/addmm/
|
|
59
|
+
# conv2d only) — explicit list, not a gc walk. A new autotuned kernel
|
|
60
|
+
# is added here the day its autotune exception is granted. Supervisor
|
|
61
|
+
# ruling 2026-08-16: NO autotuner lives outside this regime (the one
|
|
62
|
+
# historical candidate outside it, the FlagGems kernels/utils remnant,
|
|
63
|
+
# was dead code and was removed 2026-08-17). Since the owner directive of
|
|
64
|
+
# 2026-09-06 the regime is the CERTIFIED DIRECTORY (`kernels/
|
|
65
|
+
# autotune_certified.py`, `config/autotune/<vendor>/<profile>/`): a shape it
|
|
66
|
+
# holds is applied at load; a shape it lacks sweeps at runtime with the
|
|
67
|
+
# consensus screen and lands HERE, the machine's local replay cache.
|
|
68
|
+
_KERNEL_SITES = (
|
|
69
|
+
("nbxtensor.ops.matmul", "matmul_kernel"),
|
|
70
|
+
("nbxtensor.ops.matmul", "addmm_kernel"),
|
|
71
|
+
("nbxtensor.ops.baddbmm_op", "baddbmm_kernel"),
|
|
72
|
+
("nbxtensor.ops.conv2d", "conv2d_forward_kernel"),
|
|
73
|
+
("nbxtensor.ops.depthwise_conv2d", "depthwise_conv2d_kernel"),
|
|
74
|
+
# workstream 1: certified per key like the five above (the softmax's module is imported with the wrappers; the
|
|
75
|
+
# matrix-unit flash's only when an attention takes it — a certification before any attention found no tuner)
|
|
76
|
+
("nbxtensor.ops.softmax", "softmax_forward_kernel"),
|
|
77
|
+
("nbxtensor.ops.flash_attention_m8n8k4", "flash_attention_m8n8k4_kernel"),
|
|
78
|
+
# C3: the tl.dot flash's tile, certified where the profile declares no hand table (`sdpa_thresholds`)
|
|
79
|
+
("nbxtensor.ops.flash_attention", "flash_attention_certified_kernel"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _arch_fingerprint() -> Optional[str]:
|
|
84
|
+
"""None when the driver query fails — no fingerprint means NO
|
|
85
|
+
artifact (capture/seed no-op), never a shared cross-arch bucket
|
|
86
|
+
(an sm_80 config seeded onto sm_70 fails at compile inside a user
|
|
87
|
+
run)."""
|
|
88
|
+
try:
|
|
89
|
+
from nbxtensor.backend.launcher import target as _nbx_target # engine data, no driver probe (R33)
|
|
90
|
+
target = _nbx_target()
|
|
91
|
+
return f"{target.backend}-{target.arch}"
|
|
92
|
+
except Exception:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _running_generator() -> Optional[str]:
|
|
97
|
+
"""The code generator about to run, as the certified directory's proofs record it
|
|
98
|
+
(`kernels.autotune_certified.running_generator`: the Triton distribution, the target, and the
|
|
99
|
+
out-of-tree backend's source hash where one generates the code). None when it cannot be named."""
|
|
100
|
+
try:
|
|
101
|
+
from nbxtensor.ops.autotune_certified import running_generator
|
|
102
|
+
return running_generator()
|
|
103
|
+
except Exception:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _generator_tag() -> Optional[str]:
|
|
108
|
+
"""A file-safe tag of the running generator: its backend hash's short id where the backend is
|
|
109
|
+
out of tree (`msl-v0.1-42e` -> `42e`), else the whole identity flattened."""
|
|
110
|
+
gen = _running_generator()
|
|
111
|
+
if not gen:
|
|
112
|
+
return None
|
|
113
|
+
import re as _re
|
|
114
|
+
m = _re.search(r"msl-v[\d.]+-([0-9a-f]{3,})", gen) # the proof records the short id (`msl-v0.1-42e`)
|
|
115
|
+
tag = m.group(1) if m else _re.sub(r"[^0-9A-Za-z.]+", "_", gen).strip("_")
|
|
116
|
+
return tag[:40] or None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _artifact_path() -> Optional[str]:
|
|
120
|
+
"""The replay artifact of THIS backend, arch AND code generator. A configuration is a property
|
|
121
|
+
of the compiler that produced it (the certified directory's generator door, 2026-09-06): a
|
|
122
|
+
sweep made under one generator is never seeded under another. Before 2026-09-28 the file was
|
|
123
|
+
named by arch alone, and on the Mac the two "served" gates of the triton-ext bump took 545 of
|
|
124
|
+
their 1 400 key uses from sweeps the retired compiler had made. No generator name, no artifact."""
|
|
125
|
+
arch = _arch_fingerprint()
|
|
126
|
+
if arch is None:
|
|
127
|
+
return None
|
|
128
|
+
gen = _generator_tag()
|
|
129
|
+
if gen is None:
|
|
130
|
+
return None
|
|
131
|
+
return os.path.join(_dir(), f"autotune_configs_{arch}-{gen}.json")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _autotuners() -> Iterator[Tuple[str, object]]:
|
|
135
|
+
"""Every Autotuner the process has loaded from the kernel library — the
|
|
136
|
+
five sanctioned sites first (imported on demand), then every other
|
|
137
|
+
`neurobrix.kernels.ops.*` module already in sys.modules. Since 2026-09-05
|
|
138
|
+
the engine is the only persistence of a sweep (upstream's on-disk cache
|
|
139
|
+
keyed its files by a torch-importing driver probe and was dropped), so
|
|
140
|
+
every tuner's selections are captured, not only the matmul class's."""
|
|
141
|
+
import importlib
|
|
142
|
+
import sys
|
|
143
|
+
from triton.runtime.autotuner import Autotuner
|
|
144
|
+
seen = set()
|
|
145
|
+
for mod_name, attr in _KERNEL_SITES:
|
|
146
|
+
try:
|
|
147
|
+
obj = getattr(importlib.import_module(mod_name), attr, None)
|
|
148
|
+
except Exception:
|
|
149
|
+
continue
|
|
150
|
+
if isinstance(obj, Autotuner):
|
|
151
|
+
seen.add(id(obj))
|
|
152
|
+
yield f"{mod_name}.{attr}", obj
|
|
153
|
+
for mod_name, mod in list(sys.modules.items()):
|
|
154
|
+
# the kernel files: the library's (`nbxtensor.ops`) and the engine's still to move (`neurobrix.kernels.ops`)
|
|
155
|
+
if not mod_name.startswith(("nbxtensor.ops.", "neurobrix.kernels.ops.")) or mod is None:
|
|
156
|
+
continue
|
|
157
|
+
for attr, obj in list(vars(mod).items()):
|
|
158
|
+
if isinstance(obj, Autotuner) and id(obj) not in seen:
|
|
159
|
+
seen.add(id(obj))
|
|
160
|
+
yield f"{mod_name}.{attr}", obj
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _config_to_dict(cfg) -> Dict:
|
|
164
|
+
return {"kwargs": dict(cfg.kwargs), "num_warps": cfg.num_warps,
|
|
165
|
+
"num_stages": cfg.num_stages, "num_ctas": cfg.num_ctas,
|
|
166
|
+
"maxnreg": cfg.maxnreg}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _config_from_dict(d):
|
|
170
|
+
import triton
|
|
171
|
+
return triton.Config(dict(d["kwargs"]), num_warps=d["num_warps"],
|
|
172
|
+
num_stages=d["num_stages"], num_ctas=d["num_ctas"],
|
|
173
|
+
maxnreg=d.get("maxnreg"))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _exclusions_path() -> Optional[str]:
|
|
177
|
+
"""Where the correctness screen's exclusions are kept, beside the sweep."""
|
|
178
|
+
path = _artifact_path()
|
|
179
|
+
if path is None:
|
|
180
|
+
return None
|
|
181
|
+
return path.replace("autotune_configs_", "autotune_exclusions_")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def record_screen_exclusions(entries) -> int:
|
|
185
|
+
"""Persist configs the correctness screen refused, so a later sweep and its readers see them.
|
|
186
|
+
|
|
187
|
+
A config excluded for being WRONG and a config that merely lost on speed
|
|
188
|
+
are indistinguishable in a sweep that records only the winner — and they
|
|
189
|
+
are not the same fact at all. One is a tuning outcome; the other is a
|
|
190
|
+
backend defect with a shape attached.
|
|
191
|
+
|
|
192
|
+
Keyed by kernel, key and config so repeated runs merge instead of
|
|
193
|
+
accumulating duplicates. Best-effort: a sweep that cannot be written must
|
|
194
|
+
never fail a launch, and the exclusions are also printed as they happen.
|
|
195
|
+
"""
|
|
196
|
+
path = _exclusions_path()
|
|
197
|
+
if path is None or not entries:
|
|
198
|
+
return 0
|
|
199
|
+
try:
|
|
200
|
+
os.makedirs(_dir(), exist_ok=True)
|
|
201
|
+
stored: Dict[str, Dict] = {}
|
|
202
|
+
try:
|
|
203
|
+
with open(path) as f:
|
|
204
|
+
stored = json.load(f)
|
|
205
|
+
except (OSError, ValueError):
|
|
206
|
+
stored = {}
|
|
207
|
+
added = 0
|
|
208
|
+
for entry in entries:
|
|
209
|
+
entry = dict(entry)
|
|
210
|
+
ident = f"{entry.get('kernel')}::{entry.get('key')!r}::{entry.get('config')}"
|
|
211
|
+
if ident not in stored:
|
|
212
|
+
stored[ident] = entry
|
|
213
|
+
added += 1
|
|
214
|
+
if added:
|
|
215
|
+
with open(path, "w") as f:
|
|
216
|
+
json.dump(stored, f, indent=1, default=str)
|
|
217
|
+
return added
|
|
218
|
+
except OSError:
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def screen_exclusions() -> Dict[str, Dict]:
|
|
223
|
+
"""Everything the screen has refused on this machine."""
|
|
224
|
+
path = _exclusions_path()
|
|
225
|
+
if path is None:
|
|
226
|
+
return {}
|
|
227
|
+
try:
|
|
228
|
+
with open(path) as f:
|
|
229
|
+
return json.load(f)
|
|
230
|
+
except (OSError, ValueError):
|
|
231
|
+
return {}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
#: Keys whose config was chosen WITHOUT measurement -- e.g. the bench was
|
|
235
|
+
#: skipped because its arguments alone exceed the machine's available memory,
|
|
236
|
+
#: so timing candidates would have measured the swap and not the kernels. A
|
|
237
|
+
#: choice made without measurement must never be recorded as if measured:
|
|
238
|
+
#: persisted, it would outlive the pressure that forced it and keep deciding
|
|
239
|
+
#: on machines and days it knows nothing about.
|
|
240
|
+
_UNMEASURED: set = set()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def mark_unmeasured(at, key) -> None:
|
|
244
|
+
_UNMEASURED.add((id(at), tuple(key) if isinstance(key, (list, tuple)) else key))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def is_unmeasured(at, key) -> bool:
|
|
248
|
+
return (id(at), tuple(key) if isinstance(key, (list, tuple)) else key) in _UNMEASURED
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def capture() -> int:
|
|
252
|
+
"""Merge every selected config into the artifact. Returns the
|
|
253
|
+
number of NEW entries written (0 = artifact already covers this
|
|
254
|
+
process's selections)."""
|
|
255
|
+
path = _artifact_path()
|
|
256
|
+
if path is None:
|
|
257
|
+
return 0
|
|
258
|
+
# THE RECORD CARRIES WHAT IT IS. A configuration the screen seated without
|
|
259
|
+
# an oracle is the fastest among candidates nothing verified, and until the
|
|
260
|
+
# owner's ruling of 2026-09-12 it was written here exactly like one an fp64
|
|
261
|
+
# oracle had adjudicated. The engine still runs and the screen still ranks
|
|
262
|
+
# by speed; what it may no longer do is produce a record that reads as a
|
|
263
|
+
# validation. The certified directory is a different path entirely
|
|
264
|
+
# (`config/autotune/`, filled only by `neurobrix autotune certify`), so
|
|
265
|
+
# nothing here can reach it — this is the mention where it IS recorded.
|
|
266
|
+
try:
|
|
267
|
+
from nbxtensor.backend.launcher import adjudicated as _adjudicated, unscreened as _unscreened
|
|
268
|
+
unverified = {(u.kernel, repr(u.key)): u.reason for u in _unscreened()}
|
|
269
|
+
verified = _adjudicated()
|
|
270
|
+
except Exception: # never turn a record into a failure
|
|
271
|
+
unverified, verified = {}, {}
|
|
272
|
+
|
|
273
|
+
entries: Dict[str, Dict] = {}
|
|
274
|
+
skipped_unmeasured = 0
|
|
275
|
+
for qual, at in _autotuners():
|
|
276
|
+
short = qual.rsplit(".", 1)[-1]
|
|
277
|
+
for key, cfg in getattr(at, "cache", {}).items():
|
|
278
|
+
if is_unmeasured(at, key):
|
|
279
|
+
skipped_unmeasured += 1
|
|
280
|
+
continue
|
|
281
|
+
rec = _config_to_dict(cfg)
|
|
282
|
+
if (id(at), key) in _TIMINGS:
|
|
283
|
+
rec["timing"] = _TIMINGS[(id(at), key)]
|
|
284
|
+
adjudicator = verified.get((short, repr(key)))
|
|
285
|
+
if adjudicator is not None:
|
|
286
|
+
rec["screened"] = True
|
|
287
|
+
rec["screened_by"] = adjudicator
|
|
288
|
+
reason = unverified.get((short, repr(key)))
|
|
289
|
+
if reason is not None:
|
|
290
|
+
rec["screened"] = False
|
|
291
|
+
rec["unscreened_reason"] = reason
|
|
292
|
+
rec["provenance"] = ("fastest among candidates nothing "
|
|
293
|
+
"verified — NOT a validated setting")
|
|
294
|
+
# A shape over the screening budget is screened on ROW WINDOWS, and the proof
|
|
295
|
+
# names them: what was verified and what was not must always be readable
|
|
296
|
+
# (owner, 2026-09-23). Recorded for the key whether the windows were adjudicated
|
|
297
|
+
# by the fp64 oracle or, where no windowed oracle covers the kernel, by consensus.
|
|
298
|
+
try:
|
|
299
|
+
from nbxtensor.backend.launcher import screen_windows_of
|
|
300
|
+
win = screen_windows_of(key)
|
|
301
|
+
except Exception: # noqa: BLE001
|
|
302
|
+
win = None
|
|
303
|
+
if win is not None:
|
|
304
|
+
rec["screened"] = True
|
|
305
|
+
rec["screened_on_windows"] = win["windows"]
|
|
306
|
+
rec["screened_by"] = win["adjudicated_by"]
|
|
307
|
+
rec["screen_kept"] = f"{win['kept']} of {win['candidates']}"
|
|
308
|
+
rec["provenance"] = (
|
|
309
|
+
f"verified on {win['windows']} by {win['adjudicated_by']}; the rest of "
|
|
310
|
+
f"the output and the input buffers were NOT compared")
|
|
311
|
+
entries[f"{qual}::{key!r}"] = rec
|
|
312
|
+
if skipped_unmeasured:
|
|
313
|
+
print(f"[AUTOTUNE_CACHE] {skipped_unmeasured} choice(s) made without "
|
|
314
|
+
f"measurement were NOT persisted", flush=True)
|
|
315
|
+
if not entries:
|
|
316
|
+
return 0
|
|
317
|
+
try:
|
|
318
|
+
os.makedirs(_dir(), exist_ok=True)
|
|
319
|
+
stored: Dict[str, Dict] = {}
|
|
320
|
+
try:
|
|
321
|
+
with open(path) as f:
|
|
322
|
+
stored = json.load(f)
|
|
323
|
+
except (OSError, ValueError):
|
|
324
|
+
stored = {}
|
|
325
|
+
new = {k: v for k, v in entries.items() if k not in stored}
|
|
326
|
+
if new:
|
|
327
|
+
stored.update(new)
|
|
328
|
+
with open(path, "w") as f:
|
|
329
|
+
json.dump(stored, f)
|
|
330
|
+
return len(new)
|
|
331
|
+
except OSError:
|
|
332
|
+
return 0
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def seed() -> int:
|
|
336
|
+
"""Insert stored configs into the Autotuner caches. Returns the
|
|
337
|
+
number of entries seeded.
|
|
338
|
+
|
|
339
|
+
MEMBERSHIP GATE (staleness safety): a stored config is seeded ONLY
|
|
340
|
+
if it is a member of the Autotuner's CURRENT declared config space
|
|
341
|
+
(kwargs + num_warps + num_stages equality against at.configs) — a
|
|
342
|
+
member is by definition a config a fresh bench could legally
|
|
343
|
+
select for the current kernel source and current policy. This
|
|
344
|
+
refuses resurrection of configs removed as buggy, refuses configs
|
|
345
|
+
whose constexpr names drifted with a source edit, and honors
|
|
346
|
+
NBX_DISABLE_AUTOTUNE's pinned single-config list (one member).
|
|
347
|
+
Non-members and non-literal keys are skipped: that shape simply
|
|
348
|
+
re-tunes, exactly the pre-E2 behavior."""
|
|
349
|
+
path = _artifact_path()
|
|
350
|
+
if path is None:
|
|
351
|
+
return 0
|
|
352
|
+
try:
|
|
353
|
+
with open(path) as f:
|
|
354
|
+
stored = json.load(f)
|
|
355
|
+
except (OSError, ValueError):
|
|
356
|
+
return 0
|
|
357
|
+
return _seed_entries(stored)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _seed_entries(stored: Dict[str, Dict]) -> int:
|
|
361
|
+
"""Insert `stored` configs into the Autotuner caches under the
|
|
362
|
+
membership gate documented on `seed()`. Returns the number seeded."""
|
|
363
|
+
seeded = 0
|
|
364
|
+
for qual, at in _autotuners():
|
|
365
|
+
prefix = f"{qual}::"
|
|
366
|
+
space = {(tuple(sorted(c.kwargs.items())), c.num_warps,
|
|
367
|
+
c.num_stages) for c in getattr(at, "configs", [])}
|
|
368
|
+
for k, d in stored.items():
|
|
369
|
+
if not k.startswith(prefix):
|
|
370
|
+
continue
|
|
371
|
+
member = (tuple(sorted(dict(d["kwargs"]).items())),
|
|
372
|
+
d["num_warps"], d["num_stages"])
|
|
373
|
+
if member not in space:
|
|
374
|
+
continue
|
|
375
|
+
try:
|
|
376
|
+
key = ast.literal_eval(k[len(prefix):])
|
|
377
|
+
except (ValueError, SyntaxError):
|
|
378
|
+
continue
|
|
379
|
+
if key not in at.cache:
|
|
380
|
+
try:
|
|
381
|
+
at.cache[key] = _config_from_dict(d)
|
|
382
|
+
seeded += 1
|
|
383
|
+
except Exception:
|
|
384
|
+
continue
|
|
385
|
+
return seeded
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
# The Autotuner's key, and the bench margin of a key this process measured
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
def key_of(at, args, kwargs) -> tuple:
|
|
392
|
+
"""The Autotuner's cache key for a call, as `Autotuner.run` forms it."""
|
|
393
|
+
_args = {**dict(zip(at.arg_names, args)), **kwargs}
|
|
394
|
+
key = [_args[k] for k in at.keys if k in _args]
|
|
395
|
+
for _, arg in _args.items():
|
|
396
|
+
if hasattr(arg, "dtype"):
|
|
397
|
+
key.append(str(arg.dtype))
|
|
398
|
+
return tuple(key)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _qual_of(at) -> Optional[str]:
|
|
402
|
+
for qual, obj in _autotuners():
|
|
403
|
+
if obj is at:
|
|
404
|
+
return qual
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
_TIMINGS: Dict[Tuple[int, tuple], Dict] = {} # (id(tuner), key) -> best/second/margin of a bench this process ran
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def note_timings(at, key: tuple, timings) -> None:
|
|
412
|
+
"""Keep the bench of a key this process just measured: the best time, the
|
|
413
|
+
second-best, and the MARGIN between them. A cache that records only the
|
|
414
|
+
winner cannot say whether the winner was clear or a near-tie the timer
|
|
415
|
+
could flip on the next run — a gate comparing two sweeps needs exactly
|
|
416
|
+
that to tell a change of choice from noise. Written beside the config by
|
|
417
|
+
`capture()`."""
|
|
418
|
+
if not timings:
|
|
419
|
+
return
|
|
420
|
+
try:
|
|
421
|
+
times = sorted(float(t) for t in timings.values() if t is not None and float(t) == float(t))
|
|
422
|
+
except (TypeError, ValueError):
|
|
423
|
+
return
|
|
424
|
+
if not times:
|
|
425
|
+
return
|
|
426
|
+
best = times[0]
|
|
427
|
+
second = times[1] if len(times) > 1 else None
|
|
428
|
+
_TIMINGS[(id(at), key)] = {
|
|
429
|
+
"best_ms": best, "second_ms": second, "candidates": len(times),
|
|
430
|
+
"margin": (second / best - 1.0) if (second is not None and best > 0) else None}
|