Skip to content

Commit c680716

Browse files
committed
feat(relax-cuda): real path_c region runs through call_dps_packed on gb10 CUDA VM
make_real_kernel_driver was PROVEN on Metal; this wires + proves it on tvm.cuda(0) (gb10, Grace-Blackwell aarch64, CUDA 13.x). Two e2e proofs: Stage A (scratch/pr3_cuda_real_kernel_driver_gb10.py): the REAL 17-param MR path_c tilelang kernel compiles for target=cuda (127.6s, simplify-gate avoids the 18-23min mamba3-bwd blowup; 149KB CUDA-C, 1 __shared__) and runs through the adapter pack/unpack on tvm.cuda(0): 14,680,063/14,680,064 nonzero (matches the Metal 14.68M proof). Stage B (scratch/pr3_cuda_relax_vm_gb10.py): the SAME region as an R.call_dps_packed leaf in one @R.function, compiled target=cuda, run on relax.VirtualMachine(ex, tvm.cuda(0)) with a CUDA device-tensor input. Output on cuda:0, 14.68M nonzero, max|VM - direct| = 0.0 (byte-exact). The external-function boundary executes the REAL tilelang-CUDA kernel e2e through the Relax graph. CUDA-specific fix to make_region_dps_packed (path_c_dps_adapter.py): the call_dps_packed ABI tensors arrive as CUDA device tensors on the CUDA VM; np.from_dlpack RAISES 'Unsupported device in DLTensor' for them. Added device-agnostic _dps_arg_to_host_numpy (np.from_dlpack for CPU/Metal, .numpy() host-copy for CUDA) and _dps_writeback_host_to_arg (aliased view for CPU/Metal, host->device copyto for CUDA). CPU-VM path (path_c_relax_step_real) regression-tested unchanged. Fail-loud: both paths RAISE if neither readback/writeback route is available. RULE #1 honored.
1 parent a7a5249 commit c680716

3 files changed

Lines changed: 270 additions & 3 deletions

File tree

cppmega_mlx/runtime/path_c_dps_adapter.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,13 @@ def _packed(*args: Any) -> None:
192192
f"{len(leaf.logical_inputs)+1} args "
193193
f"(inputs {len(leaf.logical_inputs)} + 1 output), got {len(args)}"
194194
)
195-
in_arrays = [np.from_dlpack(a) for a in args[:-1]]
196-
out_array = np.from_dlpack(args[-1])
195+
# Import the call_dps_packed ABI tensors to host numpy. On a CUDA Relax VM
196+
# (tvm.cuda(0)) these arrive as device tensors; ``np.from_dlpack`` RAISES
197+
# "Unsupported device in DLTensor" for them, so we route device tensors
198+
# through ``.numpy()`` (an explicit host copy). CPU/Metal tensors still use
199+
# the zero-copy ``np.from_dlpack``. RULE #1: if neither works we RAISE.
200+
in_arrays = [_dps_arg_to_host_numpy(a) for a in args[:-1]]
201+
out_tensor = args[-1]
197202

198203
# Allocate the physical banks we own.
199204
banks = {b: np.zeros((n,), dtype=np.float32) for b, n in bank_shapes.items()}
@@ -215,13 +220,67 @@ def _packed(*args: Any) -> None:
215220
_drive_region_compute(leaf, banks)
216221

217222
# Unpack the logical-output sub-range into the trailing DPS output tensor.
223+
# On CUDA the DPS output is a device tensor that does NOT alias a host numpy
224+
# view, so we write the result back EXPLICITLY (host->device copy) rather
225+
# than via an aliasing ``out_array[...] =``.
218226
m = lmap[leaf.logical_output]
227+
out_host_shape = tuple(int(d) for d in out_tensor.shape)
219228
out_flat = banks[m.bank][m.offset : m.offset + m.size]
220-
out_array[...] = out_flat.reshape(out_array.shape)
229+
_dps_writeback_host_to_arg(out_tensor, out_flat.reshape(out_host_shape))
221230

222231
return _packed
223232

224233

234+
def _dps_arg_to_host_numpy(arg: Any) -> np.ndarray:
235+
"""Import a call_dps_packed ABI tensor to a host numpy array, device-agnostically.
236+
237+
CPU/Metal DLPack-capable tensors import zero-copy via ``np.from_dlpack``; CUDA
238+
tensors (whose device DLPack export is rejected by numpy with "Unsupported
239+
device in DLTensor") are copied to host via the tensor's ``.numpy()`` method.
240+
RULE #1 (fail loud): if BOTH paths fail the original errors are surfaced."""
241+
242+
try:
243+
return np.from_dlpack(arg)
244+
except Exception as dlpack_err: # noqa: BLE001 -- CUDA: Unsupported device in DLTensor
245+
to_numpy = getattr(arg, "numpy", None)
246+
if to_numpy is None:
247+
raise RuntimeError(
248+
"FAIL-LOUD: DPS arg is neither host-DLPack-importable nor has a "
249+
f".numpy() host-copy method (type={type(arg).__name__}); "
250+
f"np.from_dlpack raised: {dlpack_err}"
251+
) from dlpack_err
252+
return np.ascontiguousarray(to_numpy())
253+
254+
255+
def _dps_writeback_host_to_arg(out_tensor: Any, host_result: np.ndarray) -> None:
256+
"""Write a host numpy result back into a call_dps_packed output tensor,
257+
device-agnostically.
258+
259+
For a CPU/Metal tensor that aliases a host numpy view we assign through that
260+
view; for a CUDA device tensor (which does NOT alias host memory) we build a
261+
same-device source tensor and ``copyto`` the device output in place. RULE #1:
262+
if neither path is available we RAISE."""
263+
264+
host_result = np.ascontiguousarray(host_result, dtype=np.float32)
265+
# CPU/Metal: alias the output's host buffer and assign in place.
266+
try:
267+
view = np.from_dlpack(out_tensor)
268+
view[...] = host_result.reshape(view.shape)
269+
return
270+
except Exception: # noqa: BLE001 -- CUDA: device DLPack rejected by numpy
271+
pass
272+
# CUDA (and any device tensor with a .device + copyto): host->device copy.
273+
dev = getattr(out_tensor, "device", None)
274+
copyto = getattr(out_tensor, "copyto", None)
275+
if dev is None or copyto is None:
276+
raise RuntimeError(
277+
"FAIL-LOUD: DPS output tensor is not host-DLPack-aliasable and lacks "
278+
f"a (.device, .copyto) device-writeback path (type={type(out_tensor).__name__})"
279+
)
280+
src = tvm.runtime.tensor(host_result, device=dev)
281+
src.copyto(out_tensor)
282+
283+
225284
# Hook the region compute. Default = a transparent reference matching the region's
226285
# logical semantics (so the adapter ABI is testable on CPU without a live GPU); the
227286
# real path is set by ``set_region_kernel_driver`` to call ``leaf.kernel`` on device.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Stage A: REAL path_c MR tilelang JITKernel through the call_dps_packed adapter
2+
pack/unpack on tvm.cuda(0) (gb10). Direct mirror of scratch/pr3_real_kernel_driver.py
3+
(proven on Metal: 14.68M nonzero) but on CUDA. Proves the on-device real-kernel path
4+
of the adapter works on gb10 CUDA."""
5+
from __future__ import annotations
6+
import sys, time
7+
import numpy as np
8+
import tvm, tvm_ffi
9+
10+
from cppmega_mlx.runtime.path_c_dps_adapter import (
11+
parse_logical_to_physical, parse_physical_bank_shapes, prim_bank_param_order,
12+
PathCRegionLeaf, set_region_kernel_driver, make_region_dps_packed,
13+
make_real_kernel_driver,
14+
)
15+
from cppmega_mlx.runtime.path_c_fusion import (
16+
build_path_c_aot_autograd_region, build_path_c_model_region_from_route_symbols,
17+
_compile_tilelang_prim_func, _tilelang_compile_pass_configs_for_prim_func,
18+
_path_c_default_target,
19+
)
20+
from cppmega_mlx.runtime.path_c_fusion_schedules import path_c_fusion_schedule_template
21+
from cppmega_mlx.recipes.model_factory import local_gb10_quarter_profile
22+
23+
24+
def build_real_kernel():
25+
cfg = local_gb10_quarter_profile().hybrid_config()
26+
region = build_path_c_model_region_from_route_symbols(
27+
region_name="mr_path_c", route_symbols=("M", "R"), model_config=cfg)
28+
prim = path_c_fusion_schedule_template(build_path_c_aot_autograd_region(region))
29+
target = _path_c_default_target()
30+
assert target == "cuda", f"expected cuda target on gb10, got {target}"
31+
t0 = time.time()
32+
kernel = _compile_tilelang_prim_func(
33+
prim, target=target, execution_backend="tvm_ffi",
34+
pass_configs=_tilelang_compile_pass_configs_for_prim_func(prim) or None)
35+
src = kernel.get_kernel_source()
36+
print(f" tilelang.compile(target=cuda) OK in {time.time()-t0:.2f}s -> JITKernel, "
37+
f"{len(kernel.params)} params, {len(src)} bytes CUDA-C")
38+
# report smem usage hint
39+
nshared = src.count("__shared__")
40+
print(f" kernel src has {nshared} __shared__ decls")
41+
return prim, kernel
42+
43+
44+
def main() -> int:
45+
print("Stage A -- REAL path_c MR JITKernel through call_dps_packed adapter on tvm.cuda(0)")
46+
dev = tvm.cuda(0)
47+
print("TVM:", tvm.__version__, "cuda.exist=", dev.exist)
48+
if not dev.exist:
49+
raise RuntimeError("FAIL: tvm.cuda(0) not present")
50+
prim, kernel = build_real_kernel()
51+
lmap = parse_logical_to_physical(prim)
52+
bank_shapes = parse_physical_bank_shapes(prim)
53+
order = prim_bank_param_order(prim)
54+
print(f" ABI: {len(prim.params)} params, {len(lmap)} logical tensors, "
55+
f"{len(bank_shapes)} banks; bank order[:5]={order[:5]}")
56+
57+
leaf = PathCRegionLeaf(
58+
name="mr_path_c_fwd", run_backward=0, prim=prim, kernel=kernel,
59+
logical_map=lmap, bank_shapes=bank_shapes, bank_param_order=order,
60+
logical_inputs=("route_0_M_hidden",), logical_output="route_0_M_hidden_after")
61+
62+
driver = make_real_kernel_driver(leaf, dev) # <-- on tvm.cuda(0)
63+
set_region_kernel_driver(driver)
64+
packed = make_region_dps_packed(leaf)
65+
66+
m_in = lmap["route_0_M_hidden"]; m_out = lmap["route_0_M_hidden_after"]
67+
x = (np.random.rand(*m_in.logical_shape).astype("float32") - 0.5) * 0.01
68+
out = np.zeros(m_out.logical_shape, np.float32)
69+
print(f" driving region fwd: in {m_in.logical_shape} -> out {m_out.logical_shape} "
70+
f"via REAL CUDA kernel...")
71+
t0 = time.time()
72+
packed(tvm_ffi.from_dlpack(x), tvm_ffi.from_dlpack(out))
73+
dt = time.time()-t0
74+
nz = int(np.count_nonzero(out))
75+
print(f" REAL kernel executed on CUDA in {dt:.2f}s; out mean={out.mean():.3e} "
76+
f"std={out.std():.3e} nonzero={nz}/{out.size}")
77+
if nz == 0:
78+
raise RuntimeError("FAIL-LOUD: real CUDA kernel produced ALL-ZERO output")
79+
print(f"STAGE-A PASS: REAL path_c MR JITKernel runs on gb10 CUDA THROUGH the "
80+
f"call_dps_packed adapter pack/unpack ({nz} nonzero outputs).")
81+
return 0
82+
83+
84+
if __name__ == "__main__":
85+
sys.exit(main())

scratch/pr3_cuda_relax_vm_gb10.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Stage B: a REAL path_c MR region as an R.call_dps_packed LEAF inside ONE Relax
2+
@R.function, COMPILED FOR target=cuda and RUN ON relax.VirtualMachine(ex, tvm.cuda(0)).
3+
The packed func runs the REAL tilelang-compiled CUDA JITKernel (via make_real_kernel_driver)
4+
through the adapter pack/unpack. Proves the call_dps_packed external-function boundary
5+
executes the real path_c-CUDA kernel end-to-end ON gb10 THROUGH the Relax graph + CUDA VM.
6+
7+
Reference check: the SAME real kernel run via the direct adapter driver (Stage A path,
8+
CPU-imported input) -- the VM graph output must match it (the graph boundary must execute
9+
the identical real compute, not a stand-in)."""
10+
from __future__ import annotations
11+
import sys, time
12+
import numpy as np
13+
import tvm, tvm_ffi
14+
from tvm import relax
15+
16+
from cppmega_mlx.runtime.path_c_dps_adapter import (
17+
parse_logical_to_physical, parse_physical_bank_shapes, prim_bank_param_order,
18+
PathCRegionLeaf, set_region_kernel_driver, make_region_dps_packed,
19+
register_region_dps_packed, make_real_kernel_driver, emit_region_call,
20+
)
21+
from cppmega_mlx.runtime.path_c_fusion import (
22+
build_path_c_aot_autograd_region, build_path_c_model_region_from_route_symbols,
23+
_compile_tilelang_prim_func, _tilelang_compile_pass_configs_for_prim_func,
24+
_path_c_default_target,
25+
)
26+
from cppmega_mlx.runtime.path_c_fusion_schedules import path_c_fusion_schedule_template
27+
from cppmega_mlx.recipes.model_factory import local_gb10_quarter_profile
28+
29+
30+
def build_real_kernel():
31+
cfg = local_gb10_quarter_profile().hybrid_config()
32+
region = build_path_c_model_region_from_route_symbols(
33+
region_name="mr_path_c", route_symbols=("M", "R"), model_config=cfg)
34+
prim = path_c_fusion_schedule_template(build_path_c_aot_autograd_region(region))
35+
target = _path_c_default_target()
36+
assert target == "cuda", f"expected cuda target on gb10, got {target}"
37+
t0 = time.time()
38+
kernel = _compile_tilelang_prim_func(
39+
prim, target=target, execution_backend="tvm_ffi",
40+
pass_configs=_tilelang_compile_pass_configs_for_prim_func(prim) or None)
41+
print(f" tilelang.compile(target=cuda) OK in {time.time()-t0:.2f}s -> "
42+
f"{len(kernel.params)} params, {len(kernel.get_kernel_source())} bytes CUDA-C")
43+
return prim, kernel
44+
45+
46+
def main() -> int:
47+
print("Stage B -- REAL path_c MR call_dps_packed leaf on the CUDA Relax VM (gb10)")
48+
dev = tvm.cuda(0)
49+
print("TVM:", tvm.__version__, "cuda.exist=", dev.exist)
50+
if not dev.exist:
51+
raise RuntimeError("FAIL: tvm.cuda(0) not present")
52+
prim, kernel = build_real_kernel()
53+
lmap = parse_logical_to_physical(prim)
54+
bank_shapes = parse_physical_bank_shapes(prim)
55+
order = prim_bank_param_order(prim)
56+
57+
leaf = PathCRegionLeaf(
58+
name="mr_path_c_fwd", run_backward=0, prim=prim, kernel=kernel,
59+
logical_map=lmap, bank_shapes=bank_shapes, bank_param_order=order,
60+
logical_inputs=("route_0_M_hidden",), logical_output="route_0_M_hidden_after")
61+
62+
# Install the REAL on-device CUDA driver + register the region as a packed func.
63+
driver = make_real_kernel_driver(leaf, dev)
64+
set_region_kernel_driver(driver)
65+
packed_name = "pathc.mr_fwd.cuda"
66+
register_region_dps_packed(leaf, packed_name)
67+
68+
m_in = lmap["route_0_M_hidden"]; m_out = lmap["route_0_M_hidden_after"]
69+
in_sinfo = relax.TensorStructInfo(tuple(int(d) for d in m_in.logical_shape), "float32")
70+
out_sinfo = relax.TensorStructInfo(tuple(int(d) for d in m_out.logical_shape), "float32")
71+
72+
# Assemble the @R.function: ONE call_dps_packed leaf (the real region).
73+
bb = relax.BlockBuilder()
74+
xv = relax.Var("x", in_sinfo)
75+
with bb.function("train_step", [xv]):
76+
with bb.dataflow():
77+
y = emit_region_call(bb, packed_name, [xv], out_sinfo)
78+
out = bb.emit_output(y)
79+
bb.emit_func_output(out)
80+
mod = bb.get()
81+
if not relax.analysis.well_formed(mod):
82+
raise RuntimeError("FAIL-LOUD: assembled CUDA call_dps_packed step not well-formed")
83+
print(" @R.function well_formed: True; compiling for target=cuda + CUDA VM...")
84+
85+
ex = tvm.compile(mod, target=tvm.target.Target("cuda"))
86+
vm = relax.VirtualMachine(ex, dev)
87+
88+
# Input: a CUDA device tensor fed to the VM (the real graph entry on gb10).
89+
x_np = ((np.random.default_rng(0).random(m_in.logical_shape, np.float32) - 0.5)
90+
* 0.01).astype(np.float32)
91+
x_cuda = tvm.runtime.tensor(x_np, device=dev)
92+
print(f" running CUDA VM train_step: in {m_in.logical_shape} (device={x_cuda.device})...")
93+
t0 = time.time()
94+
res = vm["train_step"](x_cuda)
95+
dev.sync()
96+
out_vm = res.numpy() # CUDA -> host
97+
dt = time.time()-t0
98+
nz = int(np.count_nonzero(out_vm))
99+
print(f" CUDA VM executed in {dt:.2f}s; out device was {res.device}; "
100+
f"mean={out_vm.mean():.3e} std={out_vm.std():.3e} nonzero={nz}/{out_vm.size}")
101+
if nz == 0:
102+
raise RuntimeError("FAIL-LOUD: CUDA VM call_dps_packed produced ALL-ZERO output")
103+
104+
# Reference: SAME real kernel via the direct adapter packed (host-imported input).
105+
set_region_kernel_driver(make_real_kernel_driver(leaf, dev))
106+
direct = make_region_dps_packed(leaf)
107+
ref_out = np.zeros(m_out.logical_shape, np.float32)
108+
direct(tvm_ffi.from_dlpack(x_np), tvm_ffi.from_dlpack(ref_out))
109+
maxdiff = float(np.abs(out_vm - ref_out).max())
110+
print(f" REF (same real CUDA kernel via direct adapter): nonzero="
111+
f"{int(np.count_nonzero(ref_out))}; max|VM - REF| = {maxdiff:.3e}")
112+
if not np.allclose(out_vm, ref_out, rtol=1e-4, atol=1e-6):
113+
raise RuntimeError(f"FAIL-LOUD: CUDA VM graph output disagrees with the same "
114+
f"real kernel run directly; max abs diff={maxdiff}")
115+
print(f"STAGE-B PASS: a REAL path_c MR region runs THROUGH the Relax R.call_dps_packed "
116+
f"graph on the gb10 CUDA VM ({nz} nonzero, byte-matching the direct real-kernel "
117+
f"run, max diff {maxdiff:.1e}). The external-function boundary executes the REAL "
118+
f"tilelang-CUDA kernel end-to-end on gb10.")
119+
return 0
120+
121+
122+
if __name__ == "__main__":
123+
sys.exit(main())

0 commit comments

Comments
 (0)