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,1988 @@
1
+ """The device allocator: allocation, the pool and its parked blocks, the free queues, peer copies and the allocator's
2
+ own accounting (live bytes, peaks), with the malloc call-site trace (NBX_MALLOC_TRACE). Cut out of tensor.py (M2, 2026-10-29),
3
+ unchanged."""
4
+
5
+ from __future__ import annotations
6
+ from nbxtensor.backend import faults as _faults
7
+
8
+ import bisect
9
+ import ctypes
10
+ import os
11
+ import time
12
+ import sys
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+
15
+ from nbxtensor.backend import runtime as _rt
16
+ from nbxtensor.memory.pool import (SegmentPool, LARGE, range_add, range_extend, range_unmapped, range_reclaimable)
17
+
18
+
19
+
20
+ # ============================================================================
21
+ # MALLOC CALL-SITE LOGGER — env-gated, zero cost when disabled
22
+ # ============================================================================
23
+ #
24
+ # When NBX_MALLOC_TRACE=<path> is set, every DeviceAllocator.malloc_cuda
25
+ # records one row and every free_cuda records the matching "F" row. The
26
+ # buffer is flushed to <path> at interpreter exit via atexit. When unset,
27
+ # the allocator pays a single module-level `is None` check per call — no
28
+ # frame walk, no list append.
29
+ #
30
+ # How to use:
31
+ # NBX_MALLOC_TRACE=/tmp/run.tsv neurobrix run --model ... --triton
32
+ #
33
+ # Row format (tab-separated):
34
+ # event_id \t M|F \t ptr \t nbytes \t <file:line func> (an F row carries the
35
+ # engine frames that freed it, innermost first)
36
+ #
37
+ # Recipes:
38
+ # - Total allocation volume by site:
39
+ # awk '$2=="M"{by[$5]+=$4} END{for(k in by)print by[k],k}' run.tsv \
40
+ # | sort -rn | head
41
+ # - Live-set composition at peak: walk events, track ptr→nbytes live
42
+ # map, record the set at each new peak, then aggregate by site.
43
+ # This is the discriminator between "leak" (unfreed allocs), "peak
44
+ # concurrency" (tensors retained by a deferred-free mechanism), and
45
+ # "fragmentation" (total freed > 80% but driver rejects next alloc).
46
+ # In April 2026 this tool killed a wrong hypothesis in ~30 min on
47
+ # PixArt triton (confirmed _deferred accumulation as root cause,
48
+ # not workspace leak as initially hypothesised). Kept permanent —
49
+ # same discriminator work will be needed for Qwen3 long-context,
50
+ # Sana 4Kpx, video upscalers.
51
+ # ============================================================================
52
+
53
+ _MALLOC_TRACE_FILE: Optional[str] = os.environ.get("NBX_MALLOC_TRACE") or None
54
+ _MALLOC_TRACE_EVENTS: List[Tuple[int, str, int, int, str]] = [] # (eid, kind, ptr, nbytes, site)
55
+ _MALLOC_TRACE_COUNTER: List[int] = [0]
56
+
57
+
58
+ def _extract_neurobrix_site(max_frames: int = 12) -> str:
59
+ """Return nearest neurobrix/ frame as 'file:line func', skipping this file.
60
+
61
+ The logger only fires inside DeviceAllocator — we want the CALLER of the
62
+ allocator (a wrapper, the dispatch, the engine's sequence, ...) not the allocator
63
+ itself. Walk the stack until the first frame of the library or the engine
64
+ (`_is_caller_frame`) that is not the allocator's or the tensor's own.
65
+ """
66
+ f = sys._getframe(2) # skip this fn + malloc_cuda/free_cuda
67
+ n = 0
68
+ while f is not None and n < max_frames:
69
+ fname = f.f_code.co_filename
70
+ if _is_caller_frame(fname):
71
+ return f"{_rel_site(fname)}:{f.f_lineno} {f.f_code.co_name}"
72
+ f = f.f_back
73
+ n += 1
74
+ return "<unknown>"
75
+
76
+
77
+ #: the allocator's and the tensor's own files: never the site a trace names
78
+ _NOT_A_CALLER = ("/nbxtensor/memory/allocator.py", "/nbxtensor/memory/memory_pool.py", "/nbxtensor/memory/slab.py",
79
+ "/nbxtensor/tensor.py")
80
+
81
+
82
+ def _is_caller_frame(fname: str) -> bool:
83
+ """A frame of the library or of the engine that calls it (the engine's tree under `neurobrix/`). The trace keyed on
84
+ 'neurobrix' alone until the library moved out (M1, 2026-10-28): every wrapper's frame was then skipped and a site
85
+ named only the engine loop that dispatched it."""
86
+ return ("/nbxtensor/" in fname or "/neurobrix/" in fname) and not fname.endswith(_NOT_A_CALLER)
87
+
88
+
89
+ def _rel_site(fname: str) -> str:
90
+ """`nbxtensor/ops/conv_wrappers.py` or `neurobrix/triton/sequence.py`: the path from the package."""
91
+ for pkg in ("/neurobrix/", "/nbxtensor/"): # the engine first: a tree may be named nbxtensor
92
+ if pkg in fname:
93
+ return pkg.strip("/") + "/" + fname.rsplit(pkg, 1)[1]
94
+ return fname
95
+
96
+
97
+ def _record_malloc_site(ptr: int, nbytes: int, dev: int, pooled: bool = True) -> None:
98
+ """One allocation of the trace: kind "M" for a pooled block, "A" for one outside the pool (a weight arena, handed back
99
+ to the driver whole) — the two the pool's replay (`memory.pool.simulate`) treats apart."""
100
+ eid = _MALLOC_TRACE_COUNTER[0]
101
+ _MALLOC_TRACE_COUNTER[0] = eid + 1
102
+ site = _extract_neurobrix_site()
103
+ _MALLOC_TRACE_EVENTS.append((eid, "M" if pooled else "A", ptr, nbytes, site))
104
+
105
+
106
+ def _extract_neurobrix_chain(max_frames: int = 40, keep: int = 8) -> str:
107
+ """The engine frames of the caller, innermost first, ' < '-joined — for
108
+ a FREE row. A block freed by a finalizer names `free()` as its site,
109
+ which says nothing; the frames above it name what dropped the last
110
+ reference (Ming's arenas freed under live weights, 2026-09-16)."""
111
+ f = sys._getframe(2)
112
+ n = 0
113
+ out = []
114
+ while f is not None and n < max_frames and len(out) < keep:
115
+ fname = f.f_code.co_filename
116
+ if _is_caller_frame(fname):
117
+ out.append(f"{_rel_site(fname)}:{f.f_lineno} {f.f_code.co_name}")
118
+ f = f.f_back
119
+ n += 1
120
+ return " < ".join(out) or "<unknown>"
121
+
122
+
123
+ def _record_free_site(ptr: int, nbytes: int) -> None:
124
+ eid = _MALLOC_TRACE_COUNTER[0]
125
+ _MALLOC_TRACE_COUNTER[0] = eid + 1
126
+ _MALLOC_TRACE_EVENTS.append((eid, "F", ptr, nbytes, _extract_neurobrix_chain()))
127
+
128
+
129
+ def _flush_malloc_trace() -> None:
130
+ path = _MALLOC_TRACE_FILE
131
+ if path is None or not _MALLOC_TRACE_EVENTS:
132
+ return
133
+ try:
134
+ with open(path, "w") as fh:
135
+ for eid, kind, ptr, nbytes, site in _MALLOC_TRACE_EVENTS:
136
+ fh.write(f"{eid}\t{kind}\t{ptr}\t{nbytes}\t{site}\n")
137
+ except Exception:
138
+ pass
139
+
140
+
141
+ import atexit as _atexit
142
+ _atexit.register(_flush_malloc_trace)
143
+
144
+ # The floor watchdog stops a run with SIGTERM (5 s grace before SIGKILL),
145
+ # and Python's default SIGTERM action skips atexit — so a floor-stopped
146
+ # traced run died with an EMPTY trace (measured: x8@1024, 2026-09-21).
147
+ # When, and only when, the trace is armed, convert SIGTERM to SystemExit
148
+ # so the flush runs inside the grace window. Unarmed runs keep the
149
+ # default signal behavior untouched.
150
+ if _MALLOC_TRACE_FILE:
151
+ import signal as _signal_mt
152
+
153
+ def _flush_on_term(_signum, _frame):
154
+ raise SystemExit(143)
155
+
156
+ try:
157
+ _signal_mt.signal(_signal_mt.SIGTERM, _flush_on_term)
158
+ except (ValueError, OSError):
159
+ pass # non-main thread or exotic host: trace
160
+
161
+
162
+
163
+ class ImpossibleExtentError(RuntimeError):
164
+ """An op would produce a tensor with an extent that cannot exist (<= 0 where the op defines
165
+ a length, or a rank the op does not produce). Raised at the op, with its input shapes and its
166
+ arithmetic, instead of letting an impossible tensor travel on and fail far away. The graph
167
+ executors add the op uid and the bound symbol values."""
168
+
169
+
170
+ class DeviceOOMError(RuntimeError):
171
+ """The device allocator could not honor a request: the driver refused
172
+ the allocation after the deferred-free drain and the single retry.
173
+ Typed so a caller that names its own key in the refusal (the chunked
174
+ SDPA prefill re-raises it with op, shape, rows and bytes asked) catches
175
+ exactly this and nothing else — every other RuntimeError keeps propagating.
176
+
177
+ It also CARRIES the figures it prints, as attributes. The refusal already
178
+ computes requested / live / pool-cached / driver-free in order to write the
179
+ message; a caller that wants to reshape the work rather than give up then had
180
+ to parse them back out of English. They are the input a controller needs to
181
+ re-enter the placement cascade at the op-level tiling rung with the real free
182
+ figure at the moment of failure instead of the estimate made before the run
183
+ (`docs/reference/adaptive-memory-a-runtime-controller.md`, addition 3).
184
+
185
+ Every field is bytes, and `None` where the runtime could not answer — never 0,
186
+ which is a legitimate reading and would be acted on as one.
187
+ """
188
+
189
+ def __init__(self, message: str, *, requested: Optional[int] = None,
190
+ device_idx: Optional[int] = None, live: Optional[int] = None,
191
+ pool_cached: Optional[int] = None, pool_blocks: Optional[int] = None,
192
+ driver_free: Optional[int] = None, driver_total: Optional[int] = None):
193
+ super().__init__(message)
194
+ self.requested = requested
195
+ self.device_idx = device_idx
196
+ self.live = live
197
+ self.pool_cached = pool_cached
198
+ self.pool_blocks = pool_blocks
199
+ self.driver_free = driver_free
200
+ self.driver_total = driver_total
201
+
202
+ @property
203
+ def shortfall(self) -> Optional[int]:
204
+ """How many bytes the request was short by, or None if the driver did
205
+ not report its free figure. This is the number a reshape has to close."""
206
+ if self.requested is None or self.driver_free is None:
207
+ return None
208
+ return max(0, self.requested - self.driver_free)
209
+
210
+
211
+ class AllocatorInterceptor:
212
+ """What stands in front of the allocator's five device operations (allocate, free, copy, set, synchronise) while
213
+ it is installed (`DeviceAllocator.install_interceptor`): each method receives the DIRECT operation it stands in
214
+ front of (`DeviceAllocator.malloc_direct`, ...) and decides whether and how to call it. A replay recording carves
215
+ its run's allocations from a slab and records the copies it must reproduce (`memory.slab.SlabInterceptor`).
216
+
217
+ The defaults pass straight through; a subclass overrides only what it changes. One interceptor at a time: two
218
+ would each believe it sees every allocation."""
219
+
220
+ def malloc(self, direct, nbytes: int, dev_idx: Optional[int], pooled: bool) -> int:
221
+ return direct(nbytes, dev_idx, pooled)
222
+
223
+ def free(self, direct, ptr: int):
224
+ return direct(ptr)
225
+
226
+ def memcpy(self, direct, dst: int, src: int, nbytes: int, kind: int):
227
+ return direct(dst, src, nbytes, kind)
228
+
229
+ def memset(self, direct, ptr: int, value: int, nbytes: int):
230
+ return direct(ptr, value, nbytes)
231
+
232
+ def sync(self, direct):
233
+ return direct()
234
+
235
+
236
+ class DeviceAllocator:
237
+ """GPU + pinned-host memory allocator via raw runtime API.
238
+
239
+ Tracks running byte counters per device and for pinned host memory
240
+ so diagnostics (especially for zero3 CPU offload) don't need to
241
+ round-trip through cudaMemGetInfo or torch.cuda.memory_allocated —
242
+ both of which are unavailable or incorrect for NBXTensor.
243
+ """
244
+
245
+ # Running counters — populated by every malloc_cuda/free_cuda and
246
+ # malloc_host_pinned/free_host_pinned. Class-level so the accounting
247
+ # is process-wide. Not thread-safe; NeuroBrix runtime is
248
+ # single-threaded per component by convention.
249
+ _cuda_live_bytes: Dict[int, int] = {} # device_idx -> live
250
+ _cuda_peak_bytes: Dict[int, int] = {} # device_idx -> peak
251
+ _cuda_ptr_size: Dict[int, int] = {} # ptr -> nbytes (for free accounting)
252
+ _range_bases: list = [] # live allocation bases, sorted (holds())
253
+ _range_size: dict = {} # base -> bytes, mirrors the two size tables
254
+ _cuda_ptr_device: Dict[int, int] = {} # ptr -> device_idx (REQUIRED for cudaFreeAsync correctness)
255
+
256
+ # Which generation of the device runtime issued a pointer.
257
+ #
258
+ # A pointer is only meaningful to the runtime that issued it. Normally
259
+ # there is exactly one for the life of the process and this is a constant
260
+ # 0 that never costs a thought. When a runtime IS replaced, every address
261
+ # it issued dies with it — and the free-list must not outlive it, or a
262
+ # later allocation is served an address the current runtime never issued
263
+ # and the first launch that touches it refuses a perfectly good tensor.
264
+ #
265
+ # Flushing the pool at the swap is not enough: a tensor still ALIVE at
266
+ # that moment is freed afterwards, and that free parks its dead address
267
+ # for reuse. The epoch is what the flush cannot see.
268
+ _alloc_epoch: int = 0
269
+ _cuda_ptr_epoch: Dict[int, int] = {} # ptr -> the epoch that issued it
270
+ _host_pinned_live_bytes: int = 0
271
+ _host_pinned_peak_bytes: int = 0
272
+ _host_pinned_ptr_size: Dict[int, int] = {} # ptr -> nbytes
273
+
274
+ # Phase 2 — caching free-list pool. Per-device map of nbytes → list of
275
+ # cached free pointers. On free, the pointer is returned to the pool
276
+ # instead of cudaFree (which would re-fragment the driver heap). On
277
+ # malloc, the pool is checked first (exact size match, then smallest-
278
+ # fit ≥ requested). On cudaMalloc OOM, the entire pool is flushed back
279
+ # to the driver via cudaFree and the malloc is retried — this gives
280
+ # the driver a chance to coalesce its internal heap before declaring
281
+ # failure (analog to torch CachingAllocator's release-cached-blocks
282
+ # path on OOM). Default ON since 2026-09-02 (P-PREFILL-TRITON lever 1:
283
+ # the xlong triton prefill paid one cudaMalloc + one cudaFree per op
284
+ # output, 82 s of host API time on a 172 s run; the pool cut the cold
285
+ # run by 41 % with byte-identical output, gated by the full-zoo
286
+ # battery + the warm-serve sweep). `NBX_ALLOC_POOL=0` disables it
287
+ # (the differential for any allocator-suspected divergence).
288
+ # P-SANA-4KPX-RUNTIME Phase 2 origin.
289
+ # The pool SPLITS and COALESCES (S4a, 2026-10-27; the method of torch's caching allocator): every driver allocation
290
+ # is a SEGMENT made of BLOCKS chained in address order, in one of two SIZE CLASSES (small requests carve small
291
+ # segments, large ones never share a segment with them); a request takes the smallest free block of its class that
292
+ # holds it and splits off the rest when it is at least `memory.pool.POOL_SPLIT_MIN` — a large block only when the
293
+ # rest is at most the request (the half rule, 2026-11-01); a freed block merges with its free neighbours; only a
294
+ # segment wholly free returns to the driver (over the parked cap, on a refused malloc, on a flush). Exact-size
295
+ # parking kept a freed 3 GB block from ever serving a 2 GB request: 2.6-3.7 GB parked beside 6-7 GB live on
296
+ # mochi-1-preview triton at the 8 GB rung.
297
+ # The segments, their blocks and the free index live in ONE `memory.pool.SegmentPool` (`_POOL`, set below the
298
+ # class): the same object Prism's pricing replays (`memory.pool.simulate`).
299
+ _pool_enabled: bool = False # set by _maybe_init_pool() from env
300
+ #: Callables `(dev) -> bytes released` the engine registers for memory it caches outside the pool (a recorded
301
+ #: replay plan's slab, `triton/replay.release_idle_slabs`), called on a malloc refused after the pool's flush.
302
+ _idle_reclaimers: list = []
303
+ #: The `AllocatorInterceptor` in force, or None: the five public device operations go through it.
304
+ _interceptor: Optional[AllocatorInterceptor] = None
305
+ # Pool observability (NBX_ALLOC_STATS=1 prints it at exit): bytes
306
+ # parked in the free-list per device (+ peak), hit/miss counters,
307
+ # and the smallest-fit SLACK (allocated − requested) — the pool's
308
+ # own contribution to the driver-level watermark.
309
+ # (`_POOL.cached`, `_POOL.cached_peak`, `_POOL.stats`)
310
+ # Parked-bytes cap per device (bytes; 0 = unbounded), resolved once per
311
+ # device from the arch yml `memory.alloc_pool_parked_cap_fraction` ×
312
+ # the device's total memory (ONE cudaMemGetInfo at the first free on
313
+ # that device, never in the hot path afterwards).
314
+ _pool_cap_bytes: Dict[int, int] = {}
315
+ # A limit imposed on the process (`set_limit`; an application's rung, a census) is the card the plan was sized
316
+ # for, and the allocator holds to it as to a card of that size: a malloc that would carry the bytes it holds from
317
+ # the driver (live and pooled) past the rung less the runtime's own context is refused as such a card refuses it,
318
+ # and takes the same path (the pool flushed, the malloc retried, then a named OOM). device -> bytes, None: no rung.
319
+ # Measured 2026-10-28: mochi-1-preview triton at the 8 GB rung on a 32 GB card held 10.8 GB — 5.5-6.2 GB live and
320
+ # 3.0-3.7 GB of pooled free blocks no refused malloc ever asked back.
321
+ _rung_limit_bytes: Dict[int, Optional[int]] = {}
322
+ # The memory limit an application imposes (`set_limit`): device -> the bytes of the card this process holds to, the
323
+ # key None for every device; absent, the physical card. Generic: the library knows no rung, no plan, no variable.
324
+ _limit_bytes: Dict[Optional[int], Optional[int]] = {}
325
+ #: device -> {token: the most held from the driver since the window opened} (`open_window`)
326
+ _windows: Dict[int, Dict[int, int]] = {}
327
+ _window_starts: Dict[int, int] = {} # token -> held when it opened
328
+ _window_seq: int = 0
329
+
330
+ # Deferred-queue reclaimers (P-TRITON-LIVE-SET). The triton hot loops
331
+ # register their deferred dead-tensor queues here. Since 2026-10-11 the
332
+ # allocator's OWN malloc never calls them: Prism prices the queue
333
+ # (`PrismSolver._deferred_drain_mb`), so a refused malloc is raised, not
334
+ # rescued. The one reader left is `wrappers._autotune_headroom_guard`,
335
+ # for the single device allocation the engine does not make and the plan
336
+ # does not price — the Triton autotuner's benchmark buffer. A hook takes
337
+ # (device_idx, nbytes requested) and returns the bytes it freed.
338
+ _oom_reclaim_hooks: list = []
339
+
340
+ @classmethod
341
+ def register_oom_reclaim_hook(cls, fn) -> None:
342
+ if fn not in cls._oom_reclaim_hooks:
343
+ cls._oom_reclaim_hooks.append(fn)
344
+
345
+ @classmethod
346
+ def unregister_oom_reclaim_hook(cls, fn) -> None:
347
+ try:
348
+ cls._oom_reclaim_hooks.remove(fn)
349
+ except ValueError:
350
+ pass
351
+
352
+ @staticmethod
353
+ def set_device(device_id: int):
354
+ """Set current GPU device for allocations (runtime API only).
355
+
356
+ Uses only cudaSetDevice/hipSetDevice — does NOT touch the driver
357
+ context. The driver context is managed solely by ensure_triton_device
358
+ which is called just before Triton kernel launches.
359
+ """
360
+ ret = _rt.device_memory().set_device(device_id)
361
+ if ret != 0:
362
+ # An invalid ordinal (a device the process cannot see) must be
363
+ # loud here, not reported by the next unrelated launch check in
364
+ # the process as its own (2026-09-05: torch's, in the compiled
365
+ # branch — "invalid device ordinal" on an fp16 op).
366
+ DeviceAllocator.clear_last_error()
367
+ raise RuntimeError(f"set_device({device_id}) refused by the runtime (error {ret}) — "
368
+ f"the process sees {DeviceAllocator.device_count()} device(s)")
369
+ # C2 cache maintenance: a runtime-only switch to a device other
370
+ # than the cached one breaks the "runtime AND Triton both point
371
+ # at the cached idx" invariant — invalidate so the next
372
+ # ensure_triton_device performs the full sync. Switching to the
373
+ # cached device itself keeps the pair coherent (Triton's driver
374
+ # device is untouched here).
375
+ if device_id != _rt._cached_device_idx():
376
+ _rt._DEVICE_CACHE.idx = None
377
+
378
+ @staticmethod
379
+ def get_device() -> int:
380
+ """Get current GPU device."""
381
+ return _rt.device_memory().get_device()[1]
382
+
383
+ @staticmethod
384
+ def peak_device_bytes():
385
+ """The most one device held at once since the process started, as this allocator counted it from the driver
386
+ (live driver bytes, the pool's cache included); None where the backend keeps no such count."""
387
+ return max(DeviceAllocator._cuda_peak_bytes.values()) if DeviceAllocator._cuda_peak_bytes else None
388
+
389
+ @staticmethod
390
+ def device_count() -> int:
391
+ """How many GPUs this process can see. 0 if there is no runtime.
392
+
393
+ The backend table has declared `device_count` since the seam was
394
+ written, but nothing exposed it: `most_free_device` counted inline and
395
+ every other caller reached for torch. Callers that need to know
396
+ whether multi-GPU behaviour is even testable need it too.
397
+ """
398
+ try:
399
+ return max(0, _rt.device_memory().device_count()[1])
400
+ except Exception:
401
+ return 0
402
+
403
+ @staticmethod
404
+ def free_memory_mb(index: int) -> "Optional[float]":
405
+ """The driver's FREE memory on device `index`, in MB, or None when the
406
+ runtime cannot answer.
407
+
408
+ The planner's door to the machine as it IS. `DeviceState.free_mb` was
409
+ `capacity - used_mb` with `used_mb` the plan's own accounting — on a
410
+ discrete card nothing ever read what another process holds, so a plan
411
+ that fitted the card whole was accepted while a neighbour held 18 GB
412
+ of it, and died at its first allocation (five of five runs, V100-32GB,
413
+ 2026-09-20). Same query as `most_free_device`, for ONE card, the
414
+ previously-current device restored: cudaMemGetInfo / hipMemGetInfo via
415
+ ctypes, no torch (R33).
416
+ """
417
+ try:
418
+ if int(index) < 0 or int(index) >= DeviceAllocator.device_count():
419
+ return None # cudaSetDevice REPORTS a bad ordinal; it does not raise
420
+ dm = _rt.device_memory()
421
+ prev = DeviceAllocator.get_device()
422
+ dm.set_device(int(index))
423
+ _rc, free_b, _total_b = dm.mem_get_info()
424
+ try:
425
+ dm.set_device(prev)
426
+ except Exception:
427
+ pass
428
+ return free_b / (1024 * 1024)
429
+ except Exception:
430
+ return None
431
+
432
+ @staticmethod
433
+ def most_free_device() -> int:
434
+ """Return the GPU index with the most driver-free VRAM.
435
+
436
+ Used by the triton weight loader to place a CPU-staged lazy
437
+ component on the emptiest card (the loop GPU may have just
438
+ unloaded). Pure runtime-API — cudaGetDeviceCount + cudaSetDevice +
439
+ cudaMemGetInfo via ctypes; no torch (R33). The previously-current
440
+ device is restored before returning. Falls back to device 0 on any
441
+ query failure or a single-GPU host.
442
+ """
443
+ dm = _rt.device_memory()
444
+ ndev = DeviceAllocator.device_count()
445
+ if ndev <= 1:
446
+ return 0
447
+ prev = DeviceAllocator.get_device()
448
+ best_idx, best_free = 0, -1
449
+ for i in range(ndev):
450
+ try:
451
+ dm.set_device(i)
452
+ _rc, free_b, _total_b = dm.mem_get_info()
453
+ if free_b > best_free:
454
+ best_free, best_idx = free_b, i
455
+ except Exception:
456
+ continue
457
+ try:
458
+ dm.set_device(prev)
459
+ except Exception:
460
+ pass
461
+ # C2: the probe loop above issued raw cudaSetDevice calls (and
462
+ # the restore may have failed silently) — drop the fast-path
463
+ # cache so the next ensure_triton_device re-syncs fully.
464
+ _rt.invalidate_current_device_cache()
465
+ return best_idx
466
+
467
+ @staticmethod
468
+ def visible_device_memory() -> list[tuple[int, int]]:
469
+ """`[(ordinal, total_bytes)]` for every device THIS PROCESS can see.
470
+
471
+ The ordinals come back usable as-is in `cuda:<i>`, because the whole
472
+ walk goes through the GPU RUNTIME — so `CUDA_VISIBLE_DEVICES` applies
473
+ to it, exactly as it applies to every allocation made afterwards.
474
+
475
+ That is the entire point of the method. `nvidia-smi` answers the same
476
+ question from NVML, which sits OUTSIDE the mask and always reports the
477
+ whole board: a caller that picks an index from `nvidia-smi` and then
478
+ allocates on it is reading one namespace and writing another, and the
479
+ two agree only on an unpinned host. `tests/unit/kernels/
480
+ test_prefill_determinism.py` did exactly that and asked for `cuda:2`
481
+ under `CUDA_VISIBLE_DEVICES=0` (2026-09-17) — a red that had nothing
482
+ to do with its subject, on a card that was simply not there.
483
+
484
+ Pure runtime API through ctypes — cudaGetDeviceCount + cudaSetDevice +
485
+ cudaMemGetInfo, no torch (R33). The previously-current device is
486
+ restored and the fast-path cache invalidated, as in
487
+ `most_free_device`, whose inline copy of this loop this replaces.
488
+ Returns `[]` when there is no runtime or no device.
489
+ """
490
+ try:
491
+ dm = _rt.device_memory()
492
+ except Exception:
493
+ return []
494
+ ndev = DeviceAllocator.device_count()
495
+ if ndev <= 0:
496
+ return []
497
+ prev = DeviceAllocator.get_device()
498
+ out: list[tuple[int, int]] = []
499
+ for i in range(ndev):
500
+ try:
501
+ dm.set_device(i)
502
+ _rc, _free_b, total_b = dm.mem_get_info()
503
+ out.append((i, total_b))
504
+ except Exception:
505
+ continue
506
+ try:
507
+ dm.set_device(prev)
508
+ except Exception:
509
+ pass
510
+ _rt.invalidate_current_device_cache()
511
+ return out
512
+
513
+ @staticmethod
514
+ def _maybe_init_pool() -> None:
515
+ """One-shot read of the NBX_ALLOC_POOL env var (default on;
516
+ `0` disables the pool). Cached on the class."""
517
+ if not hasattr(DeviceAllocator, '_pool_enabled_init'):
518
+ DeviceAllocator._pool_enabled = os.environ.get("NBX_ALLOC_POOL", "1") != "0"
519
+ DeviceAllocator._pool_enabled_init = True
520
+ if os.environ.get("NBX_ALLOC_STATS", "0") == "1":
521
+ _atexit.register(DeviceAllocator.print_alloc_stats)
522
+
523
+ @staticmethod
524
+ def print_alloc_stats() -> None:
525
+ """Driver-level watermark per device + the pool's own numbers
526
+ (NBX_ALLOC_STATS=1, printed at exit). `peak_driver` is the peak of
527
+ bytes held from the driver (pool-parked blocks included);
528
+ `pool_peak` is the most the free-list ever parked; `slack` is the
529
+ smallest-fit over-allocation (allocated − requested) summed over
530
+ every fit hit, and its maximum for one hit."""
531
+ st = DeviceAllocator._POOL.stats
532
+ devs = sorted(set(DeviceAllocator._cuda_peak_bytes) | set(DeviceAllocator._POOL.cached_peak))
533
+ for d in devs:
534
+ print(f"[NBX_ALLOC_STATS] cuda:{d} peak_driver={DeviceAllocator._cuda_peak_bytes.get(d, 0) / 2**20:.0f}MB "
535
+ f"pool_peak={DeviceAllocator._POOL.cached_peak.get(d, 0) / 2**20:.0f}MB "
536
+ f"pool_now={DeviceAllocator._POOL.cached.get(d, 0) / 2**20:.0f}MB", flush=True)
537
+ print(f"[NBX_ALLOC_STATS] pool={'on' if DeviceAllocator._pool_enabled else 'off'} "
538
+ f"hits exact={st['exact']} fit={st['fit']} miss={st['miss']} "
539
+ f"slack_total={st['slack_total'] / 2**20:.0f}MB slack_max={st['slack_max'] / 2**20:.0f}MB "
540
+ f"flushes={st['flushes']} flushed={st['flushed_bytes'] / 2**20:.0f}MB "
541
+ f"evictions={st['evictions']} evicted={st['evicted_bytes'] / 2**20:.0f}MB "
542
+ f"range maps={st.get('range_maps', 0)} ({st.get('range_map_us', 0) / 1e6:.2f} s) "
543
+ f"reclaims={st.get('range_reclaims', 0)} unmaps={st.get('range_unmaps', 0)} "
544
+ f"ranges={ {d: len(rs) for d, rs in DeviceAllocator._POOL.ranges.items()} } "
545
+ f"cap={ {d: round(c / 2**20) for d, c in DeviceAllocator._pool_cap_bytes.items()} }MB", flush=True)
546
+
547
+ @staticmethod
548
+ def _block_reported(ptr: int, size: Optional[int], dev: int) -> None:
549
+ """The pool's report of a block's size (`SegmentPool.on_block`): the per-pointer tables follow it."""
550
+ if size is None:
551
+ DeviceAllocator._cuda_ptr_size.pop(ptr, None)
552
+ DeviceAllocator._cuda_ptr_device.pop(ptr, None)
553
+ else:
554
+ DeviceAllocator._cuda_ptr_size[ptr] = size
555
+ DeviceAllocator._cuda_ptr_device[ptr] = dev
556
+
557
+ @staticmethod
558
+ def _segment_to_driver(dev: int, ptr: int) -> int:
559
+ """Return a WHOLLY free segment (its one block, free, unchained) to the driver; its bytes."""
560
+ n = DeviceAllocator._POOL.drop_segment(dev, ptr)
561
+ _rt.device_memory().free(ptr)
562
+ DeviceAllocator._cuda_ptr_size.pop(ptr, None)
563
+ DeviceAllocator._range_del(ptr)
564
+ DeviceAllocator._cuda_ptr_device.pop(ptr, None)
565
+ DeviceAllocator._cuda_ptr_epoch.pop(ptr, None)
566
+ DeviceAllocator._cuda_live_bytes[dev] = max(0, DeviceAllocator._cuda_live_bytes.get(dev, 0) - n)
567
+ return n
568
+
569
+ @staticmethod
570
+ def _pool_parked_cap(dev: int) -> int:
571
+ """Bytes the free-list may keep parked on `dev` (0 = unbounded):
572
+ `memory.alloc_pool_parked_cap_fraction` (arch yml) × the device's
573
+ total memory, resolved once per device. No profile / no key = 0."""
574
+ cap = DeviceAllocator._pool_cap_bytes.get(dev)
575
+ if cap is not None:
576
+ return cap
577
+ # "No hardware profile yet" (Prism has not stashed it) is NOT
578
+ # "explicitly unbounded": return 0 for this call without caching, so
579
+ # the cap resolves once the profile exists. The readers live one
580
+ # layer up (kernels/wrappers.py); the import is function-scoped —
581
+ # no import cycle at load — and the profile global is Prism's.
582
+ try:
583
+ from nbxtensor.ops.wrappers import get_hardware_profile, _arch_memory_param
584
+ prof = get_hardware_profile()
585
+ if prof is None or not getattr(prof, "devices", None):
586
+ return 0
587
+ frac = float(_arch_memory_param("alloc_pool_parked_cap_fraction", 0.0))
588
+ cap = 0
589
+ if frac > 0:
590
+ total = DeviceAllocator.device_total_bytes(dev)
591
+ # A limit imposed on the process (`set_limit`: an application's rung, a census) is the device the
592
+ # plan was sized for: the pool parks a fraction of IT, as it would on a card of that size — measured
593
+ # 2026-10-27, mochi-1-preview triton at the 8 GB rung on a 32 GB card parked its 9 GB text encoder
594
+ # arena (cap 16 GB) and peaked at 22.6 GB where a card of 8 GB would have freed it
595
+ limit = DeviceAllocator._imposed_bytes(dev) # the card the application holds this process to
596
+ if limit is not None and total > 0:
597
+ total = min(total, int(limit))
598
+ if total > 0:
599
+ cap = int(total * frac)
600
+ except Exception:
601
+ return 0
602
+ DeviceAllocator._pool_cap_bytes[dev] = cap
603
+ return cap
604
+
605
+ @staticmethod
606
+ def _pool_flush(dev: Optional[int] = None) -> int:
607
+ """Return every wholly free segment to the driver (a segment still holding a live block stays). Returns the
608
+ bytes freed. Called on a refused malloc and from empty_cache_pool()."""
609
+ devs = [dev] if dev is not None else list(DeviceAllocator._POOL.free.keys())
610
+ return DeviceAllocator._POOL.flush(devs, DeviceAllocator._segment_to_driver)
611
+
612
+ @staticmethod
613
+ def empty_cache_pool() -> int:
614
+ """Public API: release all pool-cached blocks back to the driver.
615
+ Returns total bytes freed. Safe to call when pool is disabled
616
+ (no-op). Useful between phases to defragment proactively."""
617
+ DeviceAllocator._maybe_init_pool()
618
+ if not DeviceAllocator._pool_enabled:
619
+ return 0
620
+ # an expandable range's free chunks stay mapped: the next component's tensors are their pages, and a driver
621
+ # allocation that needs them (a weight arena, a refused segment) reclaims what it needs at its refusal
622
+ # (`malloc_direct`). Unmapped here, they were mapped back at once — SANA-Video 16g: 65 calls between its
623
+ # phases, 47 737 unmaps and as many maps, +26 s (2026-11-02)
624
+ return DeviceAllocator._pool_flush()
625
+
626
+ @staticmethod
627
+ def new_allocation_epoch() -> int:
628
+ """Declare that every address issued so far belongs to a runtime that
629
+ is being replaced. Returns the new epoch.
630
+
631
+ Call it AFTER releasing what can still be released (the parked pool)
632
+ and BEFORE the new runtime exists. From here on, a free of an older
633
+ pointer is a no-op rather than a park or a driver call.
634
+
635
+ The engine proper never calls this: a process has one device runtime
636
+ for its whole life. It exists because test processes legitimately
637
+ replace one, and because the alternative — a free-list that outlives
638
+ the runtime it belongs to — surfaced as an order-dependent numeric
639
+ failure in an unrelated kernel, which is the worst way for it to be
640
+ found.
641
+ """
642
+ DeviceAllocator._alloc_epoch += 1
643
+ return DeviceAllocator._alloc_epoch
644
+
645
+ @staticmethod
646
+ def set_limit(nbytes: Optional[int], dev: Optional[int] = None) -> None:
647
+ """Hold this process to a card of `nbytes` on device `dev` (None: every device), or lift the limit (`nbytes`
648
+ None). The allocator then behaves as such a card does: a malloc that would carry what it holds from the driver
649
+ (live and pooled) past the limit less the runtime's own context is refused as that card refuses it — the pool
650
+ flushed, the malloc retried, then a named DeviceOOMError — and the pool parks its fraction of the limit, not of
651
+ the physical card."""
652
+ if nbytes is not None and int(nbytes) < 0:
653
+ raise ValueError(f"set_limit: a memory limit is a byte count >= 0 or None, not {nbytes!r}")
654
+ DeviceAllocator._limit_bytes[dev] = None if nbytes is None else int(nbytes)
655
+ for table in (DeviceAllocator._rung_limit_bytes, DeviceAllocator._pool_cap_bytes):
656
+ if dev is None:
657
+ table.clear()
658
+ else:
659
+ table.pop(dev, None)
660
+
661
+ @staticmethod
662
+ def _imposed_bytes(dev: int) -> Optional[int]:
663
+ """The card `set_limit` holds device `dev` to, in bytes; None: the physical card."""
664
+ lim = DeviceAllocator._limit_bytes
665
+ return lim[dev] if dev in lim else lim.get(None)
666
+
667
+ @staticmethod
668
+ def _rung_limit(dev: int) -> Optional[int]:
669
+ """The bytes this process may hold from the driver on `dev` under a limit (`set_limit`): the limit less what
670
+ the runtime holds outside the allocator's own blocks (its context), read once from the driver; None: none."""
671
+ if dev in DeviceAllocator._rung_limit_bytes:
672
+ return DeviceAllocator._rung_limit_bytes[dev]
673
+ imposed = DeviceAllocator._imposed_bytes(dev)
674
+ limit = None
675
+ if imposed is not None:
676
+ free_b, total_b = DeviceAllocator.mem_get_info(dev)
677
+ context = max(0, int(total_b) - int(free_b) - DeviceAllocator._cuda_live_bytes.get(dev, 0))
678
+ limit = max(0, int(imposed) - context)
679
+ DeviceAllocator._rung_limit_bytes[dev] = limit
680
+ return limit
681
+
682
+ @staticmethod
683
+ def _past_budget(dm, dev: int, nbytes: int):
684
+ """The backend's allocation-refused status when `nbytes` more would take this process past the budget its
685
+ device grants (`DeviceMemory.budget`), for a backend whose driver does not refuse there itself
686
+ (`supports("budget_refusal")`: Metal, whose buffers draw on the memory the whole machine shares; its host
687
+ buffers draw on the same budget); None otherwise. The ONE account decides — the device keeps none."""
688
+ if not dm.supports("budget_refusal"):
689
+ return None
690
+ rc, cap = dm.budget()
691
+ if rc != 0:
692
+ return rc
693
+ held = DeviceAllocator._cuda_live_bytes.get(dev, 0) + DeviceAllocator._host_pinned_live_bytes
694
+ return dm.ALLOC_REFUSED if held + int(nbytes) > int(cap) else None
695
+
696
+ #: device -> its VMM (`memory.vmm.Vmm`, with the profile's chunk) when the large class grows in an expandable
697
+ #: range; False: the device keeps driver segments (no VMM, or the profile measured none: vmm_chunk_bytes 0)
698
+ _VMM: Dict[int, Any] = {}
699
+
700
+ @staticmethod
701
+ def _vmm_chunk_fact(dev: int) -> int:
702
+ """The expandable pool's chunk on `dev`: the hardware profile's measured `vmm_chunk_bytes` (first launch,
703
+ `facts_probe.vmm_chunk_bytes`; 0 = the device has no VMM). No profile installed (a tool, a test): 0. A profile
704
+ whose device states no measurement: refused by name — never a chunk the code chose."""
705
+ from nbxtensor.ops.wrappers import get_hardware_profile
706
+ prof = get_hardware_profile()
707
+ if prof is None or not getattr(prof, "devices", None):
708
+ return 0
709
+ spec = next((d for d in prof.devices if getattr(d, "index", None) == dev), None)
710
+ measured = (getattr(spec, "measured", None) or {}) if spec is not None else {}
711
+ if "vmm_chunk_bytes" not in measured:
712
+ raise RuntimeError(
713
+ f"ZERO FALLBACK: the hardware profile states no measured `vmm_chunk_bytes` for device {dev} — the "
714
+ f"expandable pool's chunk is measured at the first launch on this machine (facts_probe."
715
+ f"vmm_chunk_bytes, written into the generated profile); a committed profile records it from that tool")
716
+ return int(measured["vmm_chunk_bytes"] or 0)
717
+
718
+ @staticmethod
719
+ def use_expandable(dev: int, chunk: int, range_bytes: Optional[int] = None) -> None:
720
+ """Set `dev`'s large class before its first allocation: an expandable range of `chunk` bytes, or the driver
721
+ segments (`chunk` 0) — the chunk probe's replay (`memory.trace_replay`), which measures each candidate in its
722
+ own process; a run reads the profile's measured fact instead (`_range_vmm`). `range_bytes`: the range's size
723
+ (the replayed card's; a run's is the profile's card). A chunk on a device that cannot map is refused by name."""
724
+ if chunk <= 0:
725
+ DeviceAllocator._VMM[dev] = False
726
+ return
727
+ libs = _rt.backend_table().get("driver_libs")
728
+ from nbxtensor.memory.vmm import Vmm
729
+ lib = Vmm.load(libs) if libs else None
730
+ vv = Vmm(lib, dev) if lib is not None else None
731
+ if vv is None or not vv.supported():
732
+ raise RuntimeError(f"ZERO FALLBACK: device {dev} cannot map an expandable range (no driver VMM): a chunk "
733
+ f"of {chunk} bytes is no measurement there")
734
+ vv.chunk = int(chunk)
735
+ vv.range_bytes = range_bytes
736
+ DeviceAllocator._VMM[dev] = vv
737
+
738
+ @staticmethod
739
+ def _range_vmm(dev: int):
740
+ """The device's VMM when its large class grows in an expandable range, else None (resolved once)."""
741
+ v = DeviceAllocator._VMM.get(dev)
742
+ if v is None:
743
+ v = False
744
+ # the capability first, the fact after: a backend without the driver API (HIP, Metal), a process that sees
745
+ # no card, or a device without the VMM attribute keeps its driver segments and needs no chunk fact; only a
746
+ # device that CAN map reads the measured chunk, refused by name when the profile states none
747
+ libs = _rt.backend_table().get("driver_libs")
748
+ if libs:
749
+ from nbxtensor.memory.vmm import Vmm
750
+ lib = Vmm.load(libs)
751
+ if lib is not None:
752
+ vv = Vmm(lib, dev)
753
+ if vv.supported():
754
+ chunk = DeviceAllocator._vmm_chunk_fact(dev)
755
+ if chunk:
756
+ vv.chunk = chunk
757
+ v = vv
758
+ DeviceAllocator._VMM[dev] = v
759
+ return v or None
760
+
761
+ @staticmethod
762
+ def _range_reserve(dev: int, v) -> None:
763
+ """Reserve one more address range for `dev`'s large class: the card's memory as the hardware profile declares
764
+ it (rule 1), whole chunks; registered as device memory for the pointer checks (`holds`)."""
765
+ from nbxtensor.ops.wrappers import get_hardware_profile
766
+ given = getattr(v, "range_bytes", None) # a replay names its card's size (use_expandable)
767
+ prof = get_hardware_profile() if given is None else None
768
+ spec = next((d for d in (getattr(prof, "devices", None) or []) if getattr(d, "index", None) == dev), None)
769
+ mb = (int(given) >> 20) if given is not None else int(getattr(spec, "memory_mb", 0) or 0)
770
+ if mb <= 0:
771
+ raise RuntimeError(f"ZERO FALLBACK: the hardware profile declares no memory for device {dev}: its "
772
+ f"expandable range has no size")
773
+ size = -(-(mb << 20) // v.chunk) * v.chunk
774
+ rc, base = v.reserve(size)
775
+ if rc != 0:
776
+ raise RuntimeError(f"cuMemAddressReserve of {size} bytes on device {dev} failed: CUresult={rc}")
777
+ r = range_add(DeviceAllocator._POOL, dev, base, size, v.chunk)
778
+ r.handles = {}
779
+ DeviceAllocator._range_add(base, size)
780
+
781
+ @staticmethod
782
+ def _range_alloc(dev: int, nbytes: int) -> Optional[int]:
783
+ """A large block from `dev`'s expandable range: the best free block, else the range extended at its top (a
784
+ new range when the last is full); every chunk it touches mapped. None when the card refuses the mapping even
785
+ after the reclaim (the block goes back to the pool)."""
786
+ pool = DeviceAllocator._POOL
787
+ v = DeviceAllocator._range_vmm(dev)
788
+ ptr = pool.take(dev, nbytes)
789
+ if ptr is None:
790
+ if range_extend(pool, dev, pool.grow_size(nbytes)) is None:
791
+ DeviceAllocator._range_reserve(dev, v)
792
+ if range_extend(pool, dev, pool.grow_size(nbytes)) is None:
793
+ # larger than a whole range: the card's refusal, typed and carrying its figures as the driver
794
+ # path's (a plain RuntimeError here escaped every caller that reshapes the work on a refusal)
795
+ raise DeviceAllocator._oom(dev, nbytes, 2, _rt.device_memory())
796
+ ptr = pool.take(dev, nbytes)
797
+ actual = pool.alloc_size.pop(ptr, nbytes)
798
+ if not DeviceAllocator._range_map(dev, v, range_unmapped(pool, dev, ptr, actual)):
799
+ pool.park(dev, pool.release(ptr))
800
+ return None
801
+ DeviceAllocator._cuda_ptr_size[ptr] = actual
802
+ DeviceAllocator._cuda_ptr_device[ptr] = dev
803
+ DeviceAllocator._cuda_ptr_epoch[ptr] = DeviceAllocator._alloc_epoch
804
+ if _MALLOC_TRACE_FILE is not None:
805
+ _record_malloc_site(ptr, actual, dev)
806
+ return ptr
807
+
808
+ @staticmethod
809
+ def _range_map(dev: int, v, pairs) -> bool:
810
+ """Map the (range, chunk) pairs: a physical chunk created and mapped for each, counted live. A card that
811
+ refuses (the imposed rung, the device budget, the driver) gets ONE reclaim — a synchronisation, then every
812
+ mapped chunk lying whole inside a free block unmapped and released, and the driver segments flushed — and the
813
+ mapping is retried; False when it still refuses."""
814
+ reclaimed = False
815
+ dm = _rt.device_memory()
816
+ _t0 = time.perf_counter() if pairs else 0.0
817
+ # the physical chunks this mapping's reclaim took back, KEPT to be mapped again where this mapping needs them: a
818
+ # remap is a map + an access grant, where a release then a create paid the driver's allocation of the bytes
819
+ # again (SANA-Video 16g: 2 852 chunks of 32 MiB created over a run, 10.6 s of its mapping — vmm_v100.md);
820
+ # whatever this mapping leaves unused goes back to the driver before it returns
821
+ spare: list = []
822
+ try:
823
+ for r, i in pairs:
824
+ while True:
825
+ rc = 2
826
+ if spare:
827
+ h = spare.pop()
828
+ rc = v.map(r.chunk_va(i), r.chunk, h)
829
+ if rc != 0:
830
+ v.release(h)
831
+ else:
832
+ limit = DeviceAllocator._rung_limit(dev)
833
+ if not (limit is not None and DeviceAllocator._cuda_live_bytes.get(dev, 0) + r.chunk > limit) \
834
+ and DeviceAllocator._past_budget(dm, dev, r.chunk) is None:
835
+ rc, h = v.create(r.chunk)
836
+ if rc == 0:
837
+ rc = v.map(r.chunk_va(i), r.chunk, h)
838
+ if rc != 0:
839
+ v.release(h)
840
+ if rc == 0:
841
+ break
842
+ if reclaimed:
843
+ DeviceAllocator.clear_last_error()
844
+ return False
845
+ # what this mapping still needs, never every free chunk: unmapping them all and mapping them back at
846
+ # the next growths cost HAT-L x4 4 143 unmaps + remaps per run, 2 s of its 12 (the supervisor 10:55)
847
+ DeviceAllocator._range_reclaim(dev, need=(len(pairs) - pairs.index((r, i))) * r.chunk, keep=spare)
848
+ reclaimed = True
849
+ r.mapped.add(i)
850
+ r.handles[i] = h
851
+ DeviceAllocator._POOL.stats["range_maps"] = DeviceAllocator._POOL.stats.get("range_maps", 0) + 1
852
+ live = DeviceAllocator._cuda_live_bytes.get(dev, 0) + r.chunk
853
+ DeviceAllocator._cuda_live_bytes[dev] = live
854
+ if live > DeviceAllocator._cuda_peak_bytes.get(dev, 0):
855
+ DeviceAllocator._cuda_peak_bytes[dev] = live
856
+ for _t, _p in (DeviceAllocator._windows.get(dev) or {}).items():
857
+ if live > _p:
858
+ DeviceAllocator._windows[dev][_t] = live
859
+ finally:
860
+ for h in spare:
861
+ v.release(h)
862
+ if pairs:
863
+ st = DeviceAllocator._POOL.stats
864
+ st["range_map_us"] = st.get("range_map_us", 0) + int((time.perf_counter() - _t0) * 1e6)
865
+ return True
866
+
867
+ @staticmethod
868
+ def _range_unmap(dev: int, r, i: int, keep: Optional[list] = None) -> None:
869
+ """Unmap chunk `i` of `r`; its physical handle released to the driver, or appended to `keep` (a mapping's own
870
+ reclaim, which maps it again elsewhere in the range)."""
871
+ v = DeviceAllocator._VMM.get(dev)
872
+ v.unmap(r.chunk_va(i), r.chunk)
873
+ h = r.handles.pop(i)
874
+ if keep is None:
875
+ v.release(h)
876
+ else:
877
+ keep.append(h)
878
+ DeviceAllocator._cuda_live_bytes[dev] = max(0, DeviceAllocator._cuda_live_bytes.get(dev, 0) - r.chunk)
879
+
880
+ @staticmethod
881
+ def _range_reclaim(dev: int, need: Optional[int] = None, keep: Optional[list] = None) -> int:
882
+ """ONE synchronisation of `dev`, then the mapped chunks lying whole inside free blocks of its ranges unmapped and
883
+ released (nothing a kernel may still read: the work is drained) — the highest addresses first, until `need`
884
+ bytes are given back (None: all of them), the driver segments flushed when the chunks were not enough. The
885
+ bytes given back. `keep`: the handles are appended there instead of released (a range mapping's own reclaim)."""
886
+ DeviceAllocator.device_synchronize(dev)
887
+ st = DeviceAllocator._POOL.stats
888
+ st["range_reclaims"] = st.get("range_reclaims", 0) + 1
889
+ out = 0
890
+ pairs = sorted(range_reclaimable(DeviceAllocator._POOL, dev), key=lambda ri: -ri[0].chunk_va(ri[1]))
891
+ for r, i in pairs:
892
+ if need is not None and out >= need:
893
+ return out
894
+ st["range_unmaps"] = st.get("range_unmaps", 0) + 1
895
+ DeviceAllocator._range_unmap(dev, r, i, keep=keep)
896
+ r.mapped.discard(i)
897
+ out += r.chunk
898
+ return out + DeviceAllocator._pool_flush(dev)
899
+
900
+ @staticmethod
901
+ def _oom(dev: int, nbytes: int, ret: int, dm) -> "DeviceOOMError":
902
+ """The named refusal of `nbytes` on `dev` (the driver's code `ret`): what was live, what the pool held, what the
903
+ driver had free — the text every log reader keys on."""
904
+ DeviceAllocator.clear_last_error() # a refused call must not poison the next one
905
+ # Diagnostic: how much live + how big the pool was at OOM time.
906
+ live_now = DeviceAllocator._cuda_live_bytes.get(dev, 0)
907
+ pool_total = 0
908
+ pool_count = 0
909
+ per_dev = DeviceAllocator._POOL.free.get(dev, {})
910
+ for sz, ptrs in per_dev.items():
911
+ pool_total += sz * len(ptrs)
912
+ pool_count += len(ptrs)
913
+ try:
914
+ _rc, driver_free, driver_total = dm.mem_get_info() # a runtime without the symbol raises: 0 below
915
+ except Exception:
916
+ driver_free = driver_total = 0
917
+ # The message is unchanged to the byte: the queue runner's error
918
+ # extractor and every log-reading tool key on this text.
919
+ return DeviceOOMError(
920
+ f"GPU malloc failed (error {ret}) for {nbytes} bytes "
921
+ f"[device cuda:{dev} live_tracked={live_now/1024/1024:.0f}MB "
922
+ f"pool_cached={pool_total/1024/1024:.0f}MB ({pool_count} blocks) "
923
+ f"driver_free={driver_free/1024/1024:.0f}MB / "
924
+ f"driver_total={driver_total/1024/1024:.0f}MB]"
925
+ + (f" under the imposed limit: {DeviceAllocator._rung_limit_bytes[dev]/1024/1024:.0f}MB this process may "
926
+ f"hold (memory.set_limit less the runtime's context)"
927
+ if DeviceAllocator._rung_limit_bytes.get(dev) is not None else ""),
928
+ requested=nbytes, device_idx=dev, live=live_now,
929
+ pool_cached=pool_total, pool_blocks=pool_count,
930
+ driver_free=driver_free if driver_total else None,
931
+ driver_total=driver_total or None)
932
+
933
+ @staticmethod
934
+ def _driver_malloc(ptr_obj, nbytes: int, dev: int) -> int:
935
+ """The runtime's malloc, or the refusal a card of the imposed rung gives (cudaErrorMemoryAllocation, 2)."""
936
+ limit = DeviceAllocator._rung_limit(dev)
937
+ if limit is not None and DeviceAllocator._cuda_live_bytes.get(dev, 0) + int(nbytes) > limit:
938
+ return 2
939
+ dm = _rt.device_memory()
940
+ refused = DeviceAllocator._past_budget(dm, dev, nbytes)
941
+ if refused is not None:
942
+ ptr_obj.value = None
943
+ return refused
944
+ rc, ptr = dm.malloc(nbytes)
945
+ ptr_obj.value = ptr or None
946
+ return rc
947
+
948
+ @staticmethod
949
+ def install_interceptor(interceptor: AllocatorInterceptor) -> None:
950
+ """Put `interceptor` in front of the five device operations. Installing the one in force again is a no-op; a
951
+ second, different one is refused by name (each would believe it sees every allocation)."""
952
+ cur = DeviceAllocator._interceptor
953
+ if cur is not None and cur is not interceptor:
954
+ raise RuntimeError(
955
+ f"an allocator interceptor is already installed ({type(cur).__module__}.{type(cur).__qualname__}); "
956
+ f"{type(interceptor).__module__}.{type(interceptor).__qualname__} cannot stand beside it")
957
+ DeviceAllocator._interceptor = interceptor
958
+
959
+ @staticmethod
960
+ def remove_interceptor(interceptor: AllocatorInterceptor) -> None:
961
+ """Take `interceptor` away; refused by name when it is not the one in force."""
962
+ if DeviceAllocator._interceptor is not interceptor:
963
+ raise RuntimeError(f"{type(interceptor).__qualname__} is not the allocator interceptor in force")
964
+ DeviceAllocator._interceptor = None
965
+
966
+ @staticmethod
967
+ def malloc_cuda(nbytes: int, dev_idx: Optional[int] = None, pooled: bool = True) -> int:
968
+ """Allocate GPU memory: through the interceptor in force, else directly (`malloc_direct`)."""
969
+ i = DeviceAllocator._interceptor
970
+ if i is None:
971
+ return DeviceAllocator.malloc_direct(nbytes, dev_idx, pooled)
972
+ return i.malloc(DeviceAllocator.malloc_direct, nbytes, dev_idx, pooled)
973
+
974
+ @staticmethod
975
+ def malloc_direct(nbytes: int, dev_idx: Optional[int] = None, pooled: bool = True) -> int:
976
+ """Allocate GPU memory.
977
+
978
+ Default path: synchronous cudaMalloc. The stream-ordered allocator
979
+ (cudaMallocAsync) was prototyped but disabled because its per-
980
+ device memory pools don't grant cross-device peer access by
981
+ default — freeing a cuda:N pointer from a Python-GC site while
982
+ cuda:M is current produced illegal memory accesses on the multi-
983
+ GPU pipeline_parallel path (Qwen3-30B --triton). The synchronous
984
+ path combined with TritonSequence's _deferred batch-release is
985
+ stable across single-GPU, zero3, and pipeline_parallel.
986
+
987
+ Phase 2 path (NBX_ALLOC_POOL=1): satisfies requests from a per-
988
+ device pool first (`memory.pool.SegmentPool`: two size classes, the
989
+ half rule); on a miss the device grows by `grow_size` and the request
990
+ is taken from the new segment; on cudaMalloc OOM, flushes the pool's
991
+ wholly free segments back to the driver and retries (analog to torch
992
+ CachingAllocator release_cached_blocks → retry).
993
+
994
+ dev_idx (C3): the CURRENT device index, when the caller already
995
+ knows it — skips the cudaGetDevice round-trip used purely for
996
+ pool/accounting bookkeeping. The caller MUST guarantee the
997
+ runtime current device equals dev_idx (cudaMalloc allocates on
998
+ the current device). None (default) keeps the internal query.
999
+
1000
+ pooled=False: a block that never enters the pool — taken from the driver (a refusal still flushes the pool and
1001
+ retries) and handed straight back to it when freed. A component's weight arena (`ComponentArena`): large and
1002
+ held for a whole component or segment; pooled, the small tensors that outlive it split it once freed and pin
1003
+ it whole (mochi-1-preview triton at the 8 GB rung, 2026-10-28: 3 553 MB of freed blocks held by ~19 MB of
1004
+ live tensors refused the transformer's 5.2 GB segment).
1005
+ """
1006
+ if nbytes == 0:
1007
+ return 0
1008
+ DeviceAllocator._maybe_init_pool()
1009
+ dev = dev_idx if dev_idx is not None else DeviceAllocator.get_device()
1010
+
1011
+ # The large class in the device's expandable range (R16, `memory.vmm`): blocks of one reserved address range,
1012
+ # chunks mapped as it grows, whole free chunks unmapped at a refusal — no driver segment, so no remainder a
1013
+ # live neighbour pins keeps its pages (Wan2.1-I2V triton 16g, 2026-11-01).
1014
+ if DeviceAllocator._pool_enabled and pooled and DeviceAllocator._POOL.cls(nbytes) == LARGE \
1015
+ and DeviceAllocator._range_vmm(dev) is not None:
1016
+ ptr = DeviceAllocator._range_alloc(dev, nbytes)
1017
+ if ptr is None:
1018
+ raise DeviceAllocator._oom(dev, nbytes, 2, _rt.device_memory())
1019
+ return ptr
1020
+
1021
+ # Pool fast path
1022
+ if DeviceAllocator._pool_enabled and pooled:
1023
+ ptr = DeviceAllocator._POOL.take(dev, nbytes)
1024
+ if ptr is not None:
1025
+ # alloc size: exact-size hit reuses nbytes; smallest-fit
1026
+ # populated the pool's alloc_size with the bigger size.
1027
+ actual = DeviceAllocator._POOL.alloc_size.pop(ptr, nbytes)
1028
+ DeviceAllocator._cuda_ptr_size[ptr] = actual
1029
+ DeviceAllocator._cuda_ptr_device[ptr] = dev # its segment's range stays registered
1030
+ DeviceAllocator._cuda_ptr_epoch[ptr] = DeviceAllocator._alloc_epoch
1031
+ if _MALLOC_TRACE_FILE is not None:
1032
+ _record_malloc_site(ptr, actual, dev)
1033
+ # No live-byte change: pool blocks stay counted as live
1034
+ # for the lifetime of the segment (pool == still allocated
1035
+ # from the driver's POV). Peak unchanged.
1036
+ return ptr
1037
+
1038
+ # The device grows by what the pool asks for the request (`grow_size`: a small segment for a small request,
1039
+ # the request for a large one) while its wholly free segments stay parked: with the pool's two size classes and
1040
+ # its half rule (memory/pool.py), no segment is pinned by a block smaller than its free remainder, so what a
1041
+ # refused malloc flushes is whole. 164e89dc flushed the pool before every growth instead (Sana_1600M_4Kpx
1042
+ # triton on a V100-16GB, lot 2026-10-28: 4.5 GB parked in 12 split segments beside 9.4 GB alive refused a
1043
+ # 2.96 GB convolution) — and emptied the small free segments the scalars then carved large blocks for: Allegro
1044
+ # triton 16g, gate 2026-11-01, 4.4 GB of free blocks in pinned segments refused a 2.5 GB gelu.
1045
+ grown = DeviceAllocator._POOL.grow_size(nbytes) if DeviceAllocator._pool_enabled and pooled else nbytes
1046
+ ptr_obj = ctypes.c_void_p()
1047
+ dm = _rt.device_memory()
1048
+ ret = DeviceAllocator._driver_malloc(ptr_obj, grown, dev)
1049
+
1050
+ # No reclaim-and-retry of the hot loops' deferred dead-tensor queues here
1051
+ # (ZERO FALLBACK, 2026-10-11). That rescue drained the queue on a refused
1052
+ # malloc and retried — `[NBX_OOM_RECLAIM] freed ...` 1 279 times in one
1053
+ # 16 GB Allegro run — so a plan that did not leave room for the queue ran
1054
+ # anyway. Prism now PRICES the queue (`PrismSolver._deferred_drain_mb`, the
1055
+ # vendor profile's `memory.deferred_drain_cliff_bytes`), and a malloc refused
1056
+ # with the queue inside its price is a plan defect, raised below and named
1057
+ # by the hot loop with the op, the queue it held and the price it had.
1058
+
1059
+ # Phase 2 OOM-retry: if cudaMalloc fails and the pool holds cached
1060
+ # blocks, flush them back to the driver and retry. Defragments the
1061
+ # driver's internal heap before declaring failure.
1062
+ if ret != 0 and DeviceAllocator._pool_enabled:
1063
+ DeviceAllocator.clear_last_error() # a refused call must not poison the next one
1064
+ freed = DeviceAllocator._pool_flush(dev)
1065
+ if DeviceAllocator._POOL.ranges.get(dev):
1066
+ freed += DeviceAllocator._range_reclaim(dev, need=grown) # the range's free chunks, what it needs
1067
+ if freed > 0:
1068
+ ret = DeviceAllocator._driver_malloc(ptr_obj, grown, dev) # what the pool records, never the request
1069
+
1070
+ # The engine's other cached memory, released the same way: a recorded replay plan's slab between two runs of
1071
+ # its sequence (`triton/replay.release_idle_slabs`), freed through the pool, flushed, retried once.
1072
+ if ret != 0 and DeviceAllocator._idle_reclaimers:
1073
+ DeviceAllocator.clear_last_error() # a refused call must not poison the next one
1074
+ got = sum(int(r(dev) or 0) for r in list(DeviceAllocator._idle_reclaimers))
1075
+ if got > 0:
1076
+ if DeviceAllocator._pool_enabled:
1077
+ DeviceAllocator._pool_flush(dev)
1078
+ ret, _p = dm.malloc(grown) # the runtime's malloc itself, as before (no rung check here)
1079
+ ptr_obj.value = _p or None
1080
+
1081
+ # Pre-allocation OOM-recovery (independent of pool): trigger a
1082
+ # cycle-collecting gc.collect() then retry. Sana 4Kpx triton
1083
+ # showed ~25 GB of NBXTensor instances pinned alive at conv::62
1084
+ # OOM by Python frame-args lists from recursive conv2d_wrapper
1085
+ # band loops; gc.collect() picks them up if they form a cycle.
1086
+ # Gated by NBX_GC_ON_OOM=1 (opt-in until validated cross-model).
1087
+ if ret != 0 and os.environ.get("NBX_GC_ON_OOM", "0") == "1":
1088
+ DeviceAllocator.clear_last_error() # a refused call must not poison the next one
1089
+ import gc as _gc_oom
1090
+ _gc_oom.collect()
1091
+ ret = DeviceAllocator._driver_malloc(ptr_obj, grown, dev)
1092
+
1093
+ if ret != 0:
1094
+ raise DeviceAllocator._oom(dev, nbytes, ret, dm)
1095
+ p = ptr_obj.value or 0
1096
+ DeviceAllocator._cuda_ptr_size[p] = grown
1097
+ DeviceAllocator._range_add(p, grown)
1098
+ DeviceAllocator._cuda_ptr_device[p] = dev
1099
+ if DeviceAllocator._pool_enabled and pooled:
1100
+ # the new segment joins the pool free, and the request is taken from it (carved, for a small one)
1101
+ DeviceAllocator._POOL.add_free_segment(dev, p, grown, nbytes)
1102
+ taken = DeviceAllocator._POOL.take(dev, nbytes)
1103
+ assert taken == p, f"the pool did not hand back the segment it was just given: {taken} for {p:#x}"
1104
+ DeviceAllocator._cuda_ptr_size[p] = DeviceAllocator._POOL.alloc_size.pop(p, grown)
1105
+ DeviceAllocator._cuda_ptr_epoch[p] = DeviceAllocator._alloc_epoch
1106
+ live = DeviceAllocator._cuda_live_bytes.get(dev, 0) + grown
1107
+ DeviceAllocator._cuda_live_bytes[dev] = live
1108
+ peak = DeviceAllocator._cuda_peak_bytes.get(dev, 0)
1109
+ if live > peak:
1110
+ DeviceAllocator._cuda_peak_bytes[dev] = live
1111
+ _win = DeviceAllocator._windows.get(dev)
1112
+ if _win:
1113
+ for _t, _p in _win.items():
1114
+ if live > _p:
1115
+ _win[_t] = live
1116
+ if _MALLOC_TRACE_FILE is not None:
1117
+ _record_malloc_site(p, nbytes, dev, pooled=DeviceAllocator._pool_enabled and pooled)
1118
+ return p
1119
+
1120
+ @staticmethod
1121
+ def free_cuda(ptr: int):
1122
+ """Free GPU memory: through the interceptor in force, else directly (`free_direct`)."""
1123
+ i = DeviceAllocator._interceptor
1124
+ if i is None:
1125
+ return DeviceAllocator.free_direct(ptr)
1126
+ return i.free(DeviceAllocator.free_direct, ptr)
1127
+
1128
+ @staticmethod
1129
+ def free_direct(ptr: int):
1130
+ """Free GPU memory.
1131
+
1132
+ Default path: cudaFree. Blocks the calling thread until the
1133
+ device is idle for the relevant stream, so TritonSequence
1134
+ batches freeable tensors into _deferred and releases them
1135
+ after a sync at the end of each forward pass.
1136
+
1137
+ Phase 2 path (NBX_ALLOC_POOL=1): the pointer is returned to the
1138
+ per-device free-list pool instead of cudaFree, keeping the
1139
+ driver's internal heap intact (avoids fragmenting it with the
1140
+ churn of small/medium tensors typical of a diffusion forward
1141
+ pass). Live byte counters stay unchanged because the block is
1142
+ STILL allocated from the driver's POV — it just sits in the
1143
+ pool waiting to satisfy the next compatible malloc. cudaFree
1144
+ eventually fires from _pool_flush() on OOM-retry or from
1145
+ empty_cache_pool().
1146
+ """
1147
+ if not ptr:
1148
+ return
1149
+ DeviceAllocator._maybe_init_pool()
1150
+
1151
+ # A pointer issued by a runtime that no longer exists.
1152
+ #
1153
+ # Its memory went when that runtime did, so there is nothing to give
1154
+ # back — and both of the things this function otherwise does would be
1155
+ # wrong: parking it hands a dead address to the next allocation, and
1156
+ # freeing it asks the CURRENT runtime to release something it never
1157
+ # issued, which it correctly refuses and loudly. Drop the bookkeeping
1158
+ # and say nothing. Only a process that replaces its runtime ever
1159
+ # reaches this; with one runtime the epoch never moves.
1160
+ _epoch = DeviceAllocator._cuda_ptr_epoch.pop(ptr, DeviceAllocator._alloc_epoch)
1161
+ if _epoch != DeviceAllocator._alloc_epoch:
1162
+ _dev = DeviceAllocator._cuda_ptr_device.pop(ptr, None)
1163
+ _sz = DeviceAllocator._cuda_ptr_size.pop(ptr, None)
1164
+ DeviceAllocator._POOL.alloc_size.pop(ptr, None)
1165
+ if _dev is not None and _sz:
1166
+ _live = DeviceAllocator._cuda_live_bytes.get(_dev, 0) - _sz
1167
+ DeviceAllocator._cuda_live_bytes[_dev] = max(0, _live)
1168
+ return
1169
+
1170
+ # The pool: the block goes back to its segment, merged with its free neighbours; a segment wholly free over
1171
+ # the parked cap returns to the driver. A block of a segment is never handed to the driver alone.
1172
+ if DeviceAllocator._pool_enabled and ptr in DeviceAllocator._POOL.blocks:
1173
+ if DeviceAllocator._POOL.blocks[ptr][3]:
1174
+ print(f"[NBX-ALLOC-ERROR] free of a block already free: {ptr:#x}", flush=True)
1175
+ return
1176
+ dev = DeviceAllocator._POOL.blocks[ptr][4]
1177
+ nbytes = DeviceAllocator._POOL.blocks[ptr][0]
1178
+ cached = DeviceAllocator._POOL.park(dev, DeviceAllocator._POOL.release(ptr))
1179
+ cap = DeviceAllocator._pool_parked_cap(dev)
1180
+ if cap and cached > cap:
1181
+ # a range's free chunks stay mapped (they are the next request's pages): they are unmapped only at a
1182
+ # refusal (`_range_reclaim`), as torch's expandable segments release only on an out-of-memory — an unmap
1183
+ # here needed a device synchronisation at every free over the cap (Allegro triton 16g's VAE: 3x slower)
1184
+ DeviceAllocator._POOL.evict(dev, cached - cap, DeviceAllocator._segment_to_driver)
1185
+ if _MALLOC_TRACE_FILE is not None:
1186
+ _record_free_site(ptr, nbytes)
1187
+ return
1188
+
1189
+ alloc_dev = DeviceAllocator._cuda_ptr_device.pop(ptr, None)
1190
+ _rc = _rt.device_memory().free(ptr)
1191
+ if _rc not in (None, 0):
1192
+ # Finalizer context — raising here cascades through GC; a
1193
+ # LOUD print preserves the sticky-error surfacing site.
1194
+ print(f"[NBX-CUDA-ERROR] cudaFree rc={_rc} ptr={ptr:#x} "
1195
+ f"dev={alloc_dev} — sticky async error surfaced at "
1196
+ f"this free; the fault is at or before it.", flush=True)
1197
+ nbytes = DeviceAllocator._cuda_ptr_size.pop(ptr, None)
1198
+ DeviceAllocator._range_del(ptr)
1199
+ if _MALLOC_TRACE_FILE is not None and nbytes is not None:
1200
+ _record_free_site(ptr, nbytes)
1201
+ if nbytes is not None:
1202
+ if alloc_dev is not None:
1203
+ live = DeviceAllocator._cuda_live_bytes.get(alloc_dev, 0) - nbytes
1204
+ DeviceAllocator._cuda_live_bytes[alloc_dev] = live
1205
+ else:
1206
+ dev = DeviceAllocator.get_device()
1207
+ live = DeviceAllocator._cuda_live_bytes.get(dev, 0) - nbytes
1208
+ if live < 0:
1209
+ for d in DeviceAllocator._cuda_live_bytes:
1210
+ if DeviceAllocator._cuda_live_bytes[d] >= nbytes:
1211
+ DeviceAllocator._cuda_live_bytes[d] -= nbytes
1212
+ break
1213
+ else:
1214
+ DeviceAllocator._cuda_live_bytes[dev] = live
1215
+
1216
+ @staticmethod
1217
+ def bind_mapped(ptr: int, nbytes: int) -> None:
1218
+ """Make `nbytes` of a file mapping at host address `ptr` readable by kernels IN PLACE — on a unified
1219
+ device only (one pool: the GPU reads the mapping's pages, nothing is copied). A discrete device has no
1220
+ such address space: refused by name. Registered in the range table, so `holds()` knows the range."""
1221
+ if _rt.gpu_backend() != "metal":
1222
+ raise RuntimeError(f"ZERO FALLBACK: binding a file mapping in place needs unified memory; backend "
1223
+ f"{_rt.gpu_backend()!r} copies host experts into device slots")
1224
+ start = _rt.device_memory().map_host(int(ptr), int(nbytes))
1225
+ page = int(os.sysconf("SC_PAGESIZE"))
1226
+ end = -(-(int(ptr) + int(nbytes)) // page) * page
1227
+ DeviceAllocator._range_add(start, max(DeviceAllocator._range_size.get(start, 0), end - start))
1228
+
1229
+ @staticmethod
1230
+ def unbind_mapped(ptr: int) -> None:
1231
+ """Undo `bind_mapped` for the range starting at `ptr`'s page."""
1232
+ page = int(os.sysconf("SC_PAGESIZE"))
1233
+ start = int(ptr) // page * page
1234
+ _rt.device_memory().unmap_host(start)
1235
+ DeviceAllocator._range_del(start)
1236
+
1237
+ @staticmethod
1238
+ def malloc_host_pinned(nbytes: int) -> int:
1239
+ """Allocate page-locked host memory. Enables non_blocking H2D DMA.
1240
+
1241
+ Uses cudaMallocHost (or hipHostMalloc on ROCm). The returned
1242
+ pointer is a HOST address — numpy can wrap it via
1243
+ np.frombuffer(ctypes.string_at(ptr, nbytes), ...) or ctypes
1244
+ casting, and cudaMemcpy kind=1 (H2D) uses it as src.
1245
+ """
1246
+ if nbytes == 0:
1247
+ return 0
1248
+ dm = _rt.device_memory()
1249
+ if not dm.supports("host_alloc"):
1250
+ raise RuntimeError(
1251
+ f"Pinned host allocation unsupported on backend "
1252
+ f"{_rt.gpu_backend()!r}")
1253
+ ret = DeviceAllocator._past_budget(dm, DeviceAllocator.get_device(), nbytes)
1254
+ if ret is None:
1255
+ ret, p = dm.host_alloc(nbytes)
1256
+ if ret != 0:
1257
+ DeviceAllocator.clear_last_error() # a refused call must not poison the next one
1258
+ raise RuntimeError(
1259
+ f"Host pinned malloc failed (error {ret}) for {nbytes} bytes")
1260
+ DeviceAllocator._host_pinned_ptr_size[p] = nbytes
1261
+ DeviceAllocator._range_add(p, nbytes) # device-mapped (UVA): a kernel may read it, so holds() knows it
1262
+ DeviceAllocator._host_pinned_live_bytes += nbytes
1263
+ if DeviceAllocator._host_pinned_live_bytes > DeviceAllocator._host_pinned_peak_bytes:
1264
+ DeviceAllocator._host_pinned_peak_bytes = DeviceAllocator._host_pinned_live_bytes
1265
+ return p
1266
+
1267
+ @staticmethod
1268
+ def register_host_pinned(ptr: int, nbytes: int) -> None:
1269
+ """Page-lock `nbytes` of existing host memory at `ptr` (the backend's `host_register`), accounted as a pinned
1270
+ allocation is: in `holds()`'s ranges (device-mapped under UVA) and in the pinned live / peak bytes.
1271
+ `unregister_host_pinned` undoes it. A backend without the primitive is refused by name — the caller asks
1272
+ `supports("host_register")` first."""
1273
+ dm = _rt.device_memory()
1274
+ if not dm.supports("host_register"):
1275
+ raise RuntimeError(f"host memory registration unsupported on backend {_rt.gpu_backend()!r}")
1276
+ ret = dm.host_register(int(ptr), int(nbytes))
1277
+ if ret != 0:
1278
+ DeviceAllocator.clear_last_error()
1279
+ raise RuntimeError(f"Host memory registration failed (error {ret}) for {nbytes} bytes")
1280
+ DeviceAllocator._host_pinned_ptr_size[ptr] = nbytes
1281
+ DeviceAllocator._range_add(ptr, nbytes)
1282
+ DeviceAllocator._host_pinned_live_bytes += nbytes
1283
+ if DeviceAllocator._host_pinned_live_bytes > DeviceAllocator._host_pinned_peak_bytes:
1284
+ DeviceAllocator._host_pinned_peak_bytes = DeviceAllocator._host_pinned_live_bytes
1285
+
1286
+ @staticmethod
1287
+ def unregister_host_pinned(ptr: int) -> None:
1288
+ if ptr:
1289
+ _rt.device_memory().host_unregister(int(ptr))
1290
+ nbytes = DeviceAllocator._host_pinned_ptr_size.pop(ptr, None)
1291
+ DeviceAllocator._range_del(ptr)
1292
+ if nbytes is not None:
1293
+ DeviceAllocator._host_pinned_live_bytes -= nbytes
1294
+
1295
+ @staticmethod
1296
+ def free_host_pinned(ptr: int):
1297
+ if ptr:
1298
+ dm = _rt.device_memory()
1299
+ if not dm.supports("host_free"):
1300
+ return
1301
+ dm.host_free(ptr)
1302
+ nbytes = DeviceAllocator._host_pinned_ptr_size.pop(ptr, None)
1303
+ DeviceAllocator._range_del(ptr)
1304
+ if nbytes is not None:
1305
+ DeviceAllocator._host_pinned_live_bytes -= nbytes
1306
+
1307
+ # ------------------------------------------------------------------
1308
+ # Accounting API — use instead of cudaMemGetInfo for per-NeuroBrix
1309
+ # observability (cudaMemGetInfo returns device-wide numbers
1310
+ # including other processes, which is useless for debugging a
1311
+ # specific zero3 leak).
1312
+ # ------------------------------------------------------------------
1313
+
1314
+ @staticmethod
1315
+ def memory_allocated(device_idx: Optional[int] = None) -> int:
1316
+ """Live bytes allocated via malloc_cuda on the given device.
1317
+ With device_idx=None, returns the total across all devices."""
1318
+ if device_idx is None:
1319
+ return sum(DeviceAllocator._cuda_live_bytes.values())
1320
+ return DeviceAllocator._cuda_live_bytes.get(device_idx, 0)
1321
+
1322
+ @staticmethod
1323
+ def peak_memory_allocated(device_idx: Optional[int] = None) -> int:
1324
+ """Highest live-bytes watermark observed for the device.
1325
+ With device_idx=None, returns the total peak across all devices."""
1326
+ if device_idx is None:
1327
+ return sum(DeviceAllocator._cuda_peak_bytes.values())
1328
+ return DeviceAllocator._cuda_peak_bytes.get(device_idx, 0)
1329
+
1330
+ @staticmethod
1331
+ def device_free_bytes(device_idx: Optional[int] = None) -> int:
1332
+ """Driver-free bytes on `device_idx` (None = current device), via
1333
+ cudaMemGetInfo/hipMemGetInfo through ctypes (R33: no torch).
1334
+
1335
+ Returns -1 when the query is unavailable (CPU-only host, runtime
1336
+ load failure) so callers can distinguish "unknown" from "0 bytes
1337
+ free". Read-only feasibility probe for wrapper-internal memory
1338
+ streaming gates (conv3d temporal chunk-streaming) — deliberately
1339
+ NOT an accounting API (device-wide, includes other processes) and
1340
+ NOT a per-launch kernel-config source (hardware params stay in the
1341
+ vendor YAMLs)."""
1342
+ prev_dev = None
1343
+ try:
1344
+ dm = _rt.device_memory()
1345
+ if (device_idx is not None
1346
+ and DeviceAllocator.get_device() != device_idx):
1347
+ # Probe on another device: switch, query, RESTORE (the
1348
+ # allocator's own doctrine — a GC-site free of a cuda:N
1349
+ # pointer must never leave cuda:N current; the C3 fast path
1350
+ # contracts that the runtime current device is the caller's).
1351
+ prev_dev = DeviceAllocator.get_device()
1352
+ DeviceAllocator.set_device(device_idx)
1353
+ _rc, free_b, _total_b = dm.mem_get_info()
1354
+ free = int(free_b)
1355
+ # Pool-aware (2026-09-02): bytes parked in the free-list are
1356
+ # reclaimable on demand (the malloc OOM path flushes them, the
1357
+ # autotune guard flushes them), so every feasibility probe —
1358
+ # the SDPA chunk decision, the drain pressure gate, the conv3d
1359
+ # chunk-stream gate — reads the same headroom it read with the
1360
+ # pool off. Without this the probes under-read free memory by
1361
+ # the parked bytes and shrank their chunks on edge rows.
1362
+ if DeviceAllocator._pool_enabled:
1363
+ d = device_idx if device_idx is not None else DeviceAllocator.get_device()
1364
+ free += DeviceAllocator._POOL.cached.get(d, 0)
1365
+ return free
1366
+ except Exception:
1367
+ return -1
1368
+ finally:
1369
+ if prev_dev is not None:
1370
+ try:
1371
+ DeviceAllocator.set_device(prev_dev)
1372
+ except Exception:
1373
+ pass
1374
+
1375
+ @staticmethod
1376
+ def device_total_bytes(device_idx: Optional[int] = None) -> int:
1377
+ """Total device memory on `device_idx` (None = current device) via
1378
+ cudaMemGetInfo/hipMemGetInfo through ctypes; -1 when unavailable.
1379
+ Same contract and caveats as `device_free_bytes`."""
1380
+ prev_dev = None
1381
+ try:
1382
+ dm = _rt.device_memory()
1383
+ if (device_idx is not None
1384
+ and DeviceAllocator.get_device() != device_idx):
1385
+ # Probe on another device: switch, query, RESTORE (the
1386
+ # allocator's own doctrine — a GC-site free of a cuda:N
1387
+ # pointer must never leave cuda:N current; the C3 fast path
1388
+ # contracts that the runtime current device is the caller's).
1389
+ prev_dev = DeviceAllocator.get_device()
1390
+ DeviceAllocator.set_device(device_idx)
1391
+ _rc, _free_b, total_b = dm.mem_get_info()
1392
+ return int(total_b)
1393
+ except Exception:
1394
+ return -1
1395
+ finally:
1396
+ if prev_dev is not None:
1397
+ try:
1398
+ DeviceAllocator.set_device(prev_dev)
1399
+ except Exception:
1400
+ pass
1401
+
1402
+ @staticmethod
1403
+ def open_window(device_idx: int) -> int:
1404
+ """Start watching what this process holds from the driver on `device_idx` (live and pooled bytes, the
1405
+ quantity the peak counts); a token for `close_window`. The process-wide peak is never reset by it: a window
1406
+ measures one stretch (a component from its load to its release) beside it."""
1407
+ DeviceAllocator._window_seq += 1
1408
+ t = DeviceAllocator._window_seq
1409
+ held = DeviceAllocator._cuda_live_bytes.get(int(device_idx), 0)
1410
+ DeviceAllocator._windows.setdefault(int(device_idx), {})[t] = held
1411
+ DeviceAllocator._window_starts[t] = held
1412
+ return t
1413
+
1414
+ @staticmethod
1415
+ def window_start(token: int) -> Optional[int]:
1416
+ """What this process held from the driver when the window `token` opened (what other stretches already held:
1417
+ the part of the window's peak that is not its own); None for a token never opened."""
1418
+ return DeviceAllocator._window_starts.get(token)
1419
+
1420
+ @staticmethod
1421
+ def close_window(device_idx: int, token: int) -> Optional[int]:
1422
+ """The most this process held from the driver on `device_idx` since `open_window` returned `token`, and the
1423
+ window closed; None for a token not open there."""
1424
+ DeviceAllocator._window_starts.pop(token, None)
1425
+ return DeviceAllocator._windows.get(int(device_idx), {}).pop(token, None)
1426
+
1427
+ @staticmethod
1428
+ def reset_peak_memory(device_idx: Optional[int] = None) -> None:
1429
+ """Reset the peak watermark to the current live value.
1430
+ With device_idx=None, resets all devices."""
1431
+ if device_idx is None:
1432
+ for d in list(DeviceAllocator._cuda_live_bytes.keys()):
1433
+ DeviceAllocator._cuda_peak_bytes[d] = (
1434
+ DeviceAllocator._cuda_live_bytes[d])
1435
+ else:
1436
+ DeviceAllocator._cuda_peak_bytes[device_idx] = (
1437
+ DeviceAllocator._cuda_live_bytes.get(device_idx, 0))
1438
+
1439
+ @staticmethod
1440
+ def host_pinned_allocated() -> int:
1441
+ """Live bytes allocated via malloc_host_pinned."""
1442
+ return DeviceAllocator._host_pinned_live_bytes
1443
+
1444
+ @staticmethod
1445
+ def host_pinned_peak() -> int:
1446
+ """Peak pinned host bytes observed."""
1447
+ return DeviceAllocator._host_pinned_peak_bytes
1448
+
1449
+ @staticmethod
1450
+ def memset_cuda(ptr: int, value: int, nbytes: int):
1451
+ """Set device bytes: through the interceptor in force, else directly (`memset_direct`)."""
1452
+ i = DeviceAllocator._interceptor
1453
+ if i is None:
1454
+ return DeviceAllocator.memset_direct(ptr, value, nbytes)
1455
+ return i.memset(DeviceAllocator.memset_direct, ptr, value, nbytes)
1456
+
1457
+ @staticmethod
1458
+ def memset_direct(ptr: int, value: int, nbytes: int):
1459
+ if nbytes > 0 and ptr:
1460
+ _rt.device_memory().memset(ptr, value, nbytes)
1461
+
1462
+ @staticmethod
1463
+ def memcpy(dst: int, src: int, nbytes: int, kind: int = 3):
1464
+ """Copy memory (kind: 0=H2H, 1=H2D, 2=D2H, 3=D2D): through the interceptor in force, else directly
1465
+ (`memcpy_direct`)."""
1466
+ i = DeviceAllocator._interceptor
1467
+ if i is None:
1468
+ return DeviceAllocator.memcpy_direct(dst, src, nbytes, kind)
1469
+ return i.memcpy(DeviceAllocator.memcpy_direct, dst, src, nbytes, kind)
1470
+
1471
+ @staticmethod
1472
+ def memcpy_direct(dst: int, src: int, nbytes: int, kind: int = 3):
1473
+ """Copy memory. kind: 0=H2H, 1=H2D, 2=D2H, 3=D2D.
1474
+
1475
+ NOTE (NVIDIA API-sync-behavior spec): for D2D the "synchronous"
1476
+ cudaMemcpy performs NO host-side synchronization — the copy is
1477
+ enqueued on the calling device's legacy stream and the host
1478
+ returns immediately. Lifetime of src/dst must be guaranteed by
1479
+ stream ordering, never by host program order.
1480
+ """
1481
+ if nbytes > 0:
1482
+ _rc = _rt.device_memory().memcpy(dst, src, nbytes, kind)
1483
+ if _rc not in (None, 0):
1484
+ raise RuntimeError(
1485
+ f"cudaMemcpy failed rc={_rc} (kind={kind}, "
1486
+ f"nbytes={nbytes}) — CUDA errors are STICKY: this is "
1487
+ f"the first CHECKED call after the poisoning site, "
1488
+ f"not necessarily the fault itself.")
1489
+
1490
+ @staticmethod
1491
+ def sync_device():
1492
+ """Synchronize the current device: through the interceptor in force, else directly (`sync_direct`)."""
1493
+ i = DeviceAllocator._interceptor
1494
+ if i is None:
1495
+ return DeviceAllocator.sync_direct()
1496
+ return i.sync(DeviceAllocator.sync_direct)
1497
+
1498
+ @staticmethod
1499
+ def sync_direct():
1500
+ """Synchronize current GPU device. Waits for all pending GPU ops.
1501
+
1502
+ RAISES on a nonzero return: cudaDeviceSynchronize returns any
1503
+ prior async error (sticky semantics) — discarding it hid the
1504
+ true poisoning site of the multi-GPU error-700 class behind the
1505
+ next checked malloc (R16 research, 2026-07-24)."""
1506
+ rt = _rt.gpu_runtime()
1507
+ backend = _rt.backend_table()
1508
+ sync_name = backend.get("sync")
1509
+ if sync_name:
1510
+ _rc = getattr(rt, sync_name)()
1511
+ if _rc not in (None, 0):
1512
+ raise RuntimeError(
1513
+ f"deviceSynchronize failed rc={_rc} on current device "
1514
+ f"{DeviceAllocator.get_device()} — sticky async error "
1515
+ f"surfaced at this sync; the fault is at or before it.")
1516
+ # The portable half of the same contract: a backend that does not
1517
+ # honour `tl.device_assert` reports through the fault channel, and
1518
+ # this is where it is observed. The sync above has retired the work
1519
+ # that would have written it.
1520
+ _faults.check_device_faults()
1521
+
1522
+ # ------------------------------------------------------------------
1523
+ # Async stream + event primitives (zero torch).
1524
+ # Zero3 pipelining opens a dedicated transfer stream so that the H2D
1525
+ # copy of block N+1 overlaps with the compute on block N which runs
1526
+ # on the default stream. Events synchronize the two streams so we
1527
+ # never read a weight that's still being copied.
1528
+ #
1529
+ # Handles are opaque ctypes.c_void_p values surfaced as Python ints
1530
+ # (0 is the default/NULL stream). Keep them in Python state; do not
1531
+ # re-wrap them on each use — ctypes creates a fresh opaque object
1532
+ # each call.
1533
+ # ------------------------------------------------------------------
1534
+
1535
+ @staticmethod
1536
+ def create_stream() -> int:
1537
+ """Create a new GPU stream (`cudaStreamCreate` / `hipStreamCreate`:
1538
+ a BLOCKING stream, i.e. one that implicitly orders against the
1539
+ legacy default stream). Returns an opaque handle.
1540
+
1541
+ This ordering is load-bearing: the deferred-free drain skips its
1542
+ host sync under the allocator pool on the premise that every
1543
+ NeuroBrix triton stream orders against the legacy stream (kernel
1544
+ launches and D2D copies on the legacy stream; the zero3 transfer
1545
+ stream created here). The only non-blocking stream in the engine
1546
+ is the replay-graph capture stream, bracketed by context syncs."""
1547
+ rt = _rt.gpu_runtime()
1548
+ backend = _rt.backend_table()
1549
+ fn_name = backend.get("stream_create")
1550
+ if fn_name is None:
1551
+ raise RuntimeError(
1552
+ f"Stream create unsupported on backend {_rt.gpu_backend()!r}")
1553
+ h = ctypes.c_void_p()
1554
+ ret = getattr(rt, fn_name)(ctypes.byref(h))
1555
+ if ret != 0:
1556
+ DeviceAllocator.clear_last_error() # a refused call must not poison the next one
1557
+ raise RuntimeError(f"stream create failed (error {ret})")
1558
+ return h.value or 0
1559
+
1560
+ @staticmethod
1561
+ def clear_last_error() -> int:
1562
+ """Read-and-reset the runtime's sticky "last error". A refused call
1563
+ (an ordering event asked for an interval, a peer pair that cannot be
1564
+ enabled) leaves its code there, and the next unrelated launch check
1565
+ in the process — torch's, in the compiled branch — reports it as its
1566
+ own (found 2026-09-05: `invalid device ordinal` on an fp16 op after
1567
+ the stopwatch refusal test). Every expected refusal clears it."""
1568
+ rt = _rt.gpu_runtime()
1569
+ fn_name = _rt.backend_table().get("get_last_error")
1570
+ return int(getattr(rt, fn_name)()) if fn_name else 0
1571
+
1572
+ @staticmethod
1573
+ def _rt_call(key: str, *args):
1574
+ rt = _rt.gpu_runtime()
1575
+ fn_name = _rt.backend_table().get(key)
1576
+ if fn_name is None:
1577
+ raise RuntimeError(f"{key} unsupported on backend {_rt.gpu_backend()!r}")
1578
+ ret = getattr(rt, fn_name)(*args)
1579
+ if ret != 0:
1580
+ DeviceAllocator.clear_last_error()
1581
+ raise RuntimeError(f"{fn_name} failed (error {ret})")
1582
+
1583
+ @staticmethod
1584
+ def destroy_stream(stream: int):
1585
+ """Destroy a stream created by create_stream. No-op for default (0)."""
1586
+ if not stream:
1587
+ return
1588
+ rt = _rt.gpu_runtime()
1589
+ backend = _rt.backend_table()
1590
+ fn_name = backend.get("stream_destroy")
1591
+ if fn_name is None:
1592
+ return
1593
+ getattr(rt, fn_name)(ctypes.c_void_p(stream))
1594
+
1595
+ @staticmethod
1596
+ def holds(ptr: int) -> bool:
1597
+ """True when `ptr` lies inside a live device allocation this allocator
1598
+ handed out (a block's base, or a view inside it). The launcher asks
1599
+ before dispatching a kernel on an address: a foreign pointer is
1600
+ refused, never launched (launcher contract, Metal workstream 2026-09-05).
1601
+ O(log n): the live ranges are re-sorted only when a registration
1602
+ changed since the last call."""
1603
+ ptr = int(ptr)
1604
+ size = DeviceAllocator._range_size.get(ptr)
1605
+ if size is not None:
1606
+ return True
1607
+ bases = DeviceAllocator._range_bases
1608
+ i = bisect.bisect_right(bases, ptr) - 1
1609
+ return i >= 0 and bases[i] <= ptr < bases[i] + DeviceAllocator._range_size[bases[i]]
1610
+
1611
+ @staticmethod
1612
+ def containing(ptr: int):
1613
+ """(base, size) of the live range `ptr` lies in, the NEAREST one: the greatest base at or below `ptr` whose range
1614
+ holds it. In-place expert bindings overlap where two projections share a page; the first range in insertion
1615
+ order resolved a projection to its neighbour's shorter range and a launch read past it (DSV2 triton printed
1616
+ garbage, 2026-10-20). The nearest base is the projection's own binding, which runs to its end. None when no
1617
+ range holds `ptr`. O(log n) plus the ranges walked down past."""
1618
+ ptr = int(ptr)
1619
+ bases = DeviceAllocator._range_bases
1620
+ sizes = DeviceAllocator._range_size
1621
+ i = bisect.bisect_right(bases, ptr) - 1
1622
+ while i >= 0:
1623
+ b = bases[i]
1624
+ if ptr < b + sizes[b]:
1625
+ return b, sizes[b]
1626
+ i -= 1
1627
+ return None
1628
+
1629
+ @staticmethod
1630
+ def _range_add(base: int, nbytes: int) -> None:
1631
+ """Register a live range for `holds()` — O(log n) insert, kept sorted
1632
+ so a launch pays one bisect and never a re-sort."""
1633
+ if base not in DeviceAllocator._range_size:
1634
+ bisect.insort(DeviceAllocator._range_bases, base)
1635
+ DeviceAllocator._range_size[base] = int(nbytes)
1636
+
1637
+ @staticmethod
1638
+ def _range_del(base: int) -> None:
1639
+ if DeviceAllocator._range_size.pop(base, None) is not None:
1640
+ bases = DeviceAllocator._range_bases
1641
+ i = bisect.bisect_left(bases, base)
1642
+ if i < len(bases) and bases[i] == base:
1643
+ bases.pop(i)
1644
+
1645
+ @staticmethod
1646
+ def device_synchronize(device_idx: Optional[int] = None):
1647
+ """Block the caller until every stream of the device drains.
1648
+
1649
+ Selects the device first when one is given (the runtime's device
1650
+ synchronize is per current device) and restores the previous one."""
1651
+ rt = _rt.gpu_runtime()
1652
+ backend = _rt.backend_table()
1653
+ fn_name = backend.get("sync")
1654
+ if fn_name is None:
1655
+ return
1656
+ prev = None
1657
+ if device_idx is not None:
1658
+ prev = DeviceAllocator.get_device()
1659
+ if prev != device_idx:
1660
+ DeviceAllocator.set_device(device_idx)
1661
+ try:
1662
+ ret = getattr(rt, fn_name)()
1663
+ if ret != 0:
1664
+ DeviceAllocator.clear_last_error()
1665
+ raise RuntimeError(f"{fn_name} failed with error {ret}")
1666
+ finally:
1667
+ if prev is not None and prev != device_idx:
1668
+ DeviceAllocator.set_device(prev)
1669
+
1670
+ @staticmethod
1671
+ def mem_get_info(device_idx: Optional[int] = None) -> Tuple[int, int]:
1672
+ """(free_bytes, total_bytes) of a device, from the runtime."""
1673
+ dm = _rt.device_memory()
1674
+ mem_fn = _rt.backend_table().get("mem_get_info", "cudaMemGetInfo") # named in the refusal, as before
1675
+ prev = None
1676
+ if device_idx is not None:
1677
+ prev = DeviceAllocator.get_device()
1678
+ if prev != device_idx:
1679
+ DeviceAllocator.set_device(device_idx)
1680
+ try:
1681
+ ret, free_b, total_b = dm.mem_get_info()
1682
+ if ret != 0:
1683
+ DeviceAllocator.clear_last_error()
1684
+ raise RuntimeError(f"{mem_fn} failed with error {ret}")
1685
+ return int(free_b), int(total_b)
1686
+ finally:
1687
+ if prev is not None and prev != device_idx:
1688
+ DeviceAllocator.set_device(prev)
1689
+
1690
+ @staticmethod
1691
+ def stream_synchronize(stream: int):
1692
+ """Block the caller until the given stream drains."""
1693
+ rt = _rt.gpu_runtime()
1694
+ backend = _rt.backend_table()
1695
+ fn_name = backend.get("stream_sync")
1696
+ if fn_name is None:
1697
+ return
1698
+ getattr(rt, fn_name)(ctypes.c_void_p(stream))
1699
+
1700
+ @staticmethod
1701
+ def create_event(timing: bool = True) -> int:
1702
+ """Create a new event. Returns opaque handle.
1703
+
1704
+ `timing=False` asks for an event that can order streams but cannot be
1705
+ used to measure an interval, and that is what almost every event in
1706
+ this engine actually wants. It is not a micro-optimisation: recording
1707
+ a timing-enabled event costs **9.5 us** on this rig against ~1 us for
1708
+ an ordering-only one, because the runtime has to place a clock read
1709
+ in the stream and keep the result addressable. Measured 2026-09-03 —
1710
+ it was 28 % of a cross-GPU collective's entire host cost.
1711
+
1712
+ Pass timing=True (the default, kept for compatibility) only when the
1713
+ handle is going to reach `event_elapsed_ms`.
1714
+ """
1715
+ rt = _rt.gpu_runtime()
1716
+ backend = _rt.backend_table()
1717
+ h = ctypes.c_void_p()
1718
+ flags_fn = backend.get("event_create_flags")
1719
+ if not timing and flags_fn is not None:
1720
+ # 0x02 = cudaEventDisableTiming / hipEventDisableTiming.
1721
+ ret = getattr(rt, flags_fn)(ctypes.byref(h), ctypes.c_uint(0x02))
1722
+ else:
1723
+ fn_name = backend.get("event_create")
1724
+ if fn_name is None:
1725
+ raise RuntimeError(
1726
+ f"Event create unsupported on backend "
1727
+ f"{_rt.gpu_backend()!r}")
1728
+ ret = getattr(rt, fn_name)(ctypes.byref(h))
1729
+ if ret != 0:
1730
+ DeviceAllocator.clear_last_error() # a refused call must not poison the next one
1731
+ raise RuntimeError(f"event create failed (error {ret})")
1732
+ return h.value or 0
1733
+
1734
+ @staticmethod
1735
+ def destroy_event(event: int):
1736
+ """Destroy an event created by create_event."""
1737
+ if not event:
1738
+ return
1739
+ rt = _rt.gpu_runtime()
1740
+ backend = _rt.backend_table()
1741
+ fn_name = backend.get("event_destroy")
1742
+ if fn_name is None:
1743
+ return
1744
+ getattr(rt, fn_name)(ctypes.c_void_p(event))
1745
+
1746
+ @staticmethod
1747
+ def record_event(event: int, stream: int = 0):
1748
+ """Capture the current state of `stream` into `event`."""
1749
+ rt = _rt.gpu_runtime()
1750
+ backend = _rt.backend_table()
1751
+ fn_name = backend.get("event_record")
1752
+ if fn_name is None:
1753
+ return
1754
+ getattr(rt, fn_name)(
1755
+ ctypes.c_void_p(event), ctypes.c_void_p(stream))
1756
+
1757
+ @staticmethod
1758
+ def stream_wait_event(stream: int, event: int, flags: int = 0):
1759
+ """Make `stream` wait for `event` without blocking the host."""
1760
+ rt = _rt.gpu_runtime()
1761
+ backend = _rt.backend_table()
1762
+ fn_name = backend.get("stream_wait_event")
1763
+ if fn_name is None:
1764
+ return
1765
+ getattr(rt, fn_name)(
1766
+ ctypes.c_void_p(stream), ctypes.c_void_p(event),
1767
+ ctypes.c_uint(flags))
1768
+
1769
+ @staticmethod
1770
+ def memcpy_async(dst: int, src: int, nbytes: int,
1771
+ kind: int = 1, stream: int = 0):
1772
+ """Asynchronous memcpy on a specific stream.
1773
+
1774
+ kind: 0=H2H, 1=H2D, 2=D2H, 3=D2D (matches cudaMemcpyKind).
1775
+ For H2D to actually overlap with compute, `src` must be pinned.
1776
+ """
1777
+ if nbytes <= 0:
1778
+ return
1779
+ rt = _rt.gpu_runtime()
1780
+ backend = _rt.backend_table()
1781
+ fn_name = backend.get("memcpy_async")
1782
+ if fn_name is None:
1783
+ # Fallback to synchronous memcpy if async is unsupported.
1784
+ DeviceAllocator.memcpy(dst, src, nbytes, kind)
1785
+ return
1786
+ getattr(rt, fn_name)(
1787
+ ctypes.c_void_p(dst), ctypes.c_void_p(src),
1788
+ ctypes.c_size_t(nbytes), ctypes.c_int(kind),
1789
+ ctypes.c_void_p(stream))
1790
+
1791
+ @staticmethod
1792
+ def event_synchronize(event: int):
1793
+ """Block the host until `event` has been reached."""
1794
+ if not event:
1795
+ return
1796
+ rt = _rt.gpu_runtime()
1797
+ fn_name = _rt.backend_table().get("event_sync")
1798
+ if fn_name is None:
1799
+ DeviceAllocator.sync_device()
1800
+ return
1801
+ ret = getattr(rt, fn_name)(ctypes.c_void_p(event))
1802
+ if ret != 0:
1803
+ DeviceAllocator.clear_last_error() # a refused call must not poison the next one
1804
+ raise RuntimeError(f"event synchronize failed (error {ret})")
1805
+
1806
+ @staticmethod
1807
+ def event_elapsed_ms(start: int, end: int) -> float:
1808
+ """Milliseconds between two recorded events, measured ON THE DEVICE.
1809
+
1810
+ Both events MUST have been recorded on streams of the SAME device:
1811
+ the runtime's elapsed-time query is undefined across devices because
1812
+ each device has its own clock domain. To time a multi-device
1813
+ collective, record both events on one device's stream and make that
1814
+ stream `stream_wait_event` on the other devices' completion events —
1815
+ then the interval covers the critical path without ever comparing
1816
+ two clocks.
1817
+
1818
+ The caller is responsible for having synchronized `end`.
1819
+ """
1820
+ rt = _rt.gpu_runtime()
1821
+ fn_name = _rt.backend_table().get("event_elapsed")
1822
+ if fn_name is None:
1823
+ raise RuntimeError(
1824
+ f"event timing unsupported on backend "
1825
+ f"{_rt.gpu_backend()!r}")
1826
+ fn = getattr(rt, fn_name)
1827
+ fn.restype = ctypes.c_int
1828
+ out = ctypes.c_float()
1829
+ ret = fn(ctypes.byref(out), ctypes.c_void_p(start), ctypes.c_void_p(end))
1830
+ if ret != 0:
1831
+ DeviceAllocator.clear_last_error() # a refusal must not poison the next call
1832
+ raise RuntimeError(
1833
+ f"event elapsed failed (error {ret}) — the usual cause is "
1834
+ f"two events recorded on DIFFERENT devices, which has no "
1835
+ f"defined answer.")
1836
+ return float(out.value)
1837
+
1838
+ @staticmethod
1839
+ def can_access_peer(src_dev: int, dst_dev: int) -> bool:
1840
+ """Whether `src_dev` can address `dst_dev`'s memory directly.
1841
+
1842
+ False does not mean a copy will fail — it means the driver will
1843
+ stage it through host memory, at host-link speed rather than
1844
+ interconnect speed.
1845
+ """
1846
+ if src_dev == dst_dev:
1847
+ return True
1848
+ rt = _rt.gpu_runtime()
1849
+ fn_name = _rt.backend_table().get("device_can_access_peer")
1850
+ if fn_name is None:
1851
+ return False
1852
+ out = ctypes.c_int()
1853
+ ret = getattr(rt, fn_name)(
1854
+ ctypes.byref(out), ctypes.c_int(src_dev), ctypes.c_int(dst_dev))
1855
+ if ret != 0:
1856
+ DeviceAllocator.clear_last_error()
1857
+ return ret == 0 and bool(out.value)
1858
+
1859
+ # Pairs already granted, so the hot transfer path pays the query once.
1860
+ _peer_enabled: set = set()
1861
+ # Pairs the hardware refused, so a rig without peer links does not retry
1862
+ # on every single transfer.
1863
+ _peer_refused: set = set()
1864
+
1865
+ @staticmethod
1866
+ def ensure_peer_access(src_dev: int, dst_dev: int) -> bool:
1867
+ """Grant src -> dst once, then remember the answer.
1868
+
1869
+ Called on the cross-device transfer path, which is hot enough that
1870
+ asking the driver every time would cost more than it saves. Both
1871
+ outcomes are memoised: a rig whose cards have no direct links must
1872
+ not re-ask on every hand-off.
1873
+
1874
+ A `False` here is not an error — the copy still works, the driver
1875
+ just routes it through host memory. It is 2.4x slower at 4 KB and
1876
+ 8.7x slower at 34 MB on this rig (measured 2026-09-03).
1877
+ """
1878
+ if src_dev == dst_dev:
1879
+ return True
1880
+ key = (src_dev, dst_dev)
1881
+ if key in DeviceAllocator._peer_enabled:
1882
+ return True
1883
+ if key in DeviceAllocator._peer_refused:
1884
+ return False
1885
+ if os.environ.get("NBX_DISABLE_PEER_ACCESS") == "1":
1886
+ # Escape hatch and A/B control. Direct card-to-card access is a
1887
+ # driver-level grant; if a rig ever misbehaves under it, this
1888
+ # restores the host-staged route without a code change, and it is
1889
+ # how the speedup was measured against the old behaviour.
1890
+ DeviceAllocator._peer_refused.add(key)
1891
+ return False
1892
+ try:
1893
+ granted = DeviceAllocator.enable_peer_access(src_dev, dst_dev)
1894
+ except RuntimeError:
1895
+ granted = False
1896
+ (DeviceAllocator._peer_enabled if granted
1897
+ else DeviceAllocator._peer_refused).add(key)
1898
+ return granted
1899
+
1900
+ @staticmethod
1901
+ def enable_peer_access(src_dev: int, dst_dev: int) -> bool:
1902
+ """Let `src_dev` reach `dst_dev` over the interconnect directly.
1903
+
1904
+ Direction matters: this grants src -> dst only, so a bidirectional
1905
+ pair needs both calls. Returns True if access is in force afterwards
1906
+ (including when it already was), False if the hardware cannot do it.
1907
+
1908
+ Why this exists: a cross-device `memcpy(kind=3)` with peer access
1909
+ OFF is bounced through host memory by the driver. On this rig every
1910
+ GPU pair reports NV2 (two bonded NVLinks) and the copy still would
1911
+ not use them. Measured cost of the difference:
1912
+ `validation_outputs/tp_collective_latency_2026_09_03/`.
1913
+ """
1914
+ if src_dev == dst_dev:
1915
+ return True
1916
+ if not DeviceAllocator.can_access_peer(src_dev, dst_dev):
1917
+ return False
1918
+ rt = _rt.gpu_runtime()
1919
+ fn_name = _rt.backend_table().get("device_enable_peer_access")
1920
+ if fn_name is None:
1921
+ return False
1922
+ previous = DeviceAllocator.get_device()
1923
+ try:
1924
+ DeviceAllocator.set_device(src_dev)
1925
+ ret = getattr(rt, fn_name)(ctypes.c_int(dst_dev), ctypes.c_uint(0))
1926
+ finally:
1927
+ DeviceAllocator.set_device(previous)
1928
+ # 704 = cudaErrorPeerAccessAlreadyEnabled. Idempotence is the point:
1929
+ # callers enable defensively and must not have to track state.
1930
+ if ret in (0, 704):
1931
+ return True
1932
+ raise RuntimeError(
1933
+ f"enable peer access {src_dev}->{dst_dev} failed (error {ret})")
1934
+
1935
+ @staticmethod
1936
+ def ensure_triton_device(device_idx: int):
1937
+ """Set GPU device for Triton JIT context. Zero torch. Universal.
1938
+
1939
+ Uses cudaSetDevice (runtime) + Triton's own driver API.
1940
+ Does NOT call cuCtxSetCurrent — that conflicts with the runtime
1941
+ context on multi-GPU systems.
1942
+
1943
+ C2 fast path: called before every kernel launch, so when the
1944
+ requested device is already fully current (see the
1945
+ per-thread device-cache invariant) the pair of
1946
+ driver calls is skipped. Any device change outside this
1947
+ function must invalidate the cache (set_device does so
1948
+ automatically; raw switches call
1949
+ invalidate_current_device_cache()).
1950
+ """
1951
+ if device_idx == _rt._cached_device_idx():
1952
+ return
1953
+ # 1. Set runtime device (cudaSetDevice / hipSetDevice)
1954
+ _rt.device_memory().set_device(device_idx)
1955
+ # 2. Tell the launcher which device to target. The NeuroBrix launcher
1956
+ # binds the runtime's primary context of the current device at each
1957
+ # launch; only the differential arm (NBX_LAUNCHER=triton) still
1958
+ # drives upstream's launcher, whose driver must be told (and whose
1959
+ # backend probes import torch — R33, universal since 2026-09-05).
1960
+ if os.environ.get("NBX_LAUNCHER", "nbx").lower() == "triton":
1961
+ import triton.runtime.driver
1962
+ active = triton.runtime.driver.active
1963
+ # A single-device backend has nothing to set and need not
1964
+ # implement the setter: triton-msl's MetalDriver provides
1965
+ # `get_current_device` and no `set_current_device` (measured
1966
+ # 2026-09-05), and this arm is exactly how the Metal A/B is run.
1967
+ # Falling through would raise AttributeError at the first launch;
1968
+ # assuming it worked would risk launching on a device nobody
1969
+ # selected. So the absence is handled by VERIFYING instead.
1970
+ set_current = getattr(active, "set_current_device", None)
1971
+ if set_current is not None:
1972
+ set_current(device_idx)
1973
+ else:
1974
+ current = active.get_current_device()
1975
+ if current != device_idx:
1976
+ raise RuntimeError(
1977
+ f"Triton backend "
1978
+ f"{active.get_current_target().backend!r} exposes no "
1979
+ f"set_current_device and reports device {current}, "
1980
+ f"but device {device_idx} was requested. The kernel "
1981
+ f"would launch on the wrong device, so this refuses "
1982
+ f"instead.")
1983
+ # Cache only after BOTH calls succeeded (this thread's state).
1984
+ _rt._DEVICE_CACHE.idx = device_idx
1985
+
1986
+
1987
+ #: the pool of every device's segments (memory/pool.py), its block sizes mirrored in the per-pointer tables
1988
+ DeviceAllocator._POOL = SegmentPool(on_block=DeviceAllocator._block_reported)