use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::ir::ModelMetadata;
#[derive(Error, Debug)]
#[allow(dead_code)]
pub enum FingerprintError {
#[error("Model fingerprinting failed: {reason}")]
FingerprintFailed { reason: String },
#[error("Missing required metadata for fingerprinting: {field}")]
MissingField { field: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ModelFingerprint {
pub architecture: String,
pub total_params: u64,
pub layer_count: u32,
pub expert_count: u32,
pub attention_types: Vec<String>,
pub hidden_size: u64,
pub dtype: String,
pub intermediate_size: Option<u64>,
pub num_attention_heads: u32,
pub num_kv_heads: Option<u32>,
pub vocab_size: u64,
}
impl ModelFingerprint {
pub fn from_metadata(metadata: &ModelMetadata) -> Result<Self, FingerprintError> {
if metadata.architecture.is_empty() {
return Err(FingerprintError::MissingField {
field: "architecture".to_string(),
});
}
let mut attention_types = metadata.unique_layer_types();
attention_types.sort();
Ok(Self {
architecture: metadata.architecture.clone(),
total_params: metadata.param_count,
layer_count: metadata.num_layers,
expert_count: metadata.num_experts.unwrap_or(0),
attention_types,
hidden_size: metadata.hidden_size,
dtype: metadata.dtype.clone(),
intermediate_size: metadata.intermediate_size,
num_attention_heads: metadata.num_attention_heads,
num_kv_heads: metadata.num_kv_heads,
vocab_size: metadata.vocab_size,
})
}
pub fn stable_id(&self) -> String {
let mut hasher = DefaultHasher::new();
self.architecture.hash(&mut hasher);
self.total_params.hash(&mut hasher);
self.layer_count.hash(&mut hasher);
self.hidden_size.hash(&mut hasher);
self.expert_count.hash(&mut hasher);
self.vocab_size.hash(&mut hasher);
format!("model-{:016x}", hasher.finish())
}
pub fn estimated_f16_size_bytes(&self) -> u64 {
self.total_params * 2
}
pub fn estimated_size_bytes(&self, bits: u8) -> u64 {
(self.total_params as f64 * bits as f64 / 8.0) as u64
}
pub fn is_moe(&self) -> bool {
self.expert_count > 1
}
}
impl std::fmt::Display for ModelFingerprint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} ({}B params, {} layers, hidden={}{})",
self.architecture,
self.total_params as f64 / 1_000_000_000.0,
self.layer_count,
self.hidden_size,
if self.is_moe() {
format!(", {} experts", self.expert_count)
} else {
String::new()
}
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_metadata() -> ModelMetadata {
ModelMetadata {
architecture: "LlamaForCausalLM".to_string(),
model_type: "llama".to_string(),
param_count: 8_000_000_000,
hidden_size: 4096,
num_layers: 32,
layer_types: vec!["attention".to_string(); 32],
num_attention_heads: 32,
num_kv_heads: Some(8),
vocab_size: 128256,
dtype: "bfloat16".to_string(),
shard_count: 4,
num_experts: None,
top_k_experts: None,
intermediate_size: Some(14336),
raw_config: serde_json::Value::Null,
explicit_layer_types: None,
full_attention_interval: None,
attn_output_gate: None,
head_dim: None,
partial_rotary_factor: None,
rope_parameters: None,
linear_conv_kernel_dim: None,
linear_key_head_dim: None,
linear_num_key_heads: None,
linear_value_head_dim: None,
linear_num_value_heads: None,
mamba_ssm_dtype: None,
moe_intermediate_size: None,
shared_expert_intermediate_size: None,
mtp_num_hidden_layers: None,
mtp_use_dedicated_embeddings: None,
output_router_logits: None,
router_aux_loss_coef: None,
}
}
fn make_moe_metadata() -> ModelMetadata {
ModelMetadata {
architecture: "Gemma4ForConditionalGeneration".to_string(),
model_type: "gemma4".to_string(),
param_count: 27_000_000_000,
hidden_size: 2816,
num_layers: 30,
layer_types: vec![
"sliding_attention".to_string(),
"full_attention".to_string(),
],
num_attention_heads: 16,
num_kv_heads: Some(8),
vocab_size: 262144,
dtype: "bfloat16".to_string(),
shard_count: 12,
num_experts: Some(128),
top_k_experts: Some(8),
intermediate_size: Some(2112),
raw_config: serde_json::Value::Null,
explicit_layer_types: None,
full_attention_interval: None,
attn_output_gate: None,
head_dim: None,
partial_rotary_factor: None,
rope_parameters: None,
linear_conv_kernel_dim: None,
linear_key_head_dim: None,
linear_num_key_heads: None,
linear_value_head_dim: None,
linear_num_value_heads: None,
mamba_ssm_dtype: None,
moe_intermediate_size: None,
shared_expert_intermediate_size: None,
mtp_num_hidden_layers: None,
mtp_use_dedicated_embeddings: None,
output_router_logits: None,
router_aux_loss_coef: None,
}
}
#[test]
fn test_fingerprint_from_metadata() {
let metadata = make_test_metadata();
let fp = ModelFingerprint::from_metadata(&metadata).unwrap();
assert_eq!(fp.architecture, "LlamaForCausalLM");
assert_eq!(fp.total_params, 8_000_000_000);
assert_eq!(fp.layer_count, 32);
assert_eq!(fp.expert_count, 0);
assert_eq!(fp.hidden_size, 4096);
assert!(!fp.is_moe());
}
#[test]
fn test_fingerprint_from_moe_metadata() {
let metadata = make_moe_metadata();
let fp = ModelFingerprint::from_metadata(&metadata).unwrap();
assert_eq!(fp.expert_count, 128);
assert!(fp.is_moe());
assert_eq!(fp.attention_types.len(), 2);
assert!(fp.attention_types.contains(&"full_attention".to_string()));
assert!(fp
.attention_types
.contains(&"sliding_attention".to_string()));
}
#[test]
fn test_stable_id_deterministic() {
let metadata = make_test_metadata();
let fp = ModelFingerprint::from_metadata(&metadata).unwrap();
let id1 = fp.stable_id();
let id2 = fp.stable_id();
assert_eq!(id1, id2);
assert!(id1.starts_with("model-"));
}
#[test]
fn test_stable_id_different_models() {
let fp1 = ModelFingerprint::from_metadata(&make_test_metadata()).unwrap();
let fp2 = ModelFingerprint::from_metadata(&make_moe_metadata()).unwrap();
assert_ne!(fp1.stable_id(), fp2.stable_id());
}
#[test]
fn test_estimated_sizes() {
let metadata = make_test_metadata();
let fp = ModelFingerprint::from_metadata(&metadata).unwrap();
assert_eq!(fp.estimated_f16_size_bytes(), 16_000_000_000);
assert_eq!(fp.estimated_size_bytes(4), 4_000_000_000);
}
#[test]
fn test_empty_architecture_fails() {
let mut metadata = make_test_metadata();
metadata.architecture = String::new();
let result = ModelFingerprint::from_metadata(&metadata);
assert!(result.is_err());
}
#[test]
fn test_display() {
let metadata = make_test_metadata();
let fp = ModelFingerprint::from_metadata(&metadata).unwrap();
let display = format!("{}", fp);
assert!(display.contains("LlamaForCausalLM"));
assert!(display.contains("8"));
assert!(display.contains("32 layers"));
}
#[test]
fn test_attention_types_sorted_for_stability() {
let mut meta1 = make_moe_metadata();
meta1.layer_types = vec![
"full_attention".to_string(),
"sliding_attention".to_string(),
];
let mut meta2 = make_moe_metadata();
meta2.layer_types = vec![
"sliding_attention".to_string(),
"full_attention".to_string(),
];
let fp1 = ModelFingerprint::from_metadata(&meta1).unwrap();
let fp2 = ModelFingerprint::from_metadata(&meta2).unwrap();
assert_eq!(fp1.attention_types, fp2.attention_types);
assert_eq!(fp1.stable_id(), fp2.stable_id());
}
}