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,151 @@
|
|
|
1
|
+
"""The device-fault channel: a kernel on a backend that does not honour `tl.device_assert` reports a contract violation
|
|
2
|
+
through a device word, read at the sync points. Cut out of tensor.py (M2, 2026-10-29), unchanged."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ctypes
|
|
7
|
+
import functools
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
from nbxtensor.backend import capabilities as _caps
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Whether this backend's device memory is directly readable at its own
|
|
14
|
+
# `data_ptr()` from the host. The fault channel below reads its status word
|
|
15
|
+
# that way — after a flush that has already happened, so no copy and no
|
|
16
|
+
# recursion through `memcpy`. Unified memory says yes; a discrete heap does
|
|
17
|
+
# not, and must add its row (and a copy path) before the channel is armed
|
|
18
|
+
# on it.
|
|
19
|
+
_BACKEND_MEMORY_IS_HOST_READABLE = {"cuda": False, "hip": False, "metal": True}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ============================================================================
|
|
23
|
+
# DEVICE-SIDE FAULT CHANNEL
|
|
24
|
+
# ============================================================================
|
|
25
|
+
#
|
|
26
|
+
# The portable half of `tl.device_assert`, for backends that do not honour
|
|
27
|
+
# it. A kernel that detects a broken contract stores its fault code into a
|
|
28
|
+
# one-word status buffer; the code is a `tl.constexpr`, so on a backend that
|
|
29
|
+
# DOES trap the wrapper passes 0 and not one instruction is emitted (the
|
|
30
|
+
# CUDA path is unchanged down to the generated PTX bar one unused kernel
|
|
31
|
+
# parameter).
|
|
32
|
+
#
|
|
33
|
+
# Why a device-side word rather than checking the indices on the host:
|
|
34
|
+
# measured 2026-09-10 on this machine, a host readback of the index tensor
|
|
35
|
+
# costs +0.24 ms (1 id) to +0.29 ms (512 ids) per call against an embedding
|
|
36
|
+
# launch of 0.017–0.020 ms — 13x to 17x the op it guards, and flat in the
|
|
37
|
+
# number of indices because the cost is the `flush()` a D2H copy performs,
|
|
38
|
+
# not the data. That is the same trap the D2D measurement of 2026-09-07
|
|
39
|
+
# found (1,086 host-ordered copies in an 8-token decode: 0.007 s -> 4.08 s).
|
|
40
|
+
# The status word costs one conditional store on the failing path and a
|
|
41
|
+
# 4-byte read at a sync that was already happening.
|
|
42
|
+
#
|
|
43
|
+
# It is READ where the host observes device results — after `sync_device()`
|
|
44
|
+
# and after a copy to the host — which is the sticky-error contract
|
|
45
|
+
# `sync_device` already documents: the fault is at or before that point.
|
|
46
|
+
_FAULT_MESSAGES: list = []
|
|
47
|
+
_FAULT_BUFFERS: dict = {}
|
|
48
|
+
_FAULT_LOCK = threading.Lock()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def register_device_fault(message: str) -> int:
|
|
52
|
+
"""Reserve a fault code for `message`. Called at import by the kernel
|
|
53
|
+
that raises it, so codes are stable within a process and never zero
|
|
54
|
+
(zero is 'clean', and it is what a disarmed kernel is compiled with)."""
|
|
55
|
+
with _FAULT_LOCK:
|
|
56
|
+
if message not in _FAULT_MESSAGES:
|
|
57
|
+
_FAULT_MESSAGES.append(message)
|
|
58
|
+
return _FAULT_MESSAGES.index(message) + 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def device_fault_code(message: str, *, armed: bool = None) -> int:
|
|
62
|
+
"""The code a kernel should be compiled with: its own where the backend
|
|
63
|
+
needs the channel, 0 where the backend traps by itself."""
|
|
64
|
+
if armed is None:
|
|
65
|
+
armed = not _caps.backend_traps_on_device_assert()
|
|
66
|
+
return register_device_fault(message) if armed else 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@functools.lru_cache(maxsize=None)
|
|
70
|
+
def device_fault_code_cached(message: str) -> int:
|
|
71
|
+
"""`device_fault_code` memoised per message — it runs at every launch of
|
|
72
|
+
an armed kernel, and the answer cannot change within a process."""
|
|
73
|
+
return device_fault_code(message)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fault_channel(message: str, spare: 'NBXTensor') -> tuple:
|
|
77
|
+
"""`(pointer_argument, FAULT_CODE)` for a kernel that carries the channel.
|
|
78
|
+
|
|
79
|
+
Where the backend HONOURS `tl.device_assert`, the code is 0 and the
|
|
80
|
+
generated kernel contains no fault store at all — proven on the emitted
|
|
81
|
+
code, not assumed: with FAULT_CODE=0 the body carries neither the store nor
|
|
82
|
+
the reduction that feeds it. The pointer is then a bound-but-unread
|
|
83
|
+
argument, so `spare` — a tensor the call already owns — is handed over and
|
|
84
|
+
NOTHING IS ALLOCATED.
|
|
85
|
+
|
|
86
|
+
That matters because the fault buffer is never freed by contract (a frozen
|
|
87
|
+
replay plan records its raw pointer), so allocating it on a backend that
|
|
88
|
+
can never write to it is a permanent allocation for a dead path. Reported
|
|
89
|
+
from the other machine 2026-09-11, whose CUDA proof also says the guard
|
|
90
|
+
itself holds: three kernels refuse an out-of-range index by name, six
|
|
91
|
+
outputs identical, +0.13% wall.
|
|
92
|
+
|
|
93
|
+
`spare` is never dereferenced through this argument on that path; passing
|
|
94
|
+
the output tensor is therefore safe and costs nothing.
|
|
95
|
+
"""
|
|
96
|
+
code = device_fault_code_cached(message)
|
|
97
|
+
if code == 0:
|
|
98
|
+
return spare, 0
|
|
99
|
+
return device_fault_buffer(spare._device_idx), code
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def device_fault_buffer(device_idx: int) -> 'NBXTensor':
|
|
103
|
+
"""This device's zeroed one-word status buffer.
|
|
104
|
+
|
|
105
|
+
Handed to the kernel as a TENSOR, not as a raw address: the launcher
|
|
106
|
+
binds a tensor as a pointer argument, and a bare int arrives as an
|
|
107
|
+
int64 scalar that `tl.store` refuses.
|
|
108
|
+
|
|
109
|
+
Never freed: a kernel launch records this raw pointer, and a frozen
|
|
110
|
+
replay plan may hold it long after the call that produced it — the same
|
|
111
|
+
reason `_INT64_ARRAY_CACHE` is never evicted.
|
|
112
|
+
"""
|
|
113
|
+
buf = _FAULT_BUFFERS.get(device_idx) # hot path: no lock
|
|
114
|
+
if buf is None:
|
|
115
|
+
with _FAULT_LOCK:
|
|
116
|
+
buf = _FAULT_BUFFERS.get(device_idx)
|
|
117
|
+
if buf is None:
|
|
118
|
+
from nbxtensor.dtype.nbx_dtype import NBXDtype # the tensor module imports this one
|
|
119
|
+
from nbxtensor.tensor import NBXTensor
|
|
120
|
+
buf = NBXTensor.zeros((1,), NBXDtype.int32, f"cuda:{device_idx}")
|
|
121
|
+
_FAULT_BUFFERS[device_idx] = buf
|
|
122
|
+
return buf
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_device_faults() -> None:
|
|
126
|
+
"""Raise if any kernel reported a broken contract since the last check.
|
|
127
|
+
|
|
128
|
+
Reads the status word straight at its own address: this runs only after
|
|
129
|
+
a flush (a completed sync, or a copy that flushed), and only on a
|
|
130
|
+
backend whose device memory the host can read directly. The word is
|
|
131
|
+
cleared as it is read, so one violation raises once.
|
|
132
|
+
"""
|
|
133
|
+
if not _FAULT_BUFFERS:
|
|
134
|
+
return
|
|
135
|
+
if not _caps._backend_capability(
|
|
136
|
+
_BACKEND_MEMORY_IS_HOST_READABLE, "_BACKEND_MEMORY_IS_HOST_READABLE",
|
|
137
|
+
"whether the host can read its device memory directly"):
|
|
138
|
+
return
|
|
139
|
+
for device_idx, buf in list(_FAULT_BUFFERS.items()):
|
|
140
|
+
word = ctypes.c_int32.from_address(buf.data_ptr())
|
|
141
|
+
code = word.value
|
|
142
|
+
if code:
|
|
143
|
+
word.value = 0
|
|
144
|
+
message = (_FAULT_MESSAGES[code - 1]
|
|
145
|
+
if 1 <= code <= len(_FAULT_MESSAGES)
|
|
146
|
+
else f"unregistered fault code {code}")
|
|
147
|
+
raise RuntimeError(
|
|
148
|
+
f"device-side contract violated on device {device_idx}: "
|
|
149
|
+
f"{message}. The kernel reported it through the fault "
|
|
150
|
+
f"channel because this backend does not honour "
|
|
151
|
+
f"`tl.device_assert`; the fault is at or before this point.")
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""What the machine actually has free, right now.
|
|
2
|
+
|
|
3
|
+
A budget that describes what the hardware *recommends* is not a measure of
|
|
4
|
+
what is *available*. On unified memory the two are unrelated: this M4 Pro
|
|
5
|
+
reports `recommendedMaxWorkingSetSize` = 18 186 MB and does not lower it by
|
|
6
|
+
one byte when another process holds a third of the machine. A user never has
|
|
7
|
+
an empty machine — a browser, a mail client, sometimes a container or a VM —
|
|
8
|
+
so a plan sized against the recommendation is accepted and then killed by the
|
|
9
|
+
system under memory pressure, mid-render, without a word.
|
|
10
|
+
|
|
11
|
+
This module measures the other number. It has two consumers and they want
|
|
12
|
+
the same reading:
|
|
13
|
+
|
|
14
|
+
* the planner, which must refuse loudly and name BOTH figures rather than
|
|
15
|
+
start and be killed;
|
|
16
|
+
* the measurement protocol, because a green obtained on a half-occupied
|
|
17
|
+
machine is not the same green as one obtained on a free machine, and
|
|
18
|
+
without this line written at the time nobody can tell them apart later.
|
|
19
|
+
|
|
20
|
+
Nothing is guessed. Where the platform is not one this module knows how to
|
|
21
|
+
read, `available_mb` is None and `source` says so — the caller then announces
|
|
22
|
+
that it could not be measured, which is honest, rather than substituting a
|
|
23
|
+
number, which is not.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Optional, Tuple
|
|
32
|
+
|
|
33
|
+
_MB = 1024 * 1024
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class MemoryState:
|
|
38
|
+
"""One reading of the machine, at one instant."""
|
|
39
|
+
|
|
40
|
+
total_mb: Optional[int] = None
|
|
41
|
+
available_mb: Optional[int] = None
|
|
42
|
+
swap_used_mb: Optional[int] = None
|
|
43
|
+
swap_total_mb: Optional[int] = None
|
|
44
|
+
#: (command, resident MB), largest first — this is what names the VM or
|
|
45
|
+
#: the browser holding the memory, without hardcoding either.
|
|
46
|
+
largest_residents: Tuple[Tuple[str, int], ...] = field(default=())
|
|
47
|
+
#: How it was read, or why it could not be.
|
|
48
|
+
source: str = "not measured"
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def measured(self) -> bool:
|
|
52
|
+
return self.available_mb is not None
|
|
53
|
+
|
|
54
|
+
def describe(self) -> str:
|
|
55
|
+
"""The one line every measurement cell writes beside its result."""
|
|
56
|
+
if not self.measured:
|
|
57
|
+
return f"machine memory: NOT MEASURED ({self.source})"
|
|
58
|
+
parts = [f"{self.available_mb} MB free"]
|
|
59
|
+
if self.total_mb:
|
|
60
|
+
parts.append(f"of {self.total_mb} MB")
|
|
61
|
+
if self.swap_total_mb:
|
|
62
|
+
parts.append(f"swap {self.swap_used_mb}/{self.swap_total_mb} MB")
|
|
63
|
+
if self.largest_residents:
|
|
64
|
+
biggest = ", ".join(f"{name} {mb} MB"
|
|
65
|
+
for name, mb in self.largest_residents)
|
|
66
|
+
parts.append(f"largest resident: {biggest}")
|
|
67
|
+
return "machine memory: " + ", ".join(parts)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _run(cmd) -> Optional[str]:
|
|
71
|
+
try:
|
|
72
|
+
out = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
|
73
|
+
except (OSError, subprocess.SubprocessError):
|
|
74
|
+
return None
|
|
75
|
+
return out.stdout if out.returncode == 0 else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _largest_residents(count: int = 3) -> Tuple[Tuple[str, int], ...]:
|
|
79
|
+
"""The biggest resident processes, by RSS. Named, never acted on."""
|
|
80
|
+
out = _run(["ps", "-Ao", "rss,comm", "-r"])
|
|
81
|
+
if not out:
|
|
82
|
+
return ()
|
|
83
|
+
found = []
|
|
84
|
+
for line in out.splitlines()[1:]:
|
|
85
|
+
line = line.strip()
|
|
86
|
+
if not line:
|
|
87
|
+
continue
|
|
88
|
+
rss, _, comm = line.partition(" ")
|
|
89
|
+
if not rss.isdigit():
|
|
90
|
+
continue
|
|
91
|
+
mb = int(rss) // 1024
|
|
92
|
+
if mb <= 0:
|
|
93
|
+
continue
|
|
94
|
+
found.append((comm.strip().rsplit("/", 1)[-1], mb))
|
|
95
|
+
if len(found) >= count:
|
|
96
|
+
break
|
|
97
|
+
return tuple(found)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _available_mb_from_vm_stat(vm: str) -> int:
|
|
101
|
+
"""The available figure from a `vm_stat` text (the reader's one parsing, testable on a captured text)."""
|
|
102
|
+
page = re.search(r"page size of (\d+) bytes", vm)
|
|
103
|
+
page_size = int(page.group(1)) if page else 4096
|
|
104
|
+
|
|
105
|
+
def pages(label: str) -> int:
|
|
106
|
+
m = re.search(rf"{label}:\s+(\d+)", vm)
|
|
107
|
+
return int(m.group(1)) if m else 0
|
|
108
|
+
|
|
109
|
+
# The reclaimable set, what a new allocation gets back WITHOUT the compressor: free pages, purgeable
|
|
110
|
+
# pages, and the whole file cache — `File-backed pages`, active or inactive (Activity Monitor's
|
|
111
|
+
# "Cached Files"); the speculative queue is already inside it (measured on this M4 Pro:
|
|
112
|
+
# active + inactive + speculative == file-backed + anonymous). Anonymous pages are NOT counted,
|
|
113
|
+
# active or inactive: they come back only through the compressor and swap. The previous formula
|
|
114
|
+
# (free + inactive + purgeable + speculative) subtracted every ACTIVE file page — 4.4 GB of the
|
|
115
|
+
# container just copied when orpheus-3b was refused at placement (2026-09-29, inbox 68).
|
|
116
|
+
available_pages = (pages("Pages free") + pages("Pages purgeable") + pages("File-backed pages"))
|
|
117
|
+
return available_pages * page_size // _MB
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _macos_state() -> MemoryState:
|
|
121
|
+
"""`vm_stat` for pages, `sysctl` for the totals and for swap.
|
|
122
|
+
|
|
123
|
+
Available is free + purgeable + file-backed pages: the file cache is
|
|
124
|
+
reclaimable on demand whichever queue holds it, and is never subtracted;
|
|
125
|
+
anonymous, wired and compressed pages are not reclaimable without the
|
|
126
|
+
compressor and are deliberately excluded (`_available_mb_from_vm_stat`).
|
|
127
|
+
"""
|
|
128
|
+
vm = _run(["vm_stat"])
|
|
129
|
+
if not vm:
|
|
130
|
+
return MemoryState(source="vm_stat unavailable")
|
|
131
|
+
available_mb = _available_mb_from_vm_stat(vm)
|
|
132
|
+
|
|
133
|
+
total_mb = None
|
|
134
|
+
memsize = _run(["sysctl", "-n", "hw.memsize"])
|
|
135
|
+
if memsize and memsize.strip().isdigit():
|
|
136
|
+
total_mb = int(memsize.strip()) // _MB
|
|
137
|
+
|
|
138
|
+
swap_used = swap_total = None
|
|
139
|
+
swap = _run(["sysctl", "-n", "vm.swapusage"])
|
|
140
|
+
if swap:
|
|
141
|
+
tot = re.search(r"total\s*=\s*([\d.]+)M", swap)
|
|
142
|
+
use = re.search(r"used\s*=\s*([\d.]+)M", swap)
|
|
143
|
+
if tot:
|
|
144
|
+
swap_total = int(float(tot.group(1)))
|
|
145
|
+
if use:
|
|
146
|
+
swap_used = int(float(use.group(1)))
|
|
147
|
+
|
|
148
|
+
return MemoryState(total_mb=total_mb, available_mb=available_mb,
|
|
149
|
+
swap_used_mb=swap_used, swap_total_mb=swap_total,
|
|
150
|
+
largest_residents=_largest_residents(),
|
|
151
|
+
source="vm_stat + sysctl")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _linux_state() -> MemoryState:
|
|
155
|
+
"""`MemAvailable` is the kernel's own estimate of what a new allocation
|
|
156
|
+
can have without swapping — exactly the question, already answered."""
|
|
157
|
+
try:
|
|
158
|
+
text = open("/proc/meminfo").read()
|
|
159
|
+
except OSError:
|
|
160
|
+
return MemoryState(source="/proc/meminfo unreadable")
|
|
161
|
+
|
|
162
|
+
def kb(label: str) -> Optional[int]:
|
|
163
|
+
m = re.search(rf"^{label}:\s+(\d+) kB", text, re.MULTILINE)
|
|
164
|
+
return int(m.group(1)) if m else None
|
|
165
|
+
|
|
166
|
+
total, avail = kb("MemTotal"), kb("MemAvailable")
|
|
167
|
+
swap_total, swap_free = kb("SwapTotal"), kb("SwapFree")
|
|
168
|
+
return MemoryState(
|
|
169
|
+
total_mb=total // 1024 if total else None,
|
|
170
|
+
available_mb=avail // 1024 if avail is not None else None,
|
|
171
|
+
swap_total_mb=swap_total // 1024 if swap_total else None,
|
|
172
|
+
swap_used_mb=((swap_total - swap_free) // 1024
|
|
173
|
+
if swap_total is not None and swap_free is not None
|
|
174
|
+
else None),
|
|
175
|
+
largest_residents=_largest_residents(),
|
|
176
|
+
source="/proc/meminfo")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def memory_state() -> MemoryState:
|
|
180
|
+
"""Read the machine now. Never cached: the whole point is that it moves."""
|
|
181
|
+
if sys.platform == "darwin":
|
|
182
|
+
return _macos_state()
|
|
183
|
+
if sys.platform.startswith("linux"):
|
|
184
|
+
return _linux_state()
|
|
185
|
+
return MemoryState(source=f"no reader for platform {sys.platform!r}")
|