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)
}
pub fn has_pooler(&self) -> bool {
self.dense.is_some()
}
pub fn graph(&self) -> String {
let mut g = "cls".to_string();
if self.dense.is_some() {
g = format!("tanh(pooler({g}))");
}
if self.norm.is_some() {
g = format!("norm({g})");
}
if self.out.is_some() {
g = format!("classifier({g})");
}
g
}
}
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"
))),
}
}
fn missing_pooler_note(has_dense: bool, has_out: bool, labels: &[String]) -> Option<String> {
if has_dense || !has_out || labels.is_empty() {
return None;
}
Some(format!(
"ferrox: NOTE this reranker head runs classifier(cls) — it carries {CLS_OUT_W} and \
labels ({}) but no {CLS_W}. A HuggingFace BertForSequenceClassification was trained \
as classifier(tanh(pooler(cls))), and llama.cpp's converter deletes pooler.dense by \
name (conversion/bert.py, \"we are only using BERT for embeddings so we don't need \
the pooling layer\"), so this file cannot carry it. ferrox runs the pooler whenever \
a file DOES carry it and will not invent one. The ranking is the checkpoint's; the \
score SCALE is not (about ±0.2 rather than about ±11 on \
ms-marco-MiniLM-L6-v2), so an absolute relevance threshold will not fire. See \
ferrox issue #82.",
labels.join(", "),
))
}
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"
)));
}
if let Some(note) = missing_pooler_note(dense.is_some(), out.is_some(), &labels) {
eprintln!("{note}");
}
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 the_graph_names_a_tanh_exactly_when_a_tanh_is_applied() {
let out = || {
Some(Dense {
w: WeightMatrix::F32(Tensor::new(vec![1.0, -0.5], vec![1, 2])),
b: None,
})
};
let pooler = || {
Some(Dense {
w: WeightMatrix::F32(Tensor::new(vec![0.3, 0.7, -0.2, 0.4], vec![2, 2])),
b: None,
})
};
let x = [0.5f32, 0.25];
let two_x = [1.0f32, 0.5];
for (head, want_graph, want_pooler) in [
(
RankHead {
dense: pooler(),
norm: None,
out: out(),
labels: vec![],
eps: 1e-12,
},
"classifier(tanh(pooler(cls)))",
true,
),
(
RankHead {
dense: None,
norm: None,
out: out(),
labels: vec![],
eps: 1e-12,
},
"classifier(cls)",
false,
),
] {
assert_eq!(head.graph(), want_graph);
assert_eq!(head.has_pooler(), want_pooler);
assert_eq!(head.graph().contains("tanh"), head.has_pooler());
let linear = (head.score(&two_x) - 2.0 * head.score(&x)).abs() < 1e-6;
assert_eq!(
linear,
!want_pooler,
"{want_graph} was {} but its graph says otherwise: f(x)={}, f(2x)={}",
match linear {
true => "linear",
false => "non-linear",
},
head.score(&x),
head.score(&two_x),
);
}
}
#[test]
fn the_missing_pooler_note_fires_only_on_a_labelled_head_with_no_pooler() {
let labelled = vec!["LABEL_0".to_string()];
for has_dense in [false, true] {
for has_out in [false, true] {
for labels in [&[][..], &labelled[..]] {
let got = missing_pooler_note(has_dense, has_out, labels);
let want = !has_dense && has_out && !labels.is_empty();
assert_eq!(
got.is_some(),
want,
"dense={has_dense} out={has_out} labels={labels:?} produced {got:?}"
);
}
}
}
let note = missing_pooler_note(false, true, &labelled).expect("the note fires");
assert!(note.contains("LABEL_0"), "{note}");
assert!(note.contains("#82"), "{note}");
}
#[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");
}
}