Rust in Everything
基于 candle 0.11.0

rope_theta 静默变成 10000

主线位置:推理主线第一步「读 config」,config.json 是 transformers 5 存出来的(自己微调后 save_pretrained,或者下载的新模型)。

版本:PyTorch 2.15.0 / transformers 5.22.0(CPU)、candle 0.11.0(CPU,accelerate)
flowchart TB
  old["4.x 的 config"] -->|"rope_theta 在顶层"| ok["100000"]
  old --> tf["transformers 5 读入"]
  tf --> sv["save_pretrained"]
  sv --> rp["rope_parameters 里"]
  rp --> serde["LlamaConfig serde"]
  serde --> def["default_rope 10000"]
  rp -->|"自己补"| ok

实验代码:PyTorch 侧 resave_cfg.py,candle 侧 crate pitb-exp 的 bin p20_rope(release)。权重是本地的 SmolLM2-135M-Instruct。平台 M2 Max、macOS 27、rustc 1.99.0。candle 的 LlamaConfig 读哪些键、写死哪些,见对齐 logits的 config 一节。

PyTorch 的写法

微调完 model.save_pretrained(out),或者只存 config:

orig = json.load(open(f"{model_dir}/config.json"))
cfg = AutoConfig.from_pretrained(model_dir)
cfg.save_pretrained(out)
saved = json.load(open(f"{out}/config.json"))
print("reload:  ", AutoConfig.from_pretrained(out).rope_parameters)
original: {'rope_interleaved': False, 'rope_scaling': None, 'rope_theta': 100000} transformers_version 4.42.3
cfg.rope_parameters: {'rope_theta': 100000, 'rope_type': 'default'}
resaved:  {'rope_interleaved': False, 'rope_parameters': {'rope_theta': 100000, 'rope_type': 'default'}} transformers_version 5.22.0
reload:   {'rope_theta': 100000, 'rope_type': 'default'}

(打印的是名字里带 rope 的键。)原始文件是 transformers 4.42.3 写的,rope_theta 在顶层;transformers 5.22.0 读进来时把 rope_theta、rope_scaling 合并成 rope_parameters,存回去就只有 rope_parameters。transformers 自己两种都读得懂,用户感觉不到这次改写。

搬到 candle 的现象

用重存的 config.json、原来的权重,照常加载和生成:

let cfg: LlamaConfig = serde_json::from_slice(&std::fs::read(&path)?)?;
let cfg = cfg.into_config(false);
println!("plain serde: rope_theta={}", cfg.rope_theta);
let model = Llama::load(st_vb(DType::F32, &dev)?, &cfg)?;
let (logits, greedy) = run(&model, &cfg, DType::F32, &dev)?;
plain serde: rope_theta=10000
plain serde: logits max|diff|=6.10e-1 (max|ref| 20.6), greedy same prefix 18/20 [7042, 30, 7042, 314, 260, 3995, 2240, 281, 4649, 284, 260, 3575, 282, 260, 3432, 6151, 282, 260, 5761, 599]

没有任何报错或警告。logits 和 transformers f32 差 0.61,20 个贪心 token 前 18 个相同,最后两个变了。prompt 只有 24 个位置,RoPE 的低频分量还没转开,所以只差一点;在长文本上差得更多(对齐 logits 在 249 个 token 上量过,16% 的位置 argmax 不同)。

原因

candle 0.11.0 的 LlamaConfig 是一个普通的 serde 结构体,rope_theta 缺省时取 default_rope:

    #[serde(default = "default_rope")]
    pub rope_theta: f32,
// ...
fn default_rope() -> f32 {
    10_000.0
}

结构体里没有 rope_parameters 字段,serde 默认忽略不认识的键,于是 rope_parameters 被丢掉、rope_theta 用默认值。10000 是原版 Llama 的值(config_7b_v1 里也写的是它),SmolLM2 用的是 100000。同样的事发生在 Llama 3 的 rope_scaling 上(rope_type: llama3 那一组也进了 rope_parameters,candle 读到 None)。candle-transformers 0.11.0 里只有 gemma4 的 config 读 rope_parameters;origin/main 上也一样,llama.rs 没有改。这是上游没有跟上 transformers 5 的 config 格式。

改法

先读成 serde_json::Value,顶层没有 rope_theta 时从 rope_parameters 里取,再填回 LlamaConfig(它的字段都是 pub)。只认 rope_type 为 default 的情况,其他类型直接报错,不要静默套用:

fn read_config(path: &str) -> Result<Config> {
  let raw: serde_json::Value = serde_json::from_slice(&std::fs::read(path)?)?;
  let mut cfg: LlamaConfig = serde_json::from_value(raw.clone())?;
  if raw.get("rope_theta").is_none() {
    let rp = &raw["rope_parameters"];
    match (rp["rope_type"].as_str(), rp["rope_theta"].as_f64()) {
      (Some("default") | None, Some(theta)) => cfg.rope_theta = theta as f32,
      (t, theta) => bail!("rope_parameters not handled: type {t:?} theta {theta:?}"),
    }
  }
  Ok(cfg.into_config(false))
}
read_config: rope_theta=100000
read_config: logits max|diff|=2.98e-5 (max|ref| 20.6), greedy same prefix 20/20

和用原始 config 一样:差 2.98e-5(最大 |ref| 20.6,容差 1e-4),贪心 20/20。rope_type 是 llama3 的模型要把 factor、low_freq_factor 等搬进 rope_scaling,这里没有实测,所以留成报错。另一个办法是直接用模型仓库里原始的 config.json,但自己微调存出来的目录里只有新格式。

怎么检查

  • 加载后打印 cfg.rope_theta,和 Python 里 AutoConfig.from_pretrained(d).rope_parameters["rope_theta"] 比。这一行能抓住整类问题:candle 只读 13 个键,其他的都可能被静默忽略。
  • 看 config.json 里的 transformers_version:5.x 写的文件里,rope_theta 在 rope_parameters 下面。
  • 对照测试至少要有一个 logits 比较(容差 1e-4),只比「生成的文字通不通顺」抓不到它:rope_theta 错了的模型照样写出流畅的句子。

小结

  • transformers 5.22.0 存 config 时把 rope_theta 挪进 rope_parameters;transformers 自己读得懂。
  • candle 0.11.0 的 LlamaConfig 不认 rope_parameters,rope_theta 静默取默认的 10000:加载、生成都不报错,SmolLM2 的 logits 差 0.61,贪心 18/20。origin/main 没有改。
  • 读 config 时先读成 Value,从 rope_parameters 里补 rope_theta,不认识的 rope_type 报错;补上后差 2.98e-5、贪心 20/20。

💬 关联讨论

发起讨论
加载中...