Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/realizar/src/gguf/config.rs
Line
Count
Source
1
//! GGUF configuration extraction
2
//!
3
//! Extracts model configuration from GGUF metadata.
4
//!
5
//! This module defines `GGUFConfig` which holds the transformer
6
//! architecture parameters needed for inference.
7
8
use super::types::GGUFModel;
9
use crate::error::{RealizarError, Result};
10
11
/// Configuration for GGUF transformer inference
12
#[derive(Debug, Clone)]
13
pub struct GGUFConfig {
14
    /// Model architecture (e.g., "phi2", "llama", "qwen2")
15
    pub architecture: String,
16
    /// Embedding dimension (hidden size)
17
    pub hidden_dim: usize,
18
    /// Number of transformer layers
19
    pub num_layers: usize,
20
    /// Number of attention heads
21
    pub num_heads: usize,
22
    /// Number of key-value heads (for GQA, often num_heads or num_heads/8)
23
    pub num_kv_heads: usize,
24
    /// Vocabulary size
25
    pub vocab_size: usize,
26
    /// FFN intermediate dimension
27
    pub intermediate_dim: usize,
28
    /// Context length
29
    pub context_length: usize,
30
    /// RoPE theta (position encoding base)
31
    pub rope_theta: f32,
32
    /// Layer norm epsilon
33
    pub eps: f32,
34
    /// RoPE type: 0 = NORM (adjacent pairs), 2 = NEOX (split halves)
35
    pub rope_type: u32,
36
}
37
38
impl GGUFConfig {
39
    /// Extract configuration from GGUF model metadata
40
    ///
41
    /// # Errors
42
    ///
43
    /// Returns an error if required metadata fields are missing from the GGUF model.
44
12
    pub fn from_gguf(model: &GGUFModel) -> Result<Self> {
45
12
        let 
architecture10
= model
46
12
            .architecture()
47
12
            .ok_or_else(|| RealizarError::InvalidShape {
48
2
                reason: "Missing general.architecture in GGUF metadata".to_string(),
49
2
            })?
50
10
            .to_string();
51
52
10
        let hidden_dim = model
53
10
            .embedding_dim()
54
10
            .ok_or_else(|| RealizarError::InvalidShape {
55
0
                reason: "Missing embedding_length in GGUF metadata".to_string(),
56
0
            })?;
57
58
10
        let num_layers = model
59
10
            .num_layers()
60
10
            .ok_or_else(|| RealizarError::InvalidShape {
61
0
                reason: "Missing block_count in GGUF metadata".to_string(),
62
0
            })?;
63
64
        // Try to get num_heads, default based on hidden_dim if not found
65
10
        let num_heads = model.num_heads().unwrap_or(hidden_dim / 64);
66
67
        // Get vocab_size from token_embd tensor
68
        // After dims.reverse(), shape is [vocab_size, hidden_dim] - vocab is at index 0
69
10
        let vocab_size = model
70
10
            .tensors
71
10
            .iter()
72
10
            .find(|t| 
t.name9
==
"token_embd.weight"9
)
73
10
            .map_or(32000, |t| 
t.dims.first()9
.
copied9
().
unwrap_or9
(32000) as usize);
74
75
        // Infer intermediate_dim from ffn_up tensor
76
        // After dims.reverse(), shape is [intermediate_dim, hidden_dim] - intermediate is at index 0
77
10
        let intermediate_dim = model
78
10
            .tensors
79
10
            .iter()
80
70
            .
find10
(|t| t.name == "blk.0.ffn_up.weight")
81
10
            .map_or(hidden_dim * 4, |t| 
{9
82
9
                t.dims.first().copied().unwrap_or(hidden_dim as u64 * 4) as usize
83
9
            });
84
85
10
        let context_length = model.context_length().unwrap_or(2048);
86
87
        // Read rope_theta from metadata, or use default (10000.0 for LLaMA-style)
88
        // Qwen2 uses 1000000.0, which is read from qwen2.rope.freq_base
89
10
        let rope_theta = model.rope_freq_base().unwrap_or(10000.0);
90
91
        // Read RMSNorm epsilon from metadata, or use default (1e-5 for LLaMA-style)
92
        // Qwen2 uses 1e-6, which is read from qwen2.attention.layer_norm_rms_epsilon
93
10
        let eps = model.rms_epsilon().unwrap_or(1e-5);
94
95
        // num_kv_heads (for GQA - e.g., Qwen uses fewer KV heads than Q heads)
96
10
        let num_kv_heads = model.num_kv_heads().unwrap_or(num_heads);
97
98
        // Read rope_type: 0 = NORM (adjacent pairs, default for LLaMA), 2 = NEOX (split halves)
99
        // LLaMA models use type 0 (adjacent pairs) per llama.cpp's LLAMA_ROPE_TYPE_NORM
100
10
        let rope_type = model.rope_type().unwrap_or(0);
101
102
10
        Ok(Self {
103
10
            architecture,
104
10
            hidden_dim,
105
10
            num_layers,
106
10
            num_heads,
107
10
            num_kv_heads,
108
10
            vocab_size,
109
10
            intermediate_dim,
110
10
            context_length,
111
10
            rope_theta,
112
10
            eps,
113
10
            rope_type,
114
10
        })
115
12
    }
116
}
117
118
#[cfg(test)]
119
mod tests {
120
    use super::*;
121
122
    #[test]
123
1
    fn test_gguf_config_creation() {
124
1
        let config = GGUFConfig {
125
1
            architecture: "llama".to_string(),
126
1
            hidden_dim: 4096,
127
1
            num_layers: 32,
128
1
            num_heads: 32,
129
1
            num_kv_heads: 8,
130
1
            vocab_size: 32000,
131
1
            intermediate_dim: 11008,
132
1
            context_length: 4096,
133
1
            rope_theta: 10000.0,
134
1
            eps: 1e-5,
135
1
            rope_type: 0,
136
1
        };
137
138
1
        assert_eq!(config.architecture, "llama");
139
1
        assert_eq!(config.hidden_dim, 4096);
140
1
        assert_eq!(config.num_layers, 32);
141
1
        assert_eq!(config.num_heads, 32);
142
1
        assert_eq!(config.num_kv_heads, 8);
143
1
        assert_eq!(config.vocab_size, 32000);
144
1
        assert_eq!(config.intermediate_dim, 11008);
145
1
        assert_eq!(config.context_length, 4096);
146
1
        assert!((config.rope_theta - 10000.0).abs() < f32::EPSILON);
147
1
        assert!((config.eps - 1e-5).abs() < f32::EPSILON);
148
1
        assert_eq!(config.rope_type, 0);
149
1
    }
150
151
    #[test]
152
1
    fn test_gguf_config_clone() {
153
1
        let config = GGUFConfig {
154
1
            architecture: "qwen2".to_string(),
155
1
            hidden_dim: 2048,
156
1
            num_layers: 24,
157
1
            num_heads: 16,
158
1
            num_kv_heads: 2,
159
1
            vocab_size: 151936,
160
1
            intermediate_dim: 5632,
161
1
            context_length: 32768,
162
1
            rope_theta: 1_000_000.0,
163
1
            eps: 1e-6,
164
1
            rope_type: 2,
165
1
        };
166
167
1
        let cloned = config.clone();
168
1
        assert_eq!(cloned.architecture, "qwen2");
169
1
        assert_eq!(cloned.hidden_dim, config.hidden_dim);
170
1
        assert_eq!(cloned.rope_type, 2);
171
1
    }
172
173
    #[test]
174
1
    fn test_gguf_config_debug() {
175
1
        let config = GGUFConfig {
176
1
            architecture: "phi2".to_string(),
177
1
            hidden_dim: 2560,
178
1
            num_layers: 32,
179
1
            num_heads: 32,
180
1
            num_kv_heads: 32,
181
1
            vocab_size: 51200,
182
1
            intermediate_dim: 10240,
183
1
            context_length: 2048,
184
1
            rope_theta: 10000.0,
185
1
            eps: 1e-5,
186
1
            rope_type: 0,
187
1
        };
188
189
1
        let debug_str = format!("{:?}", config);
190
1
        assert!(debug_str.contains("phi2"));
191
1
        assert!(debug_str.contains("2560"));
192
1
    }
193
}