## 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