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 @@
|
|
|
1
|
+
"""Dtypes: the one table of widths (`dtypes.yml`) and its reader (`itemsize`)."""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Host conversions between bfloat16, float16 and float32 — the one module that does them.
|
|
2
|
+
|
|
3
|
+
numpy has no bfloat16. A bfloat16 array on the host is carried as its 16-bit patterns (`uint16`), or as the 2-byte
|
|
4
|
+
void array a bfloat16 tensor's host copy comes back as (`<V2`, the array interface's typestr for a type numpy does
|
|
5
|
+
not know). Every conversion between that carrier and real numbers lives here, once: rounding float32 to bfloat16,
|
|
6
|
+
widening bfloat16 to float32, and narrowing bfloat16 to float16.
|
|
7
|
+
|
|
8
|
+
Pure functions of numpy arrays: no device, no global state, no thread pool of their own (R-NBXT 11). A caller that
|
|
9
|
+
splits a large array across workers calls them on each span with `out=`.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
__all__ = ["f32_to_bf16_bits", "bf16_bits_to_f32", "bf16_bits_to_f16", "bf16_bits", "bf16_abs_max", "saturate_scalar"]
|
|
18
|
+
|
|
19
|
+
# The quiet NaN every NaN becomes, sign dropped: PyTorch's `round_to_nearest_even`
|
|
20
|
+
# (torch/headeronly/util/BFloat16.h: `if (std::isnan(src)) return UINT16_C(0x7FC0);`).
|
|
21
|
+
_QUIET_NAN = np.uint16(0x7FC0)
|
|
22
|
+
# The elements one step of the rounding works on: its temporaries are one step's, never the operand's size (a 5.1 GB
|
|
23
|
+
# operand took four full-size uint32 temporaries and the Mac's certifier to its 14 GB guard, 2026-09-28).
|
|
24
|
+
_STEP = 1 << 18
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def bf16_bits(a: np.ndarray) -> np.ndarray:
|
|
28
|
+
"""The `uint16` patterns of a bfloat16 host carrier: the array itself when it already is `uint16`, a view when
|
|
29
|
+
it is the 2-byte void carrier. Anything else is refused by name."""
|
|
30
|
+
a = np.asarray(a)
|
|
31
|
+
if a.dtype == np.uint16:
|
|
32
|
+
return a
|
|
33
|
+
if a.dtype.kind == "V" and a.dtype.itemsize == 2:
|
|
34
|
+
return np.ascontiguousarray(a).view(np.uint16)
|
|
35
|
+
raise TypeError(f"a bfloat16 host carrier is uint16 bits or a 2-byte void array, not {a.dtype}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def f32_to_bf16_bits(a: np.ndarray, out: Optional[np.ndarray] = None) -> np.ndarray:
|
|
39
|
+
"""float32 values narrowed to bfloat16, returned as their `uint16` patterns: round to nearest, ties to even —
|
|
40
|
+
the rounding `torch.Tensor.to(torch.bfloat16)` applies and the device cast applies. `+0x7FFF` alone rounds an
|
|
41
|
+
exact half down; `((u >> 16) & 1)` adds the one that makes it ties-to-even. Values past the bfloat16 maximum
|
|
42
|
+
become infinities; every NaN becomes the quiet NaN 0x7FC0. Streamed in fixed steps: the peak is the output plus
|
|
43
|
+
one step."""
|
|
44
|
+
a = np.ascontiguousarray(a, dtype=np.float32)
|
|
45
|
+
flat = a.reshape(-1)
|
|
46
|
+
u = flat.view(np.uint32)
|
|
47
|
+
bits = np.empty(flat.shape, dtype=np.uint16) if out is None else out.reshape(-1)
|
|
48
|
+
for i in range(0, flat.size, _STEP):
|
|
49
|
+
uc = u[i:i + _STEP]
|
|
50
|
+
t = np.right_shift(uc, np.uint32(16))
|
|
51
|
+
np.bitwise_and(t, np.uint32(1), out=t)
|
|
52
|
+
np.add(t, np.uint32(0x7FFF), out=t)
|
|
53
|
+
np.add(t, uc, out=t)
|
|
54
|
+
np.right_shift(t, np.uint32(16), out=t)
|
|
55
|
+
bits[i:i + _STEP] = t
|
|
56
|
+
nan = np.isnan(flat[i:i + _STEP])
|
|
57
|
+
if nan.any():
|
|
58
|
+
bits[i:i + _STEP][nan] = _QUIET_NAN
|
|
59
|
+
return bits.reshape(a.shape) if out is None else out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bf16_bits_to_f32(bits: np.ndarray) -> np.ndarray:
|
|
63
|
+
"""The exact float32 values of bfloat16 patterns (`uint16`, or the 2-byte void carrier): a bfloat16 is the top
|
|
64
|
+
sixteen bits of a float32, so the widening loses nothing."""
|
|
65
|
+
b = np.ascontiguousarray(bf16_bits(bits))
|
|
66
|
+
return (b.astype(np.uint32) << np.uint32(16)).view(np.float32).reshape(b.shape)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def bf16_bits_to_f16(bits: np.ndarray, out: Optional[np.ndarray] = None) -> np.ndarray:
|
|
70
|
+
"""bfloat16 patterns narrowed to float16, exactly as widening to float32 then numpy's float32 -> float16 cast
|
|
71
|
+
(round to nearest even; past the float16 range: infinity). `out`, when given, is a float16 array of the same
|
|
72
|
+
number of elements the result is written into (a caller splitting the work across workers passes spans)."""
|
|
73
|
+
src = np.ascontiguousarray(bf16_bits(bits)).reshape(-1)
|
|
74
|
+
dst = np.empty(src.shape, dtype=np.float16) if out is None else out.reshape(-1)
|
|
75
|
+
np.copyto(dst, (src.astype(np.uint32) << np.uint32(16)).view(np.float32), casting="unsafe")
|
|
76
|
+
return dst.reshape(np.shape(bits)) if out is None else out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def bf16_abs_max(bits: np.ndarray) -> float:
|
|
80
|
+
"""The largest magnitude among bfloat16 patterns, NaN ignored (an infinity is infinite): what a caller compares to
|
|
81
|
+
a narrower dtype's `finite_max` to know whether the values fit it. Streamed in fixed steps."""
|
|
82
|
+
flat = np.ascontiguousarray(bf16_bits(bits)).reshape(-1)
|
|
83
|
+
top = 0.0
|
|
84
|
+
for i in range(0, flat.size, _STEP):
|
|
85
|
+
v = np.abs((flat[i:i + _STEP].astype(np.uint32) << np.uint32(16)).view(np.float32))
|
|
86
|
+
v = v[~np.isnan(v)]
|
|
87
|
+
if v.size:
|
|
88
|
+
top = max(top, float(v.max()))
|
|
89
|
+
return top
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def saturate_scalar(value, dtype):
|
|
93
|
+
"""A Python number clamped to the finite range of a half floating `dtype` (a name or an enum member; the range is
|
|
94
|
+
the dtype table's `max`), anything else unchanged: a bool, a non-number, a dtype wider than two bytes or not
|
|
95
|
+
floating, a NaN, an infinity the graph wrote on purpose. A mask sentinel written for one half format then holds in
|
|
96
|
+
the other (bfloat16's -3.39e38 becomes float16's -65504)."""
|
|
97
|
+
from nbxtensor.dtype.itemsize import canonical_name, finite_max, itemsize, kind
|
|
98
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
99
|
+
return value
|
|
100
|
+
name = canonical_name(dtype)
|
|
101
|
+
if kind(name) != "float" or itemsize(name) != 2:
|
|
102
|
+
return value
|
|
103
|
+
v = float(value)
|
|
104
|
+
if v != v or v in (float("inf"), float("-inf")):
|
|
105
|
+
return value
|
|
106
|
+
top = finite_max(name)
|
|
107
|
+
return max(-top, min(top, v))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# The dtype table: the width of every dtype the engine knows, one entry each.
|
|
2
|
+
#
|
|
3
|
+
# Read through ONE reader and nowhere else: `nbxtensor/dtype/itemsize.py` (both engine branches, Prism, NBXTensor, the
|
|
4
|
+
# wrappers, the launch keys, the certifier). A dtype absent from this file is refused BY NAME: no width is ever guessed
|
|
5
|
+
# (the silent `.get(dtype, 4)` sized a complex64 at half its bytes until 2026-09-22, Kokoro-82M's vocoder).
|
|
6
|
+
#
|
|
7
|
+
# Per entry:
|
|
8
|
+
# bits storage bits per element (bytes = bits / 8 for every dense dtype)
|
|
9
|
+
# typestr the array-interface type string of the element (`__cuda_array_interface__`, numpy's
|
|
10
|
+
# `__array_interface__`); bfloat16, which numpy cannot name, is the 2-byte void '<V2'
|
|
11
|
+
# numpy the numpy dtype name, when numpy has one
|
|
12
|
+
# aliases other spellings a caller may name it by (torch's `half`, `float`, `double`, `int`,
|
|
13
|
+
# `long`, `byte`)
|
|
14
|
+
# kind float, int, uint, bool or complex — the class the promotion rule reads
|
|
15
|
+
# (nbxtensor.dtype.promotion)
|
|
16
|
+
# max a float's largest finite value: (2 - 2^(1-p)) x 2^emax for an IEEE-style format of p
|
|
17
|
+
# significant bits (binary64, binary32, binary16; bfloat16 = binary32's exponent with
|
|
18
|
+
# 8 significant bits; TF32 = 11 significant bits), and the OCP OFP8 v1.0 table for the
|
|
19
|
+
# two float8 formats (E4M3: 448, no infinity; E5M2: 57344)
|
|
20
|
+
# safetensors the dtype code a safetensors header writes for it (spec:
|
|
21
|
+
# https://github.com/huggingface/safetensors — `Dtype`), when it has one
|
|
22
|
+
# autotune_keys the spellings a launch key / certified directory carries for it
|
|
23
|
+
# (`kernels/autotune_certified._DTYPES`)
|
|
24
|
+
# without_fp64 what a store with no float64 holds in its place (the device's or the engine's
|
|
25
|
+
# fp64 ceiling: MPS refuses a float64 tensor and the vendors' own code narrows to
|
|
26
|
+
# float32 there; the Triton kernels are fp32-max, `TritonSequence._parse_dtype`)
|
|
27
|
+
#
|
|
28
|
+
# A hi+lo split operand is TWO tensors of the low dtype: that is a representation, computed by
|
|
29
|
+
# the readers' `representation_bytes(dtype, numel, split=True)`, never an entry here.
|
|
30
|
+
|
|
31
|
+
dtypes:
|
|
32
|
+
# IEEE 754-2008 binary64.
|
|
33
|
+
float64: {bits: 64, kind: float, max: 1.7976931348623157e+308, safetensors: F64, autotune_keys: [fp64], without_fp64: float32, typestr: '<f8', numpy: float64, aliases: [double]}
|
|
34
|
+
# IEEE 754-2008 binary32.
|
|
35
|
+
float32: {bits: 32, kind: float, max: 3.4028234663852886e+38, safetensors: F32, autotune_keys: [fp32], typestr: '<f4', numpy: float32, aliases: [float]}
|
|
36
|
+
# NVIDIA TF32 (A100 architecture whitepaper): a tensor-core COMPUTE format of 19 significant
|
|
37
|
+
# bits, stored and moved in a full 32-bit fp32 word.
|
|
38
|
+
tfloat32: {bits: 32, kind: float, max: 3.4011621342146535e+38}
|
|
39
|
+
# Google Brain bfloat16: the top 16 bits of a binary32.
|
|
40
|
+
bfloat16: {bits: 16, kind: float, max: 3.3895313892515355e+38, safetensors: BF16, autotune_keys: [bf16], typestr: '<V2'}
|
|
41
|
+
# IEEE 754-2008 binary16.
|
|
42
|
+
float16: {bits: 16, kind: float, max: 65504.0, safetensors: F16, autotune_keys: [fp16], typestr: '<f2', numpy: float16, aliases: [half]}
|
|
43
|
+
# OCP 8-bit Floating Point Specification (OFP8) v1.0, E4M3 (finite, no inf) and E5M2;
|
|
44
|
+
# torch.float8_e4m3fn / torch.float8_e5m2. The fnuz variants appear nowhere in this code.
|
|
45
|
+
float8_e4m3fn: {bits: 8, kind: float, max: 448.0, safetensors: F8_E4M3}
|
|
46
|
+
float8_e5m2: {bits: 8, kind: float, max: 57344.0, safetensors: F8_E5M2}
|
|
47
|
+
# Two's-complement integers of the stated width (C99 <stdint.h>, numpy, torch).
|
|
48
|
+
int64: {bits: 64, kind: int, safetensors: I64, autotune_keys: [int64], typestr: '<i8', numpy: int64, aliases: [long]}
|
|
49
|
+
int32: {bits: 32, kind: int, safetensors: I32, autotune_keys: [int32], typestr: '<i4', numpy: int32, aliases: [int]}
|
|
50
|
+
int16: {bits: 16, kind: int, safetensors: I16, autotune_keys: [int16], typestr: '<i2', numpy: int16}
|
|
51
|
+
int8: {bits: 8, kind: int, safetensors: I8, autotune_keys: [int8], typestr: '<i1', numpy: int8}
|
|
52
|
+
# Unsigned integers of the stated width; uint16/32/64 are carried by launch keys (a bf16 bit
|
|
53
|
+
# carrier, sort and RNG kernels), not by any container.
|
|
54
|
+
uint64: {bits: 64, kind: uint, autotune_keys: [uint64]}
|
|
55
|
+
uint32: {bits: 32, kind: uint, autotune_keys: [uint32]}
|
|
56
|
+
uint16: {bits: 16, kind: uint, autotune_keys: [uint16]}
|
|
57
|
+
uint8: {bits: 8, kind: uint, safetensors: U8, autotune_keys: [uint8], typestr: '<u1', numpy: uint8, aliases: [byte]}
|
|
58
|
+
# One byte per element in torch and numpy (no bit packing); `int1` is Triton's spelling.
|
|
59
|
+
bool: {bits: 8, kind: bool, safetensors: BOOL, autotune_keys: [int1, bool], typestr: '|b1', numpy: bool}
|
|
60
|
+
# Two binary32 / two binary64, interleaved [real, imag] (numpy and torch layout).
|
|
61
|
+
complex64: {bits: 64, kind: complex, typestr: '<c8', numpy: complex64}
|
|
62
|
+
complex128: {bits: 128, kind: complex, without_fp64: complex64, typestr: '<c16', numpy: complex128}
|
|
63
|
+
|
|
64
|
+
# Packed weight encodings: bits per element is a fraction of a byte and the group scales live
|
|
65
|
+
# beside the packed values. Only the encodings this engine executes
|
|
66
|
+
# (`core/runtime/weight_encoding.SUPPORTED_WEIGHT_ENCODINGS`).
|
|
67
|
+
packed:
|
|
68
|
+
# Weight-only int4, groups of 128 along in_features, asymmetric: one float16 scale and one
|
|
69
|
+
# float16 minimum per group; 8 nibbles per int32 (core/runtime/weight_encoding.py).
|
|
70
|
+
int4-g128-asym: {bits: 4, group_size: 128, scale_dtype: float16, min_dtype: float16}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""The dtype table and its reader — the ONE place a dtype's width is written (`dtypes.yml`, beside this module).
|
|
2
|
+
|
|
3
|
+
Every width a dtype has is read from `dtypes.yml` through this module, and through no other reader: NeuroBrix's two
|
|
4
|
+
engine branches read it here.
|
|
5
|
+
|
|
6
|
+
Torch-free and numpy-free, at import and at execution.
|
|
7
|
+
|
|
8
|
+
A dtype is named as the code passes it today: a name ("float16"), a torch spelling ("torch.float16", as a traced
|
|
9
|
+
graph writes some — read as text, torch is never imported), or an enum member read by its NAME (NBXDtype is an IntEnum,
|
|
10
|
+
and `str()` of one is its bare number on Python 3.11+ — never parsed). A name the table does not carry is refused by
|
|
11
|
+
name, never given a width.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import copy
|
|
16
|
+
import functools
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import Any, Dict, Tuple
|
|
20
|
+
|
|
21
|
+
_SOURCE = "nbxtensor/dtype/dtypes.yml"
|
|
22
|
+
_TABLE = Path(__file__).resolve().parent / "dtypes.yml"
|
|
23
|
+
_TORCH_PREFIX = "torch."
|
|
24
|
+
_KINDS = ("float", "int", "uint", "bool", "complex")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@functools.lru_cache(maxsize=1)
|
|
28
|
+
def _raw_table() -> Dict[str, Any]:
|
|
29
|
+
import yaml
|
|
30
|
+
if not _TABLE.is_file():
|
|
31
|
+
raise FileNotFoundError(f"ZERO FALLBACK: the dtype table is not found: {_TABLE}")
|
|
32
|
+
table = yaml.safe_load(_TABLE.read_text()) or {}
|
|
33
|
+
for section in ("dtypes", "packed"):
|
|
34
|
+
if not isinstance(table.get(section), dict):
|
|
35
|
+
raise KeyError(f"ZERO FALLBACK: {_TABLE} carries no {section!r} section")
|
|
36
|
+
return table
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def dtype_table() -> Dict[str, Any]:
|
|
40
|
+
"""The whole table as written (`dtypes`, `packed`, and every other section), a copy: the reader a client keeps of
|
|
41
|
+
its own is fed from here, never from a second file."""
|
|
42
|
+
return copy.deepcopy(_raw_table())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@functools.lru_cache(maxsize=1)
|
|
46
|
+
def _tables() -> Tuple[Dict[str, Dict[str, Any]], Dict[str, Dict[str, Any]], Dict[str, str], Dict[str, str]]:
|
|
47
|
+
table = _raw_table()
|
|
48
|
+
dense, packed = dict(table["dtypes"]), dict(table["packed"])
|
|
49
|
+
for name, entry in dense.items():
|
|
50
|
+
bits = entry.get("bits")
|
|
51
|
+
if not isinstance(bits, int) or bits <= 0 or bits % 8:
|
|
52
|
+
raise ValueError(f"ZERO FALLBACK: {_SOURCE} gives dtype {name!r} {bits!r} bits — a dense "
|
|
53
|
+
f"dtype is a whole number of bytes")
|
|
54
|
+
if entry.get("kind") not in _KINDS:
|
|
55
|
+
raise ValueError(f"ZERO FALLBACK: {_SOURCE} gives dtype {name!r} kind {entry.get('kind')!r} — one of "
|
|
56
|
+
f"{', '.join(_KINDS)}")
|
|
57
|
+
for name, entry in packed.items():
|
|
58
|
+
for key in ("bits", "group_size", "scale_dtype", "min_dtype"):
|
|
59
|
+
if key not in entry:
|
|
60
|
+
raise KeyError(f"ZERO FALLBACK: {_SOURCE} packed encoding {name!r} states no {key}")
|
|
61
|
+
safetensors = {e["safetensors"]: n for n, e in dense.items() if "safetensors" in e}
|
|
62
|
+
keys = {k: n for n, e in dense.items() for k in (e.get("autotune_keys") or ())}
|
|
63
|
+
return dense, packed, safetensors, keys
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def canonical_name(dtype: Any) -> str:
|
|
67
|
+
"""The table's name for `dtype` (not checked against the table: `itemsize` refuses)."""
|
|
68
|
+
if isinstance(dtype, Enum):
|
|
69
|
+
name = dtype.name
|
|
70
|
+
if name.endswith("_"): # `bool_`: Python's keyword escape, not a dtype name
|
|
71
|
+
name = name[:-1]
|
|
72
|
+
elif isinstance(dtype, str):
|
|
73
|
+
name = dtype
|
|
74
|
+
elif type(dtype).__module__ == "torch": # a torch.dtype: its str() is its torch spelling
|
|
75
|
+
name = str(dtype)
|
|
76
|
+
else:
|
|
77
|
+
raise TypeError(f"ZERO FALLBACK: {dtype!r} ({type(dtype).__name__}) is not a dtype name, "
|
|
78
|
+
f"a torch dtype or a dtype enum member ({_SOURCE})")
|
|
79
|
+
return name[len(_TORCH_PREFIX):] if name.startswith(_TORCH_PREFIX) else name
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _dense_entry(dtype: Any) -> Tuple[str, Dict[str, Any]]:
|
|
83
|
+
dense, packed, _, _ = _tables()
|
|
84
|
+
name = canonical_name(dtype)
|
|
85
|
+
if name in dense:
|
|
86
|
+
return name, dense[name]
|
|
87
|
+
if name in packed:
|
|
88
|
+
raise ValueError(f"ZERO FALLBACK: {name!r} is a packed encoding of {packed[name]['bits']} bits "
|
|
89
|
+
f"per element ({_SOURCE}) and has no whole itemsize — its bytes are "
|
|
90
|
+
f"representation_bytes({name!r}, numel)")
|
|
91
|
+
raise ValueError(f"ZERO FALLBACK: unknown dtype {dtype!r}: {_SOURCE} has no entry {name!r} "
|
|
92
|
+
f"(known: {', '.join(sorted(dense))}). Add it there with its source; no width "
|
|
93
|
+
f"is ever guessed.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def known_dtypes() -> Tuple[str, ...]:
|
|
97
|
+
"""Every dense dtype the table carries, sorted."""
|
|
98
|
+
return tuple(sorted(_tables()[0]))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def itemsize(dtype: Any) -> int:
|
|
102
|
+
"""Bytes per element of `dtype`, refused by name when the table does not carry it."""
|
|
103
|
+
return _dense_entry(dtype)[1]["bits"] // 8
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def kind(dtype: Any) -> str:
|
|
107
|
+
"""The class of `dtype`: float, int, uint, bool or complex (the table's `kind`)."""
|
|
108
|
+
return _dense_entry(dtype)[1]["kind"]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def finite_max(dtype: Any) -> float:
|
|
112
|
+
"""The largest finite value of a floating `dtype` (the table's `max`); any other kind is refused by name."""
|
|
113
|
+
name, entry = _dense_entry(dtype)
|
|
114
|
+
if entry.get("kind") != "float":
|
|
115
|
+
raise ValueError(f"ZERO FALLBACK: {name!r} is not a floating dtype: it has no finite maximum")
|
|
116
|
+
if "max" not in entry:
|
|
117
|
+
raise KeyError(f"ZERO FALLBACK: {_SOURCE} states no max for {name!r}")
|
|
118
|
+
return float(entry["max"])
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def itemsize_table() -> Dict[str, int]:
|
|
122
|
+
"""{name: bytes per element} over every dense dtype (a copy: the caller may not edit the table)."""
|
|
123
|
+
return {name: entry["bits"] // 8 for name, entry in _tables()[0].items()}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def from_safetensors(code: str) -> str:
|
|
127
|
+
"""The dtype name of a safetensors header code ("BF16" -> "bfloat16"), refused by name."""
|
|
128
|
+
names = _tables()[2]
|
|
129
|
+
if code not in names:
|
|
130
|
+
raise ValueError(f"ZERO FALLBACK: safetensors dtype code {code!r} has no entry in {_SOURCE} "
|
|
131
|
+
f"(known: {', '.join(sorted(names))})")
|
|
132
|
+
return names[code]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def from_autotune_key(key: str) -> str:
|
|
136
|
+
"""The dtype name of a launch-key spelling ("fp16" -> "float16", "int1" -> "bool")."""
|
|
137
|
+
names = _tables()[3]
|
|
138
|
+
if key not in names:
|
|
139
|
+
raise ValueError(f"ZERO FALLBACK: launch-key dtype {key!r} has no entry in {_SOURCE} "
|
|
140
|
+
f"(known: {', '.join(sorted(names))})")
|
|
141
|
+
return names[key]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def storage_dtype(dtype: Any, stores_fp64: bool) -> str:
|
|
145
|
+
"""The dtype a tensor of `dtype` is STORED in by a store that does (`stores_fp64`) or does not
|
|
146
|
+
hold 64-bit floats: float64 -> float32 and complex128 -> complex64 without, unchanged with."""
|
|
147
|
+
name, entry = _dense_entry(dtype)
|
|
148
|
+
if stores_fp64:
|
|
149
|
+
return name
|
|
150
|
+
return entry.get("without_fp64", name)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def representation_bytes(dtype: Any, numel: int, split: bool = False) -> int:
|
|
154
|
+
"""Bytes `numel` elements of `dtype` occupy as represented: a dense dtype at its itemsize, a
|
|
155
|
+
hi+lo SPLIT operand as two tensors of `dtype` (its low dtype), a packed encoding as its packed
|
|
156
|
+
values plus one scale and one minimum per group."""
|
|
157
|
+
numel = int(numel)
|
|
158
|
+
packed = _tables()[1]
|
|
159
|
+
name = canonical_name(dtype)
|
|
160
|
+
if name in packed:
|
|
161
|
+
if split:
|
|
162
|
+
raise ValueError(f"ZERO FALLBACK: a packed encoding ({name!r}) is not split hi+lo")
|
|
163
|
+
e = packed[name]
|
|
164
|
+
groups = -(-numel // int(e["group_size"]))
|
|
165
|
+
return -(-numel * int(e["bits"]) // 8) + groups * (itemsize(e["scale_dtype"]) + itemsize(e["min_dtype"]))
|
|
166
|
+
return numel * itemsize(dtype) * (2 if split else 1)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""NBXDtype and its tables: the dtype enum NBXTensor carries (its values frozen: new members are appended, never
|
|
2
|
+
renumbered), and the tables derived from the ONE dtype table (`dtypes.yml`): byte sizes, the spellings it parses (a
|
|
3
|
+
table name, a launch-key spelling, an alias such as torch's `half`; a `torch.` prefix read as text), the numpy names
|
|
4
|
+
and the array-interface type strings, the floating and complex kinds."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from enum import IntEnum
|
|
9
|
+
|
|
10
|
+
# ============================================================================
|
|
11
|
+
# DTYPE SYSTEM
|
|
12
|
+
# ============================================================================
|
|
13
|
+
|
|
14
|
+
class NBXDtype(IntEnum):
|
|
15
|
+
float16 = 0
|
|
16
|
+
bfloat16 = 1
|
|
17
|
+
float32 = 2
|
|
18
|
+
float64 = 3
|
|
19
|
+
int8 = 4
|
|
20
|
+
int16 = 5
|
|
21
|
+
int32 = 6
|
|
22
|
+
int64 = 7
|
|
23
|
+
uint8 = 8
|
|
24
|
+
bool_ = 9
|
|
25
|
+
complex64 = 10
|
|
26
|
+
complex128 = 11
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _table_name(m: "NBXDtype") -> str:
|
|
30
|
+
"""A member's name in the dtype table (`bool_` is Python's keyword escape of `bool`)."""
|
|
31
|
+
return "bool" if m is NBXDtype.bool_ else m.name
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _derive():
|
|
35
|
+
"""Every table of the enum, read from the one dtype table (`dtypes.yml`): widths, the spellings a caller may name
|
|
36
|
+
a dtype by, the numpy names, the array-interface type strings, the floating and complex kinds."""
|
|
37
|
+
from .itemsize import dtype_table
|
|
38
|
+
entries = dtype_table()["dtypes"]
|
|
39
|
+
sizes, names, numpy_names, typestr, floating, complex_ = {}, {}, {}, {}, set(), set()
|
|
40
|
+
for m in NBXDtype:
|
|
41
|
+
e = entries[_table_name(m)]
|
|
42
|
+
sizes[m] = int(e["bits"]) // 8
|
|
43
|
+
for spelling in [_table_name(m), *(e.get("autotune_keys") or ()), *(e.get("aliases") or ())]:
|
|
44
|
+
names[spelling] = m
|
|
45
|
+
if "numpy" in e:
|
|
46
|
+
numpy_names[e["numpy"]] = m
|
|
47
|
+
typestr[m] = e["typestr"]
|
|
48
|
+
if e["kind"] == "float":
|
|
49
|
+
floating.add(m)
|
|
50
|
+
elif e["kind"] == "complex":
|
|
51
|
+
complex_.add(m)
|
|
52
|
+
return sizes, names, numpy_names, typestr, frozenset(floating), frozenset(complex_)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Read once from the table: no width, spelling or type string is written here (R-NBXT 2, one fact once).
|
|
56
|
+
_DTYPE_SIZES, _DTYPE_FROM_STR, _NUMPY_DTYPE_MAP, _DTYPE_TYPESTR, _FLOATING_DTYPES, _COMPLEX_DTYPES = _derive()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def dtype_size(dtype: NBXDtype) -> int:
|
|
60
|
+
return _DTYPE_SIZES[dtype]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_dtype(d) -> NBXDtype:
|
|
64
|
+
"""Parse dtype from string or NBXDtype."""
|
|
65
|
+
if isinstance(d, NBXDtype):
|
|
66
|
+
return d
|
|
67
|
+
if isinstance(d, str):
|
|
68
|
+
s = d.replace('torch.', '')
|
|
69
|
+
return _DTYPE_FROM_STR[s]
|
|
70
|
+
# Unknown type — try string conversion
|
|
71
|
+
s = str(d).replace('torch.', '')
|
|
72
|
+
return _DTYPE_FROM_STR[s]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""The operand decisions of the kernels, as dtype names (DESIGN rule 12: the mechanism in the library): the dtype a
|
|
2
|
+
contraction's partials are summed in, how a matrix unit carries an operand, how an attention's operands are aligned
|
|
3
|
+
when they disagree, the scale of a split representation. Pure functions of dtype names and of a device's matrix unit
|
|
4
|
+
(`ops._configs.matrix_unit`); the application decides which device and which policy, never these rules.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from nbxtensor.dtype import itemsize as _itemsize
|
|
11
|
+
|
|
12
|
+
__all__ = ["contraction_accumulator_dtype", "contraction_accumulator_bytes", "matrix_unit_representation",
|
|
13
|
+
"matrix_unit_flash_representation", "ATTENTION_OPERAND_ALIGNMENTS", "attention_operand_alignment",
|
|
14
|
+
"attention_operand_dtypes", "matrix_unit_split_scale", "constant_load_dtype"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def contraction_accumulator_dtype(dtype: str) -> str:
|
|
18
|
+
"""The dtype (a name) the partial results of a contraction split over the axis it reduces
|
|
19
|
+
are summed in, when a stretch runs in slices of that axis (`core/strategies/chunked_piece`):
|
|
20
|
+
the split contraction is the SAME contraction, so its partials are summed in the accumulator
|
|
21
|
+
its kernel keeps — float32 for a half dtype (every mm/bmm/conv kernel accumulates fp32, and
|
|
22
|
+
`sum` is an AMP_FP32 op), the dtype itself for float32 and float64 — and the sum is stored
|
|
23
|
+
once, in the dtype the contraction's own output was given. A partial summed at the pass
|
|
24
|
+
dtype rounds once per slice where the whole op rounds once. Any other dtype is refused by
|
|
25
|
+
name: a contraction over a token axis yields a floating value.
|
|
26
|
+
|
|
27
|
+
The one definition both engines read."""
|
|
28
|
+
if dtype in ("float16", "bfloat16"):
|
|
29
|
+
return "float32"
|
|
30
|
+
if dtype in ("float32", "float64"):
|
|
31
|
+
return dtype
|
|
32
|
+
raise ValueError(f"contraction_accumulator_dtype: {dtype!r} is not a floating dtype a "
|
|
33
|
+
f"contraction's partials are summed in")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def contraction_accumulator_bytes(store_bytes: int) -> int:
|
|
37
|
+
"""`contraction_accumulator_dtype` read on a WIDTH (bytes per element), for Prism, which prices
|
|
38
|
+
a plan in widths: a 2-byte store (fp16 or bf16 alike) accumulates in 4 bytes; 4 and 8 are their
|
|
39
|
+
own accumulator. The same rule (a unit test holds the two equal over every floating dtype)."""
|
|
40
|
+
if store_bytes == 2:
|
|
41
|
+
return 4
|
|
42
|
+
if store_bytes in (4, 8):
|
|
43
|
+
return store_bytes
|
|
44
|
+
raise ValueError(f"contraction_accumulator_bytes: a {store_bytes}-byte store is no floating "
|
|
45
|
+
f"dtype a contraction's partials are summed in")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def matrix_unit_representation(dtype: str, unit: dict) -> Optional[str]:
|
|
49
|
+
"""How the hardware profile's matrix unit (`matrix_unit`, nbxtensor.ops._configs.matrix_unit) carries one GEMM
|
|
50
|
+
operand whose dtype in memory is `dtype` (a name): "native" when it IS the unit's operand dtype (one MMA term
|
|
51
|
+
per operand), "split" when the profile declares `matrix_unit.fp32_split` with this dtype among its `operands`
|
|
52
|
+
(the value, widened to fp32, is carried as hi + lo of the operand dtype under a power-of-two scale, see
|
|
53
|
+
`matrix_unit_split_scale`; hi*hi + hi*lo + lo*hi with fp32 accumulation outside the unit, the vendor's fp32
|
|
54
|
+
arithmetic kept), else None (the GEMM keeps its tl.dot kernel). The ONE decision: the wrappers launch with it
|
|
55
|
+
and the derived census keys with it (nbxtensor.ops.launch_keys.matrix_unit_operands)."""
|
|
56
|
+
if not unit or not unit.get("mm"):
|
|
57
|
+
return None
|
|
58
|
+
if dtype == unit["operand_dtype"]:
|
|
59
|
+
return "native"
|
|
60
|
+
split = unit.get("fp32_split") or {}
|
|
61
|
+
if split.get("mm") and dtype in (split.get("operands") or ()):
|
|
62
|
+
return "split"
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def matrix_unit_flash_representation(q: str, k: str, v: str, unit: dict) -> Optional[str]:
|
|
67
|
+
"""How the profile's matrix unit carries an attention's three operands (names, already aligned by
|
|
68
|
+
`attention_operand_dtypes`) in its flash kernel (nbxtensor/ops/flash_attention_m8n8k4.py): "native" when all three
|
|
69
|
+
ARE the unit's operand dtype and the profile lists `matrix_unit.flash` rows; "split" when all three are among
|
|
70
|
+
`matrix_unit.fp32_split.operands` and the profile lists `matrix_unit.fp32_split.flash` rows — Q, K, V and the
|
|
71
|
+
softmax's P carried as hi + lo as `matrix_unit_representation` carries a GEMM operand, the vendor's fp32
|
|
72
|
+
arithmetic kept; else None (the attention keeps its tl.dot kernel). The ONE decision the wrapper launches with
|
|
73
|
+
and the derived census routes with (nbxtensor.ops.launch_keys.unit_flash)."""
|
|
74
|
+
if not unit or not (q == k == v):
|
|
75
|
+
return None
|
|
76
|
+
if q == unit["operand_dtype"]:
|
|
77
|
+
return "native" if unit.get("flash") else None
|
|
78
|
+
split = unit.get("fp32_split") or {}
|
|
79
|
+
if split.get("flash") and q in (split.get("operands") or ()):
|
|
80
|
+
return "split"
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
#: The ways a hardware profile may align attention operands that disagree in dtype
|
|
85
|
+
#: (`precision.attention_operands`): every operand takes the narrowest of the three, or the widest.
|
|
86
|
+
ATTENTION_OPERAND_ALIGNMENTS = ("narrowest", "widest")
|
|
87
|
+
#: The operand dtypes the alignment chooses among; their widths are the dtype table's.
|
|
88
|
+
_ATTENTION_OPERAND_DTYPES = ("float16", "bfloat16", "float32", "float64")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def attention_operand_alignment(profile: dict) -> str:
|
|
92
|
+
"""The alignment the hardware profile declares for attention operands that disagree in dtype
|
|
93
|
+
(`precision.attention_operands`, one of ATTENTION_OPERAND_ALIGNMENTS). A profile that declares
|
|
94
|
+
none, or another word, is refused by name: the choice moves a whole attention between the
|
|
95
|
+
matrix unit and the scalar units, so it is never guessed.
|
|
96
|
+
|
|
97
|
+
The one definition both engines read."""
|
|
98
|
+
v = (profile.get("precision") or {}).get("attention_operands")
|
|
99
|
+
if v not in ATTENTION_OPERAND_ALIGNMENTS:
|
|
100
|
+
raise ValueError(f"hardware profile {profile.get('architecture')!r}: "
|
|
101
|
+
f"precision.attention_operands is {v!r}, not one of "
|
|
102
|
+
f"{ATTENTION_OPERAND_ALIGNMENTS}")
|
|
103
|
+
return v
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def attention_operand_dtypes(q: str, k: str, v: str, alignment: str,
|
|
107
|
+
q_round: Optional[str] = None) -> tuple:
|
|
108
|
+
"""The (q, k, v, q_round) dtypes (names) an attention computes with — the ONE decision both
|
|
109
|
+
engines' attention reads (R30): the Triton wrapper (`launch_keys.sdpa_operand_dtypes`), the
|
|
110
|
+
derived census and Prism's width estimate through it, and the compiled engine through its
|
|
111
|
+
twin. The kernels need matching operands: three that already match (Q judged at its KV-cache
|
|
112
|
+
rounding `q_round` when the cache asks for one) are returned unchanged; three that disagree
|
|
113
|
+
all take the narrowest or the widest of them, as the profile's `alignment` declares
|
|
114
|
+
(`attention_operand_alignment`), the first in (q, k, v) order on a tie, and q_round is
|
|
115
|
+
dropped. A non-floating operand is refused by name.
|
|
116
|
+
|
|
117
|
+
Measured on V100 (2026-10-08, nbx/campaigns/2026_10_08_allegro_prof/probe): an fp16 attention
|
|
118
|
+
whose V alone came out of an fp32-stored GEMM ran 22.0 s/op on the scalar flash when aligned
|
|
119
|
+
to the widest, 1.16 s/op on the matrix-unit flash when aligned to the narrowest.
|
|
120
|
+
|
|
121
|
+
The one definition both engines read."""
|
|
122
|
+
qj = q_round if q_round is not None else q
|
|
123
|
+
for d in (qj, k, v):
|
|
124
|
+
if d not in _ATTENTION_OPERAND_DTYPES:
|
|
125
|
+
raise ValueError(f"attention_operand_dtypes: {d!r} is not a floating dtype an "
|
|
126
|
+
f"attention computes in")
|
|
127
|
+
if qj == k == v:
|
|
128
|
+
return q, k, v, q_round
|
|
129
|
+
if alignment not in ATTENTION_OPERAND_ALIGNMENTS:
|
|
130
|
+
raise ValueError(f"attention_operand_dtypes: alignment {alignment!r} is not one of "
|
|
131
|
+
f"{ATTENTION_OPERAND_ALIGNMENTS}")
|
|
132
|
+
pick = min if alignment == "narrowest" else max
|
|
133
|
+
t = pick((qj, k, v), key=_itemsize.itemsize)
|
|
134
|
+
return t, t, t, None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def matrix_unit_split_scale(unit: dict) -> tuple:
|
|
138
|
+
"""(hi_exp, lo_shift) of the split representation, derived from the unit's operand dtype (never written in a
|
|
139
|
+
kernel): each operand tile is scaled by the power of two that brings its largest magnitude into
|
|
140
|
+
[2^hi_exp, 2^(hi_exp+1)) — the top binade below the operand dtype's overflow, so no hi overflows and every
|
|
141
|
+
value down to 2^-(hi_exp+1+|min exponent|) of the tile's largest stays normal — and lo, the residual
|
|
142
|
+
x - hi, is stored times 2^lo_shift (the operand dtype's significand width) so it does not underflow where hi
|
|
143
|
+
does not (Ootomo & Yokota 2022, arXiv 2203.03341, eq. 19-24)."""
|
|
144
|
+
import numpy as np
|
|
145
|
+
fi = np.finfo(np.dtype(unit["operand_dtype"]))
|
|
146
|
+
return int(fi.maxexp) - 2, int(fi.nmant) + 1
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def constant_load_dtype(traced: str, compute: str, stores_fp64: bool) -> str:
|
|
150
|
+
"""The dtype (a name) an embedded graph constant is bound in by the Triton engines
|
|
151
|
+
(`GraphExecutor._load_constant_triton`): a bfloat16 constant is decoded to the compute dtype
|
|
152
|
+
when that is half (fp16 bits from bf16 on hardware computing fp16, the bf16 bits kept under
|
|
153
|
+
bf16), to float32 otherwise; float64 / complex128 are held as the branch holds them
|
|
154
|
+
(`dtype.itemsize.storage_dtype`); every other dtype is kept as traced. The loader and Prism's width
|
|
155
|
+
pass both ask it."""
|
|
156
|
+
if traced == "bfloat16":
|
|
157
|
+
return compute if compute in ("float16", "bfloat16") else "float32"
|
|
158
|
+
return _itemsize.storage_dtype(traced, stores_fp64)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""The output dtype of an op (DESIGN rule 12: the mechanism in the library): read from the op's traced record, or
|
|
2
|
+
decided by the cast-back rule of an op computed in float32 inside a half forward. Pure functions of dtype names and of
|
|
3
|
+
a plain dict; the one definition every engine imports (an engine holding torch dtypes, NBXDtype or strings reads the
|
|
4
|
+
same answer: only the last dotted segment of a dtype's name is read).
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, FrozenSet, Optional
|
|
9
|
+
|
|
10
|
+
__all__ = ["COMPLEX_DTYPE_NAMES", "traced_output_dtype_name", "traced_output_is_complex", "amp_fp32_output_dtype"]
|
|
11
|
+
|
|
12
|
+
COMPLEX_DTYPE_NAMES: FrozenSet[str] = frozenset({"complex32", "complex64", "complex128", "chalf", "cfloat", "cdouble"})
|
|
13
|
+
_GRAPH_FLOAT_NAMES: FrozenSet[str] = frozenset({"float16", "bfloat16", "float32", "float64"})
|
|
14
|
+
_HALF_GRAPH_NAMES: FrozenSet[str] = frozenset({"float16", "bfloat16"})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _name(dtype) -> str:
|
|
18
|
+
return str(dtype).rsplit(".", 1)[-1]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def traced_output_dtype_name(record: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
22
|
+
"""The plain name of the op's first traced output dtype (`output_dtypes[0]` at the record's TOP level), or None
|
|
23
|
+
when the record states none."""
|
|
24
|
+
if not isinstance(record, dict):
|
|
25
|
+
return None
|
|
26
|
+
dts = record.get("output_dtypes") or ()
|
|
27
|
+
return _name(dts[0]) if dts else None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def traced_output_is_complex(record: Optional[Dict[str, Any]]) -> bool:
|
|
31
|
+
"""True when the op's traced record types an output complex.
|
|
32
|
+
|
|
33
|
+
The traced OUTPUT dtype is the only signal: an op's name cannot answer (a plain `mul` by the scalar `1j` produces
|
|
34
|
+
complex), nor can its inputs (that scalar is no input tensor). It matters because a half input at a
|
|
35
|
+
complex-producing op yields complex32, which `exp`, `angle` and the inverse real FFT have no kernel for. Only
|
|
36
|
+
`output_dtypes` at the record's TOP level is read: a nested `attributes` dict reads as "not complex"."""
|
|
37
|
+
if not isinstance(record, dict):
|
|
38
|
+
return False
|
|
39
|
+
return any(_name(d) in COMPLEX_DTYPE_NAMES for d in (record.get("output_dtypes") or ()))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def amp_fp32_output_dtype(compute_dtype: str, graph_dtype: Optional[str], safe: bool,
|
|
43
|
+
narrowed: bool, traced: Optional[str] = None) -> str:
|
|
44
|
+
"""The OUTPUT dtype (a name) of an op computed in float32 inside under the half compute dtype `compute_dtype`.
|
|
45
|
+
|
|
46
|
+
* bfloat16 on a bfloat16 graph (`graph_dtype`, the dtype the model was traced in): bfloat16 — a float32 result
|
|
47
|
+
always fits bfloat16's range, and the model's own forward returns its input's dtype there.
|
|
48
|
+
* bfloat16 on a graph of another floating dtype (a float32 model computed in bfloat16): float32, the dtype the
|
|
49
|
+
model's forward returned. A graph dtype that is no floating dtype is refused by name.
|
|
50
|
+
* float16: float16 only under the precision contract — `safe` (narrows every such op) or `narrowed` (this op is
|
|
51
|
+
in the narrowed set); float32 otherwise. An op `traced` in float32 inside a half graph is the model's own
|
|
52
|
+
float32 island: only `narrowed` brings it to float16, never `safe`."""
|
|
53
|
+
if compute_dtype == "bfloat16":
|
|
54
|
+
if graph_dtype not in _GRAPH_FLOAT_NAMES:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"amp_fp32_output_dtype: under bfloat16 compute the output follows the graph dtype, and "
|
|
57
|
+
f"{graph_dtype!r} is none of {sorted(_GRAPH_FLOAT_NAMES)} — the traced graph must state it")
|
|
58
|
+
return "bfloat16" if graph_dtype == "bfloat16" else "float32"
|
|
59
|
+
if compute_dtype == "float16":
|
|
60
|
+
if traced == "float32" and graph_dtype in _HALF_GRAPH_NAMES:
|
|
61
|
+
return "float16" if narrowed else "float32"
|
|
62
|
+
return "float16" if (safe or narrowed) else "float32"
|
|
63
|
+
raise ValueError(f"amp_fp32_output_dtype: compute dtype {compute_dtype!r} is not a half dtype — the cast-back "
|
|
64
|
+
f"rule exists for bfloat16 and float16 only")
|