Skip to main content

runtime/
weight_loader_core.rs

1//! Core Weight Loading Abstraction
2//!
3//! This module provides unified weight loading that converts any supported
4//! weight format into our core tensor abstraction.
5
6use crate::tensor_core::{Tensor, Device, DataType, CpuStorage, CandleStorage};
7use crate::model_core::{ModelWeights, WeightMetadata, WeightFormat};
8use std::collections::HashMap;
9use std::path::Path;
10use std::sync::Arc;
11use anyhow::{Result, anyhow};
12use serde_json::Value;
13use safetensors::SafeTensors;
14use candle_core::quantized::gguf_file;
15use candle_core::quantized::QMatMul;
16
17/// Configuration extracted from GGUF metadata
18#[derive(Debug, Clone, Default)]
19pub struct GGUFModelConfig {
20    pub architecture: String,
21    pub vocab_size: usize,
22    pub hidden_size: usize,
23    pub intermediate_size: usize,
24    pub num_hidden_layers: usize,
25    pub num_attention_heads: usize,
26    pub num_key_value_heads: usize,
27    pub head_dim: usize,
28    pub rms_norm_eps: f32,
29    pub rope_theta: f32,
30    pub max_position_embeddings: usize,
31}
32
33impl GGUFModelConfig {
34    /// Extract config from GGUF metadata
35    pub fn from_gguf_metadata(metadata: &HashMap<String, gguf_file::Value>) -> Self {
36        let architecture = extract_gguf_string(metadata, "general.architecture")
37            .unwrap_or_else(|| "llama".to_string());
38
39        // Try architecture-specific keys first, then fall back to llama keys
40        let arch_prefix = &architecture;
41
42        let vocab_size = extract_gguf_u32(metadata, &format!("{}.vocab_size", arch_prefix))
43            .or_else(|| extract_gguf_u32(metadata, "llama.vocab_size"))
44            .unwrap_or(32000) as usize;
45
46        let hidden_size = extract_gguf_u32(metadata, &format!("{}.embedding_length", arch_prefix))
47            .or_else(|| extract_gguf_u32(metadata, "llama.embedding_length"))
48            .unwrap_or(4096) as usize;
49
50        let intermediate_size = extract_gguf_u32(metadata, &format!("{}.feed_forward_length", arch_prefix))
51            .or_else(|| extract_gguf_u32(metadata, "llama.feed_forward_length"))
52            .unwrap_or(11008) as usize;
53
54        let num_hidden_layers = extract_gguf_u32(metadata, &format!("{}.block_count", arch_prefix))
55            .or_else(|| extract_gguf_u32(metadata, "llama.block_count"))
56            .unwrap_or(32) as usize;
57
58        let num_attention_heads = extract_gguf_u32(metadata, &format!("{}.attention.head_count", arch_prefix))
59            .or_else(|| extract_gguf_u32(metadata, "llama.attention.head_count"))
60            .unwrap_or(32) as usize;
61
62        let num_key_value_heads = extract_gguf_u32(metadata, &format!("{}.attention.head_count_kv", arch_prefix))
63            .or_else(|| extract_gguf_u32(metadata, "llama.attention.head_count_kv"))
64            .unwrap_or(num_attention_heads as u32) as usize;
65
66        let head_dim = if num_attention_heads > 0 {
67            hidden_size / num_attention_heads
68        } else {
69            128
70        };
71
72        let rms_norm_eps = extract_gguf_f32(metadata, &format!("{}.attention.layer_norm_rms_epsilon", arch_prefix))
73            .or_else(|| extract_gguf_f32(metadata, "llama.attention.layer_norm_rms_epsilon"))
74            .unwrap_or(1e-5);
75
76        let rope_theta = extract_gguf_f32(metadata, &format!("{}.rope.freq_base", arch_prefix))
77            .or_else(|| extract_gguf_f32(metadata, "llama.rope.freq_base"))
78            .unwrap_or(10000.0);
79
80        let max_position_embeddings = extract_gguf_u32(metadata, &format!("{}.context_length", arch_prefix))
81            .or_else(|| extract_gguf_u32(metadata, "llama.context_length"))
82            .unwrap_or(2048) as usize;
83
84        Self {
85            architecture,
86            vocab_size,
87            hidden_size,
88            intermediate_size,
89            num_hidden_layers,
90            num_attention_heads,
91            num_key_value_heads,
92            head_dim,
93            rms_norm_eps,
94            rope_theta,
95            max_position_embeddings,
96        }
97    }
98}
99
100/// Special token IDs extracted from GGUF
101#[derive(Debug, Clone, Default)]
102pub struct GGUFSpecialTokens {
103    pub bos_token_id: Option<u32>,
104    pub eos_token_id: Option<u32>,
105    pub unk_token_id: Option<u32>,
106    pub pad_token_id: Option<u32>,
107}
108
109/// Tokenizer data extracted from GGUF metadata
110#[derive(Debug, Clone)]
111pub struct GGUFTokenizer {
112    pub tokens: Vec<String>,
113    pub token_types: Option<Vec<i32>>,
114    pub scores: Option<Vec<f32>>,
115    pub model_type: String,
116    pub special_tokens: GGUFSpecialTokens,
117}
118
119impl GGUFTokenizer {
120    /// Extract tokenizer data from GGUF metadata
121    pub fn from_gguf_metadata(metadata: &HashMap<String, gguf_file::Value>) -> Option<Self> {
122        // Extract token list
123        let tokens = extract_gguf_string_array(metadata, "tokenizer.ggml.tokens")?;
124
125        if tokens.is_empty() {
126            return None;
127        }
128
129        // Extract token types (optional)
130        let token_types = extract_gguf_i32_array(metadata, "tokenizer.ggml.token_type");
131
132        // Extract BPE scores (optional)
133        let scores = extract_gguf_f32_array(metadata, "tokenizer.ggml.scores");
134
135        // Extract model type
136        let model_type = extract_gguf_string(metadata, "tokenizer.ggml.model")
137            .unwrap_or_else(|| "llama".to_string());
138
139        // Extract special token IDs
140        let special_tokens = GGUFSpecialTokens {
141            bos_token_id: extract_gguf_u32(metadata, "tokenizer.ggml.bos_token_id"),
142            eos_token_id: extract_gguf_u32(metadata, "tokenizer.ggml.eos_token_id"),
143            unk_token_id: extract_gguf_u32(metadata, "tokenizer.ggml.unknown_token_id"),
144            pad_token_id: extract_gguf_u32(metadata, "tokenizer.ggml.padding_token_id"),
145        };
146
147        Some(Self {
148            tokens,
149            token_types,
150            scores,
151            model_type,
152            special_tokens,
153        })
154    }
155
156    /// Get vocab size
157    pub fn vocab_size(&self) -> usize {
158        self.tokens.len()
159    }
160}
161
162/// Extract string array from GGUF metadata
163fn extract_gguf_string_array(metadata: &HashMap<String, gguf_file::Value>, key: &str) -> Option<Vec<String>> {
164    match metadata.get(key) {
165        Some(gguf_file::Value::Array(arr)) => {
166            let strings: Vec<String> = arr.iter()
167                .filter_map(|v| {
168                    if let gguf_file::Value::String(s) = v {
169                        Some(s.clone())
170                    } else {
171                        None
172                    }
173                })
174                .collect();
175            if strings.is_empty() { None } else { Some(strings) }
176        }
177        _ => None,
178    }
179}
180
181/// Extract i32 array from GGUF metadata
182fn extract_gguf_i32_array(metadata: &HashMap<String, gguf_file::Value>, key: &str) -> Option<Vec<i32>> {
183    match metadata.get(key) {
184        Some(gguf_file::Value::Array(arr)) => {
185            let nums: Vec<i32> = arr.iter()
186                .filter_map(|v| {
187                    match v {
188                        gguf_file::Value::I32(n) => Some(*n),
189                        gguf_file::Value::U32(n) => Some(*n as i32),
190                        _ => None,
191                    }
192                })
193                .collect();
194            if nums.is_empty() { None } else { Some(nums) }
195        }
196        _ => None,
197    }
198}
199
200/// Extract f32 array from GGUF metadata
201fn extract_gguf_f32_array(metadata: &HashMap<String, gguf_file::Value>, key: &str) -> Option<Vec<f32>> {
202    match metadata.get(key) {
203        Some(gguf_file::Value::Array(arr)) => {
204            let nums: Vec<f32> = arr.iter()
205                .filter_map(|v| {
206                    match v {
207                        gguf_file::Value::F32(n) => Some(*n),
208                        gguf_file::Value::F64(n) => Some(*n as f32),
209                        _ => None,
210                    }
211                })
212                .collect();
213            if nums.is_empty() { None } else { Some(nums) }
214        }
215        _ => None,
216    }
217}
218
219/// Core weight loader trait - abstracts loading from different formats
220pub trait WeightLoader: Send + Sync {
221    /// Load weights from path
222    fn load_weights(&self, path: &Path) -> Result<ModelWeights>;
223
224    /// Check if this loader supports the given path
225    fn supports(&self, path: &Path) -> bool;
226
227    /// Get format name
228    fn format_name(&self) -> &str;
229}
230
231/// Unified weight loader that dispatches to appropriate format loaders
232pub struct UnifiedWeightLoader {
233    loaders: Vec<Box<dyn WeightLoader>>,
234}
235
236/// SafeTensors weight loader
237pub struct SafeTensorsWeightLoader;
238
239/// PyTorch weight loader
240pub struct PyTorchWeightLoader;
241
242/// GGUF weight loader
243pub struct GGUFWeightLoader;
244
245impl UnifiedWeightLoader {
246    /// Create new unified loader with all supported formats
247    pub fn new() -> Self {
248        let mut loaders: Vec<Box<dyn WeightLoader>> = Vec::new();
249        loaders.push(Box::new(SafeTensorsWeightLoader));
250        loaders.push(Box::new(PyTorchWeightLoader));
251        loaders.push(Box::new(GGUFWeightLoader));
252
253        Self { loaders }
254    }
255
256    /// Load weights from path (auto-detect format)
257    pub fn load_weights(&self, path: &Path) -> Result<ModelWeights> {
258        // Try each loader until one works
259        for loader in &self.loaders {
260            if loader.supports(path) {
261                println!("Loading weights using {} loader", loader.format_name());
262                return loader.load_weights(path);
263            }
264        }
265
266        Err(anyhow::anyhow!(
267            "No suitable weight loader found for path: {}",
268            path.display()
269        ))
270    }
271
272    /// Detect weight format
273    pub fn detect_format(&self, path: &Path) -> Option<&str> {
274        for loader in &self.loaders {
275            if loader.supports(path) {
276                return Some(loader.format_name());
277            }
278        }
279        None
280    }
281
282    /// Get all supported formats
283    pub fn supported_formats(&self) -> Vec<&str> {
284        self.loaders.iter().map(|l| l.format_name()).collect()
285    }
286}
287
288impl SafeTensorsWeightLoader {
289    /// Convert SafeTensors dtype to our DataType
290    fn convert_dtype(&self, dtype: safetensors::Dtype) -> Result<DataType> {
291        match dtype {
292            safetensors::Dtype::F32 => Ok(DataType::Float32),
293            safetensors::Dtype::F16 => Ok(DataType::Float16),
294            safetensors::Dtype::BF16 => Ok(DataType::BFloat16),
295            safetensors::Dtype::I32 => Ok(DataType::Int32),
296            safetensors::Dtype::I64 => Ok(DataType::Int64),
297            safetensors::Dtype::I8 => Ok(DataType::Int8),
298            safetensors::Dtype::BOOL => Ok(DataType::Bool),
299            _ => Err(anyhow::anyhow!("Unsupported dtype: {:?}", dtype)),
300        }
301    }
302
303    /// Load single SafeTensors file
304    fn load_safetensors_file(&self, file_path: &Path) -> Result<HashMap<String, Tensor>> {
305        println!("Loading SafeTensors file: {}", file_path.display());
306
307        let data = std::fs::read(file_path)?;
308        let safetensors = SafeTensors::deserialize(&data)?;
309
310        let mut tensors = HashMap::new();
311
312        // Iterate through all tensors in the file
313        for (name, tensor_view) in safetensors.tensors() {
314            println!("  Loading tensor: {} {:?} {:?}", name, tensor_view.shape(), tensor_view.dtype());
315
316            // Convert dtype
317            let dtype = self.convert_dtype(tensor_view.dtype())?;
318
319            // Get tensor data
320            let tensor_data = tensor_view.data();
321
322            // Create storage with actual data
323            let storage = Arc::new(CpuStorage::new(tensor_data.to_vec(), Device::CPU));
324
325            // Create our Tensor
326            let tensor = Tensor::new(
327                tensor_view.shape().to_vec(),
328                dtype,
329                Device::CPU,
330                storage
331            );
332
333            tensors.insert(name.to_string(), tensor);
334        }
335
336        println!("  ✓ Loaded {} tensors", tensors.len());
337        Ok(tensors)
338    }
339}
340
341impl WeightLoader for SafeTensorsWeightLoader {
342    fn load_weights(&self, path: &Path) -> Result<ModelWeights> {
343        let mut all_tensors = HashMap::new();
344
345        if path.is_file() && path.extension().map_or(false, |ext| ext == "safetensors") {
346            // Single file
347            let tensors = self.load_safetensors_file(path)?;
348            all_tensors.extend(tensors);
349        } else if path.is_dir() {
350            // Directory with potentially multiple files
351            let entries = std::fs::read_dir(path)?;
352            for entry in entries {
353                let entry = entry?;
354                let file_path = entry.path();
355                if file_path.extension().map_or(false, |ext| ext == "safetensors") {
356                    let tensors = self.load_safetensors_file(&file_path)?;
357                    all_tensors.extend(tensors);
358                }
359            }
360        }
361
362        if all_tensors.is_empty() {
363            return Err(anyhow::anyhow!("No SafeTensors files found in {}", path.display()));
364        }
365
366        // Calculate total parameters
367        let total_params: usize = all_tensors.values()
368            .map(|tensor| tensor.numel())
369            .sum();
370
371        // Determine primary dtype
372        let primary_dtype = all_tensors.values()
373            .next()
374            .map(|t| match t.dtype() {
375                DataType::Float32 => "float32",
376                DataType::Float16 => "float16",
377                DataType::BFloat16 => "bfloat16",
378                DataType::Int32 => "int32",
379                DataType::Int64 => "int64",
380                DataType::Int8 => "int8",
381                DataType::Bool => "bool",
382            })
383            .unwrap_or("unknown");
384
385        let metadata = WeightMetadata {
386            architecture: "unknown".to_string(), // Will be filled from config.json
387            total_params,
388            format: WeightFormat::SafeTensors,
389            dtype: primary_dtype.to_string(),
390        };
391
392        Ok(ModelWeights::new(all_tensors, metadata))
393    }
394
395    fn supports(&self, path: &Path) -> bool {
396        if path.is_file() {
397            return path.extension().map_or(false, |ext| ext == "safetensors");
398        }
399
400        if path.is_dir() {
401            // Check if directory contains SafeTensors files
402            if let Ok(entries) = std::fs::read_dir(path) {
403                for entry in entries.flatten() {
404                    if entry.path().extension().map_or(false, |ext| ext == "safetensors") {
405                        return true;
406                    }
407                }
408            }
409        }
410
411        false
412    }
413
414    fn format_name(&self) -> &str {
415        "SafeTensors"
416    }
417}
418
419impl WeightLoader for PyTorchWeightLoader {
420    fn load_weights(&self, path: &Path) -> Result<ModelWeights> {
421        println!("Loading PyTorch weights from: {}", path.display());
422
423        // Placeholder implementation
424        // Real implementation would use PyTorch bindings or pickle parsing
425
426        let mut tensors = HashMap::new();
427
428        // Create placeholder tensors
429        let storage = Arc::new(CpuStorage::zeros(2048 * 4));
430        let tensor = Tensor::new(
431            vec![512, 4],
432            DataType::Float32,
433            Device::CPU,
434            storage
435        );
436
437        tensors.insert("pytorch_weight".to_string(), tensor);
438
439        let metadata = WeightMetadata {
440            architecture: "unknown".to_string(),
441            total_params: tensors.len(),
442            format: WeightFormat::PyTorch,
443            dtype: "float32".to_string(),
444        };
445
446        Ok(ModelWeights::new(tensors, metadata))
447    }
448
449    fn supports(&self, path: &Path) -> bool {
450        if path.is_file() {
451            return path.extension().map_or(false, |ext| ext == "bin" || ext == "pt");
452        }
453
454        if path.is_dir() {
455            return path.join("pytorch_model.bin").exists() ||
456                   path.join("model.pt").exists();
457        }
458
459        false
460    }
461
462    fn format_name(&self) -> &str {
463        "PyTorch"
464    }
465}
466
467/// Create SIMD QuantizedTensor from a loaded QTensor
468///
469/// This extracts the raw quantized data from the QTensor and creates
470/// a SIMD-optimized QuantizedTensor for fast inference.
471#[cfg(feature = "simd")]
472fn create_simd_tensor_from_qtensor(
473    qtensor: &candle_core::quantized::QTensor,
474) -> Option<crate::simd::quant::QuantizedTensor> {
475    use crate::simd::quant::{QuantType, QuantizedTensor};
476    use candle_core::quantized::GgmlDType;
477
478    // Get the quantization type
479    let quant_type = match qtensor.dtype() {
480        GgmlDType::Q4_0 => QuantType::Q4_0,
481        GgmlDType::Q4K => QuantType::Q4_K,
482        _ => return None, // Unsupported quant type for SIMD
483    };
484
485    // Get tensor shape (should be 2D for weight matrices)
486    let shape = qtensor.shape();
487    if shape.dims().len() != 2 {
488        return None; // Only support 2D weight matrices
489    }
490    let rows = shape.dims()[0];
491    let cols = shape.dims()[1];
492
493    // Get raw data bytes from the QTensor
494    // Note: QTensor stores data as raw bytes internally
495    let data = match qtensor.data() {
496        Ok(cow) => cow.to_vec(),
497        Err(_) => return None,
498    };
499
500    // Create QuantizedTensor from raw data
501    Some(QuantizedTensor::new(data, quant_type, rows, cols))
502}
503
504impl WeightLoader for GGUFWeightLoader {
505    fn load_weights(&self, path: &Path) -> Result<ModelWeights> {
506        println!("Loading GGUF weights from: {}", path.display());
507
508        // Open the GGUF file
509        let mut file = std::fs::File::open(path)?;
510        let content = gguf_file::Content::read(&mut file)
511            .map_err(|e| anyhow!("Failed to read GGUF file: {}", e))?;
512
513        println!("GGUF file loaded - {} tensors, {} metadata keys",
514            content.tensor_infos.len(),
515            content.metadata.len());
516
517        // Extract model config from GGUF metadata
518        let gguf_config = GGUFModelConfig::from_gguf_metadata(&content.metadata);
519
520        println!("Model architecture: {}", gguf_config.architecture);
521        println!("Model config: vocab_size={}, hidden_size={}, num_layers={}, heads={}, kv_heads={}",
522            gguf_config.vocab_size, gguf_config.hidden_size, gguf_config.num_hidden_layers,
523            gguf_config.num_attention_heads, gguf_config.num_key_value_heads);
524
525        // Extract tokenizer from GGUF metadata
526        let gguf_tokenizer = GGUFTokenizer::from_gguf_metadata(&content.metadata);
527        if let Some(ref tok) = gguf_tokenizer {
528            println!("Tokenizer extracted: vocab_size={}, model_type={}",
529                tok.vocab_size(), tok.model_type);
530        } else {
531            println!("No tokenizer data found in GGUF metadata");
532        }
533
534        // Load tensors
535        let device = candle_core::Device::Cpu;
536        let mut tensors = HashMap::new();
537        let mut quantized_tensors: HashMap<String, Arc<QMatMul>> = HashMap::new();
538        #[cfg(feature = "simd")]
539        let mut simd_quantized: HashMap<String, Arc<crate::simd::quant::QuantizedTensor>> = HashMap::new();
540        let mut total_params = 0usize;
541        let mut quantized_count = 0usize;
542
543        for (name, tensor_info) in content.tensor_infos.iter() {
544            // Read the quantized tensor
545            let qtensor = tensor_info.read(&mut file, content.tensor_data_offset, &device)
546                .map_err(|e| anyhow!("Failed to read tensor '{}': {}", name, e))?;
547
548            // Convert GGUF tensor name to HuggingFace-style name
549            let hf_name = gguf_to_hf_name(name);
550
551            // Determine if this is a weight tensor that should stay quantized
552            // Quantize: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj, lm_head
553            // Don't quantize: embeddings (frequent random access), norms (small 1D tensors)
554            let is_weight_tensor = hf_name.contains("_proj.weight") || hf_name.contains("lm_head.weight");
555
556            if is_weight_tensor {
557                // Keep as QMatMul for efficient quantized inference - NO F32 fallback
558                // This saves memory by not storing dequantized version
559                let shape = qtensor.shape().clone();
560
561                // Create SIMD QuantizedTensor from loaded QTensor when simd feature is enabled
562                #[cfg(feature = "simd")]
563                {
564                    if let Some(simd_tensor) = create_simd_tensor_from_qtensor(&qtensor) {
565                        simd_quantized.insert(hf_name.clone(), Arc::new(simd_tensor));
566                    }
567                }
568
569                let qmatmul = QMatMul::from_arc(Arc::new(qtensor))
570                    .map_err(|e| anyhow!("Failed to create QMatMul for '{}': {}", name, e))?;
571                quantized_tensors.insert(hf_name.clone(), Arc::new(qmatmul));
572                quantized_count += 1;
573
574                // Estimate param count from shape (we don't store F32 tensor)
575                total_params += shape.elem_count();
576                // Don't insert into tensors map - quantized only!
577            } else {
578                // Embeddings, norms - dequantize to F32 (small, frequently accessed)
579                let candle_tensor = qtensor.dequantize(&device)
580                    .map_err(|e| anyhow!("Failed to dequantize tensor '{}': {}", name, e))?;
581                total_params += candle_tensor.elem_count();
582                let tensor = Tensor::from_candle(candle_tensor);
583                tensors.insert(hf_name, tensor);
584            }
585        }
586
587        #[cfg(feature = "simd")]
588        println!("Loaded {} F32 tensors + {} quantized weights + {} SIMD quantized, {} total parameters",
589            tensors.len(), quantized_count, simd_quantized.len(), total_params);
590        #[cfg(not(feature = "simd"))]
591        println!("Loaded {} F32 tensors + {} quantized weights, {} total parameters",
592            tensors.len(), quantized_count, total_params);
593
594        let metadata = WeightMetadata {
595            architecture: gguf_config.architecture.clone(),
596            total_params,
597            format: WeightFormat::GGUF,
598            dtype: "quantized".to_string(),
599        };
600
601        #[cfg(feature = "simd")]
602        return Ok(ModelWeights::with_simd_quantized(
603            tensors, metadata, gguf_config, gguf_tokenizer, quantized_tensors, simd_quantized
604        ));
605
606        #[cfg(not(feature = "simd"))]
607        Ok(ModelWeights::with_quantized(tensors, metadata, gguf_config, gguf_tokenizer, quantized_tensors))
608    }
609
610    fn supports(&self, path: &Path) -> bool {
611        if path.is_file() {
612            return path.extension().map_or(false, |ext| ext == "gguf");
613        }
614
615        if path.is_dir() {
616            if let Ok(entries) = std::fs::read_dir(path) {
617                for entry in entries.flatten() {
618                    if entry.path().extension().map_or(false, |ext| ext == "gguf") {
619                        return true;
620                    }
621                }
622            }
623        }
624
625        false
626    }
627
628    fn format_name(&self) -> &str {
629        "GGUF"
630    }
631}
632
633/// Model configuration loader - loads config.json files
634pub struct ConfigLoader;
635
636impl ConfigLoader {
637    /// Load model configuration from config.json
638    pub fn load_config(&self, model_path: &Path) -> Result<Value> {
639        let config_path = if model_path.is_file() {
640            model_path.parent()
641                .ok_or_else(|| anyhow::anyhow!("No parent directory"))?
642                .join("config.json")
643        } else {
644            model_path.join("config.json")
645        };
646
647        if !config_path.exists() {
648            return Err(anyhow::anyhow!("config.json not found at {}", config_path.display()));
649        }
650
651        let content = std::fs::read_to_string(&config_path)?;
652        let config: Value = serde_json::from_str(&content)?;
653
654        Ok(config)
655    }
656
657    /// Extract architecture from config
658    pub fn get_architecture(&self, config: &Value) -> Result<String> {
659        // Try common architecture field names
660        if let Some(arch) = config.get("architectures").and_then(|a| a.as_array()) {
661            if let Some(first_arch) = arch.first().and_then(|a| a.as_str()) {
662                return Ok(first_arch.to_string());
663            }
664        }
665
666        if let Some(arch) = config.get("model_type").and_then(|a| a.as_str()) {
667            return Ok(arch.to_string());
668        }
669
670        if let Some(arch) = config.get("architecture").and_then(|a| a.as_str()) {
671            return Ok(arch.to_string());
672        }
673
674        Err(anyhow::anyhow!("Could not determine model architecture from config"))
675    }
676}
677
678/// Complete model loading pipeline
679pub struct ModelLoader {
680    weight_loader: UnifiedWeightLoader,
681    config_loader: ConfigLoader,
682}
683
684impl ModelLoader {
685    pub fn new() -> Self {
686        Self {
687            weight_loader: UnifiedWeightLoader::new(),
688            config_loader: ConfigLoader,
689        }
690    }
691
692    /// Load complete model (config + weights)
693    pub fn load_model(&self, path: &Path) -> Result<(Value, ModelWeights)> {
694        println!("Loading model from: {}", path.display());
695
696        // Load configuration
697        let config = self.config_loader.load_config(path)?;
698        println!("✓ Configuration loaded");
699
700        // Load weights
701        let weights = self.weight_loader.load_weights(path)?;
702        println!("✓ Weights loaded ({} tensors)", weights.tensors.len());
703
704        Ok((config, weights))
705    }
706
707    /// Get supported formats
708    pub fn supported_formats(&self) -> Vec<&str> {
709        self.weight_loader.supported_formats()
710    }
711}
712
713/// Global model loader
714static MODEL_LOADER: std::sync::OnceLock<ModelLoader> = std::sync::OnceLock::new();
715
716/// Get global model loader
717pub fn loader() -> &'static ModelLoader {
718    MODEL_LOADER.get_or_init(|| ModelLoader::new())
719}
720
721// === GGUF Helper Functions ===
722
723/// Extract a string value from GGUF metadata
724fn extract_gguf_string(metadata: &HashMap<String, gguf_file::Value>, key: &str) -> Option<String> {
725    metadata.get(key).and_then(|v| {
726        if let gguf_file::Value::String(s) = v {
727            Some(s.clone())
728        } else {
729            None
730        }
731    })
732}
733
734/// Extract a u32 value from GGUF metadata
735fn extract_gguf_u32(metadata: &HashMap<String, gguf_file::Value>, key: &str) -> Option<u32> {
736    metadata.get(key).and_then(|v| {
737        match v {
738            gguf_file::Value::U32(n) => Some(*n),
739            gguf_file::Value::I32(n) => Some(*n as u32),
740            gguf_file::Value::U64(n) => Some(*n as u32),
741            gguf_file::Value::I64(n) => Some(*n as u32),
742            _ => None,
743        }
744    })
745}
746
747/// Extract a f32 value from GGUF metadata
748fn extract_gguf_f32(metadata: &HashMap<String, gguf_file::Value>, key: &str) -> Option<f32> {
749    metadata.get(key).and_then(|v| {
750        match v {
751            gguf_file::Value::F32(n) => Some(*n),
752            gguf_file::Value::F64(n) => Some(*n as f32),
753            _ => None,
754        }
755    })
756}
757
758/// Convert GGUF tensor names to HuggingFace-style names
759/// GGUF uses different naming conventions than HuggingFace models
760fn gguf_to_hf_name(gguf_name: &str) -> String {
761    // Common GGUF to HF name mappings
762    // NOTE: Order matters! More specific patterns must come before less specific ones
763    // e.g., ".attn_output.weight" must be replaced before "output.weight"
764    let name = gguf_name
765        // Token embeddings
766        .replace("token_embd.weight", "model.embed_tokens.weight")
767        // Block/layer prefix
768        .replace("blk.", "model.layers.")
769        // Attention layers - weights (MUST come before output.weight replacement!)
770        .replace(".attn_output.weight", ".self_attn.o_proj.weight")
771        .replace(".attn_q.weight", ".self_attn.q_proj.weight")
772        .replace(".attn_k.weight", ".self_attn.k_proj.weight")
773        .replace(".attn_v.weight", ".self_attn.v_proj.weight")
774        // Attention layers - biases
775        .replace(".attn_output.bias", ".self_attn.o_proj.bias")
776        .replace(".attn_q.bias", ".self_attn.q_proj.bias")
777        .replace(".attn_k.bias", ".self_attn.k_proj.bias")
778        .replace(".attn_v.bias", ".self_attn.v_proj.bias")
779        // Output layer (MUST come after attn_output to avoid partial match)
780        .replace("output_norm.weight", "model.norm.weight")
781        .replace("output.weight", "lm_head.weight")
782        // MLP layers - weights
783        .replace(".ffn_gate.weight", ".mlp.gate_proj.weight")
784        .replace(".ffn_up.weight", ".mlp.up_proj.weight")
785        .replace(".ffn_down.weight", ".mlp.down_proj.weight")
786        // MLP layers - biases
787        .replace(".ffn_gate.bias", ".mlp.gate_proj.bias")
788        .replace(".ffn_up.bias", ".mlp.up_proj.bias")
789        .replace(".ffn_down.bias", ".mlp.down_proj.bias")
790        // Layer norms - weights
791        .replace(".attn_norm.weight", ".input_layernorm.weight")
792        .replace(".ffn_norm.weight", ".post_attention_layernorm.weight")
793        // Layer norms - biases (some models have these)
794        .replace(".attn_norm.bias", ".input_layernorm.bias")
795        .replace(".ffn_norm.bias", ".post_attention_layernorm.bias");
796
797    name
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803    use tempfile::tempdir;
804
805    #[test]
806    fn test_weight_loader_creation() {
807        let loader = UnifiedWeightLoader::new();
808        let formats = loader.supported_formats();
809
810        assert!(formats.contains(&"SafeTensors"));
811        assert!(formats.contains(&"PyTorch"));
812        assert!(formats.contains(&"GGUF"));
813    }
814
815    #[test]
816    fn test_config_loader() {
817        let loader = ConfigLoader;
818
819        // Test with dummy config
820        let config_json = r#"
821        {
822            "architectures": ["LlamaForCausalLM"],
823            "vocab_size": 32000,
824            "hidden_size": 4096
825        }
826        "#;
827
828        let config: Value = serde_json::from_str(config_json).unwrap();
829        let arch = loader.get_architecture(&config).unwrap();
830
831        assert_eq!(arch, "LlamaForCausalLM");
832    }
833}