Rust in Everything
基于 candle-core 0.11.0

A weight is negative, too large or not a valid number

主线位置:推理主线的最后一步,logits 交给 LogitsProcessor 取下一个 token。各种 Sampling 怎么取样见 取样。代码在 candle-transformers 的 generation/mod.rs。

flowchart TB
  cli["温度 0 来自命令行"] --> fs["from_sampling TopK t=0"]
  fs --> div["logits 除以 0"]
  div --> inf["正数变 inf"]
  inf --> nan["softmax 里 inf 减 inf 得 NaN"]
  nan --> rand["WeightedIndex::new 报错"]
  cli --> fix["t 不大于 0 映射成 ArgMax"]

实验 crate exp-candle/pb-pit,bin temp0,SmolLM2-135M Q8_0 GGUF 对「What is Rust?」prefill 之后的真实 logits,CPU,release,M2 Max、macOS 27、rustc 1.99.0。

现象

命令行有 --temperature 和 --top-k 两个参数,用户想要确定性的输出,传了 --temperature 0:

/// What a CLI typically does with `--temperature` and `--top-k`.
fn sampling(temperature: f64, top_k: Option<usize>, guard: bool) -> Sampling {
  if guard && temperature <= 0.0 {
    return Sampling::ArgMax;
  }
  match top_k {
    None => Sampling::All { temperature },
    Some(k) => Sampling::TopK { k, temperature },
  }
}

let mut lp = LogitsProcessor::from_sampling(42, sampling(0.0, Some(40), false));
let next = lp.sample(&logits)?;
Error: A weight is negative, too large or not a valid number
exit=1

报错里没有「temperature」,也没有 candle 的任何字样。再看看中间发生了什么,以及温度要多小才出事(probe):

49152 logits: 48582 positive, max 31.949
logits / 0.0: max inf; softmax: 49152 NaN of 49152
All { temperature: 1e-3 }: Ok(66)
All { temperature: 1e-7 }: Ok(66)
All { temperature: 1e-30 }: Ok(66)
All { temperature: 1e-38 }: Err("A weight is negative, too large or not a valid number")
LogitsProcessor::new(42, Some(0.0), None): Ok(66)
  • 除以 0 之后最大值是 inf,softmax 先减最大值,inf - inf 是 NaN,整行 49152 个概率全是 NaN。
  • 很小但不是 0 的温度没事,直到 31.9 / 1e-38 超出 f32 的范围(约 3.4e38)又变成 inf。
  • 老接口 new 传 Some(0.0) 不出错,返回 66(「R」,和贪心一样)。

出处

sample_f 对 All、TopK、TopP、TopKThenTopP 都先算:

        let prs = |temperature: f64| -> Result<Vec<f32>> {
            let logits = (&logits / temperature)?;
            let prs = candle_nn::ops::softmax_last_dim(&logits)?;
            let mut prs = prs.to_vec1()?;
            f(&mut prs);
            Ok(prs)
        };

没有检查温度。NaN 的概率一路传到 sample_multinomial,rand 0.9 的 WeightedIndex::new 返回 InvalidWeight,Error::wrap 原样包起来,显示的就是 rand 的那句话。TopK 先排序截断再取样,排序用 total_cmp,NaN 不会让它 panic,照样走到 WeightedIndex。

温度 0 的保护只在 LogitsProcessor::new 里:

    pub fn new(seed: u64, temperature: Option<f64>, top_p: Option<f64>) -> Self {
        let temperature = temperature.and_then(|v| if v < 1e-7 { None } else { Some(v) });
        let sampling = match temperature {
            None => Sampling::ArgMax,

from_sampling 原样收下 Sampling。只有 GumbelSoftmax 在 candle-nn 的 gumbel_softmax 里自己处理了 temperature <= 0.0(直接 argmax)。而 new 造不出 TopK,要用 top-k 就只能走 from_sampling。

📝NOTE
冷知识:保护留在了老接口里

#848(「Use softmax-last-dim in the quantized example.」,2023-09)给 LogitsProcessor::new 加了 v < 1e-7 就当作没有温度的判断,那时 sample 只看 self.temperature: Option<f64>,None 走 argmax,所以所有入口都受保护。半年后 #1923(「Add topk sampling.」)把两个字段换成了 Sampling 枚举,新增 from_sampling(seed, sampling),new 改成先做同样的判断、再构造 Sampling 交给 from_sampling。判断留在了 new 里,from_sampling 和枚举本身都不检查,TopK 又只能从 from_sampling 进来。#1923 提交时工作区版本是 0.4.2,之后的 #2009 把版本号改成 0.5.0,所以首次发布应是 0.5.0(本地仓库没有 0.5.0 的 tag,能查到的最早包含它的 tag 是 0.5.1)。两个提交都只有标题。

修法

在构造 Sampling 的地方自己把不大于 0 的温度映射成 ArgMax(上面 sampling 函数的 guard):

for (t, k) in [(0.0, Some(40)), (0.0, None), (0.8, Some(40))] {
  let mut lp = LogitsProcessor::from_sampling(42, sampling(t, k, true));
  let next = lp.sample(&logits)?;
  println!("temperature {t}, top_k {k:?}: next token {next} {:?}", tok.decode(&[next], false).map_err(E::msg)?);
}
temperature 0, top_k Some(40): next token 66 "R"
temperature 0, top_k None: next token 66 "R"
temperature 0.8, top_k Some(40): next token 66 "R"

candle 自带的 candle-examples/examples/quantized/main.rs 也是这样写的:temperature <= 0. 时用 Sampling::ArgMax。阈值用 <= 0.0 还是 new 的 < 1e-7 都行;上面的实验说明 1e-7 到 1e-30 之间并不会出错,只是和贪心没有区别。

怎么确认没中招

  • 搜 from_sampling(,看每处构造 Sampling 的温度能不能是 0;命令行、配置文件、HTTP 请求里来的温度都要过一遍这个映射。
  • 看到 A weight is negative, too large or not a valid number,先打印传进去的 Sampling,再检查 logits 里有没有 NaN / inf(模型本身算出 NaN 时也是这句话)。
  • 写一个温度为 0 的单元测试:取样结果应当等于 logits.argmax(0)。

小结

  • from_sampling 不检查温度;温度 0 让 logits 变成无穷、softmax 变成整行 NaN,rand 的 WeightedIndex 报 A weight is negative, too large or not a valid number。
  • 只有 LogitsProcessor::new 把小于 1e-7 的温度换成 ArgMax,但它造不出 TopK;GumbelSoftmax 自己处理了温度 0。
  • 构造 Sampling 时把不大于 0 的温度映射成 Sampling::ArgMax。

💬 关联讨论

发起讨论
加载中...