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/gpu/adapters/apr.rs
Line
Count
Source
1
//! APR to GpuModel Adapter (PMAT-106)
2
//!
3
//! Converts APR transformers to `GpuModel` for GPU inference.
4
//!
5
//! # Overview
6
//!
7
//! This module provides adapters for both F32 and Q4 APR formats:
8
//! - [`AprF32ToGpuAdapter`] - For `.apr` files with F32 weights (direct copy)
9
//! - [`AprToGpuAdapter`] - For GGUF Q4_0 models (dequantizes to F32)
10
//!
11
//! # Coverage Impact
12
//!
13
//! Testing these adapters exercises:
14
//! - `apr_transformer/mod.rs` - F32 weight extraction
15
//! - `apr_transformer/q4_simd.rs` - Q4 weight extraction
16
//! - `gpu/scheduler/model.rs` - GpuModel creation
17
//! - `quantize/dequant.rs` - Q4_0 dequantization
18
19
use crate::apr_transformer::{
20
    AprTransformer, AprTransformerConfig, AprTransformerLayer,
21
    QuantizedAprTransformerQ4, QuantizedAprLayerQ4,
22
};
23
use crate::gpu::scheduler::{GpuModel, GpuModelConfig, BlockWeights};
24
use crate::quantize::dequantize_q4_0;
25
use crate::error::Result;
26
use thiserror::Error;
27
28
/// Errors during APR to GPU conversion
29
#[derive(Debug, Error)]
30
pub enum AprGpuError {
31
    /// Dequantization failed
32
    #[error("Failed to dequantize Q4_0 weights: {0}")]
33
    DequantError(String),
34
35
    /// Weight dimension mismatch
36
    #[error("Weight dimension mismatch: expected {expected}, got {actual}")]
37
    DimensionMismatch {
38
        /// Expected number of elements
39
        expected: usize,
40
        /// Actual number of elements
41
        actual: usize,
42
    },
43
44
    /// GpuModel creation failed
45
    #[error("Failed to create GpuModel: {0}")]
46
    GpuModelError(String),
47
}
48
49
/// Adapter for converting F32 APR models to GPU format
50
///
51
/// Used for `.apr` files which contain F32 weights. No dequantization needed.
52
pub struct AprF32ToGpuAdapter;
53
54
impl AprF32ToGpuAdapter {
55
    /// Convert F32 APR transformer to GpuModel
56
    ///
57
    /// # Arguments
58
    ///
59
    /// * `apr` - Source APR transformer with F32 weights
60
    ///
61
    /// # Returns
62
    ///
63
    /// `GpuModel` ready for GPU inference
64
    ///
65
    /// # Example
66
    ///
67
    /// ```ignore
68
    /// use realizar::apr_transformer::AprTransformer;
69
    /// use realizar::gpu::adapters::AprF32ToGpuAdapter;
70
    ///
71
    /// let apr = AprTransformer::from_apr_bytes(&data)?;
72
    /// let gpu_model = AprF32ToGpuAdapter::to_gpu_model(&apr)?;
73
    /// ```
74
0
    pub fn to_gpu_model(apr: &AprTransformer) -> Result<GpuModel> {
75
0
        let config = AprToGpuAdapter::config_to_gpu(&apr.config);
76
0
        let hidden_dim = config.hidden_dim;
77
0
        let intermediate_dim = config.intermediate_dim;
78
79
        // Embedding weights (already F32)
80
0
        let embedding_weights = apr.token_embedding.clone();
81
82
        // LM head weights (already F32)
83
0
        let lm_head_weight = apr.lm_head_weight.clone();
84
85
        // Phase 22 FIX: Transpose LM head from APR [vocab_size, hidden_dim] to GPU [hidden_dim, vocab_size]
86
        // APR stores weights as [out_dim, in_dim], GPU matmul expects [in_dim, out_dim]
87
0
        let lm_head_weight_t = transpose_matrix(&lm_head_weight, config.vocab_size, hidden_dim);
88
89
        // Convert each layer
90
0
        let mut block_weights = Vec::with_capacity(apr.layers.len());
91
0
        for layer in &apr.layers {
92
0
            block_weights.push(Self::convert_layer(
93
0
                layer,
94
0
                hidden_dim,
95
0
                intermediate_dim,
96
0
                config.num_heads,
97
0
                config.num_kv_heads,
98
0
            ));
99
0
        }
100
101
        // Final norm
102
0
        let final_norm_weight = apr.output_norm_weight.clone();
103
0
        let final_norm_bias = apr.output_norm_bias.clone().unwrap_or_else(|| vec![0.0; hidden_dim]);
104
105
        // LM head bias
106
0
        let lm_head_bias = apr.lm_head_bias.clone().unwrap_or_else(|| vec![0.0; config.vocab_size]);
107
108
        // Create GpuModel using internal constructor
109
0
        GpuModel::from_apr_weights(
110
0
            config,
111
0
            embedding_weights,
112
0
            block_weights,
113
0
            final_norm_weight,
114
0
            final_norm_bias,
115
0
            lm_head_weight,
116
0
            lm_head_weight_t,
117
0
            lm_head_bias,
118
        )
119
0
    }
120
121
    /// Convert a single F32 layer to BlockWeights
122
0
    fn convert_layer(
123
0
        layer: &AprTransformerLayer,
124
0
        hidden_dim: usize,
125
0
        intermediate_dim: usize,
126
0
        num_heads: usize,
127
0
        num_kv_heads: usize,
128
0
    ) -> BlockWeights {
129
        // Phase 21 FIX: APR stores weights as [out_dim, in_dim] row-major,
130
        // but GPU gemm expects [in_dim, out_dim]. Transpose all projection weights.
131
0
        let head_dim = hidden_dim / num_heads;
132
0
        let kv_dim = num_kv_heads * head_dim;
133
0
        let qkv_out_dim = hidden_dim + 2 * kv_dim;
134
135
        // Transpose QKV: [qkv_out_dim, hidden_dim] -> [hidden_dim, qkv_out_dim]
136
0
        let qkv_weight_t = transpose_matrix(&layer.qkv_weight, qkv_out_dim, hidden_dim);
137
138
        // Transpose output projection: [hidden_dim, hidden_dim] -> [hidden_dim, hidden_dim]
139
0
        let out_weight_t = transpose_matrix(&layer.attn_output_weight, hidden_dim, hidden_dim);
140
141
        // Transpose FFN up (fc1): [intermediate_dim, hidden_dim] -> [hidden_dim, intermediate_dim]
142
0
        let fc1_weight_t = transpose_matrix(&layer.ffn_up_weight, intermediate_dim, hidden_dim);
143
144
        // Transpose FFN down (fc2): [hidden_dim, intermediate_dim] -> [intermediate_dim, hidden_dim]
145
0
        let fc2_weight_t = transpose_matrix(&layer.ffn_down_weight, hidden_dim, intermediate_dim);
146
147
        // Transpose gate weight if present: [intermediate_dim, hidden_dim] -> [hidden_dim, intermediate_dim]
148
0
        let gate_weight_t = layer.ffn_gate_weight.as_ref().map(|w| {
149
0
            transpose_matrix(w, intermediate_dim, hidden_dim)
150
0
        });
151
152
        BlockWeights {
153
0
            attn_norm_weight: layer.attn_norm_weight.clone(),
154
0
            attn_norm_bias: layer.attn_norm_bias.clone().unwrap_or_else(|| vec![0.0; hidden_dim]),
155
0
            qkv_weight: qkv_weight_t,
156
0
            qkv_bias: layer.qkv_bias.clone().unwrap_or_default(),
157
0
            out_weight: out_weight_t,
158
0
            out_bias: layer.attn_output_bias.clone().unwrap_or_else(|| vec![0.0; hidden_dim]),
159
            // Use actual FFN norm if available, otherwise identity (Phase 21 fix)
160
0
            ffn_norm_weight: layer.ffn_norm_weight.clone().unwrap_or_else(|| vec![1.0; hidden_dim]),
161
0
            ffn_norm_bias: layer.ffn_norm_bias.clone().unwrap_or_else(|| vec![0.0; hidden_dim]),
162
0
            ffn_fc1_weight: fc1_weight_t,
163
0
            ffn_fc1_bias: layer.ffn_up_bias.clone().unwrap_or_else(|| vec![0.0; intermediate_dim]),
164
0
            ffn_fc2_weight: fc2_weight_t,
165
0
            ffn_fc2_bias: layer.ffn_down_bias.clone().unwrap_or_else(|| vec![0.0; hidden_dim]),
166
            // SwiGLU gate weight - critical for Qwen/LLaMA models
167
0
            ffn_gate_weight: gate_weight_t,
168
        }
169
0
    }
170
}
171
172
/// Adapter for converting Q4 APR models to GPU format
173
pub struct AprToGpuAdapter;
174
175
impl AprToGpuAdapter {
176
    /// Convert APR config to GPU config
177
    #[must_use]
178
1
    pub fn config_to_gpu(apr_config: &AprTransformerConfig) -> GpuModelConfig {
179
1
        GpuModelConfig {
180
1
            vocab_size: apr_config.vocab_size,
181
1
            hidden_dim: apr_config.hidden_dim,
182
1
            num_heads: apr_config.num_heads,
183
1
            num_kv_heads: apr_config.num_kv_heads,
184
1
            num_layers: apr_config.num_layers,
185
1
            intermediate_dim: apr_config.intermediate_dim,
186
1
            eps: apr_config.eps,
187
1
            rope_theta: apr_config.rope_theta,
188
1
        }
189
1
    }
190
191
    /// Dequantize a Q4_0 tensor to F32
192
    ///
193
    /// # Arguments
194
    ///
195
    /// * `data` - Raw Q4_0 quantized bytes
196
    /// * `expected_elements` - Expected number of output elements
197
    ///
198
    /// # Returns
199
    ///
200
    /// Dequantized F32 vector
201
0
    pub fn dequantize_tensor(data: &[u8], expected_elements: usize) -> Result<Vec<f32>> {
202
0
        let result = dequantize_q4_0(data)?;
203
204
        // Validate dimensions
205
0
        if result.len() < expected_elements {
206
            // Pad with zeros if needed (can happen with block alignment)
207
0
            let mut padded = result;
208
0
            padded.resize(expected_elements, 0.0);
209
0
            Ok(padded)
210
        } else {
211
            // Truncate to expected size
212
0
            Ok(result.into_iter().take(expected_elements).collect())
213
        }
214
0
    }
215
216
    /// Extract QKV weights from APR layer
217
    ///
218
    /// APR stores QKV as a single tensor, which matches GpuModel format.
219
0
    pub fn extract_qkv_weights(
220
0
        layer: &QuantizedAprLayerQ4,
221
0
        hidden_dim: usize,
222
0
        num_heads: usize,
223
0
        num_kv_heads: usize,
224
0
    ) -> Result<Vec<f32>> {
225
0
        let head_dim = hidden_dim / num_heads;
226
0
        let kv_dim = num_kv_heads * head_dim;
227
0
        let qkv_out_dim = hidden_dim + 2 * kv_dim;
228
0
        let expected = hidden_dim * qkv_out_dim;
229
230
0
        Self::dequantize_tensor(&layer.qkv_weight.data, expected)
231
0
    }
232
233
    /// Extract output projection weights
234
0
    pub fn extract_out_weights(
235
0
        layer: &QuantizedAprLayerQ4,
236
0
        hidden_dim: usize,
237
0
    ) -> Result<Vec<f32>> {
238
0
        let expected = hidden_dim * hidden_dim;
239
0
        Self::dequantize_tensor(&layer.attn_output_weight.data, expected)
240
0
    }
241
242
    /// Extract FFN weights (fc1 = up, fc2 = down)
243
    ///
244
    /// Note: APR uses SwiGLU with separate gate/up, but GpuModel combines them.
245
    /// For compatibility, we return up weights as fc1.
246
0
    pub fn extract_ffn_weights(
247
0
        layer: &QuantizedAprLayerQ4,
248
0
        hidden_dim: usize,
249
0
        intermediate_dim: usize,
250
0
    ) -> Result<(Vec<f32>, Vec<f32>)> {
251
        // FC1 (up projection): [hidden_dim, intermediate_dim]
252
0
        let fc1_expected = hidden_dim * intermediate_dim;
253
0
        let fc1 = Self::dequantize_tensor(&layer.ffn_up_weight.data, fc1_expected)?;
254
255
        // FC2 (down projection): [intermediate_dim, hidden_dim]
256
0
        let fc2_expected = intermediate_dim * hidden_dim;
257
0
        let fc2 = Self::dequantize_tensor(&layer.ffn_down_weight.data, fc2_expected)?;
258
259
0
        Ok((fc1, fc2))
260
0
    }
261
262
    /// Convert full APR transformer to GpuModel
263
    ///
264
    /// # Arguments
265
    ///
266
    /// * `apr` - Source APR transformer with Q4_0 weights
267
    ///
268
    /// # Returns
269
    ///
270
    /// `GpuModel` ready for GPU inference
271
    ///
272
    /// # Example
273
    ///
274
    /// ```ignore
275
    /// use realizar::apr_transformer::QuantizedAprTransformerQ4;
276
    /// use realizar::gpu::adapters::AprToGpuAdapter;
277
    ///
278
    /// let apr = QuantizedAprTransformerQ4::from_gguf(&gguf_model);
279
    /// let gpu_model = AprToGpuAdapter::to_gpu_model(&apr)?;
280
    /// ```
281
0
    pub fn to_gpu_model(apr: &QuantizedAprTransformerQ4) -> Result<GpuModel> {
282
0
        let config = Self::config_to_gpu(&apr.config);
283
0
        let hidden_dim = config.hidden_dim;
284
0
        let intermediate_dim = config.intermediate_dim;
285
286
        // Embedding weights (already F32 in APR)
287
0
        let embedding_weights = apr.token_embedding.clone();
288
289
        // Dequantize LM head
290
0
        let lm_head_expected = hidden_dim * config.vocab_size;
291
0
        let lm_head_weight = Self::dequantize_tensor(&apr.lm_head_weight.data, lm_head_expected)?;
292
293
        // Phase 22 FIX: Transpose LM head from APR [vocab_size, hidden_dim] to GPU [hidden_dim, vocab_size]
294
        // APR stores weights as [out_dim, in_dim], GPU matmul expects [in_dim, out_dim]
295
0
        let lm_head_weight_t = transpose_matrix(&lm_head_weight, config.vocab_size, hidden_dim);
296
297
        // Convert each layer
298
0
        let mut block_weights = Vec::with_capacity(apr.layers.len());
299
0
        for layer in &apr.layers {
300
0
            let qkv = Self::extract_qkv_weights(layer, hidden_dim, config.num_heads, config.num_kv_heads)?;
301
0
            let out = Self::extract_out_weights(layer, hidden_dim)?;
302
0
            let (fc1, fc2) = Self::extract_ffn_weights(layer, hidden_dim, intermediate_dim)?;
303
304
            // Extract gate weight for SwiGLU (optional)
305
0
            let ffn_gate_weight = if let Some(ref gate) = layer.ffn_gate_weight {
306
0
                let gate_expected = hidden_dim * intermediate_dim;
307
0
                Some(Self::dequantize_tensor(&gate.data, gate_expected)?)
308
            } else {
309
0
                None
310
            };
311
312
0
            block_weights.push(BlockWeights {
313
0
                attn_norm_weight: layer.attn_norm_weight.clone(),
314
0
                attn_norm_bias: vec![0.0; hidden_dim], // APR doesn't use bias
315
0
                qkv_weight: qkv,
316
0
                qkv_bias: vec![], // No bias in APR
317
0
                out_weight: out,
318
0
                out_bias: vec![0.0; hidden_dim],
319
0
                ffn_norm_weight: layer.ffn_norm_weight.clone().unwrap_or_else(|| vec![1.0; hidden_dim]),
320
0
                ffn_norm_bias: vec![0.0; hidden_dim],
321
0
                ffn_fc1_weight: fc1,
322
0
                ffn_fc1_bias: vec![0.0; intermediate_dim],
323
0
                ffn_fc2_weight: fc2,
324
0
                ffn_fc2_bias: vec![0.0; hidden_dim],
325
0
                ffn_gate_weight,
326
            });
327
        }
328
329
        // Final norm
330
0
        let final_norm_weight = apr.output_norm_weight.clone();
331
0
        let final_norm_bias = vec![0.0; hidden_dim];
332
333
        // LM head bias
334
0
        let lm_head_bias = vec![0.0; config.vocab_size];
335
336
        // Create GpuModel using internal constructor
337
0
        GpuModel::from_apr_weights(
338
0
            config,
339
0
            embedding_weights,
340
0
            block_weights,
341
0
            final_norm_weight,
342
0
            final_norm_bias,
343
0
            lm_head_weight,
344
0
            lm_head_weight_t,
345
0
            lm_head_bias,
346
        )
347
0
    }
348
}
349
350
/// Transpose a row-major matrix
351
2
fn transpose_matrix(data: &[f32], rows: usize, cols: usize) -> Vec<f32> {
352
2
    let mut transposed = vec![0.0; rows * cols];
353
4
    for i in 0..
rows2
{
354
10
        for j in 0..
cols4
{
355
10
            transposed[j * rows + i] = data[i * cols + j];
356
10
        }
357
    }
358
2
    transposed
359
2
}
360
361
#[cfg(test)]
362
mod tests {
363
    use super::*;
364
    use crate::apr_transformer::AprTransformerConfig;
365
366
    #[test]
367
1
    fn test_config_to_gpu() {
368
1
        let apr_config = AprTransformerConfig {
369
1
            architecture: "test".to_string(),
370
1
            hidden_dim: 512,
371
1
            num_layers: 4,
372
1
            num_heads: 8,
373
1
            num_kv_heads: 4,
374
1
            vocab_size: 32000,
375
1
            intermediate_dim: 1024,
376
1
            context_length: 2048,
377
1
            rope_theta: 10000.0,
378
1
            eps: 1e-5,
379
1
        };
380
381
1
        let gpu_config = AprToGpuAdapter::config_to_gpu(&apr_config);
382
383
1
        assert_eq!(gpu_config.vocab_size, 32000);
384
1
        assert_eq!(gpu_config.hidden_dim, 512);
385
1
        assert_eq!(gpu_config.num_heads, 8);
386
1
        assert_eq!(gpu_config.num_kv_heads, 4);
387
1
        assert_eq!(gpu_config.num_layers, 4);
388
1
        assert_eq!(gpu_config.intermediate_dim, 1024);
389
1
        assert_eq!(gpu_config.eps, 1e-5);
390
1
    }
391
392
    #[test]
393
1
    fn test_transpose_matrix() {
394
1
        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; // 2x3
395
1
        let transposed = transpose_matrix(&data, 2, 3); // 3x2
396
397
1
        assert_eq!(transposed, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
398
1
    }
399
400
    #[test]
401
1
    fn test_transpose_identity() {
402
1
        let data = vec![1.0, 2.0, 3.0, 4.0]; // 2x2
403
1
        let transposed = transpose_matrix(&data, 2, 2);
404
405
1
        assert_eq!(transposed, vec![1.0, 3.0, 2.0, 4.0]);
406
1
    }
407
}