Rust in Everything
基于 burn 0.22.1

一次 matmul

主线位置:算子主线从头到尾。Tensor::matmul → Numeric::matmul → Dispatch →(Autodiff)→ Flex 的 gemm;或者 →(Autodiff)→ Fusion 登记 IR,读数据时才 drain → CubeBackend → autotune → cubek 内核 → cubecl 提交。每一步的源码和细讲的页见怎么读这一卷的算子主线表,本页用实验把它们一层层露出来。

sequenceDiagram
  participant M as main 线程
  participant D as Dispatch
  participant A as Autodiff
  participant F as Fusion
  participant U as DSU 设备线程
  participant W as DSD 设备线程
  M->>D: a.matmul(b)
  D->>A: float_matmul
  A->>F: float_matmul 并记反向步骤
  F-->>M: register 后立刻返回
  M->>F: c.into_data()
  F->>U: read_tensor_float
  U->>U: drain 流 并执行 matmul
  U->>W: Client launch 内核
  W->>W: 编译 MSL 并提交
  U-->>M: 读回的数据

实验程序

实验 crate intro-lean 只开 burn 的 flex、metal、tracing 三个 feature(默认 feature 开着,所以 cubecl 后端自带 fusion 和 autotune,见怎么读这一卷),另外直接依赖 burn-backend 和 burn-dispatch,只为调用 Backend::name。没有开 autotune-checks:那个 feature 让 autotune 把每个候选都跑一遍再核对结果,会改变下面的内核数和计时。四个设备、一个 2×2 的 matmul:

use burn::tensor::{Device, DeviceKind, Tensor};
use burn_backend::Backend;
use burn_dispatch::Dispatch;

fn devices() -> Vec<(&'static str, Device)> {
  vec![
    ("flex", Device::flex()),
    ("flex.autodiff()", Device::flex().autodiff()),
    ("metal", Device::metal(DeviceKind::DefaultDevice)),
    ("metal.autodiff()", Device::metal(DeviceKind::DefaultDevice).autodiff()),
  ]
}

fn one_matmul(device: &Device) -> Vec<f32> {
  let a = Tensor::<2>::from_floats([[1.0, 2.0], [3.0, 4.0]], device);
  let b = Tensor::<2>::from_floats([[5.0, 6.0], [7.0, 8.0]], device);
  let c = a.matmul(b);
  c.into_data().try_to_vec::<f32>().unwrap()
}

fn names() {
  for (label, device) in devices() {
    let name = Dispatch::name(device.as_dispatch());
    println!("{label:18} {device:?}");
    println!("{:18} name = {name}", "");
    println!("{:18} result = {:?}", "", one_matmul(&device));
  }
}

除非另外说明,下面的输出都是 debug 构建;计时一节是 release。

每一层叫什么

Device 是一个门面,as_dispatch 取出里面的 DispatchDevice。每个后端的 Backend::name 都把内层的名字包一层:Dispatch 写 dispatch<{inner}>,Autodiff 写 autodiff<..>,Fusion 写 fusion<..>,CubeBackend 写 cubecl<{client.name()}>,Flex 直接返回 "flex"。所以名字就是这次调用要穿过的层:

flex               Device<Flex(Cpu)>
                   name = dispatch<flex>
                   result = [19.0, 22.0, 43.0, 50.0]
flex.autodiff()    Device<Autodiff { device: Flex(Cpu), checkpointing: Disabled }>
                   name = dispatch<autodiff<flex>>
                   result = [19.0, 22.0, 43.0, 50.0]
metal              Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))>
                   name = dispatch<fusion<cubecl<wgpu<msl>>>>
                   result = [19.0, 22.0, 43.0, 50.0]
metal.autodiff()   Device<Autodiff { device: Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal })), checkpointing: Disabled }>
                   name = dispatch<autodiff<fusion<cubecl<wgpu<msl>>>>>
                   result = [19.0, 22.0, 43.0, 50.0]

几点从输出里直接读得出来:

  • .autodiff() 不是换了一个设备,而是把原设备包进 DispatchDevice::Autodiff { device, checkpointing };后端名多出最外面的 autodiff<..>(Dispatch 之内)。
  • Metal 设备的值是 Cube(Wgpu(..backend: Metal)):七种 cubecl 运行时共用 Cube 这一个变体,「是 Metal」只是值里的一个字段。
  • fusion 套在 cubecl 外面,因为 burn-cubecl 开了 fusion feature 时 Cube = Fusion<CubeBackend>。Flex 是 eager 后端,没有这一层。
  • wgpu<msl>:Metal 走的是 wgpu 运行时加 MSL 编译器(cubecl 的 metal = ["wgpu-msl"]),不是原生的 cubecl-metal,细节见 wgpu 与 Metal。

名字说明了层级,但没说明「什么时候」「在哪个线程上」经过它们。下面用 lldb 和 tracing 看这两点。

Flex:调用当场算完

在 Flex 的 gemm 调用处(burn_flex::ops::matmul::gemm_call)下断点,跑 flex.autodiff() 那一组,bt 的前 11 帧(参数和过长的类型名截断,标 ...):

* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: intro-lean`burn_flex::ops::matmul::gemm_call::<f32>(m=2, n=2, k=2, ...)
    frame #1: intro-lean`burn_flex::ops::matmul::matmul_2d_strided::<f32>
    frame #2: intro-lean`burn_flex::ops::matmul::matmul_gemm::<f32>
    frame #3: intro-lean`burn_flex::ops::matmul::matmul
    frame #4: intro-lean`<burn_flex::backend::Flex as ...FloatTensorOps<burn_flex::backend::Flex>>::float_matmul
    frame #5: intro-lean`<burn_autodiff::backend::Autodiff<burn_flex::backend::Flex> as ...>::float_matmul
    frame #6: intro-lean`<burn_dispatch::backend::Dispatch as ...FloatTensorOps<burn_dispatch::backend::Dispatch>>::float_matmul
    frame #7: intro-lean`<burn_tensor::bridge::kind::Float as burn_tensor::bridge::ops::numeric::Numeric>::matmul
    frame #8: intro-lean`<burn_tensor::tensor::api::base::Tensor<2>>::matmul
    frame #9: intro-lean`intro_lean::trace
    frame #10: intro-lean`intro_lean::main

从下往上正好是算子主线的前四步:泛型的 Tensor::matmul → 不泛型的 Numeric::matmul → Dispatch::float_matmul(match 是宏展开在这一帧里的,不单独占一帧)→ Autodiff::float_matmul → Flex::float_matmul → matmul::matmul 按 dtype 选 matmul_gemm::<f32>,最后 gemm_call 调 gemm::gemm。全程在 main 线程上,a.matmul(b) 返回时结果已经在内存里。.autodiff() 只多了第 5 帧:Autodiff 登记反向步骤之后把原语交给内层(见一个算子的反向)。

Metal:matmul 只登记一条 IR

同样的程序跑 metal.autodiff(),断点下在 GlobalFusionClient::register 的 MatmulOps 实例上:

* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = breakpoint 1.58
    ...
    frame #2: intro-lean`<burn_fusion::client::GlobalFusionClient<burn_cubecl::fusion::FusionCubeRuntime>>::register::<...MatmulOps...>
    frame #3: intro-lean`<burn_fusion::backend::Fusion<burn_cubecl::backend::CubeBackend> as ...>::float_matmul
    frame #4: intro-lean`<burn_autodiff::backend::Autodiff<burn_fusion::backend::Fusion<burn_cubecl::backend::CubeBackend>> as ...>::float_matmul
    frame #5: intro-lean`<burn_dispatch::backend::Dispatch as ...FloatTensorOps<burn_dispatch::backend::Dispatch>>::float_matmul
    frame #6: intro-lean`<burn_tensor::bridge::kind::Float as burn_tensor::bridge::ops::numeric::Numeric>::matmul
    frame #7: intro-lean`<burn_tensor::tensor::api::base::Tensor<2>>::matmul
    frame #8: intro-lean`intro_lean::trace
    frame #9: intro-lean`intro_lean::main

前几层一样,Dispatch 的 match 这次走到了 Cube 分支,内层是 Fusion<CubeBackend>。Fusion::float_matmul 不碰 GPU:

    fn float_matmul(lhs: FloatTensor<Self>, rhs: FloatTensor<Self>) -> FloatTensor<Self> {
        binary_float_ops!(MatmulOps, B::float_matmul);

        let streams = StreamId::current();

        let client = lhs.client.clone();
        let desc = MatmulOpIr::create(lhs.into_ir(), rhs.into_ir(), || {
            client.create_empty_handle()
        });

        client
            .register(
                streams,
                OperationIr::Float(desc.out.dtype, FloatOperationIr::Matmul(desc.clone())),
                MatmulOps::<B>::new(desc.into()),
            )
            .output()
    }

它做两件事:把这次调用描述成 OperationIr::Float(.., FloatOperationIr::Matmul(..))(给融合搜索看的数据),再附上一个 MatmulOps(真要执行时调 B::float_matmul 的闭包),一起交给 register 送进当前线程的流,返回一个还没有数据的 FusionTensor。IR 长什么样、流怎么排队,见 burn-ir 和 Fusion 装饰器。

❗IMPORTANT
动脑筋:Metal 上 a.matmul(b) 这一行要多久?

两个 1024×1024 的 f32 矩阵,已经在 Metal 设备上,前面已经跑过同一个形状的 matmul(内核编译过、autotune 选过)。程序先计时 let c = a.clone().matmul(b.clone()); 这一行,再计时到 device.sync() 返回。release 构建。matmul 这一行大约是微秒级、毫秒级,还是和 sync 之后差不多?答案在本页末尾。

内核什么时候提交:into_data

开 tracing feature 之后,burn 和 cubecl 在几个地方编进了 span:各层的 float_from_data / float_into_data、cubecl 的 launch_inner(字段里带内核名)、内核编译(integrate)、内存池。matmul 本身没有 span。这些 span 是 trace 级别,要配 subscriber 才看得到(tracing 卷讲 Layer)。实验里写了一个只实现 on_new_span 的 Layer,每建一个 span 就打印线程名、target 和祖先链:

impl<S> Layer<S> for SpanPrinter
where
  S: tracing::Subscriber + for<'a> LookupSpan<'a>,
{
  fn on_new_span(&self, attrs: &Attributes<'_>, _id: &Id, ctx: Context<'_, S>) {
    let meta = attrs.metadata();
    // cubecl's persistent cache is a turso (SQLite) database; its spans are noise here.
    if meta.target().starts_with("turso") {
      return;
    }
    // ... 从 ctx.lookup_current() 收集祖先 span 的名字,从 attrs 里取 kernel.name 字段
    let thread = std::thread::current().name().unwrap_or("?").to_string();
    println!("[span] [{thread}] {:<28} {}{}", meta.target(), path.join(" > "), kernel);
  }
}

先用一个张量把设备初始化掉,再装上 subscriber,跑一次 from_data ×2、matmul、into_data(metal.autodiff(),autotune 缓存已有这个形状;内存池的 span 省略,标 ...):

--- from_data x2
[span] [main] burn_autodiff::ops::tensor   float_from_data
[span] [main] burn_fusion::ops::tensor     float_from_data > float_from_data
[span] [main] burn_cubecl::ops::tensor     float_from_data > float_from_data > float_from_data
[span] [main] burn_autodiff::ops::tensor   float_from_data
[span] [main] burn_fusion::ops::tensor     float_from_data > float_from_data
[span] [main] burn_cubecl::ops::tensor     float_from_data > float_from_data > float_from_data
--- matmul
--- into_data
[span] [main] cubecl_environment::future::base block_on
[span] [main] burn_autodiff::ops::tensor   block_on > float_into_data
[span] [main] burn_fusion::ops::tensor     block_on > float_into_data > float_into_data
[span] [DSU-100-16384] cubecl_environment::persistence::store new
[span] [DSU-100-16384] cubecl_environment::future::base new > block_on
...
[span] [DSU-100-16384] cubecl_runtime::client       launch_inner  kernel=cubek_matmul::multi_level::components::batch::partitioned_matmul::matmul::matmul_entry::MatmulEntry<...>
[span] [main] burn_cubecl::ops::tensor     block_on > float_into_data > float_into_data > float_into_data
[span] [main] burn_cubecl::kernel::contiguous block_on > float_into_data > float_into_data > float_into_data > into_contiguous_aligned
[span] [DSD-4-16384] cubecl_server::memory_management::memory_manage reserve
...
[span] [DSD-4-16384] cubecl_core::codegen::integrator integrate
...
--- result [19.0, 22.0, 43.0, 50.0]

--- matmul 和 --- into_data 之间一个 span 也没有:matmul 那一行只登记了 IR。from_data 的 span 嵌套了三层(autodiff > fusion > cubecl),和后端名一致。读数据时:

  1. 同步的 into_data 外面包了一层 block_on(futures_lite 的,burn 不依赖 tokio),里面是各层的 async fn float_into_data。
  2. Fusion 的 float_into_data 把读请求交给另一个线程 DSU-100-16384,matmul 内核在那个线程上 launch_inner。在它之前的 persistence::store new 和一串 block_on 是打开 cubecl 的持久缓存、查这个形状的 autotune 结果(缓存见下文)。
  3. 内核编译(integrate)和显存分配又在第三个线程 DSD-4-16384 上。

这两个线程是 cubecl 起的设备服务线程,DeviceClient::new 起线程时把名字定成 DS{U|D}-<设备类型>-<设备号>:U 是 DeviceServiceStage::Upstream(文档注释:「Produces work ahead of kernel dispatch (e.g. autodiff graph, fusion)」),D 是 Downstream(「Consumes upstream work and streams kernels to the device」)。每个阶段每个设备一个线程,让融合这类上游工作和内核下发重叠。

想知道 launch_inner 是从哪条路调过来的,在 on_new_span 里遇到 launch_inner 时 Backtrace::force_capture(),只留 burn / cubecl / cubek 的帧。93 帧里挑出算子主线相关的(帧号是过滤后的序号,中间的闭包、catch_unwind、线程局部存储帧省略):

    bt  1 <cubecl_runtime::client::Client>::launch_inner
    bt  2 <cubecl_runtime::client::Client>::launch
    bt  3 <cubecl_core::compute::launcher::KernelLauncher>::launch::<cubek_matmul::...::MatmulEntry<...>>
    ...
    bt 13 cubek_matmul::launch::launch_ref
    bt 14 burn_cubecl::kernel::matmul::base::launch_matmul::<cubek_matmul::multi_level::strategy::Strategy>
    bt 15 burn_cubecl::kernel::matmul::tune::base::matmul_autotune::{closure#3}::{closure#9}
    ...
    bt 19 <cubecl_runtime::tune::local::LocalTuner<cubek_matmul::tune_key::MatmulAutotuneKey, cubecl_core::id::CubeTuneId>>::execute::<...>
    bt 20 burn_cubecl::kernel::matmul::tune::base::matmul_autotune
    bt 21 burn_cubecl::kernel::matmul::base::matmul
    bt 22 <burn_cubecl::backend::CubeBackend as burn_backend::backend::ops::tensor::FloatTensorOps<burn_cubecl::backend::CubeBackend>>::float_matmul
    bt 23 <<burn_fusion::backend::Fusion<_> as ...>::float_matmul::MatmulOps<burn_cubecl::backend::CubeBackend> as burn_fusion::stream::exec...
    ...
    bt 25 <burn_fusion::op::UnfusedOpInArena<burn_cubecl::fusion::FusionCubeRuntime>>::execute
    ...
    bt 38 <burn_fusion::stream::execution::ordering::OrderedExecution<burn_cubecl::fusion::FusionCubeRuntime>>::execute_operations
    ...
    bt 47 <burn_fusion::stream::queue::base::OperationQueue<burn_cubecl::fusion::FusionCubeRuntime>>::execute_block_optimization
    ...
    bt 51 <burn_fusion::stream::execution::processor::Processor<burn_cubecl_fusion::optim::base::CubeOptimization>>::process::<...>
    bt 52 <burn_fusion::stream::multi::MultiStream<burn_cubecl::fusion::FusionCubeRuntime>>::drain::{closure#0}
    ...
    bt 55 <burn_fusion::server::FusionServer<burn_cubecl::fusion::FusionCubeRuntime>>::drain_stream
    bt 56 <burn_fusion::server::FusionServer<burn_cubecl::fusion::FusionCubeRuntime>>::prepare_read
    bt 57 <burn_fusion::server::FusionServer<burn_cubecl::fusion::FusionCubeRuntime>>::read_float::<burn_cubecl::backend::CubeBackend>
    bt 58 <burn_fusion::server::FusionServer<burn_cubecl::fusion::FusionCubeRuntime>>::float_data::<burn_cubecl::backend::CubeBackend>
    bt 59 <burn_fusion::client::GlobalFusionClient<burn_cubecl::fusion::FusionCubeRuntime>>::read_tensor_float::<burn_cubecl::backend::CubeBackend>::{closure#0}
    ...
    bt 84 <cubecl_common::device::handle::channel::custom_channel::Server>::start
    bt 85 <cubecl_common::device::handle::channel::custom_channel::DeviceClient>::new::<...>::{closure#0}
    ...

从下往上读,就是算子主线的后半段:

帧发生了什么讲它的页
84–85设备服务线程的主循环,收到 main 线程发来的读请求运行时与 Client
55–59FusionServer::drain_stream:读之前先把这个流里攒下的操作全部执行Fusion 装饰器
38–52流被切成块、问各个优化器能不能融合;matmul 单独一块,最后以 UnfusedOp 执行融合搜索
22–23登记时附上的 MatmulOps 被执行,调 CubeBackend::float_matmulburn-cubecl 后端
15–21matmul_autotune 经 LocalTuner 查这个形状的调优结果,执行选中的那个候选autotune
1–14cubek 的 launch_ref 按策略选出内核族,KernelLauncher::launch → Client::launch#[cube] 内核

这次的 matmul 没有被融合:一串里只有一个 matmul、后面没有逐元素操作可以接上。matmul 后面接逐元素 op 时 burn-cubecl-fusion 会把它们编进同一个内核,见生成融合内核。

第一次和之后

空缓存的第一次:两万多个内核

把 autotune 缓存删掉,再跑一次上面的 2×2 实验,按内核名数 launch_inner(debug 构建):

run 1: launches=18125 integrate=6
   1 burn_cubecl::kernel::unary_numeric::unary_numeric::UnaryNumeric<burn_cubecl::tensor::base::CubeTensor::copy::C
  51 cubecl_std::throughput::runners::compute_cmma::compute_cmma_throughput::ComputeCmmaThroughput
18046 cubecl_std::throughput::runners::launch_overhead::launch_overhead::LaunchOverhead
   7 cubecl_std::throughput::runners::memory_direct::memory_direct_throughput::MemoryDirectThroughput
   1 cubecl_std::throughput::runners::memory_probe::prime_buffer::PrimeBuffer
   4 cubek_matmul::multi_level::components::batch::partitioned_matmul::matmul::matmul_entry::MatmulEntry<cubek_matm
run 2: launches=1 integrate=1
   1 cubek_matmul::multi_level::components::batch::partitioned_matmul::matmul::matmul_entry::MatmulEntry<cubek_matm

同一个 2×2 矩阵乘,第一次(空缓存,debug 构建)提交了 18 125 个内核,第二次(另一个进程)只有 1 个;release 构建里第一次的数目是几万到二十几万,随测速采样而变,见 autotune。多出来的几乎都是 cubecl_std::throughput 下的测速内核:matmul 的 autotune 用 roofline_bounds 给候选设一个「离峰值多近就够了」的上界,峰值算力、显存带宽和内核启动开销要先在这台设备上实测一遍;4 个 MatmulEntry 里有 3 个是 autotune 在比较候选,1 个是真正的计算。

📝NOTE
没有蠢问题

问: 一个 2×2 的矩阵乘,为什么要先跑一万八千次 LaunchOverhead?这是每次启动都要付的代价吗?

答: 不是每次。测速结果和 autotune 结果都写进 cubecl 的持久缓存,第二个进程直接读出来,上面的 run 2 只剩 1 个内核。这个缓存默认就开着:cubecl-runtime 的默认 feature 里有 persistence,不需要打开 burn/persistence。它是一个 turso(Rust 写的 SQLite)数据库,CacheConfig 的默认值是 Target:从当前目录往上找最外层的 Cargo.toml,放在它旁边的 target/environment/default.db(还有 -wal、-tshm 两个文件);找不到 Cargo.toml 时退回系统的用户缓存目录。它不看 CARGO_TARGET_DIR:本卷的实验把编译产物放在别处,缓存却出现在每个实验 crate 自己的 target/environment/ 下。换一个目录运行同一个二进制,就会再测一遍。

计时:1024×1024

release 构建,time metal 1024 模式:Tensor::<2>::random 建两个 1024×1024 的 f32 矩阵、sync,然后同一个 matmul 跑 6 次,每次先记 matmul 返回的时间,再 device.sync()、记总时间。第一组是空缓存的新进程,第二组是紧接着的另一个进程:

## process 1 (empty cache)
metal 1024x1024 f32: 2x random + sync (incl. device init): 249.754667ms
run 0: matmul returned after 44.333µs, after sync 4.22719725s
run 1: matmul returned after 11.875µs, after sync 2.625292ms
run 2: matmul returned after 9.334µs, after sync 1.887292ms
run 3: matmul returned after 7.375µs, after sync 1.94525ms
run 4: matmul returned after 7.375µs, after sync 2.119583ms
run 5: matmul returned after 7.542µs, after sync 1.888292ms
## process 2
metal 1024x1024 f32: 2x random + sync (incl. device init): 101.383ms
run 0: matmul returned after 21.291µs, after sync 110.741208ms
run 1: matmul returned after 6.708µs, after sync 1.794375ms
run 2: matmul returned after 2.958µs, after sync 1.898083ms
...
## flex
flex 1024x1024 f32: 2x random + sync (incl. device init): 13.251ms
run 0: matmul returned after 6.278084ms, after sync 6.278209ms
run 1: matmul returned after 5.207875ms, after sync 5.208167ms
run 2: matmul returned after 5.12625ms, after sync 5.126459ms
...
第一次 matmul(到 sync)之后每次第一次里有什么
Metal,空缓存的新进程4.23 s约 1.9 ms设备测速 + autotune 比较候选 + 编译 MSL
Metal,缓存已有111 ms约 1.8–1.9 msautotune 结果从缓存读,内核仍要编译(编译产物默认不进缓存,见下)
Flex6.3 ms约 5.1–5.8 ms没有编译和调优

数字只对这台机器(M2 Max、macOS 27、rustc 1.99.0)有效。1024³ 的矩阵乘约 2.1 GFLOP,稳定后 Metal 约 1.9 ms、Flex 约 5 ms。内核编译不进持久缓存,所以缓存已有时第一次仍要 111 ms。burn 的 crate 文档说 persistence 之外「Compiled-kernel caching also requires compilation.cache = true」,但在 wgpu<msl> 上打开它也不缓存编译产物(只有 SPIR-V 会存),实测见 wgpu 与 Metal。这些阶段各自的机制见 autotune 和 wgpu 与 Metal。

autotune 选了谁,可以让 cubecl 自己说:环境变量 CUBECL_DEBUG_LOG=stdout(override_from_env 读它)把编译、性能和 autotune 三种日志都打到标准输出。空缓存跑一次 time metal 1024,近 3.8 万行输出里有每个内核的 MSL 源码(以 #include <metal_stdlib> 开头),和这几行(长行截断,标 ...):

Fastest result matmul_simple_cyclic_cmma_multi_rows-MatmulAutotuneKey - Definition: MatmulProblemDefinition { m: 1024, n: 1024, k: 1024, ...
Short circuiting autotune. matmul_simple_cyclic_cmma_multi_rows is close enough to peak throughput.
Autotune[14] name matmul_simple_cyclic_cmma_multi_rows => BenchmarkComputations { mean: 1.332687ms, median: 1.333292ms, variance: 0ns, min: 1.332083ms, max: 1.333292ms }
Autotune[12] name matmul_simple_cyclic_cmma => BenchmarkComputations { mean: 1.56325ms, median: 1.56325ms, variance: 0ns, min: 1.56325ms, max: 1.56325ms }
matmul_naive: The autotune is skipped manually.
...

这一次只实测了两个候选,较快的那个已经「close enough to peak throughput」(同一段日志的 Calculated bounds 一行里阈值是 threshold: 0.8),autotune 提前收工;日志里另有 31 行「The autotune is skipped manually」。提前收工是开日志带来的:CUBECL_DEBUG_LOG 让每次启动都阻塞,测出来的启动开销从约 3.6 µs 变成约 1.6 ms,上界跟着放宽;不开日志时同一个形状会实测 10 个候选里的 6 个,不会提前收工。所以开着这个变量时的计时和 autotune 选择都不能当真,细节见 autotune。

动脑筋答案

Metal 上 matmul 那一行是微秒级:缓存热了之后 3–12 µs,第一次 21–44 µs;时间都花在 sync 里(稳定后约 1.9 ms)。Flex 正好相反,matmul 返回时已经算完,sync 前后只差零点几微秒:

## Metal(缓存已有)
run 1: matmul returned after 6.708µs, after sync 1.794375ms
run 2: matmul returned after 2.958µs, after sync 1.898083ms
## Flex
run 1: matmul returned after 5.207875ms, after sync 5.208167ms
  • Metal 的几微秒是走完 Tensor::matmul → Dispatch → Fusion::float_matmul 的时间:建一个 MatmulOpIr、register 进流,没有碰 GPU。
  • sync 这一步才让融合流 drain、在设备线程上提交内核并等 GPU 做完。所以在 GPU 上计时要在结束处 sync(或读数据),否则测到的只是登记的时间。
  • Flex 的 sync 几乎不花时间:eager 后端没有排队的东西。

小结

  • Backend::name 一层套一层,就是一次调用要穿过的层:Flex 上 dispatch<flex>,Metal 上 dispatch<fusion<cubecl<wgpu<msl>>>>,.autodiff() 在 dispatch 里面再包一层 autodiff。
  • 前半段(Tensor::matmul → Numeric::matmul → Dispatch → Autodiff)两种设备一样,都在调用线程上。
  • Flex 在同一个调用里由 gemm 算完;Metal 上 Fusion 只把调用记成 OperationIr 送进流,几微秒就返回。
  • 内核在读数据(或 sync)时提交:FusionServer::drain_stream 在 cubecl 的上游设备线程上执行流,CubeBackend → matmul_autotune → cubek → Client::launch;编译和下发在下游线程。
  • 空缓存的第一次要先测设备(上万个测速内核)、比较候选、编译 MSL,1024² 的 matmul 首次 4.2 s;autotune 结果默认持久化在最外层 Cargo.toml 旁的 target/environment/,编译产物默认不缓存。

💬 关联讨论

发起讨论
加载中...