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,75 @@
1
+ """Triton Arena — NBXTensor slot storage for compiled execution.
2
+
3
+ Memory layout: [weights | inputs | intermediates]
4
+ All slots hold NBXTensor. Zero torch dependency.
5
+ """
6
+
7
+ from operator import is_ as _is
8
+
9
+
10
+ class Arena:
11
+ """O(1) tensor storage via integer-indexed list."""
12
+
13
+ __slots__ = ('_slots', '_num_weights', '_num_inputs', 'epoch', 'views')
14
+
15
+ def __init__(self, size: int, num_weights: int = 0, num_inputs: int = 0):
16
+ self._slots: list = [None] * size
17
+ self._num_weights = num_weights
18
+ self._num_inputs = num_inputs
19
+ # bumped whenever the weight slots may have been dropped or replaced (`clear_all`, `restore_from`): a bind
20
+ # memoized against an older epoch binds again (`TritonSequence.bind_weights`)
21
+ self.epoch = 0
22
+ # {tensor id: (source weight, its view)}: the views a bind derives from weights (a pre-transposed weight), one
23
+ # per source object so a re-bind of the same weights hands the same views back; dropped with the weights
24
+ self.views = {}
25
+
26
+ def __getitem__(self, idx: int):
27
+ return self._slots[idx]
28
+
29
+ def __setitem__(self, idx: int, value):
30
+ self._slots[idx] = value
31
+
32
+ def __len__(self) -> int:
33
+ return len(self._slots)
34
+
35
+ def restore_from(self, snapshot: list) -> None:
36
+ """Bulk-restore every slot from a same-length snapshot list.
37
+
38
+ C-level slice assign. The frozen-plan replay restores the full
39
+ arena after every replayed step; the per-slot Python loop it
40
+ replaces cost ~140k `__setitem__` calls per decode token on the
41
+ 30B row (2026-08-23 host profile), most of one of the two
42
+ ~16 ms GPU-idle holes in the step.
43
+ """
44
+ if len(snapshot) != len(self._slots):
45
+ raise ValueError(
46
+ f"arena restore length mismatch: snapshot "
47
+ f"{len(snapshot)} vs arena {len(self._slots)}")
48
+ # The epoch moves only when the weight slots really change: a replayed step restores the very weight objects
49
+ # its bind put there, and an epoch bumped regardless made every next step re-bind them all (~20k weights,
50
+ # 24 ms a token on Qwen3-30B-A3B's expert-store decode, 2026-10-28). Identity, compared at C level.
51
+ nw = self._num_weights
52
+ same = all(map(_is, snapshot[:nw], self._slots[:nw]))
53
+ self._slots[:] = snapshot
54
+ if not same:
55
+ self.epoch += 1
56
+
57
+ def clear_intermediates(self):
58
+ """Free intermediate slots (keep weights + inputs)."""
59
+ start = self._num_weights + self._num_inputs
60
+ for i in range(start, len(self._slots)):
61
+ self._slots[i] = None
62
+
63
+ def clear_inputs(self) -> None:
64
+ """Clear input tensors (for next inference)."""
65
+ start = self._num_weights
66
+ end = self._num_weights + self._num_inputs
67
+ for i in range(start, end):
68
+ self._slots[i] = None
69
+
70
+ def clear_all(self) -> None:
71
+ """Clear all tensors."""
72
+ for i in range(len(self._slots)):
73
+ self._slots[i] = None
74
+ self.views.clear()
75
+ self.epoch += 1
@@ -0,0 +1,150 @@
1
+ """The memory primitives of one device backend — what the allocator's account (`memory.allocator.DeviceAllocator`) asks
2
+ a backend for, and nothing else: no byte is counted here (the account counts), no policy is decided here (the account
3
+ decides: the pool, the rung, the OOM report).
4
+
5
+ A primitive returns the backend's own status code beside its value (0 = success), as the runtime gives it: the account
6
+ checks it and names the failure, exactly as it did when it called the runtime's symbols itself. One implementation
7
+ serves every backend that exposes the C ABI of a CUDA-style runtime (`AbiMemory`: CUDA, HIP, and Metal's runtime
8
+ emulation today); a backend with a native API implements `DeviceMemory` directly (Metal next, the Mac's M2).
9
+
10
+ Built per call from the runtime in force (`backend.runtime.device_memory()`), never cached: a test that substitutes the
11
+ runtime or its table is read at once. Torch-free, numpy-free.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import ctypes
16
+ from typing import Tuple
17
+
18
+
19
+ class DeviceMemory:
20
+ """The contract. Every method returns the backend's status code first (0 = success)."""
21
+
22
+ def supports(self, primitive: str) -> bool: # e.g. "host_alloc": a backend may not offer it
23
+ raise NotImplementedError
24
+
25
+ def malloc(self, nbytes: int) -> Tuple[int, int]: # (rc, device pointer)
26
+ raise NotImplementedError
27
+
28
+ def free(self, ptr: int) -> int: # rc; a backend may release when its queue is done
29
+ raise NotImplementedError
30
+
31
+ def mem_get_info(self) -> Tuple[int, int, int]: # (rc, free bytes, total bytes) of the current device
32
+ raise NotImplementedError
33
+
34
+ def memset(self, ptr: int, value: int, nbytes: int) -> int:
35
+ raise NotImplementedError
36
+
37
+ def memcpy(self, dst: int, src: int, nbytes: int, kind: int) -> int: # kind: 0 H2H, 1 H2D, 2 D2H, 3 D2D
38
+ raise NotImplementedError
39
+
40
+ def host_alloc(self, nbytes: int) -> Tuple[int, int]: # (rc, page-locked / shared host pointer)
41
+ raise NotImplementedError
42
+
43
+ def host_free(self, ptr: int) -> int:
44
+ raise NotImplementedError
45
+
46
+ def host_register(self, ptr: int, nbytes: int) -> int: # rc; page-locks existing host bytes (mapped, portable)
47
+ raise NotImplementedError
48
+
49
+ def host_unregister(self, ptr: int) -> int:
50
+ raise NotImplementedError
51
+
52
+ def set_device(self, index: int) -> int:
53
+ raise NotImplementedError
54
+
55
+ def get_device(self) -> Tuple[int, int]: # (rc, current device index)
56
+ raise NotImplementedError
57
+
58
+ def device_count(self) -> Tuple[int, int]: # (rc, number of devices)
59
+ raise NotImplementedError
60
+
61
+ def budget(self) -> Tuple[int, int]: # (rc, bytes the device grants this process at most)
62
+ raise NotImplementedError
63
+
64
+ def map_host(self, ptr: int, nbytes: int) -> int: # unified memory: a device-visible address of host bytes
65
+ raise NotImplementedError
66
+
67
+ def unmap_host(self, start: int) -> None:
68
+ raise NotImplementedError
69
+
70
+
71
+ class AbiMemory(DeviceMemory):
72
+ """A runtime exposing the C ABI of a CUDA-style runtime, through its table of symbol names
73
+ (`backend.runtime._GPU_BACKENDS[...]`, or Metal's emulation table): the calls the account made itself before."""
74
+
75
+ __slots__ = ("_rt", "_table")
76
+
77
+ def __init__(self, rt, table: dict):
78
+ self._rt = rt
79
+ self._table = table
80
+
81
+ #: the contract's names that are table keys of another spelling
82
+ _KEYS = {"host_alloc": "malloc_host", "host_free": "free_host"}
83
+
84
+ def supports(self, primitive):
85
+ return self._KEYS.get(primitive, primitive) in self._table
86
+
87
+ def _fn(self, name: str):
88
+ return getattr(self._rt, self._table[name])
89
+
90
+ def malloc(self, nbytes):
91
+ p = ctypes.c_void_p()
92
+ rc = self._fn("malloc")(ctypes.byref(p), ctypes.c_size_t(int(nbytes)))
93
+ return rc, (p.value or 0)
94
+
95
+ def free(self, ptr):
96
+ return self._fn("free")(ctypes.c_void_p(int(ptr)))
97
+
98
+ def mem_get_info(self):
99
+ free_b, total_b = ctypes.c_size_t(), ctypes.c_size_t()
100
+ rc = getattr(self._rt, self._table.get("mem_get_info", "cudaMemGetInfo"))(ctypes.byref(free_b),
101
+ ctypes.byref(total_b))
102
+ return rc, free_b.value, total_b.value
103
+
104
+ def memset(self, ptr, value, nbytes):
105
+ return self._fn("memset")(ctypes.c_void_p(int(ptr)), ctypes.c_int(int(value)), ctypes.c_size_t(int(nbytes)))
106
+
107
+ def memcpy(self, dst, src, nbytes, kind):
108
+ return self._fn("memcpy")(ctypes.c_void_p(int(dst)), ctypes.c_void_p(int(src)), ctypes.c_size_t(int(nbytes)),
109
+ ctypes.c_int(int(kind)))
110
+
111
+ def host_alloc(self, nbytes):
112
+ p = ctypes.c_void_p()
113
+ rc = self._fn("malloc_host")(ctypes.byref(p), ctypes.c_size_t(int(nbytes)))
114
+ return rc, (p.value or 0)
115
+
116
+ def host_free(self, ptr):
117
+ return self._fn("free_host")(ctypes.c_void_p(int(ptr)))
118
+
119
+ def host_register(self, ptr, nbytes):
120
+ # flags 0 (the Default flag): under unified addressing the bytes are mapped and portable, as the
121
+ # runtime's own page-locked allocation is
122
+ return self._fn("host_register")(ctypes.c_void_p(int(ptr)), ctypes.c_size_t(int(nbytes)), ctypes.c_uint(0))
123
+
124
+ def host_unregister(self, ptr):
125
+ return self._fn("host_unregister")(ctypes.c_void_p(int(ptr)))
126
+
127
+ def set_device(self, index):
128
+ return self._fn("set_device")(ctypes.c_int(int(index)))
129
+
130
+ def get_device(self):
131
+ d = ctypes.c_int()
132
+ rc = self._fn("get_device")(ctypes.byref(d))
133
+ return rc, d.value
134
+
135
+ def device_count(self):
136
+ n = ctypes.c_int()
137
+ rc = self._fn("device_count")(ctypes.byref(n))
138
+ return rc, n.value
139
+
140
+ def budget(self):
141
+ """The device's total, as the runtime reads it (a C-ABI runtime grants the whole device)."""
142
+ rc, _free, total = self.mem_get_info()
143
+ return rc, total
144
+
145
+ def map_host(self, ptr, nbytes):
146
+ """Metal's runtime emulation maps host bytes for the device (`bind_mapped`); a C-ABI runtime has no such call."""
147
+ return self._rt.bind_mapped(int(ptr), int(nbytes))
148
+
149
+ def unmap_host(self, start):
150
+ self._rt.unbind_mapped(start)
@@ -0,0 +1,154 @@
1
+ """Triton-side device utilities — device-string parsing and cross-device
2
+ NBXTensor transfer, shared by the compiled (TritonSequence) and op-by-op
3
+ (triton_sequential) execution paths.
4
+
5
+ Pipeline-parallel placement shards a component's weights across GPUs (block N
6
+ on cuda:i, block N+k on cuda:j). At a stage boundary the activation produced on
7
+ the previous stage's device must be moved to the next stage's device before the
8
+ op runs — `NBXTensor.to('cuda:N')` is intentionally a no-op (it only casts
9
+ dtype), so the move is a real D2D `memcpy` here. R33-pure (DeviceAllocator +
10
+ NBXTensor only; no torch). Extracted from `sequence.py:_transfer_tensor` /
11
+ `_needs_move` so both modes share ONE implementation (R30 parity by
12
+ construction). Model-agnostic.
13
+
14
+ `parse_device_idx` / `parse_device_idxs` are the SINGLE triton-side device-
15
+ string parsers (brick-consolidation E1). They replace six divergent
16
+ `_parse_device_idx` copies that lived in the triton flow handlers, which
17
+ disagreed on every non-trivial form of the Prism device string:
18
+
19
+ - the fast path `device.split(":")[-1]` returned the LAST ordinal for a
20
+ bare comma list ("cuda:2,cuda:3" → 3) while the compound branch of the
21
+ same copies returned the FIRST for the fgp-prefixed form
22
+ ("fgp:cuda:0,cuda:1" → 0);
23
+ - three copies had no compound branch at all and silently returned 0 for
24
+ every "fgp:"/"tp:" string (wrong device whenever the primary was not
25
+ cuda:0);
26
+ - one naive copy (`int(s.split(":")[1])`) CRASHED with ValueError on any
27
+ compound string.
28
+
29
+ Canonical semantics here: the primary device is the FIRST CUDA ordinal named
30
+ in the string ("fgp:cuda:1,cuda:2" → 1, "cuda:2,cuda:3" → 2), a bare integer
31
+ string is accepted as an ordinal, and anything unparseable (bare "cuda",
32
+ "cpu") resolves to 0 — the behaviour every flow copy converged on for the
33
+ degenerate forms.
34
+ """
35
+
36
+ import re
37
+ from typing import List
38
+
39
+ from nbxtensor.tensor import NBXTensor
40
+ from nbxtensor.memory.allocator import DeviceAllocator
41
+
42
+ _CUDA_ORDINAL_RE = re.compile(r"cuda:(\d+)")
43
+
44
+
45
+ def parse_device_idxs(device) -> List[int]:
46
+ """All CUDA ordinals named in a device string, in order of appearance.
47
+
48
+ Handles every Prism device-string form: "cuda:2" → [2],
49
+ "fgp:cuda:0,cuda:1" → [0, 1], "tp:cuda:2,cuda:3" → [2, 3],
50
+ "cuda" / "cpu" / None → [].
51
+ """
52
+ return [int(m) for m in _CUDA_ORDINAL_RE.findall(str(device))]
53
+
54
+
55
+ def parse_device_idx(device) -> int:
56
+ """Primary (FIRST) CUDA ordinal of a Prism device string.
57
+
58
+ "cuda:2" → 2, "fgp:cuda:1,cuda:2" → 1, "cuda:2,cuda:3" → 2, "3" → 3,
59
+ "cuda" / "cpu" / unparseable → 0. Never raises — the flow handlers feed
60
+ this straight into `DeviceAllocator.set_device` at setup time and every
61
+ historical copy resolved degenerate strings to device 0.
62
+ """
63
+ idxs = parse_device_idxs(device)
64
+ if idxs:
65
+ return idxs[0]
66
+ try:
67
+ return int(str(device).strip())
68
+ except (ValueError, TypeError):
69
+ return 0
70
+
71
+
72
+ def needs_move(t: NBXTensor, target_dev: int) -> bool:
73
+ """True iff `t` must be transferred to land on cuda:target_dev.
74
+
75
+ 1. CPU source (zero3 offload) → must H2D (even if `_device_idx` is 0).
76
+ 2. Different CUDA device → must D2D.
77
+ 3. Already on target → no-op.
78
+ """
79
+ if getattr(t, "_device", "cuda") == "cpu":
80
+ return True
81
+ return t._device_idx != target_dev
82
+
83
+
84
+ def transfer_tensor(tensor: NBXTensor, target_dev: int) -> NBXTensor:
85
+ """Copy an NBXTensor to cuda:target_dev, preserving shape/strides/dtype.
86
+
87
+ kind=1 (H2D) for a CPU source (zero3), kind=3 (D2D) for a cross-GPU move.
88
+
89
+ Stride handling — dense-window rule (P-TRITON-MLA root fix): the flat
90
+ ``memcpy(tensor._nbytes)`` from ``data_ptr()`` is only meaningful when
91
+ the view's elements tile exactly one dense span from data_ptr()
92
+ (`NBXTensor.spans_densely`, the one definition — it also refuses an
93
+ overlapping view whose span happens to equal its numel). For such views (contiguous tensors, the zero3
94
+ pre-transposed weight ``.t()``, dim-0 narrows) the strides are carried
95
+ over unchanged — the historical contract, preserved byte-for-byte.
96
+ Every OTHER view — interior narrow/slice (span > numel, offset
97
+ window), step>1 de-interleave slices, broadcast/expand (stride-0,
98
+ span < numel) — is materialised via ``.contiguous()`` on the SOURCE
99
+ device first. The old code only materialised the expand case; an
100
+ interior/strided view was flat-copied and its original strides
101
+ re-attached over an nbytes-sized allocation — the strides address a
102
+ window FAR LARGER than the allocation, so the first downstream kernel
103
+ reading the view walks past the end of the new buffer — async illegal
104
+ memory access (error 700) surfacing a few launches later. Proven at
105
+ the DeepSeek-Coder-V2-Lite pipeline_parallel boundary (block.13 MLA
106
+ rope q/k de-interleave: a [1,16,23,64] narrow-of-transpose view,
107
+ strides addressing a 70528-element span over a 23552-element
108
+ allocation → 2/3 of reads OOB → strided_copy fault surfacing at
109
+ aten.cat::91, BOTH triton modes — this helper is shared by
110
+ _run_multi_device and the triton_sequential per-op transfer block).
111
+ """
112
+ if not tensor.spans_densely():
113
+ tensor = tensor.contiguous()
114
+ src_device = getattr(tensor, "_device", "cuda")
115
+ kind = 1 if src_device == "cpu" else 3
116
+ if kind == 3:
117
+ # D2D read barrier: the memcpy runs on the TARGET device's
118
+ # legacy stream, which does NOT wait for the SOURCE device's
119
+ # triton (non-default) stream — reading a tensor a kernel is
120
+ # still writing violates the "never read a weight still being
121
+ # copied" contract the zero3 H2D path enforces with events.
122
+ # Sync the source device before the peer copy (this is the
123
+ # declared slow path; an event-based overlap is the perf-layer
124
+ # upgrade). Proven on the Qwen3-Omni audio triton leg:
125
+ # deterministic error-700 poison at step-2 entry on the
126
+ # block-scatter placement, gone under CUDA_LAUNCH_BLOCKING,
127
+ # allocator ledger clean, flow tail fully quiesced — only the
128
+ # run-entry transfer region remained.
129
+ _src_idx = getattr(tensor, "_device_idx", None)
130
+ if _src_idx is not None:
131
+ # Let the copy use the direct card-to-card link. Without this the
132
+ # driver stages every peer copy through HOST memory — it never
133
+ # touches NVLink/xGMI whatever the topology reports. Memoised, so
134
+ # this is one dict lookup per hand-off after the first.
135
+ # Measured 2026-09-03: 8.7x on a 34 MB stage boundary
136
+ # (48 ms -> 5.5 ms), 2.4x at 4 KB.
137
+ DeviceAllocator.ensure_peer_access(_src_idx, target_dev)
138
+ _prev = DeviceAllocator.get_device()
139
+ if _prev != _src_idx:
140
+ DeviceAllocator.set_device(_src_idx)
141
+ DeviceAllocator.sync_device()
142
+ DeviceAllocator.set_device(_prev)
143
+ else:
144
+ DeviceAllocator.sync_device()
145
+ DeviceAllocator.set_device(target_dev)
146
+ if tensor._nbytes > 0:
147
+ dst_raw_ptr = DeviceAllocator.malloc_cuda(tensor._nbytes)
148
+ DeviceAllocator.memcpy(dst_raw_ptr, tensor.data_ptr(),
149
+ tensor._nbytes, kind=kind)
150
+ else:
151
+ dst_raw_ptr = 0
152
+ return NBXTensor(
153
+ dst_raw_ptr, tensor._shape, tensor._strides, tensor._dtype,
154
+ "cuda", owns_data=True, device_idx=target_dev, offset=0)
@@ -0,0 +1,43 @@
1
+ """Where tensors placed one after another in one block land, and how many bytes the block takes: the layout every arena
2
+ of the library allocates (`memory_pool.ComponentArena`, `memory_pool.HostPinnedArena`, an expert store's slot arena),
3
+ and the very figure a planner prices an arena at — one function, so the plan and the allocation cannot differ (M3,
4
+ docs/nbxtensor/COST.md section 2). Pure arithmetic: no device, torch-free, numpy-free.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Iterable, Sequence, Tuple
10
+
11
+ from nbxtensor.dtype.itemsize import itemsize
12
+ from nbxtensor.shape import numel
13
+
14
+ #: every tensor of an arena starts on this boundary (the widest vector load of any backend; CUDA's allocation granule
15
+ #: for a sub-allocation)
16
+ ARENA_ALIGNMENT = 256
17
+
18
+
19
+ def aligned(nbytes: int, alignment: int = ARENA_ALIGNMENT) -> int:
20
+ """`nbytes` rounded up to `alignment`."""
21
+ return -(-int(nbytes) // alignment) * alignment
22
+
23
+
24
+ def tensor_nbytes(shape: Sequence[int], dtype) -> int:
25
+ """The bytes a dense tensor of `shape` holds at `dtype` (a name, a torch spelling or an NBXDtype, as `itemsize`)."""
26
+ return numel(tuple(int(d) for d in shape)) * itemsize(dtype)
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ArenaLayout:
31
+ """`total_bytes`: the block; `offsets`: where each tensor starts in it, in placement order."""
32
+ total_bytes: int
33
+ offsets: Tuple[int, ...]
34
+
35
+
36
+ def arena_layout(sizes: Iterable[int], alignment: int = ARENA_ALIGNMENT) -> ArenaLayout:
37
+ """The layout of tensors of `sizes` bytes (placement order) in one arena: each at the next `alignment` boundary, a
38
+ zero-byte tensor taking no room — the bump allocation `ComponentArena.alloc` performs."""
39
+ offsets, at = [], 0
40
+ for n in sizes:
41
+ offsets.append(at)
42
+ at += aligned(n, alignment) if int(n) else 0
43
+ return ArenaLayout(at, tuple(offsets))
@@ -0,0 +1,191 @@
1
+ """Triton Memory Pool — arena-based GPU memory allocator.
2
+
3
+ Pre-allocates one contiguous block per component, sub-allocates inside
4
+ via bump pointer. No individual free, no fragmentation.
5
+
6
+ Zero torch. Uses DeviceAllocator (hardware-agnostic: CUDA/ROCm).
7
+ """
8
+
9
+ import gc
10
+
11
+ import numpy as np
12
+
13
+ from nbxtensor.memory.allocator import DeviceAllocator
14
+ from nbxtensor.memory.layout import ARENA_ALIGNMENT, aligned
15
+ from nbxtensor.memory.device_transfer import parse_device_idxs
16
+
17
+
18
+ def release_flow_memory(device=None) -> None:
19
+ """Flow-boundary memory release — triton-side mirror of the compiled
20
+ `MemoryManager.release_flow_memory` discipline (R30 naming parity,
21
+ separate implementation: R33-pure, DeviceAllocator only, no torch).
22
+
23
+ Consolidates the bare `gc.collect()` calls the triton flow handlers
24
+ ran at stage boundaries. Those calls skipped the sync-before-free
25
+ protection: `gc.collect()` triggers NBXTensor / ComponentArena
26
+ finalizers (`cudaFree`) on cyclic or lingering references, and freeing
27
+ memory an in-flight kernel is still reading is the err-700 class
28
+ documented in `core/memory/manager.py`. Ordering here:
29
+
30
+ 1. sync every CUDA device named in `device` (the flow's Prism
31
+ primary-device string — may be compound, e.g. "fgp:cuda:0,cuda:1")
32
+ BEFORE any reference can be finalized;
33
+ 2. gc.collect() — drop refs, run finalizers;
34
+ 3. DeviceAllocator.empty_cache_pool() — return pool-cached blocks to
35
+ the driver (no-op under NBX_ALLOC_POOL=0; individual frees then
36
+ already went straight to cudaFree).
37
+ """
38
+ idxs = parse_device_idxs(device)
39
+ if idxs:
40
+ prev = DeviceAllocator.get_device()
41
+ for i in idxs:
42
+ DeviceAllocator.set_device(i)
43
+ DeviceAllocator.sync_device()
44
+ DeviceAllocator.set_device(prev)
45
+ gc.collect()
46
+ DeviceAllocator.empty_cache_pool()
47
+
48
+
49
+ class ComponentArena:
50
+ """Single contiguous GPU memory block for one component's weights.
51
+
52
+ Allocates one big block via DeviceAllocator.malloc_cuda, then
53
+ sub-allocates inside via bump pointer with 256-byte alignment.
54
+
55
+ Usage:
56
+ arena = ComponentArena(total_bytes, device_idx)
57
+ ptr1 = arena.alloc(1024) # sub-allocate 1024 bytes
58
+ ptr2 = arena.alloc(4096) # sub-allocate 4096 bytes
59
+ arena.free() # release entire block at once
60
+ """
61
+
62
+ ALIGNMENT = ARENA_ALIGNMENT # the library's one arena layout (memory.layout)
63
+
64
+ def __init__(self, total_bytes: int, device_idx: int):
65
+ # Initialize _base_ptr BEFORE the malloc call so __del__ is safe
66
+ # even if malloc_cuda raises (OOM on one device shouldn't leave
67
+ # a half-constructed object whose __del__ then crashes on
68
+ # attribute access). This closes a secondary issue flagged
69
+ # during the zero3 leak investigation (see tests/scratch/
70
+ # zero3_leak_investigation/REPORT.md §0.5).
71
+ self.device_idx = device_idx
72
+ self._base_ptr = 0
73
+ self._total = self._align(total_bytes)
74
+ self._offset = 0
75
+
76
+ DeviceAllocator.set_device(device_idx)
77
+ # never pooled: once freed, its bytes go back to the driver whole (DeviceAllocator.malloc_cuda `pooled`)
78
+ self._base_ptr = DeviceAllocator.malloc_cuda(self._total, pooled=False)
79
+ if self._base_ptr == 0 and self._total > 0:
80
+ raise RuntimeError(
81
+ f"ComponentArena: failed to allocate {self._total / 1e9:.2f} GB "
82
+ f"on device {device_idx}")
83
+
84
+ def alloc(self, nbytes: int) -> int:
85
+ """Sub-allocate nbytes from the arena. Returns GPU pointer."""
86
+ if nbytes == 0:
87
+ return 0
88
+ aligned = self._align(nbytes)
89
+ if self._offset + aligned > self._total:
90
+ raise RuntimeError(
91
+ f"ComponentArena: out of memory. "
92
+ f"Requested {nbytes} bytes, "
93
+ f"used {self._offset}/{self._total} bytes "
94
+ f"on device {self.device_idx}")
95
+ ptr = self._base_ptr + self._offset
96
+ self._offset += aligned
97
+ return ptr
98
+
99
+ def free(self):
100
+ """Release the entire arena block."""
101
+ if self._base_ptr:
102
+ DeviceAllocator.set_device(self.device_idx)
103
+ DeviceAllocator.free_cuda(self._base_ptr)
104
+ self._base_ptr = 0
105
+ self._offset = 0
106
+ self._total = 0
107
+
108
+ def reset(self):
109
+ """Reset bump pointer without freeing GPU memory.
110
+
111
+ Allows reuse of the same arena for a new component load
112
+ (e.g., lifecycle reload in lazy_sequential strategy).
113
+ """
114
+ self._offset = 0
115
+
116
+ @property
117
+ def used(self) -> int:
118
+ return self._offset
119
+
120
+ @property
121
+ def capacity(self) -> int:
122
+ return self._total
123
+
124
+ def _align(self, n: int) -> int:
125
+ return aligned(n, self.ALIGNMENT)
126
+
127
+ def __del__(self):
128
+ # Guard: __init__ may raise before _base_ptr is set (rare but
129
+ # happens when malloc_cuda throws OOM immediately). Let __del__
130
+ # remain a no-op in that case.
131
+ if getattr(self, '_base_ptr', 0):
132
+ self.free()
133
+
134
+
135
+ class HostPinnedArena:
136
+ """One page-locked host block for a component's host-held weights (zero3's block weights, an expert store's
137
+ routed experts), sub-allocated like `ComponentArena`. A pinned allocation per weight cost Qwen3-30B-A3B 18 864
138
+ page-locking calls — about 40 % of a 447 s load (py-spy, V100 rack, 2026-10-27); one block costs one. Each
139
+ weight holds the arena as its `base`, so the block is released when the last of them is."""
140
+
141
+ ALIGNMENT = ARENA_ALIGNMENT
142
+
143
+ def __init__(self, total_bytes: int):
144
+ """On a backend that page-locks existing memory (`host_register`), the block is ordinary host memory, written
145
+ by the load and page-locked by `pin()` in one call before any transfer reads it: a page-locked allocation
146
+ ran at 1.2 GiB/s on one thread, its threads serialised by the runtime (16 GiB in 13.0 s; 4 or 8 threads,
147
+ 16 s), while 16 GiB written by 16 threads and then registered took 4.9 s (V100 rack, 2026-10-30) — and the
148
+ load's own writes are that touch. Elsewhere the block is the backend's page-locked allocation, as before."""
149
+ self._base_ptr = 0
150
+ self._total = aligned(total_bytes, self.ALIGNMENT)
151
+ self._offset = 0
152
+ self._pageable = None # the host bytes until `pin()` page-locks them (a register-capable backend)
153
+ self._registered = False
154
+ if self._total:
155
+ from nbxtensor.backend import runtime as _rt
156
+ if _rt.device_memory().supports("host_register"):
157
+ self._pageable = np.empty(self._total + self.ALIGNMENT, dtype=np.uint8)
158
+ self._base_ptr = aligned(self._pageable.ctypes.data, self.ALIGNMENT)
159
+ else:
160
+ self._base_ptr = DeviceAllocator.malloc_host_pinned(self._total)
161
+
162
+ def pin(self) -> None:
163
+ """Page-lock the block (once; a no-op where it was allocated page-locked). Called by the loader after the
164
+ last weight is written and before any transfer reads one."""
165
+ if self._pageable is not None and not self._registered:
166
+ DeviceAllocator.register_host_pinned(self._base_ptr, self._total)
167
+ self._registered = True
168
+
169
+ def alloc(self, nbytes: int) -> int:
170
+ if nbytes == 0:
171
+ return 0
172
+ size = aligned(nbytes, self.ALIGNMENT)
173
+ if self._offset + size > self._total:
174
+ raise RuntimeError(f"HostPinnedArena: {nbytes} bytes asked, {self._offset}/{self._total} used — the "
175
+ f"sizing pass and the load disagree on a host weight's bytes")
176
+ ptr = self._base_ptr + self._offset
177
+ self._offset += size
178
+ return ptr
179
+
180
+ @property
181
+ def used(self) -> int:
182
+ return self._offset
183
+
184
+ def __del__(self):
185
+ if getattr(self, "_base_ptr", 0):
186
+ if getattr(self, "_pageable", None) is None:
187
+ DeviceAllocator.free_host_pinned(self._base_ptr)
188
+ elif self._registered:
189
+ DeviceAllocator.unregister_host_pinned(self._base_ptr)
190
+ self._pageable = None
191
+ self._base_ptr = 0