#![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::{
nomic_bert_layer_tensor, NomicBertConfig, NOMIC_BERT_TENSOR_EMBED_NORM_BIAS,
NOMIC_BERT_TENSOR_EMBED_NORM_WEIGHT, NOMIC_BERT_TENSOR_TOKEN_EMBD,
NOMIC_BERT_TENSOR_TOKEN_TYPES,
};
pub const NOMIC_BERT_BLOCK_REQUIRED_SUFFIXES: &[&str] = &[
"attn_qkv.weight",
"attn_output.weight",
"attn_output_norm.weight",
"attn_output_norm.bias",
"ffn_up.weight",
"ffn_gate.weight",
"ffn_down.weight",
"layer_output_norm.weight",
"layer_output_norm.bias",
];
pub const NOMIC_BERT_BLOCK_OPTIONAL_SUFFIXES: &[&str] = &[
"attn_qkv.bias",
"attn_output.bias",
"ffn_up.bias",
"ffn_gate.bias",
"ffn_down.bias",
];
pub fn validate_tensor_set(gguf: &GgufFile, cfg: &NomicBertConfig) -> Result<()> {
let names: std::collections::HashSet<&str> = gguf.tensor_names().into_iter().collect();
let mut missing: Vec<String> = Vec::new();
for n in &[
NOMIC_BERT_TENSOR_TOKEN_EMBD,
NOMIC_BERT_TENSOR_EMBED_NORM_WEIGHT,
NOMIC_BERT_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 NOMIC_BERT_BLOCK_REQUIRED_SUFFIXES {
let key = nomic_bert_layer_tensor(layer_idx, suffix);
if !names.contains(key.as_str()) {
missing.push(key);
}
}
}
if !missing.is_empty() {
missing.sort();
return Err(anyhow!(
"nomic-bert GGUF missing {} tensor(s): {}",
missing.len(),
missing.join(", ")
));
}
Ok(())
}
pub struct LoadedNomicBertWeights {
tensors: HashMap<String, MlxBuffer>,
tensors_bf16: HashMap<String, MlxBuffer>,
_device: MlxDevice,
}
const LINEAR_WEIGHT_SUFFIXES: &[&str] = &[
"attn_qkv.weight",
"attn_output.weight",
"ffn_up.weight",
"ffn_gate.weight",
"ffn_down.weight",
];
impl std::fmt::Debug for LoadedNomicBertWeights {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LoadedNomicBertWeights")
.field("tensor_count", &self.tensors.len())
.finish()
}
}
impl LoadedNomicBertWeights {
pub fn load(gguf: &GgufFile, _cfg: &NomicBertConfig, device: MlxDevice) -> Result<Self> {
use mlx_native::ops::elementwise::{cast, CastDirection};
use mlx_native::DType;
use mlx_native::KernelRegistry;
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!("nomic-bert load_tensor_f32('{}'): {e}", name))?;
tensors.insert((*name).to_string(), buf);
}
let mut tensors_bf16: HashMap<String, MlxBuffer> = HashMap::new();
let mut registry = KernelRegistry::new();
let mut encoder = device
.command_encoder()
.map_err(|e| anyhow!("nomic-bert pre-cast: command_encoder: {e}"))?;
for (name, src) in &tensors {
let is_linear = LINEAR_WEIGHT_SUFFIXES
.iter()
.any(|sfx| name.starts_with("blk.") && name.ends_with(sfx));
if !is_linear {
continue;
}
let n_elems = src.element_count();
let dst = device
.alloc_buffer(n_elems * 2, DType::BF16, src.shape().to_vec())
.map_err(|e| anyhow!("nomic-bert pre-cast: alloc bf16 for '{name}': {e}"))?;
cast(
&mut encoder,
&mut registry,
device.metal_device(),
src,
&dst,
n_elems,
CastDirection::F32ToBF16,
)
.map_err(|e| anyhow!("nomic-bert pre-cast: cast('{name}'): {e}"))?;
tensors_bf16.insert(name.clone(), dst);
}
encoder
.commit_and_wait()
.map_err(|e| anyhow!("nomic-bert pre-cast: commit_and_wait: {e}"))?;
Ok(Self {
tensors,
tensors_bf16,
_device: device,
})
}
pub fn load_from_path(path: &Path, cfg: &NomicBertConfig) -> Result<Self> {
let gguf = GgufFile::open(path)
.map_err(|e| anyhow!("open nomic-bert GGUF {}: {e}", path.display()))?;
validate_tensor_set(&gguf, cfg)?;
let device =
MlxDevice::new().map_err(|e| anyhow!("create MlxDevice for nomic-bert load: {e}"))?;
Self::load(&gguf, cfg, device)
}
pub fn empty(device: MlxDevice) -> Self {
Self {
tensors: HashMap::new(),
tensors_bf16: HashMap::new(),
_device: device,
}
}
#[cfg(test)]
pub(crate) fn from_tensors_for_test(
tensors: HashMap<String, MlxBuffer>,
device: MlxDevice,
) -> Self {
use mlx_native::ops::elementwise::{cast, CastDirection};
use mlx_native::DType;
use mlx_native::KernelRegistry;
let mut tensors_bf16: HashMap<String, MlxBuffer> = HashMap::new();
let mut registry = KernelRegistry::new();
if let Ok(mut encoder) = device.command_encoder() {
for (name, src) in &tensors {
let is_linear = LINEAR_WEIGHT_SUFFIXES
.iter()
.any(|sfx| name.starts_with("blk.") && name.ends_with(sfx));
if !is_linear {
continue;
}
let n_elems = src.element_count();
if let Ok(dst) = device.alloc_buffer(n_elems * 2, DType::BF16, src.shape().to_vec())
{
if cast(
&mut encoder,
&mut registry,
device.metal_device(),
src,
&dst,
n_elems,
CastDirection::F32ToBF16,
)
.is_ok()
{
tensors_bf16.insert(name.clone(), dst);
}
}
}
let _ = encoder.commit_and_wait();
}
Self {
tensors,
tensors_bf16,
_device: device,
}
}
pub fn weight_bf16(&self, name: &str) -> Option<&MlxBuffer> {
self.tensors_bf16.get(name)
}
pub fn block_weight_bf16(&self, layer_idx: usize, suffix: &str) -> Option<&MlxBuffer> {
let key = nomic_bert_layer_tensor(layer_idx, suffix);
self.tensors_bf16.get(&key)
}
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(NOMIC_BERT_TENSOR_TOKEN_EMBD)
.ok_or_else(|| anyhow!("nomic-bert missing '{}'", NOMIC_BERT_TENSOR_TOKEN_EMBD))
}
pub fn token_types_weight(&self) -> Option<&MlxBuffer> {
self.tensors.get(NOMIC_BERT_TENSOR_TOKEN_TYPES)
}
pub fn embed_norm_weight(&self) -> Result<&MlxBuffer> {
self.tensors
.get(NOMIC_BERT_TENSOR_EMBED_NORM_WEIGHT)
.ok_or_else(|| {
anyhow!(
"nomic-bert missing '{}'",
NOMIC_BERT_TENSOR_EMBED_NORM_WEIGHT
)
})
}
pub fn embed_norm_bias(&self) -> Result<&MlxBuffer> {
self.tensors
.get(NOMIC_BERT_TENSOR_EMBED_NORM_BIAS)
.ok_or_else(|| anyhow!("nomic-bert missing '{}'", NOMIC_BERT_TENSOR_EMBED_NORM_BIAS))
}
pub fn block_required(&self, layer_idx: usize, suffix: &str) -> Result<&MlxBuffer> {
let key = nomic_bert_layer_tensor(layer_idx, suffix);
self.tensors
.get(&key)
.ok_or_else(|| anyhow!("nomic-bert missing '{}'", key))
}
pub fn block_optional(&self, layer_idx: usize, suffix: &str) -> Option<&MlxBuffer> {
let key = nomic_bert_layer_tensor(layer_idx, suffix);
self.tensors.get(&key)
}
}
#[cfg(test)]
mod tests {
use super::super::super::bert::config::PoolingType;
use super::super::config::NomicBertConfig;
use super::*;
fn synthetic_cfg(layers: usize) -> NomicBertConfig {
NomicBertConfig {
hidden_size: 768,
num_attention_heads: 12,
num_hidden_layers: layers,
intermediate_size: 3072,
max_position_embeddings: 2048,
vocab_size: 30522,
type_vocab_size: 2,
layer_norm_eps: 1e-12,
pooling_type: PoolingType::Mean,
rope_freq_base: 1000.0,
causal_attention: false,
}
}
#[test]
fn block_required_suffixes_cover_every_forward_pass_op() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
for s in [
"attn_qkv.weight",
"attn_output.weight",
"attn_output_norm.weight",
"attn_output_norm.bias",
"ffn_up.weight",
"ffn_gate.weight",
"ffn_down.weight",
"layer_output_norm.weight",
"layer_output_norm.bias",
] {
assert!(
NOMIC_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 NOMIC_BERT_BLOCK_OPTIONAL_SUFFIXES {
assert!(s.ends_with(".bias"), "optional must be .bias: '{}'", s);
}
}
#[test]
fn no_separate_qkv_in_required_suffixes() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
for s in ["attn_q.weight", "attn_k.weight", "attn_v.weight"] {
assert!(
!NOMIC_BERT_BLOCK_REQUIRED_SUFFIXES.contains(&s),
"{} must not be in required (nomic-bert is fused-QKV)",
s
);
}
}
#[test]
fn no_position_embd_in_required_stem() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let gguf_dummy_names = vec![
NOMIC_BERT_TENSOR_TOKEN_EMBD.to_string(),
NOMIC_BERT_TENSOR_EMBED_NORM_WEIGHT.to_string(),
NOMIC_BERT_TENSOR_EMBED_NORM_BIAS.to_string(),
];
let mut all_names = gguf_dummy_names;
for s in NOMIC_BERT_BLOCK_REQUIRED_SUFFIXES {
all_names.push(nomic_bert_layer_tensor(0, s));
}
assert!(!all_names.contains(&"position_embd.weight".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 = LoadedNomicBertWeights::empty(device);
assert_eq!(w.len(), 0);
assert!(w.is_empty());
assert!(w.token_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_qkv.weight").is_err());
assert!(w.block_optional(0, "attn_qkv.bias").is_none());
assert!(w.get("anything").is_none());
}
#[test]
fn validate_tensor_set_passes_on_real_nomic_gguf() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let path = Path::new("/opt/hf2q/models/bert-test/nomic-embed-text-v1.5-f16.gguf");
if !path.exists() {
eprintln!("skipping: nomic GGUF fixture not at {}", path.display());
return;
}
let gguf = GgufFile::open(path).expect("open nomic GGUF");
let cfg = NomicBertConfig::from_gguf(&gguf).expect("parse nomic config");
validate_tensor_set(&gguf, &cfg).expect("nomic GGUF must satisfy required-set");
}
#[test]
fn validate_tensor_set_rejects_bert_gguf_naming_missing_fused_tensor() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let path = Path::new("/opt/hf2q/models/bert-test/bge-small-en-v1.5-f16.gguf");
if !path.exists() {
eprintln!("skipping: bge GGUF fixture not at {}", path.display());
return;
}
let gguf = GgufFile::open(path).expect("open bge GGUF");
let cfg = synthetic_cfg(12);
let err = validate_tensor_set(&gguf, &cfg)
.expect_err("bge must fail nomic-bert validation (no fused QKV)");
let msg = format!("{err}");
assert!(
msg.contains("missing"),
"error must say 'missing', got: {msg}"
);
assert!(
msg.contains("attn_qkv.weight") || msg.contains("ffn_gate.weight"),
"error must name a nomic-only tensor, got: {msg}"
);
}
#[test]
fn synthetic_required_count_matches_layer_count() {
let _gpu = crate::inference::hf2q_gpu_test_lock();
let cfg = synthetic_cfg(2);
let stem = 3;
let per_block = NOMIC_BERT_BLOCK_REQUIRED_SUFFIXES.len();
let expected = stem + per_block * cfg.num_hidden_layers;
assert_eq!(stem + 9 * 2, expected);
}
}