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.
Files changed (291) hide show
  1. nbxtensor/THIRD_PARTY_NOTICES.md +287 -0
  2. nbxtensor/__init__.py +17 -0
  3. nbxtensor/backend/__init__.py +1 -0
  4. nbxtensor/backend/capabilities.py +207 -0
  5. nbxtensor/backend/cpu/__init__.py +1 -0
  6. nbxtensor/backend/cpu/blas.py +102 -0
  7. nbxtensor/backend/cpu/pool.py +204 -0
  8. nbxtensor/backend/cpu_backend.py +162 -0
  9. nbxtensor/backend/device_facts.py +105 -0
  10. nbxtensor/backend/device_files.py +129 -0
  11. nbxtensor/backend/device_names.py +18 -0
  12. nbxtensor/backend/devices/amd/cdna.yml +228 -0
  13. nbxtensor/backend/devices/amd/cdna2.yml +228 -0
  14. nbxtensor/backend/devices/amd/cdna3.yml +227 -0
  15. nbxtensor/backend/devices/apple/apple_m1.yml +266 -0
  16. nbxtensor/backend/devices/apple/apple_m1_max.yml +266 -0
  17. nbxtensor/backend/devices/apple/apple_m1_pro.yml +266 -0
  18. nbxtensor/backend/devices/apple/apple_m1_ultra.yml +266 -0
  19. nbxtensor/backend/devices/apple/apple_m2.yml +266 -0
  20. nbxtensor/backend/devices/apple/apple_m2_max.yml +266 -0
  21. nbxtensor/backend/devices/apple/apple_m2_pro.yml +266 -0
  22. nbxtensor/backend/devices/apple/apple_m2_ultra.yml +266 -0
  23. nbxtensor/backend/devices/apple/apple_m3.yml +266 -0
  24. nbxtensor/backend/devices/apple/apple_m3_max.yml +266 -0
  25. nbxtensor/backend/devices/apple/apple_m3_pro.yml +266 -0
  26. nbxtensor/backend/devices/apple/apple_m3_ultra.yml +266 -0
  27. nbxtensor/backend/devices/apple/apple_m4.yml +266 -0
  28. nbxtensor/backend/devices/apple/apple_m4_max.yml +266 -0
  29. nbxtensor/backend/devices/apple/apple_m4_pro.yml +308 -0
  30. nbxtensor/backend/devices/apple/apple_m5.yml +269 -0
  31. nbxtensor/backend/devices/apple/apple_m5_max.yml +269 -0
  32. nbxtensor/backend/devices/apple/apple_m5_pro.yml +269 -0
  33. nbxtensor/backend/devices/apple/apple_m5_ultra.yml +269 -0
  34. nbxtensor/backend/devices/apple/apple_m6.yml +269 -0
  35. nbxtensor/backend/devices/apple/apple_silicon.yml +263 -0
  36. nbxtensor/backend/devices/nvidia/ampere.yml +214 -0
  37. nbxtensor/backend/devices/nvidia/hopper.yml +213 -0
  38. nbxtensor/backend/devices/nvidia/volta.yml +411 -0
  39. nbxtensor/backend/faults.py +151 -0
  40. nbxtensor/backend/host_memory.py +185 -0
  41. nbxtensor/backend/launcher.py +2350 -0
  42. nbxtensor/backend/launcher_contract.py +429 -0
  43. nbxtensor/backend/metal_backend.py +661 -0
  44. nbxtensor/backend/metal_device.py +1274 -0
  45. nbxtensor/backend/process_memory.py +73 -0
  46. nbxtensor/backend/replay_graph.py +322 -0
  47. nbxtensor/backend/runtime.py +409 -0
  48. nbxtensor/backend/triton_ext_driver.py +735 -0
  49. nbxtensor/dtype/__init__.py +1 -0
  50. nbxtensor/dtype/convert.py +107 -0
  51. nbxtensor/dtype/dtypes.yml +70 -0
  52. nbxtensor/dtype/itemsize.py +166 -0
  53. nbxtensor/dtype/nbx_dtype.py +72 -0
  54. nbxtensor/dtype/operands.py +158 -0
  55. nbxtensor/dtype/outputs.py +64 -0
  56. nbxtensor/dtype/policy.py +155 -0
  57. nbxtensor/dtype/policy.yml +51 -0
  58. nbxtensor/dtype/promotion.py +112 -0
  59. nbxtensor/host.py +55 -0
  60. nbxtensor/io/__init__.py +1 -0
  61. nbxtensor/io/read_rate.py +197 -0
  62. nbxtensor/io/safetensors.py +314 -0
  63. nbxtensor/memory/__init__.py +9 -0
  64. nbxtensor/memory/allocator.py +1988 -0
  65. nbxtensor/memory/arena.py +75 -0
  66. nbxtensor/memory/device_memory.py +150 -0
  67. nbxtensor/memory/device_transfer.py +154 -0
  68. nbxtensor/memory/layout.py +43 -0
  69. nbxtensor/memory/memory_pool.py +191 -0
  70. nbxtensor/memory/metal.py +96 -0
  71. nbxtensor/memory/oom_advice.py +91 -0
  72. nbxtensor/memory/pointer_tables.py +323 -0
  73. nbxtensor/memory/pool.py +507 -0
  74. nbxtensor/memory/shadow.py +43 -0
  75. nbxtensor/memory/slab.py +273 -0
  76. nbxtensor/memory/trace_replay.py +122 -0
  77. nbxtensor/memory/traces/sana-video-2b-720p.16g.2026-11-02.tsv +13610 -0
  78. nbxtensor/memory/vmm.py +118 -0
  79. nbxtensor/ops/__init__.py +1 -0
  80. nbxtensor/ops/_autotune_policy.py +90 -0
  81. nbxtensor/ops/_common.py +32 -0
  82. nbxtensor/ops/_configs.py +1041 -0
  83. nbxtensor/ops/_cost.py +231 -0
  84. nbxtensor/ops/abs.py +17 -0
  85. nbxtensor/ops/adaptive_avg_pool2d.py +161 -0
  86. nbxtensor/ops/add.py +96 -0
  87. nbxtensor/ops/addcdiv.py +29 -0
  88. nbxtensor/ops/addcmul.py +29 -0
  89. nbxtensor/ops/addmv_op.py +64 -0
  90. nbxtensor/ops/addr.py +68 -0
  91. nbxtensor/ops/all_reduce.py +83 -0
  92. nbxtensor/ops/amax.py +40 -0
  93. nbxtensor/ops/any_reduce.py +83 -0
  94. nbxtensor/ops/arange_op.py +27 -0
  95. nbxtensor/ops/argmax.py +107 -0
  96. nbxtensor/ops/argmin.py +93 -0
  97. nbxtensor/ops/atan2.py +71 -0
  98. nbxtensor/ops/attention_wrappers.py +1648 -0
  99. nbxtensor/ops/autotune_bucket.py +137 -0
  100. nbxtensor/ops/autotune_cache.py +430 -0
  101. nbxtensor/ops/autotune_certified.py +1262 -0
  102. nbxtensor/ops/autotune_refusals.py +380 -0
  103. nbxtensor/ops/autotune_settings.py +58 -0
  104. nbxtensor/ops/avg_pool2d.py +73 -0
  105. nbxtensor/ops/baddbmm_op.py +132 -0
  106. nbxtensor/ops/batch_norm.py +163 -0
  107. nbxtensor/ops/binary_strided.py +196 -0
  108. nbxtensor/ops/bitwise_and.py +26 -0
  109. nbxtensor/ops/bitwise_left_shift.py +47 -0
  110. nbxtensor/ops/bitwise_not.py +24 -0
  111. nbxtensor/ops/bitwise_or.py +26 -0
  112. nbxtensor/ops/bitwise_right_shift.py +47 -0
  113. nbxtensor/ops/bucketize.py +37 -0
  114. nbxtensor/ops/cat_op.py +87 -0
  115. nbxtensor/ops/celu.py +20 -0
  116. nbxtensor/ops/clamp.py +24 -0
  117. nbxtensor/ops/clamp_min.py +22 -0
  118. nbxtensor/ops/conv2d.py +265 -0
  119. nbxtensor/ops/conv2d_m8n8k4.py +165 -0
  120. nbxtensor/ops/conv_depthwise2d.py +154 -0
  121. nbxtensor/ops/conv_transpose2d.py +148 -0
  122. nbxtensor/ops/conv_wrappers.py +1482 -0
  123. nbxtensor/ops/copy.py +18 -0
  124. nbxtensor/ops/copy_op.py +39 -0
  125. nbxtensor/ops/cos.py +17 -0
  126. nbxtensor/ops/count_nonzero.py +64 -0
  127. nbxtensor/ops/cross_entropy.py +67 -0
  128. nbxtensor/ops/cummax.py +109 -0
  129. nbxtensor/ops/cummin.py +105 -0
  130. nbxtensor/ops/cumsum.py +244 -0
  131. nbxtensor/ops/decode_attn_vec.py +162 -0
  132. nbxtensor/ops/depthwise_conv2d.py +132 -0
  133. nbxtensor/ops/dequant_gemv.py +190 -0
  134. nbxtensor/ops/dft.py +67 -0
  135. nbxtensor/ops/dispatch.py +1228 -0
  136. nbxtensor/ops/div.py +75 -0
  137. nbxtensor/ops/dot_op.py +67 -0
  138. nbxtensor/ops/dropout.py +23 -0
  139. nbxtensor/ops/dtype_convert.py +42 -0
  140. nbxtensor/ops/elementwise_launch.py +81 -0
  141. nbxtensor/ops/elementwise_wrappers.py +2315 -0
  142. nbxtensor/ops/elu.py +20 -0
  143. nbxtensor/ops/embedding.py +69 -0
  144. nbxtensor/ops/eq.py +32 -0
  145. nbxtensor/ops/erf.py +18 -0
  146. nbxtensor/ops/exp.py +17 -0
  147. nbxtensor/ops/exp2.py +18 -0
  148. nbxtensor/ops/fft_op.py +125 -0
  149. nbxtensor/ops/fill_op.py +20 -0
  150. nbxtensor/ops/flash_attention.py +307 -0
  151. nbxtensor/ops/flash_attention_m8n8k4.py +384 -0
  152. nbxtensor/ops/flash_decode.py +78 -0
  153. nbxtensor/ops/flip_op.py +60 -0
  154. nbxtensor/ops/floor.py +90 -0
  155. nbxtensor/ops/full_op.py +24 -0
  156. nbxtensor/ops/fused_moe.py +417 -0
  157. nbxtensor/ops/fused_moe_m8n8k4.py +99 -0
  158. nbxtensor/ops/fused_upsample_conv.py +413 -0
  159. nbxtensor/ops/gather_op.py +52 -0
  160. nbxtensor/ops/ge.py +32 -0
  161. nbxtensor/ops/gelu.py +31 -0
  162. nbxtensor/ops/gemv_vec.py +80 -0
  163. nbxtensor/ops/glu.py +43 -0
  164. nbxtensor/ops/grid_sampler.py +145 -0
  165. nbxtensor/ops/group_norm_tiled.py +67 -0
  166. nbxtensor/ops/groupnorm.py +123 -0
  167. nbxtensor/ops/gt.py +32 -0
  168. nbxtensor/ops/hardsigmoid.py +18 -0
  169. nbxtensor/ops/hardswish.py +19 -0
  170. nbxtensor/ops/im2col.py +57 -0
  171. nbxtensor/ops/index_add.py +95 -0
  172. nbxtensor/ops/index_put_op.py +92 -0
  173. nbxtensor/ops/index_select.py +121 -0
  174. nbxtensor/ops/index_wrappers.py +770 -0
  175. nbxtensor/ops/isfinite.py +20 -0
  176. nbxtensor/ops/isinf.py +20 -0
  177. nbxtensor/ops/isnan.py +18 -0
  178. nbxtensor/ops/launch_keys.py +1054 -0
  179. nbxtensor/ops/launch_observer.py +54 -0
  180. nbxtensor/ops/layernorm.py +71 -0
  181. nbxtensor/ops/le.py +35 -0
  182. nbxtensor/ops/leaky_relu.py +19 -0
  183. nbxtensor/ops/lerp.py +76 -0
  184. nbxtensor/ops/linalg_wrappers.py +1370 -0
  185. nbxtensor/ops/linspace_op.py +47 -0
  186. nbxtensor/ops/log.py +17 -0
  187. nbxtensor/ops/log_sigmoid.py +23 -0
  188. nbxtensor/ops/logical_and.py +26 -0
  189. nbxtensor/ops/logical_not.py +24 -0
  190. nbxtensor/ops/logical_or.py +26 -0
  191. nbxtensor/ops/logical_xor.py +37 -0
  192. nbxtensor/ops/loss_wrappers.py +282 -0
  193. nbxtensor/ops/lt.py +32 -0
  194. nbxtensor/ops/masked_fill.py +21 -0
  195. nbxtensor/ops/matmul.py +377 -0
  196. nbxtensor/ops/matmul_m8n8k4.py +235 -0
  197. nbxtensor/ops/max_pool2d.py +74 -0
  198. nbxtensor/ops/max_pool3d.py +93 -0
  199. nbxtensor/ops/max_reduce.py +91 -0
  200. nbxtensor/ops/maximum.py +18 -0
  201. nbxtensor/ops/mean.py +40 -0
  202. nbxtensor/ops/min_reduce.py +91 -0
  203. nbxtensor/ops/minimum.py +18 -0
  204. nbxtensor/ops/mish.py +22 -0
  205. nbxtensor/ops/mma_m8n8k4.py +152 -0
  206. nbxtensor/ops/moe_align.py +195 -0
  207. nbxtensor/ops/moe_decode_vec.py +374 -0
  208. nbxtensor/ops/moe_device_residency.py +141 -0
  209. nbxtensor/ops/moe_wrappers.py +345 -0
  210. nbxtensor/ops/mse_loss.py +79 -0
  211. nbxtensor/ops/mul.py +51 -0
  212. nbxtensor/ops/nan_to_num.py +23 -0
  213. nbxtensor/ops/ne.py +32 -0
  214. nbxtensor/ops/neg.py +17 -0
  215. nbxtensor/ops/nllloss.py +124 -0
  216. nbxtensor/ops/nonzero.py +33 -0
  217. nbxtensor/ops/nonzero_op.py +62 -0
  218. nbxtensor/ops/norm_wrappers.py +579 -0
  219. nbxtensor/ops/ones_op.py +20 -0
  220. nbxtensor/ops/oracles/__init__.py +6 -0
  221. nbxtensor/ops/oracles/conv2d_fp64.py +450 -0
  222. nbxtensor/ops/pad_op.py +159 -0
  223. nbxtensor/ops/paged_decode_attn.py +215 -0
  224. nbxtensor/ops/pixel_shuffle.py +144 -0
  225. nbxtensor/ops/pixel_unshuffle.py +72 -0
  226. nbxtensor/ops/pow.py +137 -0
  227. nbxtensor/ops/prod.py +81 -0
  228. nbxtensor/ops/rand_op.py +52 -0
  229. nbxtensor/ops/random_wrappers.py +366 -0
  230. nbxtensor/ops/reciprocal.py +17 -0
  231. nbxtensor/ops/reduction_wrappers.py +845 -0
  232. nbxtensor/ops/relu.py +18 -0
  233. nbxtensor/ops/remainder.py +46 -0
  234. nbxtensor/ops/repeat_interleave.py +48 -0
  235. nbxtensor/ops/repeat_op.py +132 -0
  236. nbxtensor/ops/residual_chain.py +374 -0
  237. nbxtensor/ops/rmsnorm.py +68 -0
  238. nbxtensor/ops/rope.py +142 -0
  239. nbxtensor/ops/rsqrt.py +17 -0
  240. nbxtensor/ops/scaled_softmax.py +96 -0
  241. nbxtensor/ops/scatter_op.py +139 -0
  242. nbxtensor/ops/screen_oracle.py +479 -0
  243. nbxtensor/ops/selu.py +22 -0
  244. nbxtensor/ops/shape_wrappers.py +1032 -0
  245. nbxtensor/ops/sigmoid.py +19 -0
  246. nbxtensor/ops/silu.py +27 -0
  247. nbxtensor/ops/sin.py +17 -0
  248. nbxtensor/ops/softmax.py +169 -0
  249. nbxtensor/ops/softplus.py +27 -0
  250. nbxtensor/ops/sort_op.py +304 -0
  251. nbxtensor/ops/spectral_wrappers.py +481 -0
  252. nbxtensor/ops/sqrt.py +17 -0
  253. nbxtensor/ops/stack_op.py +84 -0
  254. nbxtensor/ops/std.py +113 -0
  255. nbxtensor/ops/strided_copy.py +200 -0
  256. nbxtensor/ops/sub.py +35 -0
  257. nbxtensor/ops/sum.py +40 -0
  258. nbxtensor/ops/tan.py +19 -0
  259. nbxtensor/ops/tanh.py +21 -0
  260. nbxtensor/ops/threshold.py +19 -0
  261. nbxtensor/ops/tiling.yml +91 -0
  262. nbxtensor/ops/tiling_policy.py +43 -0
  263. nbxtensor/ops/tiling_sizes.py +370 -0
  264. nbxtensor/ops/topk.py +267 -0
  265. nbxtensor/ops/trace_op.py +64 -0
  266. nbxtensor/ops/tril.py +64 -0
  267. nbxtensor/ops/triu.py +62 -0
  268. nbxtensor/ops/unfold_op.py +37 -0
  269. nbxtensor/ops/unique_op.py +97 -0
  270. nbxtensor/ops/upsample_bilinear2d.py +106 -0
  271. nbxtensor/ops/upsample_nearest2d.py +52 -0
  272. nbxtensor/ops/upsample_nearest3d.py +61 -0
  273. nbxtensor/ops/var.py +136 -0
  274. nbxtensor/ops/vector_norm.py +216 -0
  275. nbxtensor/ops/weight_norm.py +104 -0
  276. nbxtensor/ops/where.py +39 -0
  277. nbxtensor/ops/wrappers.py +825 -0
  278. nbxtensor/ops/zeros_op.py +20 -0
  279. nbxtensor/quant/__init__.py +1 -0
  280. nbxtensor/quant/quantized_tensor.py +187 -0
  281. nbxtensor/random/__init__.py +1 -0
  282. nbxtensor/random/mt_randn.py +154 -0
  283. nbxtensor/random/rng_pin.py +59 -0
  284. nbxtensor/random/rng_stream.py +118 -0
  285. nbxtensor/random/seeded_draw.py +223 -0
  286. nbxtensor/shape/__init__.py +157 -0
  287. nbxtensor/tensor.py +1867 -0
  288. nbxtensor-0.0.1.dist-info/METADATA +379 -0
  289. nbxtensor-0.0.1.dist-info/RECORD +291 -0
  290. nbxtensor-0.0.1.dist-info/WHEEL +4 -0
  291. 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 = []