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