Rust in Everything
基于 candle 0.11.0

unknown variant quick_gelu

主线位置:推理主线的第一步「读 config」。config.json 的 hidden_act 字符串反序列化成激活函数。

版本:PyTorch 2.15.0 / transformers 5.22.0(CPU)、candle 0.11.0(CPU,accelerate;Metal)
flowchart TB
  cfg["config.json"] --> name["hidden_act 字符串"]
  name -->|transformers| fn["ACT2FN 查表"]
  name -->|"candle_nn Activation"| de{"认识吗"}
  de -->|"gelu silu ..."| ok["Activation"]
  de -->|"quick_gelu"| e1["unknown variant"]
  de -->|"leakyrelu"| e2["expected newtype"]
  name -->|"HfAct 包一层"| all["都能解析"]

本页的对照:PyTorch 侧是实验代码里的 p14.py,candle 侧是实验 crate pita-t 的 p14_act(features metal accelerate,release)。平台 M2 Max、macOS 27、rustc 1.99.0。

PyTorch 的写法

transformers 的模型按 config.hidden_act 在 ACT2FN 表里取激活函数:

from transformers.activations import ACT2FN
for name in ["quick_gelu", "leaky_relu", "gelu", "silu"]:
  f = ACT2FN[name]
quick_gelu  -> QuickGELUActivation
leaky_relu  -> LeakyReLU
gelu        -> GELUActivation
silu        -> SiLUActivation

任何在表里的名字都能用。quick_gelu 是 transformers 5.22.0 里 CLIPTextConfig 和 CLIPVisionConfig 的默认 hidden_act,QuickGELUActivation 是 input * torch.sigmoid(1.702 * input);leaky_relu 是不带参数的 nn.LeakyReLU,斜率 0.01。

搬到 candle 的现象

手写模型时照 transformers 的 PretrainedConfig 写一个 serde 结构体,hidden_act 用 candle-nn 现成的枚举:

#[derive(Debug, Deserialize)]
struct Config {
  hidden_size: usize,
  hidden_act: Activation,   // candle_nn::Activation
}
serde_json::from_str::<Config>(cfg)
{"hidden_size": 512, "hidden_act": "quick_gelu"}
  -> Err("unknown variant `quick_gelu`, expected one of `gelu`, `gelu_new`, `newgelu`, `relu`, `relu2`, `relu6`, `silu`, `sigmoid`, `hardsigmoid`, `swiglu`, `swish`, `mish`, `hardswish`, `elu`, `leakyrelu`, `gelu_pytorch_tanh`, `gelupytorchtanh` at line 1 column 47")
{"hidden_size": 512, "hidden_act": "leaky_relu"}
  -> Err("unknown variant `leaky_relu`, expected one of `gelu`, `gelu_new`, `newgelu`, `relu`, `relu2`, `relu6`, `silu`, `sigmoid`, `hardsigmoid`, `swiglu`, `swish`, `mish`, `hardswish`, `elu`, `leakyrelu`, `gelu_pytorch_tanh`, `gelupytorchtanh` at line 1 column 47")
{"hidden_size": 512, "hidden_act": "leakyrelu"}
  -> Err("invalid type: unit variant, expected newtype variant at line 1 column 47")
{"hidden_size": 512, "hidden_act": {"leakyrelu": 0.01}}
  -> Ok(LeakyRelu(0.01))

CPU 和 Metal 相同(这一步不碰张量)。整个 config 读不进来,模型建不起来。好处是它不会静默换成别的函数;坏处是报错只说「不认识」,leakyrelu 那一行的「unit variant / newtype variant」还要读者懂 serde 的术语。

原因

candle_nn::Activation 是一个 #[serde(rename_all = "lowercase")] 的枚举,只给 gelu、gelu_new、gelu_pytorch_tanh 三个加了 alias:

#[derive(Debug, Clone, Copy, PartialEq, serde::Deserialize, serde::Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Activation {
    #[default]
    #[serde(alias = "gelu")]
    Gelu,
// ...
    Elu(f64),
    LeakyRelu(f64),
    #[serde(alias = "gelu_pytorch_tanh")]
    GeluPytorchTanh,
}
  • 没有 QuickGelu 变体,所以 quick_gelu 不认识。
  • LeakyRelu 小写之后是 leakyrelu,没有下划线,transformers 的 leaky_relu 对不上。
  • LeakyRelu(f64) 带一个参数,serde 的外部标记格式要求写成 {"leakyrelu": 0.01};只给字符串,serde 认出了变体名,却缺参数,报「期望 newtype variant」。

transformers 5.22.0 的 24 个激活名字里,Activation 能解析 11 个、语义都对,另外 13 个报 unknown variant,完整的表见常用层的「hidden_act 字符串对 Activation」。candle-transformers 的 CLIP 自己定义了一个只有 QuickGelu 的 Activation,但它没有实现 Deserialize,CLIP 的配置是在代码里写死的,帮不上读 config.json 的忙。origin/main 上 activation.rs 没有改。

改法

包一层枚举:先列 Activation 缺的几个名字,最后一个变体用 #[serde(untagged)] 把剩下的交给 Activation:

#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
enum HfAct {
  QuickGelu,
  LeakyRelu,
  #[serde(untagged)]
  Candle(Activation),
}

impl Module for HfAct {
  fn forward(&self, xs: &Tensor) -> candle_core::Result<Tensor> {
    match self {
      // transformers QuickGELUActivation: input * torch.sigmoid(1.702 * input)
      Self::QuickGelu => xs * candle_nn::ops::sigmoid(&(xs * 1.702)?)?,
      // transformers maps "leaky_relu" to nn.LeakyReLU(), default slope 0.01
      Self::LeakyRelu => candle_nn::ops::leaky_relu(xs, 0.01),
      Self::Candle(a) => a.forward(xs),
    }
  }
}

在 [-6, 6] 上取 1201 个点,和 transformers 的 ACT2FN[name] 比:

== cpu
quick_gelu  -> QuickGelu: max|diff| vs transformers 2.980e-8
leaky_relu  -> LeakyRelu: max|diff| vs transformers 0.000e0
gelu        -> Candle(Gelu): max|diff| vs transformers 4.768e-7
silu        -> Candle(Silu): max|diff| vs transformers 2.384e-7
unknown: Err("data did not match any variant of untagged enum HfAct")
== metal
quick_gelu  -> QuickGelu: max|diff| vs transformers 4.768e-7
leaky_relu  -> LeakyRelu: max|diff| vs transformers 0.000e0
gelu        -> Candle(Gelu): max|diff| vs transformers 2.384e-7
silu        -> Candle(Silu): max|diff| vs transformers 9.537e-7

(Metal 的最后一行同 CPU。)差都在 f32 的舍入范围内(输出最大约 6,f32 在那里一个 ulp 是 4.8e-7);leaky_relu 只是乘 0.01 或原样返回,差 0。gelu、silu 这些已有的名字照旧落到 Candle(..)。真正不认识的名字(这里试的是 tanh)仍然报错,只是报错变成了 untagged 枚举的笼统说法;需要更清楚的报错,就把 hidden_act 先读成 String,自己 match 再给出名字。#[serde(untagged)] 用在单个变体上是较新的 serde 才有的写法,本页用的是 cargo 缓存里的 serde 1.0.229。

怎么检查

  • 新模型第一次加载,先只反序列化 config.json,确认 hidden_act 解析出来的是哪个变体({:?} 打出来),再和 transformers 的 type(ACT2FN[name]).__name__ 对一下。
  • 带参数的激活(leaky_relu、elu)在 transformers 里用的是默认参数,搬过来时把默认值写进代码,不要指望 config 里有。
  • 用 candle-transformers 里现成的模型时,看它自己的激活枚举认哪些名字;BERT 的 HiddenAct 只认 gelu、geluapproximate、relu 三个。

小结

  • candle_nn::Activation 不认识 quick_gelu(CLIP 的默认值)、leaky_relu 等 13 个 transformers 名字,报 unknown variant;"leakyrelu" 要写成 {"leakyrelu": 0.01}。
  • 解析失败会让整个 config 读不进来,不会静默换函数。
  • 改法:外面包一个 HfAct,补上缺的名字,其余用 #[serde(untagged)] 交给 Activation;和 transformers 差 ≤ 9.5e-7。

💬 关联讨论

发起讨论
加载中...