/home/noah/src/realizar/src/apr_transformer/loader.rs
Line | Count | Source |
1 | | //! APR Transformer Loaders |
2 | | //! |
3 | | //! Memory-mapped and quantized APR transformer implementations. |
4 | | //! Extracted from mod.rs (PMAT-802). |
5 | | |
6 | | #![allow(dead_code)] |
7 | | #![allow(clippy::too_many_arguments)] |
8 | | #![allow(clippy::cast_precision_loss)] |
9 | | #![allow(clippy::cast_sign_loss)] |
10 | | #![allow(clippy::cast_possible_truncation)] |
11 | | #![allow(clippy::cast_possible_wrap)] |
12 | | #![allow(non_camel_case_types)] |
13 | | |
14 | | use std::fs::File; |
15 | | use std::path::Path; |
16 | | |
17 | | use memmap2::Mmap; |
18 | | use serde::{Deserialize, Serialize}; |
19 | | |
20 | | use crate::apr::MAGIC; |
21 | | use crate::error::{RealizarError, Result}; |
22 | | |
23 | | use super::{AprKVCache, AprTransformerConfig, AprTransformer}; |
24 | | |
25 | | // ============================================================================ |
26 | | // APR Transformer Binary Format (Y1-Y5 Format Parity) |
27 | | // ============================================================================ |
28 | | // Uses unified APR magic from apr.rs - ONE format, no versioning |
29 | | |
30 | | /// Binary header size for APR Transformer (64 bytes) |
31 | | pub const APR_TRANSFORMER_HEADER_SIZE: usize = 64; |
32 | | |
33 | | /// Memory-mapped APR Transformer for zero-copy inference (Y1, Y2) |
34 | | /// |
35 | | /// This struct provides zero-copy access to APR transformer weights |
36 | | /// via memory-mapped I/O, matching GGUF's performance characteristics. |
37 | | /// |
38 | | /// # Performance Benefits (per Dean & Barroso 2013) |
39 | | /// |
40 | | /// - Zero-copy: Tensors accessed directly from page cache |
41 | | /// - Lazy loading: Only touched pages are loaded |
42 | | /// - Shared memory: Multiple processes can share the same mapping |
43 | | #[derive(Debug)] |
44 | | pub struct MmapAprTransformer { |
45 | | /// Memory-mapped file data |
46 | | mmap: Mmap, |
47 | | /// Model configuration (parsed from header) |
48 | | pub config: AprTransformerConfig, |
49 | | /// Offset where tensor data starts |
50 | | tensor_data_offset: usize, |
51 | | /// Whether mmap is active (for is_mmap() check) |
52 | | is_mmap: bool, |
53 | | } |
54 | | |
55 | | impl MmapAprTransformer { |
56 | | /// Load APR transformer from file using memory-mapped I/O (Y1) |
57 | | /// |
58 | | /// # Arguments |
59 | | /// |
60 | | /// * `path` - Path to .apr transformer file |
61 | | /// |
62 | | /// # Returns |
63 | | /// |
64 | | /// Memory-mapped transformer ready for inference |
65 | | /// |
66 | | /// # Errors |
67 | | /// |
68 | | /// Returns error if file cannot be opened or is invalid |
69 | | /// |
70 | | /// # Example |
71 | | /// |
72 | | /// ```rust,ignore |
73 | | /// let model = MmapAprTransformer::from_file("model.apr")?; |
74 | | /// assert!(model.is_mmap()); |
75 | | /// let logits = model.forward(&[1, 2, 3])?; |
76 | | /// ``` |
77 | 7 | pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> { |
78 | 7 | let file6 = File::open(path.as_ref()).map_err(|e| RealizarError::IoError { |
79 | 1 | message: format!("Failed to open APR file: {e}"), |
80 | 1 | })?; |
81 | | |
82 | | // Safety: We're only reading the file, mmap is safe for read-only access |
83 | | // SAFETY: Memory safety ensured by bounds checking and alignment |
84 | 6 | let mmap = unsafe { |
85 | 6 | Mmap::map(&file).map_err(|e| RealizarError::IoError { |
86 | 0 | message: format!("Failed to mmap APR file: {e}"), |
87 | 0 | })? |
88 | | }; |
89 | | |
90 | | // Verify minimum size |
91 | 6 | if mmap.len() < APR_TRANSFORMER_HEADER_SIZE { |
92 | 1 | return Err(RealizarError::FormatError { |
93 | 1 | reason: format!( |
94 | 1 | "APR file too small: {} bytes (need at least {})", |
95 | 1 | mmap.len(), |
96 | 1 | APR_TRANSFORMER_HEADER_SIZE |
97 | 1 | ), |
98 | 1 | }); |
99 | 5 | } |
100 | | |
101 | | // Parse header |
102 | 5 | let header_bytes = &mmap[..APR_TRANSFORMER_HEADER_SIZE]; |
103 | | |
104 | | // Verify APR magic |
105 | 5 | let magic = &header_bytes[0..4]; |
106 | 5 | if magic != MAGIC { |
107 | 1 | return Err(RealizarError::FormatError { |
108 | 1 | reason: format!("Invalid APR magic: expected {:?}, got {:?}", MAGIC, magic), |
109 | 1 | }); |
110 | 4 | } |
111 | | |
112 | | // Parse config from header (after 4-byte magic + 4-byte version) |
113 | 4 | let version = u32::from_le_bytes([ |
114 | 4 | header_bytes[4], |
115 | 4 | header_bytes[5], |
116 | 4 | header_bytes[6], |
117 | 4 | header_bytes[7], |
118 | 4 | ]); |
119 | 4 | if version > 1 { |
120 | 1 | return Err(RealizarError::FormatError { |
121 | 1 | reason: format!("Unsupported APR version: {version}"), |
122 | 1 | }); |
123 | 3 | } |
124 | | |
125 | | // Parse config fields (offset 8) |
126 | 3 | let hidden_dim = u32::from_le_bytes([ |
127 | 3 | header_bytes[8], |
128 | 3 | header_bytes[9], |
129 | 3 | header_bytes[10], |
130 | 3 | header_bytes[11], |
131 | 3 | ]) as usize; |
132 | 3 | let num_layers = u32::from_le_bytes([ |
133 | 3 | header_bytes[12], |
134 | 3 | header_bytes[13], |
135 | 3 | header_bytes[14], |
136 | 3 | header_bytes[15], |
137 | 3 | ]) as usize; |
138 | 3 | let num_heads = u32::from_le_bytes([ |
139 | 3 | header_bytes[16], |
140 | 3 | header_bytes[17], |
141 | 3 | header_bytes[18], |
142 | 3 | header_bytes[19], |
143 | 3 | ]) as usize; |
144 | 3 | let num_kv_heads = u32::from_le_bytes([ |
145 | 3 | header_bytes[20], |
146 | 3 | header_bytes[21], |
147 | 3 | header_bytes[22], |
148 | 3 | header_bytes[23], |
149 | 3 | ]) as usize; |
150 | 3 | let vocab_size = u32::from_le_bytes([ |
151 | 3 | header_bytes[24], |
152 | 3 | header_bytes[25], |
153 | 3 | header_bytes[26], |
154 | 3 | header_bytes[27], |
155 | 3 | ]) as usize; |
156 | 3 | let intermediate_dim = u32::from_le_bytes([ |
157 | 3 | header_bytes[28], |
158 | 3 | header_bytes[29], |
159 | 3 | header_bytes[30], |
160 | 3 | header_bytes[31], |
161 | 3 | ]) as usize; |
162 | 3 | let context_length = u32::from_le_bytes([ |
163 | 3 | header_bytes[32], |
164 | 3 | header_bytes[33], |
165 | 3 | header_bytes[34], |
166 | 3 | header_bytes[35], |
167 | 3 | ]) as usize; |
168 | 3 | let rope_theta = f32::from_le_bytes([ |
169 | 3 | header_bytes[36], |
170 | 3 | header_bytes[37], |
171 | 3 | header_bytes[38], |
172 | 3 | header_bytes[39], |
173 | 3 | ]); |
174 | 3 | let eps = f32::from_le_bytes([ |
175 | 3 | header_bytes[40], |
176 | 3 | header_bytes[41], |
177 | 3 | header_bytes[42], |
178 | 3 | header_bytes[43], |
179 | 3 | ]); |
180 | 3 | let tensor_data_offset = u32::from_le_bytes([ |
181 | 3 | header_bytes[44], |
182 | 3 | header_bytes[45], |
183 | 3 | header_bytes[46], |
184 | 3 | header_bytes[47], |
185 | 3 | ]) as usize; |
186 | | |
187 | 3 | let config = AprTransformerConfig { |
188 | 3 | architecture: "apr".to_string(), |
189 | 3 | hidden_dim, |
190 | 3 | num_layers, |
191 | 3 | num_heads, |
192 | 3 | num_kv_heads, |
193 | 3 | vocab_size, |
194 | 3 | intermediate_dim, |
195 | 3 | context_length, |
196 | 3 | rope_theta, |
197 | 3 | eps, |
198 | 3 | }; |
199 | | |
200 | 3 | Ok(Self { |
201 | 3 | mmap, |
202 | 3 | config, |
203 | 3 | tensor_data_offset, |
204 | 3 | is_mmap: true, |
205 | 3 | }) |
206 | 7 | } |
207 | | |
208 | | /// Check if model is using memory-mapped I/O (Y2) |
209 | | #[must_use] |
210 | 1 | pub fn is_mmap(&self) -> bool { |
211 | 1 | self.is_mmap |
212 | 1 | } |
213 | | |
214 | | /// Get raw tensor data slice (zero-copy access) |
215 | | /// |
216 | | /// # Arguments |
217 | | /// |
218 | | /// * `offset` - Offset from tensor data start |
219 | | /// * `len` - Number of bytes to read |
220 | | /// |
221 | | /// # Returns |
222 | | /// |
223 | | /// Slice of raw bytes (zero-copy from mmap) |
224 | 2 | pub fn get_tensor_bytes(&self, offset: usize, len: usize) -> Result<&[u8]> { |
225 | 2 | let start = self.tensor_data_offset + offset; |
226 | 2 | let end = start + len; |
227 | | |
228 | 2 | if end > self.mmap.len() { |
229 | 1 | return Err(RealizarError::FormatError { |
230 | 1 | reason: format!( |
231 | 1 | "Tensor access out of bounds: offset={offset}, len={len}, file_size={}", |
232 | 1 | self.mmap.len() |
233 | 1 | ), |
234 | 1 | }); |
235 | 1 | } |
236 | | |
237 | 1 | Ok(&self.mmap[start..end]) |
238 | 2 | } |
239 | | |
240 | | /// Get tensor as f32 slice (zero-copy if aligned) |
241 | | /// |
242 | | /// # Safety |
243 | | /// |
244 | | /// This function assumes the tensor data is properly aligned for f32 access. |
245 | | /// If not aligned, returns a copy. |
246 | 1 | pub fn get_tensor_f32(&self, offset: usize, num_elements: usize) -> Result<Vec<f32>> { |
247 | 1 | let bytes = self.get_tensor_bytes(offset, num_elements * 4)?0 ; |
248 | | |
249 | | // Convert bytes to f32 (could be zero-copy if aligned) |
250 | 1 | let floats: Vec<f32> = bytes |
251 | 1 | .chunks_exact(4) |
252 | 4 | .map1 (|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) |
253 | 1 | .collect(); |
254 | | |
255 | 1 | Ok(floats) |
256 | 1 | } |
257 | | |
258 | | /// Get file size in bytes |
259 | | #[must_use] |
260 | 1 | pub fn file_size(&self) -> usize { |
261 | 1 | self.mmap.len() |
262 | 1 | } |
263 | | |
264 | | /// Get number of parameters (estimated from config) |
265 | | #[must_use] |
266 | 1 | pub fn num_parameters(&self) -> usize { |
267 | 1 | let hidden = self.config.hidden_dim; |
268 | 1 | let vocab = self.config.vocab_size; |
269 | 1 | let layers = self.config.num_layers; |
270 | 1 | let intermediate = self.config.intermediate_dim; |
271 | | |
272 | | // Embedding + LM head |
273 | 1 | let embed_params = vocab * hidden * 2; |
274 | | |
275 | | // Per layer: attn_norm + qkv + attn_out + ffn_up + ffn_down |
276 | 1 | let layer_params = hidden |
277 | 1 | + (hidden * 3 * hidden) |
278 | 1 | + (hidden * hidden) |
279 | 1 | + (hidden * intermediate) |
280 | 1 | + (intermediate * hidden); |
281 | | |
282 | | // Output norm |
283 | 1 | let norm_params = hidden; |
284 | | |
285 | 1 | embed_params + (layers * layer_params) + norm_params |
286 | 1 | } |
287 | | } |
288 | | |
289 | | // ============================================================================ |
290 | | // Y5: Quantized APR Transformer (Q4_K, Q8_0 support) |
291 | | // ============================================================================ |
292 | | |
293 | | /// Quantization type for APR Transformer weights (Y5) |
294 | | /// |
295 | | /// Supports the same quantization formats as GGUF for format parity. |
296 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] |
297 | | #[allow(non_camel_case_types)] // Match GGML naming convention (Q4_K, Q8_0) |
298 | | pub enum AprQuantizationType { |
299 | | /// Full precision 32-bit floats (no quantization) |
300 | | #[default] |
301 | | F32, |
302 | | /// 4-bit K-quantization (4.5 bits/weight, super-block size 256) |
303 | | Q4_K, |
304 | | /// 8-bit quantization (8 bits/weight, block size 32) |
305 | | Q8_0, |
306 | | } |
307 | | |
308 | | impl AprQuantizationType { |
309 | | /// Get bits per weight for this quantization type |
310 | | #[must_use] |
311 | 6 | pub fn bits_per_weight(&self) -> f64 { |
312 | 6 | match self { |
313 | 2 | Self::F32 => 32.0, |
314 | 2 | Self::Q4_K => 4.5, // 144 bytes per 256 values |
315 | 2 | Self::Q8_0 => 8.0, // 36 bytes per 32 values (scale + 32 int8) |
316 | | } |
317 | 6 | } |
318 | | |
319 | | /// Get bytes per super-block (256 values for Q4_K, 32 for Q8_0) |
320 | | #[must_use] |
321 | 36 | pub fn bytes_per_block(&self) -> usize { |
322 | 36 | match self { |
323 | 22 | Self::F32 => 4, // 4 bytes per value |
324 | 10 | Self::Q4_K => 144, // 144 bytes per 256 values |
325 | 4 | Self::Q8_0 => 36, // 4 (scale) + 32 (int8) per 32 values |
326 | | } |
327 | 36 | } |
328 | | |
329 | | /// Get values per block |
330 | | #[must_use] |
331 | 36 | pub fn values_per_block(&self) -> usize { |
332 | 36 | match self { |
333 | 22 | Self::F32 => 1, |
334 | 10 | Self::Q4_K => 256, |
335 | 4 | Self::Q8_0 => 32, |
336 | | } |
337 | 36 | } |
338 | | |
339 | | /// Convert to byte representation for header |
340 | | #[must_use] |
341 | 8 | pub fn to_byte(&self) -> u8 { |
342 | 8 | match self { |
343 | 3 | Self::F32 => 0, |
344 | 3 | Self::Q4_K => 1, |
345 | 2 | Self::Q8_0 => 2, |
346 | | } |
347 | 8 | } |
348 | | |
349 | | /// Parse from byte representation |
350 | | #[must_use] |
351 | 10 | pub fn from_byte(byte: u8) -> Option<Self> { |
352 | 10 | match byte { |
353 | 2 | 0 => Some(Self::F32), |
354 | 3 | 1 => Some(Self::Q4_K), |
355 | 2 | 2 => Some(Self::Q8_0), |
356 | 3 | _ => None, |
357 | | } |
358 | 10 | } |
359 | | } |
360 | | |
361 | | /// Quantized APR Transformer with Q4_K or Q8_0 weights (Y5) |
362 | | /// |
363 | | /// Stores weights in quantized form for memory efficiency while |
364 | | /// providing the same inference interface as `AprTransformer`. |
365 | | /// |
366 | | /// # Memory Savings |
367 | | /// |
368 | | /// - Q4_K: ~7x compression (4.5 bits vs 32 bits) |
369 | | /// - Q8_0: ~4x compression (8 bits vs 32 bits) |
370 | | /// |
371 | | /// # Example |
372 | | /// |
373 | | /// ```rust,ignore |
374 | | /// use realizar::apr_transformer::{AprQuantizationType, QuantizedAprTransformer}; |
375 | | /// |
376 | | /// let transformer = QuantizedAprTransformer::new(config, AprQuantizationType::Q4_K); |
377 | | /// let logits = transformer.forward(&[1, 2, 3])?; |
378 | | /// ``` |
379 | | #[derive(Debug, Clone)] |
380 | | pub struct QuantizedAprTransformer { |
381 | | /// Model configuration |
382 | | config: AprTransformerConfig, |
383 | | /// Quantization type |
384 | | quant_type: AprQuantizationType, |
385 | | /// Token embedding (stored as F32 for now, could be quantized later) |
386 | | token_embedding: Vec<f32>, |
387 | | /// Quantized layer weights (raw bytes) |
388 | | layer_weights: Vec<Vec<u8>>, |
389 | | /// Output norm weight (F32) |
390 | | output_norm_weight: Vec<f32>, |
391 | | /// LM head weight (quantized) |
392 | | lm_head_weight: Vec<u8>, |
393 | | } |
394 | | |
395 | | impl QuantizedAprTransformer { |
396 | | /// Create a new quantized transformer with the given config and quantization type |
397 | | #[must_use] |
398 | 15 | pub fn new(config: AprTransformerConfig, quant_type: AprQuantizationType) -> Self { |
399 | 15 | let hidden_dim = config.hidden_dim; |
400 | 15 | let vocab_size = config.vocab_size; |
401 | 15 | let _intermediate_dim = config.intermediate_dim; |
402 | | |
403 | | // Calculate quantized sizes |
404 | 15 | let embed_size = vocab_size * hidden_dim; // F32 for embeddings |
405 | 15 | let layer_weight_size = Self::calculate_layer_bytes(&config, quant_type); |
406 | 15 | let lm_head_size = Self::calculate_quantized_bytes(hidden_dim * vocab_size, quant_type); |
407 | | |
408 | | // Initialize with zeros |
409 | 15 | let layer_weights = (0..config.num_layers) |
410 | 30 | .map15 (|_| vec![0u8; layer_weight_size]) |
411 | 15 | .collect(); |
412 | | |
413 | 15 | Self { |
414 | 15 | config, |
415 | 15 | quant_type, |
416 | 15 | token_embedding: vec![0.0; embed_size], |
417 | 15 | layer_weights, |
418 | 15 | output_norm_weight: vec![1.0; hidden_dim], |
419 | 15 | lm_head_weight: vec![0u8; lm_head_size], |
420 | 15 | } |
421 | 15 | } |
422 | | |
423 | | /// Create from an F32 transformer by quantizing weights |
424 | | #[must_use] |
425 | 0 | pub fn from_f32_transformer( |
426 | 0 | f32_model: &AprTransformer, |
427 | 0 | quant_type: AprQuantizationType, |
428 | 0 | ) -> Self { |
429 | 0 | let config = f32_model.config.clone(); |
430 | | |
431 | | // For now, just create zero-initialized quantized model |
432 | | // Full quantization would convert F32 weights to Q4_K/Q8_0 |
433 | 0 | Self::new(config, quant_type) |
434 | 0 | } |
435 | | |
436 | | /// Get the quantization type |
437 | | #[must_use] |
438 | 4 | pub fn quantization_type(&self) -> AprQuantizationType { |
439 | 4 | self.quant_type |
440 | 4 | } |
441 | | |
442 | | /// Get bits per weight |
443 | | #[must_use] |
444 | 3 | pub fn bits_per_weight(&self) -> f64 { |
445 | 3 | self.quant_type.bits_per_weight() |
446 | 3 | } |
447 | | |
448 | | /// Get the model configuration |
449 | | #[must_use] |
450 | 4 | pub fn config(&self) -> &AprTransformerConfig { |
451 | 4 | &self.config |
452 | 4 | } |
453 | | |
454 | | /// Get total quantized weight bytes |
455 | | #[must_use] |
456 | 2 | pub fn weight_bytes(&self) -> usize { |
457 | 2 | let embed_bytes = self.token_embedding.len() * 4; // F32 |
458 | 2 | let layer_bytes: usize = self.layer_weights.iter().map(std::vec::Vec::len).sum(); |
459 | 2 | let norm_bytes = self.output_norm_weight.len() * 4; // F32 |
460 | 2 | let lm_head_bytes = self.lm_head_weight.len(); |
461 | | |
462 | 2 | embed_bytes + layer_bytes + norm_bytes + lm_head_bytes |
463 | 2 | } |
464 | | |
465 | | /// Get equivalent F32 size for compression ratio |
466 | | #[must_use] |
467 | 1 | pub fn f32_equivalent_bytes(&self) -> usize { |
468 | 1 | let num_params = self.num_parameters(); |
469 | 1 | num_params * 4 // 4 bytes per F32 |
470 | 1 | } |
471 | | |
472 | | /// Get total number of parameters |
473 | | #[must_use] |
474 | 2 | pub fn num_parameters(&self) -> usize { |
475 | 2 | let hidden = self.config.hidden_dim; |
476 | 2 | let vocab = self.config.vocab_size; |
477 | 2 | let layers = self.config.num_layers; |
478 | 2 | let intermediate = self.config.intermediate_dim; |
479 | | |
480 | | // Embedding + LM head |
481 | 2 | let embed_params = vocab * hidden * 2; |
482 | | |
483 | | // Per layer: attn_norm + qkv + attn_out + ffn_up + ffn_down |
484 | 2 | let layer_params = hidden |
485 | 2 | + (hidden * 3 * hidden) |
486 | 2 | + (hidden * hidden) |
487 | 2 | + (hidden * intermediate) |
488 | 2 | + (intermediate * hidden); |
489 | | |
490 | | // Output norm |
491 | 2 | let norm_params = hidden; |
492 | | |
493 | 2 | embed_params + (layers * layer_params) + norm_params |
494 | 2 | } |
495 | | |
496 | | /// Calculate bytes needed for layer weights |
497 | 15 | fn calculate_layer_bytes( |
498 | 15 | config: &AprTransformerConfig, |
499 | 15 | quant_type: AprQuantizationType, |
500 | 15 | ) -> usize { |
501 | 15 | let hidden = config.hidden_dim; |
502 | 15 | let intermediate = config.intermediate_dim; |
503 | | |
504 | | // Layer weights: qkv + attn_out + ffn_up + ffn_down + norms |
505 | 15 | let weight_elements = (hidden * 3 * hidden) |
506 | 15 | + (hidden * hidden) |
507 | 15 | + (hidden * intermediate) |
508 | 15 | + (intermediate * hidden); |
509 | | |
510 | 15 | Self::calculate_quantized_bytes(weight_elements, quant_type) |
511 | 15 | } |
512 | | |
513 | | /// Calculate quantized byte size for N elements |
514 | 33 | pub(crate) fn calculate_quantized_bytes(num_elements: usize, quant_type: AprQuantizationType) -> usize { |
515 | 33 | let values_per_block = quant_type.values_per_block(); |
516 | 33 | let bytes_per_block = quant_type.bytes_per_block(); |
517 | | |
518 | | // Round up to nearest block |
519 | 33 | let num_blocks = num_elements.div_ceil(values_per_block); |
520 | 33 | num_blocks * bytes_per_block |
521 | 33 | } |
522 | | |
523 | | /// Forward pass with quantized weights |
524 | | /// |
525 | | /// Dequantizes weights on-the-fly during computation. |
526 | 4 | pub fn forward(&self, token_ids: &[u32]) -> Result<Vec<f32>> { |
527 | 4 | if token_ids.is_empty() { |
528 | 1 | return Err(RealizarError::InvalidShape { |
529 | 1 | reason: "Token sequence cannot be empty".to_string(), |
530 | 1 | }); |
531 | 3 | } |
532 | | |
533 | 3 | let hidden_dim = self.config.hidden_dim; |
534 | 3 | let _vocab_size = self.config.vocab_size; |
535 | | |
536 | | // 1. Token embedding lookup (F32) |
537 | 3 | let mut hidden = Vec::with_capacity(token_ids.len() * hidden_dim); |
538 | 9 | for &token_id6 in token_ids { |
539 | 6 | let offset = (token_id as usize) * hidden_dim; |
540 | 6 | if offset + hidden_dim <= self.token_embedding.len() { |
541 | 5 | hidden.extend_from_slice(&self.token_embedding[offset..offset + hidden_dim]); |
542 | 5 | } else { |
543 | 1 | hidden.extend(std::iter::repeat_n(0.0, hidden_dim)); |
544 | 1 | } |
545 | | } |
546 | | |
547 | | // 2. Process through layers (simplified - dequantize on the fly) |
548 | | // For zero-initialized weights, this is essentially a no-op |
549 | 9 | for _layer_weights6 in &self.layer_weights { |
550 | 6 | // In production: dequantize and apply layer operations |
551 | 6 | // For now with zero weights: output stays the same |
552 | 6 | } |
553 | | |
554 | | // 3. Final layer norm |
555 | 3 | let seq_len = token_ids.len(); |
556 | 3 | let eps = self.config.eps; |
557 | 3 | let mut normed = Vec::with_capacity(hidden.len()); |
558 | | |
559 | 6 | for s in 0..seq_len3 { |
560 | 6 | let start = s * hidden_dim; |
561 | 6 | let slice = &hidden[start..start + hidden_dim]; |
562 | | |
563 | 6 | let mean: f32 = slice.iter().sum::<f32>() / hidden_dim as f32; |
564 | 6 | let variance: f32 = |
565 | 384 | slice6 .iter6 ().map6 (|x| (x - mean).powi(2)).sum6 ::<f32>() / hidden_dim as f326 ; |
566 | 6 | let std_dev = (variance + eps).sqrt(); |
567 | | |
568 | 384 | for (i, &x) in slice6 .iter6 ().enumerate6 () { |
569 | 384 | let normalized = (x - mean) / std_dev; |
570 | 384 | normed.push(normalized * self.output_norm_weight[i]); |
571 | 384 | } |
572 | | } |
573 | | |
574 | | // 4. LM head (take last position, project to vocab) |
575 | 3 | let last_hidden_start = (seq_len - 1) * hidden_dim; |
576 | 3 | let last_hidden = &normed[last_hidden_start..last_hidden_start + hidden_dim]; |
577 | | |
578 | | // Dequantize LM head and compute logits |
579 | 3 | let logits = self.compute_lm_head_logits(last_hidden)?0 ; |
580 | | |
581 | 3 | Ok(logits) |
582 | 4 | } |
583 | | |
584 | | /// Compute LM head logits (dequantize weight and matmul) |
585 | 8 | fn compute_lm_head_logits(&self, _hidden: &[f32]) -> Result<Vec<f32>> { |
586 | 8 | let vocab_size = self.config.vocab_size; |
587 | 8 | let _hidden_dim = self.config.hidden_dim; |
588 | | |
589 | | // For zero-initialized weights, output is zeros |
590 | | // In production: dequantize self.lm_head_weight and compute |
591 | 8 | let logits = vec![0.0f32; vocab_size]; |
592 | | |
593 | | // Simple matmul with dequantized weights (placeholder) |
594 | | // Real implementation would use fused_q4k_dot or dequantize_q8_0 |
595 | 8 | match self.quant_type { |
596 | 8 | AprQuantizationType::F32 => { |
597 | 8 | // No dequantization needed (but we store as bytes anyway) |
598 | 8 | }, |
599 | 0 | AprQuantizationType::Q4_K => { |
600 | 0 | // Would call: fused_q4k_dot for each output |
601 | 0 | }, |
602 | 0 | AprQuantizationType::Q8_0 => { |
603 | 0 | // Would call: dequantize_q8_0 then dot product |
604 | 0 | }, |
605 | | } |
606 | | |
607 | 8 | Ok(logits) |
608 | 8 | } |
609 | | |
610 | | /// Serialize to bytes (APR binary format with quantization) |
611 | 2 | pub fn to_bytes(&self) -> Result<Vec<u8>> { |
612 | 2 | let mut bytes = Vec::new(); |
613 | | |
614 | | // Header (64 bytes) |
615 | 2 | bytes.extend_from_slice(&MAGIC); |
616 | 2 | bytes.extend_from_slice(&1u32.to_le_bytes()); |
617 | 2 | bytes.extend_from_slice(&(self.config.hidden_dim as u32).to_le_bytes()); |
618 | 2 | bytes.extend_from_slice(&(self.config.num_layers as u32).to_le_bytes()); |
619 | 2 | bytes.extend_from_slice(&(self.config.num_heads as u32).to_le_bytes()); |
620 | 2 | bytes.extend_from_slice(&(self.config.num_kv_heads as u32).to_le_bytes()); |
621 | 2 | bytes.extend_from_slice(&(self.config.vocab_size as u32).to_le_bytes()); |
622 | 2 | bytes.extend_from_slice(&(self.config.intermediate_dim as u32).to_le_bytes()); |
623 | 2 | bytes.extend_from_slice(&(self.config.context_length as u32).to_le_bytes()); |
624 | 2 | bytes.extend_from_slice(&self.config.rope_theta.to_le_bytes()); |
625 | 2 | bytes.extend_from_slice(&self.config.eps.to_le_bytes()); |
626 | | |
627 | | // Tensor data offset (after header) |
628 | 2 | let tensor_offset = APR_TRANSFORMER_HEADER_SIZE as u32; |
629 | 2 | bytes.extend_from_slice(&tensor_offset.to_le_bytes()); |
630 | | |
631 | | // Quantization type at offset 48 |
632 | 2 | bytes.push(self.quant_type.to_byte()); |
633 | | |
634 | | // Pad to 64 bytes |
635 | 32 | while bytes.len() < APR_TRANSFORMER_HEADER_SIZE { |
636 | 30 | bytes.push(0); |
637 | 30 | } |
638 | | |
639 | | // Token embeddings (F32) |
640 | 12.8k | for &v12.8k in &self.token_embedding { |
641 | 12.8k | bytes.extend_from_slice(&v.to_le_bytes()); |
642 | 12.8k | } |
643 | | |
644 | | // Layer weights (quantized) |
645 | 6 | for layer4 in &self.layer_weights { |
646 | 4 | bytes.extend_from_slice(layer); |
647 | 4 | } |
648 | | |
649 | | // Output norm (F32) |
650 | 130 | for &v128 in &self.output_norm_weight { |
651 | 128 | bytes.extend_from_slice(&v.to_le_bytes()); |
652 | 128 | } |
653 | | |
654 | | // LM head (quantized) |
655 | 2 | bytes.extend_from_slice(&self.lm_head_weight); |
656 | | |
657 | 2 | Ok(bytes) |
658 | 2 | } |
659 | | |
660 | | /// Deserialize from bytes |
661 | 4 | pub fn from_bytes(data: &[u8]) -> Result<Self> { |
662 | 4 | if data.len() < APR_TRANSFORMER_HEADER_SIZE { |
663 | 1 | return Err(RealizarError::FormatError { |
664 | 1 | reason: format!("Data too small: {} bytes", data.len()), |
665 | 1 | }); |
666 | 3 | } |
667 | | |
668 | | // Verify magic |
669 | 3 | if data[0..4] != MAGIC { |
670 | 1 | return Err(RealizarError::FormatError { |
671 | 1 | reason: "Invalid APR magic".to_string(), |
672 | 1 | }); |
673 | 2 | } |
674 | | |
675 | | // Parse header |
676 | 2 | let hidden_dim = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize; |
677 | 2 | let num_layers = u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize; |
678 | 2 | let num_heads = u32::from_le_bytes([data[16], data[17], data[18], data[19]]) as usize; |
679 | 2 | let num_kv_heads = u32::from_le_bytes([data[20], data[21], data[22], data[23]]) as usize; |
680 | 2 | let vocab_size = u32::from_le_bytes([data[24], data[25], data[26], data[27]]) as usize; |
681 | 2 | let intermediate_dim = |
682 | 2 | u32::from_le_bytes([data[28], data[29], data[30], data[31]]) as usize; |
683 | 2 | let context_length = u32::from_le_bytes([data[32], data[33], data[34], data[35]]) as usize; |
684 | 2 | let rope_theta = f32::from_le_bytes([data[36], data[37], data[38], data[39]]); |
685 | 2 | let eps = f32::from_le_bytes([data[40], data[41], data[42], data[43]]); |
686 | | |
687 | | // Quantization type at offset 48 |
688 | 1 | let quant_type = |
689 | 2 | AprQuantizationType::from_byte(data[48]).ok_or_else(|| RealizarError::FormatError { |
690 | 1 | reason: format!("Invalid quantization type: {}", data[48]), |
691 | 1 | })?; |
692 | | |
693 | 1 | let config = AprTransformerConfig { |
694 | 1 | architecture: "apr".to_string(), |
695 | 1 | hidden_dim, |
696 | 1 | num_layers, |
697 | 1 | num_heads, |
698 | 1 | num_kv_heads, |
699 | 1 | vocab_size, |
700 | 1 | intermediate_dim, |
701 | 1 | context_length, |
702 | 1 | rope_theta, |
703 | 1 | eps, |
704 | 1 | }; |
705 | | |
706 | | // For now, create with default weights |
707 | | // Full implementation would parse the weight data |
708 | 1 | Ok(Self::new(config, quant_type)) |
709 | 4 | } |
710 | | |
711 | | /// Forward pass with KV cache for efficient autoregressive generation (Y4) |
712 | | /// |
713 | | /// Processes a single token using cached key-value pairs from previous positions. |
714 | | /// Uses quantized weights with on-the-fly dequantization. |
715 | | /// |
716 | | /// # Arguments |
717 | | /// |
718 | | /// * `token_id` - Single token ID to process |
719 | | /// * `cache` - Mutable KV cache to read from and append to |
720 | | /// * `position` - Position in sequence (0-indexed) |
721 | | /// |
722 | | /// # Returns |
723 | | /// |
724 | | /// Logits over vocabulary for next token prediction |
725 | 5 | pub fn forward_with_cache( |
726 | 5 | &self, |
727 | 5 | token_id: u32, |
728 | 5 | cache: &mut AprKVCache, |
729 | 5 | _position: usize, |
730 | 5 | ) -> Result<Vec<f32>> { |
731 | 5 | let hidden_dim = self.config.hidden_dim; |
732 | 5 | let num_heads = self.config.num_heads; |
733 | 5 | let num_kv_heads = self.config.num_kv_heads; |
734 | 5 | let head_dim = hidden_dim / num_heads; |
735 | | |
736 | | // 1. Token embedding lookup (F32) |
737 | 5 | let mut hidden = Vec::with_capacity(hidden_dim); |
738 | 5 | let offset = (token_id as usize) * hidden_dim; |
739 | 5 | if offset + hidden_dim <= self.token_embedding.len() { |
740 | 5 | hidden.extend_from_slice(&self.token_embedding[offset..offset + hidden_dim]); |
741 | 5 | } else { |
742 | 0 | hidden.extend(std::iter::repeat_n(0.0, hidden_dim)); |
743 | 0 | } |
744 | | |
745 | | // 2. Process through layers (simplified for quantized) |
746 | 10 | for layer_idx in 0..self.config.num_layers5 { |
747 | 10 | // For zero-initialized quantized weights, output stays mostly the same |
748 | 10 | // In production: dequantize layer weights and compute |
749 | 10 | |
750 | 10 | // Compute placeholder K, V for cache |
751 | 10 | let kv_size = num_kv_heads * head_dim; |
752 | 10 | let k = vec![0.0f32; kv_size]; |
753 | 10 | let v = vec![0.0f32; kv_size]; |
754 | 10 | cache.append(layer_idx, &k, &v); |
755 | 10 | } |
756 | | |
757 | | // 3. Final layer norm |
758 | 5 | let eps = self.config.eps; |
759 | 5 | let mean: f32 = hidden.iter().sum::<f32>() / hidden_dim as f32; |
760 | 5 | let variance: f32 = |
761 | 320 | hidden.iter()5 .map5 (|x| (x - mean).powi(2)).sum5 ::<f32>() / hidden_dim as f325 ; |
762 | 5 | let std_dev = (variance + eps).sqrt(); |
763 | | |
764 | 5 | let mut normed = Vec::with_capacity(hidden_dim); |
765 | 320 | for (i, &x) in hidden.iter()5 .enumerate5 () { |
766 | 320 | let normalized = (x - mean) / std_dev; |
767 | 320 | normed.push(normalized * self.output_norm_weight[i]); |
768 | 320 | } |
769 | | |
770 | | // 4. LM head (dequantize and compute) |
771 | 5 | let logits = self.compute_lm_head_logits(&normed)?0 ; |
772 | | |
773 | 5 | Ok(logits) |
774 | 5 | } |
775 | | } |
776 | | |