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,155 @@
1
+ """Precision by policy (DESIGN rule 12): an explicit, immutable object that says in which dtype each op computes.
2
+
3
+ Modelled on JAX's jmp (`Policy(param_dtype, compute_dtype, output_dtype)`, `get_policy("params=float32,
4
+ compute=float16,output=float32")`), with the op classes of PyTorch's autocast as its default (`policy.yml`): a
5
+ policy is plain data — three dtype names, a flag, and the op classes as a mapping of names — handed to the calls that
6
+ use it. No hidden state, no context manager, no global switch: a call computes in the dtype the policy it was given
7
+ says (rule 11: what a Rust core would take as a struct).
8
+
9
+ - `param`: the dtype weights are kept in; `compute`: the dtype the "lower" class computes in; `output`: the dtype a
10
+ caller's results are returned in.
11
+ - `compute_dtype(op, input_dtypes)`: the dtype op `op` computes in, from its class: `float32` (whatever the compute
12
+ dtype; or the compute dtype for a `float32_half_io` op when `float32_ops_half_io` is set), `lower` (the compute
13
+ dtype), `promote` (the widest floating input), none (its inputs promoted among themselves).
14
+ - `fill_value(op, value, dtype)`: a scalar fill of a `scalar_fill` / `creation_fill` op clamped to `dtype`'s finite
15
+ range (a mask sentinel written for bfloat16 does not overflow float16).
16
+
17
+ An application builds the policy from what it knows (NeuroBrix: its DtypeEngine, from the hardware profile and the
18
+ container) and passes it; the library knows no model and no profile.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import dataclasses
23
+ import functools
24
+ from pathlib import Path
25
+ from typing import Dict, FrozenSet, Mapping, Optional, Sequence, Tuple
26
+
27
+ from nbxtensor.dtype.itemsize import canonical_name, finite_max, kind
28
+ from nbxtensor.dtype.promotion import binary_result
29
+
30
+ _FILE = Path(__file__).resolve().parent / "policy.yml"
31
+ CLASSES = ("float32", "lower", "promote", "float32_half_io", "scalar_fill", "creation_fill")
32
+
33
+ __all__ = ["Policy", "default_classes", "get_policy", "CLASSES"]
34
+
35
+
36
+ @functools.lru_cache(maxsize=1)
37
+ def _classes_from_file() -> Tuple[Tuple[str, FrozenSet[str]], ...]:
38
+ import yaml
39
+ data = (yaml.safe_load(_FILE.read_text()) or {}).get("classes") or {}
40
+ unknown = sorted(set(data) - set(CLASSES))
41
+ missing = sorted(set(CLASSES) - set(data))
42
+ if unknown or missing:
43
+ raise ValueError(f"{_FILE}: classes {unknown} unknown, {missing} missing (known: {', '.join(CLASSES)})")
44
+ return tuple((c, frozenset(str(o) for o in data[c])) for c in CLASSES)
45
+
46
+
47
+ def default_classes() -> Dict[str, FrozenSet[str]]:
48
+ """The shipped op classes (`policy.yml`): {class: frozenset of op names}."""
49
+ return dict(_classes_from_file())
50
+
51
+
52
+ def _result(names: Sequence[str]) -> str:
53
+ """The dtype inputs of `names` compute in together: the type promotion of every pair (`binary_result`)."""
54
+ return functools.reduce(binary_result, names)
55
+
56
+
57
+ def _floating(dtype: str) -> bool:
58
+ return kind(dtype) == "float"
59
+
60
+
61
+ @dataclasses.dataclass(frozen=True)
62
+ class Policy:
63
+ param: str = "float32"
64
+ compute: str = "float32"
65
+ output: str = "float32"
66
+ float32_ops_half_io: bool = False
67
+ classes: Tuple[Tuple[str, FrozenSet[str]], ...] = dataclasses.field(default_factory=_classes_from_file)
68
+
69
+ def __post_init__(self):
70
+ for field in ("param", "compute", "output"):
71
+ name = canonical_name(getattr(self, field))
72
+ if not _floating(name):
73
+ raise ValueError(f"Policy.{field}: {name!r} is not a floating dtype")
74
+ object.__setattr__(self, field, name)
75
+ names = [c for c, _ in self.classes]
76
+ if sorted(names) != sorted(CLASSES):
77
+ raise ValueError(f"Policy.classes: {names} — a policy names every class ({', '.join(CLASSES)})")
78
+ object.__setattr__(self, "classes", tuple((c, frozenset(ops)) for c, ops in self.classes))
79
+
80
+ def _ops(self, cls: str) -> FrozenSet[str]:
81
+ return dict(self.classes)[cls]
82
+
83
+ def ops(self, cls: str) -> FrozenSet[str]:
84
+ """The op names of class `cls`."""
85
+ if cls not in CLASSES:
86
+ raise KeyError(f"Policy.ops: {cls!r} is not a class ({', '.join(CLASSES)})")
87
+ return self._ops(cls)
88
+
89
+ def op_class(self, op: str) -> Optional[str]:
90
+ """The compute class of `op` (`float32`, `lower`, `promote`), or None when it is in none."""
91
+ for cls in ("float32", "lower", "promote"):
92
+ if op in self._ops(cls):
93
+ return cls
94
+ return None
95
+
96
+ def compute_dtype(self, op: str, input_dtypes: Sequence[str]) -> Optional[str]:
97
+ """The dtype `op` computes in, given its inputs' dtypes; None when it has no input (a creation op takes the
98
+ dtype it is asked for). An op with no floating input computes in its inputs' promoted dtype."""
99
+ names = [canonical_name(d) for d in input_dtypes]
100
+ if not names:
101
+ return None
102
+ if not any(_floating(n) for n in names):
103
+ return _result(names)
104
+ cls = self.op_class(op)
105
+ if cls == "float32":
106
+ if self.float32_ops_half_io and op in self._ops("float32_half_io"):
107
+ return self.compute
108
+ return "float32"
109
+ if cls == "lower":
110
+ return self.compute
111
+ return _result([n for n in names if _floating(n)] if cls == "promote" else names)
112
+
113
+ def fill_value(self, op: str, value, dtype: str):
114
+ """`value`, a scalar a fill op converts to `dtype`, clamped to that dtype's finite range when `op` is a fill
115
+ op (a value of another kind, or of an op in no fill class, is returned unchanged)."""
116
+ if op not in self._ops("scalar_fill") and op not in self._ops("creation_fill"):
117
+ return value
118
+ if not isinstance(value, float):
119
+ return value
120
+ name = canonical_name(dtype)
121
+ if not _floating(name):
122
+ return value
123
+ top = finite_max(name)
124
+ return max(-top, min(top, value))
125
+
126
+ def replace(self, **changes) -> "Policy":
127
+ """A new policy with `changes` (the policy itself never changes)."""
128
+ return dataclasses.replace(self, **changes)
129
+
130
+ def as_dict(self) -> Dict[str, object]:
131
+ """The policy as plain data (names and lists only)."""
132
+ return {"param": self.param, "compute": self.compute, "output": self.output,
133
+ "float32_ops_half_io": self.float32_ops_half_io,
134
+ "classes": {c: sorted(ops) for c, ops in self.classes}}
135
+
136
+
137
+ def get_policy(spec: str, classes: Optional[Mapping[str, Sequence[str]]] = None) -> Policy:
138
+ """A policy from jmp's spelling: `"params=float32,compute=float16,output=float32"` (also `p=`, `c=`, `o=`, and
139
+ `"float16"` alone for all three); `classes` replaces the shipped op classes."""
140
+ spec = spec.strip()
141
+ fields: Dict[str, str] = {}
142
+ if "=" not in spec:
143
+ fields = {"param": spec, "compute": spec, "output": spec}
144
+ else:
145
+ alias = {"p": "param", "params": "param", "param": "param", "c": "compute", "compute": "compute",
146
+ "o": "output", "output": "output"}
147
+ for part in spec.split(","):
148
+ key, _, value = part.partition("=")
149
+ key = key.strip().lower()
150
+ if key not in alias:
151
+ raise ValueError(f"get_policy: {key!r} names no field (params, compute, output)")
152
+ fields[alias[key]] = value.strip()
153
+ if classes is not None:
154
+ fields["classes"] = tuple((c, frozenset(classes[c])) for c in CLASSES)
155
+ return Policy(**fields)
@@ -0,0 +1,51 @@
1
+ # The default precision policy's op classes (nbxtensor.dtype.policy, DESIGN rule 12): which ops compute in float32
2
+ # whatever the policy's compute dtype, which ones compute in it, which ones take the widest of their inputs. An op in
3
+ # no class keeps its inputs' dtypes (promoted among them). Op names are ATen's base names, the spelling a traced graph
4
+ # carries.
5
+ #
6
+ # Source: PyTorch's autocast tables (aten/src/ATen/autocast_mode.h: AT_FORALL_FP32, AT_FORALL_FP32_SET_OPT_DTYPE,
7
+ # AT_FORALL_LOWER_PRECISION_FP, AT_FORALL_PROMOTE), as NeuroBrix's DtypeEngine carried them
8
+ # (moved here from its engine on 2026-10-29) with its documented deviations: polar and
9
+ # view_as_complex compute in float32 (complex32 does not exist); div computes in the lower precision class; the
10
+ # scaled-dot-product attention ops are in no class (the attention kernels choose their operand dtypes); the high-level
11
+ # RNN ops (lstm, gru, rnn_tanh, rnn_relu) are lower precision, their kernels accumulate in float32.
12
+
13
+ classes:
14
+ # compute in float32 (numerical range or accuracy): transcendental, power, norms, losses, reductions, softmax
15
+ float32: [
16
+ _softmax, _upsample_bicubic2d_aa, _upsample_bilinear2d_aa, _upsample_nearest_exact1d, _upsample_nearest_exact2d,
17
+ _upsample_nearest_exact3d, acos, asin, batch_norm, binary_cross_entropy_with_logits, cdist, cosh,
18
+ cosine_embedding_loss, cosine_similarity, cudnn_batch_norm, cumprod, cumsum, dist, erfinv, exp, expm1,
19
+ frobenius_norm, group_norm, hinge_embedding_loss, huber_loss, instance_norm, kl_div, l1_loss, layer_norm,
20
+ linalg_matrix_norm, linalg_vector_norm, log, log10, log1p, log2, log_softmax, logsumexp, margin_ranking_loss,
21
+ mse_loss, multi_margin_loss, multilabel_margin_loss, native_batch_norm, native_group_norm, native_layer_norm,
22
+ nll_loss, nll_loss2d, nuclear_norm, pdist, poisson_nll_loss, polar, pow, prod, reciprocal, renorm, rsqrt, sinh,
23
+ smooth_l1_loss, soft_margin_loss, softmax, softplus, sum, tan, triplet_margin_loss, upsample_bicubic2d,
24
+ upsample_bilinear2d, upsample_linear1d, upsample_nearest1d, upsample_nearest2d, upsample_nearest3d,
25
+ upsample_trilinear3d, view_as_complex
26
+ ]
27
+ # compute in the policy's compute dtype: convolutions, matrix products, RNN cells, some activations
28
+ lower: [
29
+ _convolution, _thnn_fused_gru_cell, _thnn_fused_lstm_cell, addbmm, addmm, addmv, addr, baddbmm, bmm,
30
+ chain_matmul, conv1d, conv2d, conv3d, conv_tbc, conv_transpose1d, conv_transpose2d, conv_transpose3d,
31
+ convolution, div, einsum, gru, gru_cell, linalg_multi_dot, linalg_vecdot, linear, lstm, lstm_cell, matmul, mm,
32
+ mv, prelu, rnn_relu, rnn_relu_cell, rnn_tanh, rnn_tanh_cell
33
+ ]
34
+ # compute in the widest floating dtype among their inputs (the inputs must agree)
35
+ promote: [
36
+ addcdiv, addcmul, atan2, bilinear, cross, dot, grid_sampler, grid_sampler_2d, grid_sampler_3d, index_add,
37
+ index_put, scatter_add, tensordot, vdot
38
+ ]
39
+ # float32-class ops whose kernels accumulate in float32 internally: a policy may let them take half-precision
40
+ # inputs and outputs (`Policy.float32_ops_half_io`)
41
+ float32_half_io: [
42
+ _log_softmax, _softmax, group_norm, layer_norm, log_softmax, native_group_norm, native_layer_norm, softmax
43
+ ]
44
+ # a scalar fill value converted to a tensor's dtype: clamped to that dtype's finite range (`Policy.fill_value`)
45
+ scalar_fill: [
46
+ fill, fill_, index_fill, index_fill_, masked_fill, masked_fill_
47
+ ]
48
+ # a creation op's scalar fill converted to its requested dtype: clamped likewise
49
+ creation_fill: [
50
+ full, full_like, new_full
51
+ ]
@@ -0,0 +1,112 @@
1
+ """Type promotion: the one rule for the dtype an op's floating inputs are brought to.
2
+
3
+ Two rules decide it, both read on dtype NAMES (plain data, R-NBXT 11) through the one table:
4
+
5
+ - `promoted(dtypes)`: an op reading floating inputs of different widths reads them all at the WIDEST, when that
6
+ widest is wider than two bytes; among two-byte floats alone nothing is promoted (which half dtype an op computes
7
+ in is the caller's precision policy, not a promotion).
8
+ - `binary_result(a, b)`: an element-wise op of two tensors computes in the higher of the two by kind, then by
9
+ width; two different dtypes of one kind and width (float16 and bfloat16) keep the first operand's.
10
+ - `float_math_input(dtype)`: a floating-math op (rsqrt, exp, log ...) given an integer or bool input reads it as
11
+ float32, as PyTorch's type promotion does; a floating or complex input is read as it is. Without it rsqrt of an
12
+ int64 2 truncated 0.7071 to 0 (Kokoro's vocoder residual collapsed to zeros in NeuroBrix, 2026).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from typing import Optional, Sequence
17
+
18
+ from .itemsize import itemsize, kind
19
+
20
+ __all__ = ["promoted", "float_math_input", "binary_result", "scalar_result"]
21
+
22
+ # The order of the kinds a binary result climbs (PyTorch type promotion: bool < unsigned < signed < float < complex).
23
+ _KIND_ORDER = ("bool", "uint", "int", "float", "complex")
24
+
25
+
26
+ def promoted(dtypes: Sequence[str]) -> Optional[str]:
27
+ """The dtype every floating input of an op is brought to, or None when no promotion applies: the widest
28
+ floating dtype among `dtypes` (the first of them in order on a tie), when it is wider than two bytes."""
29
+ widest, size = None, 0
30
+ for d in dtypes:
31
+ if kind(d) != "float":
32
+ continue
33
+ s = itemsize(d)
34
+ if s > size:
35
+ widest, size = d, s
36
+ return widest if widest is not None and size > 2 else None
37
+
38
+
39
+ def float_math_input(dtype: str) -> str:
40
+ """The dtype a floating-math op reads an input of `dtype` at: float32 for an integer or bool input, the input's
41
+ own dtype otherwise."""
42
+ return dtype if kind(dtype) in ("float", "complex") else "float32"
43
+
44
+
45
+ def _of_kind(k: str, nbytes: int) -> str:
46
+ """The table's dtype of kind `k` and `nbytes` bytes (the standard formats: intN, uintN, floatN, complexN)."""
47
+ name = {"int": "int", "uint": "uint", "float": "float", "complex": "complex"}[k] + str(8 * nbytes)
48
+ if kind(name) != k or itemsize(name) != nbytes:
49
+ raise ValueError(f"no {k} dtype of {nbytes} bytes in the dtype table")
50
+ return name
51
+
52
+
53
+ def binary_result(a: str, b: str) -> str:
54
+ """The dtype an element-wise op of two tensors of dtypes `a` and `b` computes and writes in — the Array API
55
+ standard's type promotion (data-apis.org/array-api, "Type Promotion Rules"), the same whatever the operands' order:
56
+ within a kind the wider; a signed with an unsigned integer, the signed integer wide enough for both (none holds
57
+ uint64: refused by name); a real float with a complex, the complex carrying the wider float's precision. What the
58
+ standard leaves unspecified follows PyTorch, the engine's oracle: across kinds the higher kind (bool, unsigned,
59
+ signed, float, complex) with its own width; two floats of one width and different formats (float16, bfloat16)
60
+ meet in float32."""
61
+ ka, kb = kind(a), kind(b)
62
+ if ka == kb:
63
+ if itemsize(a) != itemsize(b):
64
+ return a if itemsize(a) > itemsize(b) else b
65
+ if a == b or ka not in ("float", "complex"):
66
+ return a
67
+ return _of_kind(ka, 2 * itemsize(a)) # float16 with bfloat16: float32
68
+ if {ka, kb} == {"int", "uint"}:
69
+ signed, unsigned = (a, b) if ka == "int" else (b, a)
70
+ if itemsize(unsigned) >= 8:
71
+ raise ValueError(f"binary_result({a}, {b}): no signed integer holds every {unsigned} value")
72
+ return _of_kind("int", max(itemsize(signed), 2 * itemsize(unsigned)))
73
+ if {ka, kb} == {"float", "complex"}:
74
+ real, cplx = (a, b) if ka == "float" else (b, a)
75
+ return _of_kind("complex", max(itemsize(cplx), 2 * itemsize(real)))
76
+ return a if _KIND_ORDER.index(ka) > _KIND_ORDER.index(kb) else b
77
+
78
+
79
+ def _int_bounds(dtype: str):
80
+ bits = 8 * itemsize(dtype)
81
+ return (0, (1 << bits) - 1) if kind(dtype) == "uint" else (-(1 << (bits - 1)), (1 << (bits - 1)) - 1)
82
+
83
+
84
+ def scalar_result(dtype: str, scalar) -> str:
85
+ """The dtype an element-wise op of an array of `dtype` and a Python scalar computes in: the scalar is WEAK, it takes
86
+ the array's dtype (the Array API standard's "mixing arrays with Python scalars", NumPy's NEP 50) — a bool with a
87
+ bool array, an int with an integer array when the value fits it (refused by name otherwise: the standard leaves
88
+ the overflow open, a silent wrap is never taken), an int or a float with a real float array, an int, float or
89
+ complex with a complex array, a complex with a real float array as the complex of that precision (complex64 for a
90
+ half float: the table holds no complex32). Left open by the
91
+ standard and decided by NEP 50: a float with an integer or bool array is float64, a complex with one is
92
+ complex128, an int with a bool array is int64 (PyTorch differs on the first: float32; README, Array API)."""
93
+ k = kind(dtype)
94
+ if isinstance(scalar, bool):
95
+ return dtype
96
+ if isinstance(scalar, int):
97
+ if k in ("int", "uint"):
98
+ lo, hi = _int_bounds(dtype)
99
+ if not lo <= scalar <= hi:
100
+ raise OverflowError(f"scalar_result: {scalar} does not fit {dtype} [{lo}, {hi}]")
101
+ return dtype
102
+ return "int64" if k == "bool" else dtype
103
+ if isinstance(scalar, float):
104
+ return dtype if k in ("float", "complex") else "float64"
105
+ if isinstance(scalar, complex):
106
+ if k == "complex":
107
+ return dtype
108
+ if k != "float":
109
+ return "complex128"
110
+ # the complex of the float's precision; a half float has none in the table (no complex32): complex64 holds it
111
+ return _of_kind("complex", max(2 * itemsize(dtype), itemsize("complex64")))
112
+ raise TypeError(f"scalar_result: {type(scalar).__name__} is not a Python scalar (bool, int, float, complex)")
nbxtensor/host.py ADDED
@@ -0,0 +1,55 @@
1
+ """What the application that runs nbxtensor provides to it: the facts and policies the library does not own.
2
+
3
+ nbxtensor computes; the application decides. The decisions still read here: the hardware vendor's files (tiles,
4
+ budgets, the matrix unit) and where they live, the dtype policy that picks an attention's and a matrix unit's operand
5
+ dtypes, and where a backend build may live. Each is moving to a fact the library reads itself or a setting with a
6
+ default (docs/nbxtensor/API.md section 11); what left already: the memory limit (`memory.set_limit`), the certified
7
+ store (`ops.autotune_settings.set_store`), the device's memory (`backend.device_facts`), the launch observer
8
+ (`ops.launch_observer`). The application provides each service ONCE, by name (`provide`); the library reads it by name
9
+ (`need`). A service no application provided is refused by name, never guessed.
10
+
11
+ Each service is a callable. An application provides a function that reads its own state at call time, so a value it
12
+ changes later is the one read.
13
+
14
+ Torch-free, numpy-free, and nothing here imports the application.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from typing import Callable, Dict
19
+
20
+ #: the services this library reads, and what each one answers — the contract an application fulfils
21
+ SERVICES: Dict[str, str] = {
22
+ "vendors_dir": "() -> the directory of the application's own vendor files (pathlib.Path); optional: absent, the library's device files alone",
23
+ }
24
+
25
+ #: hooks an application MAY provide; absent, the library runs as the value here describes (never a guessed fact)
26
+ _ABSENT: dict = {}
27
+
28
+ _PROVIDED: Dict[str, Callable] = {}
29
+
30
+
31
+ def provide(name: str, fn: Callable) -> None:
32
+ """The application provides service `name` (one of `SERVICES`). Providing it again replaces it."""
33
+ if name not in SERVICES:
34
+ raise KeyError(f"nbxtensor.host: {name!r} is not a service this library reads (known: {', '.join(sorted(SERVICES))})")
35
+ if not callable(fn):
36
+ raise TypeError(f"nbxtensor.host: service {name!r} must be callable, got {type(fn).__name__}")
37
+ _PROVIDED[name] = fn
38
+
39
+
40
+ def need(name: str) -> Callable:
41
+ """Service `name` as the application provided it; refused by name when none did."""
42
+ try:
43
+ return _PROVIDED[name]
44
+ except KeyError:
45
+ if name in _ABSENT:
46
+ return _ABSENT[name]
47
+ if name not in SERVICES:
48
+ raise KeyError(f"nbxtensor.host: {name!r} is not a service this library reads") from None
49
+ raise RuntimeError(f"ZERO FALLBACK: nbxtensor needs the service {name!r} ({SERVICES[name]}) and the application "
50
+ f"running it provided none (nbxtensor.host.provide)") from None
51
+
52
+
53
+ def provided() -> Dict[str, Callable]:
54
+ """The services provided so far (a copy)."""
55
+ return dict(_PROVIDED)
@@ -0,0 +1 @@
1
+ """Reading tensors from their files into NBXTensor (`io.safetensors`)."""
@@ -0,0 +1,197 @@
1
+ """How fast a machine reads its weight files, by number of parallel reads — the measurement behind a hardware profile's
2
+ I/O worker count (`measure`). The reads are the loaders' own: pieces of `PIECE` bytes through `os.preadv` from a pool
3
+ of threads (`nbxtensor.io.safetensors.read_bytes`). Each trial evicts the files' cached pages first and bypasses the
4
+ cache while it reads (macOS `msync(MS_INVALIDATE)` + `F_NOCACHE`; Linux `posix_fadvise(DONTNEED)`), so the rate is the
5
+ storage path's (a disk, a RAID, an NFS or RDMA mount), never the page cache's."""
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import sys
10
+ import time
11
+ from concurrent.futures import ThreadPoolExecutor
12
+ from typing import Dict, List, Optional, Sequence
13
+
14
+ __all__ = ["PIECE", "WITNESS_BYTES", "START_BYTES", "mount_of", "pieces", "trial", "choose", "measure", "workers_for"]
15
+
16
+ PIECE = 16 << 20 # the loaders' read piece
17
+
18
+
19
+ def _no_cache(fd: int) -> None:
20
+ if sys.platform == "darwin":
21
+ import fcntl
22
+ fcntl.fcntl(fd, 48, 1) # F_NOCACHE: this descriptor's reads add nothing to the unified buffer cache
23
+ elif hasattr(os, "posix_fadvise"):
24
+ os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
25
+
26
+
27
+ def _evict(path: str, size: int) -> None:
28
+ """The file's pages out of the cache before a trial: F_NOCACHE alone still SERVES pages cached earlier (measured
29
+ 2026-10-30 on the Mac's NFS cache: 19 064 MiB/s cached, 32 MiB/s after this). macOS: msync(MS_INVALIDATE) on a
30
+ shared read-only mapping; Linux: posix_fadvise(DONTNEED)."""
31
+ fd = os.open(path, os.O_RDONLY)
32
+ try:
33
+ if sys.platform == "darwin":
34
+ import ctypes
35
+ import ctypes.util
36
+ libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
37
+ libc.mmap.restype = ctypes.c_void_p
38
+ libc.mmap.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int, ctypes.c_int, ctypes.c_int,
39
+ ctypes.c_longlong]
40
+ libc.msync.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int]
41
+ libc.munmap.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
42
+ addr = libc.mmap(None, size, 1, 1, fd, 0) # PROT_READ, MAP_SHARED
43
+ if libc.msync(addr, size, 2) != 0: # MS_INVALIDATE
44
+ raise OSError(ctypes.get_errno(), f"msync(MS_INVALIDATE) {path}")
45
+ libc.munmap(addr, size)
46
+ elif hasattr(os, "posix_fadvise"):
47
+ os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
48
+ finally:
49
+ os.close(fd)
50
+
51
+
52
+ def pieces(files, max_bytes: int):
53
+ """(path, offset, bytes) pieces of the loaders' size over `files`, in order, until `max_bytes`; and their total."""
54
+ out, total = [], 0
55
+ for path in files:
56
+ size = os.path.getsize(path)
57
+ for off in range(0, size, PIECE):
58
+ n = min(PIECE, size - off)
59
+ out.append((path, off, n))
60
+ total += n
61
+ if total >= max_bytes:
62
+ return out, total
63
+ return out, total
64
+
65
+
66
+ def trial(pieces, workers: int) -> float:
67
+ """Bytes per second reading `pieces` with `workers` threads, the cache bypassed."""
68
+ for path in {p for p, _, _ in pieces}:
69
+ _evict(path, os.path.getsize(path))
70
+ fds = {}
71
+ for path, _, _ in pieces:
72
+ if path not in fds:
73
+ fd = os.open(path, os.O_RDONLY)
74
+ _no_cache(fd)
75
+ fds[path] = fd
76
+ bufs = [bytearray(PIECE) for _ in range(workers)]
77
+
78
+ def read(i):
79
+ path, off, n = pieces[i]
80
+ buf = memoryview(bufs[i % workers])[:n]
81
+ got = 0
82
+ while got < n:
83
+ got += os.preadv(fds[path], [buf[got:]], off + got)
84
+ return n
85
+
86
+ t = time.perf_counter()
87
+ with ThreadPoolExecutor(workers) as pool:
88
+ total = sum(pool.map(read, range(len(pieces))))
89
+ dt = time.perf_counter() - t
90
+ for fd in fds.values():
91
+ os.close(fd)
92
+ return total / dt
93
+
94
+
95
+ def choose(rates, within: float) -> int:
96
+ """The smallest worker count whose rate is within `within` of the best: a worker past it holds a shard in host
97
+ memory for nothing."""
98
+ best = max(rates.values())
99
+ return min(c for c, r in rates.items() if r >= (1 - within) * best)
100
+
101
+
102
+ def measure(files: Sequence[str], max_bytes: int, max_workers: int, within: float = 0.05) -> Dict[str, object]:
103
+ """Rates (bytes/s) at 1, 2, 4, ... `max_workers` parallel reads of `files` (at most `max_bytes` per trial), the
104
+ worker count `choose` takes, and whether the storage was steady: the one-worker trial is run first and last, and a
105
+ gap past `within` between them makes `steady` False — such a measure is not to be recorded."""
106
+ ps, total = pieces(list(files), max_bytes)
107
+ counts: List[int] = []
108
+ n = 1
109
+ while n <= max_workers:
110
+ counts.append(n)
111
+ n *= 2
112
+ rates = {c: trial(ps, c) for c in counts}
113
+ last = trial(ps, 1)
114
+ steady = abs(last - rates[1]) <= within * max(last, rates[1])
115
+ return {"bytes": total, "rates": rates, "workers": choose(rates, within), "steady": steady, "last_one_worker": last}
116
+
117
+
118
+ WITNESS_BYTES = 1 << 30 # the witness file written when the storage holds no weight file yet
119
+ START_BYTES = 2 << 30 # the first trial: one shard's read, at most this
120
+
121
+
122
+ def mount_of(path) -> str:
123
+ """The mount point the file `path` lives on: the storage a read of it goes through."""
124
+ p = os.path.realpath(path)
125
+ while not os.path.ismount(p):
126
+ p = os.path.dirname(p)
127
+ return p
128
+
129
+
130
+ def workers_for(directory, witness_dir=None, max_workers: Optional[int] = None,
131
+ pattern: str = "*/components/*/weights/*.safetensors") -> Dict[str, object]:
132
+ """The parallel reads to keep in flight on the storage a weight cache is read from, MEASURED there:
133
+ {"workers": n, "storage": the mount point read, "read": "shards" or "witness"}.
134
+
135
+ The files read are `directory`'s largest matching `pattern` (the cache's shards). When it holds none yet, a witness
136
+ of `WITNESS_BYTES` is written in `witness_dir` (a local scratch directory the caller names, on the local cache's
137
+ storage when there is one) and removed — NEVER in `directory`, which may be a cache shared over a network; with no
138
+ `witness_dir`, refused by name. The answer says which storage it read. Counts 1, 2, 4, ... up to `max_workers`
139
+ (the machine's `os.cpu_count()` when not given).
140
+
141
+ The first trial reads one shard (the largest file, at most `START_BYTES`): a loader reads whole shards, and a
142
+ shorter trial undervalues parallel reads (64 MiB picked 4 on the rack's RDMA cache, 2 GiB 8/16/16 — the count
143
+ the planner prices host memory by is the one a load sees). The trials then grow while the measure is not stable:
144
+ at a given size, up to three measures; when two of them are steady (`measure`'s first/last one-worker check) the
145
+ answer is their median — one draw fluctuates and a plan prices the count. Otherwise the trial size is quadrupled,
146
+ up to everything the files hold. When no size gives a stable measure, refused by name: never a silent None, never
147
+ a default."""
148
+ from pathlib import Path
149
+ root = Path(directory)
150
+ max_workers = max_workers or os.cpu_count() or 1
151
+ files = [str(p) for p in sorted(root.glob(pattern), key=lambda p: -p.stat().st_size)[:4]]
152
+ witness = None
153
+ if not files:
154
+ if witness_dir is None:
155
+ raise RuntimeError(f"ZERO FALLBACK: {root} holds no weight file to measure its reads on, and no local "
156
+ f"directory was named for a witness file (it is never written into the cache itself)")
157
+ Path(witness_dir).mkdir(parents=True, exist_ok=True)
158
+ witness = Path(witness_dir) / f".read_rate_witness_{os.getpid()}"
159
+ with open(witness, "wb") as f:
160
+ for _ in range(WITNESS_BYTES // PIECE):
161
+ f.write(os.urandom(PIECE))
162
+ files = [str(witness)]
163
+ try:
164
+ available = sum(os.path.getsize(p) for p in files)
165
+ # A loader reads whole shards: a trial shorter than one is a rate no load sees.
166
+ size, tried = max(4 * PIECE, min(max(os.path.getsize(p) for p in files), START_BYTES)), []
167
+ while True:
168
+ size = min(size, available)
169
+ picks = []
170
+ for _ in range(3):
171
+ m = measure(files, size, max_workers)
172
+ if m["steady"]:
173
+ picks.append(int(m["workers"]))
174
+ if len(picks) == 2:
175
+ break
176
+ tried.append(f"{size >> 20} MiB: {len(picks)}/3 steady")
177
+ if len(picks) >= 2:
178
+ n = picks[0] if picks[0] == picks[1] else _median_of_three(files, size, max_workers, picks)
179
+ return {"workers": n, "storage": mount_of(files[0]), "read": "witness" if witness else "shards"}
180
+ if size >= available:
181
+ raise RuntimeError(
182
+ f"ZERO FALLBACK: no stable read-rate measure on {mount_of(files[0])} up to {available >> 20} MiB per "
183
+ f"trial ({'; '.join(tried)}): another reader on the storage? measure again on a quiet machine "
184
+ f"(tools/measure_io_workers.py) — no worker count is assumed")
185
+ size *= 4
186
+ finally:
187
+ if witness is not None:
188
+ witness.unlink(missing_ok=True)
189
+
190
+
191
+ def _median_of_three(files, size, max_workers, picks):
192
+ """Two steady draws disagree: a third steady one decides (their median)."""
193
+ for _ in range(3):
194
+ m = measure(files, size, max_workers)
195
+ if m["steady"]:
196
+ return sorted(picks + [int(m["workers"])])[1]
197
+ return sorted(picks)[0]