1use ferrox_gguf::{ShardedGguf, TensorSource};
25
26use crate::bert_encoder::{BertEncoder, BertHparams, BertLayer};
27use crate::loader::{
28 assert_every_tensor_consumed, load_f32_vec, load_f32_vec_optional, load_weight_matrix,
29 LoadError,
30};
31use crate::pooling::PoolingType;
32
33pub const BERT_ARCH: &str = "bert";
35
36const DEFAULT_CLS_ID: u32 = 101;
39const DEFAULT_SEP_ID: u32 = 102;
40
41fn meta_u64(file: &impl TensorSource, key: &str) -> Result<u64, LoadError> {
42 file.metadata_u64(key)
43 .ok_or_else(|| LoadError::MissingHparam(key.to_string()))
44}
45
46fn refuse(what: &str) -> LoadError {
47 LoadError::UnsupportedFeature(BERT_ARCH.to_string(), what.to_string())
48}
49
50fn reject_tensor(file: &ShardedGguf, name: &str, why: &str) -> Result<(), LoadError> {
52 if file.find_tensor(name).is_some() {
53 return Err(refuse(&format!("checkpoint carries '{name}': {why}")));
54 }
55 Ok(())
56}
57
58pub fn check_arch(arch: &str) -> Result<(), LoadError> {
61 if arch == BERT_ARCH {
62 Ok(())
63 } else {
64 Err(LoadError::UnsupportedArchitecture(arch.to_string()))
65 }
66}
67
68pub fn read_bert_hparams(file: &impl TensorSource) -> Result<BertHparams, LoadError> {
71 let arch = file
72 .metadata_str("general.architecture")
73 .ok_or_else(|| LoadError::MissingHparam("general.architecture".into()))?
74 .to_string();
75 check_arch(&arch)?;
76 let p = |suffix: &str| format!("{arch}.{suffix}");
77
78 let n_layer = meta_u64(file, &p("block_count"))? as usize;
79 let n_embd = meta_u64(file, &p("embedding_length"))? as usize;
80 let n_ff = meta_u64(file, &p("feed_forward_length"))? as usize;
81 let n_head = meta_u64(file, &p("attention.head_count"))? as usize;
82 let n_head_kv = file
83 .metadata_u64(&p("attention.head_count_kv"))
84 .unwrap_or(n_head as u64) as usize;
85 let n_ctx_train = meta_u64(file, &p("context_length"))? as usize;
86
87 let layer_norm_eps = file
92 .metadata_f32(&p("attention.layer_norm_epsilon"))
93 .ok_or_else(|| LoadError::MissingHparam(p("attention.layer_norm_epsilon")))?;
94
95 let n_token_types = meta_u64(file, "tokenizer.ggml.token_type_count")? as usize;
98 if n_token_types == 0 {
99 return Err(refuse("tokenizer.ggml.token_type_count is 0"));
100 }
101
102 if file.metadata_bool(&p("attention.causal")).unwrap_or(false) {
105 return Err(refuse(
106 "bert.attention.causal is true, but this graph applies no mask — \
107 a causal BERT would need a decoder path",
108 ));
109 }
110
111 if n_head == 0 || n_head_kv == 0 || !n_head.is_multiple_of(n_head_kv) {
112 return Err(refuse(&format!(
113 "head_count {n_head} is not a multiple of head_count_kv {n_head_kv}"
114 )));
115 }
116 if !n_embd.is_multiple_of(n_head) {
117 return Err(refuse(&format!(
118 "embedding_length {n_embd} is not divisible by head_count {n_head}"
119 )));
120 }
121 if file.metadata_u64(&p("expert_count")).unwrap_or(0) != 0
122 || file.metadata_u64(&p("moe_every_n_layers")).unwrap_or(0) != 0
123 {
124 return Err(refuse(
125 "expert layers (nomic-bert-moe's moe_every_n_layers) are not implemented",
126 ));
127 }
128
129 let pooling = PoolingType::from_gguf(file, &arch)
133 .map_err(|e| refuse(&e.to_string()))?
134 .unwrap_or(PoolingType::None);
135
136 let cls_id = file
137 .metadata_u64("tokenizer.ggml.bos_token_id")
138 .unwrap_or(u64::from(DEFAULT_CLS_ID)) as u32;
139 let sep_id = file
140 .metadata_u64("tokenizer.ggml.seperator_token_id")
141 .unwrap_or(u64::from(DEFAULT_SEP_ID)) as u32;
142
143 Ok(BertHparams {
144 arch,
145 n_layer,
146 n_embd,
147 n_ff,
148 n_head,
149 n_head_kv,
150 n_ctx_train,
151 n_token_types,
152 layer_norm_eps,
153 pooling,
154 cls_id,
155 sep_id,
156 })
157}
158
159pub fn load_bert_encoder_from_path(
161 path: impl AsRef<std::path::Path>,
162) -> Result<BertEncoder, LoadError> {
163 load_bert_encoder(&ShardedGguf::open(path.as_ref())?)
164}
165
166pub fn load_bert_encoder(file: &ShardedGguf) -> Result<BertEncoder, LoadError> {
169 let hp = read_bert_hparams(file)?;
170
171 let tok_embd = load_weight_matrix(file, "token_embd.weight")?;
172 let pos_embd = load_weight_matrix(file, "position_embd.weight")?;
173 if pos_embd.rows() != hp.n_ctx_train {
174 return Err(refuse(&format!(
175 "position_embd.weight has {} rows but {}.context_length says {} — the learned \
176 position table and the advertised context disagree",
177 pos_embd.rows(),
178 hp.arch,
179 hp.n_ctx_train
180 )));
181 }
182 if pos_embd.cols() != hp.n_embd || tok_embd.cols() != hp.n_embd {
183 return Err(refuse(&format!(
184 "embedding tables are {} / {} wide but embedding_length is {}",
185 tok_embd.cols(),
186 pos_embd.cols(),
187 hp.n_embd
188 )));
189 }
190
191 let type_embd = match file.find_tensor("token_types.weight") {
202 Some(_) => {
203 let table = load_weight_matrix(file, "token_types.weight")?;
204 if table.rows() != hp.n_token_types || table.cols() != hp.n_embd {
205 return Err(refuse(&format!(
206 "token_types.weight is {}x{}, expected {}x{}",
207 table.rows(),
208 table.cols(),
209 hp.n_token_types,
210 hp.n_embd
211 )));
212 }
213 Some((0..table.rows()).map(|r| table.dequant_row(r)).collect())
214 }
215 None => None,
216 };
217
218 let tok_norm_w = load_f32_vec(file, "token_embd_norm.weight")?;
219 let tok_norm_b = load_f32_vec(file, "token_embd_norm.bias")?;
220
221 let mut layers = Vec::with_capacity(hp.n_layer);
222 for l in 0..hp.n_layer {
223 let b = format!("blk.{l}");
224 reject_tensor(
225 file,
226 &format!("{b}.attn_qkv.weight"),
227 "a fused QKV projection; this graph reads separate attn_q/attn_k/attn_v",
228 )?;
229 reject_tensor(
230 file,
231 &format!("{b}.attn_q_norm.weight"),
232 "per-projection QK normalization (jina-bert-v3 / neo-bert), not implemented",
233 )?;
234 reject_tensor(
235 file,
236 &format!("{b}.attn_k_norm.weight"),
237 "per-projection QK normalization (jina-bert-v3 / neo-bert), not implemented",
238 )?;
239 reject_tensor(
240 file,
241 &format!("{b}.attn_norm_2.weight"),
242 "jina-bert-v2's second attention norm, not implemented",
243 )?;
244 reject_tensor(
245 file,
246 &format!("{b}.ffn_gate.weight"),
247 "a gated FFN (nomic-bert / jina-bert-v2 GEGLU); this graph runs a plain GELU MLP",
248 )?;
249 reject_tensor(
250 file,
251 &format!("{b}.ffn_up_exps.weight"),
252 "MoE expert tensors (nomic-bert-moe), not implemented",
253 )?;
254
255 layers.push(BertLayer {
256 wq: load_weight_matrix(file, &format!("{b}.attn_q.weight"))?,
257 bq: load_f32_vec_optional(file, &format!("{b}.attn_q.bias"))?,
258 wk: load_weight_matrix(file, &format!("{b}.attn_k.weight"))?,
259 bk: load_f32_vec_optional(file, &format!("{b}.attn_k.bias"))?,
260 wv: load_weight_matrix(file, &format!("{b}.attn_v.weight"))?,
261 bv: load_f32_vec_optional(file, &format!("{b}.attn_v.bias"))?,
262 wo: load_weight_matrix(file, &format!("{b}.attn_output.weight"))?,
263 bo: load_f32_vec_optional(file, &format!("{b}.attn_output.bias"))?,
264 attn_out_norm_w: load_f32_vec(file, &format!("{b}.attn_output_norm.weight"))?,
265 attn_out_norm_b: load_f32_vec(file, &format!("{b}.attn_output_norm.bias"))?,
266 ffn_up: load_weight_matrix(file, &format!("{b}.ffn_up.weight"))?,
267 ffn_up_b: load_f32_vec_optional(file, &format!("{b}.ffn_up.bias"))?,
268 ffn_down: load_weight_matrix(file, &format!("{b}.ffn_down.weight"))?,
269 ffn_down_b: load_f32_vec_optional(file, &format!("{b}.ffn_down.bias"))?,
270 layer_out_norm_w: load_f32_vec(file, &format!("{b}.layer_output_norm.weight"))?,
271 layer_out_norm_b: load_f32_vec(file, &format!("{b}.layer_output_norm.bias"))?,
272 });
273 }
274
275 let kv_dim = hp.n_head_kv * hp.head_dim();
276 for (l, layer) in layers.iter().enumerate() {
277 for (name, m, rows) in [
278 ("attn_q", &layer.wq, hp.n_embd),
279 ("attn_k", &layer.wk, kv_dim),
280 ("attn_v", &layer.wv, kv_dim),
281 ("attn_output", &layer.wo, hp.n_embd),
282 ("ffn_up", &layer.ffn_up, hp.n_ff),
283 ("ffn_down", &layer.ffn_down, hp.n_embd),
284 ] {
285 if m.rows() != rows {
286 return Err(refuse(&format!(
287 "blk.{l}.{name}.weight has {} output rows, expected {rows}",
288 m.rows()
289 )));
290 }
291 }
292 }
293
294 assert_every_tensor_consumed(file)?;
295
296 Ok(BertEncoder {
297 hp,
298 tok_embd,
299 type_embd,
300 pos_embd,
301 tok_norm_w,
302 tok_norm_b,
303 layers,
304 })
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
315 fn a_non_bert_architecture_is_refused_by_name() {
316 for arch in [
317 "nomic-bert",
318 "nomic-bert-moe",
319 "jina-bert-v2",
320 "jina-bert-v3",
321 "neo-bert",
322 "modern-bert",
323 "llama",
324 ] {
325 let err = check_arch(arch).unwrap_err();
326 assert!(
327 matches!(&err, LoadError::UnsupportedArchitecture(a) if a == arch),
328 "{arch} was not refused: {err}"
329 );
330 }
331 assert!(check_arch(BERT_ARCH).is_ok());
332 }
333}