/home/noah/src/realizar/src/gguf/test_helpers.rs
Line | Count | Source |
1 | | //! Test helpers for GGUF module testing |
2 | | //! |
3 | | //! This module contains shared test utilities that are used across |
4 | | //! multiple test files in the GGUF module shatter. |
5 | | //! |
6 | | //! ## Contents |
7 | | //! |
8 | | //! - `create_test_model_with_config`: Creates a minimal OwnedQuantizedModel for testing |
9 | | //! - `create_q4k_test_data`: Creates Q4_K quantized tensor data for testing |
10 | | //! |
11 | | //! ## Usage |
12 | | //! |
13 | | //! ```rust,ignore |
14 | | //! #[cfg(test)] |
15 | | //! use crate::gguf::test_helpers::{create_test_model_with_config, create_q4k_test_data}; |
16 | | //! ``` |
17 | | |
18 | | use crate::gguf::{ |
19 | | GGUFConfig, OwnedQKVWeights, OwnedQuantizedLayer, OwnedQuantizedModel, OwnedQuantizedTensor, |
20 | | }; |
21 | | |
22 | | /// Create a test model with specific configuration |
23 | | /// |
24 | | /// This helper creates a minimal `OwnedQuantizedModel` with deterministic |
25 | | /// test weights for verifying attention, FFN, and other model operations. |
26 | | /// |
27 | | /// # Arguments |
28 | | /// |
29 | | /// * `config` - The model configuration to use |
30 | | /// |
31 | | /// # Returns |
32 | | /// |
33 | | /// An `OwnedQuantizedModel` with test weights |
34 | 115 | pub(crate) fn create_test_model_with_config(config: &GGUFConfig) -> OwnedQuantizedModel { |
35 | | // Create minimal model weights for testing |
36 | 115 | let vocab_size = config.vocab_size; |
37 | 115 | let hidden_dim = config.hidden_dim; |
38 | 115 | let intermediate_dim = config.intermediate_dim; |
39 | 115 | let kv_dim = config.num_kv_heads * (hidden_dim / config.num_heads); |
40 | | |
41 | | // QKV projection: hidden_dim -> hidden_dim + 2*kv_dim (Q + K + V) |
42 | 115 | let qkv_out_dim = hidden_dim + 2 * kv_dim; |
43 | 115 | let qkv_weight = create_q4k_test_data(hidden_dim, qkv_out_dim); |
44 | | |
45 | | // Output projection: hidden_dim -> hidden_dim |
46 | 115 | let attn_output_weight = create_q4k_test_data(hidden_dim, hidden_dim); |
47 | | |
48 | | // FFN weights |
49 | 115 | let ffn_up_weight = create_q4k_test_data(hidden_dim, intermediate_dim); |
50 | 115 | let ffn_down_weight = create_q4k_test_data(intermediate_dim, hidden_dim); |
51 | | |
52 | | // Layer norm weights |
53 | 115 | let attn_norm_weight = vec![1.0f32; hidden_dim]; |
54 | | |
55 | 115 | let layer = OwnedQuantizedLayer { |
56 | 115 | attn_norm_weight, |
57 | 115 | attn_norm_bias: None, |
58 | 115 | qkv_weight: OwnedQKVWeights::Fused(qkv_weight), |
59 | 115 | qkv_bias: None, |
60 | 115 | attn_output_weight, |
61 | 115 | attn_output_bias: None, |
62 | 115 | ffn_up_weight, |
63 | 115 | ffn_up_bias: None, |
64 | 115 | ffn_down_weight, |
65 | 115 | ffn_down_bias: None, |
66 | 115 | ffn_gate_weight: None, |
67 | 115 | ffn_gate_bias: None, |
68 | 115 | ffn_norm_weight: None, |
69 | 115 | ffn_norm_bias: None, |
70 | 115 | }; |
71 | | |
72 | 115 | let token_embedding = vec![0.1f32; vocab_size * hidden_dim]; |
73 | 115 | let output_norm_weight = vec![1.0f32; hidden_dim]; |
74 | 115 | let lm_head_weight = create_q4k_test_data(hidden_dim, vocab_size); |
75 | | |
76 | 115 | OwnedQuantizedModel { |
77 | 115 | config: config.clone(), |
78 | 115 | token_embedding, |
79 | 115 | layers: vec![layer], |
80 | 115 | output_norm_weight, |
81 | 115 | output_norm_bias: None, |
82 | 115 | lm_head_weight, |
83 | 115 | lm_head_bias: None, |
84 | 115 | #[cfg(feature = "cuda")] |
85 | 115 | cuda_executor: None, |
86 | 115 | #[cfg(feature = "cuda")] |
87 | 115 | cuda_kernel_count: std::sync::atomic::AtomicU64::new(0), |
88 | 115 | #[cfg(feature = "cuda")] |
89 | 115 | cached_weight_names: std::sync::Mutex::new(std::collections::HashSet::new()), |
90 | 115 | } |
91 | 115 | } |
92 | | |
93 | | /// Create Q4_K test data for given dimensions |
94 | | /// |
95 | | /// Q4_K uses row-major storage where each row has ceil(in_dim/256) super-blocks. |
96 | | /// Each super-block is 144 bytes and covers 256 values. |
97 | | /// |
98 | | /// # Arguments |
99 | | /// |
100 | | /// * `in_dim` - Input dimension (number of columns) |
101 | | /// * `out_dim` - Output dimension (number of rows) |
102 | | /// |
103 | | /// # Returns |
104 | | /// |
105 | | /// An `OwnedQuantizedTensor` with Q4_K quantized test data |
106 | 577 | pub(crate) fn create_q4k_test_data(in_dim: usize, out_dim: usize) -> OwnedQuantizedTensor { |
107 | | // Row-major storage: each row needs ceil(in_dim/256) super-blocks |
108 | 577 | let super_blocks_per_row = in_dim.div_ceil(256); |
109 | 577 | let bytes_per_row = super_blocks_per_row * 144; |
110 | 577 | let data_size = out_dim * bytes_per_row; |
111 | 577 | let mut data = vec![0u8; data_size]; |
112 | | |
113 | 81.3k | for row in 0..out_dim577 { |
114 | 89.1k | for sb in 0..super_blocks_per_row81.3k { |
115 | 89.1k | let offset = row * bytes_per_row + sb * 144; |
116 | | // d=1.0 in f16 format |
117 | 89.1k | data[offset..offset + 2].copy_from_slice(&0x3C00_u16.to_le_bytes()); |
118 | | // dmin=0 |
119 | 89.1k | data[offset + 2..offset + 4].copy_from_slice(&0x0000_u16.to_le_bytes()); |
120 | | // Fill scales and quantized values with deterministic test pattern |
121 | 12.5M | for i12.4M in 4..144 { |
122 | 12.4M | data[offset + i] = ((row + sb + i) % 16) as u8; |
123 | 12.4M | } |
124 | | } |
125 | | } |
126 | | |
127 | 577 | OwnedQuantizedTensor { |
128 | 577 | data, |
129 | 577 | in_dim, |
130 | 577 | out_dim, |
131 | 577 | qtype: 12, // Q4_K |
132 | 577 | } |
133 | 577 | } |
134 | | |
135 | | #[cfg(test)] |
136 | | mod tests { |
137 | | use super::*; |
138 | | |
139 | | #[test] |
140 | 1 | fn test_create_q4k_test_data_basic() { |
141 | 1 | let tensor = create_q4k_test_data(256, 64); |
142 | 1 | assert_eq!(tensor.in_dim, 256); |
143 | 1 | assert_eq!(tensor.out_dim, 64); |
144 | 1 | assert_eq!(tensor.qtype, 12); // Q4_K |
145 | | // 1 super-block per row, 144 bytes each, 64 rows |
146 | 1 | assert_eq!(tensor.data.len(), 64 * 144); |
147 | 1 | } |
148 | | |
149 | | #[test] |
150 | 1 | fn test_create_q4k_test_data_multi_superblock() { |
151 | | // 512 values needs 2 super-blocks per row |
152 | 1 | let tensor = create_q4k_test_data(512, 32); |
153 | 1 | assert_eq!(tensor.in_dim, 512); |
154 | 1 | assert_eq!(tensor.out_dim, 32); |
155 | | // 2 super-blocks per row, 144 bytes each, 32 rows |
156 | 1 | assert_eq!(tensor.data.len(), 32 * 2 * 144); |
157 | 1 | } |
158 | | |
159 | | #[test] |
160 | 1 | fn test_create_test_model_with_config_basic() { |
161 | 1 | let config = GGUFConfig { |
162 | 1 | architecture: "test".to_string(), |
163 | 1 | hidden_dim: 64, |
164 | 1 | intermediate_dim: 128, |
165 | 1 | num_heads: 4, |
166 | 1 | num_kv_heads: 4, |
167 | 1 | num_layers: 1, |
168 | 1 | vocab_size: 100, |
169 | 1 | rope_theta: 10000.0, |
170 | 1 | context_length: 512, |
171 | 1 | eps: 1e-5, |
172 | 1 | rope_type: 0, |
173 | 1 | }; |
174 | | |
175 | 1 | let model = create_test_model_with_config(&config); |
176 | 1 | assert_eq!(model.config.hidden_dim, 64); |
177 | 1 | assert_eq!(model.layers.len(), 1); |
178 | 1 | assert_eq!(model.token_embedding.len(), 100 * 64); |
179 | 1 | } |
180 | | } |