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_row0 = match file.find_tensor("token_types.weight") {
197 Some(_) => {
198 let table = load_weight_matrix(file, "token_types.weight")?;
199 if table.rows() != hp.n_token_types || table.cols() != hp.n_embd {
200 return Err(refuse(&format!(
201 "token_types.weight is {}x{}, expected {}x{}",
202 table.rows(),
203 table.cols(),
204 hp.n_token_types,
205 hp.n_embd
206 )));
207 }
208 Some(table.dequant_row(0))
209 }
210 None => None,
211 };
212
213 let tok_norm_w = load_f32_vec(file, "token_embd_norm.weight")?;
214 let tok_norm_b = load_f32_vec(file, "token_embd_norm.bias")?;
215
216 let mut layers = Vec::with_capacity(hp.n_layer);
217 for l in 0..hp.n_layer {
218 let b = format!("blk.{l}");
219 reject_tensor(
220 file,
221 &format!("{b}.attn_qkv.weight"),
222 "a fused QKV projection; this graph reads separate attn_q/attn_k/attn_v",
223 )?;
224 reject_tensor(
225 file,
226 &format!("{b}.attn_q_norm.weight"),
227 "per-projection QK normalization (jina-bert-v3 / neo-bert), not implemented",
228 )?;
229 reject_tensor(
230 file,
231 &format!("{b}.attn_k_norm.weight"),
232 "per-projection QK normalization (jina-bert-v3 / neo-bert), not implemented",
233 )?;
234 reject_tensor(
235 file,
236 &format!("{b}.attn_norm_2.weight"),
237 "jina-bert-v2's second attention norm, not implemented",
238 )?;
239 reject_tensor(
240 file,
241 &format!("{b}.ffn_gate.weight"),
242 "a gated FFN (nomic-bert / jina-bert-v2 GEGLU); this graph runs a plain GELU MLP",
243 )?;
244 reject_tensor(
245 file,
246 &format!("{b}.ffn_up_exps.weight"),
247 "MoE expert tensors (nomic-bert-moe), not implemented",
248 )?;
249
250 layers.push(BertLayer {
251 wq: load_weight_matrix(file, &format!("{b}.attn_q.weight"))?,
252 bq: load_f32_vec_optional(file, &format!("{b}.attn_q.bias"))?,
253 wk: load_weight_matrix(file, &format!("{b}.attn_k.weight"))?,
254 bk: load_f32_vec_optional(file, &format!("{b}.attn_k.bias"))?,
255 wv: load_weight_matrix(file, &format!("{b}.attn_v.weight"))?,
256 bv: load_f32_vec_optional(file, &format!("{b}.attn_v.bias"))?,
257 wo: load_weight_matrix(file, &format!("{b}.attn_output.weight"))?,
258 bo: load_f32_vec_optional(file, &format!("{b}.attn_output.bias"))?,
259 attn_out_norm_w: load_f32_vec(file, &format!("{b}.attn_output_norm.weight"))?,
260 attn_out_norm_b: load_f32_vec(file, &format!("{b}.attn_output_norm.bias"))?,
261 ffn_up: load_weight_matrix(file, &format!("{b}.ffn_up.weight"))?,
262 ffn_up_b: load_f32_vec_optional(file, &format!("{b}.ffn_up.bias"))?,
263 ffn_down: load_weight_matrix(file, &format!("{b}.ffn_down.weight"))?,
264 ffn_down_b: load_f32_vec_optional(file, &format!("{b}.ffn_down.bias"))?,
265 layer_out_norm_w: load_f32_vec(file, &format!("{b}.layer_output_norm.weight"))?,
266 layer_out_norm_b: load_f32_vec(file, &format!("{b}.layer_output_norm.bias"))?,
267 });
268 }
269
270 let kv_dim = hp.n_head_kv * hp.head_dim();
271 for (l, layer) in layers.iter().enumerate() {
272 for (name, m, rows) in [
273 ("attn_q", &layer.wq, hp.n_embd),
274 ("attn_k", &layer.wk, kv_dim),
275 ("attn_v", &layer.wv, kv_dim),
276 ("attn_output", &layer.wo, hp.n_embd),
277 ("ffn_up", &layer.ffn_up, hp.n_ff),
278 ("ffn_down", &layer.ffn_down, hp.n_embd),
279 ] {
280 if m.rows() != rows {
281 return Err(refuse(&format!(
282 "blk.{l}.{name}.weight has {} output rows, expected {rows}",
283 m.rows()
284 )));
285 }
286 }
287 }
288
289 assert_every_tensor_consumed(file)?;
290
291 Ok(BertEncoder {
292 hp,
293 tok_embd,
294 type_embd_row0,
295 pos_embd,
296 tok_norm_w,
297 tok_norm_b,
298 layers,
299 })
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
310 fn a_non_bert_architecture_is_refused_by_name() {
311 for arch in [
312 "nomic-bert",
313 "nomic-bert-moe",
314 "jina-bert-v2",
315 "jina-bert-v3",
316 "neo-bert",
317 "modern-bert",
318 "llama",
319 ] {
320 let err = check_arch(arch).unwrap_err();
321 assert!(
322 matches!(&err, LoadError::UnsupportedArchitecture(a) if a == arch),
323 "{arch} was not refused: {err}"
324 );
325 }
326 assert!(check_arch(BERT_ARCH).is_ok());
327 }
328}