/home/noah/src/realizar/src/gguf/test_factory.rs
Line | Count | Source |
1 | | //! GGUF Test Factory - Synthesizes valid GGUF files in memory |
2 | | //! |
3 | | //! This module provides `GGUFBuilder` for creating valid GGUF v3 files |
4 | | //! without needing real model files. Essential for testing transformer |
5 | | //! loading code that requires properly formatted binary data. |
6 | | //! |
7 | | //! # Example |
8 | | //! |
9 | | //! ```ignore |
10 | | //! let data = GGUFBuilder::new() |
11 | | //! .architecture("llama") |
12 | | //! .hidden_dim(64) |
13 | | //! .num_layers(1) |
14 | | //! .add_f32_tensor("token_embd.weight", &[100, 64], &embedding_data) |
15 | | //! .add_q4_k_tensor("blk.0.attn_q.weight", &[64, 64], &q4k_data) |
16 | | //! .build(); |
17 | | //! |
18 | | //! let model = GGUFModel::from_bytes(&data)?; |
19 | | //! ``` |
20 | | |
21 | | use super::types::{ |
22 | | GGUF_ALIGNMENT, GGUF_MAGIC, GGUF_TYPE_F32, GGUF_TYPE_Q4_0, GGUF_TYPE_Q4_K, GGUF_TYPE_Q5_K, |
23 | | GGUF_TYPE_Q6_K, GGUF_TYPE_Q8_0, GGUF_VERSION_V3, |
24 | | }; |
25 | | |
26 | | /// Builder for creating valid GGUF v3 files in memory |
27 | | pub struct GGUFBuilder { |
28 | | /// Metadata key-value pairs (key, type, value_bytes) |
29 | | metadata: Vec<(String, u32, Vec<u8>)>, |
30 | | /// Tensor info entries (name, dims, qtype, data) |
31 | | tensors: Vec<(String, Vec<u64>, u32, Vec<u8>)>, |
32 | | } |
33 | | |
34 | | impl Default for GGUFBuilder { |
35 | 0 | fn default() -> Self { |
36 | 0 | Self::new() |
37 | 0 | } |
38 | | } |
39 | | |
40 | | impl GGUFBuilder { |
41 | | /// Create a new GGUF builder |
42 | | #[must_use] |
43 | 18 | pub fn new() -> Self { |
44 | 18 | Self { |
45 | 18 | metadata: Vec::new(), |
46 | 18 | tensors: Vec::new(), |
47 | 18 | } |
48 | 18 | } |
49 | | |
50 | | // ========================================================================= |
51 | | // Metadata Helpers |
52 | | // ========================================================================= |
53 | | |
54 | | /// Add a string metadata value |
55 | | #[must_use] |
56 | 14 | pub fn add_string(mut self, key: &str, value: &str) -> Self { |
57 | 14 | let mut bytes = Vec::new(); |
58 | | // String: u64 length + UTF-8 bytes |
59 | 14 | bytes.extend_from_slice(&(value.len() as u64).to_le_bytes()); |
60 | 14 | bytes.extend_from_slice(value.as_bytes()); |
61 | 14 | self.metadata.push((key.to_string(), 8, bytes)); // type 8 = string |
62 | 14 | self |
63 | 14 | } |
64 | | |
65 | | /// Add a u32 metadata value |
66 | | #[must_use] |
67 | 77 | pub fn add_u32(mut self, key: &str, value: u32) -> Self { |
68 | 77 | self.metadata |
69 | 77 | .push((key.to_string(), 4, value.to_le_bytes().to_vec())); // type 4 = u32 |
70 | 77 | self |
71 | 77 | } |
72 | | |
73 | | /// Add a f32 metadata value |
74 | | #[must_use] |
75 | 25 | pub fn add_f32(mut self, key: &str, value: f32) -> Self { |
76 | 25 | self.metadata |
77 | 25 | .push((key.to_string(), 6, value.to_le_bytes().to_vec())); // type 6 = f32 |
78 | 25 | self |
79 | 25 | } |
80 | | |
81 | | /// Set architecture (shorthand for general.architecture) |
82 | | #[must_use] |
83 | 14 | pub fn architecture(self, arch: &str) -> Self { |
84 | 14 | self.add_string("general.architecture", arch) |
85 | 14 | } |
86 | | |
87 | | /// Set hidden dimension (embedding length) |
88 | | #[must_use] |
89 | 13 | pub fn hidden_dim(self, arch: &str, dim: u32) -> Self { |
90 | 13 | self.add_u32(&format!("{}.embedding_length", arch), dim) |
91 | 13 | } |
92 | | |
93 | | /// Set number of layers (block count) |
94 | | #[must_use] |
95 | 13 | pub fn num_layers(self, arch: &str, count: u32) -> Self { |
96 | 13 | self.add_u32(&format!("{}.block_count", arch), count) |
97 | 13 | } |
98 | | |
99 | | /// Set number of attention heads |
100 | | #[must_use] |
101 | 13 | pub fn num_heads(self, arch: &str, count: u32) -> Self { |
102 | 13 | self.add_u32(&format!("{}.attention.head_count", arch), count) |
103 | 13 | } |
104 | | |
105 | | /// Set number of KV heads (for GQA) |
106 | | #[must_use] |
107 | 13 | pub fn num_kv_heads(self, arch: &str, count: u32) -> Self { |
108 | 13 | self.add_u32(&format!("{}.attention.head_count_kv", arch), count) |
109 | 13 | } |
110 | | |
111 | | /// Set context length |
112 | | #[must_use] |
113 | 13 | pub fn context_length(self, arch: &str, len: u32) -> Self { |
114 | 13 | self.add_u32(&format!("{}.context_length", arch), len) |
115 | 13 | } |
116 | | |
117 | | /// Set RoPE frequency base |
118 | | #[must_use] |
119 | 12 | pub fn rope_freq_base(self, arch: &str, base: f32) -> Self { |
120 | 12 | self.add_f32(&format!("{}.rope.freq_base", arch), base) |
121 | 12 | } |
122 | | |
123 | | /// Set RMS epsilon |
124 | | #[must_use] |
125 | 12 | pub fn rms_epsilon(self, arch: &str, eps: f32) -> Self { |
126 | 12 | self.add_f32(&format!("{}.attention.layer_norm_rms_epsilon", arch), eps) |
127 | 12 | } |
128 | | |
129 | | /// Set feed-forward hidden dimension |
130 | | #[must_use] |
131 | 11 | pub fn ffn_hidden_dim(self, arch: &str, dim: u32) -> Self { |
132 | 11 | self.add_u32(&format!("{}.feed_forward_length", arch), dim) |
133 | 11 | } |
134 | | |
135 | | /// Set vocab size (for completeness) |
136 | | #[must_use] |
137 | 0 | pub fn vocab_size(self, _arch: &str, size: u32) -> Self { |
138 | | // Vocab size is typically inferred from token_embd.weight shape |
139 | | // But we can store it in metadata if needed |
140 | 0 | self.add_u32("tokenizer.ggml.tokens.size", size) |
141 | 0 | } |
142 | | |
143 | | // ========================================================================= |
144 | | // Tensor Helpers |
145 | | // ========================================================================= |
146 | | |
147 | | /// Add an F32 tensor |
148 | | #[must_use] |
149 | 52 | pub fn add_f32_tensor(mut self, name: &str, dims: &[u64], data: &[f32]) -> Self { |
150 | 118k | let bytes52 : Vec<u8>52 = data52 .iter52 ().flat_map52 (|f| f.to_le_bytes()).collect52 (); |
151 | 52 | self.tensors |
152 | 52 | .push((name.to_string(), dims.to_vec(), GGUF_TYPE_F32, bytes)); |
153 | 52 | self |
154 | 52 | } |
155 | | |
156 | | /// Add a Q4_0 tensor (18 bytes per 32 elements) |
157 | | #[must_use] |
158 | 7 | pub fn add_q4_0_tensor(mut self, name: &str, dims: &[u64], data: &[u8]) -> Self { |
159 | 7 | self.tensors |
160 | 7 | .push((name.to_string(), dims.to_vec(), GGUF_TYPE_Q4_0, data.to_vec())); |
161 | 7 | self |
162 | 7 | } |
163 | | |
164 | | /// Add a Q8_0 tensor (34 bytes per 32 elements) |
165 | | #[must_use] |
166 | 7 | pub fn add_q8_0_tensor(mut self, name: &str, dims: &[u64], data: &[u8]) -> Self { |
167 | 7 | self.tensors |
168 | 7 | .push((name.to_string(), dims.to_vec(), GGUF_TYPE_Q8_0, data.to_vec())); |
169 | 7 | self |
170 | 7 | } |
171 | | |
172 | | /// Add a Q4_K tensor (144 bytes per 256 elements) |
173 | | #[must_use] |
174 | 61 | pub fn add_q4_k_tensor(mut self, name: &str, dims: &[u64], data: &[u8]) -> Self { |
175 | 61 | self.tensors |
176 | 61 | .push((name.to_string(), dims.to_vec(), GGUF_TYPE_Q4_K, data.to_vec())); |
177 | 61 | self |
178 | 61 | } |
179 | | |
180 | | /// Add a Q5_K tensor (176 bytes per 256 elements) |
181 | | #[must_use] |
182 | 7 | pub fn add_q5_k_tensor(mut self, name: &str, dims: &[u64], data: &[u8]) -> Self { |
183 | 7 | self.tensors |
184 | 7 | .push((name.to_string(), dims.to_vec(), GGUF_TYPE_Q5_K, data.to_vec())); |
185 | 7 | self |
186 | 7 | } |
187 | | |
188 | | /// Add a Q6_K tensor (210 bytes per 256 elements) |
189 | | #[must_use] |
190 | 7 | pub fn add_q6_k_tensor(mut self, name: &str, dims: &[u64], data: &[u8]) -> Self { |
191 | 7 | self.tensors |
192 | 7 | .push((name.to_string(), dims.to_vec(), GGUF_TYPE_Q6_K, data.to_vec())); |
193 | 7 | self |
194 | 7 | } |
195 | | |
196 | | // ========================================================================= |
197 | | // Build |
198 | | // ========================================================================= |
199 | | |
200 | | /// Build the GGUF file as a byte vector |
201 | | #[must_use] |
202 | 18 | pub fn build(self) -> Vec<u8> { |
203 | 18 | let mut data = Vec::new(); |
204 | | |
205 | | // Header |
206 | 18 | data.extend_from_slice(&GGUF_MAGIC.to_le_bytes()); |
207 | 18 | data.extend_from_slice(&GGUF_VERSION_V3.to_le_bytes()); |
208 | 18 | data.extend_from_slice(&(self.tensors.len() as u64).to_le_bytes()); |
209 | 18 | data.extend_from_slice(&(self.metadata.len() as u64).to_le_bytes()); |
210 | | |
211 | | // Metadata |
212 | 134 | for (key116 , value_type116 , value_bytes116 ) in &self.metadata { |
213 | 116 | // Key string: u64 length + UTF-8 bytes |
214 | 116 | data.extend_from_slice(&(key.len() as u64).to_le_bytes()); |
215 | 116 | data.extend_from_slice(key.as_bytes()); |
216 | 116 | // Value type |
217 | 116 | data.extend_from_slice(&value_type.to_le_bytes()); |
218 | 116 | // Value bytes |
219 | 116 | data.extend_from_slice(value_bytes); |
220 | 116 | } |
221 | | |
222 | | // Tensor info |
223 | 18 | let mut tensor_data_offset = 0u64; |
224 | 159 | for (name141 , dims141 , qtype141 , tensor_bytes141 ) in &self.tensors { |
225 | | // Name string |
226 | 141 | data.extend_from_slice(&(name.len() as u64).to_le_bytes()); |
227 | 141 | data.extend_from_slice(name.as_bytes()); |
228 | | |
229 | | // n_dims |
230 | 141 | data.extend_from_slice(&(dims.len() as u32).to_le_bytes()); |
231 | | |
232 | | // Dimensions (reversed for GGML order) |
233 | 243 | for dim in dims.iter()141 .rev141 () { |
234 | 243 | data.extend_from_slice(&dim.to_le_bytes()); |
235 | 243 | } |
236 | | |
237 | | // Quantization type |
238 | 141 | data.extend_from_slice(&qtype.to_le_bytes()); |
239 | | |
240 | | // Offset (relative to tensor data start) |
241 | 141 | data.extend_from_slice(&tensor_data_offset.to_le_bytes()); |
242 | | |
243 | 141 | tensor_data_offset += tensor_bytes.len() as u64; |
244 | | } |
245 | | |
246 | | // Align to GGUF_ALIGNMENT (32 bytes) |
247 | 18 | let current_len = data.len(); |
248 | 18 | let aligned = current_len.div_ceil(GGUF_ALIGNMENT) * GGUF_ALIGNMENT; |
249 | 18 | data.resize(aligned, 0); |
250 | | |
251 | | // Tensor data |
252 | 159 | for (_, _, _, tensor_bytes141 ) in &self.tensors { |
253 | 141 | data.extend_from_slice(tensor_bytes); |
254 | 141 | } |
255 | | |
256 | 18 | data |
257 | 18 | } |
258 | | } |
259 | | |
260 | | // ============================================================================= |
261 | | // Helper Functions for Creating Quantized Data |
262 | | // ============================================================================= |
263 | | |
264 | | /// Create valid Q4_0 data for a tensor with given dimensions |
265 | | /// Q4_0: 18 bytes per 32 elements (2 f16 scale + 16 bytes quants) |
266 | | #[must_use] |
267 | 5 | pub fn create_q4_0_data(num_elements: usize) -> Vec<u8> { |
268 | 5 | let num_blocks = num_elements.div_ceil(32); |
269 | 5 | let mut data = Vec::with_capacity(num_blocks * 18); |
270 | | |
271 | 898 | for _ in 0..num_blocks5 { |
272 | 898 | // f16 scale = 0.1 |
273 | 898 | let scale = half::f16::from_f32(0.1); |
274 | 898 | data.extend_from_slice(&scale.to_le_bytes()); |
275 | 898 | // 16 bytes of quants (mid-range values) |
276 | 898 | data.extend([0x88u8; 16]); |
277 | 898 | } |
278 | | |
279 | 5 | data |
280 | 5 | } |
281 | | |
282 | | /// Create valid Q8_0 data for a tensor with given dimensions |
283 | | /// Q8_0: 34 bytes per 32 elements (2 f16 scale + 32 i8 quants) |
284 | | #[must_use] |
285 | 5 | pub fn create_q8_0_data(num_elements: usize) -> Vec<u8> { |
286 | 5 | let num_blocks = num_elements.div_ceil(32); |
287 | 5 | let mut data = Vec::with_capacity(num_blocks * 34); |
288 | | |
289 | 898 | for _ in 0..num_blocks5 { |
290 | 898 | // f16 scale = 0.1 |
291 | 898 | let scale = half::f16::from_f32(0.1); |
292 | 898 | data.extend_from_slice(&scale.to_le_bytes()); |
293 | 898 | // 32 i8 quants (zeros) |
294 | 898 | data.extend([0i8 as u8; 32]); |
295 | 898 | } |
296 | | |
297 | 5 | data |
298 | 5 | } |
299 | | |
300 | | /// Create valid Q4_K data for a tensor with given dimensions |
301 | | /// Q4_K: 144 bytes per 256 elements |
302 | | #[must_use] |
303 | 55 | pub fn create_q4_k_data(num_elements: usize) -> Vec<u8> { |
304 | 55 | let num_super_blocks = num_elements.div_ceil(256); |
305 | 55 | vec![0u8; num_super_blocks * 144] |
306 | 55 | } |
307 | | |
308 | | /// Create valid Q5_K data for a tensor with given dimensions |
309 | | /// Q5_K: 176 bytes per 256 elements |
310 | | #[must_use] |
311 | 5 | pub fn create_q5_k_data(num_elements: usize) -> Vec<u8> { |
312 | 5 | let num_super_blocks = num_elements.div_ceil(256); |
313 | 5 | vec![0u8; num_super_blocks * 176] |
314 | 5 | } |
315 | | |
316 | | /// Create valid Q6_K data for a tensor with given dimensions |
317 | | /// Q6_K: 210 bytes per 256 elements |
318 | | #[must_use] |
319 | 5 | pub fn create_q6_k_data(num_elements: usize) -> Vec<u8> { |
320 | 5 | let num_super_blocks = num_elements.div_ceil(256); |
321 | 5 | vec![0u8; num_super_blocks * 210] |
322 | 5 | } |
323 | | |
324 | | /// Create F32 embedding data (small random-ish values) |
325 | | #[must_use] |
326 | 21 | pub fn create_f32_embedding_data(vocab_size: usize, hidden_dim: usize) -> Vec<f32> { |
327 | 21 | let mut data = Vec::with_capacity(vocab_size * hidden_dim); |
328 | 153k | for i in 0..(vocab_size * hidden_dim)21 { |
329 | 153k | // Pseudo-random but deterministic values |
330 | 153k | let val = ((i % 1000) as f32 - 500.0) / 5000.0; |
331 | 153k | data.push(val); |
332 | 153k | } |
333 | 21 | data |
334 | 21 | } |
335 | | |
336 | | /// Create F32 norm weights (typically ~1.0) |
337 | | #[must_use] |
338 | 19 | pub fn create_f32_norm_weights(dim: usize) -> Vec<f32> { |
339 | 19 | vec![1.0f32; dim] |
340 | 19 | } |
341 | | |
342 | | // ============================================================================= |
343 | | // Complete Model Builder |
344 | | // ============================================================================= |
345 | | |
346 | | /// Build a minimal valid LLaMA-style GGUF model |
347 | | /// |
348 | | /// This creates a complete model with: |
349 | | /// - Token embeddings (F32) |
350 | | /// - One transformer layer with Q4_K weights |
351 | | /// - Output norm (F32) |
352 | | /// - LM head (tied to token embeddings) |
353 | | #[must_use] |
354 | 5 | pub fn build_minimal_llama_gguf( |
355 | 5 | vocab_size: usize, |
356 | 5 | hidden_dim: usize, |
357 | 5 | intermediate_dim: usize, |
358 | 5 | num_heads: usize, |
359 | 5 | num_kv_heads: usize, |
360 | 5 | ) -> Vec<u8> { |
361 | 5 | let head_dim = hidden_dim / num_heads; |
362 | 5 | let kv_dim = num_kv_heads * head_dim; |
363 | | |
364 | | // Create tensor data |
365 | 5 | let embed_data = create_f32_embedding_data(vocab_size, hidden_dim); |
366 | 5 | let norm_data = create_f32_norm_weights(hidden_dim); |
367 | | |
368 | | // Q4_K weights for layer 0 |
369 | 5 | let q_data = create_q4_k_data(hidden_dim * hidden_dim); |
370 | 5 | let k_data = create_q4_k_data(hidden_dim * kv_dim); |
371 | 5 | let v_data = create_q4_k_data(hidden_dim * kv_dim); |
372 | 5 | let attn_out_data = create_q4_k_data(hidden_dim * hidden_dim); |
373 | 5 | let ffn_up_data = create_q4_k_data(hidden_dim * intermediate_dim); |
374 | 5 | let ffn_down_data = create_q4_k_data(intermediate_dim * hidden_dim); |
375 | 5 | let ffn_gate_data = create_q4_k_data(hidden_dim * intermediate_dim); |
376 | | |
377 | 5 | GGUFBuilder::new() |
378 | | // Metadata |
379 | 5 | .architecture("llama") |
380 | 5 | .hidden_dim("llama", hidden_dim as u32) |
381 | 5 | .num_layers("llama", 1) |
382 | 5 | .num_heads("llama", num_heads as u32) |
383 | 5 | .num_kv_heads("llama", num_kv_heads as u32) |
384 | 5 | .context_length("llama", 256) |
385 | 5 | .rope_freq_base("llama", 10000.0) |
386 | 5 | .rms_epsilon("llama", 1e-5) |
387 | 5 | .ffn_hidden_dim("llama", intermediate_dim as u32) |
388 | | // Token embedding |
389 | 5 | .add_f32_tensor( |
390 | 5 | "token_embd.weight", |
391 | 5 | &[vocab_size as u64, hidden_dim as u64], |
392 | 5 | &embed_data, |
393 | | ) |
394 | | // Layer 0 attention |
395 | 5 | .add_f32_tensor("blk.0.attn_norm.weight", &[hidden_dim as u64], &norm_data) |
396 | 5 | .add_q4_k_tensor( |
397 | 5 | "blk.0.attn_q.weight", |
398 | 5 | &[hidden_dim as u64, hidden_dim as u64], |
399 | 5 | &q_data, |
400 | | ) |
401 | 5 | .add_q4_k_tensor( |
402 | 5 | "blk.0.attn_k.weight", |
403 | 5 | &[hidden_dim as u64, kv_dim as u64], |
404 | 5 | &k_data, |
405 | | ) |
406 | 5 | .add_q4_k_tensor( |
407 | 5 | "blk.0.attn_v.weight", |
408 | 5 | &[hidden_dim as u64, kv_dim as u64], |
409 | 5 | &v_data, |
410 | | ) |
411 | 5 | .add_q4_k_tensor( |
412 | 5 | "blk.0.attn_output.weight", |
413 | 5 | &[hidden_dim as u64, hidden_dim as u64], |
414 | 5 | &attn_out_data, |
415 | | ) |
416 | | // Layer 0 FFN |
417 | 5 | .add_f32_tensor("blk.0.ffn_norm.weight", &[hidden_dim as u64], &norm_data) |
418 | 5 | .add_q4_k_tensor( |
419 | 5 | "blk.0.ffn_up.weight", |
420 | 5 | &[hidden_dim as u64, intermediate_dim as u64], |
421 | 5 | &ffn_up_data, |
422 | | ) |
423 | 5 | .add_q4_k_tensor( |
424 | 5 | "blk.0.ffn_down.weight", |
425 | 5 | &[intermediate_dim as u64, hidden_dim as u64], |
426 | 5 | &ffn_down_data, |
427 | | ) |
428 | 5 | .add_q4_k_tensor( |
429 | 5 | "blk.0.ffn_gate.weight", |
430 | 5 | &[hidden_dim as u64, intermediate_dim as u64], |
431 | 5 | &ffn_gate_data, |
432 | | ) |
433 | | // Output norm and head |
434 | 5 | .add_f32_tensor("output_norm.weight", &[hidden_dim as u64], &norm_data) |
435 | | // Note: LM head often tied to token_embd, so we don't add output.weight |
436 | | // The loader will fallback to token_embd.weight |
437 | 5 | .build() |
438 | 5 | } |
439 | | |
440 | | /// Build a minimal Phi-2 style GGUF model (fused QKV) |
441 | | #[must_use] |
442 | 1 | pub fn build_minimal_phi2_gguf( |
443 | 1 | vocab_size: usize, |
444 | 1 | hidden_dim: usize, |
445 | 1 | intermediate_dim: usize, |
446 | 1 | num_heads: usize, |
447 | 1 | ) -> Vec<u8> { |
448 | | // Create tensor data |
449 | 1 | let embed_data = create_f32_embedding_data(vocab_size, hidden_dim); |
450 | 1 | let norm_data = create_f32_norm_weights(hidden_dim); |
451 | | |
452 | | // Fused QKV: hidden -> 3 * hidden |
453 | 1 | let qkv_out_dim = 3 * hidden_dim; |
454 | 1 | let qkv_data = create_q4_k_data(hidden_dim * qkv_out_dim); |
455 | 1 | let attn_out_data = create_q4_k_data(hidden_dim * hidden_dim); |
456 | 1 | let ffn_up_data = create_q4_k_data(hidden_dim * intermediate_dim); |
457 | 1 | let ffn_down_data = create_q4_k_data(intermediate_dim * hidden_dim); |
458 | | |
459 | 1 | GGUFBuilder::new() |
460 | | // Metadata |
461 | 1 | .architecture("phi2") |
462 | 1 | .hidden_dim("phi2", hidden_dim as u32) |
463 | 1 | .num_layers("phi2", 1) |
464 | 1 | .num_heads("phi2", num_heads as u32) |
465 | 1 | .num_kv_heads("phi2", num_heads as u32) // MHA, not GQA |
466 | 1 | .context_length("phi2", 256) |
467 | 1 | .rope_freq_base("phi2", 10000.0) |
468 | 1 | .rms_epsilon("phi2", 1e-5) |
469 | | // Token embedding |
470 | 1 | .add_f32_tensor( |
471 | 1 | "token_embd.weight", |
472 | 1 | &[vocab_size as u64, hidden_dim as u64], |
473 | 1 | &embed_data, |
474 | | ) |
475 | | // Layer 0 attention (fused QKV) |
476 | 1 | .add_f32_tensor("blk.0.attn_norm.weight", &[hidden_dim as u64], &norm_data) |
477 | 1 | .add_q4_k_tensor( |
478 | 1 | "blk.0.attn_qkv.weight", |
479 | 1 | &[hidden_dim as u64, qkv_out_dim as u64], |
480 | 1 | &qkv_data, |
481 | | ) |
482 | 1 | .add_q4_k_tensor( |
483 | 1 | "blk.0.attn_output.weight", |
484 | 1 | &[hidden_dim as u64, hidden_dim as u64], |
485 | 1 | &attn_out_data, |
486 | | ) |
487 | | // Layer 0 FFN (no gate for Phi-2 style GELU) |
488 | 1 | .add_f32_tensor("blk.0.ffn_norm.weight", &[hidden_dim as u64], &norm_data) |
489 | 1 | .add_q4_k_tensor( |
490 | 1 | "blk.0.ffn_up.weight", |
491 | 1 | &[hidden_dim as u64, intermediate_dim as u64], |
492 | 1 | &ffn_up_data, |
493 | | ) |
494 | 1 | .add_q4_k_tensor( |
495 | 1 | "blk.0.ffn_down.weight", |
496 | 1 | &[intermediate_dim as u64, hidden_dim as u64], |
497 | 1 | &ffn_down_data, |
498 | | ) |
499 | | // Output |
500 | 1 | .add_f32_tensor("output_norm.weight", &[hidden_dim as u64], &norm_data) |
501 | 1 | .build() |
502 | 1 | } |
503 | | |
504 | | #[cfg(test)] |
505 | | mod tests { |
506 | | use super::*; |
507 | | use crate::gguf::GGUFModel; |
508 | | |
509 | | #[test] |
510 | 1 | fn test_gguf_builder_empty() { |
511 | 1 | let data = GGUFBuilder::new().build(); |
512 | | |
513 | | // Should have valid header |
514 | 1 | assert!(data.len() >= 24); // magic + version + 2 counts |
515 | | |
516 | 1 | let model = GGUFModel::from_bytes(&data).expect("Should parse empty GGUF"); |
517 | 1 | assert_eq!(model.header.magic, GGUF_MAGIC); |
518 | 1 | assert_eq!(model.header.version, GGUF_VERSION_V3); |
519 | 1 | assert_eq!(model.metadata.len(), 0); |
520 | 1 | assert_eq!(model.tensors.len(), 0); |
521 | 1 | } |
522 | | |
523 | | #[test] |
524 | 1 | fn test_gguf_builder_metadata_only() { |
525 | 1 | let data = GGUFBuilder::new() |
526 | 1 | .architecture("llama") |
527 | 1 | .add_u32("test.value", 42) |
528 | 1 | .add_f32("test.float", 3.14) |
529 | 1 | .build(); |
530 | | |
531 | 1 | let model = GGUFModel::from_bytes(&data).expect("Should parse"); |
532 | 1 | assert_eq!(model.metadata.len(), 3); |
533 | 1 | assert_eq!(model.architecture(), Some("llama")); |
534 | 1 | } |
535 | | |
536 | | #[test] |
537 | 1 | fn test_gguf_builder_with_tensor() { |
538 | 1 | let data = GGUFBuilder::new() |
539 | 1 | .add_f32_tensor("test.weight", &[4, 8], &vec![0.0f32; 32]) |
540 | 1 | .build(); |
541 | | |
542 | 1 | let model = GGUFModel::from_bytes(&data).expect("Should parse"); |
543 | 1 | assert_eq!(model.tensors.len(), 1); |
544 | 1 | assert_eq!(model.tensors[0].name, "test.weight"); |
545 | 1 | assert_eq!(model.tensors[0].n_dims, 2); |
546 | 1 | } |
547 | | |
548 | | #[test] |
549 | 1 | fn test_gguf_builder_q4_k_tensor() { |
550 | 1 | let q4k_data = create_q4_k_data(256); |
551 | 1 | let data = GGUFBuilder::new() |
552 | 1 | .add_q4_k_tensor("layer.weight", &[256], &q4k_data) |
553 | 1 | .build(); |
554 | | |
555 | 1 | let model = GGUFModel::from_bytes(&data).expect("Should parse"); |
556 | 1 | assert_eq!(model.tensors[0].qtype, GGUF_TYPE_Q4_K); |
557 | 1 | } |
558 | | |
559 | | #[test] |
560 | 1 | fn test_minimal_llama_model() { |
561 | 1 | let data = build_minimal_llama_gguf(100, 64, 128, 4, 4); |
562 | | |
563 | 1 | let model = GGUFModel::from_bytes(&data).expect("Should parse minimal LLaMA"); |
564 | | |
565 | 1 | assert_eq!(model.architecture(), Some("llama")); |
566 | 1 | assert_eq!(model.embedding_dim(), Some(64)); |
567 | 1 | assert_eq!(model.num_layers(), Some(1)); |
568 | 1 | assert_eq!(model.num_heads(), Some(4)); |
569 | | |
570 | | // Should have all expected tensors |
571 | 11 | let tensor_names1 : Vec<_>1 = model.tensors.iter()1 .map1 (|t| t.name.as_str()).collect1 (); |
572 | 1 | assert!(tensor_names.contains(&"token_embd.weight")); |
573 | 1 | assert!(tensor_names.contains(&"blk.0.attn_q.weight")); |
574 | 1 | assert!(tensor_names.contains(&"blk.0.ffn_up.weight")); |
575 | 1 | assert!(tensor_names.contains(&"output_norm.weight")); |
576 | 1 | } |
577 | | } |