Rust in Everything
基于 candle-core 0.11.0

candle 源码解析

全卷只对着一个版本写:

项版本tagcommit日期
candle 工作区(candle-core、candle-nn、candle-transformers、candle-metal-kernels 等,同一个版本号)0.11.00.11.0(没有 v 前缀)31f35b142026-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 上 gemm crate 或 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.jsonsafetensors 与 mmap
同一模型的 GGUF(bartowski 转换)SmolLM2-135M-Instruct-Q8_0.gguf、Q4_K_M.gguf生成一个 token、06 量化、量化 llama 的一次前向
bert-base-chinesemodel.safetensors、config.json、tokenizer.json、vocab.txtBERT、10 在本站中

hf-hub 不用来下载,模型一律从本地路径读。

各部分

  1. 00 导读:锁定的版本,三条主线每一步落在哪个函数、哪一页讲;feature 各打开什么,没开时的 dummy 后端;Result 无处不在的错误风格;crate 分工和规模,.metal 源码放在哪、怎么加载。
  2. 01 全景:candle 和 burn、tch-rs、PyTorch 的分界;一次 matmul、生成一个 token 各从头到尾走一遍,带调用栈和分阶段计时,查清 Metal 第一次为什么慢。
  3. 02 张量:Tensor 的两层 Arc、Layout 与视图、DType 与 Device、错误。
  4. 03 后端:Storage 枚举和后端 trait,CPU 后端,Metal 后端,自定义算子。
  5. 04 自动微分:计算图怎么记下来,backward 与 GradStore,优化器与训练循环。
  6. 05 神经网络层:VarBuilder、safetensors 与 mmap、常用层与融合算子、KV 缓存与注意力。
  7. 06 量化:GGUF 文件、量化格式、QTensor 与 QMatMul。
  8. 07 模型:BERT、量化 llama 的一次前向、取样。
  9. 08 设计取舍:枚举后端、eager 不融合、Result 到处都是、手写内核,和 burn 集中对照。
  10. 09 常见坑:用报错原文或现象做标题,每个都先跑出最小复现。
  11. 10 在本站中:本站不用 candle;用 bert-base-chinese 给本站的文章算句向量,和 burn 卷的 TF-IDF 对照。
  12. 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 卷)。

💬 关联讨论

发起讨论
加载中...