上一篇(导读)讲了我们要拆什么、为什么挑
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——这一串不是”为了多而多”,而是每一层换来了一些东西:
| 层 | 换来的能力 |
|---|---|
| MIR | rustc 给的,带 Rust 语义(move、borrow、drop) |
| dialect-mir | 把 Rust 语义降到 pliron op,可以挂 verifier、transform |
| dialect-nvvm | GPU 专属 op(nvvm.read_ptx_sreg_tid_x 等) |
| dialect-llvm | 跟 LLVM IR 同结构的 pliron 表示 |
| LLVM IR | 文本格式,可以喂给外部 llc |
| PTX | NVIDIA 的虚拟 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 子进程的。