/home/noah/src/realizar/src/gguf/transformer.rs
Line | Count | Source |
1 | | //! Quantized GGUF transformer types |
2 | | //! |
3 | | //! This module contains the quantized transformer layer and model structures |
4 | | //! that enable fused dequantization operations for memory-efficient inference. |
5 | | |
6 | | use crate::error::{RealizarError, Result}; |
7 | | use crate::quantize::QK_K; |
8 | | |
9 | | use super::config::GGUFConfig; |
10 | | use super::quantized::{QKVWeights, QuantizedTensorRef}; |
11 | | use super::types::{ |
12 | | GGUFModel, GGUF_TYPE_F32, GGUF_TYPE_Q2_K, GGUF_TYPE_Q4_0, GGUF_TYPE_Q4_1, GGUF_TYPE_Q4_K, |
13 | | GGUF_TYPE_Q5_0, GGUF_TYPE_Q5_K, GGUF_TYPE_Q6_K, GGUF_TYPE_Q8_0, |
14 | | }; |
15 | | |
16 | | /// Quantized transformer layer weights (stored as byte references) |
17 | | /// |
18 | | /// Unlike `GGUFTransformerLayer` which stores dequantized Vec<f32>, |
19 | | /// this stores references to quantized data for fused operations. |
20 | | pub struct QuantizedGGUFTransformerLayer { |
21 | | /// Attention norm weight (kept as f32 - small, read once per token) |
22 | | pub attn_norm_weight: Vec<f32>, |
23 | | /// Attention norm bias (optional) |
24 | | pub attn_norm_bias: Option<Vec<f32>>, |
25 | | /// QKV projection weights (quantized) - supports fused or separate |
26 | | pub qkv_weight: QKVWeights, |
27 | | /// QKV bias (optional, f32) |
28 | | pub qkv_bias: Option<Vec<f32>>, |
29 | | /// Attention output projection (quantized) |
30 | | pub attn_output_weight: QuantizedTensorRef, |
31 | | /// Attention output bias (optional, f32) |
32 | | pub attn_output_bias: Option<Vec<f32>>, |
33 | | /// FFN up projection (quantized) |
34 | | pub ffn_up_weight: QuantizedTensorRef, |
35 | | /// FFN up bias (optional, f32) |
36 | | pub ffn_up_bias: Option<Vec<f32>>, |
37 | | /// FFN down projection (quantized) |
38 | | pub ffn_down_weight: QuantizedTensorRef, |
39 | | /// FFN down bias (optional, f32) |
40 | | pub ffn_down_bias: Option<Vec<f32>>, |
41 | | /// FFN gate projection (quantized, SwiGLU models like LLaMA) |
42 | | pub ffn_gate_weight: Option<QuantizedTensorRef>, |
43 | | /// FFN gate bias (optional, f32) |
44 | | pub ffn_gate_bias: Option<Vec<f32>>, |
45 | | /// FFN norm weight (pre-FFN layer norm, LLaMA-style) |
46 | | pub ffn_norm_weight: Option<Vec<f32>>, |
47 | | /// FFN norm bias (optional, f32) |
48 | | pub ffn_norm_bias: Option<Vec<f32>>, |
49 | | } |
50 | | |
51 | | /// Quantized GGUF Transformer for fused inference |
52 | | /// |
53 | | /// Per Williams et al. (2009) roofline model, LLM inference is memory-bound. |
54 | | /// This transformer stores weights in quantized form and uses fused |
55 | | /// dequant+dot operations to minimize memory bandwidth. |
56 | | /// |
57 | | /// # Performance Benefits |
58 | | /// |
59 | | /// - **8x bandwidth reduction** for Q4_K vs f32 (144 bytes vs 1024 bytes per 256 values) |
60 | | /// - **Zero intermediate buffers** - dequantization happens inline with dot product |
61 | | /// - **SIMD acceleration** - AVX2/FMA fused operations when available |
62 | | /// - **Zero-copy loading** - weights stay in memory-mapped file |
63 | | /// |
64 | | /// # Architecture |
65 | | /// |
66 | | /// ```text |
67 | | /// [Memory-mapped Q4_K bytes] → [fused_q4k_dot_simd] → [f32 result] |
68 | | /// ↑ |
69 | | /// No intermediate Vec<f32>! |
70 | | /// ``` |
71 | | pub struct QuantizedGGUFTransformer<'a> { |
72 | | /// Model configuration |
73 | | pub config: GGUFConfig, |
74 | | /// Reference to memory-mapped file data |
75 | | pub data: &'a [u8], |
76 | | /// Token embedding (kept as f32 for lookup) |
77 | | pub token_embedding: Vec<f32>, |
78 | | /// Quantized layer weights |
79 | | pub layers: Vec<QuantizedGGUFTransformerLayer>, |
80 | | /// Output norm weight (f32) |
81 | | pub output_norm_weight: Vec<f32>, |
82 | | /// Output norm bias (optional) |
83 | | pub output_norm_bias: Option<Vec<f32>>, |
84 | | /// LM head weight (quantized for large vocab) |
85 | | pub lm_head_weight: QuantizedTensorRef, |
86 | | /// LM head bias (optional, f32) |
87 | | pub lm_head_bias: Option<Vec<f32>>, |
88 | | } |
89 | | |
90 | | impl<'a> QuantizedGGUFTransformer<'a> { |
91 | | /// Load quantized transformer from memory-mapped GGUF model |
92 | | /// |
93 | | /// # Arguments |
94 | | /// |
95 | | /// * `model` - Parsed GGUF model metadata |
96 | | /// * `data` - Memory-mapped file data (zero-copy) |
97 | | /// |
98 | | /// # Errors |
99 | | /// |
100 | | /// Returns error if required tensors are missing or have unsupported format |
101 | 12 | pub fn from_gguf(model: &GGUFModel, data: &'a [u8]) -> Result<Self> { |
102 | 12 | let config10 = GGUFConfig::from_gguf(model)?2 ; |
103 | | |
104 | | // Token embedding - keep as f32 for efficient lookup |
105 | 10 | let token_embedding9 = model.get_tensor_f32("token_embd.weight", data)?1 ; |
106 | | |
107 | | // Load layers with quantized weight references |
108 | 9 | let mut layers = Vec::with_capacity(config.num_layers); |
109 | 10 | for layer_idx in 0..config.num_layers9 { |
110 | 10 | let layer = Self::load_quantized_layer(model, data, layer_idx)?0 ; |
111 | 10 | layers.push(layer); |
112 | | } |
113 | | |
114 | | // Output norm - small, keep as f32 |
115 | 9 | let output_norm_weight = model.get_tensor_f32("output_norm.weight", data)?0 ; |
116 | 9 | let output_norm_bias = model.get_tensor_f32("output_norm.bias", data).ok(); |
117 | | |
118 | | // LM head - large, keep quantized |
119 | | // Fall back to token_embd.weight for tied embeddings (Qwen2, some LLaMA variants) |
120 | 9 | let lm_head_weight = Self::get_tensor_ref(model, data, "output.weight") |
121 | 9 | .or_else(|_| Self::get_tensor_ref(model, data, "token_embd.weight"))?0 ; |
122 | 9 | let lm_head_bias = model.get_tensor_f32("output.bias", data).ok(); |
123 | | |
124 | 9 | Ok(Self { |
125 | 9 | config, |
126 | 9 | data, |
127 | 9 | token_embedding, |
128 | 9 | layers, |
129 | 9 | output_norm_weight, |
130 | 9 | output_norm_bias, |
131 | 9 | lm_head_weight, |
132 | 9 | lm_head_bias, |
133 | 9 | }) |
134 | 12 | } |
135 | | |
136 | | /// Get tensor reference (offset + size + qtype) without dequantization |
137 | 95 | fn get_tensor_ref(model: &GGUFModel, data: &[u8], name: &str) -> Result<QuantizedTensorRef> { |
138 | 95 | let tensor76 = model |
139 | 95 | .tensors |
140 | 95 | .iter() |
141 | 727 | .find95 (|t| t.name == name) |
142 | 95 | .ok_or_else(|| RealizarError::InvalidShape { |
143 | 19 | reason: format!("Tensor '{}' not found", name), |
144 | 19 | })?; |
145 | | |
146 | 152 | let num_elements76 : usize76 = tensor.dims.iter()76 .map76 (|&d| d as usize).product76 (); |
147 | 76 | let offset = model.tensor_data_start + tensor.offset as usize; |
148 | | |
149 | | // Calculate byte size based on quantization type |
150 | 76 | let byte_size = match tensor.qtype { |
151 | 9 | GGUF_TYPE_F32 => num_elements * 4, |
152 | | GGUF_TYPE_Q4_0 => { |
153 | | // Q4_0: 32 elements per block |
154 | | // Layout: 1×f16 scale (2 bytes) + 16 bytes (32×4-bit values) = 18 bytes |
155 | | const BLOCK_SIZE: usize = 32; |
156 | | const BLOCK_BYTES: usize = 18; |
157 | 7 | let num_blocks = num_elements.div_ceil(BLOCK_SIZE); |
158 | 7 | num_blocks * BLOCK_BYTES |
159 | | }, |
160 | | GGUF_TYPE_Q8_0 => { |
161 | | const BLOCK_SIZE: usize = 32; |
162 | | const BLOCK_BYTES: usize = 34; // 2 (f16 scale) + 32 (i8 quants) |
163 | 7 | let num_blocks = num_elements.div_ceil(BLOCK_SIZE); |
164 | 7 | num_blocks * BLOCK_BYTES |
165 | | }, |
166 | | GGUF_TYPE_Q2_K => { |
167 | | // Q2_K: 256 elements per super-block |
168 | | // Layout: 16 bytes scales + 64 bytes quants + 2 bytes d + 2 bytes dmin = 84 bytes |
169 | | const SUPER_BLOCK_BYTES: usize = 84; |
170 | 0 | let num_super_blocks = num_elements.div_ceil(QK_K); |
171 | 0 | num_super_blocks * SUPER_BLOCK_BYTES |
172 | | }, |
173 | | GGUF_TYPE_Q4_1 => { |
174 | | // Q4_1: 32 elements per block |
175 | | // Layout: 1×f16 scale (2 bytes) + 1×f16 min (2 bytes) + 16 bytes (32×4-bit values) = 20 bytes |
176 | | const BLOCK_SIZE: usize = 32; |
177 | | const BLOCK_BYTES: usize = 20; |
178 | 0 | let num_blocks = num_elements.div_ceil(BLOCK_SIZE); |
179 | 0 | num_blocks * BLOCK_BYTES |
180 | | }, |
181 | | GGUF_TYPE_Q5_0 => { |
182 | | // Q5_0: 32 elements per block |
183 | | // Layout: 1×f16 scale (2 bytes) + 4 bytes high bits + 16 bytes quants = 22 bytes |
184 | | const BLOCK_SIZE: usize = 32; |
185 | | const BLOCK_BYTES: usize = 22; |
186 | 0 | let num_blocks = num_elements.div_ceil(BLOCK_SIZE); |
187 | 0 | num_blocks * BLOCK_BYTES |
188 | | }, |
189 | | GGUF_TYPE_Q4_K => { |
190 | | const SUPER_BLOCK_BYTES: usize = 144; |
191 | 39 | let num_super_blocks = num_elements.div_ceil(QK_K); |
192 | 39 | num_super_blocks * SUPER_BLOCK_BYTES |
193 | | }, |
194 | | GGUF_TYPE_Q5_K => { |
195 | | const SUPER_BLOCK_BYTES: usize = 176; |
196 | 7 | let num_super_blocks = num_elements.div_ceil(QK_K); |
197 | 7 | num_super_blocks * SUPER_BLOCK_BYTES |
198 | | }, |
199 | | GGUF_TYPE_Q6_K => { |
200 | | const SUPER_BLOCK_BYTES: usize = 210; |
201 | 7 | let num_super_blocks = num_elements.div_ceil(QK_K); |
202 | 7 | num_super_blocks * SUPER_BLOCK_BYTES |
203 | | }, |
204 | | _ => { |
205 | 0 | return Err(RealizarError::UnsupportedOperation { |
206 | 0 | operation: "get_tensor_ref".to_string(), |
207 | 0 | reason: format!("Unsupported quantization type: {}", tensor.qtype), |
208 | 0 | }); |
209 | | }, |
210 | | }; |
211 | | |
212 | | // PAR-058-RESOLVED: Validate byte size and auto-correct qtype if mismatch detected |
213 | | // Some GGUF files have incorrect qtype in header (e.g., Q5_0 header but Q4_0 data) |
214 | | // Detect this by checking if the calculated byte_size would exceed file bounds, |
215 | | // and try alternative qtypes that match the actual data size. |
216 | 76 | let (byte_size, actual_qtype) = { |
217 | | // Try the claimed qtype first |
218 | 76 | if offset + byte_size <= data.len() { |
219 | 76 | (byte_size, tensor.qtype) |
220 | | } else { |
221 | | // Mismatch! Try to infer correct qtype from available data |
222 | | // This happens when GGUF header has wrong qtype (e.g., qwen2.5-coder-0.5b) |
223 | 0 | let avail = data.len().saturating_sub(offset); |
224 | | |
225 | | // Try Q4_0 (18 bytes per 32 elements) |
226 | 0 | let q4_0_size = { |
227 | | const BLOCK_SIZE: usize = 32; |
228 | | const BLOCK_BYTES: usize = 18; |
229 | 0 | num_elements.div_ceil(BLOCK_SIZE) * BLOCK_BYTES |
230 | | }; |
231 | 0 | if q4_0_size <= avail && q4_0_size > 0 { |
232 | 0 | eprintln!( |
233 | 0 | "[PAR-058-RESOLVED] Tensor '{}' qtype mismatch: header says {} but byte size suggests Q4_0. Using Q4_0.", |
234 | | name, tensor.qtype |
235 | | ); |
236 | 0 | (q4_0_size, GGUF_TYPE_Q4_0) |
237 | | } else { |
238 | | // Try Q8_0 (34 bytes per 32 elements) |
239 | 0 | let q8_0_size = { |
240 | | const BLOCK_SIZE: usize = 32; |
241 | | const BLOCK_BYTES: usize = 34; |
242 | 0 | num_elements.div_ceil(BLOCK_SIZE) * BLOCK_BYTES |
243 | | }; |
244 | 0 | if q8_0_size <= avail && q8_0_size > 0 { |
245 | 0 | eprintln!( |
246 | 0 | "[PAR-058-RESOLVED] Tensor '{}' qtype mismatch: header says {} but byte size suggests Q8_0. Using Q8_0.", |
247 | | name, tensor.qtype |
248 | | ); |
249 | 0 | (q8_0_size, GGUF_TYPE_Q8_0) |
250 | | } else { |
251 | | // Fallback to original (will fail bounds check below) |
252 | 0 | (byte_size, tensor.qtype) |
253 | | } |
254 | | } |
255 | | } |
256 | | }; |
257 | | |
258 | | // Validate bounds |
259 | 76 | if offset + byte_size > data.len() { |
260 | 0 | return Err(RealizarError::InvalidShape { |
261 | 0 | reason: format!( |
262 | 0 | "Tensor '{}' data range [{}, {}) exceeds file size {}", |
263 | 0 | name, |
264 | 0 | offset, |
265 | 0 | offset + byte_size, |
266 | 0 | data.len() |
267 | 0 | ), |
268 | 0 | }); |
269 | 76 | } |
270 | | |
271 | 76 | Ok(QuantizedTensorRef { |
272 | 76 | offset, |
273 | 76 | byte_size, |
274 | 76 | num_elements, |
275 | 76 | qtype: actual_qtype, // PAR-058-RESOLVED: Use auto-corrected qtype |
276 | 76 | }) |
277 | 95 | } |
278 | | |
279 | | /// Load a single quantized transformer layer |
280 | 10 | fn load_quantized_layer( |
281 | 10 | model: &GGUFModel, |
282 | 10 | data: &[u8], |
283 | 10 | layer_idx: usize, |
284 | 10 | ) -> Result<QuantizedGGUFTransformerLayer> { |
285 | 10 | let prefix = format!("blk.{}", layer_idx); |
286 | | |
287 | | // Attention norm - small, keep as f32 |
288 | 10 | let attn_norm_weight = |
289 | 10 | model.get_tensor_f32(&format!("{}.attn_norm.weight", prefix), data)?0 ; |
290 | 10 | let attn_norm_bias = model |
291 | 10 | .get_tensor_f32(&format!("{}.attn_norm.bias", prefix), data) |
292 | 10 | .ok(); |
293 | | |
294 | | // QKV - large, keep quantized |
295 | | // Try fused first (phi-2 style), fall back to separate (llama style) |
296 | 10 | let (qkv_weight, qkv_bias) = if let Ok(fused1 ) = |
297 | 10 | Self::get_tensor_ref(model, data, &format!("{}.attn_qkv.weight", prefix)) |
298 | | { |
299 | | // phi-2 style: fused QKV tensor |
300 | 1 | let bias = model |
301 | 1 | .get_tensor_f32(&format!("{}.attn_qkv.bias", prefix), data) |
302 | 1 | .ok(); |
303 | 1 | (QKVWeights::Fused(fused), bias) |
304 | | } else { |
305 | | // llama style: separate Q, K, V tensors |
306 | 9 | let q = Self::get_tensor_ref(model, data, &format!("{}.attn_q.weight", prefix))?0 ; |
307 | 9 | let k = Self::get_tensor_ref(model, data, &format!("{}.attn_k.weight", prefix))?0 ; |
308 | 9 | let v = Self::get_tensor_ref(model, data, &format!("{}.attn_v.weight", prefix))?0 ; |
309 | | |
310 | | // Try to get biases (llama usually doesn't have them) |
311 | 9 | let q_bias = model |
312 | 9 | .get_tensor_f32(&format!("{}.attn_q.bias", prefix), data) |
313 | 9 | .ok(); |
314 | 9 | let k_bias = model |
315 | 9 | .get_tensor_f32(&format!("{}.attn_k.bias", prefix), data) |
316 | 9 | .ok(); |
317 | 9 | let v_bias = model |
318 | 9 | .get_tensor_f32(&format!("{}.attn_v.bias", prefix), data) |
319 | 9 | .ok(); |
320 | | |
321 | 9 | let bias = match (q_bias, k_bias, v_bias) { |
322 | 0 | (Some(qb), Some(kb), Some(vb)) => { |
323 | 0 | let mut combined = Vec::with_capacity(qb.len() + kb.len() + vb.len()); |
324 | 0 | combined.extend_from_slice(&qb); |
325 | 0 | combined.extend_from_slice(&kb); |
326 | 0 | combined.extend_from_slice(&vb); |
327 | 0 | Some(combined) |
328 | | }, |
329 | 9 | _ => None, |
330 | | }; |
331 | | |
332 | 9 | (QKVWeights::Separate { q, k, v }, bias) |
333 | | }; |
334 | | |
335 | | // Attention output - large, keep quantized |
336 | 10 | let attn_output_weight = |
337 | 10 | Self::get_tensor_ref(model, data, &format!("{}.attn_output.weight", prefix))?0 ; |
338 | 10 | let attn_output_bias = model |
339 | 10 | .get_tensor_f32(&format!("{}.attn_output.bias", prefix), data) |
340 | 10 | .ok(); |
341 | | |
342 | | // FFN - large, keep quantized |
343 | 10 | let ffn_up_weight = |
344 | 10 | Self::get_tensor_ref(model, data, &format!("{}.ffn_up.weight", prefix))?0 ; |
345 | 10 | let ffn_up_bias = model |
346 | 10 | .get_tensor_f32(&format!("{}.ffn_up.bias", prefix), data) |
347 | 10 | .ok(); |
348 | 10 | let ffn_down_weight = |
349 | 10 | Self::get_tensor_ref(model, data, &format!("{}.ffn_down.weight", prefix))?0 ; |
350 | 10 | let ffn_down_bias = model |
351 | 10 | .get_tensor_f32(&format!("{}.ffn_down.bias", prefix), data) |
352 | 10 | .ok(); |
353 | | |
354 | | // FFN gate - SwiGLU models like LLaMA have this |
355 | 10 | let ffn_gate_weight = |
356 | 10 | Self::get_tensor_ref(model, data, &format!("{}.ffn_gate.weight", prefix)).ok(); |
357 | 10 | let ffn_gate_bias = model |
358 | 10 | .get_tensor_f32(&format!("{}.ffn_gate.bias", prefix), data) |
359 | 10 | .ok(); |
360 | | |
361 | | // FFN norm - LLaMA-style pre-FFN layer norm |
362 | 10 | let ffn_norm_weight = model |
363 | 10 | .get_tensor_f32(&format!("{}.ffn_norm.weight", prefix), data) |
364 | 10 | .ok(); |
365 | 10 | let ffn_norm_bias = model |
366 | 10 | .get_tensor_f32(&format!("{}.ffn_norm.bias", prefix), data) |
367 | 10 | .ok(); |
368 | | |
369 | 10 | Ok(QuantizedGGUFTransformerLayer { |
370 | 10 | attn_norm_weight, |
371 | 10 | attn_norm_bias, |
372 | 10 | qkv_weight, |
373 | 10 | qkv_bias, |
374 | 10 | attn_output_weight, |
375 | 10 | attn_output_bias, |
376 | 10 | ffn_up_weight, |
377 | 10 | ffn_up_bias, |
378 | 10 | ffn_down_weight, |
379 | 10 | ffn_down_bias, |
380 | 10 | ffn_gate_weight, |
381 | 10 | ffn_gate_bias, |
382 | 10 | ffn_norm_weight, |
383 | 10 | ffn_norm_bias, |
384 | 10 | }) |
385 | 10 | } |
386 | | } |