Rust in Everything
基于 candle-core 0.11.0

后端

张量那一部分把调用送到 self.storage().matmul(..)。这一部分讲门后:Storage 怎么选后端,CPU 和 Metal 各自怎么把数算出来、什么时候真的算完,以及 trait 里没有的融合算子从哪里来。

代码集中在 candle-core/src/:storage.rs、backend.rs、op.rs(分派与契约);cpu_backend/、cpu/、accelerate.rs(CPU);metal_backend/(Metal 的 BackendStorage 实现);custom_op.rs。Metal 的命令缓冲区、内核加载和 .metal 源码在单独的 crate candle-metal-kernels。CUDA 后端(cuda_backend/、candle-kernels)本卷只读源码、没有实测。

  1. Storage 与后端 trait:Storage 永远是 Cpu、Cuda、Metal 三个变体,没开的后端换成 dummy 类型;方法先查同设备、同 dtype,再按设备对 match;BackendStorage 要求 Sized,写不成 trait 对象;每个逐元素算子是一个零大小类型,编译期展开,整数 dtype 的一元运算会 panic。
  2. CPU 后端:CpuStorage 每种 dtype 一个 Vec;Map1 / Map2 把 dtype 分派写一次;逐元素运算单线程;matmul 默认走 gemm,开 accelerate 走 sgemm,实测快 3.7 倍,但 f16 乘不了、bf16 两种构建都乘不了;三个线程池读两个不同的环境变量。
  3. Metal 后端:一个共享的并发计算编码器加自动内存屏障;每 50 次取编码器提交一次命令缓冲区;读回时等的是包含自己那次 blit 的命令缓冲区;缓冲区按 2 的幂分桶复用;.metal 源码运行时编译,GEMM、归约首次各要 4 秒以上,系统缓存命中时只要几毫秒;copy() 不复制、批维步长特殊时 matmul 静默算错两个上游 bug。
  4. 自定义算子:CustomOp1/2/3 每个后端一个前向、加一个用张量运算写的 bwd;candle-nn 的 softmax_last_dim、rms_norm、layer_norm 是融合算子但不带反向,梯度在那里静默中断;写一个 CPU + Metal 的 softplus;原地算子拿同一个张量当两个参数会死锁。
flowchart TB
  t["Tensor 方法"] --> st["Storage 枚举"]
  st -->|Cpu| cpu["CpuStorage"]
  st -->|Metal| mtl["MetalStorage"]
  st -->|Cuda| cu["CudaStorage"]
  cpu --> g["gemm 或 Accelerate"]
  mtl --> k["candle-metal-kernels"]
  k --> q["命令缓冲区"]
  nn["candle-nn ops"] --> co["CustomOp 前向"]
  co --> st
  tr["BackendStorage trait"] -.->|实现| cpu
  tr -.->|实现| mtl
  tr -.->|实现| cu

第一页是分派和契约,后两页是契约的两个实测过的实现者,最后一页是契约之外的扩展口。算完之后结果张量怎么记进计算图,见计算图怎么记下来;量化矩阵乘走的是另一套存储 QStorage,见 QTensor 与 QMatMul。

💬 关联讨论

发起讨论
加载中...