Rust in Everything
基于 candle-core 0.11.0

candle 解决什么问题

主线位置:三条主线的外面。本页回答 candle 在「一个张量运算」和「一块硬件」之间放了几层、为什么这么少;三条主线都从这张分层图出发。
flowchart TB
  user["用户代码"] --> tr["candle-transformers 模型"]
  user --> nn["candle-nn 层 融合算子"]
  tr --> nn
  nn --> tensor["Tensor 带 BackpropOp"]
  tr --> tensor
  tensor --> storage["Storage 枚举"]
  storage --> cpu["CpuStorage gemm 或 BLAS"]
  storage --> metal["MetalStorage"]
  storage --> cuda["CudaStorage"]
  metal --> mk[".metal 运行期编译"]
  cuda --> ck[".cu 构建期编译"]

candle 怎么描述自己

README 第一句:「Candle is a minimalist ML framework for Rust with a focus on performance (including GPU support) and ease of use.」工作区 Cargo.toml 里的 description 只有三个词:「Minimalist ML framework.」

FAQ 的「Why should I use Candle?」给了三条理由,原文:

  1. 「Candle's core goal is to make serverless inference possible. Full machine learning frameworks like PyTorch are very large, which makes creating instances on a cluster slow. Candle allows deployment of lightweight binaries.」
  2. 「Secondly, Candle lets you remove Python from production workloads. Python overhead can seriously hurt performance, and the GIL is a notorious source of headaches.」
  3. 「Finally, Rust is cool! A lot of the HF ecosystem already has Rust crates, like safetensors and tokenizers.」

重心是推理,不是训练。这和 burn 正好错开:burn 的 README 第一句说自己「optimized for numerical computing, training and inference」,要解决的是「训练用 Python、部署再导出」这一步(见 burn 解决什么问题)。

README 的说法落在哪

README 的 Features 一节逐条列了能力。每一条在 0.11.0 里落在哪:

README 的说法落在哪讲它的页
Simple syntax, looks and feels like PyTorchTensor 的方法名大多照搬 PyTorch(matmul、narrow、transpose、to_dtype),差别是每个调用后面多一个 ?02 张量
Model training自动微分写在 Tensor 里:每个结果张量带一个 op: BackpropOp,backward 沿它走图;candle-nn 有 SGD、AdamW04 自动微分
Embed user-defined ops/kernelsCustomOp1/2/3:用户给 CPU、CUDA、Metal 各写一份实现;candle-nn 的 softmax、rms_norm、layer_norm、sdpa 就是这样写的融合算子自定义算子
Optimized CPU backend with optional MKL support for x86 and Accelerate for macscpu_backend/ 加 cpu/ 里的 NEON / AVX 代码;matmul 默认走 gemm crate,开 accelerate / mkl 换成 BLASCPU 后端
CUDA backend, multiple GPU distribution via NCCLcuda_backend/ 和 candle-kernels,本卷只读源码源码地图
WASM supportcandle-wasm-examples/,本卷不讲—
File formats: safetensors, npz, ggml, or PyTorch filessafetensors.rs、npy.rs、pickle.rs、quantized/ggml_file.rs 和 gguf_file.rssafetensors 与 mmap、GGUF 文件
Quantization support using the llama.cpp quantized typesquantized/:GgmlDType、块结构、QTensor、QMatMul,CPU、Metal、CUDA 各一套06 量化
Serverless (on CPU), small and fast deployments默认 feature 只有 CPU,没有运行时、没有 JIT;下文有一个二进制大小的实测本页

Metal 不在这张表里。README 的「Backends」一段自 2023 年 8 月的 #437 之后没有再改过,而 Metal 后端是 2023 年 11 月从 #1308「Metal part 1 - Scaffolding for metal.」开始加的;candle-core 的 crate 文档只写了一句「CPU and Cuda backends (and M1 support)」。0.11.0 里的 Metal 后端是完整的:metal_backend/ 2864 行,加上 candle-metal-kernels 的 8198 行 Rust 和 16942 行 .metal,本卷 GPU 的实测全在它上面。

结构:少几层

candle 在张量和硬件之间只有两层:Tensor 和 Storage。

  • 后端是一个枚举。 Device 是 Cpu、Cuda(..)、Metal(..) 三个变体,Storage 同样三个变体;每个方法对两边的变体 match,设备不同就返回 Err(DeviceMismatchBinaryOp)。没有后端 trait 对象,也没有可以层层套的包装类型(见 Storage 与后端 trait)。
  • 自动微分不是一层。 记录计算图就是在结果张量里存输入张量的 Arc,由 BackpropOp 决定记不记(见计算图怎么记下来)。
  • 每个 op 立即执行。 没有惰性图,没有融合器:CPU 上调用返回时已经算完;Metal 上调用返回时内核已经编码进命令缓冲区,攒够数量或者读数据时提交(见一次 matmul)。要融合,就手写一个自定义算子,candle-nn/src/ops.rs 里的 SoftmaxLastDim、RmsNorm、LayerNorm、Sdpa 都是这样,每个设备一个内核。
  • 内核是手写的。 Metal 内核是 .metal 源码,进程里第一次用到时编译;CUDA 内核是 .cu,构建时用 nvcc 编(见源码地图)。
  • 错误是值。 几乎所有张量方法和运算符都返回 Result(见怎么读这一卷)。

「minimalist」可以从这几条看出来:一次 matmul 从用户代码到 gemm 只经过 Tensor::matmul、Storage::matmul、CpuStorage::matmul、MatMul::f 四个 candle 函数(一次 matmul 里有 lldb 的调用栈)。

二进制有多大

「Candle allows deployment of lightweight binaries」可以量一下。同一个程序,CPU 上一个 2×2 的 matmul:

use candle_core::{Device, Tensor};

fn main() -> candle_core::Result<()> {
  let a = Tensor::arange(0f32, 4.0, &Device::Cpu)?.reshape((2, 2))?;
  println!("{:?}", a.matmul(&a)?.to_vec2::<f32>()?);
  Ok(())
}
❗IMPORTANT
动脑筋:这个程序只用 CPU。只依赖 candle-core、不开 feature 时,release 二进制多大?开了 metal 又大多少?先猜一个数,再看文末。

和别的框架比

只写能在源码里核实的事实。burn 对照的是 0.22.1,它那一侧的细节见 burn 卷。

candle 0.11.0burn 0.22.1tch-rsPyTorch
自我定位「make serverless inference possible」「both a tensor library and a deep learning framework, optimized for numerical computing, training and inference」——
选后端Device / Storage 三变体枚举,每个 op 里 matchDevice 值,全局后端 Dispatch 在运行期 match,分支后面是一整套 Backend 实现用 LibTorch 的设备用自己的设备
自动微分内建在 Tensor 里Autodiff<B> 装饰器,任何后端可套用 LibTorch 的内建
融合没有通用机制,靠手写的融合算子Fusion<B> 装饰器,运行期把 op 流编成融合内核用 LibTorch 的本卷不核实
GPU 内核手写 .cu(26 个)、.metal(17 个文件)Rust 写的 #[cube] 函数,cubecl 运行期编译LibTorch 自带本卷不核实
出错返回 Result张量方法 panic,读数据、同步返回 Result——

几处说明:

  • burn:candle 的 README 把它描述成「a general crate that can leverage multiple backends so you can choose the best engine for your workload」。两边都是对设备枚举 match,差别在分支后面是什么,见 burn 解决什么问题 里的「没有蠢问题」;burn 卷的设计取舍从 burn 那一侧列过 candle 的手写内核、即时编码、Result 错误。burn 0.9.0 到 0.21 之间还有过一个用 candle 当后端的 burn-candle,同一页的冷知识讲了它的来去。完整的对照放在本卷的 08 设计取舍。
  • tch-rs:README 原文「Bindings to the torch library in Rust. Extremely versatile, but they bring in the entire torch library into the runtime.」,并说 tch-rs 的主要贡献者也参与 candle 的开发。这正是 FAQ 第一条「frameworks like PyTorch are very large」的反面:candle 不链接 LibTorch。
  • dfdx:README 说它「shapes being included in types」,编译器能直接指出形状不匹配,但「some features still require nightly」。candle 的形状是运行期的 Shape,不匹配是一个 Err。
  • PyTorch:本卷离线写作,只引用 candle 源码里能看到的关系。README 的速查表把 torch.Tensor、tensor[:, :4]、a.matmul(b)、torch.save 等十种写法和 candle 一一对上;pickle.rs 能读 PyTorch 的 .pt / .pth 检查点,VarBuilder::from_pth 用它加载权重(见 VarBuilder)。PyTorch 自身的实现细节本卷不做断言。

动脑筋答案

只依赖 candle-core、不开 feature:0.92 MB(strip 之后 0.72 MB)。开 metal:1.77 MB,几乎翻倍,虽然程序一行 Metal 代码都没有。三种构建都是 release,同一个 tiny.rs:

intro-tree [[2.0, 3.0], [6.0, 11.0]] size=919552 stripped=722080
intro-mm-noacc [[2.0, 3.0], [6.0, 11.0]] size=1767776 stripped=1512784
intro-mm [[2.0, 3.0], [6.0, 11.0]] size=1420640 stripped=1239072
  • intro-tree 不开 feature,intro-mm-noacc 开 metal,intro-mm 开 metal 和 accelerate。
  • 多出来的一大块是 .metal 源码本身:Kernels 的 get_library_source 对 Source 的 16 个变体 match,16 个 include_str! 常量都被引用,链接器删不掉。16 个文件合计 617765 字节,strings 在 strip 后的二进制里能找到 16 处 metal_stdlib。
  • 开 accelerate 反而小了 0.35 MB:matmul 换成系统的 sgemm_,gemm crate 的代码不再链接进来,BLAS 在 Accelerate 框架里,不算进二进制。
  • 依赖 candle-core 就会编译 tokenizers(见源码地图),但这个程序没用到它,链接器把它整个删掉了。

作为参照,burn 0.22.1 里一个只用 CPU 的程序,开了 metal 之后从 6.7 MB 变成 105 MB(见 burn 卷的设计取舍)。candle 多出来的是一段文本,burn 多出来的是整个 wgpu 和 cubecl 运行时。

小结

  • candle 的目标是推理:小二进制、不要 Python;训练能做,但不是重心。
  • 张量和硬件之间只有 Tensor 和 Storage 两层:后端是三变体枚举,自动微分内建在 Tensor 里,每个 op 立即执行,没有融合器。
  • 内核手写:Metal 是 .metal 源码、运行期编译;CUDA 是 .cu、构建期编译。
  • README 的 Backends 一段没写 Metal,但 0.11.0 的 Metal 后端是完整的,本卷 GPU 实测都在它上面。
  • 只做一次 CPU matmul 的程序 release 0.92 MB;开 metal 多出约 0.85 MB,其中 0.62 MB 是嵌进去的 .metal 源码。

💬 关联讨论

发起讨论
加载中...