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。