关了默认 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<..> 一层。三轮的最短时间:
| 默认 feature | default-features = false | |
|---|---|---|
| Metal,8 个逐元素 op | 362–521 µs | 1.17–1.28 ms |
| Metal,matmul 1024² | 1.28–1.87 ms | 0.93–1.90 ms |
| Flex,8 个逐元素 op | 14.9–15.3 ms | 14.8–15.1 ms |
| Flex,matmul 1024² | 5.37–6.42 ms | 21.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-on | pa-fuse-off | |
|---|---|---|
burn-cubecl | autotune、burn-cubecl-fusion、burn-fusion、default、fusion、metal、std、wgpu | metal、std、wgpu |
burn-flex | default、rayon、simd、std | std |
关掉 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。