@@ -73,11 +73,18 @@ zero-copy bridge calls = **15**, eager-host bridge calls = **0**, all elements f
7373** PHASE 2 — fused path_c at model scale** (no dense ref — it would need ~ 17 TB)
7474` B=1 S=4096 SKV=8192 H=128 topk=2048 ` :
7575
76- - fwd: finite=True, latency = 3860 ms, out_abs_mean = 0.0140
77- - fwd+bwd: finite=True, latency = 7765 ms, dq_abs_mean = 0.0051, dkv_abs_mean = 0.0571
78- - peak_mem = ** 7.45 GiB**
76+ - fwd: finite=True, latency = ** 632.7 ms ** (was 3860 ms host-bounce) , out_abs_mean = 0.0140
77+ - fwd+bwd: finite=True, latency = ** 1419.3 ms ** (was 7765 ms host-bounce) , dq_abs_mean = 0.0051, dkv_abs_mean = 0.0571
78+ - peak_mem = ** 5.38 GiB** (was 7.45 GiB)
7979- zero-copy = 15, eager = 0
8080
81+ > Re-measured 2026-06-02 after the MLX-CUDA DLPack-import deadlock fix (below)
82+ > made the ** output** torch->MLX writeback genuine deadlock-free zero-copy: fwd
83+ > ** 3860 -> 632.7 ms (6.1x)** , fwd+bwd ** 7765 -> 1419.3 ms (5.5x)** , also beating
84+ > the prior 2824 ms host-bounce fused-fwd number. The latency win now SURVIVES
85+ > through path_c; the residual is per-call wrapper/JIT-dispatch overhead, not a
86+ > host PCIe bounce.
87+
8188The proof that the ** real fused kernel ran** (not the reference, not the eager
8289host bridge): ` zero-copy bridge calls = 15 ` and ` eager = 0 ` for fwd+bwd inputs,
8390plus the TileLang compile logs (` sparse_mla_fwd ` / ` sparse_mla_bwd_kernel ` /
@@ -105,17 +112,61 @@ Through the **current path_c wrapper**:
105112
106113- ** Memory win SURVIVES** — measured ** 12.9× less peak** at ` S=4096/topk=256 `
107114 (2.07 GiB fused vs 26.8 GiB dense torch reference).
108- - ** Latency win does NOT survive** — per-call wrapper overhead dominates:
109- - the ** output** torch→MLX writeback is a host bounce
110- (` _cuda_eager._torch_cuda_to_mlx ` does ` .cpu().numpy() ` — MLX cannot * import*
111- a CUDA DLPack capsule, ` convert.cpp:155 ` ), copying the ~ 0.5 GB output
112- GPU→host→GPU every call;
113- - ` sparse_mla_fwd_interface ` does a per-call JIT-cache dispatch.
114- - Net measured fused-fwd-through-path_c ≈ 2824 ms vs dense ref 698 ms at that
115- shape — ** overhead-bound, NOT kernel-bound.** The ** input** side is true
116- zero-copy (0 eager / DLPack).
117-
118- ** Correctness is CLOSED.** The remaining work is pure-perf: a CUDA-DLPack
119- * import* into MLX (or an MLX-side output buffer the kernel writes in place) to
120- kill the output host bounce, plus hoisting the kernel handle out of the per-call
121- interface. That is a latency optimization, not a correctness gap.
115+ - ** Latency win now SURVIVES** (re-measured 2026-06-02, after the DLPack-import
116+ deadlock fix below). Both sides of the bridge are now true GPU-resident
117+ zero-copy:
118+ - ** input** side: native MLX kDLCUDA ` __dlpack__ ` -> ` torch.from_dlpack ` (always was zero-copy);
119+ - ** output** side: torch CUDA result -> ` mx.array ` now imports the foreign
120+ CUDA buffer with a single on-device ` cudaMemcpy ` into an MLX-owned buffer
121+ (no ` .cpu().numpy() ` , no GPU->host->GPU PCIe bounce).
122+ - Net fused fwd-through-path_c at S=4096/topk=2048: ** 632.7 ms** (was 3860 ms),
123+ fwd+bwd ** 1419.3 ms** (was 7765 ms) — now wrapper/JIT-dispatch-bound, not
124+ host-bounce-bound. ` sparse_mla_fwd_interface ` still does a per-call
125+ JIT-cache dispatch (the remaining overhead).
126+
127+ ** Correctness is CLOSED and the latency win is RESTORED.** The residual overhead
128+ is the per-call JIT-cache dispatch in ` sparse_mla_fwd_interface ` (hoisting the
129+ kernel handle out of the per-call interface would shave it further) — a latency
130+ optimization, not a correctness gap.
131+
132+ ## MLX-CUDA DLPack-import deadlock (root cause + fix, 2026-06-02)
133+
134+ The output-side zero-copy import (` mx.array(torch_cuda_tensor) ` ->
135+ ` convert.cpp:cuda_dlpack_to_mlx ` ) initially ** DEADLOCKED on the SECOND
136+ consecutive import + ` mx.synchronize ` ** — i.e. on every iteration of a training
137+ loop. EXIT=124 (timeout) on isolation probes.
138+
139+ ** Root cause.** The first import implementation wrapped the foreign CUDA pointer
140+ in an ` mx::array ` whose ** deleter CAPTURED the nanobind ` owner ` ** (the source
141+ torch tensor / DLPack capsule, a ` shared_ptr<nb::ndarray> ` ). Releasing that
142+ owner decrefs a Python object, which needs the ** GIL** . When MLX's ** scheduler
143+ thread** later destroyed that array's ` Data ` (e.g. during a subsequent
144+ ` mx.synchronize ` , which the main thread calls * while holding the GIL* ), the
145+ scheduler thread blocked forever waiting for the GIL -> deadlock. A
146+ ` gil_scoped_release ` around the import's own eval did NOT fix it (the deadlock is
147+ at the LATER user-called ` mx.synchronize ` , not the import's eval) — reverted.
148+
149+ ** Fix (DatasunriseOU/mlx).** A new CUDA backend primitive
150+ ` cu::copy_external_to_mlx_buffer(src, nbytes) ` (` mlx/backend/cuda/allocator.cpp ` ,
151+ declared in ` cuda.h ` , stubbed in ` no_cuda.cpp ` ): (1) ` allocator().malloc(nbytes) `
152+ -> a REAL MLX-managed buffer, (2) a single synchronous `cudaMemcpy(dst, src,
153+ nbytes, cudaMemcpyDefault)` on the ** calling** thread, (3) return the MLX-owned
154+ buffer. ` cuda_dlpack_to_mlx_contiguous ` then builds the ` mx::array ` from that
155+ MLX-owned buffer with the ** default ` allocator::free ` deleter** — so ** NO foreign
156+ buffer and NO Python/DLPack owner ever enters MLX's graph or scheduler** . The
157+ torch capsule is only READ during the copy and is released on the calling thread
158+ (which holds the GIL). ` allocator::free ` runs GIL-free, so the scheduler thread
159+ can never deadlock.
160+
161+ Tradeoff: this is a device-to-device ` cudaMemcpy ` (a single fast on-device copy,
162+ no host bounce), not pointer-aliasing zero-copy — but it is deadlock-free and
163+ correct, and recovers essentially all of the latency vs the prior
164+ ` .cpu().numpy() ` host round-trip.
165+
166+ ** Verified (gb10, /home/dave/cppmega-venv/bin/python):**
167+ - ` /tmp/iso_probe.py ` (single + second import + sync): EXIT=0 (was hang).
168+ - ` /tmp/iso2_probe.py ` (import/eval/sync x3): EXIT=0 (was EXIT=124 at Test 2).
169+ - 100x loop (torch CUDA -> ` mx.array ` -> MLX op -> ` mx.synchronize ` ): EXIT=0,
170+ ** bit-identical every iter** (max_abs_err=0.0), 0.09s total.
171+ - path_c e2e (` verify_sparse_mla_v32_pathc_e2e.py ` ): EXIT=0, zero-copy=15,
172+ eager=0, fwd cos=1.0000, latency restored as above.
0 commit comments