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/memory/slab.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Slab allocators: a run's device allocations carved out of one block per device, and the interceptor that serves
|
|
2
|
+
them (moved from the engine's frozen-plan replayer, M2).
|
|
3
|
+
|
|
4
|
+
A replay recording (the engine's `triton/replay.py`) runs a step once with every allocation it makes served from a slab,
|
|
5
|
+
so that the addresses its launches recorded stay valid when they are replayed: the replay issues the same launches in
|
|
6
|
+
the same order, so every reuse the recorded run made is safe at replay. `SlabInterceptor` stands in front of the
|
|
7
|
+
allocator (`DeviceAllocator.install_interceptor`) and routes: while recording, to the slab of the requested device;
|
|
8
|
+
between recordings, to the recycled per-step slab when one is in force; else to the allocator itself, measuring the
|
|
9
|
+
watermark a slab will be sized from. Torch-free.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from nbxtensor.memory.allocator import AllocatorInterceptor, DeviceAllocator
|
|
16
|
+
|
|
17
|
+
ALIGN = 512 # matches typical device allocation granularity
|
|
18
|
+
|
|
19
|
+
# Slabs with outstanding carved allocations — the free path consults this PERMANENTLY (not only while recording): a
|
|
20
|
+
# slab-carved tensor may die on any later run when its arena slot is overwritten, and its finalizer must return the
|
|
21
|
+
# range to the slab, never free a slab-interior pointer to the driver.
|
|
22
|
+
ACTIVE_SLABS: List["SlabAllocator"] = []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _carve(free_list: List[Tuple[int, int]], need: int) -> Optional[int]:
|
|
26
|
+
"""First fit: the offset of the first free range holding `need` aligned bytes, the range shrunk; None if none."""
|
|
27
|
+
for i, (off, sz) in enumerate(free_list):
|
|
28
|
+
if sz >= need:
|
|
29
|
+
if sz == need:
|
|
30
|
+
free_list.pop(i)
|
|
31
|
+
else:
|
|
32
|
+
free_list[i] = (off + need, sz - need)
|
|
33
|
+
return off
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _give_back(free_list: List[Tuple[int, int]], off: int, sz: int) -> List[Tuple[int, int]]:
|
|
38
|
+
"""The free list with [off, off + sz) returned, sorted and its neighbours merged."""
|
|
39
|
+
free_list.append((off, sz))
|
|
40
|
+
free_list.sort()
|
|
41
|
+
merged: List[Tuple[int, int]] = []
|
|
42
|
+
for o, s_ in free_list:
|
|
43
|
+
if merged and merged[-1][0] + merged[-1][1] == o:
|
|
44
|
+
merged[-1] = (merged[-1][0], merged[-1][1] + s_)
|
|
45
|
+
else:
|
|
46
|
+
merged.append((o, s_))
|
|
47
|
+
return merged
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def first_fit_extent(events: List[Tuple[int, int, int]]) -> int:
|
|
51
|
+
"""The slab a run needs: the highest offset `SlabAllocator`'s first fit reaches when it serves `events` — the run's
|
|
52
|
+
pooled allocations and frees in order, (+1, key, aligned bytes) and (-1, key, 0) — from a slab without end. It is
|
|
53
|
+
at least the live peak and at most the sum: a first fit leaves holes a later, larger block cannot use. Sized from
|
|
54
|
+
the live peak alone, Allegro-TI2V's encoder tile (9.0 GB live) was short by 22.6 GB at its recording, the retry
|
|
55
|
+
then took 31.7 GB of a 32 GB card for a run that never holds more than 8.6 GB (2026-11-04)."""
|
|
56
|
+
end = 1 << 62
|
|
57
|
+
free_list: List[Tuple[int, int]] = [(0, end)]
|
|
58
|
+
at: Dict[int, Tuple[int, int]] = {}
|
|
59
|
+
top = 0
|
|
60
|
+
for sign, key, need in events:
|
|
61
|
+
if sign > 0:
|
|
62
|
+
off = _carve(free_list, need)
|
|
63
|
+
at[key] = (off, need)
|
|
64
|
+
top = max(top, off + need)
|
|
65
|
+
elif key in at:
|
|
66
|
+
free_list = _give_back(free_list, *at.pop(key))
|
|
67
|
+
return top
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SlabAllocator:
|
|
71
|
+
"""First-fit free-list allocator over ONE device slab.
|
|
72
|
+
|
|
73
|
+
The recording run's malloc/free sequence drives carving/reuse; the
|
|
74
|
+
replay issues the same launches in the same order, so every reuse
|
|
75
|
+
the recorded run performed is serialization-safe at replay.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, nbytes: int, dev_idx: Optional[int]) -> None:
|
|
79
|
+
self.size = int(nbytes)
|
|
80
|
+
self.dev_idx = dev_idx
|
|
81
|
+
self.base = DeviceAllocator.malloc_direct(self.size, dev_idx)
|
|
82
|
+
self.free_list: List[Tuple[int, int]] = [(0, self.size)]
|
|
83
|
+
self.allocs: Dict[int, Tuple[int, int]] = {} # ptr -> (off, sz)
|
|
84
|
+
self.shortfall = 0 # aligned bytes we could not serve
|
|
85
|
+
# A retired slab (failed recording attempt / invalidated plan)
|
|
86
|
+
# must NOT cudaFree while tensors carved from it are alive:
|
|
87
|
+
# their finalizers free slab-interior pointers, which the seam
|
|
88
|
+
# routes back here. Actual release happens when the last
|
|
89
|
+
# outstanding alloc returns. Probe 2 proved the hazard: an
|
|
90
|
+
# eager release left the text-encoder outputs dangling
|
|
91
|
+
# ("Pointer argument cannot be accessed" + invalid cudaFree of
|
|
92
|
+
# a slab-interior pointer).
|
|
93
|
+
self.retired = False
|
|
94
|
+
ACTIVE_SLABS.append(self)
|
|
95
|
+
|
|
96
|
+
def malloc(self, nbytes: int) -> int:
|
|
97
|
+
need = (int(nbytes) + ALIGN - 1) // ALIGN * ALIGN
|
|
98
|
+
off = _carve(self.free_list, need)
|
|
99
|
+
if off is not None:
|
|
100
|
+
ptr = self.base + off
|
|
101
|
+
self.allocs[ptr] = (off, need)
|
|
102
|
+
return ptr
|
|
103
|
+
self.shortfall += need
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
def free(self, ptr: int) -> bool:
|
|
107
|
+
entry = self.allocs.pop(ptr, None)
|
|
108
|
+
if entry is None:
|
|
109
|
+
return False
|
|
110
|
+
self.free_list = _give_back(self.free_list, *entry)
|
|
111
|
+
if self.retired and not self.allocs:
|
|
112
|
+
self._release_now()
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
def retire(self) -> None:
|
|
116
|
+
"""Deferred release: cudaFree only after the last outstanding
|
|
117
|
+
carved allocation has been returned by its owner's finalizer."""
|
|
118
|
+
self.retired = True
|
|
119
|
+
if not self.allocs:
|
|
120
|
+
self._release_now()
|
|
121
|
+
|
|
122
|
+
def _release_now(self) -> None:
|
|
123
|
+
if self.base:
|
|
124
|
+
DeviceAllocator.free_direct(self.base)
|
|
125
|
+
self.base = 0
|
|
126
|
+
if self in ACTIVE_SLABS:
|
|
127
|
+
ACTIVE_SLABS.remove(self)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class StepSlab(SlabAllocator):
|
|
131
|
+
"""Per-sequence slab RECYCLED at every decode step (P-REPLAY-KV-
|
|
132
|
+
DECODE B1). reset() returns the whole range to the free list so
|
|
133
|
+
the next step's allocations carve IDENTICAL addresses (the decode
|
|
134
|
+
graph's allocation order is structurally constant — census-
|
|
135
|
+
proven); allocations from previous steps become 'stale' and their
|
|
136
|
+
later finalizer frees are absorbed silently (the range is already
|
|
137
|
+
recycled — safe because each step's consumers run before the next
|
|
138
|
+
step begins in the synchronous decode timeline)."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, nbytes: int, dev_idx: Optional[int]) -> None:
|
|
141
|
+
super().__init__(nbytes, dev_idx)
|
|
142
|
+
self.stale: set = set()
|
|
143
|
+
self.carve_sizes: List[int] = [] # diagnosis: per-step size seq
|
|
144
|
+
self.prev_carve: Optional[List[int]] = None
|
|
145
|
+
|
|
146
|
+
def malloc(self, nbytes: int) -> int:
|
|
147
|
+
"""PURE BUMP carving — mid-step frees are absorbed without
|
|
148
|
+
recycling (see free below), so placement is a pure function of
|
|
149
|
+
the allocation-size sequence (measured identical step-to-step)
|
|
150
|
+
and IMMUNE to free-timing nondeterminism (GC-cycle finalizers
|
|
151
|
+
fire at variable points; first-fit reuse turned that into
|
|
152
|
+
per-step placement churn — measured: identical size sequences,
|
|
153
|
+
differing offsets)."""
|
|
154
|
+
self.carve_sizes.append(int(nbytes))
|
|
155
|
+
need = (int(nbytes) + ALIGN - 1) // ALIGN * ALIGN
|
|
156
|
+
off, sz = self.free_list[0]
|
|
157
|
+
if sz < need:
|
|
158
|
+
self.shortfall += need
|
|
159
|
+
return 0
|
|
160
|
+
self.free_list[0] = (off + need, sz - need)
|
|
161
|
+
ptr = self.base + off
|
|
162
|
+
self.allocs[ptr] = (off, need)
|
|
163
|
+
return ptr
|
|
164
|
+
|
|
165
|
+
def reset(self) -> None:
|
|
166
|
+
self.prev_carve = self.carve_sizes
|
|
167
|
+
self.carve_sizes = []
|
|
168
|
+
self.stale.update(self.allocs.keys())
|
|
169
|
+
self.allocs.clear()
|
|
170
|
+
self.free_list = [(0, self.size)]
|
|
171
|
+
|
|
172
|
+
def free(self, ptr: int) -> bool:
|
|
173
|
+
"""Absorb without recycling: the range stays consumed until the
|
|
174
|
+
next reset (bump purity beats intra-step reuse)."""
|
|
175
|
+
if ptr in self.stale:
|
|
176
|
+
self.stale.discard(ptr)
|
|
177
|
+
return True
|
|
178
|
+
if ptr in self.allocs:
|
|
179
|
+
self.allocs.pop(ptr)
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class SlabInterceptor(AllocatorInterceptor):
|
|
185
|
+
"""The allocator's interceptor for slab-served runs. Its state is read and set by the replayer that owns it:
|
|
186
|
+
|
|
187
|
+
* `recording` with `slabs` (one per device) or `slab`: every pooled allocation is carved from the slab of the
|
|
188
|
+
REQUESTED device (serving a device-1 tensor from device 0's slab yields a pointer into the wrong card's memory —
|
|
189
|
+
a wrong result or an illegal access, never a clear error); a slab that cannot serve it, or a device without
|
|
190
|
+
one, refuses the recording through `slab_refused(reason)` and the allocator serves it.
|
|
191
|
+
* `stab_slab`: the recycled per-step slab in force; an allocation over it is served by the allocator (address
|
|
192
|
+
stability degrades for that tensor only).
|
|
193
|
+
* `measuring`: the allocator serves, and the aligned bytes are counted per pointer and device (`live`,
|
|
194
|
+
`high_water`, `live_by_dev`, `high_water_by_dev`, `total_measured`) — what a slab is sized from.
|
|
195
|
+
|
|
196
|
+
A block outside the pool (`pooled=False`, a weight arena) is never a slab's. A free returns to the slab that
|
|
197
|
+
carved the pointer, if any, before the allocator sees it."""
|
|
198
|
+
|
|
199
|
+
def __init__(self) -> None:
|
|
200
|
+
self.measuring = False
|
|
201
|
+
self.recording = False
|
|
202
|
+
self.slab: Optional[SlabAllocator] = None
|
|
203
|
+
self.slabs: Dict[int, SlabAllocator] = {}
|
|
204
|
+
self.stab_slab: Optional["StepSlab"] = None
|
|
205
|
+
# Per-device measurement: one high-water mark cannot size slabs for a run allocating on several cards without
|
|
206
|
+
# sizing each to the SUM.
|
|
207
|
+
self.live_by_dev: Dict[int, int] = {}
|
|
208
|
+
self.high_water_by_dev: Dict[int, int] = {}
|
|
209
|
+
self.measured_dev: Dict[int, int] = {}
|
|
210
|
+
# measuring counters (aligned bytes)
|
|
211
|
+
self.live = 0
|
|
212
|
+
self.high_water = 0
|
|
213
|
+
self.measured: Dict[int, int] = {} # ptr -> aligned size
|
|
214
|
+
self.total_measured = 0 # cumulative (never decremented)
|
|
215
|
+
# the run's pooled allocations and frees in order, per device (`first_fit_extent`: the slab they need)
|
|
216
|
+
self.events_by_dev: Dict[int, List[Tuple[int, int, int]]] = {}
|
|
217
|
+
self._seq = 0
|
|
218
|
+
self._key_of_ptr: Dict[int, int] = {}
|
|
219
|
+
|
|
220
|
+
def slab_refused(self, reason: str) -> None:
|
|
221
|
+
"""A recording the slabs cannot serve: the owner decides (a replayer abandons the plan, by name)."""
|
|
222
|
+
raise NotImplementedError(f"{type(self).__qualname__} does not say what a refused slab does: {reason}")
|
|
223
|
+
|
|
224
|
+
def malloc(self, direct, nbytes, dev_idx, pooled):
|
|
225
|
+
if not pooled:
|
|
226
|
+
return direct(nbytes, dev_idx, False)
|
|
227
|
+
if self.recording and (self.slab is not None or self.slabs):
|
|
228
|
+
_dev = dev_idx if dev_idx is not None else DeviceAllocator.get_device()
|
|
229
|
+
_slab = self.slabs.get(_dev, self.slab)
|
|
230
|
+
if _slab is not None:
|
|
231
|
+
ptr = _slab.malloc(nbytes)
|
|
232
|
+
if ptr:
|
|
233
|
+
return ptr
|
|
234
|
+
self.slab_refused(f"slab exhausted on device {_dev} (short {_slab.shortfall} B)")
|
|
235
|
+
else:
|
|
236
|
+
self.slab_refused(f"no slab recorded for device {_dev}")
|
|
237
|
+
return direct(nbytes, dev_idx, True)
|
|
238
|
+
if self.stab_slab is not None:
|
|
239
|
+
ptr = self.stab_slab.malloc(nbytes)
|
|
240
|
+
if ptr:
|
|
241
|
+
return ptr
|
|
242
|
+
ptr = direct(nbytes, dev_idx, True)
|
|
243
|
+
if self.measuring and ptr:
|
|
244
|
+
need = (int(nbytes) + ALIGN - 1) // ALIGN * ALIGN
|
|
245
|
+
self.measured[ptr] = need
|
|
246
|
+
self.total_measured += need
|
|
247
|
+
self.live += need
|
|
248
|
+
if self.live > self.high_water:
|
|
249
|
+
self.high_water = self.live
|
|
250
|
+
_d = dev_idx if dev_idx is not None else DeviceAllocator.get_device()
|
|
251
|
+
self.measured_dev[ptr] = _d
|
|
252
|
+
self._seq += 1 # a key per allocation: a pointer the allocator reuses is a new block
|
|
253
|
+
self._key_of_ptr[ptr] = self._seq
|
|
254
|
+
self.events_by_dev.setdefault(_d, []).append((1, self._seq, need))
|
|
255
|
+
self.live_by_dev[_d] = self.live_by_dev.get(_d, 0) + need
|
|
256
|
+
if self.live_by_dev[_d] > self.high_water_by_dev.get(_d, 0):
|
|
257
|
+
self.high_water_by_dev[_d] = self.live_by_dev[_d]
|
|
258
|
+
return ptr
|
|
259
|
+
|
|
260
|
+
def free(self, direct, ptr):
|
|
261
|
+
for slab in ACTIVE_SLABS:
|
|
262
|
+
if slab.free(ptr):
|
|
263
|
+
return # returned to its slab (maybe releasing it)
|
|
264
|
+
if self.measuring:
|
|
265
|
+
need = self.measured.pop(ptr, 0)
|
|
266
|
+
self.live -= need
|
|
267
|
+
_d = self.measured_dev.pop(ptr, None)
|
|
268
|
+
if _d is not None:
|
|
269
|
+
self.live_by_dev[_d] = self.live_by_dev.get(_d, 0) - need
|
|
270
|
+
_k = self._key_of_ptr.pop(ptr, None)
|
|
271
|
+
if _k is not None:
|
|
272
|
+
self.events_by_dev.setdefault(_d, []).append((-1, _k, 0))
|
|
273
|
+
return direct(ptr)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""The expandable range's chunk, measured by replaying a REAL run's allocations (the supervisor 2026-11-02 17:15).
|
|
2
|
+
|
|
3
|
+
A run's mapping cost is what its refusals' reclaims map again (SANA-Video_2B_720p on a 16 GB V100: 89 GB mapped over a
|
|
4
|
+
run at a 2 MiB chunk — registres/benchmarks/vmm_v100.md), and only the run's own allocation order produces those
|
|
5
|
+
refusals; a synthetic churn does not (it always favours the largest chunk). So the library carries recorded traces as
|
|
6
|
+
data (`traces/*.tsv`: every allocation and free of one gate cell, in order, by id and bytes — no model, no pointer),
|
|
7
|
+
and the chunk probe replays one through THIS allocator, on the device, under the card size the trace was recorded on
|
|
8
|
+
(`card_mb`, the imposed limit — the trace presses that class on any larger card), once with the expandable range off
|
|
9
|
+
(the driver segments: R16 OFF) and once per candidate chunk, each in its own process (a clean allocator).
|
|
10
|
+
|
|
11
|
+
The chunk is the smallest candidate whose replay costs no more than R16 OFF's replay plus OFF's own spread (max -
|
|
12
|
+
median over the repetitions): the expandable range then holds what it saves (the 32g replay: 8 GB less at its peak)
|
|
13
|
+
without paying for it in time. None when no candidate does — said, and the caller keeps the driver segments.
|
|
14
|
+
|
|
15
|
+
Torch-free, numpy-free."""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import statistics
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
|
26
|
+
|
|
27
|
+
TRACES_DIR = Path(__file__).parent / "traces"
|
|
28
|
+
_MARK = "NBX-TRACE-REPLAY " # the child's result line, whatever else it prints
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def traces() -> List[Path]:
|
|
32
|
+
"""The traces the library carries, by name."""
|
|
33
|
+
return sorted(TRACES_DIR.glob("*.tsv"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_trace(path) -> Tuple[Dict[str, str], List[Tuple[str, int, int]]]:
|
|
37
|
+
"""(the trace's header facts `# key: value`, its events (kind, id, nbytes)). A trace without `card_mb` or without
|
|
38
|
+
one event is refused by name."""
|
|
39
|
+
meta: Dict[str, str] = {}
|
|
40
|
+
events: List[Tuple[str, int, int]] = []
|
|
41
|
+
for line in open(path):
|
|
42
|
+
if line.startswith("#"):
|
|
43
|
+
key, sep, value = line[1:].strip().partition(": ")
|
|
44
|
+
if sep and " " not in key:
|
|
45
|
+
meta[key] = value.strip()
|
|
46
|
+
continue
|
|
47
|
+
parts = line.rstrip("\n").split("\t")
|
|
48
|
+
if len(parts) == 3 and parts[0] in ("M", "A", "F"):
|
|
49
|
+
events.append((parts[0], int(parts[1]), int(parts[2])))
|
|
50
|
+
if "card_mb" not in meta or not events:
|
|
51
|
+
raise RuntimeError(f"ZERO FALLBACK: the allocation trace {path} states no card_mb or holds no event")
|
|
52
|
+
return meta, events
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def replay_trace(events: Sequence[Tuple[str, int, int]], device: int, chunk: int, limit_bytes: int) -> Dict[str, float]:
|
|
56
|
+
"""Replay `events` through this process's allocator on `device` under `limit_bytes`, the large class in an
|
|
57
|
+
expandable range of `chunk` bytes (0: the driver segments). The wall seconds, the mapping seconds, the reclaims and
|
|
58
|
+
the peak held."""
|
|
59
|
+
from nbxtensor.memory.allocator import DeviceAllocator as DA
|
|
60
|
+
DA.set_device(device)
|
|
61
|
+
DA.use_expandable(device, chunk, range_bytes=limit_bytes)
|
|
62
|
+
DA.set_limit(limit_bytes, device)
|
|
63
|
+
live: Dict[int, int] = {}
|
|
64
|
+
t0 = time.perf_counter()
|
|
65
|
+
for kind, key, nbytes in events:
|
|
66
|
+
if kind == "F":
|
|
67
|
+
ptr = live.pop(key, None)
|
|
68
|
+
if ptr is not None:
|
|
69
|
+
DA.free_cuda(ptr)
|
|
70
|
+
elif nbytes:
|
|
71
|
+
live[key] = DA.malloc_cuda(nbytes, device, pooled=(kind == "M"))
|
|
72
|
+
DA.device_synchronize(device)
|
|
73
|
+
seconds = time.perf_counter() - t0
|
|
74
|
+
st = DA._POOL.stats
|
|
75
|
+
return {"seconds": seconds, "map_seconds": st.get("range_map_us", 0) / 1e6,
|
|
76
|
+
"reclaims": st.get("range_reclaims", 0), "peak_bytes": DA._cuda_peak_bytes.get(device, 0)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _child(trace: str, device: int, chunk: int) -> Dict[str, float]:
|
|
80
|
+
"""One replay in a fresh process (no allocator state carried between candidates)."""
|
|
81
|
+
code = ("import json, sys; from nbxtensor.memory import trace_replay as T; "
|
|
82
|
+
"m, ev = T.load_trace(sys.argv[1]); "
|
|
83
|
+
"print('" + _MARK + "' + json.dumps(T.replay_trace(ev, int(sys.argv[2]), int(sys.argv[3]), "
|
|
84
|
+
"int(m['card_mb']) << 20)))")
|
|
85
|
+
# the replay measures the allocator, never its diagnostics: the parent's run-level switches stay out of it
|
|
86
|
+
env = {k: v for k, v in os.environ.items() if not k.startswith(("NBX_ALLOC_STATS", "NBX_MALLOC_TRACE"))}
|
|
87
|
+
r = subprocess.run([sys.executable, "-c", code, trace, str(device), str(chunk)], capture_output=True, text=True,
|
|
88
|
+
env=env)
|
|
89
|
+
line = next((l for l in r.stdout.splitlines() if l.startswith(_MARK)), None)
|
|
90
|
+
if r.returncode != 0 or line is None:
|
|
91
|
+
raise RuntimeError(f"the allocation-trace replay of {Path(trace).name} at chunk {chunk} failed: "
|
|
92
|
+
f"{(r.stderr or r.stdout)[-400:]}")
|
|
93
|
+
return json.loads(line[len(_MARK):])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def chunk_by_replay(device: int, granularity: int, ks: Sequence[int], reps: int,
|
|
97
|
+
trace: Optional[str] = None) -> Tuple[Optional[int], Dict[int, List[float]]]:
|
|
98
|
+
"""(the chunk, every candidate's replay seconds — 0 is R16 OFF). The smallest granularity x k whose median replay
|
|
99
|
+
costs no more than OFF's median plus OFF's spread; None when none does. `trace`: one of `traces()` (the only one
|
|
100
|
+
the library carries when it carries one; several = the caller names it)."""
|
|
101
|
+
if trace is None:
|
|
102
|
+
carried = traces()
|
|
103
|
+
if len(carried) != 1:
|
|
104
|
+
raise RuntimeError(f"ZERO FALLBACK: the library carries {len(carried)} allocation traces "
|
|
105
|
+
f"({', '.join(p.name for p in carried)}): the chunk probe names the one it replays")
|
|
106
|
+
trace = str(carried[0])
|
|
107
|
+
# every replay runs in a child sized at the recorded card: what THIS process caches on the device is given back
|
|
108
|
+
# first, or the child finds the card short by it (a test process's pools after the kernels suite: 16 GB, the
|
|
109
|
+
# replay out of memory at chunk 0 — order-dependent, 2026-11-03)
|
|
110
|
+
from nbxtensor.memory.allocator import DeviceAllocator as DA
|
|
111
|
+
DA.device_synchronize(device)
|
|
112
|
+
if DA._range_vmm(device) is not None:
|
|
113
|
+
DA._range_reclaim(device, None)
|
|
114
|
+
DA._pool_flush(device)
|
|
115
|
+
times: Dict[int, List[float]] = {0: [_child(trace, device, 0)["seconds"] for _ in range(reps)]}
|
|
116
|
+
off = times[0]
|
|
117
|
+
bound = statistics.median(off) + (max(off) - statistics.median(off))
|
|
118
|
+
for chunk in [granularity * int(k) for k in sorted(ks)]: # ascending: the first within OFF's noise is the chunk
|
|
119
|
+
times[chunk] = [_child(trace, device, chunk)["seconds"] for _ in range(reps)]
|
|
120
|
+
if statistics.median(times[chunk]) <= bound:
|
|
121
|
+
return chunk, times
|
|
122
|
+
return None, times
|