use ferrox_core::matmul::layer_norm;
use ferrox_core::weight_matrix::WeightMatrix;
use ferrox_gguf::{GgufValue, ShardedGguf, TensorSource};
use crate::loader::{load_f32_vec_optional, load_weight_matrix, LoadError};
const CLS_W: &str = "cls.weight";
const CLS_B: &str = "cls.bias";
const CLS_OUT_W: &str = "cls.output.weight";
const CLS_OUT_B: &str = "cls.output.bias";
const CLS_NORM_W: &str = "cls.norm.weight";
const CLS_NORM_B: &str = "cls.norm.bias";
struct Dense {
w: WeightMatrix,
b: Option<Vec<f32>>,
}
impl Dense {
fn apply(&self, x: &[f32]) -> Vec<f32> {
let mut out = self.w.apply(x);
if let Some(b) = &self.b {
for (o, bv) in out.iter_mut().zip(b.iter()) {
*o += bv;
}
}
out
}
}
pub struct RankHead {
dense: Option<Dense>,
norm: Option<Vec<f32>>,
out: Option<Dense>,
labels: Vec<String>,
eps: f32,
}
impl RankHead {
pub fn n_cls_out(&self) -> usize {
self.labels.len().max(1)
}
pub fn labels(&self) -> &[String] {
&self.labels
}
pub fn apply(&self, pooled: &[f32]) -> Vec<f32> {
let mut cur = pooled.to_vec();
if let Some(dense) = &self.dense {
cur = dense.apply(&cur);
for v in cur.iter_mut() {
*v = v.tanh();
}
if let Some(w) = &self.norm {
let zeros = vec![0.0f32; w.len()];
cur = layer_norm(&cur, w, &zeros, self.eps);
}
}
if let Some(out) = &self.out {
cur = out.apply(&cur);
}
cur
}
pub fn score(&self, pooled: &[f32]) -> f32 {
self.apply(pooled).first().copied().unwrap_or(0.0)
}
}
fn refuse(what: String) -> LoadError {
LoadError::UnsupportedFeature("bert".to_string(), what)
}
fn read_labels(file: &impl TensorSource, arch: &str) -> Result<Vec<String>, LoadError> {
let key = format!("{arch}.classifier.output_labels");
let Some(value) = file.metadata(&key) else {
return Ok(Vec::new());
};
match value {
GgufValue::Array(items) => items
.iter()
.map(|v| match v {
GgufValue::String(s) => Ok(s.clone()),
other => Err(refuse(format!(
"{key} contains a non-string entry {other:?}; it must be an array of \
label names"
))),
})
.collect(),
other => Err(refuse(format!(
"{key} is {other:?}, but it must be an array of strings"
))),
}
}
pub fn load_rank_head(
file: &ShardedGguf,
arch: &str,
n_embd: usize,
eps: f32,
) -> Result<Option<RankHead>, LoadError> {
let has_any = [CLS_W, CLS_OUT_W, CLS_NORM_W]
.iter()
.any(|n| file.find_tensor(n).is_some());
let labels = read_labels(file, arch)?;
if !has_any {
if !labels.is_empty() {
return Err(refuse(format!(
"{arch}.classifier.output_labels names {} label(s) ({}) but the checkpoint \
carries no {CLS_W}, {CLS_OUT_W} or {CLS_NORM_W} — the classification head \
the labels describe is not in this file",
labels.len(),
labels.join(", "),
)));
}
return Ok(None);
}
let dense = match file.find_tensor(CLS_W) {
Some(_) => {
let w = load_weight_matrix(file, CLS_W)?;
if w.cols() != n_embd {
return Err(refuse(format!(
"{CLS_W} takes {} inputs but the encoder is {n_embd} wide",
w.cols()
)));
}
Some(Dense {
b: load_f32_vec_optional(file, CLS_B)?,
w,
})
}
None => None,
};
if file.find_tensor(CLS_NORM_B).is_some() {
return Err(refuse(format!(
"checkpoint carries {CLS_NORM_B}, but upstream's head norm is \
build_norm(cur, cls_norm, NULL, ...) — a weight and no bias. Applying the \
weight and dropping the bias would score wrongly and silently"
)));
}
let norm = load_f32_vec_optional(file, CLS_NORM_W)?;
if norm.is_some() && dense.is_none() {
return Err(refuse(format!(
"checkpoint carries {CLS_NORM_W} but no {CLS_W}; upstream applies the head norm \
only inside the `cls` branch, so this norm would never run"
)));
}
let out = match file.find_tensor(CLS_OUT_W) {
Some(_) => {
let w = load_weight_matrix(file, CLS_OUT_W)?;
let want = dense.as_ref().map(|d| d.w.rows()).unwrap_or(n_embd);
if w.cols() != want {
return Err(refuse(format!(
"{CLS_OUT_W} takes {} inputs but the value reaching it is {want} wide",
w.cols()
)));
}
Some(Dense {
b: load_f32_vec_optional(file, CLS_OUT_B)?,
w,
})
}
None => None,
};
let n_out = match &out {
Some(d) => d.w.rows(),
None => match &dense {
Some(d) => d.w.rows(),
None => n_embd,
},
};
if !labels.is_empty() && labels.len() != n_out {
return Err(refuse(format!(
"{arch}.classifier.output_labels names {} label(s) ({}) but the head produces \
{n_out} output(s) — the metadata and the weights describe different models",
labels.len(),
labels.join(", "),
)));
}
if labels.is_empty() && n_out > 1 {
return Err(refuse(format!(
"this classification head produces {n_out} outputs and the checkpoint carries no \
{arch}.classifier.output_labels, so nothing says which one is the relevance \
score. Refusing rather than reporting output 0 as if it were named"
)));
}
Ok(Some(RankHead {
dense,
norm,
out,
labels,
eps,
}))
}
#[cfg(test)]
mod tests {
use super::*;
use ferrox_core::tensor::Tensor;
fn dense(rows: usize, cols: usize, fill: f32, bias: Option<f32>) -> Dense {
let data: Vec<f32> = (0..rows * cols).map(|i| fill * (i as f32 + 1.0)).collect();
Dense {
w: WeightMatrix::F32(Tensor::new(data, vec![rows, cols])),
b: bias.map(|b| vec![b; rows]),
}
}
#[test]
fn the_head_applies_dense_then_tanh_then_output() {
let head = RankHead {
dense: Some(Dense {
w: WeightMatrix::F32(Tensor::new(vec![1.0, 2.0], vec![1, 2])),
b: Some(vec![0.5]),
}),
norm: None,
out: Some(Dense {
w: WeightMatrix::F32(Tensor::new(vec![3.0], vec![1, 1])),
b: Some(vec![-1.0]),
}),
labels: vec![],
eps: 1e-12,
};
let want = 3.0 * 1.75f32.tanh() - 1.0;
let got = head.score(&[0.25, 0.5]);
assert!(
(got - want).abs() < 1e-6,
"head produced {got}, hand-computed {want}"
);
assert_eq!(head.n_cls_out(), 1);
}
#[test]
fn a_head_with_no_dense_does_not_apply_a_tanh() {
let head = RankHead {
dense: None,
norm: None,
out: Some(Dense {
w: WeightMatrix::F32(Tensor::new(vec![2.0, 0.0], vec![1, 2])),
b: None,
}),
labels: vec![],
eps: 1e-12,
};
assert!((head.score(&[5.0, 1.0]) - 10.0).abs() < 1e-6);
}
#[test]
fn score_is_the_first_output_and_labels_size_the_head() {
let head = RankHead {
dense: None,
norm: None,
out: Some(dense(3, 2, 1.0, None)),
labels: vec!["a".into(), "b".into(), "c".into()],
eps: 1e-12,
};
assert_eq!(head.n_cls_out(), 3);
assert_eq!(head.labels(), ["a", "b", "c"]);
let all = head.apply(&[1.0, 1.0]);
assert_eq!(all.len(), 3);
assert_eq!(head.score(&[1.0, 1.0]), all[0]);
assert_ne!(all[0], all[1], "the fixture must distinguish the outputs");
}
}