/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 | | } |