Rust in Everything
基于 candle 0.11.0

搬一个 LLM

这一部分是推理主线的中段:模块和权重已经能建、能加载(04、05),这里把 SmolLM2-135M 整个跑起来,和 transformers 的 LlamaForCausalLM 对 logits,再带上 KV 缓存,最后换成半精度。candle 一侧用的是现成的 candle_transformers::models::llama,不自己写前向;自己写一遍见 从头写 Llama。

两边对得上的地方很多:模块结构、权重键名、RoPE 的配对方式、GQA 的头复制顺序都一样,f32 logits 差 3e-5。不同的地方集中在「transformers 替你管、candle 交给调用方」的几件事上:config 里哪些键被读、位置 index_pos、缓存什么时候换新的、Cache 的 dtype。

  1. 对齐 logits:config 映射、模块和键名、RoPE、GQA、因果 mask、只算最后一个位置,一段段对;transformers 的 eager 和 sdpa 互差 3.3e-5,candle 和它们差 3e-5 到 4.3e-5;transformers 5 存出来的 config 没有顶层 rope_theta,candle 静默用 10000。
  2. KV 缓存:DynamicCache 对 Cache,transformers 从缓存长度推位置,candle 要调用方传 index_pos;带缓存和不带缓存的 logits 差 6e-5 到 8e-5,两边都不是 0;分段 prefill;换 prompt 不换缓存时的报错和静默串题。
  3. 半精度:candle CPU 不能用 BF16 跑,F16 要关 accelerate 且累加是 f16;Metal 上 F16 差 0.045、BF16 差 0.60,和 torch 同一量级;bf16 贪心生成在一个只领先 0.094 的位置上分叉,是舍入问题,不是实现问题。
flowchart TB
  cfg["config.json"] --> load["Llama load"]
  w["bf16 权重"] -->|"VarBuilder dtype"| load
  load --> fwd["forward"]
  idx["index_pos"] --> fwd
  cache["Cache"] --> fwd
  fwd -->|"存回 k v"| cache
  fwd --> lg["F32 logits"]
  lg --> next["下一个 token"]
  next -->|"pos 加 1"| idx

candle 内部怎么跑一个 token、缓存怎么存,见源码卷的生成一个 token和 KV 缓存与注意力。

💬 关联讨论

发起讨论
加载中...