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
nbxtensor/tensor.py ADDED
@@ -0,0 +1,1867 @@
1
+ """NBXTensor — Tensor type for --triton mode.
2
+
3
+ Wraps raw CUDA device pointers with shape/strides/dtype metadata.
4
+ All metadata ops are pure Python CPU computation.
5
+ Triton kernels receive NBXTensor via .data_ptr() and .dtype.
6
+
7
+ Dependencies: ctypes (stdlib), math (stdlib), triton (lazy, for dtype mapping).
8
+
9
+ Weight loading: safetensors → numpy → cudaMemcpy H2D → NBXTensor
10
+ Allocation: raw cudaMalloc via ctypes
11
+ Metadata ops: pure Python stride/shape computation
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import bisect # module level, NOT inside the range helpers: they run on the
17
+ # finalizer path, and at interpreter shutdown `sys.meta_path` is
18
+ # None, so a function-level import raises ImportError AFTER the
19
+ # cudaFree has already happened — aborting the bookkeeping that
20
+ # follows it and printing a traceback that can mask a real
21
+ # [NBX-CUDA-ERROR] from the same free (observed 2026-09-18).
22
+ import ctypes
23
+ import functools
24
+ import math
25
+ import os
26
+ import struct
27
+ import sys
28
+ import threading
29
+ from enum import IntEnum
30
+ from typing import Dict, List, Optional, Tuple
31
+
32
+ from .shape import (MAX_NDIM, broadcast_strides, contiguous_strides, is_contiguous, max_linear_offset,
33
+ slice_bounds)
34
+ from nbxtensor.dtype.nbx_dtype import (NBXDtype, _COMPLEX_DTYPES, _DTYPE_FROM_STR, _DTYPE_SIZES, # noqa: F401
35
+ _DTYPE_TYPESTR, _FLOATING_DTYPES, _NUMPY_DTYPE_MAP, dtype_size, parse_dtype)
36
+ from nbxtensor.memory.allocator import DeviceAllocator, DeviceOOMError, ImpossibleExtentError # noqa: F401
37
+ from nbxtensor.memory import allocator as _alloc
38
+ from nbxtensor.backend import runtime as _rt, capabilities as _caps, faults as _faults # noqa: F401
39
+
40
+
41
+ # NBX_TRACE_DEL_BIG_MB=N (default off): NBXTensor.__del__ logs every
42
+ # free of an owns_data tensor >= N MB. Read ONCE at import
43
+ # (P-KERNEL-LAUNCH-HYGIENE C4) — __del__ fires per tensor, and an
44
+ # environ.get there is pure overhead on the free path. Consequence:
45
+ # changing the env var mid-process no longer applies; acceptable for a
46
+ # diagnostic that is set before launch. None == trace disabled.
47
+ try:
48
+ _TRACE_DEL_BIG_ENV = os.environ.get("NBX_TRACE_DEL_BIG_MB", "0")
49
+ _TRACE_DEL_BIG_THRESH: Optional[int] = (
50
+ int(_TRACE_DEL_BIG_ENV) * 1024 * 1024
51
+ if _TRACE_DEL_BIG_ENV != "0" else None)
52
+ except Exception:
53
+ _TRACE_DEL_BIG_THRESH = None
54
+
55
+
56
+ # ============================================================================
57
+ # TRITON DTYPE MAPPING (lazy import)
58
+ # ============================================================================
59
+
60
+ _TL_DTYPE_MAP = None
61
+
62
+
63
+ def _get_tl_dtype(nbx_dtype: NBXDtype):
64
+ """Map NBXDtype → triton.language dtype. Lazy import of triton."""
65
+ global _TL_DTYPE_MAP
66
+ if _TL_DTYPE_MAP is None:
67
+ import triton.language as tl
68
+ _TL_DTYPE_MAP = {
69
+ NBXDtype.float16: tl.float16,
70
+ NBXDtype.bfloat16: tl.bfloat16,
71
+ NBXDtype.float32: tl.float32,
72
+ NBXDtype.float64: tl.float64,
73
+ NBXDtype.int8: tl.int8,
74
+ NBXDtype.int16: tl.int16,
75
+ NBXDtype.int32: tl.int32,
76
+ NBXDtype.int64: tl.int64,
77
+ NBXDtype.uint8: tl.uint8,
78
+ NBXDtype.bool_: tl.uint8, # bool stored as uint8 (same as PyTorch)
79
+ }
80
+ result = _TL_DTYPE_MAP.get(nbx_dtype)
81
+ if result is None:
82
+ import triton.language as tl
83
+ return tl.float32
84
+ return result
85
+
86
+
87
+ # ============================================================================
88
+ # STRIDED COPY
89
+ # ============================================================================
90
+
91
+ # Shape/stride metadata buffers, keyed by (values, device). Never
92
+ # evicted — a frozen replay plan records raw device pointers inside its
93
+ # recorded kernel launches, so a freed buffer would leave the plan
94
+ # pointing at reclaimed memory. See _upload_int64_array.
95
+ _INT64_ARRAY_CACHE: dict = {}
96
+
97
+
98
+ def _saturating_cast(src_dtype, dst_dtype) -> bool:
99
+ """The protected float16 downcast (clamp finite values to ±65504) —
100
+ the triton mirror of the compiled DtypeEngine's conversion contract,
101
+ decided once here for every cast path (`.to`, `copy_`, `__setitem__`)."""
102
+ return (dst_dtype == NBXDtype.float16
103
+ and src_dtype in (NBXDtype.float32, NBXDtype.float64, NBXDtype.bfloat16))
104
+
105
+
106
+ def _copy_nd(src: 'NBXTensor', dst: 'NBXTensor'):
107
+ """Copy `src` into `dst` — any strides on either side, `src` broadcast
108
+ to `dst`'s shape, the dtype converted on the store — in ONE launch.
109
+
110
+ Real dtypes only: a complex source keeps the contiguous-then-cast
111
+ path (the kernel sees a float pointer, and a complex cast is a pair
112
+ of floats per element that `copy_kernel` already handles).
113
+ """
114
+ import triton
115
+ from nbxtensor.ops.strided_copy import strided_copy_nd_kernel
116
+
117
+ if tuple(src._shape) != tuple(dst._shape):
118
+ src = src.expand(*dst._shape)
119
+ ndim = dst.ndim
120
+ if ndim > MAX_NDIM:
121
+ raise RuntimeError(
122
+ f"NBXTensor._copy_nd: ndim={ndim} exceeds the PyTorch hard limit "
123
+ f"of {MAX_NDIM}. Please open an issue with the model name and trace site.")
124
+ n = dst._numel
125
+ if n == 0:
126
+ return
127
+ if ndim == 0:
128
+ # a scalar: one element, both offsets 0 — a rank-1 walk of extent 1
129
+ shape, sst, dst_st = (1,), (0,), (0,)
130
+ ndim = 1
131
+ else:
132
+ shape, sst, dst_st = tuple(dst._shape), tuple(src._strides), tuple(dst._strides)
133
+ shape_buf = _upload_int64_array(shape, dst._device_idx)
134
+ src_stride_buf = _upload_int64_array(sst, dst._device_idx)
135
+ dst_stride_buf = _upload_int64_array(dst_st, dst._device_idx)
136
+ _m = _ew_meta(n) # the certified tile of the count's class (ops/elementwise_launch.py)
137
+ BLOCK = _m['BLOCK_SIZE']
138
+ _rt.set_device(dst)
139
+ strided_copy_nd_kernel[(triton.cdiv(n, BLOCK),)](
140
+ src, dst, n, shape_buf, src_stride_buf, dst_stride_buf,
141
+ BLOCK_SIZE=BLOCK, num_warps=_m['num_warps'], NDIM=ndim,
142
+ SATURATE_F16=_saturating_cast(src._dtype, dst._dtype),
143
+ WIDE=_index_wide(n, max_linear_offset(shape, sst), max_linear_offset(shape, dst_st)))
144
+
145
+
146
+ def _ew_meta(n: int) -> dict:
147
+ """An element-wise launch's certified tile and warps (`ops/elementwise_launch.ew_meta`), imported at use: that
148
+ module resolves through an autotuner that allocates its scratch here."""
149
+ from nbxtensor.ops.elementwise_launch import ew_meta
150
+ return ew_meta(n)
151
+
152
+
153
+ def _upload_int64_array(values, device_idx: int) -> 'NBXTensor':
154
+ """Upload a small tuple/list of ints to the GPU as a contiguous
155
+ int64 NBXTensor. Used to hand shape and stride vectors to the N-D
156
+ strided_copy / strided_scatter kernels.
157
+
158
+ Zero-torch: goes through `NBXTensor.from_numpy`, which internally
159
+ uses `DeviceAllocator.malloc_cuda` + `DeviceAllocator.memcpy` (via
160
+ `_GPU_BACKENDS`).
161
+
162
+ MEMOISED by value. The original form allocated, uploaded and freed a
163
+ fresh buffer per call, on the reasoning that "200 bytes per upload is
164
+ amortised across a whole forward pass (< 1 ms total)". Measured on
165
+ 2026-08-20 that reasoning does not hold: `strided_copy` runs ~757
166
+ times per decode token on the canonical 30B row, so these metadata
167
+ uploads were **616 host-to-device transfers per token** — costing
168
+ more than every matmul in the model combined. The assumption was
169
+ true when the strided path was rare and false once it became hot.
170
+
171
+ The values are shapes and strides: a model touches a few hundred
172
+ distinct tuples, so the cache is bounded in practice at tens of
173
+ kilobytes.
174
+
175
+ It is deliberately NEVER evicted, and that is a correctness
176
+ requirement rather than a simplification: a frozen replay plan
177
+ records raw device POINTERS inside its kernel launches, so evicting
178
+ a buffer between recording and replay would leave the plan pointing
179
+ at freed memory.
180
+
181
+ Safe to share because every consumer is read-only — the four call
182
+ sites are `_strided_copy` and `_strided_scatter`, and the kernels
183
+ only `tl.load` these pointers, never `tl.store` to them.
184
+ """
185
+ key = (tuple(int(v) for v in values), int(device_idx))
186
+ hit = _INT64_ARRAY_CACHE.get(key)
187
+ if hit is not None:
188
+ return hit
189
+
190
+ import numpy as _np
191
+ arr = _np.asarray(values, dtype=_np.int64)
192
+ DeviceAllocator.set_device(device_idx)
193
+ buf = NBXTensor.from_numpy(arr)
194
+ _INT64_ARRAY_CACHE[key] = buf
195
+ return buf
196
+
197
+
198
+ def _upload_index_array(values, device_idx: int, max_offset: int) -> 'NBXTensor':
199
+ """The same, in the NARROWEST integer that holds every index the kernel
200
+ will compute from it.
201
+
202
+ The element type of this buffer decides the width of the whole index
203
+ computation downstream: `tl.load(shape_ptr + dim)` takes its dtype from
204
+ the pointer, and Triton then promotes the flat offset, every `%`, every
205
+ `//` and every stride multiply to match. At int64 that is a 64-bit
206
+ integer division and modulo per dimension per element, on hardware —
207
+ Apple's — that has no integer-division instruction at any width and
208
+ emulates both.
209
+
210
+ Measured 2026-09-08 on an M4 Pro, `strided_copy_kernel` over a
211
+ (2048, 5632) bf16 transpose, the same kernel and the same launch, only
212
+ the metadata's width changed:
213
+
214
+ int64 metadata 3.794 ms 12.2 GB/s
215
+ int32 metadata 1.411 ms 32.7 GB/s
216
+ a contiguous copy of the same bytes 0.384 ms 120.3 GB/s
217
+
218
+ `max_offset` is the largest linear offset the kernel can form from these
219
+ values — `sum((shape[i] - 1) * stride[i])` at the call site. int32 is
220
+ used only when that bound and every value fit in it, so the narrower
221
+ buffer computes the SAME offsets rather than nearly the same ones; a
222
+ tensor large enough to overflow keeps int64 and keeps today's behaviour.
223
+ Numerically inert on CUDA by the same argument, where it is also the
224
+ cheaper arithmetic.
225
+ """
226
+ key = (tuple(int(v) for v in values), int(device_idx), "narrow")
227
+ hit = _INT64_ARRAY_CACHE.get(key)
228
+ if hit is not None:
229
+ return hit
230
+
231
+ import numpy as _np
232
+ _I32_MAX = 2 ** 31 - 1
233
+ fits = (abs(int(max_offset)) <= _I32_MAX
234
+ and all(abs(int(v)) <= _I32_MAX for v in values))
235
+ arr = _np.asarray(values, dtype=_np.int32 if fits else _np.int64)
236
+ DeviceAllocator.set_device(device_idx)
237
+ buf = NBXTensor.from_numpy(arr)
238
+ _INT64_ARRAY_CACHE[key] = buf
239
+ return buf
240
+
241
+
242
+ def _index_wide(n_elements: int, *max_offsets: int) -> bool:
243
+ """64-bit index arithmetic in the strided kernels (`WIDE`) when the element count or an operand's largest offset
244
+ is within a factor of two of 2^31 (a masked lane forms an offset past the extent before its mask drops it; the
245
+ factor covers it). Below it the 32-bit path computes the same offsets. The kernels' flat offset was made 64-bit
246
+ from the program id for tensors past 2^31 elements, and that promoted every `%` and `//` of every copy back to
247
+ 64-bit — emulated on Apple's GPU: the (1,128,9,242,362) bf16 permute copy of a VAE conv3d ran 84 ms (4.8 GB/s)
248
+ against 8.3 ms (48 GB/s) at 32 bits, same bytes (Mac, 2026-10-21); the narrow metadata of `_upload_index_array`
249
+ had stopped mattering."""
250
+ return max([int(n_elements)] + [abs(int(b)) for b in max_offsets]) >= (1 << 30)
251
+
252
+
253
+
254
+
255
+ def _strided_copy(src: 'NBXTensor', dst: 'NBXTensor'):
256
+ """Copy non-contiguous src into contiguous dst via the N-D scalable
257
+ Triton kernel.
258
+
259
+ The kernel is parameterised by `NDIM: tl.constexpr`; Triton's
260
+ kernel cache stores one specialised compilation per distinct ndim
261
+ encountered at runtime (first call compiles, subsequent calls hit
262
+ the cache at near-zero overhead). Shape and strides travel via
263
+ small int64 scratch buffers on-device, loaded inside the kernel
264
+ body — this keeps the kernel text identical regardless of ndim.
265
+
266
+ History: the prior 5-D-hardcoded kernel silently dropped dims
267
+ beyond 5 (via `[1] * (5 - ndim)` returning `[]` when `ndim > 5`),
268
+ producing OOB reads. 6-D PixArt patchify clones corrupted
269
+ (Alpha) or crashed (Sigma) on this path; 8-D SANA-Video VAE
270
+ clones never reached production. N-D path covers all of them
271
+ without a hardcoded ceiling.
272
+ """
273
+ import triton
274
+ from nbxtensor.ops.strided_copy import strided_copy_kernel
275
+
276
+ # Complex tensors are stored interleaved [real, imag], but Triton sees the
277
+ # data_ptr as a plain float (no complex dtype). The strided kernel would copy
278
+ # ONE float per element with the per-(complex)-element strides applied as
279
+ # float offsets — corrupting the imaginary half (chatterbox s3gen stft does
280
+ # fft_r2c -> transpose -> contiguous; the transposed-complex contiguous
281
+ # mangled the imag spectrum). Reinterpret both as a float view with a
282
+ # trailing [2] dim (strides * 2 in elements, +1 for the re/imag axis) and
283
+ # recurse — this copies BOTH floats per complex element with correct strides.
284
+ # Built manually (NOT view_as_real, which calls .contiguous() on strided
285
+ # input → would recurse back into this same broken path).
286
+ if src.is_complex():
287
+ _fdt = (NBXDtype.float64 if src._dtype == NBXDtype.complex128
288
+ else NBXDtype.float32)
289
+ _fsrc = NBXTensor(
290
+ src._data_ptr, tuple(src._shape) + (2,),
291
+ tuple(s * 2 for s in src._strides) + (1,),
292
+ _fdt, src._device, src._offset * 2,
293
+ device_idx=src._device_idx,
294
+ base=src._base if src._base is not None else src, pinned=src._pinned)
295
+ _fdst_shape = tuple(dst._shape) + (2,)
296
+ _fdst = NBXTensor(
297
+ dst._data_ptr, _fdst_shape, contiguous_strides(_fdst_shape),
298
+ _fdt, dst._device, dst._offset * 2,
299
+ device_idx=dst._device_idx,
300
+ base=dst._base if dst._base is not None else dst, pinned=dst._pinned)
301
+ _strided_copy(_fsrc, _fdst)
302
+ return
303
+
304
+ ndim = src.ndim
305
+ if ndim > MAX_NDIM:
306
+ raise RuntimeError(
307
+ f"NBXTensor._strided_copy: ndim={ndim} exceeds the PyTorch "
308
+ f"hard limit of {MAX_NDIM}. Please open an issue with the "
309
+ f"model name and trace site."
310
+ )
311
+ n = src._numel
312
+
313
+ _bound = max_linear_offset(src._shape, src._strides)
314
+ shape_buf = _upload_index_array(src._shape, src._device_idx, _bound)
315
+ stride_buf = _upload_index_array(src._strides, src._device_idx, _bound)
316
+
317
+ _m = _ew_meta(n) # the certified tile of the count's class (ops/elementwise_launch.py)
318
+ BLOCK = _m['BLOCK_SIZE']
319
+ grid = (triton.cdiv(n, BLOCK),)
320
+ _rt.set_device(src)
321
+ strided_copy_kernel[grid](
322
+ src, dst, n,
323
+ shape_buf, stride_buf,
324
+ BLOCK_SIZE=BLOCK, num_warps=_m['num_warps'],
325
+ NDIM=ndim,
326
+ WIDE=_index_wide(n, _bound),
327
+ )
328
+
329
+
330
+ def _strided_scatter(src: 'NBXTensor', dst: 'NBXTensor'):
331
+ """Scatter contiguous src into non-contiguous dst via the N-D
332
+ scalable Triton kernel. Inverse of `_strided_copy`.
333
+
334
+ Used by `NBXTensor.__setitem__` for KV-cache indexed writes where
335
+ the destination is a narrow view with non-contiguous strides. KV
336
+ caches are typically 4-D today so this path rarely exercises the
337
+ N-D machinery, but it stays in sync with `_strided_copy` for
338
+ consistency and to future-proof any emerging cache layouts.
339
+ """
340
+ import triton
341
+ from nbxtensor.ops.strided_copy import strided_scatter_kernel
342
+
343
+ ndim = dst.ndim
344
+ if ndim > MAX_NDIM:
345
+ raise RuntimeError(
346
+ f"NBXTensor._strided_scatter: ndim={ndim} exceeds the "
347
+ f"PyTorch hard limit of {MAX_NDIM}. Please open an issue."
348
+ )
349
+ n = src._numel
350
+
351
+ _bound = max_linear_offset(dst._shape, dst._strides)
352
+ shape_buf = _upload_index_array(dst._shape, dst._device_idx, _bound)
353
+ stride_buf = _upload_index_array(dst._strides, dst._device_idx, _bound)
354
+
355
+ _m = _ew_meta(n) # the certified tile of the count's class (ops/elementwise_launch.py)
356
+ BLOCK = _m['BLOCK_SIZE']
357
+ grid = (triton.cdiv(n, BLOCK),)
358
+ _rt.set_device(src)
359
+ strided_scatter_kernel[grid](
360
+ src, dst, n,
361
+ shape_buf, stride_buf,
362
+ BLOCK_SIZE=BLOCK, num_warps=_m['num_warps'],
363
+ NDIM=ndim,
364
+ WIDE=_index_wide(n, _bound),
365
+ )
366
+
367
+
368
+ def _fill_constant(t: 'NBXTensor', value) -> 'NBXTensor':
369
+ """Fill a contiguous NBXTensor's storage with a scalar constant via the
370
+ pure-Triton `full_kernel`. Backs `NBXTensor.ones` and the non-zero branch
371
+ of `fill_` — the zero case stays on the cheaper byte-`memset` path. Flat
372
+ 1-D indexing assumes contiguous storage (the only callers are freshly
373
+ allocated tensors and the existing full-storage fill_ semantics).
374
+ """
375
+ if t._nbytes == 0:
376
+ return t
377
+ import triton
378
+ from nbxtensor.ops.fill_op import fill_kernel
379
+
380
+ n = t._numel
381
+ _m = _ew_meta(n) # the certified tile of the count's class (ops/elementwise_launch.py)
382
+ BLOCK = _m['BLOCK_SIZE']
383
+ if not t.is_contiguous():
384
+ # Flat 1-D indexing assumes contiguous storage; on a strided view it
385
+ # would write the wrong addresses. Fill a contiguous temp, then scatter
386
+ # into the view's strided positions.
387
+ tmp = NBXTensor.empty(t._shape, t._dtype, f"cuda:{t._device_idx}")
388
+ _rt.set_device(tmp)
389
+ fill_kernel[(triton.cdiv(n, BLOCK),)](tmp, value, n, BLOCK_SIZE=BLOCK, num_warps=_m['num_warps'])
390
+ _strided_scatter(tmp, t)
391
+ return t
392
+ grid = (triton.cdiv(n, BLOCK),)
393
+ _rt.set_device(t)
394
+ fill_kernel[grid](t, value, n, BLOCK_SIZE=BLOCK, num_warps=_m['num_warps'])
395
+ return t
396
+
397
+
398
+ # ============================================================================
399
+ # STRIDE COMPUTATION
400
+ # ============================================================================
401
+
402
+ # ============================================================================
403
+ # NBXTensor
404
+ # ============================================================================
405
+
406
+ class NBXTensor:
407
+ """Tensor descriptor for --triton mode.
408
+
409
+ Properties for Triton kernel compatibility:
410
+ .data_ptr() → raw CUDA pointer (int) — Triton reads this
411
+ .dtype → triton.language dtype (tl.float16 etc.) — Triton JIT uses this
412
+ .nbx_dtype → NBXDtype enum — internal, zero dependency
413
+ .shape → tuple of ints
414
+ .stride() → tuple of ints (element strides)
415
+
416
+ All metadata ops (view, reshape, permute...) are pure Python.
417
+ Allocation via raw cudaMalloc.
418
+ """
419
+
420
+ __slots__ = ('_data_ptr', '_shape', '_strides', '_dtype', '_device',
421
+ '_nbytes', '_numel', '_offset', '_owns_data', '_device_idx',
422
+ '_base', # reference to parent tensor (prevents GC of underlying memory);
423
+ # also holds the numpy buffer for unpinned CPU tensors
424
+ '_pinned', # True for pinned host memory (cudaMallocHost); used by __del__
425
+ # to dispatch to free_host_pinned instead of free_cuda
426
+ '_elem_size', # C6b: dtype_size(_dtype) cached at construction
427
+ '_contig', # C6a: contiguity flag cached at construction
428
+ )
429
+
430
+ def __init__(self, data_ptr: int, shape: Tuple[int, ...],
431
+ strides: Tuple[int, ...], dtype: NBXDtype,
432
+ device: str = 'cuda', offset: int = 0, owns_data: bool = False,
433
+ device_idx: int = 0, base=None, pinned: bool = False):
434
+ self._data_ptr = data_ptr
435
+ self._shape = tuple(shape)
436
+ self._strides = tuple(strides)
437
+ self._dtype = dtype
438
+ self._device = device
439
+ self._offset = offset
440
+ self._owns_data = owns_data
441
+ self._device_idx = device_idx
442
+ self._base = base # keep parent alive to prevent use-after-free on views
443
+ self._pinned = pinned
444
+ self._numel = math.prod(shape) if shape else 1
445
+ # C6b: element size cached once — shape/strides/dtype are fixed at
446
+ # construction (every view/reshape builds a NEW NBXTensor; __init__
447
+ # is the single construction path). `_elem_size` and `_nbytes` are
448
+ # derived from `dtype` HERE and never again, so assigning `_dtype`
449
+ # after construction leaves a tensor whose declared length belongs
450
+ # to the old dtype — a buffer overrun when the new dtype is smaller.
451
+ # There is no such retag left in the tree: the bf16-bits site
452
+ # (graph_executor._load_constant_triton) declares the dtype to
453
+ # from_numpy instead.
454
+ self._elem_size = dtype_size(dtype)
455
+ self._nbytes = self._numel * self._elem_size
456
+ # C6a: contiguity is a pure function of the (immutable) shape and
457
+ # strides — compute the flag once instead of per is_contiguous()
458
+ # call in the hot loops.
459
+ self._contig = is_contiguous(self._shape, self._strides)
460
+
461
+ def __del__(self):
462
+ # Targeted lifecycle trace: NBX_TRACE_DEL_BIG_MB=N (default off)
463
+ # logs every __del__ on owns=True NBXTensors >= N MB. Lets us
464
+ # tell whether a leaked NBXTensor is actually freed and when.
465
+ # Gate is the module-level constant read once at import (C4) —
466
+ # zero env lookup on the per-tensor free path. The try/except
467
+ # keeps the free below unconditional (same as before), including
468
+ # at interpreter shutdown when module globals may be gone.
469
+ try:
470
+ if (_TRACE_DEL_BIG_THRESH is not None and self._owns_data
471
+ and self._data_ptr
472
+ and self._nbytes >= _TRACE_DEL_BIG_THRESH):
473
+ print(f"[NBX_DEL data_ptr={self._data_ptr} "
474
+ f"shape={self._shape} nbytes={self._nbytes/1024/1024:.0f}MB]",
475
+ flush=True)
476
+ except Exception:
477
+ pass
478
+
479
+ # A tensor built by `NBXTensor.__new__` without `__init__` has NO
480
+ # slot set at all — `copy`, `pickle`, the routing-contract test in
481
+ # test_prefill_determinism, and any `__init__` that raises part-way
482
+ # all leave one. Reading `_owns_data` there raises AttributeError, and
483
+ # Python does not propagate an exception out of `__del__`: it prints an
484
+ # unraisable exception and ABANDONS THE REST OF THIS METHOD — which is
485
+ # the free below. So the one case the old line could not survive is the
486
+ # one where failing silently would leak a device buffer.
487
+ #
488
+ # Returning early is correct and not a papered-over free: `__init__` is
489
+ # pure assignment and allocates nothing, so a tensor that did not finish
490
+ # it cannot own device memory. Ownership is recorded FROM the
491
+ # `owns_data` argument, never established inside the tensor.
492
+ try:
493
+ owns, ptr = self._owns_data, self._data_ptr
494
+ except AttributeError:
495
+ return
496
+
497
+ if owns and ptr:
498
+ # Interpreter teardown clears the allocator's module before this one (it was imported while this module
499
+ # was importing, and modules are cleared in reverse order of import): its globals are None, a free would
500
+ # raise inside it. The process is exiting and hands every device buffer back to the driver as it does.
501
+ if getattr(_alloc, "DeviceAllocator", None) is None:
502
+ return
503
+ if self._device == 'cuda':
504
+ DeviceAllocator.free_cuda(ptr)
505
+ elif self._device == 'cpu' and self._pinned:
506
+ # Pinned host memory allocated via cudaMallocHost.
507
+ DeviceAllocator.free_host_pinned(ptr)
508
+ # Unpinned CPU: backed by self._base (numpy array) — Python
509
+ # GC drops it automatically when self goes out of scope.
510
+
511
+ # ========================================================================
512
+ # PROPERTIES
513
+ # ========================================================================
514
+
515
+ def data_ptr(self) -> int:
516
+ """Raw CUDA pointer. Triton kernels read this."""
517
+ return self._data_ptr + self._offset * self._elem_size
518
+
519
+ @property
520
+ def shape(self) -> Tuple[int, ...]:
521
+ return self._shape
522
+
523
+ @property
524
+ def dtype(self):
525
+ """Returns triton.language dtype for Triton JIT compatibility."""
526
+ return _get_tl_dtype(self._dtype)
527
+
528
+ @property
529
+ def nbx_dtype(self) -> NBXDtype:
530
+ """Internal dtype — zero dependency."""
531
+ return self._dtype
532
+
533
+ @property
534
+ def device(self):
535
+ """Device descriptor. Returns self (NBXTensor is its own device context)."""
536
+ return self
537
+
538
+ @property
539
+ def type(self) -> str:
540
+ """Device type string for device.type compatibility."""
541
+ return self._device
542
+
543
+ @property
544
+ def index(self) -> int:
545
+ """Device index for device.index compatibility."""
546
+ return 0 # default GPU 0
547
+
548
+ @property
549
+ def raw_device(self) -> str:
550
+ return self._device
551
+
552
+ @property
553
+ def ndim(self) -> int:
554
+ return len(self._shape)
555
+
556
+ def numel(self) -> int:
557
+ """Element count. Method (not property) for ATen API compat."""
558
+ return self._numel
559
+
560
+ def nbytes(self) -> int:
561
+ return self._nbytes
562
+
563
+ def element_size(self) -> int:
564
+ return dtype_size(self._dtype)
565
+
566
+ def is_contiguous(self) -> bool:
567
+ """Method for ATen API compat. Returns the flag computed at
568
+ construction (C6a): _shape/_strides are never mutated after
569
+ __init__ — every view/reshape/slice constructs a new NBXTensor."""
570
+ return self._contig
571
+
572
+ def is_expanded(self) -> bool:
573
+ """True when the view replays elements along a broadcast axis.
574
+
575
+ Expand views have one or more (shape > 1, stride == 0) axes, which
576
+ means `numel * element_size > actual_backing_bytes`. Any caller that
577
+ intends to `memcpy(nbytes)` across a device boundary MUST first
578
+ materialise via `contiguous()` — otherwise the H2D / D2D read runs
579
+ past the real allocation and copies garbage.
580
+ """
581
+ return any(st == 0 and sh > 1
582
+ for sh, st in zip(self._shape, self._strides))
583
+
584
+ def spans_densely(self) -> bool:
585
+ """True when the view's elements tile exactly one dense span of
586
+ `nbytes` starting at data_ptr() — contiguous, or a permutation of a
587
+ contiguous layout (a transpose). False for an expand view (stride 0
588
+ on a broadcast axis) and for a narrow on an inner axis (gaps between
589
+ rows): a single memcpy of nbytes copies the wrong bytes from those.
590
+ The one definition: `triton.device_transfer.transfer_tensor` asks it too."""
591
+ if self._numel == 0:
592
+ return True
593
+ expected = 1
594
+ for sh, st in sorted(((sh, st) for sh, st in zip(self._shape, self._strides)
595
+ if sh != 1), key=lambda p: p[1]):
596
+ if st != expected:
597
+ return False
598
+ expected *= sh
599
+ return True
600
+
601
+ @property
602
+ def is_cuda(self) -> bool:
603
+ return self._device == 'cuda'
604
+
605
+ @property
606
+ def is_cpu(self) -> bool:
607
+ """True for NBXTensor allocated on host memory (zero3 offload,
608
+ pinned or unpinned). Every NBXTensor is either CUDA or CPU
609
+ today; there is no third state."""
610
+ return self._device == 'cpu'
611
+
612
+ @property
613
+ def is_pinned(self) -> bool:
614
+ """True for CPU-backed tensors allocated via cudaMallocHost
615
+ (fast non_blocking H2D DMA). False otherwise, including all
616
+ CUDA tensors."""
617
+ return self._pinned
618
+
619
+ # ========================================================================
620
+ # TYPE CHECKS
621
+ # ========================================================================
622
+
623
+ def is_floating_point(self) -> bool:
624
+ return self._dtype in _FLOATING_DTYPES
625
+
626
+ def is_complex(self) -> bool:
627
+ return self._dtype in _COMPLEX_DTYPES
628
+
629
+ # ========================================================================
630
+ # COMPLEX SUPPORT — complex64/128 stored as interleaved [real, imag] pairs
631
+ # (numpy '<c8'/'<c16', torch convention). Reinterprets are zero-copy when
632
+ # contiguous: the complex storage IS the float [..,N,2] storage. _offset is
633
+ # in elements of _dtype, so the complex↔float offset converts ×2 / ÷2.
634
+ # ========================================================================
635
+
636
+ def view_as_real(self) -> 'NBXTensor':
637
+ """complex [..,N] → float [..,N,2] (interleaved real/imag)."""
638
+ if not self.is_complex():
639
+ return self
640
+ x = self if self.is_contiguous() else self.contiguous()
641
+ fdt = NBXDtype.float64 if x._dtype == NBXDtype.complex128 else NBXDtype.float32
642
+ new_shape = x._shape + (2,)
643
+ return NBXTensor(x._data_ptr, new_shape, contiguous_strides(new_shape),
644
+ fdt, x._device, x._offset * 2,
645
+ device_idx=x._device_idx,
646
+ base=x._base if x._base is not None else x, pinned=x._pinned)
647
+
648
+ def view_as_complex(self) -> 'NBXTensor':
649
+ """float [..,N,2] → complex [..,N] (interleaved real/imag).
650
+
651
+ complex64 requires fp32 real/imag pairs and complex128 requires fp64
652
+ pairs — there is no complex32. A half-precision input (fp16/bf16, e.g.
653
+ a rotary-embedding q/k narrowed to the V100 compute dtype) therefore
654
+ CANNOT be reinterpreted as complex64 in place: the storage is half the
655
+ size the complex64 tag implies, so the downstream complex kernels read
656
+ past the data and produce garbage/Inf. Promote fp16/bf16 to fp32 first
657
+ (a real copy), then tag complex64. fp32→complex64 and fp64→complex128
658
+ stay zero-copy views.
659
+ """
660
+ x = self if self.is_contiguous() else self.contiguous()
661
+ if x._dtype in (NBXDtype.float16, NBXDtype.bfloat16):
662
+ x = x.to(NBXDtype.float32)
663
+ assert x._shape and x._shape[-1] == 2, \
664
+ f"view_as_complex expects last dim == 2, got {x._shape}"
665
+ cdt = NBXDtype.complex128 if x._dtype == NBXDtype.float64 else NBXDtype.complex64
666
+ new_shape = x._shape[:-1]
667
+ return NBXTensor(x._data_ptr, new_shape, contiguous_strides(new_shape),
668
+ cdt, x._device, x._offset // 2,
669
+ device_idx=x._device_idx,
670
+ base=x._base if x._base is not None else x, pinned=x._pinned)
671
+
672
+ @property
673
+ def real(self) -> 'NBXTensor':
674
+ """Real part (strided view, stride 2) of a complex tensor; self if real."""
675
+ if not self.is_complex():
676
+ return self
677
+ vr = self.view_as_real()
678
+ return vr.select(vr.ndim - 1, 0)
679
+
680
+ @property
681
+ def imag(self) -> 'NBXTensor':
682
+ """Imaginary part (strided view, stride 2) of a complex tensor."""
683
+ if not self.is_complex():
684
+ return NBXTensor.zeros(self._shape, dtype=self._dtype, device=self._device)
685
+ vr = self.view_as_real()
686
+ return vr.select(vr.ndim - 1, 1)
687
+
688
+ # ========================================================================
689
+ # SHAPE QUERY METHODS
690
+ # ========================================================================
691
+
692
+ def size(self, dim=None):
693
+ if dim is not None:
694
+ return self._shape[dim]
695
+ return self._shape
696
+
697
+ def dim(self) -> int:
698
+ return len(self._shape)
699
+
700
+ def stride(self, dim=None):
701
+ if dim is not None:
702
+ return self._strides[dim]
703
+ return self._strides
704
+
705
+ # ========================================================================
706
+ # __cuda_array_interface__ — standard CUDA interop
707
+ # ========================================================================
708
+
709
+ @property
710
+ def __cuda_array_interface__(self):
711
+ return {
712
+ 'shape': self._shape,
713
+ 'typestr': _DTYPE_TYPESTR[self._dtype],
714
+ 'data': (self.data_ptr(), False),
715
+ 'version': 3,
716
+ 'strides': tuple(s * dtype_size(self._dtype) for s in self._strides),
717
+ }
718
+
719
+ # ========================================================================
720
+ # FACTORY METHODS — raw cudaMalloc
721
+ # ========================================================================
722
+
723
+ @staticmethod
724
+ def empty(shape, dtype=None, device='cuda') -> 'NBXTensor':
725
+ if isinstance(shape, int):
726
+ shape = (shape,)
727
+ if isinstance(shape, list):
728
+ shape = tuple(shape)
729
+ nbx_dt = dtype if isinstance(dtype, NBXDtype) else parse_dtype(str(dtype)) if dtype else NBXDtype.float32
730
+ dev_str = 'cuda'
731
+ # C3: resolve an explicit device index WITHOUT touching the
732
+ # driver; cudaGetDevice fires at most once per allocation, and
733
+ # not at all on the hot path (explicit device matching the
734
+ # cached current device).
735
+ dev_idx = None
736
+ if isinstance(device, str):
737
+ if ':' in device:
738
+ dev_idx = int(device.split(':')[1])
739
+ elif hasattr(device, '_device_idx'):
740
+ dev_idx = device._device_idx
741
+ elif hasattr(device, 'index') and device.index is not None:
742
+ dev_idx = device.index
743
+
744
+ numel = math.prod(shape) if shape else 1
745
+ nbytes = numel * dtype_size(nbx_dt)
746
+ if nbytes == 0:
747
+ if dev_idx is None:
748
+ dev_idx = DeviceAllocator.get_device()
749
+ return NBXTensor(0, shape, contiguous_strides(shape), nbx_dt, dev_str, device_idx=dev_idx)
750
+
751
+ if dev_idx is None:
752
+ # No explicit device: allocate on the current one. Single
753
+ # query, no switch, and the resolved idx is threaded into
754
+ # malloc_cuda so it does not re-query.
755
+ dev_idx = DeviceAllocator.get_device()
756
+ ptr = DeviceAllocator.malloc_cuda(nbytes, dev_idx=dev_idx)
757
+ elif dev_idx == _rt._cached_device_idx():
758
+ # Explicit device already fully current (C2 cache invariant
759
+ # guarantees the runtime device equals the cached idx): no
760
+ # driver query, no switch.
761
+ ptr = DeviceAllocator.malloc_cuda(nbytes, dev_idx=dev_idx)
762
+ else:
763
+ # Multi-GPU: allocate on the requested device, restore the
764
+ # previous one after. The restore target comes from a real
765
+ # driver query (never from the cache) so a cold/invalidated
766
+ # cache can never misdirect the restore.
767
+ cur = DeviceAllocator.get_device()
768
+ if cur != dev_idx:
769
+ DeviceAllocator.set_device(dev_idx)
770
+ ptr = DeviceAllocator.malloc_cuda(nbytes, dev_idx=dev_idx)
771
+ if cur != dev_idx:
772
+ DeviceAllocator.set_device(cur)
773
+
774
+ return NBXTensor(ptr, shape, contiguous_strides(shape), nbx_dt, dev_str,
775
+ owns_data=True, device_idx=dev_idx)
776
+
777
+ @staticmethod
778
+ def zeros(shape, dtype=None, device='cuda') -> 'NBXTensor':
779
+ t = NBXTensor.empty(shape, dtype, device)
780
+ if t._nbytes > 0:
781
+ DeviceAllocator.memset_cuda(t._data_ptr, 0, t._nbytes)
782
+ return t
783
+
784
+ @staticmethod
785
+ def ones(shape, dtype=None, device='cuda') -> 'NBXTensor':
786
+ return _fill_constant(NBXTensor.empty(shape, dtype, device), 1.0)
787
+
788
+ @staticmethod
789
+ def empty_like(other: 'NBXTensor', dtype=None, device=None) -> 'NBXTensor':
790
+ dev = device if device else f"cuda:{other._device_idx}"
791
+ return NBXTensor.empty(other._shape,
792
+ dtype if dtype is not None else other._dtype,
793
+ dev)
794
+
795
+ @staticmethod
796
+ def zeros_like(other: 'NBXTensor', dtype=None, device=None) -> 'NBXTensor':
797
+ return NBXTensor.zeros(other._shape,
798
+ dtype if dtype is not None else other._dtype,
799
+ device if device else other._device)
800
+
801
+ @staticmethod
802
+ def ones_like(other: 'NBXTensor', dtype=None, device=None) -> 'NBXTensor':
803
+ return NBXTensor.ones(other._shape,
804
+ dtype if dtype is not None else other._dtype,
805
+ device if device else other._device)
806
+
807
+ @staticmethod
808
+ def from_numpy(arr, dtype: 'Optional[NBXDtype]' = None) -> 'NBXTensor':
809
+ """Load numpy array to GPU. For weight loading from safetensors.
810
+
811
+ `dtype` names the NBX dtype the BITS in `arr` already are, for the
812
+ types numpy cannot express — bf16 travels in a uint16 container. It
813
+ is authoritative and must agree with the container's element size:
814
+ the buffer allocated here is `arr.nbytes`, and a tensor whose dtype
815
+ implies a different element size declares a length its own
816
+ allocation does not have. That mismatch used to be created by
817
+ retagging `_dtype` after construction (the tensor kept the
818
+ `_elem_size` / `_nbytes` of the dtype it was BUILT with): TinyLlama's
819
+ 22 rotary tables, uint16 bf16 bits mistagged float32 by the fallback
820
+ below, each declared 524288 bytes over a 262144-byte buffer, and
821
+ every consumer that trusts `_nbytes` then ran off the end into
822
+ whichever allocation followed it — which is why the answer depended
823
+ on the sequence length. Measured 2026-09-07 on Apple.
824
+
825
+ An unmapped numpy dtype is refused rather than silently called
826
+ float32: guessing an element size is what produced the above.
827
+ """
828
+ import numpy as np
829
+ dtype_name = str(arr.dtype)
830
+ if dtype is not None:
831
+ nbx_dt = dtype if isinstance(dtype, NBXDtype) else parse_dtype(dtype)
832
+ if dtype_size(nbx_dt) != arr.dtype.itemsize:
833
+ raise ValueError(
834
+ f"from_numpy: dtype {nbx_dt.name} is "
835
+ f"{dtype_size(nbx_dt)} bytes per element but the numpy "
836
+ f"container '{dtype_name}' is {arr.dtype.itemsize}; the "
837
+ f"buffer is sized from the container, so the two must "
838
+ f"agree.")
839
+ elif dtype_name in _NUMPY_DTYPE_MAP:
840
+ nbx_dt = _NUMPY_DTYPE_MAP[dtype_name]
841
+ else:
842
+ raise TypeError(
843
+ f"from_numpy: numpy dtype '{dtype_name}' has no NBX "
844
+ f"equivalent. Pass dtype=NBXDtype.<t> to declare what the "
845
+ f"bits are (bf16 travels as uint16); do not let the element "
846
+ f"size be guessed.")
847
+ arr_c = np.ascontiguousarray(arr)
848
+ nbytes = arr_c.nbytes
849
+ shape = arr_c.shape
850
+ dev_idx = DeviceAllocator.get_device()
851
+ ptr = DeviceAllocator.malloc_cuda(nbytes)
852
+ # H2D copy
853
+ DeviceAllocator.memcpy(ptr, arr_c.ctypes.data, nbytes, kind=1)
854
+ return NBXTensor(ptr, shape, contiguous_strides(shape), nbx_dt, 'cuda',
855
+ owns_data=True, device_idx=dev_idx)
856
+
857
+ @staticmethod
858
+ def from_raw(ptr: int, shape: Tuple[int, ...], dtype: NBXDtype,
859
+ device: str = 'cuda', owns_data: bool = False,
860
+ device_idx: int = 0, base=None) -> 'NBXTensor':
861
+ """Wrap an existing raw pointer. For interop at boundaries.
862
+
863
+ Args:
864
+ base: Reference to the object owning the memory (prevents GC).
865
+ For torch.Tensor → NBXTensor conversion, pass the torch.Tensor
866
+ so it stays alive as long as the NBXTensor exists.
867
+ """
868
+ return NBXTensor(ptr, shape, contiguous_strides(shape), dtype, device,
869
+ owns_data=owns_data, device_idx=device_idx, base=base)
870
+
871
+ # ------------------------------------------------------------------
872
+ # CPU form — zero3 weight offload and similar scenarios where
873
+ # weights live on host memory between promotions to GPU. Uses numpy
874
+ # for unpinned backing (default) or cudaMallocHost for pinned
875
+ # backing (faster H2D DMA). Zero torch dependency.
876
+ # ------------------------------------------------------------------
877
+
878
+ @staticmethod
879
+ def empty_cpu(shape, dtype: 'Optional[NBXDtype]' = None,
880
+ pinned: bool = False) -> 'NBXTensor':
881
+ """Allocate a CPU-backed NBXTensor.
882
+
883
+ Unpinned (pinned=False, default): backed by a numpy array. Cheap
884
+ to allocate, usable by numpy-based prep code, but incurs a
885
+ pageable→pinned staging copy on H2D.
886
+
887
+ Pinned (pinned=True): backed by cudaMallocHost-allocated memory.
888
+ Enables non_blocking DMA at ~2× unpinned throughput. Used by
889
+ zero3 for weights that will be transferred on the hot path.
890
+ """
891
+ if isinstance(shape, int):
892
+ shape = (shape,)
893
+ if isinstance(shape, list):
894
+ shape = tuple(shape)
895
+ nbx_dt = dtype if isinstance(dtype, NBXDtype) else (
896
+ parse_dtype(str(dtype)) if dtype else NBXDtype.float32)
897
+ numel = math.prod(shape) if shape else 1
898
+ elem = dtype_size(nbx_dt)
899
+ nbytes = numel * elem
900
+
901
+ if nbytes == 0:
902
+ return NBXTensor(0, shape, contiguous_strides(shape), nbx_dt,
903
+ 'cpu', device_idx=0)
904
+
905
+ if pinned:
906
+ ptr = DeviceAllocator.malloc_host_pinned(nbytes)
907
+ return NBXTensor(ptr, shape, contiguous_strides(shape), nbx_dt,
908
+ 'cpu', owns_data=True, device_idx=0,
909
+ pinned=True)
910
+ else:
911
+ # Numpy backing — keep the array alive via _base.
912
+ import numpy as np
913
+ np_dtype_name = {
914
+ NBXDtype.float32: np.float32,
915
+ NBXDtype.float16: np.float16,
916
+ NBXDtype.bfloat16: np.uint16, # bf16 stored as uint16 bits
917
+ NBXDtype.float64: np.float64,
918
+ NBXDtype.int64: np.int64,
919
+ NBXDtype.int32: np.int32,
920
+ NBXDtype.int16: np.int16,
921
+ NBXDtype.int8: np.int8,
922
+ NBXDtype.uint8: np.uint8,
923
+ NBXDtype.bool_: np.uint8,
924
+ NBXDtype.complex64: np.complex64,
925
+ NBXDtype.complex128: np.complex128,
926
+ }
927
+ # TOTAL, and it REFUSES what it does not know. `.get(nbx_dt,
928
+ # np.float32)` stood here with no complex entry, so the host buffer
929
+ # was allocated at four bytes an element while `to_cpu` copied
930
+ # `self._nbytes`, eight bytes an element, into it. Double the buffer,
931
+ # every complex readback, at every size: `malloc(): corrupted top
932
+ # size`. And not at the copy -- the first small probe printed a
933
+ # correct-looking array and the NEXT allocation died, which is how it
934
+ # survived. complex128 was the same bug at four times over.
935
+ #
936
+ # A default makes "a dtype I do not know" indistinguishable from
937
+ # "float32": a wrong-sized allocation waiting for that dtype to
938
+ # arrive. Every NBXDtype is named above; a new one that is not gets a
939
+ # refusal naming itself, not a corrupted heap.
940
+ if nbx_dt not in np_dtype_name:
941
+ raise RuntimeError(
942
+ f"empty_cpu has no numpy backing for {nbx_dt}: add it to the "
943
+ f"map above. Defaulting would size the host buffer for "
944
+ f"another dtype and the copy that follows would write past it.")
945
+ arr = np.empty(shape, dtype=np_dtype_name[nbx_dt])
946
+ return NBXTensor(arr.ctypes.data, shape,
947
+ contiguous_strides(shape), nbx_dt, 'cpu',
948
+ owns_data=True, device_idx=0,
949
+ base=arr, # keep numpy alive
950
+ pinned=False)
951
+
952
+ def to_cuda(self, device_idx: int = 0) -> 'NBXTensor':
953
+ """Copy this tensor to a CUDA device. Returns a new GPU NBXTensor.
954
+
955
+ Handles CPU→GPU (kind=1 H2D) and GPU→GPU (kind=3 D2D). If the
956
+ tensor is already on the requested CUDA device, returns self.
957
+ A view whose elements do not tile one dense span (an expand view's
958
+ stride-0 axis, or a narrow on an inner axis — a stacked expert
959
+ slab's gate half) is materialised first: the copy is ONE memcpy of
960
+ nbytes from data_ptr, which is the view's bytes only when they are
961
+ dense.
962
+ """
963
+ if self._device == 'cuda' and self._device_idx == device_idx:
964
+ return self
965
+ src = self if self.spans_densely() else self.contiguous()
966
+ DeviceAllocator.set_device(device_idx)
967
+ ptr = DeviceAllocator.malloc_cuda(src._nbytes)
968
+ if src._nbytes > 0:
969
+ # kind: 1 = H2D if source is CPU, 3 = D2D if source is GPU.
970
+ kind = 1 if src._device == 'cpu' else 3
971
+ DeviceAllocator.memcpy(ptr, src.data_ptr(), src._nbytes, kind=kind)
972
+ return NBXTensor(ptr, src._shape, src._strides, src._dtype,
973
+ 'cuda', owns_data=True, device_idx=device_idx)
974
+
975
+ def to_cuda_async(self, device_idx: int = 0, stream: int = 0) -> 'NBXTensor':
976
+ """Non-blocking variant of to_cuda for zero3 pipelining.
977
+
978
+ Enqueues the memcpy on `stream` instead of running synchronously.
979
+ Caller is responsible for sequencing — either via stream_wait_event
980
+ before reading the returned tensor, or via stream_synchronize.
981
+
982
+ For H2D to actually overlap with compute on the default stream, the
983
+ source NBXTensor MUST be pinned (self._pinned == True). Otherwise
984
+ the driver falls back to a synchronous bounce-buffer copy and no
985
+ overlap happens.
986
+
987
+ Returns a GPU NBXTensor backed by a freshly-malloc'd pointer. The
988
+ allocation itself is synchronous (cudaMalloc); only the fill is
989
+ asynchronous.
990
+ """
991
+ if self._device == 'cuda' and self._device_idx == device_idx:
992
+ return self
993
+ src = self if self.spans_densely() else self.contiguous()
994
+ DeviceAllocator.set_device(device_idx)
995
+ ptr = DeviceAllocator.malloc_cuda(src._nbytes)
996
+ if src._nbytes > 0:
997
+ kind = 1 if src._device == 'cpu' else 3
998
+ DeviceAllocator.memcpy_async(
999
+ ptr, src.data_ptr(), src._nbytes,
1000
+ kind=kind, stream=stream)
1001
+ return NBXTensor(ptr, src._shape, src._strides, src._dtype,
1002
+ 'cuda', owns_data=True, device_idx=device_idx)
1003
+
1004
+ def to_cpu(self, pinned: bool = False) -> 'NBXTensor':
1005
+ """Copy this tensor to host memory. Returns a new CPU NBXTensor.
1006
+
1007
+ pinned=True allocates via cudaMallocHost for fast future H2D
1008
+ promotion; pinned=False uses numpy backing.
1009
+ """
1010
+ if self._device == 'cpu' and self._pinned == pinned:
1011
+ return self
1012
+ dst = NBXTensor.empty_cpu(self._shape, self._dtype, pinned=pinned)
1013
+ # The destination is row-major: a source that is not (a transpose, a
1014
+ # narrow on an inner axis, an expand) is laid out row-major on its own
1015
+ # side first — one memcpy of nbytes from data_ptr is its elements only
1016
+ # when it already is.
1017
+ src = self if self.is_contiguous() else self.contiguous()
1018
+ if src._nbytes > 0:
1019
+ # kind: 2 = D2H from GPU, 0 = H2H if we're re-packing CPU.
1020
+ kind = 2 if src._device == 'cuda' else 0
1021
+ DeviceAllocator.memcpy(dst.data_ptr(), src.data_ptr(),
1022
+ src._nbytes, kind=kind)
1023
+ # A device result crossing to the host is an observation, and a
1024
+ # D2H copy has already flushed. Checking here rather than only at
1025
+ # `sync_device` matters because a decode never calls that: it
1026
+ # reads its logits, and this is that read.
1027
+ if kind == 2:
1028
+ _faults.check_device_faults()
1029
+ return dst
1030
+
1031
+ def numpy(self):
1032
+ """Host numpy array (boundary conversion — copies to CPU). Used at the
1033
+ triton output boundary (e.g. saving a waveform). float/int dtypes only;
1034
+ bf16 has no numpy dtype (cast to float32 first)."""
1035
+ import numpy as np
1036
+ import ctypes
1037
+ f = self.contiguous()
1038
+ if f._device != 'cpu':
1039
+ f = f.to_cpu()
1040
+ typestr = _DTYPE_TYPESTR[f._dtype] # every member has its type string in the dtype table
1041
+ nbytes = f.numel() * dtype_size(f._dtype)
1042
+ # A view over the host buffer, then one copy. `ctypes.string_at(ptr, n)` hands `n` to
1043
+ # PyBytes_FromStringAndSize as a C int: a tensor of 2 GiB or more read back as
1044
+ # "Negative size passed to PyBytes_FromStringAndSize" (the certifier on 2026-09-07:
1045
+ # seven census shapes whose fp32 output exceeds 2 GiB — a 4K convolution, a
1046
+ # 1,221,120-row matmul, a 16384² baddbmm — "no config could run").
1047
+ if nbytes == 0:
1048
+ return np.empty(tuple(self._shape), dtype=np.dtype(typestr))
1049
+ buf = (ctypes.c_uint8 * nbytes).from_address(f.data_ptr())
1050
+ view = np.ctypeslib.as_array(buf).view(np.dtype(typestr)).reshape(tuple(self._shape))
1051
+ out = view.copy()
1052
+ del view, buf # nothing outlives the host buffer
1053
+ return out
1054
+
1055
+ def pin_host(self) -> 'NBXTensor':
1056
+ """Promote an unpinned CPU tensor to pinned host memory.
1057
+
1058
+ No-op if the tensor is already pinned. Used by Zero3Strategy to
1059
+ accelerate subsequent H2D transfers during block-wise execution.
1060
+ Raises on non-CPU tensors — pinning a GPU tensor is nonsensical.
1061
+ """
1062
+ if self._device != 'cpu':
1063
+ raise RuntimeError(
1064
+ f"pin_host() called on device={self._device}; pinning only "
1065
+ f"applies to CPU-backed NBXTensors")
1066
+ if self._pinned:
1067
+ return self
1068
+ return self.to_cpu(pinned=True)
1069
+
1070
+ # ========================================================================
1071
+ # METADATA OPS — pure Python, zero data movement
1072
+ # ========================================================================
1073
+
1074
+ def view(self, *shape) -> 'NBXTensor':
1075
+ # Match PyTorch's aten::view tolerance: if the source tensor is
1076
+ # expanded (stride-0 broadcast) or otherwise non-contiguous, the
1077
+ # old path silently re-strode the view as contiguous while the
1078
+ # underlying memory still held the unmaterialized broadcast —
1079
+ # later kernel launches walked past the end of the backing
1080
+ # allocation and got "Pointer argument cannot be accessed from
1081
+ # Triton" on the second batch iteration (Janus RoPE bmm). Matches
1082
+ # reshape's fallback: contiguous() first, then re-stride.
1083
+ if not self.is_contiguous():
1084
+ return self.contiguous().view(*shape)
1085
+ if len(shape) == 1 and isinstance(shape[0], (list, tuple)):
1086
+ shape = tuple(shape[0])
1087
+ neg_idx = None
1088
+ known = 1
1089
+ for i, s in enumerate(shape):
1090
+ if s == -1:
1091
+ neg_idx = i
1092
+ else:
1093
+ known *= s
1094
+ if neg_idx is not None:
1095
+ shape = list(shape)
1096
+ shape[neg_idx] = self._numel // known
1097
+ shape = tuple(shape)
1098
+ return NBXTensor(self._data_ptr, shape, contiguous_strides(shape),
1099
+ self._dtype, self._device, self._offset, device_idx=self._device_idx, base=self._base if self._base is not None else self, pinned=self._pinned)
1100
+
1101
+ def reshape(self, *shape) -> 'NBXTensor':
1102
+ if len(shape) == 1 and isinstance(shape[0], (list, tuple)):
1103
+ shape = tuple(shape[0])
1104
+ if self.is_contiguous():
1105
+ return self.view(*shape)
1106
+ return self.contiguous().view(*shape)
1107
+
1108
+ def unflatten(self, dim: int, sizes) -> 'NBXTensor':
1109
+ """Split ONE dim into `sizes` (their product its extent), as torch's `unflatten` does: a pure
1110
+ view on any strides — the split dim walks memory at its own stride, so its parts walk it at
1111
+ that stride times the sizes after them. `reshape` makes a non-contiguous tensor contiguous
1112
+ first: a sliced stretch's reading of a strided input copied the WHOLE input for every slice
1113
+ (HAT-S at 2048 px, q/k/v views of the 6 912 MB qkv, 3 x 2 304 MB per pass, 2026-10-22)."""
1114
+ dim = dim % self.ndim
1115
+ sizes = [int(v) for v in sizes]
1116
+ prod = 1
1117
+ for v in sizes:
1118
+ prod *= v
1119
+ if prod != int(self._shape[dim]):
1120
+ raise ValueError(f"unflatten: sizes {sizes} do not make extent {self._shape[dim]} of dim {dim}")
1121
+ st, inner = [], int(self._strides[dim])
1122
+ for v in reversed(sizes):
1123
+ st.append(inner)
1124
+ inner *= v
1125
+ shape = tuple(self._shape[:dim]) + tuple(sizes) + tuple(self._shape[dim + 1:])
1126
+ strides = tuple(self._strides[:dim]) + tuple(reversed(st)) + tuple(self._strides[dim + 1:])
1127
+ return self.as_strided(shape, strides)
1128
+
1129
+ def merge_dims(self, d0: int, d1: int) -> 'NBXTensor':
1130
+ """Merge two ADJACENT dims into one — as a pure view when the
1131
+ strides allow it, else via reshape (which materialises).
1132
+
1133
+ The merge is a legal view iff stride(d0) == shape(d1)*stride(d1)
1134
+ — the two dims already walk memory as one. This holds on tensors
1135
+ whose non-contiguity lives in ANOTHER dim: the bucketed KV-cache
1136
+ view `buffer[:, :, :padded, :]` is non-contiguous in the T dim,
1137
+ so plain `reshape(BH, T, D)` copied the whole padded K/V per
1138
+ layer per decode step (the copies-at-source census, 2026-08-23),
1139
+ while its (B, H) pair still merges for free. Callers that feed
1140
+ the merged tensor to a kernel must handle its strides
1141
+ (`bmm(..., allow_strided_b=True)`); the fallback keeps the
1142
+ semantics identical for every stride pattern.
1143
+ """
1144
+ d0 = d0 % self.ndim
1145
+ d1 = d1 % self.ndim
1146
+ if d1 != d0 + 1:
1147
+ raise ValueError(f"merge_dims needs adjacent dims, got {d0},{d1}")
1148
+ if self._strides[d0] != self._shape[d1] * self._strides[d1]:
1149
+ new_shape = (self._shape[:d0]
1150
+ + (self._shape[d0] * self._shape[d1],)
1151
+ + self._shape[d1 + 1:])
1152
+ return self.reshape(new_shape)
1153
+ new_shape = (self._shape[:d0]
1154
+ + (self._shape[d0] * self._shape[d1],)
1155
+ + self._shape[d1 + 1:])
1156
+ new_strides = (self._strides[:d0] + (self._strides[d1],)
1157
+ + self._strides[d1 + 1:])
1158
+ return NBXTensor(self._data_ptr, new_shape, new_strides,
1159
+ self._dtype, self._device, self._offset,
1160
+ device_idx=self._device_idx,
1161
+ base=self._base if self._base is not None else self,
1162
+ pinned=self._pinned)
1163
+
1164
+ def unsqueeze(self, dim: int) -> 'NBXTensor':
1165
+ dim = dim % (self.ndim + 1) if dim >= 0 else dim + self.ndim + 1
1166
+ new_shape = list(self._shape)
1167
+ new_strides = list(self._strides)
1168
+ stride_val = new_strides[dim] * self._shape[dim] if dim < len(self._strides) else 1
1169
+ new_shape.insert(dim, 1)
1170
+ new_strides.insert(dim, stride_val)
1171
+ return NBXTensor(self._data_ptr, tuple(new_shape), tuple(new_strides),
1172
+ self._dtype, self._device, self._offset, device_idx=self._device_idx, base=self._base if self._base is not None else self, pinned=self._pinned)
1173
+
1174
+ def squeeze(self, dim=None) -> 'NBXTensor':
1175
+ if dim is not None:
1176
+ dim = dim % self.ndim
1177
+ if self._shape[dim] != 1:
1178
+ return self
1179
+ s = list(self._shape)
1180
+ st = list(self._strides)
1181
+ s.pop(dim)
1182
+ st.pop(dim)
1183
+ else:
1184
+ s, st = [], []
1185
+ for sz, sr in zip(self._shape, self._strides):
1186
+ if sz != 1:
1187
+ s.append(sz)
1188
+ st.append(sr)
1189
+ return NBXTensor(self._data_ptr, tuple(s), tuple(st),
1190
+ self._dtype, self._device, self._offset, device_idx=self._device_idx, base=self._base if self._base is not None else self, pinned=self._pinned)
1191
+
1192
+ def permute(self, *dims) -> 'NBXTensor':
1193
+ if len(dims) == 1 and isinstance(dims[0], (list, tuple)):
1194
+ dims = tuple(dims[0])
1195
+ return NBXTensor(self._data_ptr,
1196
+ tuple(self._shape[d] for d in dims),
1197
+ tuple(self._strides[d] for d in dims),
1198
+ self._dtype, self._device, self._offset, device_idx=self._device_idx, base=self._base if self._base is not None else self, pinned=self._pinned)
1199
+
1200
+ def transpose(self, dim0: int, dim1: int) -> 'NBXTensor':
1201
+ dim0, dim1 = dim0 % self.ndim, dim1 % self.ndim
1202
+ s = list(self._shape)
1203
+ st = list(self._strides)
1204
+ s[dim0], s[dim1] = s[dim1], s[dim0]
1205
+ st[dim0], st[dim1] = st[dim1], st[dim0]
1206
+ return NBXTensor(self._data_ptr, tuple(s), tuple(st),
1207
+ self._dtype, self._device, self._offset, device_idx=self._device_idx, base=self._base if self._base is not None else self, pinned=self._pinned)
1208
+
1209
+ def t(self) -> 'NBXTensor':
1210
+ if self.ndim < 2:
1211
+ return self
1212
+ return self.transpose(0, 1)
1213
+
1214
+ @property
1215
+ def T(self) -> 'NBXTensor':
1216
+ return self.transpose(-2, -1) if self.ndim >= 2 else self
1217
+
1218
+ def expand(self, *shape) -> 'NBXTensor':
1219
+ if len(shape) == 1 and isinstance(shape[0], (list, tuple)):
1220
+ shape = tuple(shape[0])
1221
+ new_s, new_st = broadcast_strides(self._shape, self._strides, shape)
1222
+ return NBXTensor(self._data_ptr, new_s, new_st,
1223
+ self._dtype, self._device, self._offset, device_idx=self._device_idx, base=self._base if self._base is not None else self, pinned=self._pinned)
1224
+
1225
+ def expand_as(self, other) -> 'NBXTensor':
1226
+ return self.expand(*other._shape if isinstance(other, NBXTensor) else other.shape)
1227
+
1228
+ def repeat(self, *sizes) -> 'NBXTensor':
1229
+ """aten::repeat — TILE the tensor `sizes[i]` times along each dim
1230
+ (numpy.tile / torch.Tensor.repeat), NOT element-wise repeat_interleave.
1231
+ When len(sizes) > ndim, leading singleton dims are prepended. R33-pure:
1232
+ view → expand (stride-0 broadcast) → contiguous → reshape; the
1233
+ interleaved `[1,d0,1,d1,…]` view collapses to `[r0*d0, r1*d1, …]`.
1234
+ """
1235
+ if len(sizes) == 1 and isinstance(sizes[0], (list, tuple)):
1236
+ sizes = tuple(sizes[0])
1237
+ sizes = tuple(int(s) for s in sizes)
1238
+ m = len(sizes)
1239
+ assert m >= self.ndim, f"repeat sizes {sizes} fewer than ndim {self.ndim}"
1240
+ x = self.contiguous()
1241
+ base = (1,) * (m - x.ndim) + tuple(x._shape)
1242
+ view_shape, expand_shape, out_shape = [], [], []
1243
+ for i in range(m):
1244
+ view_shape += [1, base[i]]
1245
+ expand_shape += [sizes[i], base[i]]
1246
+ out_shape.append(sizes[i] * base[i])
1247
+ return (x.view(tuple(view_shape))
1248
+ .expand(tuple(expand_shape))
1249
+ .contiguous()
1250
+ .reshape(tuple(out_shape)))
1251
+
1252
+ def narrow(self, dim: int, start: int, length: int) -> 'NBXTensor':
1253
+ dim = dim % self.ndim
1254
+ s = list(self._shape)
1255
+ s[dim] = length
1256
+ new_off = self._offset + start * self._strides[dim]
1257
+ return NBXTensor(self._data_ptr, tuple(s), self._strides,
1258
+ self._dtype, self._device, new_off, device_idx=self._device_idx, base=self._base if self._base is not None else self, pinned=self._pinned)
1259
+
1260
+ def select(self, dim: int, index: int) -> 'NBXTensor':
1261
+ dim = dim % self.ndim
1262
+ # PyTorch semantics: negative index counts from the end.
1263
+ # Without this, `select(0, -1)` on a (300,) contiguous tensor
1264
+ # produced `new_off = -1 * 1 = -1`, and `data_ptr() = base - elem`
1265
+ # pointed 4 bytes BEFORE the cudaMalloc'd block. Triton's
1266
+ # cuPointerGetAttribute then flagged the pointer as CPU memory
1267
+ # and raised "Pointer argument (at 0) cannot be accessed from
1268
+ # Triton (cpu tensor?)" (PixArt T5 encoder aten.select+aten.add
1269
+ # chain at position_ids build).
1270
+ if index < 0:
1271
+ index = self._shape[dim] + index
1272
+ new_off = self._offset + index * self._strides[dim]
1273
+ s = list(self._shape)
1274
+ st = list(self._strides)
1275
+ s.pop(dim)
1276
+ st.pop(dim)
1277
+ return NBXTensor(self._data_ptr, tuple(s), tuple(st),
1278
+ self._dtype, self._device, new_off, device_idx=self._device_idx, base=self._base if self._base is not None else self, pinned=self._pinned)
1279
+
1280
+ def flatten(self, start_dim: int = 0, end_dim: int = -1) -> 'NBXTensor':
1281
+ start_dim = start_dim % self.ndim
1282
+ end_dim = end_dim % self.ndim
1283
+ if start_dim == end_dim:
1284
+ return self
1285
+ flat = 1
1286
+ for i in range(start_dim, end_dim + 1):
1287
+ flat *= self._shape[i]
1288
+ new_s = list(self._shape[:start_dim]) + [flat] + list(self._shape[end_dim + 1:])
1289
+ return self.view(*new_s)
1290
+
1291
+ def contiguous(self) -> 'NBXTensor':
1292
+ if self.is_contiguous():
1293
+ return self
1294
+ if self._device == 'cpu':
1295
+ return self._contiguous_cpu()
1296
+ new = NBXTensor.empty(self._shape, self._dtype, f"cuda:{self._device_idx}")
1297
+ n = self._numel
1298
+ if n > 0:
1299
+ _strided_copy(self, new)
1300
+ return new
1301
+
1302
+ def _contiguous_cpu(self) -> 'NBXTensor':
1303
+ """CPU-side materialization of a non-contiguous view.
1304
+
1305
+ The shared _strided_copy path launches a Triton kernel over GPU
1306
+ pointers, so it cannot reach host memory. This helper performs
1307
+ the same strided → contiguous copy in numpy (C-implemented
1308
+ `ascontiguousarray` over an `as_strided` view) and preserves the
1309
+ source's pinned / unpinned backing. Expand views (stride == 0)
1310
+ materialize correctly because numpy replays the same source
1311
+ element along zero-stride axes.
1312
+ """
1313
+ import numpy as np
1314
+ dst = NBXTensor.empty_cpu(self._shape, self._dtype, pinned=self._pinned)
1315
+ if self._numel == 0:
1316
+ return dst
1317
+ esz = dtype_size(self._dtype)
1318
+ # Byte window touched by the strided view starting at self.data_ptr():
1319
+ # last-reachable element offset + one element for esz bytes.
1320
+ max_elem_offset = 1 + sum(
1321
+ (dim - 1) * st for dim, st in zip(self._shape, self._strides)
1322
+ if dim > 0)
1323
+ n_bytes = max_elem_offset * esz
1324
+ src_bytes = np.ctypeslib.as_array(
1325
+ ctypes.cast(self.data_ptr(), ctypes.POINTER(ctypes.c_ubyte)),
1326
+ shape=(n_bytes,),
1327
+ )
1328
+ byte_strides = tuple(s * esz for s in self._strides)
1329
+ strided_view = np.lib.stride_tricks.as_strided(
1330
+ src_bytes,
1331
+ shape=tuple(self._shape) + (esz,),
1332
+ strides=byte_strides + (1,),
1333
+ )
1334
+ contig = np.ascontiguousarray(strided_view)
1335
+ ctypes.memmove(dst.data_ptr(), contig.ctypes.data, dst._nbytes)
1336
+ return dst
1337
+
1338
+ def unfold(self, dimension: int, size: int, step: int) -> 'NBXTensor':
1339
+ dimension = dimension % self.ndim
1340
+ n = (self._shape[dimension] - size) // step + 1
1341
+ s = list(self._shape)
1342
+ st = list(self._strides)
1343
+ s[dimension] = n
1344
+ s.append(size)
1345
+ st[dimension] = self._strides[dimension] * step
1346
+ st.append(self._strides[dimension])
1347
+ return NBXTensor(self._data_ptr, tuple(s), tuple(st),
1348
+ self._dtype, self._device, self._offset, device_idx=self._device_idx, base=self._base if self._base is not None else self, pinned=self._pinned)
1349
+
1350
+ def as_strided(self, size, stride, storage_offset=None) -> 'NBXTensor':
1351
+ off = storage_offset if storage_offset is not None else self._offset
1352
+ return NBXTensor(self._data_ptr, tuple(size), tuple(stride),
1353
+ self._dtype, self._device, off, device_idx=self._device_idx, base=self._base if self._base is not None else self, pinned=self._pinned)
1354
+
1355
+ def view_as(self, other) -> 'NBXTensor':
1356
+ s = other._shape if isinstance(other, NBXTensor) else other.shape
1357
+ return self.view(*s)
1358
+
1359
+ def reshape_as(self, other) -> 'NBXTensor':
1360
+ s = other._shape if isinstance(other, NBXTensor) else other.shape
1361
+ return self.reshape(*s)
1362
+
1363
+ def movedim(self, src: int, dst: int) -> 'NBXTensor':
1364
+ src, dst = src % self.ndim, dst % self.ndim
1365
+ dims = list(range(self.ndim))
1366
+ dims.pop(src)
1367
+ dims.insert(dst, src)
1368
+ return self.permute(*dims)
1369
+
1370
+ def __getitem__(self, key):
1371
+ """Basic tensor indexing: t[0], t[:, :, 0], t[:, 2:5], etc.
1372
+
1373
+ Fancy indexing by Python list of integers (e.g.
1374
+ t[:, [0, -3, -2, -1]]) is NOT supported. NBXTensor enforces
1375
+ explicit slicing via narrow, cat, or index_select_wrapper to
1376
+ guarantee numerical and structural parity with torch. Prior to
1377
+ this guard, list keys were silently no-op'd, returning the
1378
+ original tensor unchanged — a bomb that masked a real bug in
1379
+ the Sana CHI handler (see commit log). The crash is intentional
1380
+ and forces the explicit pattern at every call site.
1381
+ """
1382
+ # Reject fancy indexing by list — silently no-op'd before, now hard-fails.
1383
+ if isinstance(key, list):
1384
+ raise RuntimeError(
1385
+ "ZERO FALLBACK: NBXTensor does not support fancy indexing by "
1386
+ "Python list (e.g. tensor[[0, 1, 2]]). Use narrow + cat for "
1387
+ "static patterns or index_select_wrapper for dynamic indices. "
1388
+ "See src/neurobrix/core/components/handlers/text_encoder_handler.py "
1389
+ "for an example of the narrow + cat pattern."
1390
+ )
1391
+ if isinstance(key, tuple):
1392
+ for k in key:
1393
+ if isinstance(k, list):
1394
+ raise RuntimeError(
1395
+ "ZERO FALLBACK: NBXTensor does not support fancy indexing "
1396
+ "by Python list within a tuple (e.g. tensor[:, [0, 1, 2]]). "
1397
+ "Use narrow + cat for static patterns or "
1398
+ "index_select_wrapper for dynamic indices. See "
1399
+ "src/neurobrix/core/components/handlers/text_encoder_handler.py "
1400
+ "for an example of the narrow + cat pattern."
1401
+ )
1402
+ if isinstance(key, int):
1403
+ return self.select(0, key)
1404
+ # P-TRITON-CHAIN-CPU-POINTER C3d 2026-05-15: handle negative slice
1405
+ # start/stop per torch / numpy contract. Previous code did
1406
+ # `k.start or 0` which silently converted None → 0 but left
1407
+ # negative values untouched. For `t[-N:]` (slice(-N, None)),
1408
+ # `start` stayed at `-N` and `narrow(dim, -N, length)` computed
1409
+ # `new_off = offset - N*stride` → data_ptr pointed BEFORE the
1410
+ # cudaMalloc'd block → Triton's cuPointerGetAttribute rejected
1411
+ # the pointer with "Pointer argument (at 0) cannot be accessed
1412
+ # from Triton (cpu tensor?)". The Sana 4Kpx VAE chain wrapper's
1413
+ # `halo_carry[:, :, -top_size:, :]` triggered this at the third
1414
+ # iteration of the 4096² chains (chains 7-9 of run).
1415
+ if isinstance(key, slice):
1416
+ start, stop = slice_bounds(key.start, key.stop, self._shape[0], key.step)
1417
+ return self.narrow(0, start, stop - start)
1418
+ if isinstance(key, tuple):
1419
+ result = self
1420
+ dim_offset = 0
1421
+ for dim, k in enumerate(key):
1422
+ actual_dim = dim - dim_offset
1423
+ if isinstance(k, int):
1424
+ result = result.select(actual_dim, k)
1425
+ dim_offset += 1 # select reduces ndim
1426
+ elif isinstance(k, slice):
1427
+ start, stop = slice_bounds(k.start, k.stop, result._shape[actual_dim], k.step)
1428
+ result = result.narrow(
1429
+ actual_dim, start, stop - start)
1430
+ # slice(None) = : → no-op on this dim
1431
+ return result
1432
+
1433
+ def __setitem__(self, key, value):
1434
+ """Indexed write: buffer[..., start:end, :] = source.
1435
+
1436
+ Handles the KV cache pattern: contiguous slice assignment.
1437
+ Uses strided_scatter_kernel when the destination view is non-contiguous.
1438
+ Guards: dtype cast and device transfer before copy.
1439
+ """
1440
+ dst = self[key]
1441
+ if isinstance(value, NBXTensor):
1442
+ # Dtype guard: a complex source is cast first (the nd copy kernel
1443
+ # is real-only); a real source is converted on the copy's store.
1444
+ if value._dtype != dst._dtype and (value.is_complex() or dst.is_complex()):
1445
+ value = value.to(dst._dtype)
1446
+ # Device guard: transfer source to destination device
1447
+ if hasattr(value, '_device_idx') and hasattr(dst, '_device_idx') and value._device_idx != dst._device_idx:
1448
+ src_contig = value.contiguous()
1449
+ tmp = NBXTensor.empty(src_contig._shape, src_contig._dtype, f"cuda:{dst._device_idx}")
1450
+ DeviceAllocator.memcpy(tmp.data_ptr(), src_contig.data_ptr(), src_contig._nbytes, kind=3)
1451
+ value = tmp
1452
+ if (value.is_contiguous() and dst.is_contiguous()
1453
+ and tuple(value._shape) == tuple(dst._shape)):
1454
+ DeviceAllocator.memcpy(dst.data_ptr(), value.data_ptr(), value._nbytes)
1455
+ elif dst._numel > 0:
1456
+ # One launch reads the source by its strides and writes the
1457
+ # view by its own — no contiguous copy of the source first
1458
+ # (the KV cache's V arrives as a head-strided view: its write
1459
+ # was a strided copy, a cast and a scatter per layer per token).
1460
+ _copy_nd(value, dst)
1461
+
1462
+ def unbind(self, dim: int = 0) -> tuple:
1463
+ dim = dim % self.ndim
1464
+ return tuple(self.select(dim, i) for i in range(self._shape[dim]))
1465
+
1466
+ # ========================================================================
1467
+ # DATA OPS — cudaMemcpy
1468
+ # ========================================================================
1469
+
1470
+ def clone(self) -> 'NBXTensor':
1471
+ new = NBXTensor.empty(self._shape, self._dtype, f"cuda:{self._device_idx}")
1472
+ n = self._numel
1473
+ if n > 0:
1474
+ if self.is_contiguous():
1475
+ DeviceAllocator.memcpy(new._data_ptr, self.data_ptr(), self._nbytes)
1476
+ else:
1477
+ _strided_copy(self, new)
1478
+ return new
1479
+
1480
+ def detach(self) -> 'NBXTensor':
1481
+ return self # no autograd in triton mode
1482
+
1483
+ def fill_(self, value) -> 'NBXTensor':
1484
+ if self._nbytes == 0:
1485
+ return self
1486
+ # aten::fill carries its value as a 0-dim tensor (e.g. via
1487
+ # aten::lift_fresh); collapse it to a host scalar before the kernel.
1488
+ if isinstance(value, NBXTensor):
1489
+ value = value.item()
1490
+ if value == 0:
1491
+ DeviceAllocator.memset_cuda(self.data_ptr(), 0, self._nbytes)
1492
+ return self
1493
+ # Non-zero fill via the pure-Triton fill_kernel.
1494
+ return _fill_constant(self, value)
1495
+
1496
+ def copy_(self, src, non_blocking: bool = False) -> 'NBXTensor':
1497
+ if isinstance(src, NBXTensor):
1498
+ # PyTorch Tensor.copy_ converts src to self's dtype. A raw byte
1499
+ # memcpy across a dtype boundary reinterprets the bits — e.g. an
1500
+ # fp16 source written into an fp32 destination decodes the fp16
1501
+ # bit-pairs as fp32 (≈0), and the byte counts differ (the chatterbox
1502
+ # s3gen prompt-feat copy: fp16 src 50240 B into fp32 dst 100480 B).
1503
+ # Cast to self's dtype first, then contiguous-pack the source so the
1504
+ # flat copy of self._nbytes bytes is well-defined.
1505
+ if src.dtype != self.dtype and (src.is_complex() or self.is_complex()):
1506
+ src = src.to(self.dtype)
1507
+ if not (src.is_contiguous() and self.is_contiguous()
1508
+ and src.dtype == self.dtype
1509
+ and tuple(src._shape) == tuple(self._shape)) and not self.is_complex():
1510
+ # Strides on either side, a dtype to convert, or a source to
1511
+ # broadcast: one launch, the source read in place.
1512
+ _copy_nd(src, self)
1513
+ return self
1514
+ if not src.is_contiguous():
1515
+ src = src.contiguous()
1516
+ if not self.is_contiguous():
1517
+ # Destination is a STRIDED view, e.g. an in-place slice-assignment
1518
+ # `buf[..., :k] = src` that the tracer lowers to slice + copy_.
1519
+ # A flat memcpy of self._nbytes would write the source CONTIGUOUSLY
1520
+ # at self.data_ptr(), ignoring the view's strides — scattering the
1521
+ # data to the wrong addresses. (Wan RoPE builds the rotated Q/K via
1522
+ # an empty_like buffer + slice-copy; the flat memcpy left the buffer
1523
+ # garbage → zero self-attn Q/K → washed-out VACE.) Scatter the
1524
+ # contiguous src into self's strided layout via the same dedicated
1525
+ # kernel `__setitem__` and `_fill_constant` use. R33-pure.
1526
+ #
1527
+ # The scatter maps flat src index i -> dst's strided position of
1528
+ # multi-index i, so it requires equal element counts. A broadcast
1529
+ # copy_ (src smaller than dst) into a strided view is unsupported
1530
+ # — surface it loudly rather than silently under-writing. The old
1531
+ # flat memcpy was already broken for that case (it read past src),
1532
+ # so this raises no previously-working path.
1533
+ if src._numel != self._numel:
1534
+ raise RuntimeError(
1535
+ "NBXTensor.copy_ into a strided (non-contiguous) "
1536
+ "destination requires matching element counts; "
1537
+ "broadcast copy_ into a strided view is unsupported "
1538
+ f"(src._numel={src._numel} dst._numel={self._numel}).")
1539
+ if self.is_complex():
1540
+ # The scatter kernel sees a plain float ptr; reinterpret both
1541
+ # as a float view with a trailing [2] (re/imag) so each complex
1542
+ # element's two floats land at the correct strided positions.
1543
+ # Mirrors _strided_copy's complex handling.
1544
+ _fdt = (NBXDtype.float64 if self._dtype == NBXDtype.complex128
1545
+ else NBXDtype.float32)
1546
+ _src_shape2 = tuple(src._shape) + (2,)
1547
+ _fsrc = NBXTensor(
1548
+ src._data_ptr, _src_shape2, contiguous_strides(_src_shape2),
1549
+ _fdt, src._device, src._offset * 2,
1550
+ device_idx=src._device_idx,
1551
+ base=src._base if src._base is not None else src,
1552
+ pinned=src._pinned)
1553
+ _fdst = NBXTensor(
1554
+ self._data_ptr, tuple(self._shape) + (2,),
1555
+ tuple(s * 2 for s in self._strides) + (1,),
1556
+ _fdt, self._device, self._offset * 2,
1557
+ device_idx=self._device_idx,
1558
+ base=self._base if self._base is not None else self,
1559
+ pinned=self._pinned)
1560
+ _strided_scatter(_fsrc, _fdst)
1561
+ else:
1562
+ _strided_scatter(src, self)
1563
+ return self
1564
+ DeviceAllocator.memcpy(self.data_ptr(), src.data_ptr(), self._nbytes)
1565
+ return self
1566
+
1567
+ def item(self) -> float:
1568
+ """Read single scalar from GPU via cudaMemcpy D2H."""
1569
+ assert self._numel == 1, f"item() requires 1 element, got {self._numel}"
1570
+ esz = dtype_size(self._dtype)
1571
+ buf = (ctypes.c_char * esz)()
1572
+ DeviceAllocator.memcpy(ctypes.addressof(buf), self.data_ptr(), esz, kind=2)
1573
+
1574
+ if self._dtype == NBXDtype.float32:
1575
+ return ctypes.cast(buf, ctypes.POINTER(ctypes.c_float))[0]
1576
+ elif self._dtype == NBXDtype.float64:
1577
+ return ctypes.cast(buf, ctypes.POINTER(ctypes.c_double))[0]
1578
+ elif self._dtype == NBXDtype.int32:
1579
+ return ctypes.cast(buf, ctypes.POINTER(ctypes.c_int32))[0]
1580
+ elif self._dtype == NBXDtype.int64:
1581
+ return ctypes.cast(buf, ctypes.POINTER(ctypes.c_int64))[0]
1582
+ elif self._dtype == NBXDtype.float16:
1583
+ raw = ctypes.cast(buf, ctypes.POINTER(ctypes.c_uint16))[0]
1584
+ return struct.unpack('e', struct.pack('H', raw))[0]
1585
+ elif self._dtype == NBXDtype.bool_:
1586
+ return bool(ctypes.cast(buf, ctypes.POINTER(ctypes.c_uint8))[0])
1587
+ return 0.0
1588
+
1589
+ # ========================================================================
1590
+ # DTYPE CONVERSION
1591
+ # ========================================================================
1592
+
1593
+ def to(self, *args, **kwargs) -> 'NBXTensor':
1594
+ target = None
1595
+ if args:
1596
+ a = args[0]
1597
+ if isinstance(a, NBXDtype):
1598
+ target = a
1599
+ elif isinstance(a, str):
1600
+ # Distinguish device string ("cuda:0") from dtype string ("float32")
1601
+ if 'cuda' in a or 'cpu' in a:
1602
+ return self # .to(device) — already on device
1603
+ target = parse_dtype(a)
1604
+ elif isinstance(a, NBXTensor):
1605
+ return self
1606
+ else:
1607
+ # Check if it's a device-like object (has .type attribute = "cuda"/"cpu")
1608
+ s = str(a)
1609
+ if 'cuda' in s or 'cpu' in s or 'device' in s:
1610
+ return self # .to(device) — already on device
1611
+ target = parse_dtype(s)
1612
+ if 'dtype' in kwargs:
1613
+ d = kwargs['dtype']
1614
+ target = d if isinstance(d, NBXDtype) else parse_dtype(str(d))
1615
+ if target is None or target == self._dtype:
1616
+ return self
1617
+ # Complex tensors must never be downcast to a REAL dtype via .to():
1618
+ # PyTorch itself raises on complex->real .to() (real conversions go via
1619
+ # .real / view_as_real). When it appears here it is always a spurious
1620
+ # compute-dtype downcast of a complex tensor (e.g. the RoPE frequency
1621
+ # table, stored complex128) by the AMP/Prism dtype pipeline — which would
1622
+ # reinterpret the interleaved real/imag bytes as a single real array and
1623
+ # poison the value with NaN. Preserve the complex tensor. complex<->complex
1624
+ # casts (complex128<->complex64) still proceed below.
1625
+ if self.is_complex() and target not in _COMPLEX_DTYPES:
1626
+ return self
1627
+ # A float64 element on a backend whose kernels have none is not cast, it is misread:
1628
+ # refused by name (`backend_has_fp64`, asked only when fp64 is in play). The caller places the array at a dtype the
1629
+ # backend has (a float64 formula result is cast on the host first).
1630
+ if (self._dtype in (NBXDtype.float64, NBXDtype.complex128)
1631
+ or target in (NBXDtype.float64, NBXDtype.complex128)) and not _caps.backend_has_fp64():
1632
+ raise RuntimeError(
1633
+ f"ZERO FALLBACK: a cast from {self._dtype.name} to {target.name} on the "
1634
+ f"'{_rt.gpu_backend()}' backend, whose kernels have no float64: its lowering "
1635
+ f"narrows double in silence and the result would be garbage, not a cast "
1636
+ f"(2026-09-28: 1 139 of 143 360 bits right on the Sana 4K positional embedding). "
1637
+ f"Place the array at a dtype this backend has; a float64 formula result is cast "
1638
+ f"on the host first.")
1639
+ # Cast via Triton copy kernel (tl.store auto-converts dtype).
1640
+ # Downcasts from wider floats to float16 use the PROTECTED
1641
+ # conversion (clamp finite values to ±65504) — the triton mirror
1642
+ # of the compiled DtypeEngine contract; raw tl.store overflows
1643
+ # bf16/fp32-range sentinels (finfo.min mask fills) to ±inf.
1644
+ _sat = _saturating_cast(self._dtype, target)
1645
+ new = NBXTensor.empty(self._shape, target, f"cuda:{self._device_idx}")
1646
+ n = self._numel
1647
+ if n > 0 and not self.is_contiguous() and not self.is_complex():
1648
+ # a strided source is read in place and cast on the store —
1649
+ # one launch instead of a contiguous copy and a cast of it
1650
+ _copy_nd(self, new)
1651
+ return new
1652
+ if n > 0:
1653
+ from nbxtensor.ops.copy_op import copy_kernel
1654
+ import triton
1655
+ src = self.contiguous()
1656
+ _rt.set_device(src)
1657
+ _m = _ew_meta(n) # the certified tile of the count's class (ops/elementwise_launch.py)
1658
+ copy_kernel[(triton.cdiv(n, _m['BLOCK_SIZE']),)](src, new, n,
1659
+ BLOCK_SIZE=_m['BLOCK_SIZE'], num_warps=_m['num_warps'],
1660
+ SATURATE_F16=_sat)
1661
+ return new
1662
+
1663
+ def float(self) -> 'NBXTensor':
1664
+ return self.to(NBXDtype.float32)
1665
+
1666
+ def half(self) -> 'NBXTensor':
1667
+ return self.to(NBXDtype.float16)
1668
+
1669
+ def bfloat16(self) -> 'NBXTensor':
1670
+ return self.to(NBXDtype.bfloat16)
1671
+
1672
+ def int(self) -> 'NBXTensor':
1673
+ return self.to(NBXDtype.int32)
1674
+
1675
+ def long(self) -> 'NBXTensor':
1676
+ return self.to(NBXDtype.int64)
1677
+
1678
+ def type_as(self, other) -> 'NBXTensor':
1679
+ target = other._dtype if isinstance(other, NBXTensor) else parse_dtype(str(other.dtype))
1680
+ return self.to(target)
1681
+
1682
+ # ========================================================================
1683
+ # CREATION HELPERS
1684
+ # ========================================================================
1685
+
1686
+ def new_zeros(self, size, **kwargs) -> 'NBXTensor':
1687
+ return NBXTensor.zeros(tuple(size) if isinstance(size, list) else size,
1688
+ kwargs.get('dtype', self._dtype),
1689
+ kwargs.get('device', self._device))
1690
+
1691
+ def new_ones(self, size, **kwargs) -> 'NBXTensor':
1692
+ # Must actually FILL ones — the former `NBXTensor.empty` returned
1693
+ # uninitialized memory (a leftover TODO). The Allegro/T5 extended
1694
+ # attention mask opens with `aten::new_ones([]) & (positions >= 0)`;
1695
+ # the uninitialized 0-dim byte read as 0 -> False -> the AND zeroed the
1696
+ # whole [1,1,S,1] q-side -> extended mask = -inf on ALL keys -> every
1697
+ # T5 self-attention row went UNIFORM (softmax of a constant row), so
1698
+ # positive-prompt embeddings washed toward the pad-token mean (cond
1699
+ # branch corr 0.9288 vs the sequential oracle while the empty-prompt
1700
+ # uncond branch stayed clean).
1701
+ return NBXTensor.ones(tuple(size) if isinstance(size, list) else size,
1702
+ kwargs.get('dtype', self._dtype),
1703
+ kwargs.get('device', self._device))
1704
+
1705
+ def new_empty(self, size, **kwargs) -> 'NBXTensor':
1706
+ return NBXTensor.empty(tuple(size) if isinstance(size, list) else size,
1707
+ kwargs.get('dtype', self._dtype),
1708
+ kwargs.get('device', self._device))
1709
+
1710
+ # ========================================================================
1711
+ # STATIC ASSEMBLY — cat, stack (require data copy)
1712
+ # ========================================================================
1713
+
1714
+ @staticmethod
1715
+ def cat(tensors: list, dim: int = 0) -> 'NBXTensor':
1716
+ # Filter out 0-dim scalars and empty tensors (Gemma2 attention pattern)
1717
+ valid = [t for t in tensors if t.ndim > 0 and t._numel > 0]
1718
+ if not valid:
1719
+ return tensors[0] if tensors else NBXTensor.empty((0,), NBXDtype.float32, 'cuda')
1720
+ if len(valid) == 1:
1721
+ return valid[0]
1722
+ tensors = valid
1723
+ # Complex cat: the copy kernel concatenates the interleaved [re,im] float
1724
+ # storage as if it were flat floats. For dim==0 that is correct (whole
1725
+ # element-blocks stay contiguous), but for any inner dim it splits the
1726
+ # boundary mid-pair, pairing re of one operand with im of the next
1727
+ # (the Wan rotary freqs concat per-axis along the last dim). Route via
1728
+ # the real view: cat on the same element dim with the [.,2] pair axis
1729
+ # appended (untouched), then reinterpret as complex.
1730
+ if valid[0].is_complex():
1731
+ ndc = valid[0].ndim
1732
+ dd = dim % ndc
1733
+ real_parts = [t.view_as_real() for t in valid]
1734
+ return NBXTensor.cat(real_parts, dd).view_as_complex()
1735
+ dim = dim % tensors[0].ndim
1736
+ # Contiguous-guard (manifesto pattern): the cat copy kernels
1737
+ # flat-index their sources; a strided view (e.g. the MLA rope
1738
+ # de-interleave slices x[..., 0::2]) fed directly reads wrong
1739
+ # addresses — layout-dependent illegal access at runtime.
1740
+ # Contiguous inputs short-circuit at zero cost.
1741
+ tensors = [t.contiguous() for t in tensors]
1742
+ # Align dtypes — cat_copy_kernel requires all inputs same type
1743
+ target_dtype = tensors[0]._dtype
1744
+ tensors = [t.to(target_dtype) if t._dtype != target_dtype else t for t in tensors]
1745
+ # Align devices — all tensors must be on same GPU
1746
+ target_dev = tensors[0]._device_idx
1747
+ aligned = []
1748
+ for t in tensors:
1749
+ if hasattr(t, '_device_idx') and t._device_idx != target_dev:
1750
+ src = t.contiguous()
1751
+ dst = NBXTensor.empty(src._shape, src._dtype, f"cuda:{target_dev}")
1752
+ DeviceAllocator.memcpy(dst.data_ptr(), src.data_ptr(), src._nbytes, kind=3)
1753
+ aligned.append(dst)
1754
+ else:
1755
+ aligned.append(t)
1756
+ tensors = aligned
1757
+ out_shape = list(tensors[0]._shape)
1758
+ out_shape[dim] = sum(t._shape[dim] for t in tensors)
1759
+ # Allocate on same device as first input tensor
1760
+ DeviceAllocator.set_device(tensors[0]._device_idx)
1761
+ out = NBXTensor.empty(tuple(out_shape), target_dtype, tensors[0]._device)
1762
+
1763
+ if dim == 0:
1764
+ # Simple case: concat on first dim → contiguous slices
1765
+ offset = 0
1766
+ for t in tensors:
1767
+ tc = t.contiguous()
1768
+ if tc._nbytes > 0:
1769
+ DeviceAllocator.memcpy(
1770
+ out._data_ptr + offset * _DTYPE_SIZES[out._dtype],
1771
+ tc.data_ptr(), tc._nbytes)
1772
+ offset += tc._numel
1773
+ else:
1774
+ # General case: use cat_copy_kernel which handles dim offsets
1775
+ import triton
1776
+ from nbxtensor.ops.cat_op import cat_copy_kernel_4
1777
+
1778
+ dim_size_out = out_shape[dim]
1779
+ # Product of dims after cat dim
1780
+ dim_prod_post = 1
1781
+ for d in range(dim + 1, len(out_shape)):
1782
+ dim_prod_post *= out_shape[d]
1783
+
1784
+ # Process tensors in groups of 4 (kernel handles up to 4)
1785
+ dim_offset = 0
1786
+ for batch_start in range(0, len(tensors), 4):
1787
+ batch = tensors[batch_start:batch_start + 4]
1788
+ # Pad to 4 with dummy (zero-element) tensors
1789
+ contig = [t.contiguous() for t in batch]
1790
+ while len(contig) < 4:
1791
+ contig.append(out) # dummy, total_elements=0
1792
+
1793
+ sizes = [c._shape[dim] if i < len(batch) else 0 for i, c in enumerate(contig)]
1794
+ totals = [c._numel if i < len(batch) else 0 for i, c in enumerate(contig)]
1795
+ offsets = []
1796
+ off = dim_offset
1797
+ for s in sizes:
1798
+ offsets.append(off)
1799
+ off += s
1800
+
1801
+ max_total = max(totals)
1802
+ if max_total == 0:
1803
+ continue
1804
+ _m = _ew_meta(max_total) # the certified tile of the count's class (ops/elementwise_launch.py)
1805
+ BLOCK = _m['BLOCK_SIZE']
1806
+ grid = (triton.cdiv(max_total, BLOCK), len(batch))
1807
+ _rt.set_device(out)
1808
+ cat_copy_kernel_4[grid](
1809
+ out,
1810
+ contig[0], contig[1], contig[2], contig[3],
1811
+ sizes[0], sizes[1], sizes[2], sizes[3],
1812
+ dim_size_out, dim_prod_post,
1813
+ offsets[0], offsets[1], offsets[2], offsets[3],
1814
+ totals[0], totals[1], totals[2], totals[3],
1815
+ BLOCK_X=BLOCK, num_warps=_m['num_warps'],
1816
+ )
1817
+ dim_offset = off
1818
+
1819
+ return out
1820
+
1821
+ @staticmethod
1822
+ def stack(tensors: list, dim: int = 0) -> 'NBXTensor':
1823
+ return NBXTensor.cat([t.unsqueeze(dim) for t in tensors], dim=dim)
1824
+
1825
+ # ========================================================================
1826
+ # REPR
1827
+ # ========================================================================
1828
+
1829
+ # ========================================================================
1830
+ # PYTHON OPERATORS — delegate to kernel wrappers
1831
+ # ========================================================================
1832
+
1833
+ def __add__(self, other):
1834
+ from nbxtensor.ops.elementwise_wrappers import add
1835
+ return add(self, other)
1836
+
1837
+ def __radd__(self, other):
1838
+ from nbxtensor.ops.elementwise_wrappers import add
1839
+ return add(other, self)
1840
+
1841
+ def __sub__(self, other):
1842
+ from nbxtensor.ops.elementwise_wrappers import sub
1843
+ return sub(self, other)
1844
+
1845
+ def __rsub__(self, other):
1846
+ from nbxtensor.ops.elementwise_wrappers import rsub
1847
+ return rsub(self, other)
1848
+
1849
+ def __mul__(self, other):
1850
+ from nbxtensor.ops.elementwise_wrappers import mul
1851
+ return mul(self, other)
1852
+
1853
+ def __rmul__(self, other):
1854
+ from nbxtensor.ops.elementwise_wrappers import mul
1855
+ return mul(other, self)
1856
+
1857
+ def __truediv__(self, other):
1858
+ from nbxtensor.ops.elementwise_wrappers import div
1859
+ return div(self, other)
1860
+
1861
+ def __neg__(self):
1862
+ from nbxtensor.ops.elementwise_wrappers import neg
1863
+ return neg(self)
1864
+
1865
+ def __repr__(self) -> str:
1866
+ return (f"NBXTensor(shape={self._shape}, dtype={self._dtype.name}, "
1867
+ f"device={self._device}, ptr=0x{self._data_ptr:x})")