|
| 1 | +# CUDA Architectures and FlashInfer AOT Compilation |
| 2 | + |
| 3 | +## The Problem |
| 4 | + |
| 5 | +Our sglang build has a line that strips `12.0+PTX` from FlashInfer's |
| 6 | +architecture list, with a comment claiming "nvcc in CUDA 12.x doesn't support |
| 7 | +compute_120." Eta0's review pointed out this is wrong — CUDA 12.8+ fully |
| 8 | +supports compute_120 — and suggested we also enable FlashInfer's communication |
| 9 | +module by installing nvshmem. |
| 10 | + |
| 11 | +To understand why this matters, we need to understand how CUDA code targets |
| 12 | +different GPUs, what FlashInfer's AOT build does, and why multi-GPU |
| 13 | +communication kernels are valuable. |
| 14 | + |
| 15 | +## GPU Compute Capabilities |
| 16 | + |
| 17 | +Every NVIDIA GPU has a **compute capability** (CC) — a version number that |
| 18 | +identifies which instructions it supports. Think of it like a CPU's instruction |
| 19 | +set (x86, ARM, etc.) but for GPUs. |
| 20 | + |
| 21 | +| Compute Capability | Architecture | Example GPUs | |
| 22 | +|---|---|---| |
| 23 | +| 8.0 | Ampere | A100 | |
| 24 | +| 8.6 | Ampere | A40, RTX 3090 | |
| 25 | +| 8.9 | Ada Lovelace | L40, RTX 4090 | |
| 26 | +| 9.0 | Hopper | H100, H200 | |
| 27 | +| 10.0 | Blackwell | B200, GB200 | |
| 28 | +| 12.0 | Blackwell | DGX Spark, RTX PRO 6000 | |
| 29 | + |
| 30 | +Notice that 10.0 and 12.0 are both "Blackwell" but are **different |
| 31 | +sub-architectures** with different instruction sets. B200 (the big data center |
| 32 | +GPU) is CC 10.0. DGX Spark and RTX PRO 6000 are CC 12.0. They share the |
| 33 | +Blackwell marketing name but have different silicon and different capabilities. |
| 34 | + |
| 35 | +### The `a` Suffix |
| 36 | + |
| 37 | +Some capabilities have an `a` variant: `9.0a`, `10.0a`, `12.0a`. The `a` means |
| 38 | +"architecture-specific features" — extra instructions that only exist on that |
| 39 | +exact GPU generation and have no equivalent in the portable PTX instruction set. |
| 40 | + |
| 41 | +For example, `sm_100a` enables MXFP8 (e2m1) matrix operations that only |
| 42 | +Blackwell B200 hardware can execute. Code compiled for `sm_100` (without `a`) |
| 43 | +would miss these instructions. For high-performance inference, the `a` suffix |
| 44 | +matters. |
| 45 | + |
| 46 | +## SASS vs PTX: Two Ways to Compile CUDA |
| 47 | + |
| 48 | +When you compile CUDA code, nvcc can produce two kinds of output: |
| 49 | + |
| 50 | +**SASS (Shader Assembly)** — native machine code for a specific GPU. Fast, |
| 51 | +optimal, but only runs on that exact compute capability. |
| 52 | + |
| 53 | +**PTX (Parallel Thread Execution)** — a portable intermediate representation. |
| 54 | +Runs on any GPU with equal or higher compute capability, but must be |
| 55 | +JIT-compiled to SASS at runtime, which is slower. |
| 56 | + |
| 57 | +``` |
| 58 | +Source Code (.cu) |
| 59 | + | |
| 60 | + v |
| 61 | + nvcc compiler |
| 62 | + | |
| 63 | + +---> SASS for sm_90a (native, fast, H100 only) |
| 64 | + +---> SASS for sm_100a (native, fast, B200 only) |
| 65 | + +---> PTX for compute_90 (portable, JIT at runtime) |
| 66 | +``` |
| 67 | + |
| 68 | +### The +PTX Notation |
| 69 | + |
| 70 | +In PyTorch's `TORCH_CUDA_ARCH_LIST`, `12.0+PTX` means "compile native SASS for |
| 71 | +sm_120 AND include PTX for compute_120 as a fallback." This is PyTorch-specific |
| 72 | +syntax. The `+PTX` part tells PyTorch's build system to generate an extra |
| 73 | +`-gencode` flag: |
| 74 | + |
| 75 | +``` |
| 76 | +12.0+PTX --> -gencode arch=compute_120,code=sm_120 |
| 77 | + -gencode arch=compute_120,code=compute_120 (PTX fallback) |
| 78 | +``` |
| 79 | + |
| 80 | +Without `+PTX`, you only get the native SASS: |
| 81 | + |
| 82 | +``` |
| 83 | +12.0 --> -gencode arch=compute_120,code=sm_120 |
| 84 | +``` |
| 85 | + |
| 86 | +This notation is a **PyTorch convention**, not a CUDA standard. Other build |
| 87 | +systems (like FlashInfer's) don't understand it. |
| 88 | + |
| 89 | +## How FlashInfer's AOT Build Works |
| 90 | + |
| 91 | +FlashInfer uses **Ahead-of-Time (AOT) compilation** to pre-compile attention |
| 92 | +kernels for each target GPU architecture. This avoids the startup latency of |
| 93 | +JIT compilation (which can take minutes on first use). |
| 94 | + |
| 95 | +The AOT build is invoked via: |
| 96 | + |
| 97 | +```bash |
| 98 | +FLASHINFER_CUDA_ARCH_LIST="9.0a 10.0a" python3 -m flashinfer.aot |
| 99 | +``` |
| 100 | + |
| 101 | +### FlashInfer's Arch Parser |
| 102 | + |
| 103 | +FlashInfer parses `FLASHINFER_CUDA_ARCH_LIST` with this logic: |
| 104 | + |
| 105 | +```python |
| 106 | +for arch in os.environ["FLASHINFER_CUDA_ARCH_LIST"].split(" "): |
| 107 | + major, minor = arch.split(".") |
| 108 | + major = int(major) |
| 109 | + self.TARGET_CUDA_ARCHS.add((int(major), str(minor))) |
| 110 | +``` |
| 111 | + |
| 112 | +It splits on spaces, then splits each value on `.` to get a `(major, minor)` |
| 113 | +tuple. The minor part is kept as a string to support suffixes like `"0a"`. |
| 114 | + |
| 115 | +Then it generates nvcc flags: |
| 116 | + |
| 117 | +```python |
| 118 | +f"-gencode=arch=compute_{major}{minor},code=sm_{major}{minor}" |
| 119 | +``` |
| 120 | + |
| 121 | +For `"9.0a"`, this produces: |
| 122 | +``` |
| 123 | +-gencode=arch=compute_90a,code=sm_90a |
| 124 | +``` |
| 125 | + |
| 126 | +### Why 12.0+PTX Breaks |
| 127 | + |
| 128 | +If you pass `"12.0+PTX"` to FlashInfer: |
| 129 | + |
| 130 | +1. `arch.split(".")` gives `["12", "0+PTX"]` |
| 131 | +2. `minor = "0+PTX"` |
| 132 | +3. The gencode flag becomes: `-gencode=arch=compute_120+PTX,code=sm_120+PTX` |
| 133 | +4. nvcc doesn't understand `compute_120+PTX` and crashes |
| 134 | + |
| 135 | +This is why our build.bash filters it out. **The filtering itself is correct |
| 136 | +and necessary.** The comment explaining it was wrong — it blamed nvcc for not |
| 137 | +supporting compute_120, when the real issue is FlashInfer's parser not |
| 138 | +understanding the `+PTX` suffix. |
| 139 | + |
| 140 | +### The Right Fix: Convert, Don't Strip |
| 141 | + |
| 142 | +Instead of stripping `12.0+PTX` entirely (losing all sm_120 support), we should |
| 143 | +**convert** it to `12.0a` like the vllm-tensorizer image does: |
| 144 | + |
| 145 | +```bash |
| 146 | +# Current (strips 12.0+PTX, no sm_120 kernels): |
| 147 | +FLASHINFER_ARCH_LIST="$(echo "${TORCH_CUDA_ARCH_LIST}" | sed 's/12\.0+PTX//')" |
| 148 | + |
| 149 | +# Better (converts to 12.0a, native sm_120a kernels): |
| 150 | +FLASHINFER_ARCH_LIST="$(echo "${TORCH_CUDA_ARCH_LIST}" \ |
| 151 | + | sed -E 's@\b(9|10|12)\.0\b@\1\.0a@g; s@\+PTX\b@@g')" |
| 152 | +``` |
| 153 | + |
| 154 | +The vllm-tensorizer approach handles all architectures uniformly: |
| 155 | +1. Convert bare `.0` to `.0a` (adds architecture-specific features) |
| 156 | +2. Strip `+PTX` globally (FlashInfer only generates native SASS, no PTX) |
| 157 | + |
| 158 | +## What sm_120 Kernels We're Missing |
| 159 | + |
| 160 | +Without sm_120a in FlashInfer's arch list, these kernels are not compiled: |
| 161 | + |
| 162 | +| Kernel | Purpose | Why it Matters | |
| 163 | +|---|---|---| |
| 164 | +| `mla_sm120.cu` | Multi-Latent Attention using CGA and TMA | Dedicated attention kernel for CC 12.0 GPUs | |
| 165 | +| `fp4_quantization_sm120` | FP4 quantization | Native FP4 support on Blackwell | |
| 166 | +| `cutlass_fused_moe_sm120` | Fused MoE dispatch | Efficient expert routing | |
| 167 | +| `gemm_sm120_cutlass_fp4` | FP4 GEMM | Low-precision matrix multiply | |
| 168 | +| `xqa_mla` | XQA MLA backend | sm_120-exclusive MLA implementation | |
| 169 | + |
| 170 | +The MLA kernel (`mla_sm120.cu`) uses **CGA (Cooperative Group Arrays)** and |
| 171 | +**TMA (Tensor Memory Accelerator)** — hardware features exclusive to sm_120a |
| 172 | +that have no PTX equivalent. Without native compilation, these kernels simply |
| 173 | +don't exist. There's no PTX fallback — FlashInfer would fall back to JIT |
| 174 | +compilation from source at runtime (minutes of startup) or use a generic |
| 175 | +implementation that doesn't leverage the hardware. |
| 176 | + |
| 177 | +### sgl-kernel Already Has sm_120a |
| 178 | + |
| 179 | +Importantly, **sgl-kernel handles this correctly already.** It ignores |
| 180 | +`TORCH_CUDA_ARCH_LIST` entirely and manages its own gencode flags through CMake |
| 181 | +options. When CUDA >= 12.8, sgl-kernel automatically adds: |
| 182 | + |
| 183 | +``` |
| 184 | +-gencode=arch=compute_120a,code=sm_120a |
| 185 | +``` |
| 186 | + |
| 187 | +So the gap is FlashInfer-specific. |
| 188 | + |
| 189 | +## The Communication Module and nvshmem |
| 190 | + |
| 191 | +### What FlashInfer's Comm Module Provides |
| 192 | + |
| 193 | +FlashInfer includes a communication module (`flashinfer.comm`) that provides |
| 194 | +optimized multi-GPU collective operations: |
| 195 | + |
| 196 | +| Feature | What It Does | |
| 197 | +|---|---| |
| 198 | +| TRT-LLM allreduce fusion | Fused allreduce + residual add + RMSNorm in one kernel | |
| 199 | +| vLLM custom allreduce | CUDA IPC-based allreduce for vLLM | |
| 200 | +| NVSHMEM operations | GPU-initiated allreduce, alltoall, barrier | |
| 201 | +| MoE alltoall | Expert-parallel dispatch/combine for MoE models | |
| 202 | +| Multi-Node NVLink | MNNVL allreduce for multi-node setups | |
| 203 | + |
| 204 | +The key innovation is **operation fusion**. Normally, tensor-parallel inference |
| 205 | +does three separate kernel launches per transformer layer: |
| 206 | + |
| 207 | +``` |
| 208 | +1. allreduce (collect results from all GPUs) |
| 209 | +2. residual add (add skip connection) |
| 210 | +3. RMSNorm (normalize) |
| 211 | +``` |
| 212 | + |
| 213 | +FlashInfer's comm module fuses these into a single kernel: |
| 214 | + |
| 215 | +``` |
| 216 | +1. allreduce + residual + RMSNorm (one kernel, one memory pass) |
| 217 | +``` |
| 218 | + |
| 219 | +This reduces kernel launch overhead and memory bandwidth usage significantly on |
| 220 | +H100+ GPUs. |
| 221 | + |
| 222 | +### sglang Uses This |
| 223 | + |
| 224 | +sglang v0.5.9 imports `flashinfer.comm` in |
| 225 | +`python/sglang/srt/layers/flashinfer_comm_fusion.py`: |
| 226 | + |
| 227 | +```python |
| 228 | +import flashinfer.comm as comm |
| 229 | + |
| 230 | +# Creates CUDA IPC workspace for fused allreduce |
| 231 | +workspace = comm.trtllm_create_ipc_workspace_for_all_reduce_fusion() |
| 232 | + |
| 233 | +# Performs fused allreduce + residual + RMSNorm |
| 234 | +comm.trtllm_allreduce_fusion( |
| 235 | + pattern=AllReduceFusionPattern.kARResidualRMSNorm, |
| 236 | + ... |
| 237 | +) |
| 238 | +``` |
| 239 | + |
| 240 | +This is auto-enabled for DeepSeek, GLM, Qwen, and other MoE models when |
| 241 | +running with tensor parallelism (TP > 1) on SM90+ GPUs. |
| 242 | + |
| 243 | +**Our `--add-comm false` silently disables this optimization.** sglang falls |
| 244 | +back to standard NCCL allreduce (three separate kernels instead of one fused |
| 245 | +kernel). |
| 246 | + |
| 247 | +### Why We Had `--add-comm false` |
| 248 | + |
| 249 | +The comm module requires `nvidia-nvshmem-cu12` as a build dependency. When we |
| 250 | +wrote the build script, the base image didn't have nvshmem and we didn't want |
| 251 | +to add it. So we disabled the entire comm module to avoid a build failure. |
| 252 | + |
| 253 | +The vllm-tensorizer image already installs nvshmem in its builder stage: |
| 254 | + |
| 255 | +```dockerfile |
| 256 | +python3 -m pip install --no-cache-dir \ |
| 257 | + "nvidia-nvshmem-cu${CUDA_VERSION%%.*}" |
| 258 | +``` |
| 259 | + |
| 260 | +This adds ~145MB to the builder stage download but **does not affect the final |
| 261 | +image size** because it's a multi-stage build — the builder stage is discarded |
| 262 | +after wheel compilation. |
| 263 | + |
| 264 | +### What SLIME Needs |
| 265 | + |
| 266 | +SLIME itself doesn't directly call `flashinfer.comm`. It inherits whatever |
| 267 | +sglang provides. SLIME's RL coordination uses `torch.distributed` (NCCL) for |
| 268 | +weight sync and process group management. However, when SLIME drives sglang for |
| 269 | +rollout inference with TP > 1, the fused allreduce would automatically |
| 270 | +activate — so enabling it benefits SLIME's inference performance. |
| 271 | + |
| 272 | +SLIME does use nvshmem separately for **DeepEP** (expert-parallel MoE |
| 273 | +dispatch), but that's a different nvshmem dependency path, not related to |
| 274 | +FlashInfer's comm module. |
| 275 | + |
| 276 | +## Summary of Required Changes |
| 277 | + |
| 278 | +1. **Fix the comment** — CUDA 12.8+ supports compute_120. The issue is |
| 279 | + FlashInfer's parser, not nvcc. |
| 280 | + |
| 281 | +2. **Convert `12.0+PTX` to `12.0a`** instead of stripping it — gives FlashInfer |
| 282 | + native sm_120a Blackwell kernels, matching vllm-tensorizer's approach. |
| 283 | + |
| 284 | +3. **Install `nvidia-nvshmem-cu12`** in the builder stage — build-time only |
| 285 | + dependency, ~145MB, discarded in final image. |
| 286 | + |
| 287 | +4. **Remove `--add-comm false`** — enables the fused allreduce+RMSNorm |
| 288 | + optimization for TP deployments on H100+ GPUs. |
| 289 | + |
| 290 | +## Key Concepts to Remember |
| 291 | + |
| 292 | +- **Compute capability** identifies a GPU's instruction set (8.0 = A100, 10.0 = |
| 293 | + B200, 12.0 = DGX Spark) |
| 294 | +- **SASS** = native GPU machine code, **PTX** = portable intermediate code |
| 295 | +- **`+PTX`** is PyTorch syntax for "include PTX fallback" — not understood by |
| 296 | + all build systems |
| 297 | +- **AOT** = pre-compile kernels ahead of time (fast startup, no JIT latency) |
| 298 | +- **The `a` suffix** enables architecture-exclusive instructions (no PTX |
| 299 | + equivalent) |
| 300 | +- **Fused operations** combine multiple kernels into one, reducing memory |
| 301 | + bandwidth and launch overhead |
| 302 | +- **Multi-stage Docker builds** mean builder dependencies don't bloat the final |
| 303 | + image |
0 commit comments