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/pool.py
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""The caching pool of the device allocator: its segments, their blocks, the free index — and the same rules replayed
|
|
2
|
+
as a pure function of an allocation sequence (`simulate`), which is how a planner prices what the allocator holds
|
|
3
|
+
beyond the tensors alive (M3, docs/nbxtensor/COST.md section 2: the 1.3 factor it replaces).
|
|
4
|
+
|
|
5
|
+
Every driver allocation the pool takes is a SEGMENT made of BLOCKS chained in address order (the method of torch's
|
|
6
|
+
caching allocator, S4a 2026-10-27), in one of two SIZE CLASSES: a request of at most `POOL_SMALL_BYTES` is served only
|
|
7
|
+
from small segments of `POOL_SMALL_SEGMENT_BYTES`, a larger one only from large segments grown to the request (torch's
|
|
8
|
+
kSmallSize / kSmallBuffer). A request takes the smallest free block of its class that holds it (rounded up to `align`)
|
|
9
|
+
and splits off the rest when it is at least `split_min` — in the large class only when what is left is at most the
|
|
10
|
+
request itself (the half rule), so a block is never pinned by something smaller than its remainder. A freed block
|
|
11
|
+
merges with its free neighbours; only a segment wholly free returns to the driver — over the parked cap, on a refused
|
|
12
|
+
malloc, on a flush. A wholly free segment stays parked across a growth: it is the next request of its size's block.
|
|
13
|
+
|
|
14
|
+
Why the classes and the half rule (Allegro triton on a V100-16GB, gate of 2026-11-01): with one class and a free split,
|
|
15
|
+
a 17 KB scalar took a freed 2.5 GB block and pinned its remainder alive, a 632 MB request did the same to the next —
|
|
16
|
+
4.4 GB of free blocks in pinned segments beside 9.7 GB alive refused a 2.5 GB gelu. The real trace replayed through
|
|
17
|
+
these rules (campaign 2026_11_01_gate_nbxtensor, trace_allegro/replay.py; also mochi-1-preview 32g and Kokoro) completes
|
|
18
|
+
with the least held of every pooled rule tried, and 164e89dc's flush of the pool before every growth — which only
|
|
19
|
+
emptied the small free segments the scalars would have used — is withdrawn with it.
|
|
20
|
+
|
|
21
|
+
Mirrored, a block's size is reported to `on_block(ptr, size, dev)` (`size` None: the block is gone) — the allocator
|
|
22
|
+
keeps its per-pointer tables by it.
|
|
23
|
+
|
|
24
|
+
Torch-free, device-free: the driver is reached only through the callable a caller passes in (`flush`, `evict`).
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import bisect
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import Callable, Dict, Iterable, List, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
#: a request is rounded up to this many bytes before it is matched (the allocator's `_POOL_ALIGN`)
|
|
33
|
+
POOL_ALIGN = 512
|
|
34
|
+
#: the least remainder a block is split for; a smaller one stays with the block it was cut from
|
|
35
|
+
POOL_SPLIT_MIN = 1 << 20
|
|
36
|
+
#: the largest request of the small class (torch's caching allocator: kSmallSize)
|
|
37
|
+
POOL_SMALL_BYTES = 1 << 20
|
|
38
|
+
#: the segment a small request grows the device by, carved for the small requests that follow (kSmallBuffer)
|
|
39
|
+
POOL_SMALL_SEGMENT_BYTES = 1 << 21
|
|
40
|
+
|
|
41
|
+
LARGE, SMALL = 0, 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _no_report(ptr: int, size: Optional[int], dev: int) -> None:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SegmentPool:
|
|
49
|
+
"""The free blocks of every device's segments, and the blocks in use inside them."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, align: int = POOL_ALIGN, split_min: int = POOL_SPLIT_MIN, small: int = POOL_SMALL_BYTES,
|
|
52
|
+
small_segment: int = POOL_SMALL_SEGMENT_BYTES,
|
|
53
|
+
on_block: Callable[[int, Optional[int], int], None] = _no_report) -> None:
|
|
54
|
+
self.align = int(align)
|
|
55
|
+
self.split_min = int(split_min)
|
|
56
|
+
self.small = int(small)
|
|
57
|
+
self.small_segment = int(small_segment)
|
|
58
|
+
self.on_block = on_block
|
|
59
|
+
self.blocks: Dict[int, list] = {} # ptr -> [size, prev ptr, next ptr, free, device, class]
|
|
60
|
+
self.free: Dict[int, Dict[int, list]] = {} # device -> {size: [ptr, ...]} (the FREE blocks, by size)
|
|
61
|
+
self.sizes: Dict[int, list] = {} # device -> sorted sizes present in free[dev]
|
|
62
|
+
self._cls_free: Dict[Tuple[int, int], Dict[int, list]] = {} # (device, class) -> {size: [ptr, ...]}
|
|
63
|
+
self._cls_sizes: Dict[Tuple[int, int], list] = {} # (device, class) -> sorted sizes
|
|
64
|
+
self.alloc_size: Dict[int, int] = {} # ptr -> the block's size, for the caller of the last take
|
|
65
|
+
self.cached: Dict[int, int] = {} # device -> bytes parked in free blocks
|
|
66
|
+
self.cached_peak: Dict[int, int] = {} # device -> peak of the above
|
|
67
|
+
self.stats: Dict[str, int] = {"exact": 0, "fit": 0, "miss": 0, "slack_total": 0, "slack_max": 0,
|
|
68
|
+
"flushes": 0, "flushed_bytes": 0, "evictions": 0, "evicted_bytes": 0}
|
|
69
|
+
#: device -> its expandable ranges (`Range`), the large class's address space where the device maps chunks
|
|
70
|
+
#: (`memory.vmm`); none: the large class grows by driver segments as the small one does
|
|
71
|
+
self.ranges: Dict[int, List["Range"]] = {}
|
|
72
|
+
|
|
73
|
+
# -- the classes ---------------------------------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def cls(self, nbytes: int) -> int:
|
|
76
|
+
"""The size class of a request: SMALL up to `small` bytes, LARGE beyond."""
|
|
77
|
+
return SMALL if int(nbytes) <= self.small else LARGE
|
|
78
|
+
|
|
79
|
+
def grow_size(self, nbytes: int) -> int:
|
|
80
|
+
"""The bytes the device grows by for a request the pool cannot serve: a small segment for a small request, the
|
|
81
|
+
request itself (rounded up to `align`) for a large one."""
|
|
82
|
+
if self.cls(nbytes) == SMALL:
|
|
83
|
+
return self.small_segment
|
|
84
|
+
return -(-int(nbytes) // self.align) * self.align
|
|
85
|
+
|
|
86
|
+
# -- the free index ------------------------------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _add(index: dict, sizes: dict, key, ptr: int, size: int) -> None:
|
|
90
|
+
per = index.setdefault(key, {})
|
|
91
|
+
lst = per.get(size)
|
|
92
|
+
if lst is None:
|
|
93
|
+
per[size] = [ptr]
|
|
94
|
+
bisect.insort(sizes.setdefault(key, []), size)
|
|
95
|
+
else:
|
|
96
|
+
lst.append(ptr)
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _remove(index: dict, sizes: dict, key, ptr: int, size: int) -> None:
|
|
100
|
+
per = index.get(key, {})
|
|
101
|
+
lst = per.get(size)
|
|
102
|
+
lst.remove(ptr)
|
|
103
|
+
if not lst:
|
|
104
|
+
del per[size]
|
|
105
|
+
sz = sizes[key]
|
|
106
|
+
sz.pop(bisect.bisect_left(sz, size))
|
|
107
|
+
|
|
108
|
+
def _index_add(self, dev: int, ptr: int, size: int) -> None:
|
|
109
|
+
self._add(self.free, self.sizes, dev, ptr, size)
|
|
110
|
+
self._add(self._cls_free, self._cls_sizes, (dev, self.blocks[ptr][5]), ptr, size)
|
|
111
|
+
|
|
112
|
+
def _index_remove(self, dev: int, ptr: int, size: int) -> None:
|
|
113
|
+
self._remove(self.free, self.sizes, dev, ptr, size)
|
|
114
|
+
self._remove(self._cls_free, self._cls_sizes, (dev, self.blocks[ptr][5]), ptr, size)
|
|
115
|
+
|
|
116
|
+
# -- blocks --------------------------------------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def take(self, dev: int, nbytes: int) -> Optional[int]:
|
|
119
|
+
"""The smallest free block of the request's class on `dev` that holds `nbytes` (rounded up to `align`), split
|
|
120
|
+
when what is left is at least `split_min` — and, in the large class, at most the request; None on a miss (no
|
|
121
|
+
such block, or the only ones would be left with more free than the request pins). The block's size is left in
|
|
122
|
+
`alloc_size[ptr]` for the caller (it may exceed the request by less than the split threshold)."""
|
|
123
|
+
st = self.stats
|
|
124
|
+
cls = self.cls(nbytes)
|
|
125
|
+
sizes = self._cls_sizes.get((dev, cls))
|
|
126
|
+
if not sizes:
|
|
127
|
+
st["miss"] += 1
|
|
128
|
+
return None
|
|
129
|
+
want = -(-int(nbytes) // self.align) * self.align
|
|
130
|
+
k = bisect.bisect_left(sizes, want)
|
|
131
|
+
if k >= len(sizes):
|
|
132
|
+
st["miss"] += 1
|
|
133
|
+
return None
|
|
134
|
+
sz = sizes[k]
|
|
135
|
+
ptr = self._cls_free[(dev, cls)][sz][-1]
|
|
136
|
+
if (cls == LARGE and sz - want >= self.split_min and sz - want > want
|
|
137
|
+
and not (self.ranges and _range_of(self, dev, ptr) is not None)):
|
|
138
|
+
# the half rule: a larger block would leave even more pinned — in a driver segment; in an expandable range
|
|
139
|
+
# a remainder pins no page, its whole chunks are unmapped at a reclaim (R16)
|
|
140
|
+
st["miss"] += 1
|
|
141
|
+
return None
|
|
142
|
+
self._index_remove(dev, ptr, sz)
|
|
143
|
+
blk = self.blocks[ptr]
|
|
144
|
+
blk[3] = False
|
|
145
|
+
if sz - want >= self.split_min:
|
|
146
|
+
rest = ptr + want
|
|
147
|
+
self.blocks[rest] = [sz - want, ptr, blk[2], True, dev, cls]
|
|
148
|
+
if blk[2] is not None:
|
|
149
|
+
self.blocks[blk[2]][1] = rest
|
|
150
|
+
blk[0], blk[2] = want, rest
|
|
151
|
+
if self.ranges:
|
|
152
|
+
r = _range_of(self, dev, ptr)
|
|
153
|
+
if r is not None and r.last == ptr:
|
|
154
|
+
r.last = rest
|
|
155
|
+
self.on_block(rest, sz - want, dev)
|
|
156
|
+
self._index_add(dev, rest, sz - want)
|
|
157
|
+
st["fit"] += 1
|
|
158
|
+
else:
|
|
159
|
+
st["exact" if sz == int(nbytes) else "fit"] += 1
|
|
160
|
+
if sz > int(nbytes):
|
|
161
|
+
st["slack_total"] += sz - int(nbytes)
|
|
162
|
+
st["slack_max"] = max(st["slack_max"], sz - int(nbytes))
|
|
163
|
+
self.alloc_size[ptr] = blk[0]
|
|
164
|
+
self.cached[dev] = self.cached.get(dev, 0) - blk[0]
|
|
165
|
+
return ptr
|
|
166
|
+
|
|
167
|
+
def add_free_segment(self, dev: int, ptr: int, nbytes: int, request: int) -> None:
|
|
168
|
+
"""A driver allocation of `nbytes` grown for `request` (`grow_size`) joins the pool as a one-block FREE segment
|
|
169
|
+
of the request's class, parked; the caller takes the request from it next."""
|
|
170
|
+
cls = self.cls(request)
|
|
171
|
+
self.blocks[ptr] = [int(nbytes), None, None, True, dev, cls]
|
|
172
|
+
self._index_add(dev, ptr, int(nbytes))
|
|
173
|
+
self.cached[dev] = self.cached.get(dev, 0) + int(nbytes)
|
|
174
|
+
|
|
175
|
+
def release(self, ptr: int) -> int:
|
|
176
|
+
"""`ptr`'s block is free: merged with its free neighbours and indexed; returns the bytes it adds to the pool's
|
|
177
|
+
parked total (`cached` is the caller's to update, with its peak: `park`)."""
|
|
178
|
+
blk = self.blocks[ptr]
|
|
179
|
+
dev = blk[4]
|
|
180
|
+
added = blk[0]
|
|
181
|
+
blk[3] = True
|
|
182
|
+
nxt = blk[2]
|
|
183
|
+
r = _range_of(self, dev, ptr) if self.ranges else None
|
|
184
|
+
if nxt is not None and self.blocks[nxt][3]:
|
|
185
|
+
self._index_remove(dev, nxt, self.blocks[nxt][0])
|
|
186
|
+
nb = self.blocks.pop(nxt)
|
|
187
|
+
if r is not None and r.last == nxt:
|
|
188
|
+
r.last = ptr
|
|
189
|
+
self.on_block(nxt, None, dev)
|
|
190
|
+
blk[0] += nb[0]
|
|
191
|
+
blk[2] = nb[2]
|
|
192
|
+
if nb[2] is not None:
|
|
193
|
+
self.blocks[nb[2]][1] = ptr
|
|
194
|
+
prv = blk[1]
|
|
195
|
+
if prv is not None and self.blocks[prv][3]:
|
|
196
|
+
pb = self.blocks[prv]
|
|
197
|
+
self._index_remove(dev, prv, pb[0])
|
|
198
|
+
pb[0] += blk[0]
|
|
199
|
+
pb[2] = blk[2]
|
|
200
|
+
if blk[2] is not None:
|
|
201
|
+
self.blocks[blk[2]][1] = prv
|
|
202
|
+
self.blocks.pop(ptr)
|
|
203
|
+
self.on_block(ptr, None, dev)
|
|
204
|
+
if r is not None and r.last == ptr:
|
|
205
|
+
r.last = prv
|
|
206
|
+
ptr, blk = prv, pb
|
|
207
|
+
self.on_block(ptr, blk[0], dev)
|
|
208
|
+
self._index_add(dev, ptr, blk[0])
|
|
209
|
+
return added
|
|
210
|
+
|
|
211
|
+
def park(self, dev: int, added: int) -> int:
|
|
212
|
+
"""`added` bytes more parked on `dev`; the new parked total (its peak kept)."""
|
|
213
|
+
cached = self.cached.get(dev, 0) + added
|
|
214
|
+
self.cached[dev] = cached
|
|
215
|
+
if cached > self.cached_peak.get(dev, 0):
|
|
216
|
+
self.cached_peak[dev] = cached
|
|
217
|
+
return cached
|
|
218
|
+
|
|
219
|
+
# -- segments ------------------------------------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def free_segments(self, dev: int) -> List[Tuple[int, int]]:
|
|
222
|
+
"""The wholly free segments on `dev`, largest first: (ptr, bytes)."""
|
|
223
|
+
out = []
|
|
224
|
+
for size, ptrs in self.free.get(dev, {}).items():
|
|
225
|
+
for p in ptrs:
|
|
226
|
+
b = self.blocks.get(p)
|
|
227
|
+
if b is not None and b[1] is None and b[2] is None and _range_of(self, dev, p) is None:
|
|
228
|
+
out.append((p, size))
|
|
229
|
+
return sorted(out, key=lambda x: -x[1])
|
|
230
|
+
|
|
231
|
+
def drop_segment(self, dev: int, ptr: int) -> int:
|
|
232
|
+
"""A WHOLLY free segment (its one block, free, unchained) leaves the pool; its bytes. The driver's release is
|
|
233
|
+
the caller's."""
|
|
234
|
+
blk = self.blocks[ptr]
|
|
235
|
+
self._index_remove(dev, ptr, blk[0])
|
|
236
|
+
self.blocks.pop(ptr)
|
|
237
|
+
self.alloc_size.pop(ptr, None)
|
|
238
|
+
return blk[0]
|
|
239
|
+
|
|
240
|
+
def evict(self, dev: int, want: int, to_driver: Callable[[int, int], int]) -> int:
|
|
241
|
+
"""Wholly free segments on `dev` to the driver (`to_driver(dev, ptr)`), largest first, until at least `want`
|
|
242
|
+
bytes are freed or none is left. Returns the bytes freed."""
|
|
243
|
+
freed = 0
|
|
244
|
+
st = self.stats
|
|
245
|
+
for p, _size in self.free_segments(dev):
|
|
246
|
+
if freed >= want:
|
|
247
|
+
break
|
|
248
|
+
freed += to_driver(dev, p)
|
|
249
|
+
st["evictions"] += 1
|
|
250
|
+
self.cached[dev] = max(0, self.cached.get(dev, 0) - freed)
|
|
251
|
+
st["evicted_bytes"] += freed
|
|
252
|
+
return freed
|
|
253
|
+
|
|
254
|
+
def flush(self, devs: Iterable[int], to_driver: Callable[[int, int], int]) -> int:
|
|
255
|
+
"""Every wholly free segment of `devs` to the driver (a segment still holding a live block stays). Returns the
|
|
256
|
+
bytes freed."""
|
|
257
|
+
freed = 0
|
|
258
|
+
self.stats["flushes"] += 1
|
|
259
|
+
for d in list(devs):
|
|
260
|
+
got = 0
|
|
261
|
+
for p, _size in self.free_segments(d):
|
|
262
|
+
got += to_driver(d, p)
|
|
263
|
+
self.cached[d] = max(0, self.cached.get(d, 0) - got)
|
|
264
|
+
freed += got
|
|
265
|
+
self.stats["flushed_bytes"] += freed
|
|
266
|
+
return freed
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# -- expandable ranges (R16) --------------------------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
class Range:
|
|
272
|
+
"""One reserved virtual address range of a device: [base, base + size), chunks of `chunk` bytes mapped into it as
|
|
273
|
+
the pool grows (`mapped`: chunk indices), `top` the offset the next growth starts at, `last` the block ending at
|
|
274
|
+
`top` (None while empty). The pool's large blocks of the range chain through it in address order, so a free block
|
|
275
|
+
coalesces across growth boundaries; a chunk lying whole inside a free block can be unmapped and its physical
|
|
276
|
+
memory mapped again elsewhere — the remainders a live neighbour pins give their pages back (R16)."""
|
|
277
|
+
|
|
278
|
+
def __init__(self, base: int, size: int, chunk: int) -> None:
|
|
279
|
+
self.base, self.size, self.chunk = int(base), int(size), int(chunk)
|
|
280
|
+
self.top = 0
|
|
281
|
+
self.last: Optional[int] = None
|
|
282
|
+
self.mapped: set = set()
|
|
283
|
+
|
|
284
|
+
def holds(self, ptr: int) -> bool:
|
|
285
|
+
return self.base <= ptr < self.base + self.size
|
|
286
|
+
|
|
287
|
+
def chunks_touching(self, lo: int, hi: int) -> range:
|
|
288
|
+
return range((lo - self.base) // self.chunk, -(-(hi - self.base) // self.chunk))
|
|
289
|
+
|
|
290
|
+
def chunks_within(self, lo: int, hi: int) -> range:
|
|
291
|
+
return range(-(-(lo - self.base) // self.chunk), (hi - self.base) // self.chunk)
|
|
292
|
+
|
|
293
|
+
def chunk_va(self, i: int) -> int:
|
|
294
|
+
return self.base + i * self.chunk
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def mapped_bytes(self) -> int:
|
|
298
|
+
return len(self.mapped) * self.chunk
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _range_of(pool: "SegmentPool", dev: int, ptr: int) -> Optional[Range]:
|
|
302
|
+
for r in pool.ranges.get(dev, ()):
|
|
303
|
+
if r.holds(ptr):
|
|
304
|
+
return r
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def range_add(pool: "SegmentPool", dev: int, base: int, size: int, chunk: int) -> Range:
|
|
309
|
+
"""A reserved range joins the device's large class."""
|
|
310
|
+
r = Range(base, size, chunk)
|
|
311
|
+
pool.ranges.setdefault(dev, []).append(r)
|
|
312
|
+
return r
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def range_extend(pool: "SegmentPool", dev: int, nbytes: int) -> Optional[Tuple[Range, int, int]]:
|
|
316
|
+
"""Grow the device's last range by `nbytes` (rounded to the pool's align) at its top: the new bytes join the
|
|
317
|
+
block ending at the top when it is free (one block, coalesced), else become a free block chained after it.
|
|
318
|
+
Returns (range, lo, hi) — the caller maps the chunks touching [lo, hi) not yet mapped — or None when no range of
|
|
319
|
+
the device has the room (the caller reserves another)."""
|
|
320
|
+
n = -(-int(nbytes) // pool.align) * pool.align
|
|
321
|
+
rs = pool.ranges.get(dev) or []
|
|
322
|
+
r = rs[-1] if rs else None
|
|
323
|
+
if r is None or r.top + n > r.size:
|
|
324
|
+
return None
|
|
325
|
+
lo = r.base + r.top
|
|
326
|
+
last = r.last
|
|
327
|
+
if last is not None and pool.blocks[last][3]:
|
|
328
|
+
blk = pool.blocks[last]
|
|
329
|
+
pool._index_remove(dev, last, blk[0])
|
|
330
|
+
blk[0] += n
|
|
331
|
+
pool._index_add(dev, last, blk[0])
|
|
332
|
+
pool.on_block(last, blk[0], dev)
|
|
333
|
+
else:
|
|
334
|
+
pool.blocks[lo] = [n, last, None, True, dev, LARGE]
|
|
335
|
+
if last is not None:
|
|
336
|
+
pool.blocks[last][2] = lo
|
|
337
|
+
pool._index_add(dev, lo, n)
|
|
338
|
+
pool.on_block(lo, n, dev)
|
|
339
|
+
r.last = lo
|
|
340
|
+
r.top += n
|
|
341
|
+
pool.cached[dev] = pool.cached.get(dev, 0) + n
|
|
342
|
+
return r, lo, lo + n
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def range_unmapped(pool: "SegmentPool", dev: int, ptr: int, nbytes: int) -> List[Tuple[Range, int]]:
|
|
346
|
+
"""The (range, chunk) pairs a block [ptr, ptr + nbytes) touches that are not mapped (unmapped by a reclaim, or
|
|
347
|
+
the growth's own new chunks)."""
|
|
348
|
+
r = _range_of(pool, dev, ptr)
|
|
349
|
+
if r is None:
|
|
350
|
+
return []
|
|
351
|
+
return [(r, i) for i in r.chunks_touching(ptr, ptr + nbytes) if i not in r.mapped]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def range_reclaimable(pool: "SegmentPool", dev: int) -> List[Tuple[Range, int]]:
|
|
355
|
+
"""The mapped chunks lying WHOLE inside free blocks of the device's ranges: what an unmap gives back without
|
|
356
|
+
touching a live byte."""
|
|
357
|
+
out = []
|
|
358
|
+
for r in pool.ranges.get(dev, ()):
|
|
359
|
+
for ptr, blk in pool.blocks.items():
|
|
360
|
+
if blk[3] and blk[4] == dev and r.holds(ptr):
|
|
361
|
+
out.extend((r, i) for i in r.chunks_within(ptr, ptr + blk[0]) if i in r.mapped)
|
|
362
|
+
return out
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# -- the same rules, replayed -------------------------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
@dataclass(frozen=True)
|
|
368
|
+
class Simulation:
|
|
369
|
+
"""What the allocator holds for a sequence of allocations and frees: `peak_bytes` from the driver (live blocks, the
|
|
370
|
+
free blocks of segments something alive pins, and parked whole segments), `peak_live_bytes` of the tensors alive,
|
|
371
|
+
`parked_bytes` the most parked at once."""
|
|
372
|
+
peak_bytes: int
|
|
373
|
+
peak_live_bytes: int
|
|
374
|
+
parked_bytes: int
|
|
375
|
+
#: requests the card of `cap` bytes refused even after the reclaim (0 without a cap)
|
|
376
|
+
refusals: int = 0
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def simulate(events: Iterable[Tuple[str, object, int]], parked_cap: int = 0, align: int = POOL_ALIGN,
|
|
380
|
+
split_min: int = POOL_SPLIT_MIN, pooled: bool = True, vmm_chunk: Optional[int] = None,
|
|
381
|
+
cap: Optional[int] = None) -> Simulation:
|
|
382
|
+
"""Replay `events` — ("alloc", key, nbytes), ("arena", key, nbytes) and ("free", key, 0), `key` any hashable naming
|
|
383
|
+
one allocation — through the allocator's own pool (`SegmentPool`) and its own order: a request is served from the
|
|
384
|
+
pool first; on a miss the device grows by `grow_size` and the new segment is taken from; a freed block parks, and
|
|
385
|
+
wholly free segments beyond `parked_cap` (0: unbounded) return to the driver. An "arena" is a block outside the
|
|
386
|
+
pool (`malloc_cuda(pooled=False)`, a weight arena): straight from the driver, back to it whole when freed.
|
|
387
|
+
`pooled=False`: the pool is off (`NBX_ALLOC_POOL=0`), every free returns to the driver. One device; the driver is a
|
|
388
|
+
byte counter that never refuses (what a refusal flushes is the parked whole segments, counted in `parked_bytes`).
|
|
389
|
+
|
|
390
|
+
`vmm_chunk`: the device maps chunks of that many bytes into an expandable range for the large class (R16,
|
|
391
|
+
`memory.vmm`): a large miss extends the range and maps the chunks it touches, a block taken over unmapped chunks
|
|
392
|
+
maps them, and a refusal (`cap`: the card's bytes; None: never) unmaps every mapped chunk lying whole inside a free
|
|
393
|
+
block before it is retried — the reclaim the allocator makes, after one synchronisation."""
|
|
394
|
+
pool = SegmentPool(align, split_min)
|
|
395
|
+
held = live = peak = peak_live = parked = refusals = 0
|
|
396
|
+
rng = range_add(pool, 0, 1 << 44, 1 << 46, int(vmm_chunk)) if (pooled and vmm_chunk) else None
|
|
397
|
+
|
|
398
|
+
def map_chunks(pairs) -> bool:
|
|
399
|
+
"""Map the (range, chunk) pairs, reclaiming once when the card would refuse; False when it still refuses."""
|
|
400
|
+
nonlocal held
|
|
401
|
+
need = len(pairs) * rng.chunk
|
|
402
|
+
if cap is not None and held + need > cap:
|
|
403
|
+
# the allocator's reclaim: whole free chunks, the highest addresses first, until the mapping fits; then
|
|
404
|
+
# the driver segments
|
|
405
|
+
for r, i in sorted(range_reclaimable(pool, 0), key=lambda ri: -ri[0].chunk_va(ri[1])):
|
|
406
|
+
if held + need <= cap:
|
|
407
|
+
break
|
|
408
|
+
r.mapped.discard(i)
|
|
409
|
+
held -= r.chunk
|
|
410
|
+
if held + need > cap:
|
|
411
|
+
pool.flush([0], to_driver)
|
|
412
|
+
if held + need > cap:
|
|
413
|
+
return False
|
|
414
|
+
for r, i in pairs:
|
|
415
|
+
r.mapped.add(i)
|
|
416
|
+
held += need
|
|
417
|
+
return True
|
|
418
|
+
next_ptr = 1 << 40
|
|
419
|
+
where: Dict[object, Tuple[int, int]] = {} # key -> (ptr, requested bytes)
|
|
420
|
+
seg_bytes: Dict[int, int] = {}
|
|
421
|
+
arenas: set = set() # the ptrs of blocks outside the pool
|
|
422
|
+
|
|
423
|
+
def to_driver(dev: int, ptr: int) -> int:
|
|
424
|
+
nonlocal held
|
|
425
|
+
n = pool.drop_segment(dev, ptr)
|
|
426
|
+
held -= seg_bytes.pop(ptr)
|
|
427
|
+
return n
|
|
428
|
+
|
|
429
|
+
for kind, key, nbytes in events:
|
|
430
|
+
if kind == "arena":
|
|
431
|
+
n = int(nbytes)
|
|
432
|
+
if n == 0:
|
|
433
|
+
continue
|
|
434
|
+
ptr = next_ptr
|
|
435
|
+
next_ptr += -(-n // align) * align + align
|
|
436
|
+
seg_bytes[ptr] = n
|
|
437
|
+
arenas.add(ptr)
|
|
438
|
+
held += n
|
|
439
|
+
where[key] = (ptr, n)
|
|
440
|
+
live += n
|
|
441
|
+
peak = max(peak, held)
|
|
442
|
+
peak_live = max(peak_live, live)
|
|
443
|
+
continue
|
|
444
|
+
if kind == "alloc":
|
|
445
|
+
n = int(nbytes)
|
|
446
|
+
if n == 0:
|
|
447
|
+
continue
|
|
448
|
+
if rng is not None and pool.cls(n) == LARGE:
|
|
449
|
+
ptr = pool.take(0, n)
|
|
450
|
+
if ptr is None:
|
|
451
|
+
_r, lo, hi = range_extend(pool, 0, pool.grow_size(n))
|
|
452
|
+
ptr = pool.take(0, n)
|
|
453
|
+
ok = map_chunks(range_unmapped(pool, 0, ptr, pool.alloc_size.get(ptr, n)))
|
|
454
|
+
pool.alloc_size.pop(ptr, None)
|
|
455
|
+
if not ok:
|
|
456
|
+
refusals += 1
|
|
457
|
+
pool.park(0, pool.release(ptr))
|
|
458
|
+
continue
|
|
459
|
+
where[key] = (ptr, n)
|
|
460
|
+
live += n
|
|
461
|
+
peak = max(peak, held)
|
|
462
|
+
peak_live = max(peak_live, live)
|
|
463
|
+
continue
|
|
464
|
+
ptr = pool.take(0, n) if pooled else None
|
|
465
|
+
if ptr is None:
|
|
466
|
+
grown = pool.grow_size(n) if pooled else n
|
|
467
|
+
if cap is not None and held + grown > cap and pooled:
|
|
468
|
+
pool.flush([0], to_driver) # the refusal's flush, then the retry
|
|
469
|
+
if rng is not None:
|
|
470
|
+
for r, i in range_reclaimable(pool, 0):
|
|
471
|
+
r.mapped.discard(i)
|
|
472
|
+
held -= r.chunk
|
|
473
|
+
if cap is not None and held + grown > cap:
|
|
474
|
+
refusals += 1
|
|
475
|
+
continue
|
|
476
|
+
seg = next_ptr
|
|
477
|
+
next_ptr += grown + align
|
|
478
|
+
seg_bytes[seg] = grown
|
|
479
|
+
held += grown
|
|
480
|
+
if pooled:
|
|
481
|
+
pool.add_free_segment(0, seg, grown, n)
|
|
482
|
+
ptr = pool.take(0, n)
|
|
483
|
+
else:
|
|
484
|
+
ptr = seg
|
|
485
|
+
pool.alloc_size.pop(ptr, None) if pooled else None
|
|
486
|
+
where[key] = (ptr, n)
|
|
487
|
+
live += n
|
|
488
|
+
peak = max(peak, held)
|
|
489
|
+
peak_live = max(peak_live, live)
|
|
490
|
+
elif kind == "free":
|
|
491
|
+
if key not in where:
|
|
492
|
+
continue
|
|
493
|
+
ptr, n = where.pop(key)
|
|
494
|
+
live -= n
|
|
495
|
+
if ptr in arenas:
|
|
496
|
+
arenas.discard(ptr)
|
|
497
|
+
held -= seg_bytes.pop(ptr)
|
|
498
|
+
elif pooled:
|
|
499
|
+
cached = pool.park(0, pool.release(ptr))
|
|
500
|
+
parked = max(parked, cached)
|
|
501
|
+
if parked_cap and cached > parked_cap:
|
|
502
|
+
pool.evict(0, cached - parked_cap, to_driver)
|
|
503
|
+
else:
|
|
504
|
+
held -= seg_bytes.pop(ptr)
|
|
505
|
+
else:
|
|
506
|
+
raise ValueError(f"simulate: an event is ('alloc' | 'free', key, nbytes), not {kind!r}")
|
|
507
|
+
return Simulation(peak, peak_live, parked, refusals)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Shadow device memory: addresses that address nothing, distinct and aligned, handed out where an application runs the
|
|
2
|
+
library with no device in sight (the engine's launch census derives every kernel key a model forms without running it)
|
|
3
|
+
— moved from the engine's census, M2. The application substitutes `malloc` for the allocator's direct allocation
|
|
4
|
+
(`DeviceAllocator.malloc_direct`); nothing here is installed by itself, and a real run never records anything here.
|
|
5
|
+
|
|
6
|
+
Every allocation the shadow hands out is recorded AS a shadow: base -> nbytes, bases sorted. A consumer that must size
|
|
7
|
+
an allocation (the Metal pin sizes its whole-allocation wrap) asks `shadow_range` and gets an answer for a recorded
|
|
8
|
+
shadow and None for anything else, so an address nobody recorded keeps failing in every run, shadow or not. Measured
|
|
9
|
+
2026-09-24: the 76 MoE census failures were the pin asking the allocator about addresses only the shadow had handed
|
|
10
|
+
out. Torch-free.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import bisect
|
|
15
|
+
from typing import Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
_NEXT = [1 << 40] # addresses that address nothing, distinct and aligned
|
|
18
|
+
_RANGES: Dict[int, int] = {}
|
|
19
|
+
_BASES: List[int] = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def malloc(nbytes: int, dev_idx: Optional[int] = None, pooled: bool = True) -> int:
|
|
23
|
+
"""`DeviceAllocator.malloc_direct` in a shadow, its signature kept whole (`pooled`: a weight arena asks for a block
|
|
24
|
+
outside the pool; a shadow has no pool and answers the same address either way)."""
|
|
25
|
+
ptr = _NEXT[0]
|
|
26
|
+
_NEXT[0] += (int(nbytes) + 255) // 256 * 256 + 256
|
|
27
|
+
_RANGES[ptr] = int(nbytes)
|
|
28
|
+
_BASES.append(ptr) # monotonic: appending keeps the list sorted
|
|
29
|
+
return ptr
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def shadow_range(addr: int) -> Optional[tuple]:
|
|
33
|
+
"""(base, nbytes) of the RECORDED shadow allocation containing `addr`, or None.
|
|
34
|
+
|
|
35
|
+
None for every address this shadow did not hand out, including every address of a real
|
|
36
|
+
run, where nothing is ever recorded here."""
|
|
37
|
+
addr = int(addr)
|
|
38
|
+
i = bisect.bisect_right(_BASES, addr) - 1
|
|
39
|
+
if i < 0:
|
|
40
|
+
return None
|
|
41
|
+
base = _BASES[i]
|
|
42
|
+
size = _RANGES[base]
|
|
43
|
+
return (base, size) if addr < base + max(size, 1) else None
|