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.
- nbxtensor/THIRD_PARTY_NOTICES.md +287 -0
- nbxtensor/__init__.py +17 -0
- nbxtensor/backend/__init__.py +1 -0
- nbxtensor/backend/capabilities.py +207 -0
- nbxtensor/backend/cpu/__init__.py +1 -0
- nbxtensor/backend/cpu/blas.py +102 -0
- nbxtensor/backend/cpu/pool.py +204 -0
- nbxtensor/backend/cpu_backend.py +162 -0
- nbxtensor/backend/device_facts.py +105 -0
- nbxtensor/backend/device_files.py +129 -0
- nbxtensor/backend/device_names.py +18 -0
- nbxtensor/backend/devices/amd/cdna.yml +228 -0
- nbxtensor/backend/devices/amd/cdna2.yml +228 -0
- nbxtensor/backend/devices/amd/cdna3.yml +227 -0
- nbxtensor/backend/devices/apple/apple_m1.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m1_max.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m1_pro.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m1_ultra.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m2.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m2_max.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m2_pro.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m2_ultra.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m3.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m3_max.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m3_pro.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m3_ultra.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m4.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m4_max.yml +266 -0
- nbxtensor/backend/devices/apple/apple_m4_pro.yml +308 -0
- nbxtensor/backend/devices/apple/apple_m5.yml +269 -0
- nbxtensor/backend/devices/apple/apple_m5_max.yml +269 -0
- nbxtensor/backend/devices/apple/apple_m5_pro.yml +269 -0
- nbxtensor/backend/devices/apple/apple_m5_ultra.yml +269 -0
- nbxtensor/backend/devices/apple/apple_m6.yml +269 -0
- nbxtensor/backend/devices/apple/apple_silicon.yml +263 -0
- nbxtensor/backend/devices/nvidia/ampere.yml +214 -0
- nbxtensor/backend/devices/nvidia/hopper.yml +213 -0
- nbxtensor/backend/devices/nvidia/volta.yml +411 -0
- nbxtensor/backend/faults.py +151 -0
- nbxtensor/backend/host_memory.py +185 -0
- nbxtensor/backend/launcher.py +2350 -0
- nbxtensor/backend/launcher_contract.py +429 -0
- nbxtensor/backend/metal_backend.py +661 -0
- nbxtensor/backend/metal_device.py +1274 -0
- nbxtensor/backend/process_memory.py +73 -0
- nbxtensor/backend/replay_graph.py +322 -0
- nbxtensor/backend/runtime.py +409 -0
- nbxtensor/backend/triton_ext_driver.py +735 -0
- nbxtensor/dtype/__init__.py +1 -0
- nbxtensor/dtype/convert.py +107 -0
- nbxtensor/dtype/dtypes.yml +70 -0
- nbxtensor/dtype/itemsize.py +166 -0
- nbxtensor/dtype/nbx_dtype.py +72 -0
- nbxtensor/dtype/operands.py +158 -0
- nbxtensor/dtype/outputs.py +64 -0
- nbxtensor/dtype/policy.py +155 -0
- nbxtensor/dtype/policy.yml +51 -0
- nbxtensor/dtype/promotion.py +112 -0
- nbxtensor/host.py +55 -0
- nbxtensor/io/__init__.py +1 -0
- nbxtensor/io/read_rate.py +197 -0
- nbxtensor/io/safetensors.py +314 -0
- nbxtensor/memory/__init__.py +9 -0
- nbxtensor/memory/allocator.py +1988 -0
- nbxtensor/memory/arena.py +75 -0
- nbxtensor/memory/device_memory.py +150 -0
- nbxtensor/memory/device_transfer.py +154 -0
- nbxtensor/memory/layout.py +43 -0
- nbxtensor/memory/memory_pool.py +191 -0
- nbxtensor/memory/metal.py +96 -0
- nbxtensor/memory/oom_advice.py +91 -0
- nbxtensor/memory/pointer_tables.py +323 -0
- nbxtensor/memory/pool.py +507 -0
- nbxtensor/memory/shadow.py +43 -0
- nbxtensor/memory/slab.py +273 -0
- nbxtensor/memory/trace_replay.py +122 -0
- nbxtensor/memory/traces/sana-video-2b-720p.16g.2026-11-02.tsv +13610 -0
- nbxtensor/memory/vmm.py +118 -0
- nbxtensor/ops/__init__.py +1 -0
- nbxtensor/ops/_autotune_policy.py +90 -0
- nbxtensor/ops/_common.py +32 -0
- nbxtensor/ops/_configs.py +1041 -0
- nbxtensor/ops/_cost.py +231 -0
- nbxtensor/ops/abs.py +17 -0
- nbxtensor/ops/adaptive_avg_pool2d.py +161 -0
- nbxtensor/ops/add.py +96 -0
- nbxtensor/ops/addcdiv.py +29 -0
- nbxtensor/ops/addcmul.py +29 -0
- nbxtensor/ops/addmv_op.py +64 -0
- nbxtensor/ops/addr.py +68 -0
- nbxtensor/ops/all_reduce.py +83 -0
- nbxtensor/ops/amax.py +40 -0
- nbxtensor/ops/any_reduce.py +83 -0
- nbxtensor/ops/arange_op.py +27 -0
- nbxtensor/ops/argmax.py +107 -0
- nbxtensor/ops/argmin.py +93 -0
- nbxtensor/ops/atan2.py +71 -0
- nbxtensor/ops/attention_wrappers.py +1648 -0
- nbxtensor/ops/autotune_bucket.py +137 -0
- nbxtensor/ops/autotune_cache.py +430 -0
- nbxtensor/ops/autotune_certified.py +1262 -0
- nbxtensor/ops/autotune_refusals.py +380 -0
- nbxtensor/ops/autotune_settings.py +58 -0
- nbxtensor/ops/avg_pool2d.py +73 -0
- nbxtensor/ops/baddbmm_op.py +132 -0
- nbxtensor/ops/batch_norm.py +163 -0
- nbxtensor/ops/binary_strided.py +196 -0
- nbxtensor/ops/bitwise_and.py +26 -0
- nbxtensor/ops/bitwise_left_shift.py +47 -0
- nbxtensor/ops/bitwise_not.py +24 -0
- nbxtensor/ops/bitwise_or.py +26 -0
- nbxtensor/ops/bitwise_right_shift.py +47 -0
- nbxtensor/ops/bucketize.py +37 -0
- nbxtensor/ops/cat_op.py +87 -0
- nbxtensor/ops/celu.py +20 -0
- nbxtensor/ops/clamp.py +24 -0
- nbxtensor/ops/clamp_min.py +22 -0
- nbxtensor/ops/conv2d.py +265 -0
- nbxtensor/ops/conv2d_m8n8k4.py +165 -0
- nbxtensor/ops/conv_depthwise2d.py +154 -0
- nbxtensor/ops/conv_transpose2d.py +148 -0
- nbxtensor/ops/conv_wrappers.py +1482 -0
- nbxtensor/ops/copy.py +18 -0
- nbxtensor/ops/copy_op.py +39 -0
- nbxtensor/ops/cos.py +17 -0
- nbxtensor/ops/count_nonzero.py +64 -0
- nbxtensor/ops/cross_entropy.py +67 -0
- nbxtensor/ops/cummax.py +109 -0
- nbxtensor/ops/cummin.py +105 -0
- nbxtensor/ops/cumsum.py +244 -0
- nbxtensor/ops/decode_attn_vec.py +162 -0
- nbxtensor/ops/depthwise_conv2d.py +132 -0
- nbxtensor/ops/dequant_gemv.py +190 -0
- nbxtensor/ops/dft.py +67 -0
- nbxtensor/ops/dispatch.py +1228 -0
- nbxtensor/ops/div.py +75 -0
- nbxtensor/ops/dot_op.py +67 -0
- nbxtensor/ops/dropout.py +23 -0
- nbxtensor/ops/dtype_convert.py +42 -0
- nbxtensor/ops/elementwise_launch.py +81 -0
- nbxtensor/ops/elementwise_wrappers.py +2315 -0
- nbxtensor/ops/elu.py +20 -0
- nbxtensor/ops/embedding.py +69 -0
- nbxtensor/ops/eq.py +32 -0
- nbxtensor/ops/erf.py +18 -0
- nbxtensor/ops/exp.py +17 -0
- nbxtensor/ops/exp2.py +18 -0
- nbxtensor/ops/fft_op.py +125 -0
- nbxtensor/ops/fill_op.py +20 -0
- nbxtensor/ops/flash_attention.py +307 -0
- nbxtensor/ops/flash_attention_m8n8k4.py +384 -0
- nbxtensor/ops/flash_decode.py +78 -0
- nbxtensor/ops/flip_op.py +60 -0
- nbxtensor/ops/floor.py +90 -0
- nbxtensor/ops/full_op.py +24 -0
- nbxtensor/ops/fused_moe.py +417 -0
- nbxtensor/ops/fused_moe_m8n8k4.py +99 -0
- nbxtensor/ops/fused_upsample_conv.py +413 -0
- nbxtensor/ops/gather_op.py +52 -0
- nbxtensor/ops/ge.py +32 -0
- nbxtensor/ops/gelu.py +31 -0
- nbxtensor/ops/gemv_vec.py +80 -0
- nbxtensor/ops/glu.py +43 -0
- nbxtensor/ops/grid_sampler.py +145 -0
- nbxtensor/ops/group_norm_tiled.py +67 -0
- nbxtensor/ops/groupnorm.py +123 -0
- nbxtensor/ops/gt.py +32 -0
- nbxtensor/ops/hardsigmoid.py +18 -0
- nbxtensor/ops/hardswish.py +19 -0
- nbxtensor/ops/im2col.py +57 -0
- nbxtensor/ops/index_add.py +95 -0
- nbxtensor/ops/index_put_op.py +92 -0
- nbxtensor/ops/index_select.py +121 -0
- nbxtensor/ops/index_wrappers.py +770 -0
- nbxtensor/ops/isfinite.py +20 -0
- nbxtensor/ops/isinf.py +20 -0
- nbxtensor/ops/isnan.py +18 -0
- nbxtensor/ops/launch_keys.py +1054 -0
- nbxtensor/ops/launch_observer.py +54 -0
- nbxtensor/ops/layernorm.py +71 -0
- nbxtensor/ops/le.py +35 -0
- nbxtensor/ops/leaky_relu.py +19 -0
- nbxtensor/ops/lerp.py +76 -0
- nbxtensor/ops/linalg_wrappers.py +1370 -0
- nbxtensor/ops/linspace_op.py +47 -0
- nbxtensor/ops/log.py +17 -0
- nbxtensor/ops/log_sigmoid.py +23 -0
- nbxtensor/ops/logical_and.py +26 -0
- nbxtensor/ops/logical_not.py +24 -0
- nbxtensor/ops/logical_or.py +26 -0
- nbxtensor/ops/logical_xor.py +37 -0
- nbxtensor/ops/loss_wrappers.py +282 -0
- nbxtensor/ops/lt.py +32 -0
- nbxtensor/ops/masked_fill.py +21 -0
- nbxtensor/ops/matmul.py +377 -0
- nbxtensor/ops/matmul_m8n8k4.py +235 -0
- nbxtensor/ops/max_pool2d.py +74 -0
- nbxtensor/ops/max_pool3d.py +93 -0
- nbxtensor/ops/max_reduce.py +91 -0
- nbxtensor/ops/maximum.py +18 -0
- nbxtensor/ops/mean.py +40 -0
- nbxtensor/ops/min_reduce.py +91 -0
- nbxtensor/ops/minimum.py +18 -0
- nbxtensor/ops/mish.py +22 -0
- nbxtensor/ops/mma_m8n8k4.py +152 -0
- nbxtensor/ops/moe_align.py +195 -0
- nbxtensor/ops/moe_decode_vec.py +374 -0
- nbxtensor/ops/moe_device_residency.py +141 -0
- nbxtensor/ops/moe_wrappers.py +345 -0
- nbxtensor/ops/mse_loss.py +79 -0
- nbxtensor/ops/mul.py +51 -0
- nbxtensor/ops/nan_to_num.py +23 -0
- nbxtensor/ops/ne.py +32 -0
- nbxtensor/ops/neg.py +17 -0
- nbxtensor/ops/nllloss.py +124 -0
- nbxtensor/ops/nonzero.py +33 -0
- nbxtensor/ops/nonzero_op.py +62 -0
- nbxtensor/ops/norm_wrappers.py +579 -0
- nbxtensor/ops/ones_op.py +20 -0
- nbxtensor/ops/oracles/__init__.py +6 -0
- nbxtensor/ops/oracles/conv2d_fp64.py +450 -0
- nbxtensor/ops/pad_op.py +159 -0
- nbxtensor/ops/paged_decode_attn.py +215 -0
- nbxtensor/ops/pixel_shuffle.py +144 -0
- nbxtensor/ops/pixel_unshuffle.py +72 -0
- nbxtensor/ops/pow.py +137 -0
- nbxtensor/ops/prod.py +81 -0
- nbxtensor/ops/rand_op.py +52 -0
- nbxtensor/ops/random_wrappers.py +366 -0
- nbxtensor/ops/reciprocal.py +17 -0
- nbxtensor/ops/reduction_wrappers.py +845 -0
- nbxtensor/ops/relu.py +18 -0
- nbxtensor/ops/remainder.py +46 -0
- nbxtensor/ops/repeat_interleave.py +48 -0
- nbxtensor/ops/repeat_op.py +132 -0
- nbxtensor/ops/residual_chain.py +374 -0
- nbxtensor/ops/rmsnorm.py +68 -0
- nbxtensor/ops/rope.py +142 -0
- nbxtensor/ops/rsqrt.py +17 -0
- nbxtensor/ops/scaled_softmax.py +96 -0
- nbxtensor/ops/scatter_op.py +139 -0
- nbxtensor/ops/screen_oracle.py +479 -0
- nbxtensor/ops/selu.py +22 -0
- nbxtensor/ops/shape_wrappers.py +1032 -0
- nbxtensor/ops/sigmoid.py +19 -0
- nbxtensor/ops/silu.py +27 -0
- nbxtensor/ops/sin.py +17 -0
- nbxtensor/ops/softmax.py +169 -0
- nbxtensor/ops/softplus.py +27 -0
- nbxtensor/ops/sort_op.py +304 -0
- nbxtensor/ops/spectral_wrappers.py +481 -0
- nbxtensor/ops/sqrt.py +17 -0
- nbxtensor/ops/stack_op.py +84 -0
- nbxtensor/ops/std.py +113 -0
- nbxtensor/ops/strided_copy.py +200 -0
- nbxtensor/ops/sub.py +35 -0
- nbxtensor/ops/sum.py +40 -0
- nbxtensor/ops/tan.py +19 -0
- nbxtensor/ops/tanh.py +21 -0
- nbxtensor/ops/threshold.py +19 -0
- nbxtensor/ops/tiling.yml +91 -0
- nbxtensor/ops/tiling_policy.py +43 -0
- nbxtensor/ops/tiling_sizes.py +370 -0
- nbxtensor/ops/topk.py +267 -0
- nbxtensor/ops/trace_op.py +64 -0
- nbxtensor/ops/tril.py +64 -0
- nbxtensor/ops/triu.py +62 -0
- nbxtensor/ops/unfold_op.py +37 -0
- nbxtensor/ops/unique_op.py +97 -0
- nbxtensor/ops/upsample_bilinear2d.py +106 -0
- nbxtensor/ops/upsample_nearest2d.py +52 -0
- nbxtensor/ops/upsample_nearest3d.py +61 -0
- nbxtensor/ops/var.py +136 -0
- nbxtensor/ops/vector_norm.py +216 -0
- nbxtensor/ops/weight_norm.py +104 -0
- nbxtensor/ops/where.py +39 -0
- nbxtensor/ops/wrappers.py +825 -0
- nbxtensor/ops/zeros_op.py +20 -0
- nbxtensor/quant/__init__.py +1 -0
- nbxtensor/quant/quantized_tensor.py +187 -0
- nbxtensor/random/__init__.py +1 -0
- nbxtensor/random/mt_randn.py +154 -0
- nbxtensor/random/rng_pin.py +59 -0
- nbxtensor/random/rng_stream.py +118 -0
- nbxtensor/random/seeded_draw.py +223 -0
- nbxtensor/shape/__init__.py +157 -0
- nbxtensor/tensor.py +1867 -0
- nbxtensor-0.0.1.dist-info/METADATA +379 -0
- nbxtensor-0.0.1.dist-info/RECORD +291 -0
- nbxtensor-0.0.1.dist-info/WHEEL +4 -0
- 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})")
|