Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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