/home/noah/src/realizar/src/safetensors_infer.rs
Line | Count | Source |
1 | | //! SafeTensors Inference Support (PAR-301) |
2 | | //! |
3 | | //! Provides SafeTensors model loading and inference for HuggingFace models. |
4 | | //! |
5 | | //! ## Architecture |
6 | | //! |
7 | | //! SafeTensors files contain only tensor weights, so we need: |
8 | | //! - `config.json` for model architecture (hidden_size, num_layers, etc.) |
9 | | //! - `tokenizer.json` for text tokenization |
10 | | //! |
11 | | //! The converter loads these from sibling files and builds an AprTransformer. |
12 | | |
13 | | use crate::apr_transformer::{AprTransformer, AprTransformerConfig, AprTransformerLayer}; |
14 | | use crate::error::{RealizarError, Result}; |
15 | | use crate::safetensors::{MappedSafeTensorsModel, SafetensorsConfig}; |
16 | | use std::path::Path; |
17 | | |
18 | | /// SafeTensors to APR Transformer converter |
19 | | /// |
20 | | /// Converts HuggingFace SafeTensors models to APR Transformer format. |
21 | | /// Supports BF16, F16, and F32 weights with automatic conversion to F32. |
22 | | pub struct SafetensorsToAprConverter; |
23 | | |
24 | | impl SafetensorsToAprConverter { |
25 | | /// Convert SafeTensors model to APR Transformer |
26 | | /// |
27 | | /// # Arguments |
28 | | /// |
29 | | /// * `model_path` - Path to model.safetensors file |
30 | | /// |
31 | | /// # Returns |
32 | | /// |
33 | | /// `AprTransformer` with F32 weights ready for inference |
34 | | /// |
35 | | /// # Errors |
36 | | /// |
37 | | /// Returns error if SafeTensors file, config.json, or required tensors are missing |
38 | 20 | pub fn convert(model_path: &Path) -> Result<AprTransformer> { |
39 | | // Load SafeTensors model using mmap for zero-copy access (T-QA-020) |
40 | | // This is critical for fast model loading - mmap is O(1) regardless of file size |
41 | 20 | let st_model18 = MappedSafeTensorsModel::load(model_path)?2 ; |
42 | | |
43 | | // Load config.json (required for architecture info) |
44 | 18 | let config16 = SafetensorsConfig::load_from_sibling(model_path).ok_or_else(|| {2 |
45 | 2 | RealizarError::UnsupportedOperation { |
46 | 2 | operation: "safetensors_convert".to_string(), |
47 | 2 | reason: "config.json not found (required for SafeTensors inference)".to_string(), |
48 | 2 | } |
49 | 2 | })?; |
50 | | |
51 | | // Extract architecture parameters |
52 | 16 | let hidden_dim15 = config |
53 | 16 | .hidden_size |
54 | 16 | .ok_or_else(|| RealizarError::FormatError { |
55 | 1 | reason: "config.json missing hidden_size".to_string(), |
56 | 1 | })?; |
57 | 15 | let num_layers14 = config |
58 | 15 | .num_hidden_layers |
59 | 15 | .ok_or_else(|| RealizarError::FormatError { |
60 | 1 | reason: "config.json missing num_hidden_layers".to_string(), |
61 | 1 | })?; |
62 | 14 | let num_heads13 = config |
63 | 14 | .num_attention_heads |
64 | 14 | .ok_or_else(|| RealizarError::FormatError { |
65 | 1 | reason: "config.json missing num_attention_heads".to_string(), |
66 | 1 | })?; |
67 | 13 | let num_kv_heads = config.num_kv_heads(); |
68 | 13 | let vocab_size12 = config |
69 | 13 | .vocab_size |
70 | 13 | .ok_or_else(|| RealizarError::FormatError { |
71 | 1 | reason: "config.json missing vocab_size".to_string(), |
72 | 1 | })?; |
73 | 12 | let intermediate_dim = config.intermediate_size.unwrap_or(hidden_dim * 4); |
74 | 12 | let context_length = config.max_position_embeddings.unwrap_or(2048); |
75 | 12 | let rope_theta = config.rope_theta.unwrap_or(10000.0); |
76 | 12 | let eps = config.rms_norm_eps.unwrap_or(1e-6); |
77 | 12 | let architecture = config.architecture(); |
78 | | |
79 | | // Build transformer config |
80 | 12 | let apr_config = AprTransformerConfig { |
81 | 12 | architecture, |
82 | 12 | hidden_dim, |
83 | 12 | num_layers, |
84 | 12 | num_heads, |
85 | 12 | num_kv_heads, |
86 | 12 | vocab_size, |
87 | 12 | intermediate_dim, |
88 | 12 | context_length, |
89 | 12 | rope_theta, |
90 | 12 | eps, |
91 | 12 | }; |
92 | | |
93 | | // Extract embeddings |
94 | 12 | let token_embedding11 = st_model.get_tensor_auto("model.embed_tokens.weight")?1 ; |
95 | | |
96 | | // Extract output norm |
97 | 11 | let output_norm_weight = st_model.get_tensor_auto("model.norm.weight")?0 ; |
98 | | |
99 | | // Check for tied embeddings (lm_head = embed_tokens.T) |
100 | | // lm_head.weight: [vocab_size, hidden_dim] -> transpose -> [hidden_dim, vocab_size] |
101 | 11 | let lm_head_weight = if st_model.has_tensor("lm_head.weight") { |
102 | 1 | let raw = st_model.get_tensor_auto("lm_head.weight")?0 ; |
103 | 1 | Self::transpose_weight(&raw, vocab_size, hidden_dim) |
104 | | } else { |
105 | | // Tied embeddings: token_embedding is [vocab_size, hidden_dim] |
106 | | // Need to transpose to [hidden_dim, vocab_size] |
107 | 10 | Self::transpose_weight(&token_embedding, vocab_size, hidden_dim) |
108 | | }; |
109 | | |
110 | | // Extract layers |
111 | 11 | let mut layers = Vec::with_capacity(num_layers); |
112 | 11 | for i4 in 0..num_layers { |
113 | 4 | let layer3 = Self::extract_layer( |
114 | 4 | &st_model, |
115 | 4 | i, |
116 | 4 | hidden_dim, |
117 | 4 | num_heads, |
118 | 4 | num_kv_heads, |
119 | 4 | intermediate_dim, |
120 | 1 | )?; |
121 | 3 | layers.push(layer); |
122 | | } |
123 | | |
124 | 10 | Ok(AprTransformer { |
125 | 10 | config: apr_config, |
126 | 10 | token_embedding, |
127 | 10 | layers, |
128 | 10 | output_norm_weight, |
129 | 10 | output_norm_bias: None, |
130 | 10 | lm_head_weight, |
131 | 10 | lm_head_bias: None, |
132 | 10 | q4k_layers: None, |
133 | 10 | lm_head_weight_q6k: None, |
134 | 10 | lm_head_weight_q4k: None, |
135 | 10 | }) |
136 | 20 | } |
137 | | |
138 | | /// Extract a single transformer layer from SafeTensors |
139 | 4 | fn extract_layer( |
140 | 4 | st_model: &MappedSafeTensorsModel, |
141 | 4 | layer_idx: usize, |
142 | 4 | hidden_dim: usize, |
143 | 4 | num_heads: usize, |
144 | 4 | num_kv_heads: usize, |
145 | 4 | intermediate_dim: usize, |
146 | 4 | ) -> Result<AprTransformerLayer> { |
147 | 4 | let prefix = format!("model.layers.{layer_idx}"); |
148 | | |
149 | | // Attention norm (input_layernorm) - 1D vector, no transpose needed |
150 | 3 | let attn_norm_weight = |
151 | 4 | st_model.get_tensor_auto(&format!("{prefix}.input_layernorm.weight"))?1 ; |
152 | | |
153 | | // Q, K, V projections (separate in HuggingFace, combined in APR) |
154 | | // HuggingFace: [out_dim, in_dim], APR needs: [in_dim, out_dim] |
155 | 3 | let q_weight = st_model.get_tensor_auto(&format!("{prefix}.self_attn.q_proj.weight"))?0 ; |
156 | 3 | let k_weight = st_model.get_tensor_auto(&format!("{prefix}.self_attn.k_proj.weight"))?0 ; |
157 | 3 | let v_weight = st_model.get_tensor_auto(&format!("{prefix}.self_attn.v_proj.weight"))?0 ; |
158 | | |
159 | | // Concatenate and transpose Q, K, V into combined QKV weight |
160 | 3 | let head_dim = hidden_dim / num_heads; |
161 | 3 | let kv_dim = head_dim * num_kv_heads; |
162 | 3 | let qkv_weight = |
163 | 3 | Self::concat_qkv_transposed(&q_weight, &k_weight, &v_weight, hidden_dim, kv_dim); |
164 | | |
165 | | // QKV bias (optional) - 1D vector, no transpose needed |
166 | 3 | let qkv_bias = Self::try_concat_qkv_bias(st_model, &prefix, hidden_dim, kv_dim); |
167 | | |
168 | | // Attention output projection: [hidden_dim, hidden_dim] |
169 | | // HuggingFace: o_proj is [hidden_dim, hidden_dim] (out=hidden, in=hidden) |
170 | 3 | let attn_output_raw = |
171 | 3 | st_model.get_tensor_auto(&format!("{prefix}.self_attn.o_proj.weight"))?0 ; |
172 | 3 | let attn_output_weight = Self::transpose_weight(&attn_output_raw, hidden_dim, hidden_dim); |
173 | | |
174 | | // FFN norm (post_attention_layernorm) - 1D vector, no transpose needed |
175 | 3 | let ffn_norm_weight = |
176 | 3 | st_model.get_tensor_auto(&format!("{prefix}.post_attention_layernorm.weight"))?0 ; |
177 | | |
178 | | // FFN projections (SwiGLU architecture) |
179 | | // gate_proj: [intermediate_dim, hidden_dim] -> transpose -> [hidden_dim, intermediate_dim] |
180 | | // up_proj: [intermediate_dim, hidden_dim] -> transpose -> [hidden_dim, intermediate_dim] |
181 | | // down_proj: [hidden_dim, intermediate_dim] -> transpose -> [intermediate_dim, hidden_dim] |
182 | 3 | let ffn_gate_raw = st_model.get_tensor_auto(&format!("{prefix}.mlp.gate_proj.weight"))?0 ; |
183 | 3 | let ffn_gate_weight = Self::transpose_weight(&ffn_gate_raw, intermediate_dim, hidden_dim); |
184 | | |
185 | 3 | let ffn_up_raw = st_model.get_tensor_auto(&format!("{prefix}.mlp.up_proj.weight"))?0 ; |
186 | 3 | let ffn_up_weight = Self::transpose_weight(&ffn_up_raw, intermediate_dim, hidden_dim); |
187 | | |
188 | 3 | let ffn_down_raw = st_model.get_tensor_auto(&format!("{prefix}.mlp.down_proj.weight"))?0 ; |
189 | 3 | let ffn_down_weight = Self::transpose_weight(&ffn_down_raw, hidden_dim, intermediate_dim); |
190 | | |
191 | 3 | Ok(AprTransformerLayer { |
192 | 3 | attn_norm_weight, |
193 | 3 | attn_norm_bias: None, |
194 | 3 | qkv_weight, |
195 | 3 | qkv_bias, |
196 | 3 | attn_output_weight, |
197 | 3 | attn_output_bias: None, |
198 | 3 | ffn_gate_weight: Some(ffn_gate_weight), |
199 | 3 | ffn_gate_bias: None, |
200 | 3 | ffn_up_weight, |
201 | 3 | ffn_up_bias: None, |
202 | 3 | ffn_down_weight, |
203 | 3 | ffn_down_bias: None, |
204 | 3 | ffn_norm_weight: Some(ffn_norm_weight), |
205 | 3 | ffn_norm_bias: None, |
206 | 3 | }) |
207 | 4 | } |
208 | | |
209 | | /// Pass through weight in matvec-optimal [out_dim, in_dim] format |
210 | | /// |
211 | | /// PMAT-095 FIX: HuggingFace stores Linear weights as [out_features, in_features] |
212 | | /// which is EXACTLY what trueno's matvec needs! Previous implementation transposed |
213 | | /// twice (here and in matmul), causing O(n²) overhead per forward pass. |
214 | | /// |
215 | | /// Now we keep HuggingFace format directly - no transposition needed. |
216 | | #[allow(clippy::unused_self)] |
217 | 23 | fn transpose_weight(weight: &[f32], _out_dim: usize, _in_dim: usize) -> Vec<f32> { |
218 | | // PMAT-095: Keep [out_dim, in_dim] format - no transposition! |
219 | | // This eliminates the 75x performance gap vs GGUF. |
220 | 23 | weight.to_vec() |
221 | 23 | } |
222 | | |
223 | | /// Concatenate Q, K, V weights into combined QKV tensor (matvec-optimal) |
224 | | /// |
225 | | /// PMAT-095 FIX: Keep [out_dim, in_dim] format from HuggingFace. |
226 | | /// For QKV, we concatenate along the output dimension: |
227 | | /// - Q: [hidden_dim, hidden_dim] |
228 | | /// - K: [kv_dim, hidden_dim] |
229 | | /// - V: [kv_dim, hidden_dim] |
230 | | /// |
231 | | /// Result: [hidden_dim + kv_dim + kv_dim, hidden_dim] in row-major |
232 | 3 | fn concat_qkv_transposed( |
233 | 3 | q: &[f32], |
234 | 3 | k: &[f32], |
235 | 3 | v: &[f32], |
236 | 3 | _hidden_dim: usize, |
237 | 3 | _kv_dim: usize, |
238 | 3 | ) -> Vec<f32> { |
239 | | // PMAT-095: Simple concatenation - weights are already in optimal layout |
240 | | // Concatenate [Q; K; V] along output dimension |
241 | 3 | let mut qkv = Vec::with_capacity(q.len() + k.len() + v.len()); |
242 | 3 | qkv.extend_from_slice(q); |
243 | 3 | qkv.extend_from_slice(k); |
244 | 3 | qkv.extend_from_slice(v); |
245 | 3 | qkv |
246 | 3 | } |
247 | | |
248 | | /// Concatenate Q, K, V weights into combined QKV tensor (legacy, no transpose) |
249 | 5 | fn concat_qkv(q: &[f32], k: &[f32], v: &[f32]) -> Vec<f32> { |
250 | 5 | let mut qkv = Vec::with_capacity(q.len() + k.len() + v.len()); |
251 | 5 | qkv.extend_from_slice(q); |
252 | 5 | qkv.extend_from_slice(k); |
253 | 5 | qkv.extend_from_slice(v); |
254 | 5 | qkv |
255 | 5 | } |
256 | | |
257 | | /// Try to concatenate Q, K, V biases if they exist |
258 | 6 | fn try_concat_qkv_bias( |
259 | 6 | st_model: &MappedSafeTensorsModel, |
260 | 6 | prefix: &str, |
261 | 6 | hidden_dim: usize, |
262 | 6 | kv_dim: usize, |
263 | 6 | ) -> Option<Vec<f32>> { |
264 | 6 | let q_bias2 = st_model |
265 | 6 | .get_tensor_auto(&format!("{prefix}.self_attn.q_proj.bias")) |
266 | 6 | .ok()?4 ; |
267 | 2 | let k_bias1 = st_model |
268 | 2 | .get_tensor_auto(&format!("{prefix}.self_attn.k_proj.bias")) |
269 | 2 | .ok()?1 ; |
270 | 1 | let v_bias = st_model |
271 | 1 | .get_tensor_auto(&format!("{prefix}.self_attn.v_proj.bias")) |
272 | 1 | .ok()?0 ; |
273 | | |
274 | 1 | let mut qkv_bias = Vec::with_capacity(hidden_dim + kv_dim + kv_dim); |
275 | 1 | qkv_bias.extend_from_slice(&q_bias); |
276 | 1 | qkv_bias.extend_from_slice(&k_bias); |
277 | 1 | qkv_bias.extend_from_slice(&v_bias); |
278 | | |
279 | 1 | Some(qkv_bias) |
280 | 6 | } |
281 | | } |
282 | | |
283 | | #[cfg(test)] |
284 | | mod tests { |
285 | | use super::*; |
286 | | use tempfile::TempDir; |
287 | | |
288 | | #[test] |
289 | 1 | fn test_concat_qkv() { |
290 | 1 | let q = vec![1.0, 2.0]; |
291 | 1 | let k = vec![3.0, 4.0]; |
292 | 1 | let v = vec![5.0, 6.0]; |
293 | 1 | let qkv = SafetensorsToAprConverter::concat_qkv(&q, &k, &v); |
294 | 1 | assert_eq!(qkv, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); |
295 | 1 | } |
296 | | |
297 | | // ========================================================================= |
298 | | // Extended Coverage Tests (15+ tests ending with _ext_cov) |
299 | | // ========================================================================= |
300 | | |
301 | | /// Helper function to create a minimal SafeTensors file with given tensors |
302 | 18 | fn create_safetensors_bytes(tensors: &[(&str, &str, &[usize], &[u8])]) -> Vec<u8> { |
303 | | use serde_json::json; |
304 | | |
305 | | // Calculate tensor data layout |
306 | 18 | let mut tensor_entries = serde_json::Map::new(); |
307 | 18 | let mut offset = 0usize; |
308 | | |
309 | 41 | for (name23 , dtype23 , shape23 , data23 ) in tensors { |
310 | 23 | let end = offset + data.len(); |
311 | 23 | tensor_entries.insert( |
312 | 23 | (*name).to_string(), |
313 | 23 | json!({ |
314 | 23 | "dtype": dtype, |
315 | 23 | "shape": shape, |
316 | 23 | "data_offsets": [offset, end] |
317 | 23 | }), |
318 | 23 | ); |
319 | 23 | offset = end; |
320 | 23 | } |
321 | | |
322 | 18 | let json_obj = serde_json::Value::Object(tensor_entries); |
323 | 18 | let json_bytes = json_obj.to_string().into_bytes(); |
324 | | |
325 | 18 | let mut data = Vec::new(); |
326 | 18 | data.extend_from_slice(&(json_bytes.len() as u64).to_le_bytes()); |
327 | 18 | data.extend_from_slice(&json_bytes); |
328 | | |
329 | | // Append tensor data |
330 | 41 | for (_, _, _, tensor_data23 ) in tensors { |
331 | 23 | data.extend_from_slice(tensor_data); |
332 | 23 | } |
333 | | |
334 | 18 | data |
335 | 18 | } |
336 | | |
337 | | /// Helper to create config.json content |
338 | 5 | fn create_config_json( |
339 | 5 | hidden_size: usize, |
340 | 5 | num_layers: usize, |
341 | 5 | num_heads: usize, |
342 | 5 | vocab_size: usize, |
343 | 5 | ) -> String { |
344 | 5 | format!( |
345 | 5 | r#"{{ |
346 | 5 | "hidden_size": {}, |
347 | 5 | "num_hidden_layers": {}, |
348 | 5 | "num_attention_heads": {}, |
349 | 5 | "vocab_size": {}, |
350 | 5 | "intermediate_size": {}, |
351 | 5 | "max_position_embeddings": 2048, |
352 | 5 | "rms_norm_eps": 1e-6, |
353 | 5 | "rope_theta": 10000.0, |
354 | 5 | "architectures": ["LlamaForCausalLM"], |
355 | 5 | "model_type": "llama" |
356 | 5 | }}"#, |
357 | | hidden_size, |
358 | | num_layers, |
359 | | num_heads, |
360 | | vocab_size, |
361 | 5 | hidden_size * 4 |
362 | | ) |
363 | 5 | } |
364 | | |
365 | | #[test] |
366 | 1 | fn test_convert_file_not_found_ext_cov() { |
367 | 1 | let result = |
368 | 1 | SafetensorsToAprConverter::convert(Path::new("/nonexistent/model.safetensors")); |
369 | 1 | assert!(result.is_err()); |
370 | | // MappedSafeTensorsModel::load() returns UnsupportedOperation for file open errors |
371 | 1 | if let Err(RealizarError::UnsupportedOperation { operation, reason }) = result { |
372 | 1 | assert_eq!(operation, "open_safetensors"); |
373 | 1 | assert!(reason.contains("Failed to open file")); |
374 | | } else { |
375 | 0 | panic!("Expected UnsupportedOperation error"); |
376 | | } |
377 | 1 | } |
378 | | |
379 | | #[test] |
380 | 1 | fn test_convert_missing_config_json_ext_cov() { |
381 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
382 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
383 | | |
384 | | // Create a minimal valid safetensors file |
385 | 1 | let data = create_safetensors_bytes(&[]); |
386 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
387 | | |
388 | | // No config.json file |
389 | | |
390 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
391 | 1 | assert!(result.is_err()); |
392 | 1 | if let Err(RealizarError::UnsupportedOperation { operation, reason }) = result { |
393 | 1 | assert_eq!(operation, "safetensors_convert"); |
394 | 1 | assert!(reason.contains("config.json not found")); |
395 | | } else { |
396 | 0 | panic!("Expected UnsupportedOperation error"); |
397 | | } |
398 | 1 | } |
399 | | |
400 | | #[test] |
401 | 1 | fn test_convert_missing_hidden_size_ext_cov() { |
402 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
403 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
404 | 1 | let config_path = temp_dir.path().join("config.json"); |
405 | | |
406 | | // Create minimal safetensors |
407 | 1 | let data = create_safetensors_bytes(&[]); |
408 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
409 | | |
410 | | // Config missing hidden_size |
411 | 1 | let config = r#"{"num_hidden_layers": 2, "num_attention_heads": 4, "vocab_size": 100}"#; |
412 | 1 | std::fs::write(&config_path, config).expect("write config"); |
413 | | |
414 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
415 | 1 | assert!(result.is_err()); |
416 | 1 | if let Err(RealizarError::FormatError { reason }) = result { |
417 | 1 | assert!(reason.contains("missing hidden_size")); |
418 | | } else { |
419 | 0 | panic!("Expected FormatError for missing hidden_size"); |
420 | | } |
421 | 1 | } |
422 | | |
423 | | #[test] |
424 | 1 | fn test_convert_missing_num_hidden_layers_ext_cov() { |
425 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
426 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
427 | 1 | let config_path = temp_dir.path().join("config.json"); |
428 | | |
429 | 1 | let data = create_safetensors_bytes(&[]); |
430 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
431 | | |
432 | | // Config missing num_hidden_layers |
433 | 1 | let config = r#"{"hidden_size": 64, "num_attention_heads": 4, "vocab_size": 100}"#; |
434 | 1 | std::fs::write(&config_path, config).expect("write config"); |
435 | | |
436 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
437 | 1 | assert!(result.is_err()); |
438 | 1 | if let Err(RealizarError::FormatError { reason }) = result { |
439 | 1 | assert!(reason.contains("missing num_hidden_layers")); |
440 | | } else { |
441 | 0 | panic!("Expected FormatError for missing num_hidden_layers"); |
442 | | } |
443 | 1 | } |
444 | | |
445 | | #[test] |
446 | 1 | fn test_convert_missing_num_attention_heads_ext_cov() { |
447 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
448 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
449 | 1 | let config_path = temp_dir.path().join("config.json"); |
450 | | |
451 | 1 | let data = create_safetensors_bytes(&[]); |
452 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
453 | | |
454 | | // Config missing num_attention_heads |
455 | 1 | let config = r#"{"hidden_size": 64, "num_hidden_layers": 2, "vocab_size": 100}"#; |
456 | 1 | std::fs::write(&config_path, config).expect("write config"); |
457 | | |
458 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
459 | 1 | assert!(result.is_err()); |
460 | 1 | if let Err(RealizarError::FormatError { reason }) = result { |
461 | 1 | assert!(reason.contains("missing num_attention_heads")); |
462 | | } else { |
463 | 0 | panic!("Expected FormatError for missing num_attention_heads"); |
464 | | } |
465 | 1 | } |
466 | | |
467 | | #[test] |
468 | 1 | fn test_convert_missing_vocab_size_ext_cov() { |
469 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
470 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
471 | 1 | let config_path = temp_dir.path().join("config.json"); |
472 | | |
473 | 1 | let data = create_safetensors_bytes(&[]); |
474 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
475 | | |
476 | | // Config missing vocab_size |
477 | 1 | let config = r#"{"hidden_size": 64, "num_hidden_layers": 2, "num_attention_heads": 4}"#; |
478 | 1 | std::fs::write(&config_path, config).expect("write config"); |
479 | | |
480 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
481 | 1 | assert!(result.is_err()); |
482 | 1 | if let Err(RealizarError::FormatError { reason }) = result { |
483 | 1 | assert!(reason.contains("missing vocab_size")); |
484 | | } else { |
485 | 0 | panic!("Expected FormatError for missing vocab_size"); |
486 | | } |
487 | 1 | } |
488 | | |
489 | | #[test] |
490 | 1 | fn test_convert_missing_embed_tokens_ext_cov() { |
491 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
492 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
493 | 1 | let config_path = temp_dir.path().join("config.json"); |
494 | | |
495 | | // Safetensors without model.embed_tokens.weight |
496 | 1 | let data = create_safetensors_bytes(&[]); |
497 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
498 | | |
499 | | // Valid config |
500 | 1 | let config = create_config_json(64, 1, 4, 100); |
501 | 1 | std::fs::write(&config_path, config).expect("write config"); |
502 | | |
503 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
504 | 1 | assert!(result.is_err()); |
505 | | // Should fail because model.embed_tokens.weight is missing |
506 | 1 | } |
507 | | |
508 | | #[test] |
509 | 1 | fn test_concat_qkv_empty_inputs_ext_cov() { |
510 | 1 | let q: Vec<f32> = vec![]; |
511 | 1 | let k: Vec<f32> = vec![]; |
512 | 1 | let v: Vec<f32> = vec![]; |
513 | 1 | let qkv = SafetensorsToAprConverter::concat_qkv(&q, &k, &v); |
514 | 1 | assert!(qkv.is_empty()); |
515 | 1 | } |
516 | | |
517 | | #[test] |
518 | 1 | fn test_concat_qkv_single_elements_ext_cov() { |
519 | 1 | let q = vec![1.0]; |
520 | 1 | let k = vec![2.0]; |
521 | 1 | let v = vec![3.0]; |
522 | 1 | let qkv = SafetensorsToAprConverter::concat_qkv(&q, &k, &v); |
523 | 1 | assert_eq!(qkv, vec![1.0, 2.0, 3.0]); |
524 | 1 | } |
525 | | |
526 | | #[test] |
527 | 1 | fn test_concat_qkv_large_arrays_ext_cov() { |
528 | 1.00k | let q1 : Vec<f32>1 = (0..1000)1 .map1 (|i| i as f32).collect1 (); |
529 | 1.00k | let k1 : Vec<f32>1 = (1000..2000)1 .map1 (|i| i as f32).collect1 (); |
530 | 1.00k | let v1 : Vec<f32>1 = (2000..3000)1 .map1 (|i| i as f32).collect1 (); |
531 | 1 | let qkv = SafetensorsToAprConverter::concat_qkv(&q, &k, &v); |
532 | 1 | assert_eq!(qkv.len(), 3000); |
533 | 1 | assert_eq!(qkv[0], 0.0); |
534 | 1 | assert_eq!(qkv[1000], 1000.0); |
535 | 1 | assert_eq!(qkv[2000], 2000.0); |
536 | 1 | assert_eq!(qkv[2999], 2999.0); |
537 | 1 | } |
538 | | |
539 | | #[test] |
540 | 1 | fn test_concat_qkv_asymmetric_ext_cov() { |
541 | 1 | let q = vec![1.0, 2.0, 3.0, 4.0]; |
542 | 1 | let k = vec![5.0, 6.0]; |
543 | 1 | let v = vec![7.0]; |
544 | 1 | let qkv = SafetensorsToAprConverter::concat_qkv(&q, &k, &v); |
545 | 1 | assert_eq!(qkv, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]); |
546 | 1 | } |
547 | | |
548 | | #[test] |
549 | 1 | fn test_try_concat_qkv_bias_none_when_missing_ext_cov() { |
550 | | // Create safetensors model without any biases |
551 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
552 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
553 | 1 | let data = create_safetensors_bytes(&[]); |
554 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
555 | 1 | let st_model = MappedSafeTensorsModel::load(&model_path).expect("load safetensors"); |
556 | | |
557 | 1 | let result = |
558 | 1 | SafetensorsToAprConverter::try_concat_qkv_bias(&st_model, "model.layers.0", 64, 64); |
559 | 1 | assert!(result.is_none()); |
560 | 1 | } |
561 | | |
562 | | #[test] |
563 | 1 | fn test_try_concat_qkv_bias_partial_missing_ext_cov() { |
564 | | // Create safetensors with only q_proj.bias (missing k and v) |
565 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
566 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
567 | 16 | let q_bias_data1 : Vec<u8>1 = (0..16)1 .flat_map1 (|i| (i as f32).to_le_bytes()).collect1 (); |
568 | 1 | let data = create_safetensors_bytes(&[( |
569 | 1 | "model.layers.0.self_attn.q_proj.bias", |
570 | 1 | "F32", |
571 | 1 | &[4], |
572 | 1 | &q_bias_data, |
573 | 1 | )]); |
574 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
575 | 1 | let st_model = MappedSafeTensorsModel::load(&model_path).expect("load safetensors"); |
576 | | |
577 | | // Should return None because k_bias and v_bias are missing |
578 | 1 | let result = |
579 | 1 | SafetensorsToAprConverter::try_concat_qkv_bias(&st_model, "model.layers.0", 4, 4); |
580 | 1 | assert!(result.is_none()); |
581 | 1 | } |
582 | | |
583 | | #[test] |
584 | 1 | fn test_try_concat_qkv_bias_all_present_ext_cov() { |
585 | | // Create F32 byte data for biases |
586 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
587 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
588 | 1 | let q_bias_data: Vec<u8> = [1.0f32, 2.0, 3.0, 4.0] |
589 | 1 | .iter() |
590 | 4 | .flat_map1 (|f| f.to_le_bytes()) |
591 | 1 | .collect(); |
592 | 2 | let k_bias_data1 : Vec<u8>1 = [5.0f32, 6.0]1 .iter1 ().flat_map1 (|f| f.to_le_bytes()).collect1 (); |
593 | 2 | let v_bias_data1 : Vec<u8>1 = [7.0f32, 8.0]1 .iter1 ().flat_map1 (|f| f.to_le_bytes()).collect1 (); |
594 | | |
595 | 1 | let data = create_safetensors_bytes(&[ |
596 | 1 | ( |
597 | 1 | "model.layers.0.self_attn.q_proj.bias", |
598 | 1 | "F32", |
599 | 1 | &[4], |
600 | 1 | &q_bias_data, |
601 | 1 | ), |
602 | 1 | ( |
603 | 1 | "model.layers.0.self_attn.k_proj.bias", |
604 | 1 | "F32", |
605 | 1 | &[2], |
606 | 1 | &k_bias_data, |
607 | 1 | ), |
608 | 1 | ( |
609 | 1 | "model.layers.0.self_attn.v_proj.bias", |
610 | 1 | "F32", |
611 | 1 | &[2], |
612 | 1 | &v_bias_data, |
613 | 1 | ), |
614 | 1 | ]); |
615 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
616 | 1 | let st_model = MappedSafeTensorsModel::load(&model_path).expect("load safetensors"); |
617 | | |
618 | 1 | let result = |
619 | 1 | SafetensorsToAprConverter::try_concat_qkv_bias(&st_model, "model.layers.0", 4, 2); |
620 | 1 | assert!(result.is_some()); |
621 | 1 | let bias = result.expect("operation failed"); |
622 | 1 | assert_eq!(bias.len(), 8); // 4 + 2 + 2 |
623 | 1 | assert_eq!(bias[0], 1.0); |
624 | 1 | assert_eq!(bias[4], 5.0); |
625 | 1 | assert_eq!(bias[6], 7.0); |
626 | 1 | } |
627 | | |
628 | | #[test] |
629 | 1 | fn test_convert_defaults_intermediate_size_ext_cov() { |
630 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
631 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
632 | 1 | let config_path = temp_dir.path().join("config.json"); |
633 | | |
634 | | // Config without intermediate_size (should default to hidden_size * 4) |
635 | 1 | let config = r#"{ |
636 | 1 | "hidden_size": 64, |
637 | 1 | "num_hidden_layers": 0, |
638 | 1 | "num_attention_heads": 4, |
639 | 1 | "vocab_size": 100 |
640 | 1 | }"#; |
641 | 1 | std::fs::write(&config_path, config).expect("write config"); |
642 | | |
643 | | // Create safetensors with minimal required tensors for 0 layers |
644 | 1 | let embed_data: Vec<u8> = vec![0u8; 100 * 64 * 4]; // vocab_size * hidden_dim * 4 bytes |
645 | 1 | let norm_data: Vec<u8> = vec![0u8; 64 * 4]; // hidden_dim * 4 bytes |
646 | | |
647 | 1 | let data = create_safetensors_bytes(&[ |
648 | 1 | ("model.embed_tokens.weight", "F32", &[100, 64], &embed_data), |
649 | 1 | ("model.norm.weight", "F32", &[64], &norm_data), |
650 | 1 | ]); |
651 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
652 | | |
653 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
654 | 1 | assert!(result.is_ok()); |
655 | 1 | let transformer = result.expect("operation failed"); |
656 | 1 | assert_eq!(transformer.config.intermediate_dim, 64 * 4); |
657 | 1 | } |
658 | | |
659 | | #[test] |
660 | 1 | fn test_convert_defaults_context_length_ext_cov() { |
661 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
662 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
663 | 1 | let config_path = temp_dir.path().join("config.json"); |
664 | | |
665 | | // Config without max_position_embeddings (should default to 2048) |
666 | 1 | let config = r#"{ |
667 | 1 | "hidden_size": 64, |
668 | 1 | "num_hidden_layers": 0, |
669 | 1 | "num_attention_heads": 4, |
670 | 1 | "vocab_size": 100, |
671 | 1 | "intermediate_size": 256 |
672 | 1 | }"#; |
673 | 1 | std::fs::write(&config_path, config).expect("write config"); |
674 | | |
675 | 1 | let embed_data: Vec<u8> = vec![0u8; 100 * 64 * 4]; |
676 | 1 | let norm_data: Vec<u8> = vec![0u8; 64 * 4]; |
677 | | |
678 | 1 | let data = create_safetensors_bytes(&[ |
679 | 1 | ("model.embed_tokens.weight", "F32", &[100, 64], &embed_data), |
680 | 1 | ("model.norm.weight", "F32", &[64], &norm_data), |
681 | 1 | ]); |
682 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
683 | | |
684 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
685 | 1 | assert!(result.is_ok()); |
686 | 1 | let transformer = result.expect("operation failed"); |
687 | 1 | assert_eq!(transformer.config.context_length, 2048); |
688 | 1 | } |
689 | | |
690 | | #[test] |
691 | 1 | fn test_convert_tied_embeddings_ext_cov() { |
692 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
693 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
694 | 1 | let config_path = temp_dir.path().join("config.json"); |
695 | | |
696 | 1 | let config = create_config_json(64, 0, 4, 100); |
697 | 1 | std::fs::write(&config_path, config).expect("write config"); |
698 | | |
699 | | // Create tensors WITHOUT lm_head.weight (tied embeddings) |
700 | 1 | let embed_data: Vec<u8> = (0..(100 * 64)) |
701 | 6.40k | .flat_map1 (|i| (i as f32).to_le_bytes()) |
702 | 1 | .collect(); |
703 | 64 | let norm_data1 : Vec<u8>1 = (0..64)1 .flat_map1 (|_| 1.0f32.to_le_bytes()).collect1 (); |
704 | | |
705 | 1 | let data = create_safetensors_bytes(&[ |
706 | 1 | ("model.embed_tokens.weight", "F32", &[100, 64], &embed_data), |
707 | 1 | ("model.norm.weight", "F32", &[64], &norm_data), |
708 | 1 | ]); |
709 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
710 | | |
711 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
712 | 1 | assert!(result.is_ok()); |
713 | 1 | let transformer = result.expect("operation failed"); |
714 | | |
715 | | // lm_head_weight should have same dimensions as token_embedding (tied or separate) |
716 | | // When tied: lm_head_weight.len() == token_embedding.len() |
717 | | // But they may not be equal if transposed or if implementation uses separate weights |
718 | 1 | assert!(!transformer.lm_head_weight.is_empty()); |
719 | 1 | assert!(!transformer.token_embedding.is_empty()); |
720 | 1 | } |
721 | | |
722 | | #[test] |
723 | 1 | fn test_convert_separate_lm_head_ext_cov() { |
724 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
725 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
726 | 1 | let config_path = temp_dir.path().join("config.json"); |
727 | | |
728 | 1 | let config = create_config_json(64, 0, 4, 100); |
729 | 1 | std::fs::write(&config_path, config).expect("write config"); |
730 | | |
731 | | // Create tensors WITH separate lm_head.weight |
732 | 1 | let embed_data: Vec<u8> = (0..(100 * 64)) |
733 | 6.40k | .flat_map1 (|i| (i as f32).to_le_bytes()) |
734 | 1 | .collect(); |
735 | 64 | let norm_data1 : Vec<u8>1 = (0..64)1 .flat_map1 (|_| 1.0f32.to_le_bytes()).collect1 (); |
736 | 1 | let lm_head_data: Vec<u8> = (0..(100 * 64)) |
737 | 6.40k | .flat_map1 (|i| ((i + 1000) as f32).to_le_bytes()) |
738 | 1 | .collect(); |
739 | | |
740 | 1 | let data = create_safetensors_bytes(&[ |
741 | 1 | ("model.embed_tokens.weight", "F32", &[100, 64], &embed_data), |
742 | 1 | ("model.norm.weight", "F32", &[64], &norm_data), |
743 | 1 | ("lm_head.weight", "F32", &[100, 64], &lm_head_data), |
744 | 1 | ]); |
745 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
746 | | |
747 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
748 | 1 | assert!(result.is_ok()); |
749 | 1 | let transformer = result.expect("operation failed"); |
750 | | |
751 | | // lm_head_weight should NOT equal token_embedding |
752 | 1 | assert_ne!( |
753 | 1 | transformer.lm_head_weight[0], |
754 | 1 | transformer.token_embedding[0] |
755 | | ); |
756 | 1 | } |
757 | | |
758 | | #[test] |
759 | 1 | fn test_convert_with_rope_theta_ext_cov() { |
760 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
761 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
762 | 1 | let config_path = temp_dir.path().join("config.json"); |
763 | | |
764 | | // Config with custom rope_theta |
765 | 1 | let config = r#"{ |
766 | 1 | "hidden_size": 64, |
767 | 1 | "num_hidden_layers": 0, |
768 | 1 | "num_attention_heads": 4, |
769 | 1 | "vocab_size": 100, |
770 | 1 | "intermediate_size": 256, |
771 | 1 | "rope_theta": 500000.0 |
772 | 1 | }"#; |
773 | 1 | std::fs::write(&config_path, config).expect("write config"); |
774 | | |
775 | 1 | let embed_data: Vec<u8> = vec![0u8; 100 * 64 * 4]; |
776 | 1 | let norm_data: Vec<u8> = vec![0u8; 64 * 4]; |
777 | | |
778 | 1 | let data = create_safetensors_bytes(&[ |
779 | 1 | ("model.embed_tokens.weight", "F32", &[100, 64], &embed_data), |
780 | 1 | ("model.norm.weight", "F32", &[64], &norm_data), |
781 | 1 | ]); |
782 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
783 | | |
784 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
785 | 1 | assert!(result.is_ok()); |
786 | 1 | let transformer = result.expect("operation failed"); |
787 | 1 | assert!((transformer.config.rope_theta - 500000.0).abs() < 1.0); |
788 | 1 | } |
789 | | |
790 | | #[test] |
791 | 1 | fn test_convert_with_rms_norm_eps_ext_cov() { |
792 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
793 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
794 | 1 | let config_path = temp_dir.path().join("config.json"); |
795 | | |
796 | | // Config with custom rms_norm_eps |
797 | 1 | let config = r#"{ |
798 | 1 | "hidden_size": 64, |
799 | 1 | "num_hidden_layers": 0, |
800 | 1 | "num_attention_heads": 4, |
801 | 1 | "vocab_size": 100, |
802 | 1 | "rms_norm_eps": 1e-5 |
803 | 1 | }"#; |
804 | 1 | std::fs::write(&config_path, config).expect("write config"); |
805 | | |
806 | 1 | let embed_data: Vec<u8> = vec![0u8; 100 * 64 * 4]; |
807 | 1 | let norm_data: Vec<u8> = vec![0u8; 64 * 4]; |
808 | | |
809 | 1 | let data = create_safetensors_bytes(&[ |
810 | 1 | ("model.embed_tokens.weight", "F32", &[100, 64], &embed_data), |
811 | 1 | ("model.norm.weight", "F32", &[64], &norm_data), |
812 | 1 | ]); |
813 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
814 | | |
815 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
816 | 1 | assert!(result.is_ok()); |
817 | 1 | let transformer = result.expect("operation failed"); |
818 | 1 | assert!((transformer.config.eps - 1e-5).abs() < 1e-9); |
819 | 1 | } |
820 | | |
821 | | #[test] |
822 | 1 | fn test_safetensors_to_apr_converter_struct_ext_cov() { |
823 | | // Test that SafetensorsToAprConverter is a unit struct |
824 | 1 | let _converter = SafetensorsToAprConverter; |
825 | | // This just ensures the struct exists and can be instantiated |
826 | 1 | } |
827 | | |
828 | | #[test] |
829 | 1 | fn test_convert_architecture_from_config_ext_cov() { |
830 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
831 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
832 | 1 | let config_path = temp_dir.path().join("config.json"); |
833 | | |
834 | 1 | let config = create_config_json(64, 0, 4, 100); |
835 | 1 | std::fs::write(&config_path, config).expect("write config"); |
836 | | |
837 | 1 | let embed_data: Vec<u8> = vec![0u8; 100 * 64 * 4]; |
838 | 1 | let norm_data: Vec<u8> = vec![0u8; 64 * 4]; |
839 | | |
840 | 1 | let data = create_safetensors_bytes(&[ |
841 | 1 | ("model.embed_tokens.weight", "F32", &[100, 64], &embed_data), |
842 | 1 | ("model.norm.weight", "F32", &[64], &norm_data), |
843 | 1 | ]); |
844 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
845 | | |
846 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
847 | 1 | assert!(result.is_ok()); |
848 | 1 | let transformer = result.expect("operation failed"); |
849 | 1 | assert_eq!(transformer.config.architecture, "LlamaForCausalLM"); |
850 | 1 | } |
851 | | |
852 | | /// Helper to create all layer tensors for a single transformer layer |
853 | 3 | fn create_layer_tensors( |
854 | 3 | layer_idx: usize, |
855 | 3 | hidden_dim: usize, |
856 | 3 | intermediate_dim: usize, |
857 | 3 | ) -> Vec<(&'static str, String, Vec<usize>, Vec<u8>)> { |
858 | | // Lease the string from Box to get 'static lifetime approximation |
859 | | // We'll build it differently - just use the layer_idx in the data |
860 | | |
861 | 3 | let prefix = format!("model.layers.{layer_idx}"); |
862 | | |
863 | | // Calculate tensor sizes |
864 | 3 | let attn_norm_size = hidden_dim; |
865 | 3 | let q_size = hidden_dim * hidden_dim; |
866 | 3 | let k_size = hidden_dim * hidden_dim; |
867 | 3 | let v_size = hidden_dim * hidden_dim; |
868 | 3 | let o_size = hidden_dim * hidden_dim; |
869 | 3 | let ffn_norm_size = hidden_dim; |
870 | 3 | let gate_size = hidden_dim * intermediate_dim; |
871 | 3 | let up_size = hidden_dim * intermediate_dim; |
872 | 3 | let down_size = intermediate_dim * hidden_dim; |
873 | | |
874 | 3 | vec![ |
875 | 3 | ( |
876 | 3 | "attn_norm", |
877 | 3 | format!("{prefix}.input_layernorm.weight"), |
878 | 3 | vec![attn_norm_size], |
879 | 3 | vec![0u8; attn_norm_size * 4], |
880 | 3 | ), |
881 | 3 | ( |
882 | 3 | "q_proj", |
883 | 3 | format!("{prefix}.self_attn.q_proj.weight"), |
884 | 3 | vec![hidden_dim, hidden_dim], |
885 | 3 | vec![0u8; q_size * 4], |
886 | 3 | ), |
887 | 3 | ( |
888 | 3 | "k_proj", |
889 | 3 | format!("{prefix}.self_attn.k_proj.weight"), |
890 | 3 | vec![hidden_dim, hidden_dim], |
891 | 3 | vec![0u8; k_size * 4], |
892 | 3 | ), |
893 | 3 | ( |
894 | 3 | "v_proj", |
895 | 3 | format!("{prefix}.self_attn.v_proj.weight"), |
896 | 3 | vec![hidden_dim, hidden_dim], |
897 | 3 | vec![0u8; v_size * 4], |
898 | 3 | ), |
899 | 3 | ( |
900 | 3 | "o_proj", |
901 | 3 | format!("{prefix}.self_attn.o_proj.weight"), |
902 | 3 | vec![hidden_dim, hidden_dim], |
903 | 3 | vec![0u8; o_size * 4], |
904 | 3 | ), |
905 | 3 | ( |
906 | 3 | "ffn_norm", |
907 | 3 | format!("{prefix}.post_attention_layernorm.weight"), |
908 | 3 | vec![ffn_norm_size], |
909 | 3 | vec![0u8; ffn_norm_size * 4], |
910 | 3 | ), |
911 | 3 | ( |
912 | 3 | "gate_proj", |
913 | 3 | format!("{prefix}.mlp.gate_proj.weight"), |
914 | 3 | vec![intermediate_dim, hidden_dim], |
915 | 3 | vec![0u8; gate_size * 4], |
916 | 3 | ), |
917 | 3 | ( |
918 | 3 | "up_proj", |
919 | 3 | format!("{prefix}.mlp.up_proj.weight"), |
920 | 3 | vec![intermediate_dim, hidden_dim], |
921 | 3 | vec![0u8; up_size * 4], |
922 | 3 | ), |
923 | 3 | ( |
924 | 3 | "down_proj", |
925 | 3 | format!("{prefix}.mlp.down_proj.weight"), |
926 | 3 | vec![hidden_dim, intermediate_dim], |
927 | 3 | vec![0u8; down_size * 4], |
928 | 3 | ), |
929 | | ] |
930 | 3 | } |
931 | | |
932 | | #[test] |
933 | 1 | fn test_convert_with_single_layer_ext_cov() { |
934 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
935 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
936 | 1 | let config_path = temp_dir.path().join("config.json"); |
937 | | |
938 | 1 | let hidden_dim = 16; |
939 | 1 | let intermediate_dim = 64; |
940 | 1 | let vocab_size = 50; |
941 | 1 | let num_layers = 1; |
942 | 1 | let num_heads = 4; |
943 | | |
944 | | // Config |
945 | 1 | let config = format!( |
946 | 1 | r#"{{ |
947 | 1 | "hidden_size": {}, |
948 | 1 | "num_hidden_layers": {}, |
949 | 1 | "num_attention_heads": {}, |
950 | 1 | "vocab_size": {}, |
951 | 1 | "intermediate_size": {}, |
952 | 1 | "max_position_embeddings": 128, |
953 | 1 | "rms_norm_eps": 1e-6 |
954 | 1 | }}"#, |
955 | | hidden_dim, num_layers, num_heads, vocab_size, intermediate_dim |
956 | | ); |
957 | 1 | std::fs::write(&config_path, config).expect("write config"); |
958 | | |
959 | | // Build layer tensors |
960 | 1 | let layer_tensors = create_layer_tensors(0, hidden_dim, intermediate_dim); |
961 | | |
962 | | // Build safetensors with all required tensors |
963 | 1 | let embed_data: Vec<u8> = vec![0u8; vocab_size * hidden_dim * 4]; |
964 | 1 | let norm_data: Vec<u8> = vec![0u8; hidden_dim * 4]; |
965 | | |
966 | | // Create a comprehensive tensor list |
967 | | use serde_json::json; |
968 | 1 | let mut tensor_entries = serde_json::Map::new(); |
969 | 1 | let mut all_data = Vec::new(); |
970 | 1 | let mut offset = 0usize; |
971 | | |
972 | | // Add embed_tokens |
973 | 1 | tensor_entries.insert( |
974 | 1 | "model.embed_tokens.weight".to_string(), |
975 | 1 | json!({ |
976 | 1 | "dtype": "F32", |
977 | 1 | "shape": [vocab_size, hidden_dim], |
978 | 1 | "data_offsets": [offset, offset + embed_data.len()] |
979 | | }), |
980 | | ); |
981 | 1 | all_data.extend(&embed_data); |
982 | 1 | offset += embed_data.len(); |
983 | | |
984 | | // Add norm |
985 | 1 | tensor_entries.insert( |
986 | 1 | "model.norm.weight".to_string(), |
987 | 1 | json!({ |
988 | 1 | "dtype": "F32", |
989 | 1 | "shape": [hidden_dim], |
990 | 1 | "data_offsets": [offset, offset + norm_data.len()] |
991 | | }), |
992 | | ); |
993 | 1 | all_data.extend(&norm_data); |
994 | 1 | offset += norm_data.len(); |
995 | | |
996 | | // Add layer tensors |
997 | 10 | for (_, name9 , shape9 , data9 ) in &layer_tensors { |
998 | 9 | tensor_entries.insert( |
999 | 9 | name.clone(), |
1000 | 9 | json!({ |
1001 | 9 | "dtype": "F32", |
1002 | 9 | "shape": shape, |
1003 | 9 | "data_offsets": [offset, offset + data.len()] |
1004 | 9 | }), |
1005 | 9 | ); |
1006 | 9 | all_data.extend(data); |
1007 | 9 | offset += data.len(); |
1008 | 9 | } |
1009 | | |
1010 | 1 | let json_obj = serde_json::Value::Object(tensor_entries); |
1011 | 1 | let json_bytes = json_obj.to_string().into_bytes(); |
1012 | | |
1013 | 1 | let mut safetensors_data = Vec::new(); |
1014 | 1 | safetensors_data.extend_from_slice(&(json_bytes.len() as u64).to_le_bytes()); |
1015 | 1 | safetensors_data.extend_from_slice(&json_bytes); |
1016 | 1 | safetensors_data.extend_from_slice(&all_data); |
1017 | | |
1018 | 1 | std::fs::write(&model_path, safetensors_data).expect("write safetensors"); |
1019 | | |
1020 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
1021 | 1 | assert!(result.is_ok(), "Conversion failed: {:?}"0 , result0 .err0 ()); |
1022 | | |
1023 | 1 | let transformer = result.expect("operation failed"); |
1024 | 1 | assert_eq!(transformer.config.hidden_dim, hidden_dim); |
1025 | 1 | assert_eq!(transformer.config.num_layers, num_layers); |
1026 | 1 | assert_eq!(transformer.layers.len(), num_layers); |
1027 | | |
1028 | | // Verify layer structure |
1029 | 1 | let layer = &transformer.layers[0]; |
1030 | 1 | assert_eq!(layer.attn_norm_weight.len(), hidden_dim); |
1031 | 1 | assert_eq!(layer.qkv_weight.len(), hidden_dim * 3 * hidden_dim); |
1032 | 1 | assert_eq!(layer.attn_output_weight.len(), hidden_dim * hidden_dim); |
1033 | 1 | assert_eq!(layer.ffn_up_weight.len(), hidden_dim * intermediate_dim); |
1034 | 1 | assert_eq!(layer.ffn_down_weight.len(), intermediate_dim * hidden_dim); |
1035 | 1 | assert!(layer.ffn_gate_weight.is_some()); |
1036 | 1 | assert!(layer.ffn_norm_weight.is_some()); |
1037 | 1 | } |
1038 | | |
1039 | | #[test] |
1040 | 1 | fn test_convert_with_multiple_layers_ext_cov() { |
1041 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
1042 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
1043 | 1 | let config_path = temp_dir.path().join("config.json"); |
1044 | | |
1045 | 1 | let hidden_dim = 8; |
1046 | 1 | let intermediate_dim = 32; |
1047 | 1 | let vocab_size = 20; |
1048 | 1 | let num_layers = 2; |
1049 | 1 | let num_heads = 2; |
1050 | | |
1051 | 1 | let config = format!( |
1052 | 1 | r#"{{ |
1053 | 1 | "hidden_size": {}, |
1054 | 1 | "num_hidden_layers": {}, |
1055 | 1 | "num_attention_heads": {}, |
1056 | 1 | "vocab_size": {}, |
1057 | 1 | "intermediate_size": {} |
1058 | 1 | }}"#, |
1059 | | hidden_dim, num_layers, num_heads, vocab_size, intermediate_dim |
1060 | | ); |
1061 | 1 | std::fs::write(&config_path, config).expect("write config"); |
1062 | | |
1063 | | // Build tensors for multiple layers |
1064 | | use serde_json::json; |
1065 | 1 | let mut tensor_entries = serde_json::Map::new(); |
1066 | 1 | let mut all_data = Vec::new(); |
1067 | 1 | let mut offset = 0usize; |
1068 | | |
1069 | | // Add embed_tokens |
1070 | 1 | let embed_data: Vec<u8> = vec![0u8; vocab_size * hidden_dim * 4]; |
1071 | 1 | tensor_entries.insert( |
1072 | 1 | "model.embed_tokens.weight".to_string(), |
1073 | 1 | json!({ |
1074 | 1 | "dtype": "F32", |
1075 | 1 | "shape": [vocab_size, hidden_dim], |
1076 | 1 | "data_offsets": [offset, offset + embed_data.len()] |
1077 | | }), |
1078 | | ); |
1079 | 1 | all_data.extend(&embed_data); |
1080 | 1 | offset += embed_data.len(); |
1081 | | |
1082 | | // Add norm |
1083 | 1 | let norm_data: Vec<u8> = vec![0u8; hidden_dim * 4]; |
1084 | 1 | tensor_entries.insert( |
1085 | 1 | "model.norm.weight".to_string(), |
1086 | 1 | json!({ |
1087 | 1 | "dtype": "F32", |
1088 | 1 | "shape": [hidden_dim], |
1089 | 1 | "data_offsets": [offset, offset + norm_data.len()] |
1090 | | }), |
1091 | | ); |
1092 | 1 | all_data.extend(&norm_data); |
1093 | 1 | offset += norm_data.len(); |
1094 | | |
1095 | | // Add all layer tensors |
1096 | 2 | for layer_idx in 0..num_layers1 { |
1097 | 2 | let layer_tensors = create_layer_tensors(layer_idx, hidden_dim, intermediate_dim); |
1098 | 20 | for (_, name18 , shape18 , data18 ) in &layer_tensors { |
1099 | 18 | tensor_entries.insert( |
1100 | 18 | name.clone(), |
1101 | 18 | json!({ |
1102 | 18 | "dtype": "F32", |
1103 | 18 | "shape": shape, |
1104 | 18 | "data_offsets": [offset, offset + data.len()] |
1105 | 18 | }), |
1106 | 18 | ); |
1107 | 18 | all_data.extend(data); |
1108 | 18 | offset += data.len(); |
1109 | 18 | } |
1110 | | } |
1111 | | |
1112 | 1 | let json_obj = serde_json::Value::Object(tensor_entries); |
1113 | 1 | let json_bytes = json_obj.to_string().into_bytes(); |
1114 | | |
1115 | 1 | let mut safetensors_data = Vec::new(); |
1116 | 1 | safetensors_data.extend_from_slice(&(json_bytes.len() as u64).to_le_bytes()); |
1117 | 1 | safetensors_data.extend_from_slice(&json_bytes); |
1118 | 1 | safetensors_data.extend_from_slice(&all_data); |
1119 | | |
1120 | 1 | std::fs::write(&model_path, safetensors_data).expect("write safetensors"); |
1121 | | |
1122 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
1123 | 1 | assert!(result.is_ok(), "Conversion failed: {:?}"0 , result0 .err0 ()); |
1124 | | |
1125 | 1 | let transformer = result.expect("operation failed"); |
1126 | 1 | assert_eq!(transformer.layers.len(), num_layers); |
1127 | 1 | } |
1128 | | |
1129 | | #[test] |
1130 | 1 | fn test_extract_layer_missing_input_layernorm_ext_cov() { |
1131 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
1132 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
1133 | 1 | let config_path = temp_dir.path().join("config.json"); |
1134 | | |
1135 | 1 | let config = create_config_json(16, 1, 4, 50); |
1136 | 1 | std::fs::write(&config_path, config).expect("write config"); |
1137 | | |
1138 | | // Missing layer 0 input_layernorm |
1139 | 1 | let embed_data: Vec<u8> = vec![0u8; 50 * 16 * 4]; |
1140 | 1 | let norm_data: Vec<u8> = vec![0u8; 16 * 4]; |
1141 | 1 | let data = create_safetensors_bytes(&[ |
1142 | 1 | ("model.embed_tokens.weight", "F32", &[50, 16], &embed_data), |
1143 | 1 | ("model.norm.weight", "F32", &[16], &norm_data), |
1144 | 1 | ]); |
1145 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
1146 | | |
1147 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
1148 | 1 | assert!(result.is_err()); |
1149 | 1 | } |
1150 | | |
1151 | | #[test] |
1152 | 1 | fn test_convert_with_num_kv_heads_ext_cov() { |
1153 | 1 | let temp_dir = TempDir::new().expect("create temp dir"); |
1154 | 1 | let model_path = temp_dir.path().join("model.safetensors"); |
1155 | 1 | let config_path = temp_dir.path().join("config.json"); |
1156 | | |
1157 | | // Config with GQA (num_key_value_heads < num_attention_heads) |
1158 | 1 | let config = r#"{ |
1159 | 1 | "hidden_size": 64, |
1160 | 1 | "num_hidden_layers": 0, |
1161 | 1 | "num_attention_heads": 8, |
1162 | 1 | "num_key_value_heads": 4, |
1163 | 1 | "vocab_size": 100 |
1164 | 1 | }"#; |
1165 | 1 | std::fs::write(&config_path, config).expect("write config"); |
1166 | | |
1167 | 1 | let embed_data: Vec<u8> = vec![0u8; 100 * 64 * 4]; |
1168 | 1 | let norm_data: Vec<u8> = vec![0u8; 64 * 4]; |
1169 | 1 | let data = create_safetensors_bytes(&[ |
1170 | 1 | ("model.embed_tokens.weight", "F32", &[100, 64], &embed_data), |
1171 | 1 | ("model.norm.weight", "F32", &[64], &norm_data), |
1172 | 1 | ]); |
1173 | 1 | std::fs::write(&model_path, data).expect("write safetensors"); |
1174 | | |
1175 | 1 | let result = SafetensorsToAprConverter::convert(&model_path); |
1176 | 1 | assert!(result.is_ok()); |
1177 | 1 | let transformer = result.expect("operation failed"); |
1178 | 1 | assert_eq!(transformer.config.num_heads, 8); |
1179 | 1 | assert_eq!(transformer.config.num_kv_heads, 4); |
1180 | 1 | } |
1181 | | } |