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,507 @@
1
+ """The caching pool of the device allocator: its segments, their blocks, the free index — and the same rules replayed
2
+ as a pure function of an allocation sequence (`simulate`), which is how a planner prices what the allocator holds
3
+ beyond the tensors alive (M3, docs/nbxtensor/COST.md section 2: the 1.3 factor it replaces).
4
+
5
+ Every driver allocation the pool takes is a SEGMENT made of BLOCKS chained in address order (the method of torch's
6
+ caching allocator, S4a 2026-10-27), in one of two SIZE CLASSES: a request of at most `POOL_SMALL_BYTES` is served only
7
+ from small segments of `POOL_SMALL_SEGMENT_BYTES`, a larger one only from large segments grown to the request (torch's
8
+ kSmallSize / kSmallBuffer). A request takes the smallest free block of its class that holds it (rounded up to `align`)
9
+ and splits off the rest when it is at least `split_min` — in the large class only when what is left is at most the
10
+ request itself (the half rule), so a block is never pinned by something smaller than its remainder. A freed block
11
+ merges with its free neighbours; only a segment wholly free returns to the driver — over the parked cap, on a refused
12
+ malloc, on a flush. A wholly free segment stays parked across a growth: it is the next request of its size's block.
13
+
14
+ Why the classes and the half rule (Allegro triton on a V100-16GB, gate of 2026-11-01): with one class and a free split,
15
+ a 17 KB scalar took a freed 2.5 GB block and pinned its remainder alive, a 632 MB request did the same to the next —
16
+ 4.4 GB of free blocks in pinned segments beside 9.7 GB alive refused a 2.5 GB gelu. The real trace replayed through
17
+ these rules (campaign 2026_11_01_gate_nbxtensor, trace_allegro/replay.py; also mochi-1-preview 32g and Kokoro) completes
18
+ with the least held of every pooled rule tried, and 164e89dc's flush of the pool before every growth — which only
19
+ emptied the small free segments the scalars would have used — is withdrawn with it.
20
+
21
+ Mirrored, a block's size is reported to `on_block(ptr, size, dev)` (`size` None: the block is gone) — the allocator
22
+ keeps its per-pointer tables by it.
23
+
24
+ Torch-free, device-free: the driver is reached only through the callable a caller passes in (`flush`, `evict`).
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import bisect
29
+ from dataclasses import dataclass
30
+ from typing import Callable, Dict, Iterable, List, Optional, Tuple
31
+
32
+ #: a request is rounded up to this many bytes before it is matched (the allocator's `_POOL_ALIGN`)
33
+ POOL_ALIGN = 512
34
+ #: the least remainder a block is split for; a smaller one stays with the block it was cut from
35
+ POOL_SPLIT_MIN = 1 << 20
36
+ #: the largest request of the small class (torch's caching allocator: kSmallSize)
37
+ POOL_SMALL_BYTES = 1 << 20
38
+ #: the segment a small request grows the device by, carved for the small requests that follow (kSmallBuffer)
39
+ POOL_SMALL_SEGMENT_BYTES = 1 << 21
40
+
41
+ LARGE, SMALL = 0, 1
42
+
43
+
44
+ def _no_report(ptr: int, size: Optional[int], dev: int) -> None:
45
+ return None
46
+
47
+
48
+ class SegmentPool:
49
+ """The free blocks of every device's segments, and the blocks in use inside them."""
50
+
51
+ def __init__(self, align: int = POOL_ALIGN, split_min: int = POOL_SPLIT_MIN, small: int = POOL_SMALL_BYTES,
52
+ small_segment: int = POOL_SMALL_SEGMENT_BYTES,
53
+ on_block: Callable[[int, Optional[int], int], None] = _no_report) -> None:
54
+ self.align = int(align)
55
+ self.split_min = int(split_min)
56
+ self.small = int(small)
57
+ self.small_segment = int(small_segment)
58
+ self.on_block = on_block
59
+ self.blocks: Dict[int, list] = {} # ptr -> [size, prev ptr, next ptr, free, device, class]
60
+ self.free: Dict[int, Dict[int, list]] = {} # device -> {size: [ptr, ...]} (the FREE blocks, by size)
61
+ self.sizes: Dict[int, list] = {} # device -> sorted sizes present in free[dev]
62
+ self._cls_free: Dict[Tuple[int, int], Dict[int, list]] = {} # (device, class) -> {size: [ptr, ...]}
63
+ self._cls_sizes: Dict[Tuple[int, int], list] = {} # (device, class) -> sorted sizes
64
+ self.alloc_size: Dict[int, int] = {} # ptr -> the block's size, for the caller of the last take
65
+ self.cached: Dict[int, int] = {} # device -> bytes parked in free blocks
66
+ self.cached_peak: Dict[int, int] = {} # device -> peak of the above
67
+ self.stats: Dict[str, int] = {"exact": 0, "fit": 0, "miss": 0, "slack_total": 0, "slack_max": 0,
68
+ "flushes": 0, "flushed_bytes": 0, "evictions": 0, "evicted_bytes": 0}
69
+ #: device -> its expandable ranges (`Range`), the large class's address space where the device maps chunks
70
+ #: (`memory.vmm`); none: the large class grows by driver segments as the small one does
71
+ self.ranges: Dict[int, List["Range"]] = {}
72
+
73
+ # -- the classes ---------------------------------------------------------------------------------------------------
74
+
75
+ def cls(self, nbytes: int) -> int:
76
+ """The size class of a request: SMALL up to `small` bytes, LARGE beyond."""
77
+ return SMALL if int(nbytes) <= self.small else LARGE
78
+
79
+ def grow_size(self, nbytes: int) -> int:
80
+ """The bytes the device grows by for a request the pool cannot serve: a small segment for a small request, the
81
+ request itself (rounded up to `align`) for a large one."""
82
+ if self.cls(nbytes) == SMALL:
83
+ return self.small_segment
84
+ return -(-int(nbytes) // self.align) * self.align
85
+
86
+ # -- the free index ------------------------------------------------------------------------------------------------
87
+
88
+ @staticmethod
89
+ def _add(index: dict, sizes: dict, key, ptr: int, size: int) -> None:
90
+ per = index.setdefault(key, {})
91
+ lst = per.get(size)
92
+ if lst is None:
93
+ per[size] = [ptr]
94
+ bisect.insort(sizes.setdefault(key, []), size)
95
+ else:
96
+ lst.append(ptr)
97
+
98
+ @staticmethod
99
+ def _remove(index: dict, sizes: dict, key, ptr: int, size: int) -> None:
100
+ per = index.get(key, {})
101
+ lst = per.get(size)
102
+ lst.remove(ptr)
103
+ if not lst:
104
+ del per[size]
105
+ sz = sizes[key]
106
+ sz.pop(bisect.bisect_left(sz, size))
107
+
108
+ def _index_add(self, dev: int, ptr: int, size: int) -> None:
109
+ self._add(self.free, self.sizes, dev, ptr, size)
110
+ self._add(self._cls_free, self._cls_sizes, (dev, self.blocks[ptr][5]), ptr, size)
111
+
112
+ def _index_remove(self, dev: int, ptr: int, size: int) -> None:
113
+ self._remove(self.free, self.sizes, dev, ptr, size)
114
+ self._remove(self._cls_free, self._cls_sizes, (dev, self.blocks[ptr][5]), ptr, size)
115
+
116
+ # -- blocks --------------------------------------------------------------------------------------------------------
117
+
118
+ def take(self, dev: int, nbytes: int) -> Optional[int]:
119
+ """The smallest free block of the request's class on `dev` that holds `nbytes` (rounded up to `align`), split
120
+ when what is left is at least `split_min` — and, in the large class, at most the request; None on a miss (no
121
+ such block, or the only ones would be left with more free than the request pins). The block's size is left in
122
+ `alloc_size[ptr]` for the caller (it may exceed the request by less than the split threshold)."""
123
+ st = self.stats
124
+ cls = self.cls(nbytes)
125
+ sizes = self._cls_sizes.get((dev, cls))
126
+ if not sizes:
127
+ st["miss"] += 1
128
+ return None
129
+ want = -(-int(nbytes) // self.align) * self.align
130
+ k = bisect.bisect_left(sizes, want)
131
+ if k >= len(sizes):
132
+ st["miss"] += 1
133
+ return None
134
+ sz = sizes[k]
135
+ ptr = self._cls_free[(dev, cls)][sz][-1]
136
+ if (cls == LARGE and sz - want >= self.split_min and sz - want > want
137
+ and not (self.ranges and _range_of(self, dev, ptr) is not None)):
138
+ # the half rule: a larger block would leave even more pinned — in a driver segment; in an expandable range
139
+ # a remainder pins no page, its whole chunks are unmapped at a reclaim (R16)
140
+ st["miss"] += 1
141
+ return None
142
+ self._index_remove(dev, ptr, sz)
143
+ blk = self.blocks[ptr]
144
+ blk[3] = False
145
+ if sz - want >= self.split_min:
146
+ rest = ptr + want
147
+ self.blocks[rest] = [sz - want, ptr, blk[2], True, dev, cls]
148
+ if blk[2] is not None:
149
+ self.blocks[blk[2]][1] = rest
150
+ blk[0], blk[2] = want, rest
151
+ if self.ranges:
152
+ r = _range_of(self, dev, ptr)
153
+ if r is not None and r.last == ptr:
154
+ r.last = rest
155
+ self.on_block(rest, sz - want, dev)
156
+ self._index_add(dev, rest, sz - want)
157
+ st["fit"] += 1
158
+ else:
159
+ st["exact" if sz == int(nbytes) else "fit"] += 1
160
+ if sz > int(nbytes):
161
+ st["slack_total"] += sz - int(nbytes)
162
+ st["slack_max"] = max(st["slack_max"], sz - int(nbytes))
163
+ self.alloc_size[ptr] = blk[0]
164
+ self.cached[dev] = self.cached.get(dev, 0) - blk[0]
165
+ return ptr
166
+
167
+ def add_free_segment(self, dev: int, ptr: int, nbytes: int, request: int) -> None:
168
+ """A driver allocation of `nbytes` grown for `request` (`grow_size`) joins the pool as a one-block FREE segment
169
+ of the request's class, parked; the caller takes the request from it next."""
170
+ cls = self.cls(request)
171
+ self.blocks[ptr] = [int(nbytes), None, None, True, dev, cls]
172
+ self._index_add(dev, ptr, int(nbytes))
173
+ self.cached[dev] = self.cached.get(dev, 0) + int(nbytes)
174
+
175
+ def release(self, ptr: int) -> int:
176
+ """`ptr`'s block is free: merged with its free neighbours and indexed; returns the bytes it adds to the pool's
177
+ parked total (`cached` is the caller's to update, with its peak: `park`)."""
178
+ blk = self.blocks[ptr]
179
+ dev = blk[4]
180
+ added = blk[0]
181
+ blk[3] = True
182
+ nxt = blk[2]
183
+ r = _range_of(self, dev, ptr) if self.ranges else None
184
+ if nxt is not None and self.blocks[nxt][3]:
185
+ self._index_remove(dev, nxt, self.blocks[nxt][0])
186
+ nb = self.blocks.pop(nxt)
187
+ if r is not None and r.last == nxt:
188
+ r.last = ptr
189
+ self.on_block(nxt, None, dev)
190
+ blk[0] += nb[0]
191
+ blk[2] = nb[2]
192
+ if nb[2] is not None:
193
+ self.blocks[nb[2]][1] = ptr
194
+ prv = blk[1]
195
+ if prv is not None and self.blocks[prv][3]:
196
+ pb = self.blocks[prv]
197
+ self._index_remove(dev, prv, pb[0])
198
+ pb[0] += blk[0]
199
+ pb[2] = blk[2]
200
+ if blk[2] is not None:
201
+ self.blocks[blk[2]][1] = prv
202
+ self.blocks.pop(ptr)
203
+ self.on_block(ptr, None, dev)
204
+ if r is not None and r.last == ptr:
205
+ r.last = prv
206
+ ptr, blk = prv, pb
207
+ self.on_block(ptr, blk[0], dev)
208
+ self._index_add(dev, ptr, blk[0])
209
+ return added
210
+
211
+ def park(self, dev: int, added: int) -> int:
212
+ """`added` bytes more parked on `dev`; the new parked total (its peak kept)."""
213
+ cached = self.cached.get(dev, 0) + added
214
+ self.cached[dev] = cached
215
+ if cached > self.cached_peak.get(dev, 0):
216
+ self.cached_peak[dev] = cached
217
+ return cached
218
+
219
+ # -- segments ------------------------------------------------------------------------------------------------------
220
+
221
+ def free_segments(self, dev: int) -> List[Tuple[int, int]]:
222
+ """The wholly free segments on `dev`, largest first: (ptr, bytes)."""
223
+ out = []
224
+ for size, ptrs in self.free.get(dev, {}).items():
225
+ for p in ptrs:
226
+ b = self.blocks.get(p)
227
+ if b is not None and b[1] is None and b[2] is None and _range_of(self, dev, p) is None:
228
+ out.append((p, size))
229
+ return sorted(out, key=lambda x: -x[1])
230
+
231
+ def drop_segment(self, dev: int, ptr: int) -> int:
232
+ """A WHOLLY free segment (its one block, free, unchained) leaves the pool; its bytes. The driver's release is
233
+ the caller's."""
234
+ blk = self.blocks[ptr]
235
+ self._index_remove(dev, ptr, blk[0])
236
+ self.blocks.pop(ptr)
237
+ self.alloc_size.pop(ptr, None)
238
+ return blk[0]
239
+
240
+ def evict(self, dev: int, want: int, to_driver: Callable[[int, int], int]) -> int:
241
+ """Wholly free segments on `dev` to the driver (`to_driver(dev, ptr)`), largest first, until at least `want`
242
+ bytes are freed or none is left. Returns the bytes freed."""
243
+ freed = 0
244
+ st = self.stats
245
+ for p, _size in self.free_segments(dev):
246
+ if freed >= want:
247
+ break
248
+ freed += to_driver(dev, p)
249
+ st["evictions"] += 1
250
+ self.cached[dev] = max(0, self.cached.get(dev, 0) - freed)
251
+ st["evicted_bytes"] += freed
252
+ return freed
253
+
254
+ def flush(self, devs: Iterable[int], to_driver: Callable[[int, int], int]) -> int:
255
+ """Every wholly free segment of `devs` to the driver (a segment still holding a live block stays). Returns the
256
+ bytes freed."""
257
+ freed = 0
258
+ self.stats["flushes"] += 1
259
+ for d in list(devs):
260
+ got = 0
261
+ for p, _size in self.free_segments(d):
262
+ got += to_driver(d, p)
263
+ self.cached[d] = max(0, self.cached.get(d, 0) - got)
264
+ freed += got
265
+ self.stats["flushed_bytes"] += freed
266
+ return freed
267
+
268
+
269
+ # -- expandable ranges (R16) --------------------------------------------------------------------------------------------
270
+
271
+ class Range:
272
+ """One reserved virtual address range of a device: [base, base + size), chunks of `chunk` bytes mapped into it as
273
+ the pool grows (`mapped`: chunk indices), `top` the offset the next growth starts at, `last` the block ending at
274
+ `top` (None while empty). The pool's large blocks of the range chain through it in address order, so a free block
275
+ coalesces across growth boundaries; a chunk lying whole inside a free block can be unmapped and its physical
276
+ memory mapped again elsewhere — the remainders a live neighbour pins give their pages back (R16)."""
277
+
278
+ def __init__(self, base: int, size: int, chunk: int) -> None:
279
+ self.base, self.size, self.chunk = int(base), int(size), int(chunk)
280
+ self.top = 0
281
+ self.last: Optional[int] = None
282
+ self.mapped: set = set()
283
+
284
+ def holds(self, ptr: int) -> bool:
285
+ return self.base <= ptr < self.base + self.size
286
+
287
+ def chunks_touching(self, lo: int, hi: int) -> range:
288
+ return range((lo - self.base) // self.chunk, -(-(hi - self.base) // self.chunk))
289
+
290
+ def chunks_within(self, lo: int, hi: int) -> range:
291
+ return range(-(-(lo - self.base) // self.chunk), (hi - self.base) // self.chunk)
292
+
293
+ def chunk_va(self, i: int) -> int:
294
+ return self.base + i * self.chunk
295
+
296
+ @property
297
+ def mapped_bytes(self) -> int:
298
+ return len(self.mapped) * self.chunk
299
+
300
+
301
+ def _range_of(pool: "SegmentPool", dev: int, ptr: int) -> Optional[Range]:
302
+ for r in pool.ranges.get(dev, ()):
303
+ if r.holds(ptr):
304
+ return r
305
+ return None
306
+
307
+
308
+ def range_add(pool: "SegmentPool", dev: int, base: int, size: int, chunk: int) -> Range:
309
+ """A reserved range joins the device's large class."""
310
+ r = Range(base, size, chunk)
311
+ pool.ranges.setdefault(dev, []).append(r)
312
+ return r
313
+
314
+
315
+ def range_extend(pool: "SegmentPool", dev: int, nbytes: int) -> Optional[Tuple[Range, int, int]]:
316
+ """Grow the device's last range by `nbytes` (rounded to the pool's align) at its top: the new bytes join the
317
+ block ending at the top when it is free (one block, coalesced), else become a free block chained after it.
318
+ Returns (range, lo, hi) — the caller maps the chunks touching [lo, hi) not yet mapped — or None when no range of
319
+ the device has the room (the caller reserves another)."""
320
+ n = -(-int(nbytes) // pool.align) * pool.align
321
+ rs = pool.ranges.get(dev) or []
322
+ r = rs[-1] if rs else None
323
+ if r is None or r.top + n > r.size:
324
+ return None
325
+ lo = r.base + r.top
326
+ last = r.last
327
+ if last is not None and pool.blocks[last][3]:
328
+ blk = pool.blocks[last]
329
+ pool._index_remove(dev, last, blk[0])
330
+ blk[0] += n
331
+ pool._index_add(dev, last, blk[0])
332
+ pool.on_block(last, blk[0], dev)
333
+ else:
334
+ pool.blocks[lo] = [n, last, None, True, dev, LARGE]
335
+ if last is not None:
336
+ pool.blocks[last][2] = lo
337
+ pool._index_add(dev, lo, n)
338
+ pool.on_block(lo, n, dev)
339
+ r.last = lo
340
+ r.top += n
341
+ pool.cached[dev] = pool.cached.get(dev, 0) + n
342
+ return r, lo, lo + n
343
+
344
+
345
+ def range_unmapped(pool: "SegmentPool", dev: int, ptr: int, nbytes: int) -> List[Tuple[Range, int]]:
346
+ """The (range, chunk) pairs a block [ptr, ptr + nbytes) touches that are not mapped (unmapped by a reclaim, or
347
+ the growth's own new chunks)."""
348
+ r = _range_of(pool, dev, ptr)
349
+ if r is None:
350
+ return []
351
+ return [(r, i) for i in r.chunks_touching(ptr, ptr + nbytes) if i not in r.mapped]
352
+
353
+
354
+ def range_reclaimable(pool: "SegmentPool", dev: int) -> List[Tuple[Range, int]]:
355
+ """The mapped chunks lying WHOLE inside free blocks of the device's ranges: what an unmap gives back without
356
+ touching a live byte."""
357
+ out = []
358
+ for r in pool.ranges.get(dev, ()):
359
+ for ptr, blk in pool.blocks.items():
360
+ if blk[3] and blk[4] == dev and r.holds(ptr):
361
+ out.extend((r, i) for i in r.chunks_within(ptr, ptr + blk[0]) if i in r.mapped)
362
+ return out
363
+
364
+
365
+ # -- the same rules, replayed -------------------------------------------------------------------------------------------
366
+
367
+ @dataclass(frozen=True)
368
+ class Simulation:
369
+ """What the allocator holds for a sequence of allocations and frees: `peak_bytes` from the driver (live blocks, the
370
+ free blocks of segments something alive pins, and parked whole segments), `peak_live_bytes` of the tensors alive,
371
+ `parked_bytes` the most parked at once."""
372
+ peak_bytes: int
373
+ peak_live_bytes: int
374
+ parked_bytes: int
375
+ #: requests the card of `cap` bytes refused even after the reclaim (0 without a cap)
376
+ refusals: int = 0
377
+
378
+
379
+ def simulate(events: Iterable[Tuple[str, object, int]], parked_cap: int = 0, align: int = POOL_ALIGN,
380
+ split_min: int = POOL_SPLIT_MIN, pooled: bool = True, vmm_chunk: Optional[int] = None,
381
+ cap: Optional[int] = None) -> Simulation:
382
+ """Replay `events` — ("alloc", key, nbytes), ("arena", key, nbytes) and ("free", key, 0), `key` any hashable naming
383
+ one allocation — through the allocator's own pool (`SegmentPool`) and its own order: a request is served from the
384
+ pool first; on a miss the device grows by `grow_size` and the new segment is taken from; a freed block parks, and
385
+ wholly free segments beyond `parked_cap` (0: unbounded) return to the driver. An "arena" is a block outside the
386
+ pool (`malloc_cuda(pooled=False)`, a weight arena): straight from the driver, back to it whole when freed.
387
+ `pooled=False`: the pool is off (`NBX_ALLOC_POOL=0`), every free returns to the driver. One device; the driver is a
388
+ byte counter that never refuses (what a refusal flushes is the parked whole segments, counted in `parked_bytes`).
389
+
390
+ `vmm_chunk`: the device maps chunks of that many bytes into an expandable range for the large class (R16,
391
+ `memory.vmm`): a large miss extends the range and maps the chunks it touches, a block taken over unmapped chunks
392
+ maps them, and a refusal (`cap`: the card's bytes; None: never) unmaps every mapped chunk lying whole inside a free
393
+ block before it is retried — the reclaim the allocator makes, after one synchronisation."""
394
+ pool = SegmentPool(align, split_min)
395
+ held = live = peak = peak_live = parked = refusals = 0
396
+ rng = range_add(pool, 0, 1 << 44, 1 << 46, int(vmm_chunk)) if (pooled and vmm_chunk) else None
397
+
398
+ def map_chunks(pairs) -> bool:
399
+ """Map the (range, chunk) pairs, reclaiming once when the card would refuse; False when it still refuses."""
400
+ nonlocal held
401
+ need = len(pairs) * rng.chunk
402
+ if cap is not None and held + need > cap:
403
+ # the allocator's reclaim: whole free chunks, the highest addresses first, until the mapping fits; then
404
+ # the driver segments
405
+ for r, i in sorted(range_reclaimable(pool, 0), key=lambda ri: -ri[0].chunk_va(ri[1])):
406
+ if held + need <= cap:
407
+ break
408
+ r.mapped.discard(i)
409
+ held -= r.chunk
410
+ if held + need > cap:
411
+ pool.flush([0], to_driver)
412
+ if held + need > cap:
413
+ return False
414
+ for r, i in pairs:
415
+ r.mapped.add(i)
416
+ held += need
417
+ return True
418
+ next_ptr = 1 << 40
419
+ where: Dict[object, Tuple[int, int]] = {} # key -> (ptr, requested bytes)
420
+ seg_bytes: Dict[int, int] = {}
421
+ arenas: set = set() # the ptrs of blocks outside the pool
422
+
423
+ def to_driver(dev: int, ptr: int) -> int:
424
+ nonlocal held
425
+ n = pool.drop_segment(dev, ptr)
426
+ held -= seg_bytes.pop(ptr)
427
+ return n
428
+
429
+ for kind, key, nbytes in events:
430
+ if kind == "arena":
431
+ n = int(nbytes)
432
+ if n == 0:
433
+ continue
434
+ ptr = next_ptr
435
+ next_ptr += -(-n // align) * align + align
436
+ seg_bytes[ptr] = n
437
+ arenas.add(ptr)
438
+ held += n
439
+ where[key] = (ptr, n)
440
+ live += n
441
+ peak = max(peak, held)
442
+ peak_live = max(peak_live, live)
443
+ continue
444
+ if kind == "alloc":
445
+ n = int(nbytes)
446
+ if n == 0:
447
+ continue
448
+ if rng is not None and pool.cls(n) == LARGE:
449
+ ptr = pool.take(0, n)
450
+ if ptr is None:
451
+ _r, lo, hi = range_extend(pool, 0, pool.grow_size(n))
452
+ ptr = pool.take(0, n)
453
+ ok = map_chunks(range_unmapped(pool, 0, ptr, pool.alloc_size.get(ptr, n)))
454
+ pool.alloc_size.pop(ptr, None)
455
+ if not ok:
456
+ refusals += 1
457
+ pool.park(0, pool.release(ptr))
458
+ continue
459
+ where[key] = (ptr, n)
460
+ live += n
461
+ peak = max(peak, held)
462
+ peak_live = max(peak_live, live)
463
+ continue
464
+ ptr = pool.take(0, n) if pooled else None
465
+ if ptr is None:
466
+ grown = pool.grow_size(n) if pooled else n
467
+ if cap is not None and held + grown > cap and pooled:
468
+ pool.flush([0], to_driver) # the refusal's flush, then the retry
469
+ if rng is not None:
470
+ for r, i in range_reclaimable(pool, 0):
471
+ r.mapped.discard(i)
472
+ held -= r.chunk
473
+ if cap is not None and held + grown > cap:
474
+ refusals += 1
475
+ continue
476
+ seg = next_ptr
477
+ next_ptr += grown + align
478
+ seg_bytes[seg] = grown
479
+ held += grown
480
+ if pooled:
481
+ pool.add_free_segment(0, seg, grown, n)
482
+ ptr = pool.take(0, n)
483
+ else:
484
+ ptr = seg
485
+ pool.alloc_size.pop(ptr, None) if pooled else None
486
+ where[key] = (ptr, n)
487
+ live += n
488
+ peak = max(peak, held)
489
+ peak_live = max(peak_live, live)
490
+ elif kind == "free":
491
+ if key not in where:
492
+ continue
493
+ ptr, n = where.pop(key)
494
+ live -= n
495
+ if ptr in arenas:
496
+ arenas.discard(ptr)
497
+ held -= seg_bytes.pop(ptr)
498
+ elif pooled:
499
+ cached = pool.park(0, pool.release(ptr))
500
+ parked = max(parked, cached)
501
+ if parked_cap and cached > parked_cap:
502
+ pool.evict(0, cached - parked_cap, to_driver)
503
+ else:
504
+ held -= seg_bytes.pop(ptr)
505
+ else:
506
+ raise ValueError(f"simulate: an event is ('alloc' | 'free', key, nbytes), not {kind!r}")
507
+ return Simulation(peak, peak_live, parked, refusals)
@@ -0,0 +1,43 @@
1
+ """Shadow device memory: addresses that address nothing, distinct and aligned, handed out where an application runs the
2
+ library with no device in sight (the engine's launch census derives every kernel key a model forms without running it)
3
+ — moved from the engine's census, M2. The application substitutes `malloc` for the allocator's direct allocation
4
+ (`DeviceAllocator.malloc_direct`); nothing here is installed by itself, and a real run never records anything here.
5
+
6
+ Every allocation the shadow hands out is recorded AS a shadow: base -> nbytes, bases sorted. A consumer that must size
7
+ an allocation (the Metal pin sizes its whole-allocation wrap) asks `shadow_range` and gets an answer for a recorded
8
+ shadow and None for anything else, so an address nobody recorded keeps failing in every run, shadow or not. Measured
9
+ 2026-09-24: the 76 MoE census failures were the pin asking the allocator about addresses only the shadow had handed
10
+ out. Torch-free.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import bisect
15
+ from typing import Dict, List, Optional
16
+
17
+ _NEXT = [1 << 40] # addresses that address nothing, distinct and aligned
18
+ _RANGES: Dict[int, int] = {}
19
+ _BASES: List[int] = []
20
+
21
+
22
+ def malloc(nbytes: int, dev_idx: Optional[int] = None, pooled: bool = True) -> int:
23
+ """`DeviceAllocator.malloc_direct` in a shadow, its signature kept whole (`pooled`: a weight arena asks for a block
24
+ outside the pool; a shadow has no pool and answers the same address either way)."""
25
+ ptr = _NEXT[0]
26
+ _NEXT[0] += (int(nbytes) + 255) // 256 * 256 + 256
27
+ _RANGES[ptr] = int(nbytes)
28
+ _BASES.append(ptr) # monotonic: appending keeps the list sorted
29
+ return ptr
30
+
31
+
32
+ def shadow_range(addr: int) -> Optional[tuple]:
33
+ """(base, nbytes) of the RECORDED shadow allocation containing `addr`, or None.
34
+
35
+ None for every address this shadow did not hand out, including every address of a real
36
+ run, where nothing is ever recorded here."""
37
+ addr = int(addr)
38
+ i = bisect.bisect_right(_BASES, addr) - 1
39
+ if i < 0:
40
+ return None
41
+ base = _BASES[i]
42
+ size = _RANGES[base]
43
+ return (base, size) if addr < base + max(size, 1) else None