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,204 @@
|
|
|
1
|
+
"""The CPU uses every core it is given (host work left on one core idled the rest of the machine): one thread pool, and a loop that cuts a large array
|
|
2
|
+
into contiguous pieces run in parallel, on the model of ggml.
|
|
3
|
+
|
|
4
|
+
numpy's element-wise ops, reductions and conversions run on one core; their inner loops release the GIL. So an op over
|
|
5
|
+
a large array is cut into contiguous pieces, each piece run by a pool thread with the same numpy call on its slice:
|
|
6
|
+
the pieces run on as many cores as the pool holds. Under a size threshold the threads cost more than they save, and
|
|
7
|
+
the op runs inline; the threshold is MEASURED (inline against split, on this machine, per kind of work), never written
|
|
8
|
+
by hand.
|
|
9
|
+
|
|
10
|
+
- `threads()`: the pool's size. Default: what this library measures of the machine — on Apple silicon its performance
|
|
11
|
+
cores (`hw.perflevel0.physicalcpu`: an efficiency core in a parallel region holds every other thread at its barrier),
|
|
12
|
+
else the physical cores the process may run on (Linux: its CPU affinity and `/sys` topology; macOS:
|
|
13
|
+
`hw.physicalcpu`). An application sets its own (`set_threads`; NeuroBrix: its hardware profile's compute threads).
|
|
14
|
+
- `parallel_for(n, run, kind)`: `run(start, stop)` over contiguous pieces of `[0, n)`, inline under the measured
|
|
15
|
+
threshold of `kind`; returns the pieces' results in order.
|
|
16
|
+
- the BLAS numpy calls: as many threads as the pool outside it, one per piece inside `parallel_for` — no
|
|
17
|
+
oversubscription (`backend.cpu.blas`).
|
|
18
|
+
- `numa_nodes()`: the NUMA nodes and their CPUs (Linux `/sys/devices/system/node`), `{}` where the machine has none to
|
|
19
|
+
report.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import concurrent.futures
|
|
24
|
+
import os
|
|
25
|
+
import platform
|
|
26
|
+
import subprocess
|
|
27
|
+
import threading
|
|
28
|
+
import time
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Callable, Dict, List, Optional, Sequence, Tuple
|
|
31
|
+
|
|
32
|
+
__all__ = ["threads", "set_threads", "measured_threads", "numa_nodes", "pieces", "parallel_for",
|
|
33
|
+
"split_threshold", "set_split_threshold"]
|
|
34
|
+
|
|
35
|
+
_LOCK = threading.Lock()
|
|
36
|
+
_STATE: Dict[str, object] = {} # "threads": int set by an application; "pool": the executor; "pool_size"
|
|
37
|
+
_THRESHOLDS: Dict[str, int] = {} # kind -> elements under which the work runs inline
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _sysctl_int(name: str) -> Optional[int]:
|
|
41
|
+
r = subprocess.run(["sysctl", "-n", name], capture_output=True, text=True)
|
|
42
|
+
out = r.stdout.strip()
|
|
43
|
+
return int(out) if r.returncode == 0 and out.isdigit() else None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _linux_physical_cores() -> Optional[int]:
|
|
47
|
+
allowed = os.sched_getaffinity(0)
|
|
48
|
+
cores = set()
|
|
49
|
+
for cpu in allowed:
|
|
50
|
+
topo = Path(f"/sys/devices/system/cpu/cpu{cpu}/topology")
|
|
51
|
+
try:
|
|
52
|
+
cores.add((int((topo / "physical_package_id").read_text()), int((topo / "core_id").read_text())))
|
|
53
|
+
except (OSError, ValueError):
|
|
54
|
+
return None
|
|
55
|
+
return len(cores) or None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def measured_threads() -> int:
|
|
59
|
+
"""The compute cores this machine gives this process, as the library reads them (see the module's docstring).
|
|
60
|
+
Refused by name where no reading exists: the count is never guessed."""
|
|
61
|
+
system = platform.system()
|
|
62
|
+
if system == "Darwin":
|
|
63
|
+
n = _sysctl_int("hw.perflevel0.physicalcpu") or _sysctl_int("hw.physicalcpu")
|
|
64
|
+
elif system == "Linux":
|
|
65
|
+
n = _linux_physical_cores()
|
|
66
|
+
else:
|
|
67
|
+
n = None
|
|
68
|
+
if not n:
|
|
69
|
+
raise RuntimeError(f"ZERO FALLBACK: nbxtensor reads no physical core count on {system!r}; set the pool's size "
|
|
70
|
+
f"(nbxtensor.backend.cpu.pool.set_threads)")
|
|
71
|
+
return int(n)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def numa_nodes() -> Dict[int, Tuple[int, ...]]:
|
|
75
|
+
"""{node: CPUs} from Linux's `/sys/devices/system/node`; `{}` where the machine reports none."""
|
|
76
|
+
root = Path("/sys/devices/system/node")
|
|
77
|
+
nodes: Dict[int, Tuple[int, ...]] = {}
|
|
78
|
+
for d in sorted(root.glob("node[0-9]*")) if root.is_dir() else ():
|
|
79
|
+
cpus: List[int] = []
|
|
80
|
+
for part in (d / "cpulist").read_text().strip().split(","):
|
|
81
|
+
if part:
|
|
82
|
+
lo, _, hi = part.partition("-")
|
|
83
|
+
cpus.extend(range(int(lo), int(hi or lo) + 1))
|
|
84
|
+
nodes[int(d.name[4:])] = tuple(cpus)
|
|
85
|
+
return nodes
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def threads() -> int:
|
|
89
|
+
"""The pool's size: the application's (`set_threads`), else the library's measurement of the machine."""
|
|
90
|
+
n = _STATE.get("threads")
|
|
91
|
+
return int(n) if n is not None else measured_threads()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def set_threads(n: Optional[int]) -> None:
|
|
95
|
+
"""Set the pool's size (an int >= 1); `None` restores the library's measurement. The running pool is replaced."""
|
|
96
|
+
if n is not None and (not isinstance(n, int) or n < 1):
|
|
97
|
+
raise ValueError(f"set_threads: {n!r} — a pool holds one thread or more")
|
|
98
|
+
with _LOCK:
|
|
99
|
+
if n is None:
|
|
100
|
+
_STATE.pop("threads", None)
|
|
101
|
+
else:
|
|
102
|
+
_STATE["threads"] = n
|
|
103
|
+
pool = _STATE.pop("pool", None)
|
|
104
|
+
_STATE.pop("pool_size", None)
|
|
105
|
+
if pool is not None:
|
|
106
|
+
pool.shutdown(wait=True)
|
|
107
|
+
from nbxtensor.backend.cpu.blas import blas, set_blas_threads
|
|
108
|
+
if blas()[0] == "openblas": # the process's BLAS count follows the pool's size
|
|
109
|
+
set_blas_threads(threads())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _pool(size: int) -> concurrent.futures.ThreadPoolExecutor:
|
|
113
|
+
with _LOCK:
|
|
114
|
+
if _STATE.get("pool_size") != size:
|
|
115
|
+
old = _STATE.pop("pool", None)
|
|
116
|
+
if old is not None:
|
|
117
|
+
old.shutdown(wait=False)
|
|
118
|
+
_STATE["pool"] = concurrent.futures.ThreadPoolExecutor(max_workers=size, thread_name_prefix="nbx-cpu",
|
|
119
|
+
initializer=_one_blas_thread_here)
|
|
120
|
+
_STATE["pool_size"] = size
|
|
121
|
+
return _STATE["pool"]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _one_blas_thread_here() -> None:
|
|
125
|
+
"""A pool thread runs its BLAS single-threaded (Accelerate's setting is per thread; OpenBLAS's count is the
|
|
126
|
+
process's and is lowered by `parallel_for` itself)."""
|
|
127
|
+
from nbxtensor.backend.cpu.blas import blas, set_blas_threads
|
|
128
|
+
if blas()[0] == "accelerate":
|
|
129
|
+
set_blas_threads(1)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def pieces(n: int, parts: int) -> List[Tuple[int, int]]:
|
|
133
|
+
"""`[0, n)` cut into at most `parts` contiguous, non-empty pieces of sizes differing by one at most, in order."""
|
|
134
|
+
if n < 0 or parts < 1:
|
|
135
|
+
raise ValueError(f"pieces: n={n}, parts={parts}")
|
|
136
|
+
parts = min(parts, n) or 1
|
|
137
|
+
base, extra = divmod(n, parts)
|
|
138
|
+
out, start = [], 0
|
|
139
|
+
for i in range(parts):
|
|
140
|
+
stop = start + base + (1 if i < extra else 0)
|
|
141
|
+
if stop > start:
|
|
142
|
+
out.append((start, stop))
|
|
143
|
+
start = stop
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def split_threshold(kind: str, probe: Optional[Callable[[int], Callable[[int, int], object]]] = None) -> int:
|
|
148
|
+
"""Elements of work of `kind` under which a split costs more than it saves — set (`set_split_threshold`) or
|
|
149
|
+
MEASURED once per process with `probe(n)` (which builds the work on `n` elements: a callable of (start, stop)),
|
|
150
|
+
by timing inline against split at growing sizes. A kind never set nor probed is refused by name."""
|
|
151
|
+
if kind in _THRESHOLDS:
|
|
152
|
+
return _THRESHOLDS[kind]
|
|
153
|
+
if probe is None:
|
|
154
|
+
raise KeyError(f"split_threshold: kind {kind!r} has no threshold set and no probe to measure it")
|
|
155
|
+
size = threads()
|
|
156
|
+
n = 1 << 12
|
|
157
|
+
while n <= 1 << 26:
|
|
158
|
+
run = probe(n)
|
|
159
|
+
inline = _best_of(lambda: run(0, n))
|
|
160
|
+
split = _best_of(lambda: [f.result() for f in [_pool(size).submit(run, a, b) for a, b in pieces(n, size)]])
|
|
161
|
+
if split < inline:
|
|
162
|
+
_THRESHOLDS[kind] = n
|
|
163
|
+
return n
|
|
164
|
+
n <<= 1
|
|
165
|
+
_THRESHOLDS[kind] = n # no size measured faster split: work this large or smaller runs inline
|
|
166
|
+
return n
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _best_of(fn, repeat: int = 5) -> float:
|
|
170
|
+
best = float("inf")
|
|
171
|
+
for _ in range(repeat):
|
|
172
|
+
t = time.perf_counter()
|
|
173
|
+
fn()
|
|
174
|
+
best = min(best, time.perf_counter() - t)
|
|
175
|
+
return best
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def set_split_threshold(kind: str, n: Optional[int]) -> None:
|
|
179
|
+
"""Set (or with `None` forget, to measure again) the threshold of `kind`."""
|
|
180
|
+
if n is None:
|
|
181
|
+
_THRESHOLDS.pop(kind, None)
|
|
182
|
+
elif not isinstance(n, int) or n < 1:
|
|
183
|
+
raise ValueError(f"set_split_threshold: {n!r}")
|
|
184
|
+
else:
|
|
185
|
+
_THRESHOLDS[kind] = n
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parallel_for(n: int, run: Callable[[int, int], object], kind: str,
|
|
189
|
+
probe: Optional[Callable[[int], Callable[[int, int], object]]] = None) -> Sequence[object]:
|
|
190
|
+
"""`run(start, stop)` over contiguous pieces of `[0, n)`: inline (one piece) under `kind`'s threshold or on a pool
|
|
191
|
+
of one thread, else one piece per pool thread. The pieces' results, in order."""
|
|
192
|
+
size = threads()
|
|
193
|
+
if size == 1 or n < split_threshold(kind, probe):
|
|
194
|
+
return [run(0, n)]
|
|
195
|
+
from nbxtensor.backend.cpu.blas import blas, set_blas_threads
|
|
196
|
+
process_wide = blas()[0] == "openblas" # a count for the whole process: 1 while the pieces run
|
|
197
|
+
if process_wide:
|
|
198
|
+
set_blas_threads(1)
|
|
199
|
+
try:
|
|
200
|
+
futures = [_pool(size).submit(run, a, b) for a, b in pieces(n, size)]
|
|
201
|
+
return [f.result() for f in futures]
|
|
202
|
+
finally:
|
|
203
|
+
if process_wide:
|
|
204
|
+
set_blas_threads(size)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Triton-CPU backend detection and gate.
|
|
3
|
+
|
|
4
|
+
NeuroBrix triton modes (`--triton`, `--triton-sequential`) on a CPU-only
|
|
5
|
+
profile (no visible GPU) require the upstream `triton-cpu` package from
|
|
6
|
+
``triton-lang/triton-cpu``. We do NOT bundle, auto-fetch, or vendor it.
|
|
7
|
+
When it is missing we raise a clean error naming the install command and
|
|
8
|
+
a viable alternative (`--compiled` runs on CPU without any extra
|
|
9
|
+
install).
|
|
10
|
+
|
|
11
|
+
Rationale (per project doctrine, P-PRISM-NEVER-REFUSE v2 / S3
|
|
12
|
+
2026-05-12):
|
|
13
|
+
- Security: auto-fetching a wheel at runtime is a supply-chain surface
|
|
14
|
+
the user did not consent to.
|
|
15
|
+
- Trust: the user controls their environment; surprising side-effects
|
|
16
|
+
on `neurobrix run` damage that trust.
|
|
17
|
+
- Air-gapped deployments: enterprise ML datacenters run on isolated
|
|
18
|
+
networks where auto-fetch fails with a confusing network error.
|
|
19
|
+
- Alignment: llama.cpp, vLLM, transformers, torch itself do not
|
|
20
|
+
auto-install optional backends.
|
|
21
|
+
|
|
22
|
+
Numerical coverage gaps that remain blocked by upstream issues are
|
|
23
|
+
tracked in `triton/triton_cpu_coverage_gaps.md`. The
|
|
24
|
+
``TRITON_CPU_FP16_UPSTREAM_BLOCKED`` marker below is read by the
|
|
25
|
+
dispatcher to decide whether to refuse a fp16 launch with a clear
|
|
26
|
+
message or fall through to a documented escalation path.
|
|
27
|
+
|
|
28
|
+
R34 (model-agnostic) preserved — nothing in this module is keyed on
|
|
29
|
+
model name. R33 (zero-torch in triton) preserved — this module does
|
|
30
|
+
not import torch, even at boundary.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Marker constants — read by the triton-CPU dispatcher path to gate
|
|
39
|
+
# launches that are known to fail or drift on the current upstream.
|
|
40
|
+
# These exist so a future P-TRITON-CPU-FP16-UPSTREAM-FOLLOWUP can flip
|
|
41
|
+
# them in one place when the upstream issues close.
|
|
42
|
+
TRITON_CPU_FP16_UPSTREAM_BLOCKED: bool = True # triton-cpu #147 (Dot3D), #144
|
|
43
|
+
TRITON_CPU_MASKED_BLOCKPTR_GEMM_BLOCKED: bool = True # triton-cpu #222
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TritonCPUNotInstalledError(ImportError):
|
|
47
|
+
"""Raised when `--triton` is invoked on a CPU-only profile and the
|
|
48
|
+
upstream `triton-cpu` package is missing.
|
|
49
|
+
|
|
50
|
+
The message is intentionally actionable: install command, docs
|
|
51
|
+
pointer, and a viable already-working alternative.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_cpu_only_profile() -> bool:
|
|
56
|
+
"""True when the ENGINE resolves no GPU backend on this run.
|
|
57
|
+
|
|
58
|
+
`CUDA_VISIBLE_DEVICES=""` still forces CPU: it is the standard way to
|
|
59
|
+
hide GPUs and it is a deliberate instruction, not a probe.
|
|
60
|
+
|
|
61
|
+
Everything else is asked of the engine's own detection, which knows
|
|
62
|
+
CUDA, ROCm and Metal, rather than of `libcuda.so.1`. Loading that
|
|
63
|
+
library was the whole test until 2026-09-06, and on an Apple machine it
|
|
64
|
+
fails for the ordinary reason that there is no NVIDIA driver — so
|
|
65
|
+
`--triton` refused a real GPU as "CPU-only" and told the user to build
|
|
66
|
+
triton-cpu. The question this function asks is "has this run a GPU",
|
|
67
|
+
and one vendor's driver is not that question.
|
|
68
|
+
"""
|
|
69
|
+
visible = os.environ.get("CUDA_VISIBLE_DEVICES")
|
|
70
|
+
if visible == "":
|
|
71
|
+
return True
|
|
72
|
+
try:
|
|
73
|
+
from nbxtensor.backend.runtime import gpu_backend
|
|
74
|
+
gpu_backend()
|
|
75
|
+
return False
|
|
76
|
+
except Exception:
|
|
77
|
+
# No backend resolved at all — genuinely CPU-only. The detection
|
|
78
|
+
# raises rather than returning a sentinel, which is the answer.
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _triton_cpu_available() -> bool:
|
|
83
|
+
"""Probe the upstream `triton-cpu` install without importing it."""
|
|
84
|
+
try:
|
|
85
|
+
# Upstream exposes either `triton_cpu` (older releases) or
|
|
86
|
+
# registers itself under `triton.backends.cpu` (newer). We
|
|
87
|
+
# accept either; both indicate the user installed the package.
|
|
88
|
+
import importlib.util
|
|
89
|
+
if importlib.util.find_spec("triton_cpu") is not None:
|
|
90
|
+
return True
|
|
91
|
+
# Newer integration path: triton with CPU backend registered.
|
|
92
|
+
spec = importlib.util.find_spec("triton.backends.cpu")
|
|
93
|
+
if spec is not None:
|
|
94
|
+
return True
|
|
95
|
+
except Exception:
|
|
96
|
+
return False
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def ensure_triton_cpu_or_raise() -> None:
|
|
101
|
+
"""Verify `triton-cpu` is installed when we're on a CPU-only profile.
|
|
102
|
+
|
|
103
|
+
Call sites: the entry points of `--triton` / `--triton-sequential`
|
|
104
|
+
on `RuntimeExecutor` and the CLI before any triton import that would
|
|
105
|
+
otherwise produce a cryptic `triton.runtime.driver.active` error.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
TritonCPUNotInstalledError: with an actionable message naming
|
|
109
|
+
the install command, docs path, and the `--compiled` fallback
|
|
110
|
+
that already works on CPU without any extra install.
|
|
111
|
+
"""
|
|
112
|
+
if not _is_cpu_only_profile():
|
|
113
|
+
return # GPU visible — standard triton path.
|
|
114
|
+
if _triton_cpu_available():
|
|
115
|
+
# Pre-set the env var so the triton runtime picks the CPU
|
|
116
|
+
# backend at first driver query. Must happen BEFORE any
|
|
117
|
+
# `import triton.runtime.driver` in this process.
|
|
118
|
+
os.environ.setdefault("TRITON_CPU_BACKEND", "1")
|
|
119
|
+
return
|
|
120
|
+
raise TritonCPUNotInstalledError(
|
|
121
|
+
"neurobrix run --triton (or --triton-sequential) on a CPU-only "
|
|
122
|
+
"device requires the upstream triton-cpu package, which is NOT "
|
|
123
|
+
"installed in this environment.\n"
|
|
124
|
+
"\n"
|
|
125
|
+
"Upstream does NOT publish a PyPI wheel today — install is "
|
|
126
|
+
"build-from-source:\n"
|
|
127
|
+
" git clone https://github.com/triton-lang/triton-cpu.git\n"
|
|
128
|
+
" cd triton-cpu\n"
|
|
129
|
+
" pip install -r python/requirements.txt\n"
|
|
130
|
+
" pip install -e .\n"
|
|
131
|
+
"(prerequisites: LLVM, CMake, Ninja, C++17 compiler. "
|
|
132
|
+
"Expect ~30 min build + ~15 GB peak disk.)\n"
|
|
133
|
+
"\n"
|
|
134
|
+
"See docs/triton_cpu_install.md for full instructions and "
|
|
135
|
+
"verification command.\n"
|
|
136
|
+
"\n"
|
|
137
|
+
"Alternative (no extra install required): run with --compiled. "
|
|
138
|
+
"The PyTorch CPU backend is mature and is the recommended "
|
|
139
|
+
"production path for CPU-only inference today. Numerical "
|
|
140
|
+
"fp16 paths on triton-cpu have known upstream gaps "
|
|
141
|
+
"(triton-cpu issues #147, #222); --compiled has no such gaps."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def is_cpu_triton_active() -> bool:
|
|
146
|
+
"""True iff triton-cpu has been selected for this process.
|
|
147
|
+
|
|
148
|
+
Set after `ensure_triton_cpu_or_raise()` succeeds on a CPU-only
|
|
149
|
+
profile. Downstream code (DeviceAllocator, wrappers, kv_cache)
|
|
150
|
+
uses this to branch between CUDA pointers and host pointers.
|
|
151
|
+
"""
|
|
152
|
+
return os.environ.get("TRITON_CPU_BACKEND") == "1"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_install_instructions() -> str:
|
|
156
|
+
"""Return the install instructions string. Useful for CLI `--help`
|
|
157
|
+
text and error consolidation. Single source of truth for the
|
|
158
|
+
message so it cannot drift between sites."""
|
|
159
|
+
return (
|
|
160
|
+
"pip install triton-cpu "
|
|
161
|
+
"(see docs/triton_cpu_install.md for build-from-source)"
|
|
162
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Device facts the library reads itself, so it runs with no application around it (API.md section 11).
|
|
2
|
+
|
|
3
|
+
Each answers from the device or from the backend's own documented capability — never from an application's profile.
|
|
4
|
+
An application may still provide its own answer through `nbxtensor.host` (NeuroBrix provides the one its hardware
|
|
5
|
+
profile declares); with none provided, the library's reading here is the answer.
|
|
6
|
+
|
|
7
|
+
- `host_shares_memory_with_device()`: Metal asks the device (`MTLDevice.hasUnifiedMemory`); CUDA asks the runtime
|
|
8
|
+
(`cudaDeviceGetAttribute(cudaDevAttrIntegrated)`, attribute 18 of the CUDA runtime API's `cudaDeviceAttr`). A
|
|
9
|
+
backend whose answer is not read here is refused by name.
|
|
10
|
+
- `kernels_carry_fp64()`: whether the backend's kernels compute float64 — Metal: no (the Metal Shading Language has
|
|
11
|
+
no double type); CUDA and HIP: yes (every NVIDIA and AMD GPU computes it, at whatever rate).
|
|
12
|
+
- `memory_gb(memory_mb)` / `device_memory_gb(device_idx)`: a card's memory rounded to GB, the size a certified kernel
|
|
13
|
+
setting is proven for (an entry serves only cards of the size it was proven on: a 16 GB and a 32 GB V100 share an
|
|
14
|
+
architecture and not their best tiles under memory pressure). The device's memory is the one an application declared
|
|
15
|
+
(`declare_device_memory`: a census shadow sees no card; a plan made for a profile) or, with none declared, the
|
|
16
|
+
driver's total, read once per device.
|
|
17
|
+
- `certified_dir()`: the default store of certified kernel settings: one directory per user in the platform's cache
|
|
18
|
+
(`$XDG_CACHE_HOME`, else `~/.cache`), `nbxtensor/certified/` — empty until the autotune fills it.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import ctypes
|
|
23
|
+
import os
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
__all__ = ["host_shares_memory_with_device", "kernels_carry_fp64", "certified_dir", "memory_gb", "declare_device_memory",
|
|
27
|
+
"device_memory_gb"]
|
|
28
|
+
|
|
29
|
+
_CUDA_DEV_ATTR_INTEGRATED = 18 # cudaDevAttrIntegrated (CUDA Runtime API, enum cudaDeviceAttr)
|
|
30
|
+
_FP64 = {"cuda": True, "hip": True, "metal": False}
|
|
31
|
+
_DECLARED_MEMORY: dict = {} # device index -> bytes an application declared
|
|
32
|
+
_MEASURED_MEMORY: dict = {} # device index -> bytes the driver reported, read once
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def memory_gb(memory_mb) -> "int | None":
|
|
36
|
+
"""Memory in MB, rounded to GB (`round(mb / 1024)`); None when unknown or not positive."""
|
|
37
|
+
try:
|
|
38
|
+
mb = float(memory_mb)
|
|
39
|
+
except (TypeError, ValueError):
|
|
40
|
+
return None
|
|
41
|
+
return int(round(mb / 1024)) if mb > 0 else None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def declare_device_memory(device_idx: int, nbytes) -> None:
|
|
45
|
+
"""An application states device `device_idx`'s memory in bytes (None forgets it): a census shadow, which sees no
|
|
46
|
+
card, or a plan made for a profile whose card is not the one in this process."""
|
|
47
|
+
if nbytes is None:
|
|
48
|
+
_DECLARED_MEMORY.pop(int(device_idx), None)
|
|
49
|
+
else:
|
|
50
|
+
_DECLARED_MEMORY[int(device_idx)] = int(nbytes)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def declared_device_memory() -> dict:
|
|
54
|
+
"""The declared devices' memory, by index (a copy)."""
|
|
55
|
+
return dict(_DECLARED_MEMORY)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def device_memory_gb(device_idx: int) -> "int | None":
|
|
59
|
+
"""Device `device_idx`'s memory in GB: the declared one, else the driver's total (read once); None when neither
|
|
60
|
+
answers (no card visible and none declared)."""
|
|
61
|
+
idx = int(device_idx)
|
|
62
|
+
nbytes = _DECLARED_MEMORY.get(idx)
|
|
63
|
+
if nbytes is None:
|
|
64
|
+
if idx not in _MEASURED_MEMORY:
|
|
65
|
+
from nbxtensor.memory.allocator import DeviceAllocator
|
|
66
|
+
_MEASURED_MEMORY[idx] = int(DeviceAllocator.device_total_bytes(idx))
|
|
67
|
+
nbytes = _MEASURED_MEMORY[idx]
|
|
68
|
+
return memory_gb(nbytes / (1 << 20)) if nbytes and nbytes > 0 else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def host_shares_memory_with_device() -> bool:
|
|
72
|
+
"""Does the current device draw from the host's memory pool?"""
|
|
73
|
+
from . import runtime as _rt
|
|
74
|
+
name = _rt.gpu_backend()
|
|
75
|
+
if name == "metal":
|
|
76
|
+
return bool(_rt.gpu_runtime().has_unified_memory)
|
|
77
|
+
if name == "cuda":
|
|
78
|
+
rt = _rt.gpu_runtime()
|
|
79
|
+
rc, dev = _rt.device_memory().get_device()
|
|
80
|
+
value = ctypes.c_int()
|
|
81
|
+
if rc == 0:
|
|
82
|
+
rc = rt.cudaDeviceGetAttribute(ctypes.byref(value), ctypes.c_int(_CUDA_DEV_ATTR_INTEGRATED),
|
|
83
|
+
ctypes.c_int(dev))
|
|
84
|
+
if rc != 0:
|
|
85
|
+
raise RuntimeError(f"ZERO FALLBACK: cudaDeviceGetAttribute(cudaDevAttrIntegrated) failed (error {rc}): "
|
|
86
|
+
f"whether this device shares the host's memory is not known")
|
|
87
|
+
return bool(value.value)
|
|
88
|
+
raise RuntimeError(f"ZERO FALLBACK: whether a {name!r} device shares the host's memory is not read by nbxtensor; "
|
|
89
|
+
f"provide it (nbxtensor.host.provide('host_shares_memory_with_device', ...))")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def kernels_carry_fp64(profile=None) -> bool:
|
|
93
|
+
"""Do the backend's kernels compute float64? (`profile`: an application's hardware profile, which an application's
|
|
94
|
+
own answer reads; the library's answer is the backend's.)"""
|
|
95
|
+
from . import runtime as _rt
|
|
96
|
+
name = _rt.gpu_backend()
|
|
97
|
+
if name not in _FP64:
|
|
98
|
+
raise RuntimeError(f"ZERO FALLBACK: nbxtensor does not know whether {name!r} kernels compute float64")
|
|
99
|
+
return _FP64[name]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def certified_dir() -> Path:
|
|
103
|
+
"""The library's own store of certified kernel settings, in the user's cache."""
|
|
104
|
+
base = os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache")
|
|
105
|
+
return Path(base) / "nbxtensor" / "certified"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The library's device files: the kernel-relevant description of each device, one file per device
|
|
2
|
+
(`devices/<vendor>/<name>.yml`, beside this module) — API.md section 11.
|
|
3
|
+
|
|
4
|
+
What a kernel reads of a device lives here: its identification (`compute_capability`, `compute_capability_matches`,
|
|
5
|
+
`architecture`), the tiles, the shared memory and registers, the warp size, the matrix unit, the dispatch limits, the
|
|
6
|
+
autotune's screen tolerances and buckets. The list is the device-key census (`docs/nbxtensor/device_keys.txt`).
|
|
7
|
+
|
|
8
|
+
An application may keep its own facts about the same devices in its own files (NeuroBrix: Prism's budgets and measured
|
|
9
|
+
rates, in `config/vendors/<vendor>/<name>.yml`) and give the library their directory (`nbxtensor.host`,
|
|
10
|
+
`vendors_dir`, optional). The two halves of one device are MERGED, and a key in both is refused by name: a fact is
|
|
11
|
+
written once. With no application, the library's half is the whole description.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import copy
|
|
16
|
+
import functools
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, Iterator, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
DEVICES = Path(__file__).resolve().parent / "devices"
|
|
21
|
+
|
|
22
|
+
__all__ = ["DEVICES", "names", "read", "merge", "merged", "describe", "param", "in_force", "for_arch", "forget"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@functools.lru_cache(maxsize=None)
|
|
26
|
+
def _parsed(path: str) -> Dict[str, Any]:
|
|
27
|
+
import yaml
|
|
28
|
+
with open(path) as f:
|
|
29
|
+
return yaml.safe_load(f) or {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read(vendor: str, name: str) -> Dict[str, Any]:
|
|
33
|
+
"""The library's half of device `vendor/name` (a copy), `{}` when the library describes none."""
|
|
34
|
+
path = DEVICES / vendor / f"{name}.yml"
|
|
35
|
+
return copy.deepcopy(_parsed(str(path))) if path.is_file() else {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def merge(library: Dict[str, Any], application: Dict[str, Any], where: str = "") -> Dict[str, Any]:
|
|
39
|
+
"""The two halves of one device as one description; a key written in both is refused by name."""
|
|
40
|
+
out = copy.deepcopy(library)
|
|
41
|
+
for key, value in application.items():
|
|
42
|
+
if key not in out:
|
|
43
|
+
out[key] = copy.deepcopy(value)
|
|
44
|
+
elif isinstance(out[key], dict) and isinstance(value, dict):
|
|
45
|
+
out[key] = merge(out[key], value, f"{where}{key}.")
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(f"device file {where}{key}: written in both the library's device file and the "
|
|
48
|
+
f"application's — a fact is written once (nbxtensor/backend/devices)")
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _application_dir() -> Optional[Path]:
|
|
53
|
+
from nbxtensor import host
|
|
54
|
+
if "vendors_dir" not in host.provided():
|
|
55
|
+
return None
|
|
56
|
+
return Path(host.need("vendors_dir")())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def names() -> Iterator[Tuple[str, str, Path]]:
|
|
60
|
+
"""(vendor, name, path) of every device described by the library or by the application, once each."""
|
|
61
|
+
seen = {}
|
|
62
|
+
roots = [DEVICES] + ([d] if (d := _application_dir()) is not None else [])
|
|
63
|
+
for root in roots:
|
|
64
|
+
for path in sorted(root.glob("*/*.yml")):
|
|
65
|
+
seen.setdefault((path.parent.name, path.stem), path)
|
|
66
|
+
for (vendor, name), path in sorted(seen.items()):
|
|
67
|
+
yield vendor, name, path
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def merged(vendor: str, name: str) -> Dict[str, Any]:
|
|
71
|
+
"""Device `vendor/name` whole: the library's half merged with the application's, when it gave its directory."""
|
|
72
|
+
app = _application_dir()
|
|
73
|
+
app_half = {}
|
|
74
|
+
if app is not None and (app / vendor / f"{name}.yml").is_file():
|
|
75
|
+
app_half = copy.deepcopy(_parsed(str(app / vendor / f"{name}.yml")))
|
|
76
|
+
return merge(read(vendor, name), app_half, f"{vendor}/{name}: ")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@functools.lru_cache(maxsize=256)
|
|
80
|
+
def _described(vendor: str, name: str, app: Optional[str]) -> Dict[str, Any]:
|
|
81
|
+
app_half = {}
|
|
82
|
+
if app is not None and (Path(app) / vendor / f"{name}.yml").is_file():
|
|
83
|
+
app_half = _parsed(str(Path(app) / vendor / f"{name}.yml"))
|
|
84
|
+
lib_path = DEVICES / vendor / f"{name}.yml"
|
|
85
|
+
if not app_half and not lib_path.is_file():
|
|
86
|
+
known = ", ".join(f"{v}/{n}" for v, n, _ in names()) or "none"
|
|
87
|
+
raise LookupError(f"ZERO FALLBACK: no device file describes {vendor}/{name} (the library's "
|
|
88
|
+
f"backend/devices/{vendor}/{name}.yml, nor the application's). Known: {known}")
|
|
89
|
+
return merge(_parsed(str(lib_path)) if lib_path.is_file() else {}, app_half, f"{vendor}/{name}: ")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def describe(vendor: str, name: str) -> Dict[str, Any]:
|
|
93
|
+
"""Device `vendor/name` whole (both halves), refused by name when neither describes it — the reader a kernel
|
|
94
|
+
asks for its device's facts. Merged once per process; a copy is handed out."""
|
|
95
|
+
app = _application_dir()
|
|
96
|
+
return copy.deepcopy(_described(vendor, name, None if app is None else str(app)))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def param(vendor: str, name: str, section: str, key: str, default):
|
|
100
|
+
"""One `<section>.<key>` of device `vendor/name`, `default` when the device states no such key (the device
|
|
101
|
+
itself is refused by name when nothing describes it) — the per-op reader: no copy of the whole description."""
|
|
102
|
+
app = _application_dir()
|
|
103
|
+
value = (_described(vendor, name, None if app is None else str(app)).get(section) or {}).get(key, _ABSENT)
|
|
104
|
+
if value is _ABSENT:
|
|
105
|
+
return default
|
|
106
|
+
return copy.deepcopy(value) if isinstance(value, (dict, list)) else value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_ABSENT = object()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def in_force() -> Dict[str, Any]:
|
|
113
|
+
"""The description of the device the launch target names (both halves merged), `{}` when no device file matches
|
|
114
|
+
it — what every kernel setting reads, resolved once per process."""
|
|
115
|
+
from nbxtensor.ops import _configs
|
|
116
|
+
return _configs.active_vendor_profile()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def for_arch(arch: str) -> Dict[str, Any]:
|
|
120
|
+
"""The description an arch string resolves to, read WITHOUT a launch target (a caller the target depends on, or
|
|
121
|
+
a derivation that binds no device), and recorded as the one in force."""
|
|
122
|
+
from nbxtensor.ops import _configs
|
|
123
|
+
return _configs.vendor_profile_for_arch(arch)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def forget() -> None:
|
|
127
|
+
"""Forget the description in force: the next reader resolves it again (after the launch target is bound)."""
|
|
128
|
+
from nbxtensor.ops import _configs
|
|
129
|
+
_configs._ACTIVE_PROFILE.clear()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""The arch string a device is known by: the key its device file and its certified kernel settings are found under.
|
|
2
|
+
|
|
3
|
+
A device reports a model name ("Apple M4 Pro", "Tesla V100-SXM2-16GB"); the arch string is that name lowercased with
|
|
4
|
+
spaces hyphenated — "apple-m4-pro". One rule, owned by the library (it was a service the application provided,
|
|
5
|
+
`nbxtensor.host`), read by the Metal backend and by any application keying its own files the same way.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
__all__ = ["arch_string"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@functools.lru_cache(maxsize=16)
|
|
16
|
+
def arch_string(model: Optional[str]) -> str:
|
|
17
|
+
"""The arch string of a device's model name: lowercased, spaces hyphenated ("Apple M4 Pro" -> "apple-m4-pro")."""
|
|
18
|
+
return str(model or "").strip().lower().replace(" ", "-")
|