Rust in Everything
基于 burn 0.22.1

关了默认 feature,GPU 变慢

主线位置:算子主线的后半段被换掉了。默认情况下,Metal 设备上的 op 先进 Fusion<CubeBackend> 攒着、能合并的合成一个内核(Fusion 装饰器),matmul 第一次遇到一个形状时由 autotune 挑内核(autotune);CPU 上 Flex 用 SIMD 和 rayon 线程池(Flex:CPU 后端)。default-features = false 让这几样一起消失,代码一行没改,走的路变了。

flowchart TB
  bd["burn default"] --> core["burn-core default"]
  core --> ten["burn-tensor default"]
  ten --> dsp["burn-dispatch default"]
  dsp --> cc["burn-cubecl?/default"]
  dsp --> fx["burn-flex?/default"]
  cc --> fa["fusion autotune"]
  cc --> per["cubecl persistence"]
  fx --> sr["simd rayon"]
  off["default-features = false"] -.->|"整条链断开"| bd

本页实验用两个 crate、同一份 src/main.rs:pa-fuse-on 依赖 burn = { version = "=0.22.1", features = ["metal", "flex"] };pa-fuse-off 写成 default-features = false, features = ["std", "metal", "flex"],另有一个本地 feature restore 用来演示修法。全部 --release,平台 M2 Max、macOS 27、rustc 1.99.0。实验时机器上同时有别的编译任务在跑(load average 60–100),所以每种配置交替跑三次,下面只比最短时间。

现象

程序在 Metal 和 Flex 上各跑两件事:2048×2048 f32 上连续 8 个逐元素 op(* 0.5、+ 0.25、sin、abs 各两次),1024×1024 f32 的 matmul。每次调用后 device.sync() 再停表,第一次单独记(含内核编译、autotune),之后 100 次取最短和中位数:

// 8 element-wise ops: *, +, sin, abs, twice.
fn chain8(x: Tensor<2>) -> Tensor<2> {
  let y = (x * 0.5 + 0.25).sin().abs();
  (y * 0.5 + 0.25).sin().abs()
}

fn time(device: &Device, runs: usize, mut f: impl FnMut() -> Tensor<2>) -> (Duration, (Duration, Duration)) {
  let mut all = Vec::new();
  for _ in 0..runs {
    let t = Instant::now();
    let out = f();
    device.sync().unwrap();
    all.push(t.elapsed());
    drop(out);
  }
  let first = all.remove(0);
  (first, stats(all))
}

fn run(device: Device) {
  println!("name = {}", Dispatch::name(device.as_dispatch()));
  // x: 2048x2048, a, b: 1024x1024, all random, then device.sync()
  let (first, (min, med)) = time(&device, 101, || chain8(x.clone()));
  println!("chain8 2048x2048 f32: first {first:?}, next 100 min {min:?} median {med:?}");
  let (first, (min, med)) = time(&device, 101, || a.clone().matmul(b.clone()));
  println!("matmul 1024x1024 f32: first {first:?}, next 100 min {min:?} median {med:?}");
}

第一轮交替运行(pa-fuse-on 的 autotune 缓存已经是热的,见下文):

## on run 1 (69.35 98.78 95.72)
name = dispatch<fusion<cubecl<wgpu<msl>>>>
chain8 2048x2048 f32: first 34.292041ms, next 100 min 457.041µs median 634.875µs
matmul 1024x1024 f32: first 125.852542ms, next 100 min 1.865792ms median 2.689167ms
name = dispatch<flex>
chain8 2048x2048 f32: first 26.494333ms, next 100 min 14.903542ms median 25.282917ms
matmul 1024x1024 f32: first 17.001667ms, next 100 min 5.406833ms median 10.468834ms
## off run 1 (65.64 97.52 95.29)
name = dispatch<cubecl<wgpu<msl>>>
chain8 2048x2048 f32: first 124.452333ms, next 100 min 1.174792ms median 1.329625ms
matmul 1024x1024 f32: first 48.644083ms, next 100 min 932.958µs median 1.266791ms
name = dispatch<flex>
chain8 2048x2048 f32: first 16.174084ms, next 100 min 14.790833ms median 17.158292ms
matmul 1024x1024 f32: first 45.668375ms, next 100 min 21.404458ms median 22.328916ms

后端名里少了 fusion<..> 一层。三轮的最短时间:

默认 featuredefault-features = false
Metal,8 个逐元素 op362–521 µs1.17–1.28 ms
Metal,matmul 1024²1.28–1.87 ms0.93–1.90 ms
Flex,8 个逐元素 op14.9–15.3 ms14.8–15.1 ms
Flex,matmul 1024²5.37–6.42 ms21.4–22.3 ms
  • Metal 上的逐元素链慢了约 2.5–3 倍:没有 fusion,8 个 op 是 8 个内核,每个都读写一遍 16 MiB 的中间结果。
  • Flex 上的 matmul 慢了约 4 倍:没有 rayon,Flex 的 matmul 退回单线程(见下一节的 get_parallelism)。
  • 这台机器上、这个形状下,matmul 关掉 autotune 后并不更慢;逐元素链在 Flex 上也没有差别。变慢的是哪一部分,取决于你的模型里是什么。

完整输出在 pa-fuse-off/out/alternating-100.txt。

出处

burn 自己的 default 里没有 fusion、autotune、rayon、simd(crates/burn/Cargo.toml),它们是沿默认 feature 链一路打开的:burn/default → burn-core/default → burn-tensor/default → burn-dispatch/default,最后一环写着:

default = [
    "std",
    "burn-autodiff?/default",
    "burn-cubecl?/default",
    "burn-flex?/default",
    "burn-ndarray?/default",
    "burn-tch?/default",
]

? 的意思是「如果这个可选依赖已经被别的 feature 拉进来,就打开它的 default」。burn-cubecl 的 default 是 autotune、std、fusion 等,burn-flex 的 default 是 ["std", "simd", "rayon"]。依赖写成 default-features = false,burn/default 不再打开,整条链就断在第一环。cargo tree -e features -i 看两个 crate 最终打开的 feature(去重后):

pa-fuse-onpa-fuse-off
burn-cubeclautotune、burn-cubecl-fusion、burn-fusion、default、fusion、metal、std、wgpumetal、std、wgpu
burn-flexdefault、rayon、simd、stdstd

关掉 autotune 后 matmul 也不是没有策略:MatmulStrategy 的 Default 在没有 autotune 时返回 MatmulStrategy::Cube,交给 cubek 的 Strategy::default(),也就是 Strategy::Auto:按 cubek 的规则先挑能用硬件加速的内核,不行再退回不需要加速器的那个,不测量。上表 matmul 的结果说明这个固定选择在 1024² 上已经不差;别的形状、别的硬件上结果可能不同,本页没有测。

CPU 这一侧的开关在 Flex 的 get_parallelism 里:

fn get_parallelism(m: usize, n: usize, k: usize) -> gemm::Parallelism {
    let ops = m.saturating_mul(n).saturating_mul(k);
    if ops >= PARALLEL_THRESHOLD {
        #[cfg(feature = "rayon")]
        {
            gemm::Parallelism::Rayon(0) // 0 = use all available threads
        }
        #[cfg(not(feature = "rayon"))]
        {
            gemm::Parallelism::None
        }
    } else {
        gemm::Parallelism::None
    }
}

没有 rayon 时,再大的矩阵也交给 gemm 单线程算,不报错也不警告。只开 flex 的 CPU 项目(default-features = false, features = ["std", "flex"])同样中招,和 GPU 无关。

default-features = false 通常是为了别的目的写的:去掉 std(no_std 只有 Flex 支持)、或者不要 optim / autodiff。burn 的 fusion、autotune 两个 feature 的用途就是在这种时候把它们补回来,见怎么读这一卷 的 feature 表和那里的「没有蠢问题」。

修法

先把上表里少掉的四个写回来:

[dependencies]
burn = { version = "=0.22.1", default-features = false, features = [
  "std", "metal", "flex",
  "fusion", "autotune", # cubecl 后端
  "rayon", "simd",      # Flex
] }

pa-fuse-off --features restore 就是这四个 feature(burn/fusion、burn/autotune、burn/rayon、burn/simd):

## restore run 1 (44.24 70.73 84.06)
name = dispatch<fusion<cubecl<wgpu<msl>>>>
chain8 2048x2048 f32: first 46.502166ms, next 100 min 573.917µs median 1.991292ms
matmul 1024x1024 f32: first 3.732041958s, next 100 min 826.792µs median 2.507917ms
name = dispatch<flex>
chain8 2048x2048 f32: first 67.045792ms, next 100 min 15.144791ms median 22.415584ms
matmul 1024x1024 f32: first 10.263292ms, next 100 min 5.767792ms median 13.5225ms
## restore run 2 (44.34 69.88 83.60)
name = dispatch<fusion<cubecl<wgpu<msl>>>>
chain8 2048x2048 f32: first 31.084ms, next 100 min 485.667µs median 719.375µs
matmul 1024x1024 f32: first 3.372000666s, next 100 min 976.625µs median 1.141791ms
...

fusion 回来了,逐元素链和 Flex 的 matmul 也回到了默认配置的水平。但 matmul 的第一次每个进程都要 3.3–3.7 s:autotune 在跑,结果却没有存下来,crate 目录下也没有生成 target/environment/。原因是同一条默认链还打开了 cubecl 的磁盘缓存:cubecl-runtime 的 default 里有 persistence,它跟着 burn-cubecl → cubecl 的 default 一起被关掉了。cargo tree -e features -i cubecl-runtime 在 restore 下只剩 channel-mutex、std、toml。burn 的 persistence feature 就是为此准备的,再加上它(本地 feature restore-all):

## restore-all run 1 (72.00 66.88 78.66)
name = dispatch<fusion<cubecl<wgpu<msl>>>>
chain8 2048x2048 f32: first 22.3935ms, next 100 min 292.333µs median 370µs
matmul 1024x1024 f32: first 3.606430083s, next 100 min 803µs median 843.042µs
name = dispatch<flex>
chain8 2048x2048 f32: first 17.897291ms, next 100 min 15.094917ms median 15.654834ms
matmul 1024x1024 f32: first 5.102459ms, next 100 min 3.445542ms median 3.725958ms
## restore-all run 2 (66.31 65.79 78.21)
name = dispatch<fusion<cubecl<wgpu<msl>>>>
chain8 2048x2048 f32: first 12.571917ms, next 100 min 276.584µs median 376.375µs
matmul 1024x1024 f32: first 106.945667ms, next 100 min 803.959µs median 824.208µs
name = dispatch<flex>
chain8 2048x2048 f32: first 16.324375ms, next 100 min 15.082333ms median 15.714042ms
matmul 1024x1024 f32: first 5.784292ms, next 100 min 3.437584ms median 3.69825ms

第一个进程调优、写下 target/environment/default.db,第二个进程的 matmul 第一次只剩 107 ms(编译内核)。缓存的位置和冷热的差别见 autotune。所以完整的修法是五个 feature:

burn = { version = "=0.22.1", default-features = false, features = [
  "std", "metal", "flex",
  "fusion", "autotune", "persistence", # cubecl 后端
  "rayon", "simd",                     # Flex
] }

反过来看,default-features = false 时 matmul 第一次只要几十毫秒(上面 off 那组是 48 ms):没有 autotune,也就没有冷启动的几秒。只在乎启动时间、形状又多变的程序,关掉 autotune 可以是有意的选择;只是要知道自己关掉了什么。

怎么确认没中招

  • 打印一次 Dispatch::name(device.as_dispatch())(需要直接依赖 burn-backend 和 burn-dispatch,见一次 matmul):GPU 设备上应该是 dispatch<fusion<cubecl<...>>>。
  • cargo tree -e features -i burn-cubecl 里要有 fusion 和 autotune;cargo tree -e features -i cubecl-runtime 里要有 persistence;cargo tree -e features -i burn-flex 里要有 rayon 和 simd。
  • 第二次运行同一个程序时,GPU 上第一个 matmul 仍要几秒:autotune 结果没有落盘,查 persistence,以及当前目录往上最外层 Cargo.toml 旁边有没有 target/environment/。
  • 关掉默认 feature 之后,先用同一个基准对比一次再提交。

小结

  • fusion、autotune、autotune 的磁盘缓存 persistence(cubecl 后端)和 rayon、simd(Flex)不在 burn 的 default 列表里,是经 burn-dispatch 的 ?/default 一路打开的。
  • default-features = false 让它们一起消失:后端名里少了 fusion,Metal 上 8 个逐元素 op 慢约 3 倍,Flex 的 matmul 退回单线程、1024² 慢约 4 倍(本机实测)。
  • 只补回 fusion、autotune 还不够:没有 persistence,每个进程都要重新调优(本机 1024² matmul 第一次 3.3–3.7 s)。
  • 修法:在 features 里显式写回 fusion、autotune、persistence、rayon、simd。

💬 关联讨论

发起讨论
加载中...