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,151 @@
1
+ """The device-fault channel: a kernel on a backend that does not honour `tl.device_assert` reports a contract violation
2
+ through a device word, read at the sync points. Cut out of tensor.py (M2, 2026-10-29), unchanged."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import ctypes
7
+ import functools
8
+ import threading
9
+
10
+ from nbxtensor.backend import capabilities as _caps
11
+
12
+
13
+ # Whether this backend's device memory is directly readable at its own
14
+ # `data_ptr()` from the host. The fault channel below reads its status word
15
+ # that way — after a flush that has already happened, so no copy and no
16
+ # recursion through `memcpy`. Unified memory says yes; a discrete heap does
17
+ # not, and must add its row (and a copy path) before the channel is armed
18
+ # on it.
19
+ _BACKEND_MEMORY_IS_HOST_READABLE = {"cuda": False, "hip": False, "metal": True}
20
+
21
+
22
+ # ============================================================================
23
+ # DEVICE-SIDE FAULT CHANNEL
24
+ # ============================================================================
25
+ #
26
+ # The portable half of `tl.device_assert`, for backends that do not honour
27
+ # it. A kernel that detects a broken contract stores its fault code into a
28
+ # one-word status buffer; the code is a `tl.constexpr`, so on a backend that
29
+ # DOES trap the wrapper passes 0 and not one instruction is emitted (the
30
+ # CUDA path is unchanged down to the generated PTX bar one unused kernel
31
+ # parameter).
32
+ #
33
+ # Why a device-side word rather than checking the indices on the host:
34
+ # measured 2026-09-10 on this machine, a host readback of the index tensor
35
+ # costs +0.24 ms (1 id) to +0.29 ms (512 ids) per call against an embedding
36
+ # launch of 0.017–0.020 ms — 13x to 17x the op it guards, and flat in the
37
+ # number of indices because the cost is the `flush()` a D2H copy performs,
38
+ # not the data. That is the same trap the D2D measurement of 2026-09-07
39
+ # found (1,086 host-ordered copies in an 8-token decode: 0.007 s -> 4.08 s).
40
+ # The status word costs one conditional store on the failing path and a
41
+ # 4-byte read at a sync that was already happening.
42
+ #
43
+ # It is READ where the host observes device results — after `sync_device()`
44
+ # and after a copy to the host — which is the sticky-error contract
45
+ # `sync_device` already documents: the fault is at or before that point.
46
+ _FAULT_MESSAGES: list = []
47
+ _FAULT_BUFFERS: dict = {}
48
+ _FAULT_LOCK = threading.Lock()
49
+
50
+
51
+ def register_device_fault(message: str) -> int:
52
+ """Reserve a fault code for `message`. Called at import by the kernel
53
+ that raises it, so codes are stable within a process and never zero
54
+ (zero is 'clean', and it is what a disarmed kernel is compiled with)."""
55
+ with _FAULT_LOCK:
56
+ if message not in _FAULT_MESSAGES:
57
+ _FAULT_MESSAGES.append(message)
58
+ return _FAULT_MESSAGES.index(message) + 1
59
+
60
+
61
+ def device_fault_code(message: str, *, armed: bool = None) -> int:
62
+ """The code a kernel should be compiled with: its own where the backend
63
+ needs the channel, 0 where the backend traps by itself."""
64
+ if armed is None:
65
+ armed = not _caps.backend_traps_on_device_assert()
66
+ return register_device_fault(message) if armed else 0
67
+
68
+
69
+ @functools.lru_cache(maxsize=None)
70
+ def device_fault_code_cached(message: str) -> int:
71
+ """`device_fault_code` memoised per message — it runs at every launch of
72
+ an armed kernel, and the answer cannot change within a process."""
73
+ return device_fault_code(message)
74
+
75
+
76
+ def fault_channel(message: str, spare: 'NBXTensor') -> tuple:
77
+ """`(pointer_argument, FAULT_CODE)` for a kernel that carries the channel.
78
+
79
+ Where the backend HONOURS `tl.device_assert`, the code is 0 and the
80
+ generated kernel contains no fault store at all — proven on the emitted
81
+ code, not assumed: with FAULT_CODE=0 the body carries neither the store nor
82
+ the reduction that feeds it. The pointer is then a bound-but-unread
83
+ argument, so `spare` — a tensor the call already owns — is handed over and
84
+ NOTHING IS ALLOCATED.
85
+
86
+ That matters because the fault buffer is never freed by contract (a frozen
87
+ replay plan records its raw pointer), so allocating it on a backend that
88
+ can never write to it is a permanent allocation for a dead path. Reported
89
+ from the other machine 2026-09-11, whose CUDA proof also says the guard
90
+ itself holds: three kernels refuse an out-of-range index by name, six
91
+ outputs identical, +0.13% wall.
92
+
93
+ `spare` is never dereferenced through this argument on that path; passing
94
+ the output tensor is therefore safe and costs nothing.
95
+ """
96
+ code = device_fault_code_cached(message)
97
+ if code == 0:
98
+ return spare, 0
99
+ return device_fault_buffer(spare._device_idx), code
100
+
101
+
102
+ def device_fault_buffer(device_idx: int) -> 'NBXTensor':
103
+ """This device's zeroed one-word status buffer.
104
+
105
+ Handed to the kernel as a TENSOR, not as a raw address: the launcher
106
+ binds a tensor as a pointer argument, and a bare int arrives as an
107
+ int64 scalar that `tl.store` refuses.
108
+
109
+ Never freed: a kernel launch records this raw pointer, and a frozen
110
+ replay plan may hold it long after the call that produced it — the same
111
+ reason `_INT64_ARRAY_CACHE` is never evicted.
112
+ """
113
+ buf = _FAULT_BUFFERS.get(device_idx) # hot path: no lock
114
+ if buf is None:
115
+ with _FAULT_LOCK:
116
+ buf = _FAULT_BUFFERS.get(device_idx)
117
+ if buf is None:
118
+ from nbxtensor.dtype.nbx_dtype import NBXDtype # the tensor module imports this one
119
+ from nbxtensor.tensor import NBXTensor
120
+ buf = NBXTensor.zeros((1,), NBXDtype.int32, f"cuda:{device_idx}")
121
+ _FAULT_BUFFERS[device_idx] = buf
122
+ return buf
123
+
124
+
125
+ def check_device_faults() -> None:
126
+ """Raise if any kernel reported a broken contract since the last check.
127
+
128
+ Reads the status word straight at its own address: this runs only after
129
+ a flush (a completed sync, or a copy that flushed), and only on a
130
+ backend whose device memory the host can read directly. The word is
131
+ cleared as it is read, so one violation raises once.
132
+ """
133
+ if not _FAULT_BUFFERS:
134
+ return
135
+ if not _caps._backend_capability(
136
+ _BACKEND_MEMORY_IS_HOST_READABLE, "_BACKEND_MEMORY_IS_HOST_READABLE",
137
+ "whether the host can read its device memory directly"):
138
+ return
139
+ for device_idx, buf in list(_FAULT_BUFFERS.items()):
140
+ word = ctypes.c_int32.from_address(buf.data_ptr())
141
+ code = word.value
142
+ if code:
143
+ word.value = 0
144
+ message = (_FAULT_MESSAGES[code - 1]
145
+ if 1 <= code <= len(_FAULT_MESSAGES)
146
+ else f"unregistered fault code {code}")
147
+ raise RuntimeError(
148
+ f"device-side contract violated on device {device_idx}: "
149
+ f"{message}. The kernel reported it through the fault "
150
+ f"channel because this backend does not honour "
151
+ f"`tl.device_assert`; the fault is at or before this point.")
@@ -0,0 +1,185 @@
1
+ """What the machine actually has free, right now.
2
+
3
+ A budget that describes what the hardware *recommends* is not a measure of
4
+ what is *available*. On unified memory the two are unrelated: this M4 Pro
5
+ reports `recommendedMaxWorkingSetSize` = 18 186 MB and does not lower it by
6
+ one byte when another process holds a third of the machine. A user never has
7
+ an empty machine — a browser, a mail client, sometimes a container or a VM —
8
+ so a plan sized against the recommendation is accepted and then killed by the
9
+ system under memory pressure, mid-render, without a word.
10
+
11
+ This module measures the other number. It has two consumers and they want
12
+ the same reading:
13
+
14
+ * the planner, which must refuse loudly and name BOTH figures rather than
15
+ start and be killed;
16
+ * the measurement protocol, because a green obtained on a half-occupied
17
+ machine is not the same green as one obtained on a free machine, and
18
+ without this line written at the time nobody can tell them apart later.
19
+
20
+ Nothing is guessed. Where the platform is not one this module knows how to
21
+ read, `available_mb` is None and `source` says so — the caller then announces
22
+ that it could not be measured, which is honest, rather than substituting a
23
+ number, which is not.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ import subprocess
29
+ import sys
30
+ from dataclasses import dataclass, field
31
+ from typing import Optional, Tuple
32
+
33
+ _MB = 1024 * 1024
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class MemoryState:
38
+ """One reading of the machine, at one instant."""
39
+
40
+ total_mb: Optional[int] = None
41
+ available_mb: Optional[int] = None
42
+ swap_used_mb: Optional[int] = None
43
+ swap_total_mb: Optional[int] = None
44
+ #: (command, resident MB), largest first — this is what names the VM or
45
+ #: the browser holding the memory, without hardcoding either.
46
+ largest_residents: Tuple[Tuple[str, int], ...] = field(default=())
47
+ #: How it was read, or why it could not be.
48
+ source: str = "not measured"
49
+
50
+ @property
51
+ def measured(self) -> bool:
52
+ return self.available_mb is not None
53
+
54
+ def describe(self) -> str:
55
+ """The one line every measurement cell writes beside its result."""
56
+ if not self.measured:
57
+ return f"machine memory: NOT MEASURED ({self.source})"
58
+ parts = [f"{self.available_mb} MB free"]
59
+ if self.total_mb:
60
+ parts.append(f"of {self.total_mb} MB")
61
+ if self.swap_total_mb:
62
+ parts.append(f"swap {self.swap_used_mb}/{self.swap_total_mb} MB")
63
+ if self.largest_residents:
64
+ biggest = ", ".join(f"{name} {mb} MB"
65
+ for name, mb in self.largest_residents)
66
+ parts.append(f"largest resident: {biggest}")
67
+ return "machine memory: " + ", ".join(parts)
68
+
69
+
70
+ def _run(cmd) -> Optional[str]:
71
+ try:
72
+ out = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
73
+ except (OSError, subprocess.SubprocessError):
74
+ return None
75
+ return out.stdout if out.returncode == 0 else None
76
+
77
+
78
+ def _largest_residents(count: int = 3) -> Tuple[Tuple[str, int], ...]:
79
+ """The biggest resident processes, by RSS. Named, never acted on."""
80
+ out = _run(["ps", "-Ao", "rss,comm", "-r"])
81
+ if not out:
82
+ return ()
83
+ found = []
84
+ for line in out.splitlines()[1:]:
85
+ line = line.strip()
86
+ if not line:
87
+ continue
88
+ rss, _, comm = line.partition(" ")
89
+ if not rss.isdigit():
90
+ continue
91
+ mb = int(rss) // 1024
92
+ if mb <= 0:
93
+ continue
94
+ found.append((comm.strip().rsplit("/", 1)[-1], mb))
95
+ if len(found) >= count:
96
+ break
97
+ return tuple(found)
98
+
99
+
100
+ def _available_mb_from_vm_stat(vm: str) -> int:
101
+ """The available figure from a `vm_stat` text (the reader's one parsing, testable on a captured text)."""
102
+ page = re.search(r"page size of (\d+) bytes", vm)
103
+ page_size = int(page.group(1)) if page else 4096
104
+
105
+ def pages(label: str) -> int:
106
+ m = re.search(rf"{label}:\s+(\d+)", vm)
107
+ return int(m.group(1)) if m else 0
108
+
109
+ # The reclaimable set, what a new allocation gets back WITHOUT the compressor: free pages, purgeable
110
+ # pages, and the whole file cache — `File-backed pages`, active or inactive (Activity Monitor's
111
+ # "Cached Files"); the speculative queue is already inside it (measured on this M4 Pro:
112
+ # active + inactive + speculative == file-backed + anonymous). Anonymous pages are NOT counted,
113
+ # active or inactive: they come back only through the compressor and swap. The previous formula
114
+ # (free + inactive + purgeable + speculative) subtracted every ACTIVE file page — 4.4 GB of the
115
+ # container just copied when orpheus-3b was refused at placement (2026-09-29, inbox 68).
116
+ available_pages = (pages("Pages free") + pages("Pages purgeable") + pages("File-backed pages"))
117
+ return available_pages * page_size // _MB
118
+
119
+
120
+ def _macos_state() -> MemoryState:
121
+ """`vm_stat` for pages, `sysctl` for the totals and for swap.
122
+
123
+ Available is free + purgeable + file-backed pages: the file cache is
124
+ reclaimable on demand whichever queue holds it, and is never subtracted;
125
+ anonymous, wired and compressed pages are not reclaimable without the
126
+ compressor and are deliberately excluded (`_available_mb_from_vm_stat`).
127
+ """
128
+ vm = _run(["vm_stat"])
129
+ if not vm:
130
+ return MemoryState(source="vm_stat unavailable")
131
+ available_mb = _available_mb_from_vm_stat(vm)
132
+
133
+ total_mb = None
134
+ memsize = _run(["sysctl", "-n", "hw.memsize"])
135
+ if memsize and memsize.strip().isdigit():
136
+ total_mb = int(memsize.strip()) // _MB
137
+
138
+ swap_used = swap_total = None
139
+ swap = _run(["sysctl", "-n", "vm.swapusage"])
140
+ if swap:
141
+ tot = re.search(r"total\s*=\s*([\d.]+)M", swap)
142
+ use = re.search(r"used\s*=\s*([\d.]+)M", swap)
143
+ if tot:
144
+ swap_total = int(float(tot.group(1)))
145
+ if use:
146
+ swap_used = int(float(use.group(1)))
147
+
148
+ return MemoryState(total_mb=total_mb, available_mb=available_mb,
149
+ swap_used_mb=swap_used, swap_total_mb=swap_total,
150
+ largest_residents=_largest_residents(),
151
+ source="vm_stat + sysctl")
152
+
153
+
154
+ def _linux_state() -> MemoryState:
155
+ """`MemAvailable` is the kernel's own estimate of what a new allocation
156
+ can have without swapping — exactly the question, already answered."""
157
+ try:
158
+ text = open("/proc/meminfo").read()
159
+ except OSError:
160
+ return MemoryState(source="/proc/meminfo unreadable")
161
+
162
+ def kb(label: str) -> Optional[int]:
163
+ m = re.search(rf"^{label}:\s+(\d+) kB", text, re.MULTILINE)
164
+ return int(m.group(1)) if m else None
165
+
166
+ total, avail = kb("MemTotal"), kb("MemAvailable")
167
+ swap_total, swap_free = kb("SwapTotal"), kb("SwapFree")
168
+ return MemoryState(
169
+ total_mb=total // 1024 if total else None,
170
+ available_mb=avail // 1024 if avail is not None else None,
171
+ swap_total_mb=swap_total // 1024 if swap_total else None,
172
+ swap_used_mb=((swap_total - swap_free) // 1024
173
+ if swap_total is not None and swap_free is not None
174
+ else None),
175
+ largest_residents=_largest_residents(),
176
+ source="/proc/meminfo")
177
+
178
+
179
+ def memory_state() -> MemoryState:
180
+ """Read the machine now. Never cached: the whole point is that it moves."""
181
+ if sys.platform == "darwin":
182
+ return _macos_state()
183
+ if sys.platform.startswith("linux"):
184
+ return _linux_state()
185
+ return MemoryState(source=f"no reader for platform {sys.platform!r}")