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。