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,273 @@
1
+ """Slab allocators: a run's device allocations carved out of one block per device, and the interceptor that serves
2
+ them (moved from the engine's frozen-plan replayer, M2).
3
+
4
+ A replay recording (the engine's `triton/replay.py`) runs a step once with every allocation it makes served from a slab,
5
+ so that the addresses its launches recorded stay valid when they are replayed: the replay issues the same launches in
6
+ the same order, so every reuse the recorded run made is safe at replay. `SlabInterceptor` stands in front of the
7
+ allocator (`DeviceAllocator.install_interceptor`) and routes: while recording, to the slab of the requested device;
8
+ between recordings, to the recycled per-step slab when one is in force; else to the allocator itself, measuring the
9
+ watermark a slab will be sized from. Torch-free.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from typing import Dict, List, Optional, Tuple
14
+
15
+ from nbxtensor.memory.allocator import AllocatorInterceptor, DeviceAllocator
16
+
17
+ ALIGN = 512 # matches typical device allocation granularity
18
+
19
+ # Slabs with outstanding carved allocations — the free path consults this PERMANENTLY (not only while recording): a
20
+ # slab-carved tensor may die on any later run when its arena slot is overwritten, and its finalizer must return the
21
+ # range to the slab, never free a slab-interior pointer to the driver.
22
+ ACTIVE_SLABS: List["SlabAllocator"] = []
23
+
24
+
25
+ def _carve(free_list: List[Tuple[int, int]], need: int) -> Optional[int]:
26
+ """First fit: the offset of the first free range holding `need` aligned bytes, the range shrunk; None if none."""
27
+ for i, (off, sz) in enumerate(free_list):
28
+ if sz >= need:
29
+ if sz == need:
30
+ free_list.pop(i)
31
+ else:
32
+ free_list[i] = (off + need, sz - need)
33
+ return off
34
+ return None
35
+
36
+
37
+ def _give_back(free_list: List[Tuple[int, int]], off: int, sz: int) -> List[Tuple[int, int]]:
38
+ """The free list with [off, off + sz) returned, sorted and its neighbours merged."""
39
+ free_list.append((off, sz))
40
+ free_list.sort()
41
+ merged: List[Tuple[int, int]] = []
42
+ for o, s_ in free_list:
43
+ if merged and merged[-1][0] + merged[-1][1] == o:
44
+ merged[-1] = (merged[-1][0], merged[-1][1] + s_)
45
+ else:
46
+ merged.append((o, s_))
47
+ return merged
48
+
49
+
50
+ def first_fit_extent(events: List[Tuple[int, int, int]]) -> int:
51
+ """The slab a run needs: the highest offset `SlabAllocator`'s first fit reaches when it serves `events` — the run's
52
+ pooled allocations and frees in order, (+1, key, aligned bytes) and (-1, key, 0) — from a slab without end. It is
53
+ at least the live peak and at most the sum: a first fit leaves holes a later, larger block cannot use. Sized from
54
+ the live peak alone, Allegro-TI2V's encoder tile (9.0 GB live) was short by 22.6 GB at its recording, the retry
55
+ then took 31.7 GB of a 32 GB card for a run that never holds more than 8.6 GB (2026-11-04)."""
56
+ end = 1 << 62
57
+ free_list: List[Tuple[int, int]] = [(0, end)]
58
+ at: Dict[int, Tuple[int, int]] = {}
59
+ top = 0
60
+ for sign, key, need in events:
61
+ if sign > 0:
62
+ off = _carve(free_list, need)
63
+ at[key] = (off, need)
64
+ top = max(top, off + need)
65
+ elif key in at:
66
+ free_list = _give_back(free_list, *at.pop(key))
67
+ return top
68
+
69
+
70
+ class SlabAllocator:
71
+ """First-fit free-list allocator over ONE device slab.
72
+
73
+ The recording run's malloc/free sequence drives carving/reuse; the
74
+ replay issues the same launches in the same order, so every reuse
75
+ the recorded run performed is serialization-safe at replay.
76
+ """
77
+
78
+ def __init__(self, nbytes: int, dev_idx: Optional[int]) -> None:
79
+ self.size = int(nbytes)
80
+ self.dev_idx = dev_idx
81
+ self.base = DeviceAllocator.malloc_direct(self.size, dev_idx)
82
+ self.free_list: List[Tuple[int, int]] = [(0, self.size)]
83
+ self.allocs: Dict[int, Tuple[int, int]] = {} # ptr -> (off, sz)
84
+ self.shortfall = 0 # aligned bytes we could not serve
85
+ # A retired slab (failed recording attempt / invalidated plan)
86
+ # must NOT cudaFree while tensors carved from it are alive:
87
+ # their finalizers free slab-interior pointers, which the seam
88
+ # routes back here. Actual release happens when the last
89
+ # outstanding alloc returns. Probe 2 proved the hazard: an
90
+ # eager release left the text-encoder outputs dangling
91
+ # ("Pointer argument cannot be accessed" + invalid cudaFree of
92
+ # a slab-interior pointer).
93
+ self.retired = False
94
+ ACTIVE_SLABS.append(self)
95
+
96
+ def malloc(self, nbytes: int) -> int:
97
+ need = (int(nbytes) + ALIGN - 1) // ALIGN * ALIGN
98
+ off = _carve(self.free_list, need)
99
+ if off is not None:
100
+ ptr = self.base + off
101
+ self.allocs[ptr] = (off, need)
102
+ return ptr
103
+ self.shortfall += need
104
+ return 0
105
+
106
+ def free(self, ptr: int) -> bool:
107
+ entry = self.allocs.pop(ptr, None)
108
+ if entry is None:
109
+ return False
110
+ self.free_list = _give_back(self.free_list, *entry)
111
+ if self.retired and not self.allocs:
112
+ self._release_now()
113
+ return True
114
+
115
+ def retire(self) -> None:
116
+ """Deferred release: cudaFree only after the last outstanding
117
+ carved allocation has been returned by its owner's finalizer."""
118
+ self.retired = True
119
+ if not self.allocs:
120
+ self._release_now()
121
+
122
+ def _release_now(self) -> None:
123
+ if self.base:
124
+ DeviceAllocator.free_direct(self.base)
125
+ self.base = 0
126
+ if self in ACTIVE_SLABS:
127
+ ACTIVE_SLABS.remove(self)
128
+
129
+
130
+ class StepSlab(SlabAllocator):
131
+ """Per-sequence slab RECYCLED at every decode step (P-REPLAY-KV-
132
+ DECODE B1). reset() returns the whole range to the free list so
133
+ the next step's allocations carve IDENTICAL addresses (the decode
134
+ graph's allocation order is structurally constant — census-
135
+ proven); allocations from previous steps become 'stale' and their
136
+ later finalizer frees are absorbed silently (the range is already
137
+ recycled — safe because each step's consumers run before the next
138
+ step begins in the synchronous decode timeline)."""
139
+
140
+ def __init__(self, nbytes: int, dev_idx: Optional[int]) -> None:
141
+ super().__init__(nbytes, dev_idx)
142
+ self.stale: set = set()
143
+ self.carve_sizes: List[int] = [] # diagnosis: per-step size seq
144
+ self.prev_carve: Optional[List[int]] = None
145
+
146
+ def malloc(self, nbytes: int) -> int:
147
+ """PURE BUMP carving — mid-step frees are absorbed without
148
+ recycling (see free below), so placement is a pure function of
149
+ the allocation-size sequence (measured identical step-to-step)
150
+ and IMMUNE to free-timing nondeterminism (GC-cycle finalizers
151
+ fire at variable points; first-fit reuse turned that into
152
+ per-step placement churn — measured: identical size sequences,
153
+ differing offsets)."""
154
+ self.carve_sizes.append(int(nbytes))
155
+ need = (int(nbytes) + ALIGN - 1) // ALIGN * ALIGN
156
+ off, sz = self.free_list[0]
157
+ if sz < need:
158
+ self.shortfall += need
159
+ return 0
160
+ self.free_list[0] = (off + need, sz - need)
161
+ ptr = self.base + off
162
+ self.allocs[ptr] = (off, need)
163
+ return ptr
164
+
165
+ def reset(self) -> None:
166
+ self.prev_carve = self.carve_sizes
167
+ self.carve_sizes = []
168
+ self.stale.update(self.allocs.keys())
169
+ self.allocs.clear()
170
+ self.free_list = [(0, self.size)]
171
+
172
+ def free(self, ptr: int) -> bool:
173
+ """Absorb without recycling: the range stays consumed until the
174
+ next reset (bump purity beats intra-step reuse)."""
175
+ if ptr in self.stale:
176
+ self.stale.discard(ptr)
177
+ return True
178
+ if ptr in self.allocs:
179
+ self.allocs.pop(ptr)
180
+ return True
181
+ return False
182
+
183
+
184
+ class SlabInterceptor(AllocatorInterceptor):
185
+ """The allocator's interceptor for slab-served runs. Its state is read and set by the replayer that owns it:
186
+
187
+ * `recording` with `slabs` (one per device) or `slab`: every pooled allocation is carved from the slab of the
188
+ REQUESTED device (serving a device-1 tensor from device 0's slab yields a pointer into the wrong card's memory —
189
+ a wrong result or an illegal access, never a clear error); a slab that cannot serve it, or a device without
190
+ one, refuses the recording through `slab_refused(reason)` and the allocator serves it.
191
+ * `stab_slab`: the recycled per-step slab in force; an allocation over it is served by the allocator (address
192
+ stability degrades for that tensor only).
193
+ * `measuring`: the allocator serves, and the aligned bytes are counted per pointer and device (`live`,
194
+ `high_water`, `live_by_dev`, `high_water_by_dev`, `total_measured`) — what a slab is sized from.
195
+
196
+ A block outside the pool (`pooled=False`, a weight arena) is never a slab's. A free returns to the slab that
197
+ carved the pointer, if any, before the allocator sees it."""
198
+
199
+ def __init__(self) -> None:
200
+ self.measuring = False
201
+ self.recording = False
202
+ self.slab: Optional[SlabAllocator] = None
203
+ self.slabs: Dict[int, SlabAllocator] = {}
204
+ self.stab_slab: Optional["StepSlab"] = None
205
+ # Per-device measurement: one high-water mark cannot size slabs for a run allocating on several cards without
206
+ # sizing each to the SUM.
207
+ self.live_by_dev: Dict[int, int] = {}
208
+ self.high_water_by_dev: Dict[int, int] = {}
209
+ self.measured_dev: Dict[int, int] = {}
210
+ # measuring counters (aligned bytes)
211
+ self.live = 0
212
+ self.high_water = 0
213
+ self.measured: Dict[int, int] = {} # ptr -> aligned size
214
+ self.total_measured = 0 # cumulative (never decremented)
215
+ # the run's pooled allocations and frees in order, per device (`first_fit_extent`: the slab they need)
216
+ self.events_by_dev: Dict[int, List[Tuple[int, int, int]]] = {}
217
+ self._seq = 0
218
+ self._key_of_ptr: Dict[int, int] = {}
219
+
220
+ def slab_refused(self, reason: str) -> None:
221
+ """A recording the slabs cannot serve: the owner decides (a replayer abandons the plan, by name)."""
222
+ raise NotImplementedError(f"{type(self).__qualname__} does not say what a refused slab does: {reason}")
223
+
224
+ def malloc(self, direct, nbytes, dev_idx, pooled):
225
+ if not pooled:
226
+ return direct(nbytes, dev_idx, False)
227
+ if self.recording and (self.slab is not None or self.slabs):
228
+ _dev = dev_idx if dev_idx is not None else DeviceAllocator.get_device()
229
+ _slab = self.slabs.get(_dev, self.slab)
230
+ if _slab is not None:
231
+ ptr = _slab.malloc(nbytes)
232
+ if ptr:
233
+ return ptr
234
+ self.slab_refused(f"slab exhausted on device {_dev} (short {_slab.shortfall} B)")
235
+ else:
236
+ self.slab_refused(f"no slab recorded for device {_dev}")
237
+ return direct(nbytes, dev_idx, True)
238
+ if self.stab_slab is not None:
239
+ ptr = self.stab_slab.malloc(nbytes)
240
+ if ptr:
241
+ return ptr
242
+ ptr = direct(nbytes, dev_idx, True)
243
+ if self.measuring and ptr:
244
+ need = (int(nbytes) + ALIGN - 1) // ALIGN * ALIGN
245
+ self.measured[ptr] = need
246
+ self.total_measured += need
247
+ self.live += need
248
+ if self.live > self.high_water:
249
+ self.high_water = self.live
250
+ _d = dev_idx if dev_idx is not None else DeviceAllocator.get_device()
251
+ self.measured_dev[ptr] = _d
252
+ self._seq += 1 # a key per allocation: a pointer the allocator reuses is a new block
253
+ self._key_of_ptr[ptr] = self._seq
254
+ self.events_by_dev.setdefault(_d, []).append((1, self._seq, need))
255
+ self.live_by_dev[_d] = self.live_by_dev.get(_d, 0) + need
256
+ if self.live_by_dev[_d] > self.high_water_by_dev.get(_d, 0):
257
+ self.high_water_by_dev[_d] = self.live_by_dev[_d]
258
+ return ptr
259
+
260
+ def free(self, direct, ptr):
261
+ for slab in ACTIVE_SLABS:
262
+ if slab.free(ptr):
263
+ return # returned to its slab (maybe releasing it)
264
+ if self.measuring:
265
+ need = self.measured.pop(ptr, 0)
266
+ self.live -= need
267
+ _d = self.measured_dev.pop(ptr, None)
268
+ if _d is not None:
269
+ self.live_by_dev[_d] = self.live_by_dev.get(_d, 0) - need
270
+ _k = self._key_of_ptr.pop(ptr, None)
271
+ if _k is not None:
272
+ self.events_by_dev.setdefault(_d, []).append((-1, _k, 0))
273
+ return direct(ptr)
@@ -0,0 +1,122 @@
1
+ """The expandable range's chunk, measured by replaying a REAL run's allocations (the supervisor 2026-11-02 17:15).
2
+
3
+ A run's mapping cost is what its refusals' reclaims map again (SANA-Video_2B_720p on a 16 GB V100: 89 GB mapped over a
4
+ run at a 2 MiB chunk — registres/benchmarks/vmm_v100.md), and only the run's own allocation order produces those
5
+ refusals; a synthetic churn does not (it always favours the largest chunk). So the library carries recorded traces as
6
+ data (`traces/*.tsv`: every allocation and free of one gate cell, in order, by id and bytes — no model, no pointer),
7
+ and the chunk probe replays one through THIS allocator, on the device, under the card size the trace was recorded on
8
+ (`card_mb`, the imposed limit — the trace presses that class on any larger card), once with the expandable range off
9
+ (the driver segments: R16 OFF) and once per candidate chunk, each in its own process (a clean allocator).
10
+
11
+ The chunk is the smallest candidate whose replay costs no more than R16 OFF's replay plus OFF's own spread (max -
12
+ median over the repetitions): the expandable range then holds what it saves (the 32g replay: 8 GB less at its peak)
13
+ without paying for it in time. None when no candidate does — said, and the caller keeps the driver segments.
14
+
15
+ Torch-free, numpy-free."""
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import statistics
21
+ import subprocess
22
+ import sys
23
+ import time
24
+ from pathlib import Path
25
+ from typing import Dict, List, Optional, Sequence, Tuple
26
+
27
+ TRACES_DIR = Path(__file__).parent / "traces"
28
+ _MARK = "NBX-TRACE-REPLAY " # the child's result line, whatever else it prints
29
+
30
+
31
+ def traces() -> List[Path]:
32
+ """The traces the library carries, by name."""
33
+ return sorted(TRACES_DIR.glob("*.tsv"))
34
+
35
+
36
+ def load_trace(path) -> Tuple[Dict[str, str], List[Tuple[str, int, int]]]:
37
+ """(the trace's header facts `# key: value`, its events (kind, id, nbytes)). A trace without `card_mb` or without
38
+ one event is refused by name."""
39
+ meta: Dict[str, str] = {}
40
+ events: List[Tuple[str, int, int]] = []
41
+ for line in open(path):
42
+ if line.startswith("#"):
43
+ key, sep, value = line[1:].strip().partition(": ")
44
+ if sep and " " not in key:
45
+ meta[key] = value.strip()
46
+ continue
47
+ parts = line.rstrip("\n").split("\t")
48
+ if len(parts) == 3 and parts[0] in ("M", "A", "F"):
49
+ events.append((parts[0], int(parts[1]), int(parts[2])))
50
+ if "card_mb" not in meta or not events:
51
+ raise RuntimeError(f"ZERO FALLBACK: the allocation trace {path} states no card_mb or holds no event")
52
+ return meta, events
53
+
54
+
55
+ def replay_trace(events: Sequence[Tuple[str, int, int]], device: int, chunk: int, limit_bytes: int) -> Dict[str, float]:
56
+ """Replay `events` through this process's allocator on `device` under `limit_bytes`, the large class in an
57
+ expandable range of `chunk` bytes (0: the driver segments). The wall seconds, the mapping seconds, the reclaims and
58
+ the peak held."""
59
+ from nbxtensor.memory.allocator import DeviceAllocator as DA
60
+ DA.set_device(device)
61
+ DA.use_expandable(device, chunk, range_bytes=limit_bytes)
62
+ DA.set_limit(limit_bytes, device)
63
+ live: Dict[int, int] = {}
64
+ t0 = time.perf_counter()
65
+ for kind, key, nbytes in events:
66
+ if kind == "F":
67
+ ptr = live.pop(key, None)
68
+ if ptr is not None:
69
+ DA.free_cuda(ptr)
70
+ elif nbytes:
71
+ live[key] = DA.malloc_cuda(nbytes, device, pooled=(kind == "M"))
72
+ DA.device_synchronize(device)
73
+ seconds = time.perf_counter() - t0
74
+ st = DA._POOL.stats
75
+ return {"seconds": seconds, "map_seconds": st.get("range_map_us", 0) / 1e6,
76
+ "reclaims": st.get("range_reclaims", 0), "peak_bytes": DA._cuda_peak_bytes.get(device, 0)}
77
+
78
+
79
+ def _child(trace: str, device: int, chunk: int) -> Dict[str, float]:
80
+ """One replay in a fresh process (no allocator state carried between candidates)."""
81
+ code = ("import json, sys; from nbxtensor.memory import trace_replay as T; "
82
+ "m, ev = T.load_trace(sys.argv[1]); "
83
+ "print('" + _MARK + "' + json.dumps(T.replay_trace(ev, int(sys.argv[2]), int(sys.argv[3]), "
84
+ "int(m['card_mb']) << 20)))")
85
+ # the replay measures the allocator, never its diagnostics: the parent's run-level switches stay out of it
86
+ env = {k: v for k, v in os.environ.items() if not k.startswith(("NBX_ALLOC_STATS", "NBX_MALLOC_TRACE"))}
87
+ r = subprocess.run([sys.executable, "-c", code, trace, str(device), str(chunk)], capture_output=True, text=True,
88
+ env=env)
89
+ line = next((l for l in r.stdout.splitlines() if l.startswith(_MARK)), None)
90
+ if r.returncode != 0 or line is None:
91
+ raise RuntimeError(f"the allocation-trace replay of {Path(trace).name} at chunk {chunk} failed: "
92
+ f"{(r.stderr or r.stdout)[-400:]}")
93
+ return json.loads(line[len(_MARK):])
94
+
95
+
96
+ def chunk_by_replay(device: int, granularity: int, ks: Sequence[int], reps: int,
97
+ trace: Optional[str] = None) -> Tuple[Optional[int], Dict[int, List[float]]]:
98
+ """(the chunk, every candidate's replay seconds — 0 is R16 OFF). The smallest granularity x k whose median replay
99
+ costs no more than OFF's median plus OFF's spread; None when none does. `trace`: one of `traces()` (the only one
100
+ the library carries when it carries one; several = the caller names it)."""
101
+ if trace is None:
102
+ carried = traces()
103
+ if len(carried) != 1:
104
+ raise RuntimeError(f"ZERO FALLBACK: the library carries {len(carried)} allocation traces "
105
+ f"({', '.join(p.name for p in carried)}): the chunk probe names the one it replays")
106
+ trace = str(carried[0])
107
+ # every replay runs in a child sized at the recorded card: what THIS process caches on the device is given back
108
+ # first, or the child finds the card short by it (a test process's pools after the kernels suite: 16 GB, the
109
+ # replay out of memory at chunk 0 — order-dependent, 2026-11-03)
110
+ from nbxtensor.memory.allocator import DeviceAllocator as DA
111
+ DA.device_synchronize(device)
112
+ if DA._range_vmm(device) is not None:
113
+ DA._range_reclaim(device, None)
114
+ DA._pool_flush(device)
115
+ times: Dict[int, List[float]] = {0: [_child(trace, device, 0)["seconds"] for _ in range(reps)]}
116
+ off = times[0]
117
+ bound = statistics.median(off) + (max(off) - statistics.median(off))
118
+ for chunk in [granularity * int(k) for k in sorted(ks)]: # ascending: the first within OFF's noise is the chunk
119
+ times[chunk] = [_child(trace, device, chunk)["seconds"] for _ in range(reps)]
120
+ if statistics.median(times[chunk]) <= bound:
121
+ return chunk, times
122
+ return None, times