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,73 @@
|
|
|
1
|
+
"""What THIS process holds on a device, as the driver attributes it to our context — never the card's free/total, which
|
|
2
|
+
counts every other process (the desktop, a browser, another model) as ours (the supervisor 2026-11-02 09:50: a profile
|
|
3
|
+
fact a neighbour can falsify is not a fact; runtime_growth.json for Allegro-TI2V read 7 430 MB with another run on the
|
|
4
|
+
card, 654 MB alone).
|
|
5
|
+
|
|
6
|
+
CUDA: NVML's per-process accounting (`nvmlDeviceGetComputeRunningProcesses_v3`, `usedGpuMemory` of our pid), the device
|
|
7
|
+
found by its PCI bus id (`cudaDeviceGetPCIBusId` of the process's ordinal — NVML numbers the physical cards, the runtime
|
|
8
|
+
the visible ones). A backend without such an accounting (HIP, Metal) or a driver that does not report it answers None:
|
|
9
|
+
the caller then refuses to measure, it never falls back to the card's reading.
|
|
10
|
+
|
|
11
|
+
ctypes on the driver's management library; no torch, no vendor package.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import ctypes
|
|
16
|
+
import os
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
_NVML = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _ProcInfo(ctypes.Structure):
|
|
23
|
+
_fields_ = [("pid", ctypes.c_uint), ("usedGpuMemory", ctypes.c_ulonglong),
|
|
24
|
+
("gpuInstanceId", ctypes.c_uint), ("computeInstanceId", ctypes.c_uint)]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _nvml(libs):
|
|
28
|
+
global _NVML
|
|
29
|
+
if _NVML is None:
|
|
30
|
+
_NVML = False
|
|
31
|
+
for name in libs or ():
|
|
32
|
+
try:
|
|
33
|
+
lib = ctypes.CDLL(name)
|
|
34
|
+
except OSError:
|
|
35
|
+
continue
|
|
36
|
+
if lib.nvmlInit_v2() == 0:
|
|
37
|
+
_NVML = lib
|
|
38
|
+
break
|
|
39
|
+
return _NVML or None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def process_device_bytes(device: int) -> Optional[int]:
|
|
43
|
+
"""The bytes the driver attributes to this process on the visible `device`, or None when the backend or the driver
|
|
44
|
+
gives no per-process accounting."""
|
|
45
|
+
from nbxtensor.backend import runtime as _rt
|
|
46
|
+
table = _rt.backend_table()
|
|
47
|
+
lib = _nvml(table.get("management_libs"))
|
|
48
|
+
bus_fn = table.get("pci_bus_id")
|
|
49
|
+
if lib is None or not bus_fn:
|
|
50
|
+
return None
|
|
51
|
+
bus = ctypes.create_string_buffer(64)
|
|
52
|
+
if getattr(_rt.gpu_runtime(), bus_fn)(bus, 64, int(device)) != 0:
|
|
53
|
+
return None
|
|
54
|
+
handle = ctypes.c_void_p()
|
|
55
|
+
if lib.nvmlDeviceGetHandleByPciBusId_v2(bus, ctypes.byref(handle)) != 0:
|
|
56
|
+
return None
|
|
57
|
+
count = ctypes.c_uint(0)
|
|
58
|
+
rc = lib.nvmlDeviceGetComputeRunningProcesses_v3(handle, ctypes.byref(count), None)
|
|
59
|
+
if rc not in (0, 7): # 7: NVML_ERROR_INSUFFICIENT_SIZE, the count is set
|
|
60
|
+
return None
|
|
61
|
+
n = count.value + 8 # room for processes starting meanwhile
|
|
62
|
+
infos = (_ProcInfo * n)()
|
|
63
|
+
count = ctypes.c_uint(n)
|
|
64
|
+
if lib.nvmlDeviceGetComputeRunningProcesses_v3(handle, ctypes.byref(count), infos) != 0:
|
|
65
|
+
return None
|
|
66
|
+
me = os.getpid()
|
|
67
|
+
for i in range(count.value):
|
|
68
|
+
if infos[i].pid == me:
|
|
69
|
+
used = int(infos[i].usedGpuMemory)
|
|
70
|
+
# NVML_VALUE_NOT_AVAILABLE (all bits set): the driver does not report it (a container without the pid
|
|
71
|
+
# namespace, a WDDM device) — no reading, never a guess
|
|
72
|
+
return None if used >= (1 << 64) - 1 else used
|
|
73
|
+
return 0 # no context of ours on that device yet
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Phase 4b — per-bucket CUDA-graph capture of a FrozenPlan.
|
|
2
|
+
|
|
3
|
+
Captures a plan's recorded action list ONCE into a CUDA graph on a
|
|
4
|
+
dedicated non-blocking stream and replays it as one cuGraphLaunch per
|
|
5
|
+
step. Adoption is gated on a measured positive V100 A/B delta vs the
|
|
6
|
+
direct C-launcher replay (Phase 4a); every CUresult is checked and any
|
|
7
|
+
failure refuses LOUDLY back to direct replay for that bucket
|
|
8
|
+
(llama.cpp guard discipline — their CC<8 disable is a policy under a
|
|
9
|
+
re-capture-per-token cost model, not a hardware gate; sourced research
|
|
10
|
+
in optimization_engine_scoping.md §4b). Kill-switch: unset
|
|
11
|
+
NBX_REPLAY_GRAPH (the graph path is opt-in).
|
|
12
|
+
|
|
13
|
+
Capture legality:
|
|
14
|
+
- kernels are re-issued through the SAME compiled C launcher but on
|
|
15
|
+
the CAPTURE stream (the recorded stream may be the uncapturable
|
|
16
|
+
legacy stream; serialization semantics are preserved — one stream
|
|
17
|
+
either way). The launcher is capture-safe: arg pack + cuLaunchKernel,
|
|
18
|
+
no sync/query; JIT warmup already happened at recording.
|
|
19
|
+
- recorded D2D memcpys / memsets become cuMemcpyDtoDAsync /
|
|
20
|
+
cuMemsetD8Async on the capture stream.
|
|
21
|
+
- recorded H2D snapshot actions become DEVICE-staged D2D nodes (the
|
|
22
|
+
snapshot bytes are plan constants — uploaded once outside capture).
|
|
23
|
+
- the per-step INPUT copies (slot scan) and OUTPUT fresh copies stay
|
|
24
|
+
OUTSIDE the graph in FrozenPlan.replay — they are the step-varying
|
|
25
|
+
host-driven part.
|
|
26
|
+
|
|
27
|
+
ORDERING (the 2026-08-17 root cause): the engine's per-step input
|
|
28
|
+
copies are ASYNC legacy-stream work, and a NON_BLOCKING stream does
|
|
29
|
+
not implicitly wait for the legacy stream — an unbarriered graph
|
|
30
|
+
launch read one-step-STALE inputs (deterministic-looking token
|
|
31
|
+
doubling, masked by any diagnostic D2H sync, which made every
|
|
32
|
+
bisection "boundary" a timing artifact until the dump-run-correct /
|
|
33
|
+
plain-run-wrong pair exposed the observer effect). launch() therefore
|
|
34
|
+
brackets cuGraphLaunch with device-wide syncs: before (inputs land)
|
|
35
|
+
and after (legacy consumers read the graph's writes).
|
|
36
|
+
|
|
37
|
+
During stream capture NOTHING executes — the actions are recorded into
|
|
38
|
+
the graph. The capturing step therefore launches the instantiated
|
|
39
|
+
graph immediately after capture to actually perform its work.
|
|
40
|
+
|
|
41
|
+
R33: ctypes on libcuda only.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import ctypes
|
|
47
|
+
from typing import List, Tuple
|
|
48
|
+
|
|
49
|
+
from nbxtensor.memory.allocator import DeviceAllocator
|
|
50
|
+
|
|
51
|
+
_CUDA_SUCCESS = 0
|
|
52
|
+
_CU_STREAM_NON_BLOCKING = 1
|
|
53
|
+
_CU_STREAM_CAPTURE_MODE_RELAXED = 2
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GraphReplayError(RuntimeError):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ck(rc: int, what: str) -> None:
|
|
61
|
+
if rc != _CUDA_SUCCESS:
|
|
62
|
+
raise GraphReplayError(f"{what}: CUresult={rc}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _CudaGraphAPI:
|
|
66
|
+
"""Lazy ctypes surface over libcuda's graph entry points. Every
|
|
67
|
+
pointer crosses as c_void_p / c_size_t (no implicit c_int
|
|
68
|
+
truncation)."""
|
|
69
|
+
|
|
70
|
+
_inst = None
|
|
71
|
+
|
|
72
|
+
def __init__(self) -> None:
|
|
73
|
+
lib = ctypes.CDLL("libcuda.so.1")
|
|
74
|
+
|
|
75
|
+
def bind(name, argtypes):
|
|
76
|
+
fn = getattr(lib, name)
|
|
77
|
+
fn.argtypes = argtypes
|
|
78
|
+
fn.restype = ctypes.c_int
|
|
79
|
+
return fn
|
|
80
|
+
|
|
81
|
+
vp, sz = ctypes.c_void_p, ctypes.c_size_t
|
|
82
|
+
self.begin = bind("cuStreamBeginCapture_v2", [vp, ctypes.c_int])
|
|
83
|
+
self.end = bind("cuStreamEndCapture", [vp, ctypes.POINTER(vp)])
|
|
84
|
+
try:
|
|
85
|
+
self.instantiate = bind(
|
|
86
|
+
"cuGraphInstantiateWithFlags",
|
|
87
|
+
[ctypes.POINTER(vp), vp, ctypes.c_ulonglong])
|
|
88
|
+
self.inst_flags = True
|
|
89
|
+
except AttributeError:
|
|
90
|
+
self.instantiate = bind(
|
|
91
|
+
"cuGraphInstantiate_v2",
|
|
92
|
+
[ctypes.POINTER(vp), vp, vp, vp, sz])
|
|
93
|
+
self.inst_flags = False
|
|
94
|
+
self.launch = bind("cuGraphLaunch", [vp, vp])
|
|
95
|
+
self.graph_destroy = bind("cuGraphDestroy", [vp])
|
|
96
|
+
self.exec_destroy = bind("cuGraphExecDestroy", [vp])
|
|
97
|
+
self.stream_create = bind(
|
|
98
|
+
"cuStreamCreate", [ctypes.POINTER(vp), ctypes.c_uint])
|
|
99
|
+
self.stream_sync = bind("cuStreamSynchronize", [vp])
|
|
100
|
+
self.stream_destroy = bind("cuStreamDestroy_v2", [vp])
|
|
101
|
+
self.memcpy_dtod = bind("cuMemcpyDtoDAsync_v2", [vp, vp, sz, vp])
|
|
102
|
+
self.memset_d8 = bind(
|
|
103
|
+
"cuMemsetD8Async", [vp, ctypes.c_ubyte, sz, vp])
|
|
104
|
+
self.get_nodes = bind(
|
|
105
|
+
"cuGraphGetNodes", [vp, vp, ctypes.POINTER(sz)])
|
|
106
|
+
self.ctx_sync = bind("cuCtxSynchronize", [])
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def get(cls) -> "_CudaGraphAPI":
|
|
110
|
+
if cls._inst is None:
|
|
111
|
+
cls._inst = _CudaGraphAPI()
|
|
112
|
+
return cls._inst
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class CapturedPlan:
|
|
116
|
+
"""One bucket's action list as an instantiated CUDA graph."""
|
|
117
|
+
|
|
118
|
+
__slots__ = ("api", "stream", "graph", "gexec", "staging",
|
|
119
|
+
"launches")
|
|
120
|
+
|
|
121
|
+
def __init__(self, records: List[Tuple[str, tuple]],
|
|
122
|
+
dev_idx) -> None:
|
|
123
|
+
api = _CudaGraphAPI.get()
|
|
124
|
+
self.api = api
|
|
125
|
+
self.staging: List[int] = []
|
|
126
|
+
self.graph = ctypes.c_void_p()
|
|
127
|
+
self.gexec = ctypes.c_void_p()
|
|
128
|
+
if dev_idx is not None:
|
|
129
|
+
DeviceAllocator.set_device(dev_idx)
|
|
130
|
+
DeviceAllocator.ensure_triton_device(dev_idx)
|
|
131
|
+
stream = ctypes.c_void_p()
|
|
132
|
+
_ck(api.stream_create(ctypes.byref(stream),
|
|
133
|
+
_CU_STREAM_NON_BLOCKING), "cuStreamCreate")
|
|
134
|
+
self.stream = stream
|
|
135
|
+
n_launch = 0
|
|
136
|
+
capturing = False
|
|
137
|
+
# Pre-pass OUTSIDE capture: stage every recorded H2D snapshot
|
|
138
|
+
# into a dedicated device buffer (sync malloc/memcpy are not
|
|
139
|
+
# capture-legal; the snapshots are plan constants, so one
|
|
140
|
+
# upload serves every launch).
|
|
141
|
+
staged = {}
|
|
142
|
+
for i, (tag, rec) in enumerate(records):
|
|
143
|
+
if tag == "h":
|
|
144
|
+
dst, snap = rec
|
|
145
|
+
stage = DeviceAllocator.malloc_cuda(len(snap), dev_idx)
|
|
146
|
+
buf = (ctypes.c_char * len(snap)).from_buffer_copy(snap)
|
|
147
|
+
DeviceAllocator.memcpy(stage, ctypes.addressof(buf),
|
|
148
|
+
len(snap), 1)
|
|
149
|
+
staged[i] = stage
|
|
150
|
+
self.staging.append(int(stage))
|
|
151
|
+
try:
|
|
152
|
+
_ck(api.begin(stream, _CU_STREAM_CAPTURE_MODE_RELAXED),
|
|
153
|
+
"cuStreamBeginCapture_v2")
|
|
154
|
+
capturing = True
|
|
155
|
+
for i, (tag, rec) in enumerate(records):
|
|
156
|
+
if tag == "k":
|
|
157
|
+
kernel, g0, g1, g2, _rec_stream, flat = rec
|
|
158
|
+
kernel.run(g0, g1, g2, stream.value, kernel.function,
|
|
159
|
+
kernel.packed_metadata, None, None, None,
|
|
160
|
+
*flat)
|
|
161
|
+
n_launch += 1
|
|
162
|
+
elif tag == "m":
|
|
163
|
+
dst, src, nbytes, _kind = rec
|
|
164
|
+
_ck(api.memcpy_dtod(
|
|
165
|
+
ctypes.c_void_p(dst), ctypes.c_void_p(src),
|
|
166
|
+
nbytes, stream), "cuMemcpyDtoDAsync")
|
|
167
|
+
elif tag == "h":
|
|
168
|
+
# Recorded H2D snapshots become DEVICE-staged D2D
|
|
169
|
+
# nodes (staged in the pre-pass above): the graph
|
|
170
|
+
# copies device-to-device — cheaper per launch than
|
|
171
|
+
# host-pinned reads, and keeps the graph free of
|
|
172
|
+
# host-memory nodes.
|
|
173
|
+
dst, snap = rec
|
|
174
|
+
_ck(api.memcpy_dtod(
|
|
175
|
+
ctypes.c_void_p(dst),
|
|
176
|
+
ctypes.c_void_p(staged[i]),
|
|
177
|
+
len(snap), stream), "cuMemcpyDtoDAsync(h2d)")
|
|
178
|
+
else: # "s"
|
|
179
|
+
ptr, value, nbytes = rec
|
|
180
|
+
_ck(api.memset_d8(
|
|
181
|
+
ctypes.c_void_p(ptr), value & 0xFF, nbytes,
|
|
182
|
+
stream), "cuMemsetD8Async")
|
|
183
|
+
_ck(api.end(stream, ctypes.byref(self.graph)),
|
|
184
|
+
"cuStreamEndCapture")
|
|
185
|
+
capturing = False
|
|
186
|
+
if not self.graph.value:
|
|
187
|
+
raise GraphReplayError("cuStreamEndCapture: NULL graph")
|
|
188
|
+
n_nodes = ctypes.c_size_t(0)
|
|
189
|
+
_ck(api.get_nodes(self.graph, None, ctypes.byref(n_nodes)),
|
|
190
|
+
"cuGraphGetNodes")
|
|
191
|
+
if n_nodes.value != len(records):
|
|
192
|
+
raise GraphReplayError(
|
|
193
|
+
f"captured node count {n_nodes.value} != "
|
|
194
|
+
f"{len(records)} recorded actions — capture lost "
|
|
195
|
+
f"or merged nodes")
|
|
196
|
+
if api.inst_flags:
|
|
197
|
+
_ck(api.instantiate(ctypes.byref(self.gexec), self.graph,
|
|
198
|
+
0), "cuGraphInstantiateWithFlags")
|
|
199
|
+
else:
|
|
200
|
+
_ck(api.instantiate(ctypes.byref(self.gexec), self.graph,
|
|
201
|
+
None, None, 0), "cuGraphInstantiate_v2")
|
|
202
|
+
except BaseException:
|
|
203
|
+
if capturing:
|
|
204
|
+
# Terminate the dangling capture; error intentionally
|
|
205
|
+
# ignored — we are already refusing.
|
|
206
|
+
api.end(stream, ctypes.byref(self.graph))
|
|
207
|
+
self.destroy()
|
|
208
|
+
raise
|
|
209
|
+
self.launches = n_launch
|
|
210
|
+
|
|
211
|
+
def launch(self) -> None:
|
|
212
|
+
# DEVICE-wide sync BEFORE the graph: the per-step input copies
|
|
213
|
+
# (replay's slot scan) ride the LEGACY stream as async work,
|
|
214
|
+
# and a NON_BLOCKING capture stream does NOT wait for the
|
|
215
|
+
# legacy stream — without this barrier the graph read STALE
|
|
216
|
+
# inputs (measured on r535/V100: one-step-lagged token
|
|
217
|
+
# doubling, masked by any diagnostic D2H sync — the entire
|
|
218
|
+
# 2026-08-17 bisection's "clean/broken boundaries" were timing
|
|
219
|
+
# artifacts of that race).
|
|
220
|
+
_ck(self.api.ctx_sync(), "cuCtxSynchronize(pre)")
|
|
221
|
+
_ck(self.api.launch(self.gexec, self.stream), "cuGraphLaunch")
|
|
222
|
+
# ...and AFTER: downstream consumers (output copies, sampler)
|
|
223
|
+
# are legacy-stream work that would not wait for S either. A
|
|
224
|
+
# failure HERE means the graph's work was already submitted —
|
|
225
|
+
# the device state (e.g. KV position counters) has advanced,
|
|
226
|
+
# so re-running the action list would double-advance it: mark
|
|
227
|
+
# the error as post-execution so the caller crashes loudly
|
|
228
|
+
# instead of "recovering" into corrupt state.
|
|
229
|
+
try:
|
|
230
|
+
_ck(self.api.ctx_sync(), "cuCtxSynchronize(post)")
|
|
231
|
+
except GraphReplayError as e:
|
|
232
|
+
e.executed = True
|
|
233
|
+
raise
|
|
234
|
+
|
|
235
|
+
def destroy(self) -> None:
|
|
236
|
+
api = self.api
|
|
237
|
+
if self.gexec.value:
|
|
238
|
+
api.exec_destroy(self.gexec)
|
|
239
|
+
self.gexec = ctypes.c_void_p()
|
|
240
|
+
if self.graph.value:
|
|
241
|
+
api.graph_destroy(self.graph)
|
|
242
|
+
self.graph = ctypes.c_void_p()
|
|
243
|
+
for p in self.staging:
|
|
244
|
+
if p:
|
|
245
|
+
# device staging buffers (see the "h" action handler)
|
|
246
|
+
DeviceAllocator.free_cuda(p)
|
|
247
|
+
self.staging = []
|
|
248
|
+
if self.stream.value:
|
|
249
|
+
api.stream_destroy(self.stream)
|
|
250
|
+
self.stream = ctypes.c_void_p()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class SegmentedPlan:
|
|
254
|
+
"""A multi-card plan's action list as one CUDA graph per stretch of actions on one card (S3, 2026-10-28).
|
|
255
|
+
|
|
256
|
+
A plan recorded across cards (an expert store's card-held tail: each layer's attention on the home card, its MoE
|
|
257
|
+
op on the card that holds the block, the hidden state crossing by a peer copy) switches cards with a recorded
|
|
258
|
+
SETDEV and orders the crossings with recorded device syncs. Each maximal stretch of kernels, copies and memsets
|
|
259
|
+
between two of those is captured on ITS card (`CapturedPlan`, bracketed by that card's syncs, so a peer copy it
|
|
260
|
+
issues has landed before the next stretch starts); the SETDEVs and syncs between stretches run directly. The order
|
|
261
|
+
is the recorded order and every stretch completes before the next begins — the direct replay's semantics, one
|
|
262
|
+
graph launch per stretch instead of one Python-driven launch per kernel."""
|
|
263
|
+
|
|
264
|
+
__slots__ = ("steps", "launches", "stretches")
|
|
265
|
+
|
|
266
|
+
def __init__(self, records: List[Tuple[str, tuple]], dev_idx) -> None:
|
|
267
|
+
self.steps: List[Tuple[str, object]] = []
|
|
268
|
+
self.launches = 0
|
|
269
|
+
cur = dev_idx
|
|
270
|
+
run: List[Tuple[str, tuple]] = []
|
|
271
|
+
|
|
272
|
+
def close():
|
|
273
|
+
if run:
|
|
274
|
+
plan = CapturedPlan(list(run), cur)
|
|
275
|
+
self.steps.append(("g", (cur, plan)))
|
|
276
|
+
self.launches += plan.launches
|
|
277
|
+
run.clear()
|
|
278
|
+
try:
|
|
279
|
+
for tag, rec in records:
|
|
280
|
+
if tag == "d":
|
|
281
|
+
if rec[0] == cur:
|
|
282
|
+
continue # already this card: the stretch goes on (each graph sets its own card)
|
|
283
|
+
close()
|
|
284
|
+
cur = rec[0]
|
|
285
|
+
self.steps.append(("d", rec[0]))
|
|
286
|
+
elif tag == "y":
|
|
287
|
+
close()
|
|
288
|
+
self.steps.append(("y", rec[0]))
|
|
289
|
+
elif tag in ("k", "m", "h", "s"):
|
|
290
|
+
run.append((tag, rec))
|
|
291
|
+
else:
|
|
292
|
+
raise GraphReplayError(f"action {tag!r} has no graph form")
|
|
293
|
+
close()
|
|
294
|
+
except BaseException:
|
|
295
|
+
self.destroy()
|
|
296
|
+
raise
|
|
297
|
+
self.stretches = sum(1 for t, _ in self.steps if t == "g")
|
|
298
|
+
|
|
299
|
+
def launch(self) -> None:
|
|
300
|
+
for tag, val in self.steps:
|
|
301
|
+
if tag == "g":
|
|
302
|
+
dev, plan = val
|
|
303
|
+
if dev is not None:
|
|
304
|
+
DeviceAllocator.set_device(dev)
|
|
305
|
+
DeviceAllocator.ensure_triton_device(dev)
|
|
306
|
+
plan.launch()
|
|
307
|
+
elif tag == "d":
|
|
308
|
+
DeviceAllocator.set_device(val)
|
|
309
|
+
DeviceAllocator.ensure_triton_device(val)
|
|
310
|
+
else:
|
|
311
|
+
cur = DeviceAllocator.get_device()
|
|
312
|
+
if cur != val:
|
|
313
|
+
DeviceAllocator.set_device(val)
|
|
314
|
+
DeviceAllocator.sync_device()
|
|
315
|
+
if cur != val:
|
|
316
|
+
DeviceAllocator.set_device(cur)
|
|
317
|
+
|
|
318
|
+
def destroy(self) -> None:
|
|
319
|
+
for tag, val in self.steps:
|
|
320
|
+
if tag == "g":
|
|
321
|
+
val[1].destroy()
|
|
322
|
+
self.steps = []
|