|
| 1 | +# NVPTX backend vs source-codegen+NVRTC — A/B for all our kernels |
| 2 | + |
| 3 | +**Date:** 2026-06-07 · **HW:** NVIDIA GB10 (sm_121, aarch64-linux) · **TVM:** db5fe57 · tilelang db5fe57c |
| 4 | +**Mutex:** acquired/released cleanly on gb10 (`nvptxab`). GPU-adjacent work ran gb10-only; no local Metal dispatch. |
| 5 | +**Provenance commit:** `6f014dfb` (probes) — this doc + analysis. |
| 6 | + |
| 7 | +## TL;DR / NET VERDICT — NO-GO for NVPTX as a default on any path |
| 8 | + |
| 9 | +The TVM LLVM NVPTX backend (`src/target/cuda/llvm/codegen_nvptx.cc`, `target.build.nvptx`) |
| 10 | +is **feature-incomplete for every tensor-core kernel we ship.** It compiles only |
| 11 | +plain elementwise TIR. The instant a kernel uses `T.gemm` / pipelined shared-mem |
| 12 | +copies — i.e. ALL of our real GEMM/SSD/fp8 prims — NVPTX cannot lower the TileLang |
| 13 | +intrinsics and either throws `unknown intrinsic` or hard-`SIGABRT`s in LLVM. |
| 14 | + |
| 15 | +**Source-codegen (`codegen_cuda.cc` -> NVRTC -> ptxas) remains the only working path |
| 16 | +for 4 of the 5 kernels.** There is no EXEC-ms A/B to report for the tensor-core |
| 17 | +kernels because NVPTX never produces a runnable module for them. That feature gap |
| 18 | +is the finding — it is reported honestly, not hidden. |
| 19 | + |
| 20 | +## HOW to switch the backend (surveyed) |
| 21 | + |
| 22 | +- Default path: `tilelang.jit(...)` -> lowers TIR -> `target.build.tilelang_cuda` |
| 23 | + -> `codegen_cuda.cc` emits **CUDA C++ text** -> NVRTC -> PTX -> cubin. |
| 24 | +- Backend kinds live behind FFI globals: `tvm.ffi.get_global_func("target.build.tilelang_cuda")` |
| 25 | + (default text codegen) vs `tvm.ffi.get_global_func("target.build.nvptx")` (LLVM NVPTX, |
| 26 | + `codegen_nvptx.cc`). To force NVPTX you build a `Target({"kind":"nvptx","mcpu":"sm_121"}, |
| 27 | + host=Target("llvm"))` and feed the **same lowered device module** to `target.build.nvptx`. |
| 28 | +- `tilelang/utils/target.py` `SUPPORTED_TARGETS` does NOT list `nvptx` — there is no |
| 29 | + first-class tilelang flag to select it; you go through the raw TVM FFI build func |
| 30 | + (what the probes do). This alone means NVPTX is off the supported-frontend path today. |
| 31 | + |
| 32 | +## codegen_nvptx.cc intrinsic coverage (read at db5fe57) |
| 33 | + |
| 34 | +`CodeGenNVPTX::CreateIntrinsic` (lines 260-293) handles ONLY: |
| 35 | +`tvm_warp_shuffle{,_up,_down}` (32-bit), `tvm_warp_activemask`, `atomic_add` (32-bit), |
| 36 | +plus thread/block-index sreg reads. **Everything else falls through to |
| 37 | +`CodeGenLLVM::CreateIntrinsic`**, which `TVM_FFI_THROW`s on any unknown op |
| 38 | +(codegen_llvm.cc:1483). |
| 39 | + |
| 40 | +There is **NO** handler for any of the intrinsics our GEMM kernels emit, measured |
| 41 | +in the device TIR of a plain fp16 matmul: |
| 42 | +`ptx_ldmatrix`, `ptx_mma`, `ptx_arrive_barrier`, `ptx_arrive_barrier_expect_tx`, |
| 43 | +`ptx_fence_barrier_init`, `ptx_init_barrier_thread_count`, and TileLang's own |
| 44 | +`tl.tl_shuffle_elect`. No `tvm_mma_sync`, no `cp.async`, no `ldmatrix`. The |
| 45 | +tensor-core / async-copy / mbarrier surface is entirely absent. |
| 46 | + |
| 47 | +## Per-kernel A/B table |
| 48 | + |
| 49 | +| # | Kernel | source-codegen | NVPTX | Winner | Notes | |
| 50 | +|---|--------|----------------|-------|--------|-------| |
| 51 | +| 5 | plain elementwise add (1M f32, `nvptx_elem_probe.py`) | OK | **OK** | tie (both compile) | NVPTX emits real PTX: `// Generated by LLVM NVPTX Back-End`, `.version 8.8 .target sm_121`, `.visible .func add2_kernel`. Provenance MEASURED on gb10. No tensor cores -> no gap. No EXEC-ms taken (trivial bandwidth kernel, not representative). | |
| 52 | +| 5b | plain fp16 `T.gemm` matmul 1024³ (`nvptx_ab_probe.py`) | OK via `tilelang.jit` (src 5651 chars, `__global__`+`tl::`+`ldmatrix`) | **FAIL** | source-codegen | NVPTX: `InternalError: unknown intrinsic tl.tl_shuffle_elect` in `CodeGenNVPTX::AddFunction` -> codegen_llvm.cc:1483. TIR carries ptx_ldmatrix×2, ptx_mma×2, 4× ptx_*barrier. MEASURED gb10. | |
| 53 | +| 2 | path_c F0 `chunk_precompute_fwd_cuda_prim` (the 14.5× TC GEMM, 24.57->1.13ms) | OK (src backend, prior MEASURED 14.5×) | **FAIL (HARD ABORT)** | source-codegen | NVPTX `SIGABRT` at LLVM `Instructions.cpp:757` `"Calling a function with bad signature!"` — uncatchable. Default src path unaffected; 1.13ms stands on source backend. No NVPTX ms exists. | |
| 54 | +| 1 | ROUTED Triton `_chunk_scan_bwd_dstates` (§P1 shim, 1479ms serial prologue) | OK (src backend, only working path) | **FAIL** | source-codegen | Same T.gemm/tensor-core intrinsic wall as #5b/#2 (identical `tl.tl_shuffle_elect` + ptx_ldmatrix/ptx_mma/ptx_*barrier set). NVPTX never compiles it -> no EXEC-ms either backend. See prologue note below. | |
| 55 | +| 3 | path_c B2 `chunk_scan_combine_bwd_cuda_prim` (~905ms) / §27 gemm | OK (src backend) | **FAIL (by construction)** | source-codegen | T.gemm prim; emits the same unhandled intrinsic set proven by #5b/#2. Not separately run (identical wall, conserve gb10 time). EXTRAPOLATION from the shared root cause, labeled as such. | |
| 56 | +| 4 | path_c fp8 native MMA prim (`fp8_matmul_path_c.py`) | OK (src backend) | **FAIL (by construction)** | source-codegen | fp8 MMA needs `ptx_mma` + ldmatrix + barriers — none in codegen_nvptx. EXTRAPOLATION from shared root cause. | |
| 57 | + |
| 58 | +MEASURED: #5, #5b, #2 (run on gb10 2026-06-07). EXTRAPOLATION (shared root cause, not |
| 59 | +separately executed): #1 exec, #3, #4 — flagged honestly. |
| 60 | + |
| 61 | +## WHY (LLVM sched/regalloc vs NVRTC->ptxas) |
| 62 | + |
| 63 | +The intended question — does LLVM's instruction-scheduler / register-allocator / |
| 64 | +vectorizer beat NVRTC->ptxas — **cannot be answered for any tensor-core kernel**, |
| 65 | +because LLVM NVPTX never gets a valid module to schedule: lowering aborts at the |
| 66 | +intrinsic-translation stage, before any scheduling/regalloc runs. The cause is NOT |
| 67 | +codegen quality; it is a missing intrinsic-lowering surface in `codegen_nvptx.cc` |
| 68 | +(no mma/ldmatrix/cp.async/mbarrier translation). For the one kernel where both |
| 69 | +backends DO compile (plain elementwise), the kernel is pure memory traffic with no |
| 70 | +arithmetic to schedule, so it is not representative and we took no ms. Net: on our |
| 71 | +workload the comparison is moot — source-codegen wins by being the only path that |
| 72 | +emits a tensor-core kernel at all. |
| 73 | + |
| 74 | +## Routed-dstates PROLOGUE — backend-orthogonal (confirmed by construction) |
| 75 | + |
| 76 | +The 1479ms `_chunk_scan_bwd_dstates` cost is dominated by a **serial prologue that |
| 77 | +exists in the TIR before codegen** (an IR-level serial-loop artifact). Two facts make |
| 78 | +the orthogonality airtight: (1) the text-codegen backend choice happens strictly |
| 79 | +AFTER the TIR is fixed, so swapping `tilelang_cuda` for `nvptx` cannot alter the |
| 80 | +serial-loop structure; (2) NVPTX never compiles this kernel anyway (same tensor-core |
| 81 | +intrinsic wall), so it cannot have improved it. The real fix is the frontend |
| 82 | +prologue-opt (GEMMify / auto-fuse the serial reduction), NOT a backend swap. |
| 83 | + |
| 84 | +## Recommendation |
| 85 | + |
| 86 | +**NVPTX default: NO** — for any path. It is not feature-complete for our kernels |
| 87 | +(zero tensor-core/async-copy/mbarrier coverage), it is not even exposed as a |
| 88 | +supported tilelang target, and on the one elementwise case it merely ties. Making it |
| 89 | +default would break every GEMM/SSD/fp8 prim we ship (RULE #1: that is a silent-breakage |
| 90 | +trap, rejected). Keep source-codegen (`codegen_cuda.cc` -> NVRTC) as the default. |
| 91 | + |
| 92 | +If NVPTX is ever wanted, it is a large engineering project: add `CreateIntrinsic` |
| 93 | +lowering for `ptx_mma`/`ptx_ldmatrix`/`cp.async`/the four mbarrier intrinsics/ |
| 94 | +`tl.tl_shuffle_elect`, fix the bad-signature abort, and only THEN can an honest |
| 95 | +sched/regalloc A/B even begin. Out of scope here; flagged for the user to extend. |
| 96 | + |
| 97 | +## Reproduce |
| 98 | + |
| 99 | +``` |
| 100 | +# gb10 only (GPU mutex), tilelang+tvm SOURCE at /home/dave/source/tilelang |
| 101 | +ssh gb10 'cd /home/dave/source/tilelang && source /home/dave/cppmega-venv/bin/activate |
| 102 | + python /tmp/nvptx_elem_probe.py # -> NVPTX OK (real PTX) |
| 103 | + python /tmp/nvptx_ab_probe.py # -> NVPTX FAIL: unknown intrinsic tl.tl_shuffle_elect |
| 104 | + python /tmp/nvptx_ops_probe.py # -> lists the 6 unhandled ptx_* intrinsics; default jit OK' |
| 105 | +# probes tracked at tilelang poc/nvptx_ab/ (commit 6f014dfb) |
| 106 | +``` |
0 commit comments