candle 源码解析
全卷只对着一个版本写:
| 项 | 版本 | tag | commit | 日期 |
|---|---|---|---|---|
candle 工作区(candle-core、candle-nn、candle-transformers、candle-metal-kernels 等,同一个版本号) | 0.11.0 | 0.11.0(没有 v 前缀) | 31f35b14 | 2026-06-26 |
tag 指向的提交是「Bump candle version to 0.11.0 (#3658)」。上一个版本是 0.10.2(2026-04-02)。candle-metal-kernels、candle-kernels(CUDA)、candle-onnx 不在工作区的 members 里,而在 exclude 里,但它们的 Cargo.toml 同样写着 version = "0.11.0"。main 分支在 0.11.0 之后还有提交,没有新的 release,本卷不讲。
candle 给自己的描述是「a minimalist ML framework for Rust with a focus on performance (including GPU support) and ease of use」,README 的 FAQ 说它的核心目标是「make serverless inference possible」:PyTorch 这样的完整框架太大,在集群上起实例慢,candle 让你部署轻量的二进制;其次是把 Python 从生产负载里拿掉。这句话怎么落到代码上、和 burn 有什么不同,见 candle 解决什么问题。
本卷讲 candle-core(张量与布局、Storage 与后端、CPU 后端、Metal 后端、自动微分、safetensors、量化与 GGUF)、candle-nn(VarBuilder、常用层、融合算子、KvCache、优化器)、candle-transformers 里的两个模型(BERT、量化 llama)。实测平台是 macOS 上的 CPU(开与不开 accelerate feature)和 Metal。CUDA 后端和 candle-kernels 只读源码,凡是只读了源码的地方都会写明;candle-pyo3、candle-onnx、candle-datasets、wasm 例子、flash-attn 两个 crate、candle-transformers 里其余一百多个模型不展开,只在源码地图里给出位置和行数。
全卷沿三条线展开:
- 算子主线(一次
a.matmul(&b)?):Tensor::matmul检查形状、算出批维和(m, n, k)→ 取两边storage的读锁 →Storage::matmul先查同设备、同 dtype,再按(Cpu, Cpu)/(Metal, Metal)匹配 → CPU 上gemmcrate 或 Accelerate 的sgemm当场算完;Metal 上 MLX 的 gemm 内核编码进当前命令缓冲区就返回 → 新张量带上BackpropOp→ 读回时(to_vec2、to_scalar)Metal 提交命令缓冲区并等它完成。 - 求导主线(一次
loss.backward()?):Var是is_variable的张量 → 只要有输入需要梯度,每个算子就把输入记进结果张量的Op→backward先sorted_nodes拓扑排序,再倒着套每个Op的求导规则,梯度按TensorId累加进GradStore→candle-nn的优化器按Var取梯度、原地set。 - 推理主线(生成一个 token):
gguf_file::Content::read读头部和张量目录 →ModelWeights::from_gguf按名字取QTensor、建层 →forward(input, pos):embedding → 每层 RmsNorm、注意力(rotary、KV 缓存、Metal 上 decode 走sdpa)、MLP(QMatMul)→ logits → 取样 → 下一个 token。
三条线每一步对应的源码和页面,见怎么读这一卷。
flowchart TB intro[00 导读] --> overview[01 全景] overview --> tensor[02 张量] tensor --> backend[03 后端] backend --> ad[04 自动微分] backend --> nn[05 神经网络层] nn --> quant[06 量化] quant --> models[07 模型] ad --> design[08 设计取舍] models --> design design --> pit[09 常见坑] pit --> site[10 在本站中] site --> hands[11 动手]
实验环境
关于行为的说法(一次调用落到哪个函数、Metal 什么时候提交和等待、首次和之后各花多久、报什么错)都配一个小程序和它的真实输出,平台是 M2 Max、macOS 27、rustc 1.99.0。实验依赖 candle-core / candle-nn / candle-transformers 0.11.0(默认开 metal、accelerate,对照组去掉 accelerate)、tokenizers 0.22.2,一律离线构建;每个实验写明 debug 还是 release。Metal 计时都在结束处 device.synchronize(),并把第一次(编译 .metal 库)和之后分开。
用到的模型都在本地,只读:
| 模型 | 文件 | 用在哪 |
|---|---|---|
| HuggingFaceTB/SmolLM2-135M-Instruct(apache-2.0) | model.safetensors(BF16)、config.json、tokenizer.json | safetensors 与 mmap |
| 同一模型的 GGUF(bartowski 转换) | SmolLM2-135M-Instruct-Q8_0.gguf、Q4_K_M.gguf | 生成一个 token、06 量化、量化 llama 的一次前向 |
| bert-base-chinese | model.safetensors、config.json、tokenizer.json、vocab.txt | BERT、10 在本站中 |
hf-hub 不用来下载,模型一律从本地路径读。
各部分
- 00 导读:锁定的版本,三条主线每一步落在哪个函数、哪一页讲;feature 各打开什么,没开时的 dummy 后端;
Result无处不在的错误风格;crate 分工和规模,.metal源码放在哪、怎么加载。 - 01 全景:candle 和 burn、tch-rs、PyTorch 的分界;一次 matmul、生成一个 token 各从头到尾走一遍,带调用栈和分阶段计时,查清 Metal 第一次为什么慢。
- 02 张量:
Tensor的两层Arc、Layout与视图、DType与Device、错误。 - 03 后端:
Storage枚举和后端 trait,CPU 后端,Metal 后端,自定义算子。 - 04 自动微分:计算图怎么记下来,
backward与GradStore,优化器与训练循环。 - 05 神经网络层:
VarBuilder、safetensors 与 mmap、常用层与融合算子、KV 缓存与注意力。 - 06 量化:GGUF 文件、量化格式、
QTensor与QMatMul。 - 07 模型:BERT、量化 llama 的一次前向、取样。
- 08 设计取舍:枚举后端、eager 不融合、
Result到处都是、手写内核,和 burn 集中对照。 - 09 常见坑:用报错原文或现象做标题,每个都先跑出最小复现。
- 10 在本站中:本站不用 candle;用 bert-base-chinese 给本站的文章算句向量,和 burn 卷的 TF-IDF 对照。
- 11 动手:不依赖 candle,约 300 行写 Q8_0 量化和 GGUF 头部解析,再和 candle 的实现逐字节核对。
适合谁读
- 用 candle 跑过模型,想知道
matmul在 Metal 上为什么「返回得那么快」、第一次为什么慢、GGUF 里的权重怎么变成一次前向的读者:从导读开始,按阅读路线里「只想会用」那条读。 - 读过 burn 卷、想看另一种 Rust 深度学习框架怎么分层的读者:先读 candle 解决什么问题,再按顺序读,最后读 08 设计取舍。
- 需要的前置知识:Rust 的 trait、枚举和
?,一点深度学习(张量、矩阵乘、反向传播、Transformer 的注意力);safetensors 的头部是 JSON(见 serde 卷),candle-transformers的with_tracing包装层用的是 span(见 tracing 卷)。