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.
Files changed (291) hide show
  1. nbxtensor/THIRD_PARTY_NOTICES.md +287 -0
  2. nbxtensor/__init__.py +17 -0
  3. nbxtensor/backend/__init__.py +1 -0
  4. nbxtensor/backend/capabilities.py +207 -0
  5. nbxtensor/backend/cpu/__init__.py +1 -0
  6. nbxtensor/backend/cpu/blas.py +102 -0
  7. nbxtensor/backend/cpu/pool.py +204 -0
  8. nbxtensor/backend/cpu_backend.py +162 -0
  9. nbxtensor/backend/device_facts.py +105 -0
  10. nbxtensor/backend/device_files.py +129 -0
  11. nbxtensor/backend/device_names.py +18 -0
  12. nbxtensor/backend/devices/amd/cdna.yml +228 -0
  13. nbxtensor/backend/devices/amd/cdna2.yml +228 -0
  14. nbxtensor/backend/devices/amd/cdna3.yml +227 -0
  15. nbxtensor/backend/devices/apple/apple_m1.yml +266 -0
  16. nbxtensor/backend/devices/apple/apple_m1_max.yml +266 -0
  17. nbxtensor/backend/devices/apple/apple_m1_pro.yml +266 -0
  18. nbxtensor/backend/devices/apple/apple_m1_ultra.yml +266 -0
  19. nbxtensor/backend/devices/apple/apple_m2.yml +266 -0
  20. nbxtensor/backend/devices/apple/apple_m2_max.yml +266 -0
  21. nbxtensor/backend/devices/apple/apple_m2_pro.yml +266 -0
  22. nbxtensor/backend/devices/apple/apple_m2_ultra.yml +266 -0
  23. nbxtensor/backend/devices/apple/apple_m3.yml +266 -0
  24. nbxtensor/backend/devices/apple/apple_m3_max.yml +266 -0
  25. nbxtensor/backend/devices/apple/apple_m3_pro.yml +266 -0
  26. nbxtensor/backend/devices/apple/apple_m3_ultra.yml +266 -0
  27. nbxtensor/backend/devices/apple/apple_m4.yml +266 -0
  28. nbxtensor/backend/devices/apple/apple_m4_max.yml +266 -0
  29. nbxtensor/backend/devices/apple/apple_m4_pro.yml +308 -0
  30. nbxtensor/backend/devices/apple/apple_m5.yml +269 -0
  31. nbxtensor/backend/devices/apple/apple_m5_max.yml +269 -0
  32. nbxtensor/backend/devices/apple/apple_m5_pro.yml +269 -0
  33. nbxtensor/backend/devices/apple/apple_m5_ultra.yml +269 -0
  34. nbxtensor/backend/devices/apple/apple_m6.yml +269 -0
  35. nbxtensor/backend/devices/apple/apple_silicon.yml +263 -0
  36. nbxtensor/backend/devices/nvidia/ampere.yml +214 -0
  37. nbxtensor/backend/devices/nvidia/hopper.yml +213 -0
  38. nbxtensor/backend/devices/nvidia/volta.yml +411 -0
  39. nbxtensor/backend/faults.py +151 -0
  40. nbxtensor/backend/host_memory.py +185 -0
  41. nbxtensor/backend/launcher.py +2350 -0
  42. nbxtensor/backend/launcher_contract.py +429 -0
  43. nbxtensor/backend/metal_backend.py +661 -0
  44. nbxtensor/backend/metal_device.py +1274 -0
  45. nbxtensor/backend/process_memory.py +73 -0
  46. nbxtensor/backend/replay_graph.py +322 -0
  47. nbxtensor/backend/runtime.py +409 -0
  48. nbxtensor/backend/triton_ext_driver.py +735 -0
  49. nbxtensor/dtype/__init__.py +1 -0
  50. nbxtensor/dtype/convert.py +107 -0
  51. nbxtensor/dtype/dtypes.yml +70 -0
  52. nbxtensor/dtype/itemsize.py +166 -0
  53. nbxtensor/dtype/nbx_dtype.py +72 -0
  54. nbxtensor/dtype/operands.py +158 -0
  55. nbxtensor/dtype/outputs.py +64 -0
  56. nbxtensor/dtype/policy.py +155 -0
  57. nbxtensor/dtype/policy.yml +51 -0
  58. nbxtensor/dtype/promotion.py +112 -0
  59. nbxtensor/host.py +55 -0
  60. nbxtensor/io/__init__.py +1 -0
  61. nbxtensor/io/read_rate.py +197 -0
  62. nbxtensor/io/safetensors.py +314 -0
  63. nbxtensor/memory/__init__.py +9 -0
  64. nbxtensor/memory/allocator.py +1988 -0
  65. nbxtensor/memory/arena.py +75 -0
  66. nbxtensor/memory/device_memory.py +150 -0
  67. nbxtensor/memory/device_transfer.py +154 -0
  68. nbxtensor/memory/layout.py +43 -0
  69. nbxtensor/memory/memory_pool.py +191 -0
  70. nbxtensor/memory/metal.py +96 -0
  71. nbxtensor/memory/oom_advice.py +91 -0
  72. nbxtensor/memory/pointer_tables.py +323 -0
  73. nbxtensor/memory/pool.py +507 -0
  74. nbxtensor/memory/shadow.py +43 -0
  75. nbxtensor/memory/slab.py +273 -0
  76. nbxtensor/memory/trace_replay.py +122 -0
  77. nbxtensor/memory/traces/sana-video-2b-720p.16g.2026-11-02.tsv +13610 -0
  78. nbxtensor/memory/vmm.py +118 -0
  79. nbxtensor/ops/__init__.py +1 -0
  80. nbxtensor/ops/_autotune_policy.py +90 -0
  81. nbxtensor/ops/_common.py +32 -0
  82. nbxtensor/ops/_configs.py +1041 -0
  83. nbxtensor/ops/_cost.py +231 -0
  84. nbxtensor/ops/abs.py +17 -0
  85. nbxtensor/ops/adaptive_avg_pool2d.py +161 -0
  86. nbxtensor/ops/add.py +96 -0
  87. nbxtensor/ops/addcdiv.py +29 -0
  88. nbxtensor/ops/addcmul.py +29 -0
  89. nbxtensor/ops/addmv_op.py +64 -0
  90. nbxtensor/ops/addr.py +68 -0
  91. nbxtensor/ops/all_reduce.py +83 -0
  92. nbxtensor/ops/amax.py +40 -0
  93. nbxtensor/ops/any_reduce.py +83 -0
  94. nbxtensor/ops/arange_op.py +27 -0
  95. nbxtensor/ops/argmax.py +107 -0
  96. nbxtensor/ops/argmin.py +93 -0
  97. nbxtensor/ops/atan2.py +71 -0
  98. nbxtensor/ops/attention_wrappers.py +1648 -0
  99. nbxtensor/ops/autotune_bucket.py +137 -0
  100. nbxtensor/ops/autotune_cache.py +430 -0
  101. nbxtensor/ops/autotune_certified.py +1262 -0
  102. nbxtensor/ops/autotune_refusals.py +380 -0
  103. nbxtensor/ops/autotune_settings.py +58 -0
  104. nbxtensor/ops/avg_pool2d.py +73 -0
  105. nbxtensor/ops/baddbmm_op.py +132 -0
  106. nbxtensor/ops/batch_norm.py +163 -0
  107. nbxtensor/ops/binary_strided.py +196 -0
  108. nbxtensor/ops/bitwise_and.py +26 -0
  109. nbxtensor/ops/bitwise_left_shift.py +47 -0
  110. nbxtensor/ops/bitwise_not.py +24 -0
  111. nbxtensor/ops/bitwise_or.py +26 -0
  112. nbxtensor/ops/bitwise_right_shift.py +47 -0
  113. nbxtensor/ops/bucketize.py +37 -0
  114. nbxtensor/ops/cat_op.py +87 -0
  115. nbxtensor/ops/celu.py +20 -0
  116. nbxtensor/ops/clamp.py +24 -0
  117. nbxtensor/ops/clamp_min.py +22 -0
  118. nbxtensor/ops/conv2d.py +265 -0
  119. nbxtensor/ops/conv2d_m8n8k4.py +165 -0
  120. nbxtensor/ops/conv_depthwise2d.py +154 -0
  121. nbxtensor/ops/conv_transpose2d.py +148 -0
  122. nbxtensor/ops/conv_wrappers.py +1482 -0
  123. nbxtensor/ops/copy.py +18 -0
  124. nbxtensor/ops/copy_op.py +39 -0
  125. nbxtensor/ops/cos.py +17 -0
  126. nbxtensor/ops/count_nonzero.py +64 -0
  127. nbxtensor/ops/cross_entropy.py +67 -0
  128. nbxtensor/ops/cummax.py +109 -0
  129. nbxtensor/ops/cummin.py +105 -0
  130. nbxtensor/ops/cumsum.py +244 -0
  131. nbxtensor/ops/decode_attn_vec.py +162 -0
  132. nbxtensor/ops/depthwise_conv2d.py +132 -0
  133. nbxtensor/ops/dequant_gemv.py +190 -0
  134. nbxtensor/ops/dft.py +67 -0
  135. nbxtensor/ops/dispatch.py +1228 -0
  136. nbxtensor/ops/div.py +75 -0
  137. nbxtensor/ops/dot_op.py +67 -0
  138. nbxtensor/ops/dropout.py +23 -0
  139. nbxtensor/ops/dtype_convert.py +42 -0
  140. nbxtensor/ops/elementwise_launch.py +81 -0
  141. nbxtensor/ops/elementwise_wrappers.py +2315 -0
  142. nbxtensor/ops/elu.py +20 -0
  143. nbxtensor/ops/embedding.py +69 -0
  144. nbxtensor/ops/eq.py +32 -0
  145. nbxtensor/ops/erf.py +18 -0
  146. nbxtensor/ops/exp.py +17 -0
  147. nbxtensor/ops/exp2.py +18 -0
  148. nbxtensor/ops/fft_op.py +125 -0
  149. nbxtensor/ops/fill_op.py +20 -0
  150. nbxtensor/ops/flash_attention.py +307 -0
  151. nbxtensor/ops/flash_attention_m8n8k4.py +384 -0
  152. nbxtensor/ops/flash_decode.py +78 -0
  153. nbxtensor/ops/flip_op.py +60 -0
  154. nbxtensor/ops/floor.py +90 -0
  155. nbxtensor/ops/full_op.py +24 -0
  156. nbxtensor/ops/fused_moe.py +417 -0
  157. nbxtensor/ops/fused_moe_m8n8k4.py +99 -0
  158. nbxtensor/ops/fused_upsample_conv.py +413 -0
  159. nbxtensor/ops/gather_op.py +52 -0
  160. nbxtensor/ops/ge.py +32 -0
  161. nbxtensor/ops/gelu.py +31 -0
  162. nbxtensor/ops/gemv_vec.py +80 -0
  163. nbxtensor/ops/glu.py +43 -0
  164. nbxtensor/ops/grid_sampler.py +145 -0
  165. nbxtensor/ops/group_norm_tiled.py +67 -0
  166. nbxtensor/ops/groupnorm.py +123 -0
  167. nbxtensor/ops/gt.py +32 -0
  168. nbxtensor/ops/hardsigmoid.py +18 -0
  169. nbxtensor/ops/hardswish.py +19 -0
  170. nbxtensor/ops/im2col.py +57 -0
  171. nbxtensor/ops/index_add.py +95 -0
  172. nbxtensor/ops/index_put_op.py +92 -0
  173. nbxtensor/ops/index_select.py +121 -0
  174. nbxtensor/ops/index_wrappers.py +770 -0
  175. nbxtensor/ops/isfinite.py +20 -0
  176. nbxtensor/ops/isinf.py +20 -0
  177. nbxtensor/ops/isnan.py +18 -0
  178. nbxtensor/ops/launch_keys.py +1054 -0
  179. nbxtensor/ops/launch_observer.py +54 -0
  180. nbxtensor/ops/layernorm.py +71 -0
  181. nbxtensor/ops/le.py +35 -0
  182. nbxtensor/ops/leaky_relu.py +19 -0
  183. nbxtensor/ops/lerp.py +76 -0
  184. nbxtensor/ops/linalg_wrappers.py +1370 -0
  185. nbxtensor/ops/linspace_op.py +47 -0
  186. nbxtensor/ops/log.py +17 -0
  187. nbxtensor/ops/log_sigmoid.py +23 -0
  188. nbxtensor/ops/logical_and.py +26 -0
  189. nbxtensor/ops/logical_not.py +24 -0
  190. nbxtensor/ops/logical_or.py +26 -0
  191. nbxtensor/ops/logical_xor.py +37 -0
  192. nbxtensor/ops/loss_wrappers.py +282 -0
  193. nbxtensor/ops/lt.py +32 -0
  194. nbxtensor/ops/masked_fill.py +21 -0
  195. nbxtensor/ops/matmul.py +377 -0
  196. nbxtensor/ops/matmul_m8n8k4.py +235 -0
  197. nbxtensor/ops/max_pool2d.py +74 -0
  198. nbxtensor/ops/max_pool3d.py +93 -0
  199. nbxtensor/ops/max_reduce.py +91 -0
  200. nbxtensor/ops/maximum.py +18 -0
  201. nbxtensor/ops/mean.py +40 -0
  202. nbxtensor/ops/min_reduce.py +91 -0
  203. nbxtensor/ops/minimum.py +18 -0
  204. nbxtensor/ops/mish.py +22 -0
  205. nbxtensor/ops/mma_m8n8k4.py +152 -0
  206. nbxtensor/ops/moe_align.py +195 -0
  207. nbxtensor/ops/moe_decode_vec.py +374 -0
  208. nbxtensor/ops/moe_device_residency.py +141 -0
  209. nbxtensor/ops/moe_wrappers.py +345 -0
  210. nbxtensor/ops/mse_loss.py +79 -0
  211. nbxtensor/ops/mul.py +51 -0
  212. nbxtensor/ops/nan_to_num.py +23 -0
  213. nbxtensor/ops/ne.py +32 -0
  214. nbxtensor/ops/neg.py +17 -0
  215. nbxtensor/ops/nllloss.py +124 -0
  216. nbxtensor/ops/nonzero.py +33 -0
  217. nbxtensor/ops/nonzero_op.py +62 -0
  218. nbxtensor/ops/norm_wrappers.py +579 -0
  219. nbxtensor/ops/ones_op.py +20 -0
  220. nbxtensor/ops/oracles/__init__.py +6 -0
  221. nbxtensor/ops/oracles/conv2d_fp64.py +450 -0
  222. nbxtensor/ops/pad_op.py +159 -0
  223. nbxtensor/ops/paged_decode_attn.py +215 -0
  224. nbxtensor/ops/pixel_shuffle.py +144 -0
  225. nbxtensor/ops/pixel_unshuffle.py +72 -0
  226. nbxtensor/ops/pow.py +137 -0
  227. nbxtensor/ops/prod.py +81 -0
  228. nbxtensor/ops/rand_op.py +52 -0
  229. nbxtensor/ops/random_wrappers.py +366 -0
  230. nbxtensor/ops/reciprocal.py +17 -0
  231. nbxtensor/ops/reduction_wrappers.py +845 -0
  232. nbxtensor/ops/relu.py +18 -0
  233. nbxtensor/ops/remainder.py +46 -0
  234. nbxtensor/ops/repeat_interleave.py +48 -0
  235. nbxtensor/ops/repeat_op.py +132 -0
  236. nbxtensor/ops/residual_chain.py +374 -0
  237. nbxtensor/ops/rmsnorm.py +68 -0
  238. nbxtensor/ops/rope.py +142 -0
  239. nbxtensor/ops/rsqrt.py +17 -0
  240. nbxtensor/ops/scaled_softmax.py +96 -0
  241. nbxtensor/ops/scatter_op.py +139 -0
  242. nbxtensor/ops/screen_oracle.py +479 -0
  243. nbxtensor/ops/selu.py +22 -0
  244. nbxtensor/ops/shape_wrappers.py +1032 -0
  245. nbxtensor/ops/sigmoid.py +19 -0
  246. nbxtensor/ops/silu.py +27 -0
  247. nbxtensor/ops/sin.py +17 -0
  248. nbxtensor/ops/softmax.py +169 -0
  249. nbxtensor/ops/softplus.py +27 -0
  250. nbxtensor/ops/sort_op.py +304 -0
  251. nbxtensor/ops/spectral_wrappers.py +481 -0
  252. nbxtensor/ops/sqrt.py +17 -0
  253. nbxtensor/ops/stack_op.py +84 -0
  254. nbxtensor/ops/std.py +113 -0
  255. nbxtensor/ops/strided_copy.py +200 -0
  256. nbxtensor/ops/sub.py +35 -0
  257. nbxtensor/ops/sum.py +40 -0
  258. nbxtensor/ops/tan.py +19 -0
  259. nbxtensor/ops/tanh.py +21 -0
  260. nbxtensor/ops/threshold.py +19 -0
  261. nbxtensor/ops/tiling.yml +91 -0
  262. nbxtensor/ops/tiling_policy.py +43 -0
  263. nbxtensor/ops/tiling_sizes.py +370 -0
  264. nbxtensor/ops/topk.py +267 -0
  265. nbxtensor/ops/trace_op.py +64 -0
  266. nbxtensor/ops/tril.py +64 -0
  267. nbxtensor/ops/triu.py +62 -0
  268. nbxtensor/ops/unfold_op.py +37 -0
  269. nbxtensor/ops/unique_op.py +97 -0
  270. nbxtensor/ops/upsample_bilinear2d.py +106 -0
  271. nbxtensor/ops/upsample_nearest2d.py +52 -0
  272. nbxtensor/ops/upsample_nearest3d.py +61 -0
  273. nbxtensor/ops/var.py +136 -0
  274. nbxtensor/ops/vector_norm.py +216 -0
  275. nbxtensor/ops/weight_norm.py +104 -0
  276. nbxtensor/ops/where.py +39 -0
  277. nbxtensor/ops/wrappers.py +825 -0
  278. nbxtensor/ops/zeros_op.py +20 -0
  279. nbxtensor/quant/__init__.py +1 -0
  280. nbxtensor/quant/quantized_tensor.py +187 -0
  281. nbxtensor/random/__init__.py +1 -0
  282. nbxtensor/random/mt_randn.py +154 -0
  283. nbxtensor/random/rng_pin.py +59 -0
  284. nbxtensor/random/rng_stream.py +118 -0
  285. nbxtensor/random/seeded_draw.py +223 -0
  286. nbxtensor/shape/__init__.py +157 -0
  287. nbxtensor/tensor.py +1867 -0
  288. nbxtensor-0.0.1.dist-info/METADATA +379 -0
  289. nbxtensor-0.0.1.dist-info/RECORD +291 -0
  290. nbxtensor-0.0.1.dist-info/WHEEL +4 -0
  291. 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(" ", "-")