|
| 1 | +# Sparse-MLA (DeepSeek-V3.2) wired into path_c — e2e CORRECT on gb10 |
| 2 | + |
| 3 | +**Status (2026-06-02): DONE — e2e numerically correct.** path_c's differentiable |
| 4 | +sparse-MLA op dispatches the *real* fused v32 TileLang kernel (fwd **and** bwd) on |
| 5 | +gb10/sm_121a via genuine DLPack **zero-copy** input, and produces numerically |
| 6 | +correct output: full-tensor **fwd+bwd cos = 1.0000** vs the upstream dense |
| 7 | +reference, no inf/nan, **no row exclusion**. This is the **first fused-attention |
| 8 | +op proven e2e-correct in path_c on CUDA.** |
| 9 | + |
| 10 | +## What is wired |
| 11 | + |
| 12 | +- `cppmega_mlx/nn/_tilelang/_sparse_mla_v32_fused.py` — wraps the real fused v32 |
| 13 | + `tilelang/examples/deepseek_v32/sparse_mla_fwd.py` + `sparse_mla_bwd.py` over |
| 14 | + MLX bf16 `q/kv/indices`, exposing fwd (`v32_fused_apply`) and bwd |
| 15 | + (`v32_fused_bwd`, exercising the vectorized `T.atomic_addx4` dKV scatter). |
| 16 | +- Wired into the differentiable owner wrapper |
| 17 | + `sparse_mla_fp8_path_c_apply_prepared_float` (fwd + VJP) in |
| 18 | + `cppmega_mlx/nn/_tilelang/sparse_mla_fp8_path_c.py`. |
| 19 | +- `cppmega_mlx/nn/_tilelang/_cuda_zerocopy.py` — native MLX kDLCUDA `__dlpack__` |
| 20 | + export → `torch.from_dlpack`, NO numpy host roundtrip. The torch tensor is a |
| 21 | + real CUDA device view of the MLX allocation. |
| 22 | + |
| 23 | +### Gates (both required for the fused zero-copy path) |
| 24 | + |
| 25 | +``` |
| 26 | +CPPMEGA_SPARSE_MLA_V32_FUSED=1 # dispatch the real fused v32 kernel |
| 27 | +CPPMEGA_TILELANG_CUDA_ZEROCOPY=1 # feed inputs zero-copy via DLPack |
| 28 | +# CPPMEGA_SPARSE_MLA_V32_GB10=1 # optional: force the 99 KiB GB10 re-tile |
| 29 | +``` |
| 30 | + |
| 31 | +Default (gates OFF): callers keep the existing reference path; nothing fused runs. |
| 32 | + |
| 33 | +**RULE #1 (no silent fallback):** when gated ON + `force_path_c`, every fused |
| 34 | +failure RAISES with where+what. There is no silent reference fallback. |
| 35 | + |
| 36 | +## Layout / ABI |
| 37 | + |
| 38 | +- `q` : bf16 `(B, S, H, DQK)`, `DQK = d_v + tail = 512 + 64 = 576`. |
| 39 | +- `kv` : bf16 `(B, SKV, G, DQK)` (kv_group `G`). |
| 40 | +- `indices` : int32 `(B, S, G, topk)` (sentinel `SKV` → masked slot). |
| 41 | +- fwd → `out` bf16 `(B, S, H, d_v)`, `lse` fp32 `(B, S, H)`. |
| 42 | +- bwd → `dq` bf16 `(B, S, H, DQK)`, `dkv` bf16 `(B, SKV, G, DQK)`. |
| 43 | + |
| 44 | +## E2E verification |
| 45 | + |
| 46 | +`scripts/verify_sparse_mla_v32_pathc_e2e.py` — traps BOTH input bridges |
| 47 | +(zero-copy and eager-host) so it can prove which one fed the kernel, runs the |
| 48 | +path_c owner wrapper through fwd + `mx.grad` VJP, and asserts parity + finiteness |
| 49 | +over the **full tensor** (RULE #1: any non-finite element raises with the exact |
| 50 | +offending query rows; any eager-bridge call raises). |
| 51 | + |
| 52 | +Run on gb10: |
| 53 | + |
| 54 | +``` |
| 55 | +cd /home/dave/source/cppmega_mlx |
| 56 | +CPPMEGA_SPARSE_MLA_V32_FUSED=1 CPPMEGA_TILELANG_CUDA_ZEROCOPY=1 \ |
| 57 | + /home/dave/cppmega-venv/bin/python scripts/verify_sparse_mla_v32_pathc_e2e.py |
| 58 | +``` |
| 59 | + |
| 60 | +### Results (gb10 / sm_121a, tilelang `8385a23d`, 2026-06-02, ALL rows) |
| 61 | + |
| 62 | +**PHASE 1 — fused path_c vs upstream dense torch reference** |
| 63 | +`B=1 S=512 SKV=1024 H=128 HKV=1 DQK=576 DV=512 topk=256`: |
| 64 | + |
| 65 | +| tensor | cos | rel_err | |
| 66 | +|--------|-----|---------| |
| 67 | +| fwd | 1.0000 | 2.07e-03 | |
| 68 | +| dq | 1.0000 | 2.61e-03 | |
| 69 | +| dkv | 1.0000 | 2.35e-03 | |
| 70 | + |
| 71 | +zero-copy bridge calls = **15**, eager-host bridge calls = **0**, all elements finite. |
| 72 | + |
| 73 | +**PHASE 2 — fused path_c at model scale** (no dense ref — it would need ~17 TB) |
| 74 | +`B=1 S=4096 SKV=8192 H=128 topk=2048`: |
| 75 | + |
| 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** |
| 79 | +- zero-copy = 15, eager = 0 |
| 80 | + |
| 81 | +The proof that the **real fused kernel ran** (not the reference, not the eager |
| 82 | +host bridge): `zero-copy bridge calls = 15` and `eager = 0` for fwd+bwd inputs, |
| 83 | +plus the TileLang compile logs (`sparse_mla_fwd` / `sparse_mla_bwd_kernel` / |
| 84 | +`preprocess_kernel` / `postprocess_kernel`) — and RULE #1 raise-on-failure means |
| 85 | +a fallback could not have masked anything. |
| 86 | + |
| 87 | +## The fixed `inf` bug (tilelang `8385a23d`) |
| 88 | + |
| 89 | +The earlier blocker — `inf` on a sparse subset of query rows in the GB10 re-tile |
| 90 | +fwd — was a **merge-pass reduce-scratch buffer aliased onto `S_shared`**: the |
| 91 | +online-softmax reduce scratch and the score buffer shared the same shared-memory |
| 92 | +region, corrupting partial-max state for some rows → `inf` in the LSE |
| 93 | +recombination. Fixed Python-only in `examples/deepseek_v32/sparse_mla_fwd.py` |
| 94 | +(no C++ rebuild). Official `test_sparse_mla_fwd`/`test_sparse_mla_bwd |
| 95 | +(check_correctness=True)` now PASS at `S=512/topk=256` and `S=4096/topk=2048`; |
| 96 | +full-tensor fwd cos 0.999998, bwd dq/dkv cos 0.999996, out_inf = out_nan = 0. |
| 97 | + |
| 98 | +## Performance through path_c (honest) |
| 99 | + |
| 100 | +The **bare** fused kernel is the standalone `53.4× faster / 26.1× less memory` |
| 101 | +win (tilelang `docs/SPARSE-MLA-GB10-BENCH.md`, `S=4096/topk=256`: fused fwd |
| 102 | +2.86 ms / 0.259 GB vs dense 152.6 ms / 6.75 GB). |
| 103 | + |
| 104 | +Through the **current path_c wrapper**: |
| 105 | + |
| 106 | +- **Memory win SURVIVES** — measured **12.9× less peak** at `S=4096/topk=256` |
| 107 | + (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. |
0 commit comments