Skip to content

Commit 11b6588

Browse files
committed
test(zerocopy): add MLX-CUDA -> TileLang(cuda) zero-copy round-trip verifier
scripts/verify_mlx_cuda_zerocopy.py: builds a small MLX-CUDA array, exports it as a kDLCUDA DLPack capsule (no host roundtrip), runs a trivial target='cuda' kernel (out=A*2+1), and asserts numeric parity. A/B-tests device_type 2 (kDLCUDA) vs 13 (kDLCUDAManaged) and reports which tvm-ffi from_dlpack accepts. Run on gb10 with CPPMEGA_TILELANG_CUDA_ZEROCOPY=1 once the gate clears.
1 parent 35bf055 commit 11b6588

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env python
2+
"""Verify MLX-CUDA -> TileLang(target='cuda') zero-copy DLPack round-trip.
3+
4+
Run on a CUDA host (gb10/sm_121) with the CUDA MLX build:
5+
6+
CPPMEGA_TILELANG_CUDA_ZEROCOPY=1 \
7+
/home/dave/cppmega-venv/bin/python scripts/verify_mlx_cuda_zerocopy.py
8+
9+
It builds a small MLX-CUDA array, exports it as a kDLCUDA DLPack capsule via
10+
``cppmega_mlx.nn._tilelang._cuda_zerocopy`` (NO host roundtrip), imports it with
11+
``tvm.runtime.from_dlpack``, runs a trivial elementwise TileLang target='cuda'
12+
kernel (out = inp * 2 + 1), and checks numeric parity against the MLX reference.
13+
14+
RULE #1: any failure RAISES with where+what; there is no copy fallback.
15+
The script also A/B-tests device_type 2 (kDLCUDA) vs 13 (kDLCUDAManaged) to report
16+
which one tvm-ffi from_dlpack accepts.
17+
"""
18+
19+
import os
20+
import sys
21+
22+
import mlx.core as mx
23+
import numpy as np
24+
25+
26+
def _build_double_plus_one_kernel(n: int):
27+
import tilelang
28+
import tilelang.language as T
29+
30+
@T.prim_func
31+
def main(
32+
A: T.Tensor((n,), "float32"), # noqa: N803
33+
B: T.Tensor((n,), "float32"), # noqa: N803
34+
):
35+
with T.Kernel(T.ceildiv(n, 128), threads=128) as bx:
36+
for i in T.serial(128):
37+
idx = bx * 128 + i
38+
if idx < n:
39+
B[idx] = A[idx] * T.float32(2.0) + T.float32(1.0)
40+
41+
return tilelang.compile(main, target="cuda", out_idx=None)
42+
43+
44+
def _run_for_device_type(device_type: int, n: int = 256) -> tuple[bool, str]:
45+
os.environ["CPPMEGA_TILELANG_CUDA_DLPACK_DEVICE_TYPE"] = str(device_type)
46+
from cppmega_mlx.nn._tilelang._cuda_zerocopy import mlx_cuda_array_to_tvm_tensor
47+
48+
src = mx.random.uniform(shape=(n,), dtype=mx.float32)
49+
mx.eval(src)
50+
ref = np.array(src) * 2.0 + 1.0
51+
52+
a_tvm = mlx_cuda_array_to_tvm_tensor(src)
53+
out = mx.zeros((n,), dtype=mx.float32)
54+
mx.eval(out)
55+
b_tvm = mlx_cuda_array_to_tvm_tensor(out)
56+
57+
kernel = _build_double_plus_one_kernel(n)
58+
kernel(a_tvm, b_tvm)
59+
60+
import torch
61+
62+
torch.cuda.synchronize()
63+
mx.eval(out)
64+
got = np.array(out)
65+
max_abs = float(np.max(np.abs(got - ref)))
66+
ok = bool(np.allclose(got, ref, atol=1e-4, rtol=1e-4))
67+
return ok, f"device_type={device_type}: ok={ok} max_abs_err={max_abs:.3e}"
68+
69+
70+
def main() -> int:
71+
if not (getattr(mx, "cu", None) and mx.cu.is_available()):
72+
raise RuntimeError(
73+
"verify_mlx_cuda_zerocopy: MLX CUDA backend is not available on this host."
74+
)
75+
results = []
76+
last_exc = None
77+
for dt in (2, 13):
78+
try:
79+
ok, msg = _run_for_device_type(dt)
80+
results.append((dt, ok, msg))
81+
print("PASS" if ok else "FAIL", msg)
82+
except Exception as exc: # noqa: BLE001 - report which device_type was rejected
83+
last_exc = exc
84+
print(f"REJECTED device_type={dt}: {type(exc).__name__}: {exc}")
85+
accepted = [dt for dt, ok, _ in results if ok]
86+
if not accepted:
87+
raise RuntimeError(
88+
f"verify_mlx_cuda_zerocopy: no CUDA DLPack device_type round-tripped "
89+
f"zero-copy with numeric parity (last error: {last_exc})."
90+
)
91+
print(f"ZERO-COPY VERIFIED: accepted device_type(s)={accepted}")
92+
return 0
93+
94+
95+
if __name__ == "__main__":
96+
sys.exit(main())

0 commit comments

Comments
 (0)