wgpu 与 Metal
主线位置:算子主线最后一段「cubecl 编译 / 提交」在 macOS 上的落地。
CubeBackend和 cubek 把内核交给Client::launch之后(见运行时与 ComputeClient),wgpu 运行时的服务端WgpuServer要做三件事:第一次用到设备时选适配器、决定用哪种着色语言;第一次遇到某个内核时把 cubecl IR 编成 MSL、交给 Metal 建管线;之后每次提交直接用缓存的管线。本页讲这三件事。内核本身怎么写成 IR 见 #[cube] 内核。
flowchart TB dev["Device::metal(kind)"] --> wd["WgpuDevice backend Metal"] wd --> setup["try_create_setup 选适配器"] setup --> feat["register_metal_features"] feat -->|"MSL 3.2 可用"| msl["AutoCompiler::Msl"] feat -->|不可用| wgsl["AutoCompiler::Wgsl"] launch["Client::launch 内核"] --> pipe["WgpuServer::pipeline"] pipe -->|命中| cached["进程内管线表"] pipe -->|未命中| ir["define_kernel 得 IR"] ir --> src["MslCompiler 生成 MSL 源码"] src --> mod["create_shader_module_passthrough"] mod --> mtl["Metal newLibraryWithSource"] mtl --> cpl["create_compute_pipeline"] cpl --> cached
本页源码是 cubecl 0.11.3(cubecl-wgpu、cubecl-server、cubecl-runtime)和 burn 0.22.1。实验 crate 有三个,都在 exp-burn/ 下:rt-tune(burn 只开 metal、tracing)、rt-wgsl(只开 wgpu、tracing)、rt-cube(直接依赖 burn-cubecl)。平台 M2 Max、macOS 27、rustc 1.99.0,计时都是 release 构建。Metal 是实测;Vulkan(SPIR-V)和 WebGPU(WGSL 在浏览器里)只读了源码。
metal feature 走的是哪条路
burn 的 metal 先打开 wgpu,再一路传到 burn-wgpu 的 metal = ["cubecl/metal", ..]。到了 cubecl 这一层:
metal = ["wgpu-msl"]
metal-native = ["cubecl-metal"]
# ...
wgpu-msl = ["wgpu", "cubecl-wgpu/msl"]
metal = ["wgpu-msl"]:还是 wgpu 运行时,只是给它的 msl feature 打开了 MSL 编译器(cubecl-cpp 的 metal 方言)。cubecl 另有一个原生 Metal 运行时 cubecl-metal,feature 名是 metal-native,burn 没有任何 feature 通向它。rt-tune 的依赖树可以对一下:
cubecl-wgpu feature "cubecl-cpp"
cubecl-wgpu feature "default"
cubecl-wgpu feature "msl"
cubecl-wgpu feature "sanitize-filename"
cubecl-wgpu feature "std"
cubecl-wgpu feature "tracing"
cubecl-metal / cubecl-spirv in tree: 0
cubecl-cpp in tree: 1
(cargo tree -e features -i cubecl-wgpu,去掉了重复行和树线。)所以一次 matmul 里打印的后端名是 cubecl<wgpu<msl>>,名字里的 msl 来自 runtime_name:
fn runtime_name(backend: wgpu::Backend, options: &WgpuCompilationOptions) -> &'static str {
match backend {
wgpu::Backend::Vulkan if options.supports_vulkan_compiler => "wgpu<spirv>",
wgpu::Backend::Metal if options.supports_msl_compiler => "wgpu<msl>",
_ => "wgpu<wgsl>",
}
}
它看的是运行期的 supports_msl_compiler,不是编译期的 feature:开了 metal、但设备撑不起 MSL 时,名字会是 wgpu<wgsl>(下一节)。
问: cubecl 自己也有 Device::metal,它和 burn 的 Device::metal 是一回事吗?
答: 不是,名字撞了。cubecl 的 Device 枚举里 Metal(MetalDevice) 是原生运行时 cubecl-metal 的设备,Wgpu(WgpuDevice) 才是 wgpu 的。burn 的 Device::metal(kind) 对应的是 cubecl 的 Device::metal_msl,它的文档注释写着「Not the native Metal runtime, which is [Device::metal] and a different device altogether」。直接写 cubecl 代码时用错了,client() 会 panic 在「belongs to a runtime this build does not link」。
选设备:DeviceKind 和图形 API
burn 的四个 wgpu 构造函数共用 wgpu_device,把 DeviceKind 映射成 cubecl 的 WgpuDeviceKind,再用 .on(backend) 定图形 API(映射本身见 Device)。Device::wgpu 给 WgpuBackend::Auto,Device::metal 给 WgpuBackend::Metal。构造是惰性的:真正去找适配器是第一次用到这个设备时,在 cubecl 的设备服务线程上调 DeviceService::init → try_create_setup。
try_create_setup 先按 backend_candidates 列出要试的图形 API:固定了 API 的只有它自己一个;Auto 交给 AutoGraphicsApi::chain:
cfg_if::cfg_if! {
if #[cfg(target_family = "wasm")] {
alloc::vec![Backend::BrowserWebGpu]
} else if #[cfg(target_os = "macos")] {
alloc::vec![Backend::Metal]
} else {
alloc::vec![Backend::Vulkan, Backend::Dx12, Backend::Gl]
}
}
macOS 上 Auto 只有 Metal 一个候选,所以 Device::wgpu(..) 和 Device::metal(..) 最后用的是同一个 Metal 适配器。挑适配器看 kind(request_adapter):
DeviceKind | 怎么挑 |
|---|---|
DefaultDevice | request_adapter 带 PowerPreference::HighPerformance;环境变量 CUBECL_WGPU_DEFAULT_DEVICE 可改成别的种类(try_device_override,写法如 IntegratedGpu(1)、Cpu) |
DiscreteGpu(i) / IntegratedGpu(i) / VirtualGpu(i) / Cpu | 枚举这个 API 的全部适配器,只留 wgpu 报告的种类相同的,取第 i 个;没有就报 No Discrete GPU device found 之类,后面附上找到的适配器列表 |
Existing(id) | 不找适配器,指向用 Device::wgpu_options().setup(..) 登记过的外部 wgpu 设备 |
M2 Max 上只有一个 Metal 适配器,wgpu 报它是 IntegratedGpu。rt-tune kind <种类> 在 Device::metal(kind) 上做一次加法(release):
$ rt-tune kind integrated
Device<Cube(Wgpu(WgpuDevice { kind: IntegratedGpu(0), backend: Metal }))>
[2.0, 4.0] name=dispatch<fusion<cubecl<wgpu<msl>>>>
$ rt-tune kind discrete
Device<Cube(Wgpu(WgpuDevice { kind: DiscreteGpu(0), backend: Metal }))>
thread 'DSD-0-16384' (3354333) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cubecl-wgpu-0.11.3/src/runtime.rs:51:57:
Unable to acquire wgpu device: AdapterUnavailable { device: WgpuDevice { kind: DiscreteGpu(0), backend: Metal }, message: "No Discrete GPU device found, adapters [AdapterInfo { name: \"Apple M2 Max\", vendor: 0, device: 0, device_type: IntegratedGpu, device_pci_bus_id: \"\", driver: \"\", driver_info: \"\", backend: Metal, subgroup_min_size: 4, subgroup_max_size: 64, transient_saves_memory: Some(true), limit_bucket: None }]" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' (3354329) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/cubecl-common-0.11.3/src/device/handle/channel.rs:409:35:
called `Result::unwrap()` on an `Err` value: RecvError
IntegratedGpu(1) 和 Cpu 也是同样的两段 panic。适配器是在设备服务线程(DSD-<type_id>-<index_id>)上找的,找不到时 DeviceService::init 的 expect("Unable to acquire wgpu device") 让那个线程 panic,有用的信息在这一段;main 线程随后只看到通道断开的 RecvError。在 Apple 芯片上写 DiscreteGpu(0) 就会这样,用 DefaultDevice 或 IntegratedGpu(0);想拿到 Result 而不是 panic,用 Device::wgpu_options().device_kind(..).init()。
两条路落到同一块 GPU,差别在别处。rt-tune devices 依次在 Device::default()、Device::wgpu(DefaultDevice)、Device::metal(DefaultDevice) 上做一次加法(autotune 缓存已有,加法没有调优):
Device::default() Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Auto }))>
name=dispatch<fusion<cubecl<wgpu<msl>>>> first op 195.649042ms -> [2.0, 4.0]
Device::wgpu(DefaultDevice) Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Auto }))>
name=dispatch<fusion<cubecl<wgpu<msl>>>> first op 9.282583ms -> [2.0, 4.0]
Device::metal(DefaultDevice) Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))>
name=dispatch<fusion<cubecl<wgpu<msl>>>> first op 49.621625ms -> [2.0, 4.0]
wgpu == metal: false
- 三个都编成了 MSL(
wgpu<msl>)。 Device::wgpu和Device::default()是同一个设备(开了metal时默认设备就是Auto,见 Device),第二次只用 9 ms。Device::metal又花了 50 ms:它的DeviceId不同(图形 API 编在 id 里),cubecl 为它另起了一套服务线程、内存池和管线表,同一个加法内核要再编译一次。- 还有两处不同。一是 MSL 不可用时,
Auto允许退回 WGSL,固定成 Metal 的不允许,初始化时 panic(下一节)。二是 autotune 的结果按设备分开存,换一种写法就要重新调优,见本页的动脑筋。
MSL 能不能用:先编一个金丝雀
设备拿到之后,try_create_server 调 register_features:先问 Vulkan(只有开了 spirv 才可能是),再问 Metal,都不是就按 WGSL 登记设备能力。Metal 这一步是 register_metal_features:
// Canary for ensuring the current Metal language version is 3.2+.
const LAMBDA_CANARY: &str = r#"
#include <metal_stdlib>
using namespace metal;
kernel void __canary__(device float* out [[buffer(0)]]) {
auto f = [](float x) -> float { return x * 2.0; };
out[0] = f(1.0);
}
"#;
// ...
let has_required_family = raw.supportsFamily(MTLGPUFamily::Metal3)
|| raw.supportsFamily(MTLGPUFamily::Apple7)
|| raw.supportsFamily(MTLGPUFamily::Mac2);
let support = MslSupport::new(has_required_family, || {
autoreleasepool(|_| {
let canary = NSString::from_str(LAMBDA_CANARY);
let options = MTLCompileOptions::new();
options.setLanguageVersion(MTLLanguageVersion::Version3_2);
raw.newLibraryWithSource_options_error(&canary, Some(&options))
.map(|_| ())
.map_err(|err| format!("{err:?}"))
})
});
两道检查:GPU 家族要有 SIMD-scoped 运算(plane 和 CMMA 内核靠它),系统的 Metal 编译器要接受 MSL 3.2(生成的源码里用了 lambda)。都过了才把 supports_msl_compiler 设成 true,再登记类型(f16、bf16、i64 都可算,fp8 / fp4 只能存和转换;原子只登记了 i32、u32 和 f32 的加法与读写,注释说 64 位原子登记过、结果算错,又撤掉了)和 CMMA 尺寸。任一道不过,只打一条 log::warn!,设备按 WGSL 继续。
接着 AutoCompiler::validate_runtime 决定能不能接受这种退回:只有请求的是 WgpuBackend::Metal 时才要求 MslCompiler 可用,否则报 WgpuCompilerInitError::Unavailable(「use AutoCompiler with an automatic device to allow WGSL fallback」)。走惰性初始化时这个错误被 expect("Unable to initialize wgpu runtime") 变成 panic;用 Device::wgpu_options().graphics_api(WgpuBackend::Metal).init() 则拿到 Err。M2 Max 两道都过,本页的实验看不到退回。
PR #1512(「Add version check to ensure required features are supported in MSL」,cubecl 首次进入 v0.11.0-pre.3)之前,register_metal_features 只要拿到 Metal 适配器就调 register_features,没有返回值,也不检查什么:开了 msl 的构建在任何 Metal 设备上都生成 MSL。生成的源码用了 MSL 3.2 才有的 lambda,系统编译器旧一点就编不过。这个 PR 加了 lambda 金丝雀和 Apple7 家族检查,函数改成返回 bool,不支持时退回 WGSL 并 log::warn!(提交说明里的「Actually disable the compiler if unsupported」)。随后的 PR #1540(「fix(metal): correct MSL capability detection」,同样首次进入 v0.11.0-pre.3)把家族条件放宽成上面的 Metal3 || Apple7 || Mac2,并给金丝雀显式指定 Version3_2;现在的注释说明这和 wgpu-hal 判断 SIMD-scoped 能力的条件一致。
编一个内核:从 IR 到管线
每次提交内核,WgpuServer 先拿 pipeline 找管线:
let kernel_id = kernel.id();
let mode = kernel_id.mode;
if let Some(pipeline) = self.pipelines.get(&kernel_id) {
return Ok(pipeline.clone());
}
let mut recording = CompilationRecording::new(&kernel_id);
let cached = self.load_cached_pipeline(&kernel_id, bindings, mode)?;
// ...
let definition = cubecl_core::define_kernel(&*kernel)?;
recording.defined(&definition);
let mut compiler = C::init(self.backend, &self.compilation_options);
let mut compiled = compiler.compile_kernel(self, kernel, definition)?;
// ...
let module = self.create_module(
&compiled.entrypoint_name,
kernel_id.cube_dim.into(),
ModuleSource::resolve(repr, compiler.lang_tag(), &compiled.source)?,
mode,
)?;
let pipeline = self.create_pipeline(&compiled.entrypoint_name, repr, module, bindings)?;
self.pipelines.insert(
kernel_id.clone(),
(pipeline.clone(), compiler_info, io.clone()),
);
- 查表。 键是
KernelId:内核类型加上它的编译期参数(cube_dim、向量宽度、dtype、是否原地写等)。同一个内核换了编译期参数就是另一条,要重新编。 - 展开成 IR。
define_kernel运行#[cube]宏生成的展开代码,得到 cubecl IR(#[cube] 内核)。 - IR 到源码。
AutoCompiler::init按上一节的supports_msl_compiler选Msl,MslCompiler(cubecl-cpp的 Metal 方言)把 IR 写成 MSL 文本。 - 源码到 Metal 库。
create_module对 MSL 走create_shader_module_passthrough,把文本原样交给 wgpu;wgpu-hal 的 Metal 后端(wgpu-hal-30.0.1的src/metal/device.rs)收到ShaderInput::Msl就调newLibraryWithSource_options_error,由系统的 Metal 编译器当场编译。整个过程没有 naga 参与,wgpu 也不校验源码。 - 建管线。
create_pipeline按metal::bindings建绑定布局:所有缓冲都标成读写(同一块显存可能被多个切片共享,分不清谁只读),元数据缓冲是 uniform 或只读,然后create_compute_pipeline。模块和管线的创建都包在validated里:wgpu 的验证错误和内部错误被收成这次启动的CompilationError,不会变成轮询线程上的 panic。
CUBECL_DEBUG_LOG=stdout 会把第 3 步的结果打出来。rt-tune add 在 Metal 上把两个长 4 的张量相加,输出里只有一个内核(截掉了中间的下标计算):
[START_KERNEL_COMPILATION]
name: burn_cubecl::kernel::binary::kernel_binop::KernelBinop<
burn_cubecl::kernel::binary::AddOp,
>
id: KernelId {
type: burn_cubecl::kernel::binary::kernel_binop::KernelBinop<burn_cubecl::kernel::binary::AddOp>,
address_type: U32,
cube_dim: Dim3 {
x: 32,
...
out: ArrayView {
buffer: BufferCompilationArg {
inplace: Some(
0,
),
...
source:
```msl
#include <metal_stdlib>
using namespace metal;
...
struct alignas(16) float_4 { float i_0; float i_1; float i_2; float i_3; };
...
[[max_total_threads_per_threadgroup(64)]] [[kernel]] void kernel_binop_c_f32_n_4_2f077c2a(device float_4 * const v2 [[buffer(0)]], device float_4 const* const v7 [[buffer(1)]], constant info_st const* const v41, ...) {
...
if(v22) {
device float_4 * const v28 = &v2[v86];
float_4 const v29 = *v28;
device float_4 const* const v34 = &v7[v86];
float_4 const v35 = *v34;
float const v51 = v29.i_0;
float const v52 = v35.i_0;
float const v53 = v51 + v52;
...
float_4 const v63 = float_4{v53, v56, v59, v62};
*v28 = v63;
}
return;
}
几处可以和源码对上:KernelId 里的 inplace: Some(0) 是 launch_binop 发现左操作数可以原地写(burn-cubecl 后端),所以 MSL 里只有两个缓冲、结果写回 v2;n_4 和 float_4 是向量宽度 4;入口名末尾的 2f077c2a 是编译期参数的哈希,换参数就换名字。
对照:同一块 GPU 上的 WGSL
只开 wgpu、不开 metal 时,cubecl-wgpu 不带 msl feature,macOS 上 Auto 仍然是 Metal API,但编译器只能是 WgslCompiler。rt-wgsl 用 Device::wgpu(DefaultDevice) 跑同一个加法,日志里的源码是 WGSL:
source:
```wgsl
@group(0) @binding(1) var<storage, read_write> v7: array<vec4<f32>>;
@group(0) @binding(0) var<storage, read_write> v2: array<vec4<f32>>;
struct info_st { scalars_usize: array<u32, 4>, static_meta: array<u32, 2> }
@group(0) @binding(2) var<storage, read> info_var: info_st;
@compute @workgroup_size(32, 1, 1)
fn kernel_binop_c_f32_n_4_91757fd7(@builtin(workgroup_id) v137: vec3<u32>, @builtin(num_workgroups) v139: vec3<u32>, @builtin(local_invocation_index) v146: u32) {
...
这条路多两道工序:create_module 走 create_shader_module_trusted(关掉 wgpu 的越界检查,cubecl 自己负责),wgpu-hal 在建管线时用 naga 把 WGSL 翻成 MSL,再交给同一个 newLibraryWithSource。翻译之外更大的差别在能力:WGSL 登记不了 CMMA,matmul 的 autotune 只能在不用矩阵单元的候选里挑。两边各跑一次空缓存的进程,看 autotune 记下的结果([autotune.decisions] stdout = true,见 autotune)和之后 20 次的最短时间:
| 后端名 | 1024² 胜出的候选(中位数) | 2048² 胜出的候选(中位数) | 2048² 之后 20 次最短 | 测出的计算峰值 |
|---|---|---|---|---|
wgpu<msl> | matmul_simple_cyclic_cmma_multi_rows(1.33 ms) | 同左(3.12 ms) | 3.6–3.8 ms | 9.04 × 10¹² op/s(ComputeCmma) |
wgpu<wgsl> | matmul_double_unit_min_tile_size(1.52 ms) | 同左(13.86 ms) | 13.5–14.3 ms | 2.76 × 10¹² op/s(ComputeDirect) |
1024² 两边差不多,2048² 上 MSL 快三倍多。最后一列是 autotune 自己测的峰值(见 autotune),它说明了差距从哪来:MSL 路径用得上 SIMD-group 矩阵运算。数字只对这台机器有效。
编译缓存:只在进程里
上面的管线表 pipelines 是 WgpuServer 的一个字段,进程结束就没了。WgpuServer::new 里决定它背后有没有持久化:
#[cfg(feature = "spirv")]
let spirv_cache = compilation_store(
"vulkan",
format!("spirv_{}_{}", adapter_info.vendor, adapter_info.device),
);
// WGSL is compiled by the driver on every run, so without the SPIR-V
// store there is nothing persisted for a switch to invalidate.
#[cfg(feature = "spirv")]
let pipelines = CompilationCache::mirroring(&spirv_cache);
#[cfg(not(feature = "spirv"))]
let pipelines = CompilationCache::unbound();
持久的编译缓存只给 SPIR-V 准备:pipeline 末尾的注释是 「Only a SPIR-V kernel is stored: any other build changes nothing.」。compilation_store 还要再看配置项 compilation.cache(默认 false),两者都满足才在 target/environment/default.db 里开一个 vulkan 命名空间。原生的 CUDA、HIP、cubecl-metal 运行时各有自己的编译缓存(命名空间 cuda、hip、metal);wgpu<msl> 没有。建管线时传给 wgpu 的 cache: None 也说明没有用 wgpu 的 PipelineCache。
一次 matmul 引了 burn 文档「Compiled-kernel caching also requires compilation.cache = true」。在 rt-tune 目录下放一个 cubecl.toml:
[compilation]
cache = true
先确认配置读到了(rt-tune config 打印 CubeClRuntimeConfig::get()),再连跑两个进程,各做一次 1024² matmul(autotune 缓存已有):
compilation.cache = false, autotune.level = Balanced, autotune.disable_cache = false
compilation.cache = true, autotune.level = Balanced, autotune.disable_cache = false
--- 进程 1
1024x1024 first: 133.527ms launches=1 [MatmulEntry=1, ~compile(integrate)=1]
--- 进程 2
1024x1024 first: 687.952ms launches=1 [MatmulEntry=1, ~compile(integrate)=1]
(~compile(integrate) 数的是 cubecl 的 integrate span,每编一个新内核一次。)第二个进程仍然编译了一次,数据库里也没有多出编译相关的命名空间(sqlite3 列出的五个命名空间和打开前一样:一个 autotune/..、一个 throughput/..、三个 records/v1/..)。在 wgpu<msl> 上打开 compilation.cache 什么也不改变,每个新进程的第一次 matmul 都要重新编译 MSL。两次第一次调用 133 ms 和 688 ms 差得很多,但两次都编译了一个内核,差别不来自缓存;同样条件下其它几次运行在 100–260 ms 之间。
Vulkan 和 WebGPU(只读源码)
- Vulkan。
burn的vulkan→ cubecl 的vulkan = ["wgpu-spirv"],cubecl-wgpu带上spirv。设备是 Vulkan 适配器、扩展齐全时(register_vulkan_features),AutoCompiler选SpirvCompiler:IR 直接编成 SPIR-V 二进制,create_module同样走透传,不经 naga。上面那个 SPIR-V 编译缓存只在这条路上生效,键是「适配器 vendor/device + 内核 + 构建 id」,换一次构建就失效。cubecl-wgpu的build.rs在 macOS 上开了spirv却没设VULKAN_SDK时直接 panic(要经 MoltenVK)。本卷没有构建这条路。 - WebGPU。
webgpu = ["wgpu"],在 wasm 上Auto只有BrowserWebGpu,编译器只有 WGSL;cubecl-server的构建脚本把编译缓存限定在not(target_family = "wasm"),注释是「the browser compiles its shaders itself, so it has no artifact to store」。WGSL 的写法和上面rt-wgsl的输出一样,只是由浏览器而不是 wgpu-hal 翻译。
rt-tune 先在 Device::metal(DeviceKind::DefaultDevice) 上跑过 1024² 的 f32 matmul,autotune 结果已经写进 target/environment/default.db。下一个进程改用 Device::wgpu(DeviceKind::DefaultDevice)(同一块 GPU,同样是 wgpu<msl>),跑同一个 matmul。这次的第一次调用会启动几个 MatmulEntry 内核:1 个,还是几十个?答案在本页末尾。
动脑筋答案
几十个。依次是 Device::metal(缓存已有)、Device::wgpu 第一次、Device::wgpu 第二次,三个进程:
device: Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))>
1024x1024 first: 120.127ms launches=1 [MatmulEntry=1, ~compile(integrate)=1]
device: Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Auto }))>
1024x1024 first: 778.378ms launches=42 [MatmulEntry=41, UnaryNumeric=1, ~compile(integrate)=7]
device: Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Auto }))>
1024x1024 first: 104.427ms launches=1 [MatmulEntry=1, ~compile(integrate)=1]
数据库里多出一个命名空间:
autotune/0.11.3/device-100-0-wgpu<msl>/burn_cubecl-kernel-matmul-tune-base|1
autotune/0.11.3/device-100-16384-wgpu<msl>/burn_cubecl-kernel-matmul-tune-base|3
throughput/0.11.3/probe-v2/wgpu-msl-_spirv-0-0_Apple-M2-Max_mem4294967292_par0|4
- autotune 的表名里有
CubeTuneId的Display写出的device-{type_id}-{index_id}-{runtime}。Auto设备的 index 是 0,Metal设备是 16384(图形 API 编在 index 的高位里,所以这两个DeviceId不相等)。同一块 GPU,两张表,Device::wgpu那张是空的,于是 41 次候选测量、7 次编译。 - 没有
LaunchOverhead之类的测速内核:吞吐量缓存的命名空间按运行时名、适配器名和显存大小命名,不含设备 id,两种写法共用。 - 第三个进程只剩 1 个:
Device::wgpu自己的表也有了。实际项目里固定用一种写法构造设备,能少一轮调优。
小结
- burn 的
metal= cubecl 的wgpu-msl:wgpu 运行时加cubecl-cpp的 MSL 编译器;原生cubecl-metal(metal-native)burn 不用。后端名wgpu<msl>反映的是运行期是否真的用上了 MSL。 - macOS 上
Device::wgpu和Device::metal都落到 Metal API 和同一块 GPU,但设备 id 不同:两套服务线程、内存池、管线表和 autotune 表;Auto在 MSL 不可用时可以退回 WGSL,Metal不行。 - MSL 是否可用在建服务端时决定:GPU 家族
Metal3 / Apple7 / Mac2加一段只有 MSL 3.2 能编的 lambda 金丝雀。 - 新内核的路径:
KernelId查表未中 →define_kernel得 IR →MslCompiler出 MSL 文本 → wgpu 透传 → MetalnewLibraryWithSource→ 计算管线,放回进程内的表。 - 编译产物在
wgpu<msl>上不持久化,compilation.cache = true只对 SPIR-V(Vulkan)和原生运行时有用;每个新进程的第一次调用都要重新编译。 - 只开
wgpu时同一块 GPU 走 WGSL + naga,没有 CMMA,2048² matmul 慢三倍多。