use crate::config::{HFConfig, ModelConfig};
use crate::error::{Error, Result};
use crate::name_mapping::Architecture;
use crate::validation;
use candle_core::DType;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::Path;
use time::OffsetDateTime;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelCard {
pub model_info: ModelInfo,
pub training_info: Option<TrainingInfo>,
pub usage_info: UsageInfo,
pub evaluation_info: Option<EvaluationInfo>,
pub technical_specs: TechnicalSpecs,
pub card_metadata: CardMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
pub name: String,
pub description: Option<String>,
pub architecture: String,
pub variant: Option<String>,
pub version: Option<String>,
pub authors: Vec<String>,
pub license: Option<String>,
pub source_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrainingInfo {
pub datasets: Vec<String>,
pub procedure: Option<String>,
pub framework: Option<String>,
pub hardware: Option<String>,
pub duration: Option<String>,
pub tokens_or_steps: Option<String>,
pub learning_rate: Option<String>,
pub batch_size: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageInfo {
pub intended_uses: Vec<String>,
pub limitations: Vec<String>,
pub out_of_scope: Vec<String>,
pub bias_considerations: Option<String>,
pub ethical_considerations: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluationInfo {
pub benchmarks: HashMap<String, f64>,
pub methodology: Option<String>,
pub test_datasets: Vec<String>,
pub limitations: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TechnicalSpecs {
pub architecture: String,
pub parameter_count: u64,
pub vocab_size: usize,
pub hidden_size: usize,
pub num_layers: usize,
pub num_attention_heads: usize,
pub max_sequence_length: usize,
pub supported_dtypes: Vec<String>,
pub model_format: String,
pub file_size: Option<u64>,
pub memory_requirements: MemoryRequirements,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRequirements {
pub parameters_mb: u64,
pub inference_mb: u64,
pub training_mb: Option<u64>,
pub recommended_ram_mb: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CardMetadata {
pub created_at: String,
pub generated_by: String,
pub card_version: String,
pub tags: Vec<String>,
}
pub struct ModelCardGenerator {
pub include_technical_details: bool,
pub estimate_memory: bool,
pub additional_metadata: HashMap<String, String>,
}
impl Default for ModelCardGenerator {
fn default() -> Self {
Self {
include_technical_details: true,
estimate_memory: true,
additional_metadata: HashMap::new(),
}
}
}
impl ModelCardGenerator {
pub fn new() -> Self {
Self::default()
}
pub fn with_technical_details(mut self, include: bool) -> Self {
self.include_technical_details = include;
self
}
pub fn with_memory_estimation(mut self, estimate: bool) -> Self {
self.estimate_memory = estimate;
self
}
pub fn with_metadata<K: ToString, V: ToString>(mut self, key: K, value: V) -> Self {
self.additional_metadata
.insert(key.to_string(), value.to_string());
self
}
pub fn generate_from_config(
&self,
config: &ModelConfig,
model_path: &Path,
name: String,
) -> Result<ModelCard> {
let file_size = self.get_model_file_size(model_path);
let memory_reqs = self.calculate_memory_requirements(config)?;
let model_info = ModelInfo {
name: name.clone(),
description: Some(format!(
"A {} model with {} parameters",
config.architecture,
self.format_parameter_count(self.estimate_parameter_count(config))
)),
architecture: config.architecture.to_string(),
variant: self.infer_model_variant(&name, config),
version: None,
authors: vec!["Unknown".to_string()],
license: None,
source_url: None,
};
let usage_info = UsageInfo {
intended_uses: self.get_default_intended_uses(&config.architecture),
limitations: self.get_default_limitations(&config.architecture),
out_of_scope: self.get_default_out_of_scope_uses(&config.architecture),
bias_considerations: Some("This model may exhibit biases present in training data. Users should evaluate fairness for their specific use case.".to_string()),
ethical_considerations: Some("Consider potential misuse and ensure responsible deployment with appropriate safeguards.".to_string()),
};
let technical_specs = TechnicalSpecs {
architecture: config.architecture.to_string(),
parameter_count: self.estimate_parameter_count(config),
vocab_size: config.vocab_size,
hidden_size: config.hidden_size,
num_layers: config.num_hidden_layers,
num_attention_heads: config.num_attention_heads,
max_sequence_length: config.max_position_embeddings,
supported_dtypes: vec!["f32".to_string(), "f16".to_string(), "bf16".to_string()],
model_format: self.detect_model_format(model_path),
file_size,
memory_requirements: memory_reqs,
};
let card_metadata = CardMetadata {
created_at: OffsetDateTime::now_utc().to_string(),
generated_by: "MLMF Model Card Generator".to_string(),
card_version: "1.0".to_string(),
tags: self.generate_tags(&config.architecture, &technical_specs),
};
Ok(ModelCard {
model_info,
training_info: None, usage_info,
evaluation_info: None, technical_specs,
card_metadata,
})
}
pub fn generate_from_hf_config(
&self,
hf_config: &HFConfig,
architecture: Architecture,
model_path: &Path,
name: String,
) -> Result<ModelCard> {
let model_config = hf_config.to_model_config(architecture)?;
self.generate_from_config(&model_config, model_path, name)
}
pub fn generate_and_save_json(
&self,
config: &ModelConfig,
model_path: &Path,
output_path: &Path,
name: String,
) -> Result<()> {
let card = self.generate_from_config(config, model_path, name)?;
let json = serde_json::to_string_pretty(&card)
.map_err(|e| Error::invalid_config(format!("Failed to serialize model card: {}", e)))?;
fs::write(output_path, json).map_err(|e| {
Error::io_error(format!(
"Failed to write model card to {:?}: {}",
output_path, e
))
})?;
Ok(())
}
pub fn generate_and_save_readme(
&self,
config: &ModelConfig,
model_path: &Path,
output_path: &Path,
name: String,
) -> Result<()> {
let card = self.generate_from_config(config, model_path, name)?;
let markdown = self.generate_markdown(&card)?;
fs::write(output_path, markdown).map_err(|e| {
Error::io_error(format!(
"Failed to write README to {:?}: {}",
output_path, e
))
})?;
Ok(())
}
fn calculate_memory_requirements(&self, config: &ModelConfig) -> Result<MemoryRequirements> {
if !self.estimate_memory {
return Ok(MemoryRequirements {
parameters_mb: 0,
inference_mb: 0,
training_mb: None,
recommended_ram_mb: 0,
});
}
let memory_estimate = validation::estimate_memory_usage(config, DType::F16, Some(1), None);
let param_memory = (memory_estimate.parameters_gb * 1024.0) as u64;
let inference_memory = (memory_estimate.total_gb * 1024.0) as u64;
let training_memory = param_memory * 4; let recommended_ram = inference_memory * 2;
Ok(MemoryRequirements {
parameters_mb: param_memory,
inference_mb: inference_memory,
training_mb: Some(training_memory),
recommended_ram_mb: recommended_ram,
})
}
fn estimate_parameter_count(&self, config: &ModelConfig) -> u64 {
let embedding_params = config.vocab_size * config.hidden_size;
let attention_params =
config.num_hidden_layers * config.hidden_size * config.hidden_size * 4; let ffn_params =
config.num_hidden_layers * config.hidden_size * config.intermediate_size * 2; let norm_params = config.num_hidden_layers * config.hidden_size * 2;
(embedding_params + attention_params + ffn_params + norm_params) as u64
}
fn format_parameter_count(&self, count: u64) -> String {
if count >= 1_000_000_000 {
format!("{:.1}B", count as f64 / 1_000_000_000.0)
} else if count >= 1_000_000 {
format!("{:.1}M", count as f64 / 1_000_000.0)
} else if count >= 1_000 {
format!("{:.1}K", count as f64 / 1_000.0)
} else {
count.to_string()
}
}
fn infer_model_variant(&self, name: &str, config: &ModelConfig) -> Option<String> {
let param_count = self.estimate_parameter_count(config);
let size_variant = if param_count >= 70_000_000_000 {
"70B+"
} else if param_count >= 13_000_000_000 {
"13B"
} else if param_count >= 7_000_000_000 {
"7B"
} else if param_count >= 3_000_000_000 {
"3B"
} else if param_count >= 1_000_000_000 {
"1B"
} else {
"Small"
};
let name_lower = name.to_lowercase();
if name_lower.contains("instruct") || name_lower.contains("chat") {
Some(format!("{}-Instruct", size_variant))
} else if name_lower.contains("base") {
Some(format!("{}-Base", size_variant))
} else {
Some(size_variant.to_string())
}
}
fn get_model_file_size(&self, model_path: &Path) -> Option<u64> {
if model_path.is_file() {
fs::metadata(model_path).ok().map(|m| m.len())
} else if model_path.is_dir() {
let mut total_size = 0u64;
if let Ok(entries) = fs::read_dir(model_path) {
for entry in entries.flatten() {
if let Ok(metadata) = entry.metadata() {
if metadata.is_file() {
let path = entry.path();
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_lowercase();
if matches!(
ext.as_str(),
"safetensors" | "bin" | "pt" | "pth" | "gguf"
) {
total_size += metadata.len();
}
}
}
}
}
}
if total_size > 0 {
Some(total_size)
} else {
None
}
} else {
None
}
}
fn detect_model_format(&self, model_path: &Path) -> String {
if model_path.is_file() {
if let Some(ext) = model_path.extension() {
match ext.to_string_lossy().to_lowercase().as_str() {
"safetensors" => "SafeTensors".to_string(),
"gguf" => "GGUF".to_string(),
"pt" | "pth" | "bin" => "PyTorch".to_string(),
"onnx" => "ONNX".to_string(),
_ => "Unknown".to_string(),
}
} else {
"Unknown".to_string()
}
} else if model_path.is_dir() {
let entries = fs::read_dir(model_path).unwrap_or_else(|_| fs::read_dir(".").unwrap());
for entry in entries.flatten() {
let path = entry.path();
if let Some(ext) = path.extension() {
match ext.to_string_lossy().to_lowercase().as_str() {
"safetensors" => return "SafeTensors".to_string(),
"gguf" => return "GGUF".to_string(),
"bin" | "pt" | "pth" => return "PyTorch".to_string(),
"onnx" => return "ONNX".to_string(),
_ => continue,
}
}
}
"Mixed/Unknown".to_string()
} else {
"Unknown".to_string()
}
}
fn get_default_intended_uses(&self, architecture: &Architecture) -> Vec<String> {
match architecture {
Architecture::LLaMA | Architecture::GPT2 | Architecture::GPTNeoX => vec![
"Text generation".to_string(),
"Conversational AI".to_string(),
"Code completion".to_string(),
"Question answering".to_string(),
"Summarization".to_string(),
],
Architecture::Unknown => vec![
"General NLP tasks".to_string(),
"Research purposes".to_string(),
],
}
}
fn get_default_limitations(&self, architecture: &Architecture) -> Vec<String> {
let mut limitations = vec![
"May generate biased or harmful content".to_string(),
"Performance may vary on out-of-distribution data".to_string(),
"Computational requirements may limit deployment".to_string(),
];
match architecture {
Architecture::LLaMA | Architecture::GPT2 | Architecture::GPTNeoX => {
limitations.extend(vec![
"May generate factually incorrect information".to_string(),
"Limited by training data cutoff".to_string(),
"May struggle with complex reasoning tasks".to_string(),
]);
}
Architecture::Unknown => {
limitations.push("Unknown architecture limitations not documented".to_string());
}
}
limitations
}
fn get_default_out_of_scope_uses(&self, _architecture: &Architecture) -> Vec<String> {
vec![
"Generating harmful or illegal content".to_string(),
"Impersonation or deception".to_string(),
"Critical safety applications without human oversight".to_string(),
"Medical diagnosis or treatment recommendations".to_string(),
"Legal advice or financial recommendations".to_string(),
]
}
fn generate_tags(&self, architecture: &Architecture, specs: &TechnicalSpecs) -> Vec<String> {
let mut tags = vec![
"transformers".to_string(),
architecture.to_string().to_lowercase(),
"mlmf".to_string(),
];
if specs.parameter_count >= 70_000_000_000 {
tags.push("70b+".to_string());
} else if specs.parameter_count >= 13_000_000_000 {
tags.push("13b".to_string());
} else if specs.parameter_count >= 7_000_000_000 {
tags.push("7b".to_string());
} else if specs.parameter_count >= 1_000_000_000 {
tags.push("1b+".to_string());
}
tags.push(specs.model_format.to_lowercase());
tags
}
fn generate_markdown(&self, card: &ModelCard) -> Result<String> {
let mut md = String::new();
md.push_str(&format!("# {}\n\n", card.model_info.name));
if let Some(description) = &card.model_info.description {
md.push_str(&format!("{}\n\n", description));
}
md.push_str("## Model Details\n\n");
md.push_str(&format!(
"- **Architecture:** {}\n",
card.model_info.architecture
));
if let Some(variant) = &card.model_info.variant {
md.push_str(&format!("- **Variant:** {}\n", variant));
}
if let Some(version) = &card.model_info.version {
md.push_str(&format!("- **Version:** {}\n", version));
}
md.push_str(&format!(
"- **Parameters:** {}\n",
self.format_parameter_count(card.technical_specs.parameter_count)
));
if let Some(license) = &card.model_info.license {
md.push_str(&format!("- **License:** {}\n", license));
}
md.push_str("\n");
if self.include_technical_details {
md.push_str("## Technical Specifications\n\n");
md.push_str("| Specification | Value |\n");
md.push_str("|---------------|-------|\n");
md.push_str(&format!(
"| Architecture | {} |\n",
card.technical_specs.architecture
));
md.push_str(&format!(
"| Parameters | {} |\n",
self.format_parameter_count(card.technical_specs.parameter_count)
));
md.push_str(&format!(
"| Vocabulary Size | {} |\n",
card.technical_specs.vocab_size
));
md.push_str(&format!(
"| Hidden Size | {} |\n",
card.technical_specs.hidden_size
));
md.push_str(&format!(
"| Layers | {} |\n",
card.technical_specs.num_layers
));
md.push_str(&format!(
"| Attention Heads | {} |\n",
card.technical_specs.num_attention_heads
));
md.push_str(&format!(
"| Max Sequence Length | {} |\n",
card.technical_specs.max_sequence_length
));
md.push_str(&format!(
"| Model Format | {} |\n",
card.technical_specs.model_format
));
if let Some(file_size) = card.technical_specs.file_size {
md.push_str(&format!(
"| File Size | {:.1} GB |\n",
file_size as f64 / (1024.0 * 1024.0 * 1024.0)
));
}
md.push_str("\n");
if self.estimate_memory {
md.push_str("### Memory Requirements\n\n");
let mem = &card.technical_specs.memory_requirements;
md.push_str(&format!(
"- **Parameters:** {:.1} GB\n",
mem.parameters_mb as f64 / 1024.0
));
md.push_str(&format!(
"- **Inference:** {:.1} GB\n",
mem.inference_mb as f64 / 1024.0
));
if let Some(training_mb) = mem.training_mb {
md.push_str(&format!(
"- **Training:** {:.1} GB\n",
training_mb as f64 / 1024.0
));
}
md.push_str(&format!(
"- **Recommended RAM:** {:.1} GB\n",
mem.recommended_ram_mb as f64 / 1024.0
));
md.push_str("\n");
}
}
md.push_str("## Intended Use\n\n");
md.push_str("### Primary Use Cases\n");
for use_case in &card.usage_info.intended_uses {
md.push_str(&format!("- {}\n", use_case));
}
md.push_str("\n");
md.push_str("### Limitations\n");
for limitation in &card.usage_info.limitations {
md.push_str(&format!("- {}\n", limitation));
}
md.push_str("\n");
md.push_str("### Out-of-Scope Uses\n");
for out_of_scope in &card.usage_info.out_of_scope {
md.push_str(&format!("- {}\n", out_of_scope));
}
md.push_str("\n");
if let Some(bias) = &card.usage_info.bias_considerations {
md.push_str("### Bias Considerations\n");
md.push_str(&format!("{}\n\n", bias));
}
if let Some(ethics) = &card.usage_info.ethical_considerations {
md.push_str("### Ethical Considerations\n");
md.push_str(&format!("{}\n\n", ethics));
}
if let Some(training) = &card.training_info {
md.push_str("## Training Information\n\n");
if !training.datasets.is_empty() {
md.push_str("### Training Data\n");
for dataset in &training.datasets {
md.push_str(&format!("- {}\n", dataset));
}
md.push_str("\n");
}
if let Some(procedure) = &training.procedure {
md.push_str("### Training Procedure\n");
md.push_str(&format!("{}\n\n", procedure));
}
if let Some(framework) = &training.framework {
md.push_str(&format!("**Training Framework:** {}\n", framework));
}
if let Some(hardware) = &training.hardware {
md.push_str(&format!("**Hardware:** {}\n", hardware));
}
if let Some(duration) = &training.duration {
md.push_str(&format!("**Duration:** {}\n", duration));
}
md.push_str("\n");
}
if let Some(evaluation) = &card.evaluation_info {
md.push_str("## Evaluation\n\n");
if !evaluation.benchmarks.is_empty() {
md.push_str("### Benchmark Results\n");
md.push_str("| Benchmark | Score |\n");
md.push_str("|-----------|-------|\n");
for (benchmark, score) in &evaluation.benchmarks {
md.push_str(&format!("| {} | {:.2} |\n", benchmark, score));
}
md.push_str("\n");
}
if let Some(methodology) = &evaluation.methodology {
md.push_str("### Evaluation Methodology\n");
md.push_str(&format!("{}\n\n", methodology));
}
}
md.push_str("## Usage\n\n");
md.push_str("```rust\n");
md.push_str("use mlmf::universal_loader::load_model;\n");
md.push_str("use mlmf::LoadOptions;\n");
md.push_str("use candle_core::{Device, DType};\n\n");
md.push_str("let device = Device::cuda_if_available(0).unwrap_or(Device::Cpu);\n");
md.push_str("let options = LoadOptions {\n");
md.push_str(" device,\n");
md.push_str(" dtype: DType::F16,\n");
md.push_str(" use_mmap: true,\n");
md.push_str(" validate_cuda: false,\n");
md.push_str(" progress: None,\n");
md.push_str(" smart_mapping_oracle: None,\n");
md.push_str("};\n\n");
md.push_str(&format!(
"let model = load_model(\"{}\", options)?;\n",
card.model_info.name
));
md.push_str("```\n\n");
md.push_str("---\n\n");
md.push_str(&format!(
"*Generated by {} on {}*\n",
card.card_metadata.generated_by, card.card_metadata.created_at
));
Ok(md)
}
}
impl fmt::Display for Architecture {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Architecture::LLaMA => write!(f, "LLaMA"),
Architecture::GPT2 => write!(f, "GPT-2"),
Architecture::GPTNeoX => write!(f, "GPT-NeoX"),
Architecture::Unknown => write!(f, "Unknown"),
}
}
}