/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 | | } |