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?」给了三条理由,原文:
- 「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.」
- 「Secondly, Candle lets you remove Python from production workloads. Python overhead can seriously hurt performance, and the GIL is a notorious source of headaches.」
- 「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 PyTorch | Tensor 的方法名大多照搬 PyTorch(matmul、narrow、transpose、to_dtype),差别是每个调用后面多一个 ? | 02 张量 |
| Model training | 自动微分写在 Tensor 里:每个结果张量带一个 op: BackpropOp,backward 沿它走图;candle-nn 有 SGD、AdamW | 04 自动微分 |
| Embed user-defined ops/kernels | CustomOp1/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 macs | cpu_backend/ 加 cpu/ 里的 NEON / AVX 代码;matmul 默认走 gemm crate,开 accelerate / mkl 换成 BLAS | CPU 后端 |
| CUDA backend, multiple GPU distribution via NCCL | cuda_backend/ 和 candle-kernels,本卷只读源码 | 源码地图 |
| WASM support | candle-wasm-examples/,本卷不讲 | — |
| File formats: safetensors, npz, ggml, or PyTorch files | safetensors.rs、npy.rs、pickle.rs、quantized/ggml_file.rs 和 gguf_file.rs | safetensors 与 mmap、GGUF 文件 |
| Quantization support using the llama.cpp quantized types | quantized/: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(())
}
candle-core、不开 feature 时,release 二进制多大?开了 metal 又大多少?先猜一个数,再看文末。和别的框架比
只写能在源码里核实的事实。burn 对照的是 0.22.1,它那一侧的细节见 burn 卷。
| candle 0.11.0 | burn 0.22.1 | tch-rs | PyTorch | |
|---|---|---|---|---|
| 自我定位 | 「make serverless inference possible」 | 「both a tensor library and a deep learning framework, optimized for numerical computing, training and inference」 | — | — |
| 选后端 | Device / Storage 三变体枚举,每个 op 里 match | Device 值,全局后端 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_,gemmcrate 的代码不再链接进来,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源码。