搬一个 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。
- 对齐 logits:config 映射、模块和键名、RoPE、GQA、因果 mask、只算最后一个位置,一段段对;transformers 的
eager和sdpa互差 3.3e-5,candle 和它们差 3e-5 到 4.3e-5;transformers 5 存出来的 config 没有顶层rope_theta,candle 静默用 10000。 - KV 缓存:
DynamicCache对Cache,transformers 从缓存长度推位置,candle 要调用方传index_pos;带缓存和不带缓存的 logits 差 6e-5 到 8e-5,两边都不是 0;分段 prefill;换 prompt 不换缓存时的报错和静默串题。 - 半精度: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 缓存与注意力。
💬 关联讨论
发起讨论加载中...