cuda-oxide:hello-constant 拆解 01——鸟瞰整个管线

拆 hello-constant 系列的第一站。一条 cargo 命令背后启动了两个独立进程、七层 IR 翻译。这一篇画出全景图,后续每篇都会停在图上某个具体方框深入。

📚 系列 cuda-oxide · 第 5 篇

上一篇(导读)讲了我们要拆什么、为什么挑 hello_constant。这一篇画出全景图——两个进程、七层 IR、host vs device 双轨,看清楚整张地图之后,接下来每篇就只需停在图上某个具体方框,把它撕开看。

一条命令背后的两个进程

RUST_LOG=info cargo oxide run hello_constant 这条命令一跑,实际启动了两个独立进程:

  • 进程 A:cargo-oxide driver。不真正编译任何东西,只做配置 + 启动子进程。
  • 进程 B:rustc + 我们的 backend .so。所有的编译都在这里完成。

子进程编完之后 cargo 自动 exec 出来的 host binary 又跑起来,第三段是运行时——CUDA driver 接管,把 PTX 装到 GPU 上跑。

下面是全景图:

┌─────────────────────────────────────────────────────────────────┐
│  进程 A: cargo-oxide driver  (编排器,不真正编译任何东西)         │
│                                                                  │
│  1. 检查 / 构建 librustc_codegen_cuda.so                          │
│  2. 算好 RUSTFLAGS="-Z codegen-backend=...so"                     │
│  3. spawn 进程 B                                                  │
│  4. 等 B 退出 → 跑 ./target/release/hello_constant               │
└─────────────────────────────────────────────────────────────────┘
                              │ 启动

┌─────────────────────────────────────────────────────────────────┐
│  进程 B: rustc + 我们的 backend .so                              │
│                                                                  │
│  rustc 前端                                                       │
│   src/main.rs                                                    │
│      │ lexer / parser                                            │
│      ▼                                                            │
│   AST                                                             │
│      │ name resolution / desugar                                  │
│      ▼                                                            │
│   HIR (High-level IR)                                            │
│      │ type checking / borrow check                              │
│      ▼                                                            │
│   THIR → MIR (Middle IR, 控制流图 + SSA-ish)                     │
│      │                                                            │
│      ├──── host 路径 ────► LLVM 默认后端 ──► x86_64 binary       │
│      │                                                            │
│      └──── device 路径 ──► librustc_codegen_cuda.so(我们)        │
│                              │                                    │
│                              ▼                                    │
│                          codegen_crate(tcx)                       │
│                              │                                    │
│                              ▼                                    │
│   ┌──────────────────────────────────────────────────────────┐  │
│   │ rustc-codegen-cuda 内部 7 层                              │  │
│   │                                                            │  │
│   │ 1. 找 #[kernel] 函数 + 传递依赖                            │  │
│   │ 2. 通过 rustc_public 拿 stable MIR                         │  │
│   │ 3. mir-importer:                                           │  │
│   │       MIR  →  dialect-mir + dialect-nvvm  (pliron IR)     │  │
│   │ 4. verify (pliron Operation::verify)                       │  │
│   │ 5. mir-lower:                                              │  │
│   │       dialect-mir/nvvm  →  dialect-llvm                   │  │
│   │ 6. llvm-export:                                            │  │
│   │       dialect-llvm  →  hello_constant.ll (文本 LLVM IR)   │  │
│   │ 7. llc:                                                    │  │
│   │       .ll  →  hello_constant.ptx                          │  │
│   └──────────────────────────────────────────────────────────┘  │
│                              │                                    │
│                              ▼                                    │
│                   ./target/release/hello_constant (host binary)  │
└─────────────────────────────────────────────────────────────────┘
                              │ B 退出

┌─────────────────────────────────────────────────────────────────┐
│  进程 A 启动 host binary                                          │
│                                                                  │
│  hello_constant main()                                            │
│    │                                                              │
│    ├── CudaContext::new(0)         (连 GPU 驱动)                  │
│    ├── load_module_from_file(.ptx) (CUDA driver JIT → SASS)      │
│    ├── DeviceBuffer::zeroed(1)     (cudaMalloc)                   │
│    ├── module.hello_constant(...)  (cuLaunchKernel)               │
│    │       │                                                      │
│    │       ▼                                                      │
│    │   GPU 上 256 个线程并行执行                                  │
│    │       │                                                      │
│    │   ◀───┘ 写完 *out = 42                                       │
│    │                                                              │
│    └── out_dev.to_host_vec()       (cudaMemcpyDeviceToHost)      │
│        println!("Output: {}", ...)                                │
└─────────────────────────────────────────────────────────────────┘

不必现在记住所有细节——把它当导航地图用,接下来每篇会停在图上某个具体方框深入。

五条关键观察

带走五条观察,这一站就完成了。

观察 1:两个进程,三个角色

cargo-oxide调度员,rustc + .so编译器,host binary 才是真正调 GPU 的人。三个角色互不重叠,做的事完全不同。

新手最容易混淆的就是这一点——“我看到 PHASE 1/9 到 PHASE 9/9,以为是一个进程的事”。其实跨了三个程序。

观察 2:rustc 自己干完前端

lexer → parser → HIR → 类型检查 → MIR——这五步跟普通 Rust 编译完全相同。我们的 backend 唯一介入点是 codegen_crate(tcx) 这一个 hook,拿到的输入是已经类型检查 + 借用检查 + 单态化完毕的 MIR

不会 Rust 编译器内部?完全不用担心。我们利用的就是它把脏活全干完之后,留给我们的”干净 MIR”。

观察 3:同一份代码走两条路

hello_constant.rs
        ├── #[kernel] hello_constant   ──→  我们的 backend ──→  PTX (给 GPU)
        └── fn main() / DeviceBuffer / ...  ──→  rustc 自带 LLVM 后端 ──→ x86_64 (给 CPU)

两条路在同一次 rustc 调用里完成,不是两次编译。这就是为什么 cuda-oxide 能让你”在同一个 .rs 文件里既写 host 又写 kernel”。

观察 4:“7 层”的本质是渐进式 IR 下降

MIR → dialect-mir → dialect-nvvm → dialect-llvm → LLVM IR → PTX——这一串不是”为了多而多”,而是每一层换来了一些东西:

换来的能力
MIRrustc 给的,带 Rust 语义(move、borrow、drop)
dialect-mir把 Rust 语义降到 pliron op,可以挂 verifier、transform
dialect-nvvmGPU 专属 op(nvvm.read_ptx_sreg_tid_x 等)
dialect-llvm跟 LLVM IR 同结构的 pliron 表示
LLVM IR文本格式,可以喂给外部 llc
PTXNVIDIA 的虚拟 ISA

每一层都比上一层更接近硬件,表达力更弱、约束更明确。这是 MLIR / pliron 的设计哲学——progressive lowering(渐进式下降)

观察 5:.ptx 不是最终产物

PTX 是虚拟 ISA,类似 Java 字节码。CUDA driver 在 load_module_from_file 时再 JIT 编译成当前 GPU 的真实机器码(SASS)。

hello_constant.ptx  (文本,人类可读)

        ▼ CUDA driver JIT (ptxas 库)
hello_constant SASS  (sm_61 / sm_80 / ... 特定的真实机器码)

        ▼ 在 GPU SM 上执行

你的 hello_constant.ptx 在 sm_61、sm_80、sm_90 上跑出来的实际 SASS 指令都不一样。PTX 是向前兼容的,SASS 不是

一句话总结

一条 cargo-oxide 命令背后启动两个独立进程,host 代码走 rustc 自带 LLVM 后端编成 x86,device 代码走 cuda-oxide 内部 7 层 IR 下降到 PTX。PTX 不是机器码,真正执行前还要 CUDA driver JIT 成 SASS。这张地图记下来,接下来每篇都在它的某个方框里。

下一篇会停在进程 A那个方框,看 cargo-oxide driver 到底做了什么——它是怎么决定调用哪个 rustc、怎么拼 RUSTFLAGS、怎么 spawn 子进程的。

系列上一篇: cuda-oxide:hello-constant 全流程拆解——导读

评论区
评论功能即将上线, 敬请期待。