#![allow(dead_code)]
use std::collections::HashMap;
use std::path::Path;
use anyhow::{anyhow, Result};
use mlx_native::gguf::GgufFile;
use mlx_native::{MlxBuffer, MlxDevice};
use super::config::{
bert_layer_tensor, BertConfig, TENSOR_EMBED_NORM_BIAS, TENSOR_EMBED_NORM_WEIGHT,
TENSOR_POS_EMBD, TENSOR_TOKEN_EMBD, TENSOR_TOKEN_TYPES,
};
pub const BERT_BLOCK_REQUIRED_SUFFIXES: &[&str] = &[
"attn_q.weight",
"attn_k.weight",
"attn_v.weight",
"attn_output.weight",
"attn_output_norm.weight",
"attn_output_norm.bias",
"ffn_up.weight",
"ffn_down.weight",
"layer_output_norm.weight",
"layer_output_norm.bias",
];
pub const BERT_BLOCK_OPTIONAL_SUFFIXES: &[&str] = &[
"attn_q.bias",
"attn_k.bias",
"attn_v.bias",
"attn_output.bias",
"ffn_up.bias",
"ffn_down.bias",
];
pub fn validate_tensor_set(gguf: &GgufFile, cfg: &BertConfig) -> Result<()> {
let names: std::collections::HashSet<&str> = gguf.tensor_names().into_iter().collect();
let mut missing: Vec<String> = Vec::new();
for n in &[
TENSOR_TOKEN_EMBD,
TENSOR_POS_EMBD,
TENSOR_EMBED_NORM_WEIGHT,
TENSOR_EMBED_NORM_BIAS,
] {
if !names.contains(*n) {
missing.push((*n).to_string());
}
}
for layer_idx in 0..cfg.num_hidden_layers {
for suffix in BERT_BLOCK_REQUIRED_SUFFIXES {
let key = bert_layer_tensor(layer_idx, suffix);
if !names.contains(key.as_str()) {
missing.push(key);
}
}
}
if !missing.is_empty() {
missing.sort();
return Err(anyhow!(
"BERT GGUF missing {} tensor(s): {}",
missing.len(),
missing.join(", ")
));
}
Ok(())
}
pub struct LoadedBertWeights {
tensors: HashMap<String, MlxBuffer>,
_device: MlxDevice,
}
impl std::fmt::Debug for LoadedBertWeights {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LoadedBertWeights")
.field("tensor_count", &self.tensors.len())
.finish()
}
}
impl LoadedBertWeights {
pub fn load(gguf: &GgufFile, _cfg: &BertConfig, device: MlxDevice) -> Result<Self> {
let names = gguf.tensor_names();
let mut tensors = HashMap::with_capacity(names.len());
for name in &names {
let buf = gguf
.load_tensor_f32(*name, &device)
.map_err(|e| anyhow!("BERT load_tensor_f32('{}'): {e}", name))?;
tensors.insert((*name).to_string(), buf);
}
Ok(Self {
tensors,
_device: device,
})
}
pub fn load_from_path(path: &Path, cfg: &BertConfig) -> Result<Self> {
let gguf =
GgufFile::open(path).map_err(|e| anyhow!("open BERT GGUF {}: {e}", path.display()))?;
validate_tensor_set(&gguf, cfg)?;
let device =
MlxDevice::new().map_err(|e| anyhow!("create MlxDevice for BERT load: {e}"))?;
Self::load(&gguf, cfg, device)
}
pub fn empty(device: MlxDevice) -> Self {
Self {
tensors: HashMap::new(),
_device: device,
}
}
#[cfg(test)]
pub(crate) fn from_tensors_for_test(
tensors: HashMap<String, MlxBuffer>,
device: MlxDevice,
) -> Self {
Self {
tensors,
_device: device,
}
}
pub fn len(&self) -> usize {
self.tensors.len()
}
pub fn is_empty(&self) -> bool {
self.tensors.is_empty()
}
pub fn get(&self, name: &str) -> Option<&MlxBuffer> {
self.tensors.get(name)
}
pub fn token_embd_weight(&self) -> Result<&MlxBuffer> {
self.tensors
.get(TENSOR_TOKEN_EMBD)
.ok_or_else(|| anyhow!("BERT missing '{}'", TENSOR_TOKEN_EMBD))
}
pub fn position_embd_weight(&self) -> Result<&MlxBuffer> {
self.tensors
.get(TENSOR_POS_EMBD)
.ok_or_else(|| anyhow!("BERT missing '{}'", TENSOR_POS_EMBD))
}
pub fn token_types_weight(&self) -> Option<&MlxBuffer> {
self.tensors.get(TENSOR_TOKEN_TYPES)
}
pub fn embed_norm_weight(&self) -> Result<&MlxBuffer> {
self.tensors
.get(TENSOR_EMBED_NORM_WEIGHT)
.ok_or_else(|| anyhow!("BERT missing '{}'", TENSOR_EMBED_NORM_WEIGHT))
}
pub fn embed_norm_bias(&self) -> Result<&MlxBuffer> {
self.tensors
.get(TENSOR_EMBED_NORM_BIAS)
.ok_or_else(|| anyhow!("BERT missing '{}'", TENSOR_EMBED_NORM_BIAS))
}
pub fn block_required(&self, layer_idx: usize, suffix: &str) -> Result<&MlxBuffer> {
let key = bert_layer_tensor(layer_idx, suffix);
self.tensors
.get(&key)
.ok_or_else(|| anyhow!("BERT missing '{}'", key))
}
pub fn block_optional(&self, layer_idx: usize, suffix: &str) -> Option<&MlxBuffer> {
let key = bert_layer_tensor(layer_idx, suffix);
self.tensors.get(&key)
}
}
#[cfg(test)]
mod tests {
use super::super::config::PoolingType;
use super::*;
fn synthetic_cfg(layers: usize) -> BertConfig {
BertConfig {
hidden_size: 384,
num_attention_heads: 12,
num_hidden_layers: layers,
intermediate_size: 1536,
max_position_embeddings: 512,
vocab_size: 30522,
type_vocab_size: 2,
layer_norm_eps: 1e-12,
hidden_act: "gelu".into(),
pooling_type: PoolingType::Mean,
causal_attention: false,
}
}
fn synthetic_required_names(cfg: &BertConfig) -> Vec<String> {
let mut out = vec![
TENSOR_TOKEN_EMBD.to_string(),
TENSOR_POS_EMBD.to_string(),
TENSOR_EMBED_NORM_WEIGHT.to_string(),
TENSOR_EMBED_NORM_BIAS.to_string(),
];
for layer_idx in 0..cfg.num_hidden_layers {
for suffix in BERT_BLOCK_REQUIRED_SUFFIXES {
out.push(bert_layer_tensor(layer_idx, suffix));
}
}
out
}
#[test]
fn block_required_suffixes_cover_every_forward_pass_op() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
for s in [
"attn_q.weight",
"attn_k.weight",
"attn_v.weight",
"attn_output.weight",
"attn_output_norm.weight",
"attn_output_norm.bias",
"ffn_up.weight",
"ffn_down.weight",
"layer_output_norm.weight",
"layer_output_norm.bias",
] {
assert!(
BERT_BLOCK_REQUIRED_SUFFIXES.contains(&s),
"missing required suffix '{}'",
s
);
}
}
#[test]
fn block_optional_suffixes_are_biases_only() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
for s in BERT_BLOCK_OPTIONAL_SUFFIXES {
assert!(s.ends_with(".bias"), "optional must be .bias: '{}'", s);
}
}
#[test]
fn synthetic_required_names_count_matches_config() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let cfg = synthetic_cfg(2);
let names = synthetic_required_names(&cfg);
assert_eq!(names.len(), 4 + BERT_BLOCK_REQUIRED_SUFFIXES.len() * 2);
assert!(names.contains(&"blk.0.attn_q.weight".to_string()));
assert!(names.contains(&"blk.1.layer_output_norm.bias".to_string()));
}
#[test]
fn empty_loaded_weights_returns_errs_from_shortcuts() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let device = MlxDevice::new().expect("create device");
let w = LoadedBertWeights::empty(device);
assert_eq!(w.len(), 0);
assert!(w.is_empty());
assert!(w.token_embd_weight().is_err());
assert!(w.position_embd_weight().is_err());
assert!(w.embed_norm_weight().is_err());
assert!(w.embed_norm_bias().is_err());
assert!(w.token_types_weight().is_none());
assert!(w.block_required(0, "attn_q.weight").is_err());
assert!(w.block_optional(0, "attn_q.bias").is_none());
assert!(w.get("anything").is_none());
}
#[test]
fn validate_tensor_set_on_vocab_only_gguf_reports_missing_tensors() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let path = Path::new("/opt/llama.cpp/models/ggml-vocab-bert-bge.gguf");
if !path.exists() {
eprintln!(
"skipping: vocab GGUF fixture not found at {}",
path.display()
);
return;
}
let gguf = GgufFile::open(path).expect("open vocab gguf");
let cfg = synthetic_cfg(2);
let err = validate_tensor_set(&gguf, &cfg).expect_err("vocab-only must miss tensors");
let msg = format!("{}", err);
assert!(msg.contains("missing"), "error names missing: {msg}");
assert!(
msg.contains(TENSOR_TOKEN_EMBD)
|| msg.contains("blk.0")
|| msg.contains("attn_q.weight"),
"error should name a specific missing tensor: {msg}"
);
}
}