/home/noah/src/realizar/src/gguf/inference/forward/batch.rs
Line | Count | Source |
1 | | //! Batched forward pass variants |
2 | | //! |
3 | | //! Contains forward_batch, forward_batch_gpu, forward_batch_with_cache, |
4 | | //! and supporting batch matmul/attention helpers. |
5 | | |
6 | | use crate::error::{RealizarError, Result}; |
7 | | use crate::gguf::ops; |
8 | | use crate::gguf::{ |
9 | | DispatchMetrics, OwnedQuantizedKVCache, OwnedQuantizedModel, OwnedQuantizedTensor, |
10 | | OwnedQKVWeights, QuantizedGenerateConfig, TokenBuffer, GGUF_TYPE_Q4_K, GGUF_TYPE_Q5_K, |
11 | | GGUF_TYPE_Q6_K, |
12 | | }; |
13 | | |
14 | | impl OwnedQuantizedModel { |
15 | | /// Batched forward pass for prompt prefill (PARITY-002) |
16 | | /// |
17 | | /// Processes all prompt tokens at once, enabling GPU acceleration |
18 | | /// for the attention computation when the batch is large enough. |
19 | | /// |
20 | | /// # Arguments |
21 | | /// * `tokens` - All prompt tokens to process at once |
22 | | /// * `cache` - KV cache for storing computed K/V tensors |
23 | | /// * `metrics` - Dispatch metrics tracker for CPU/GPU decision recording |
24 | | /// |
25 | | /// # Returns |
26 | | /// Logits for next token prediction (from the last token position) |
27 | | /// |
28 | | /// # Errors |
29 | | /// Returns error if tensor operations fail |
30 | | #[cfg(feature = "gpu")] |
31 | 5 | pub fn forward_batch_with_cache( |
32 | 5 | &self, |
33 | 5 | tokens: &[u32], |
34 | 5 | cache: &mut OwnedQuantizedKVCache, |
35 | 5 | metrics: &std::sync::Arc<DispatchMetrics>, |
36 | 5 | ) -> Result<Vec<f32>> { |
37 | 5 | if tokens.is_empty() { |
38 | 0 | return Err(RealizarError::InvalidShape { |
39 | 0 | reason: "Tokens cannot be empty".to_string(), |
40 | 0 | }); |
41 | 5 | } |
42 | | |
43 | 5 | let seq_len = tokens.len(); |
44 | 5 | let hidden_dim = self.config.hidden_dim; |
45 | | |
46 | | // 1. Embed all tokens at once: [seq_len, hidden_dim] |
47 | 5 | let mut hidden_states: Vec<Vec<f32>> = tokens |
48 | 5 | .iter() |
49 | 84 | .map5 (|&token_id| self.embed(&[token_id])) |
50 | 5 | .collect(); |
51 | | |
52 | | // 2. Process through transformer layers |
53 | 5 | for (layer_idx, layer) in self.layers.iter().enumerate() { |
54 | | // Collect Q, K, V for all positions |
55 | 5 | let mut all_q: Vec<Vec<f32>> = Vec::with_capacity(seq_len); |
56 | 5 | let mut all_k: Vec<Vec<f32>> = Vec::with_capacity(seq_len); |
57 | 5 | let mut all_v: Vec<Vec<f32>> = Vec::with_capacity(seq_len); |
58 | | |
59 | 84 | for (pos, hidden) in hidden_states.iter()5 .enumerate5 () { |
60 | | // 2a. Attention layer norm |
61 | 84 | let normed = ops::layer_norm( |
62 | 84 | hidden, |
63 | 84 | &layer.attn_norm_weight, |
64 | 84 | layer.attn_norm_bias.as_deref(), |
65 | 84 | self.config.eps, |
66 | | ); |
67 | | |
68 | | // 2b. QKV projection |
69 | 84 | let mut qkv = self.qkv_matmul(&normed, &layer.qkv_weight)?0 ; |
70 | 84 | if let Some(ref bias0 ) = layer.qkv_bias { |
71 | 0 | ops::add_bias(&mut qkv, bias); |
72 | 84 | } |
73 | | |
74 | | // 2c. Extract Q, K, V and apply RoPE |
75 | | // Note: This uses hidden_dim for all (assumes non-GQA or fused QKV) |
76 | 84 | let mut q = qkv[0..hidden_dim].to_vec(); |
77 | 84 | let mut k = qkv[hidden_dim..2 * hidden_dim].to_vec(); |
78 | 84 | let v = qkv[2 * hidden_dim..3 * hidden_dim].to_vec(); |
79 | | |
80 | 84 | self.apply_rope(&mut q, pos, self.config.num_heads); |
81 | 84 | self.apply_rope(&mut k, pos, self.config.num_heads); // Same as Q for non-GQA |
82 | | |
83 | 84 | all_q.push(q); |
84 | 84 | all_k.push(k); |
85 | 84 | all_v.push(v); |
86 | | } |
87 | | |
88 | | // 2d. Compute batched attention |
89 | | // For PARITY-002: This is where GPU can accelerate! |
90 | | // Attention scores: Q @ K^T is [seq_len, seq_len] |
91 | 5 | let attn_outputs = self |
92 | 5 | .batched_attention_with_cache(&all_q, &all_k, &all_v, cache, layer_idx, metrics)?0 ; |
93 | | |
94 | | // 2e. Store all K/V in cache |
95 | 84 | for (k, v) in all_k.iter()5 .zip5 (all_v.iter()5 ) { |
96 | 84 | cache.append(layer_idx, k, v); |
97 | 84 | } |
98 | | |
99 | | // 2f. Attention output projection + residual |
100 | 84 | for (pos, attn_out) in attn_outputs.iter()5 .enumerate5 () { |
101 | 84 | let mut attn_output = self.fused_matmul(attn_out, &layer.attn_output_weight)?0 ; |
102 | 84 | if let Some(ref bias0 ) = layer.attn_output_bias { |
103 | 0 | ops::add_bias(&mut attn_output, bias); |
104 | 84 | } |
105 | | |
106 | | // Residual connection |
107 | 17.6k | for i in 0..hidden_dim84 { |
108 | 17.6k | hidden_states[pos][i] += attn_output[i]; |
109 | 17.6k | } |
110 | | } |
111 | | |
112 | | // 2g. FFN for all positions |
113 | 89 | for hidden84 in &mut hidden_states { |
114 | 84 | let mut ffn_hidden = self.fused_matmul(hidden, &layer.ffn_up_weight)?0 ; |
115 | 84 | if let Some(ref bias0 ) = layer.ffn_up_bias { |
116 | 0 | ops::add_bias(&mut ffn_hidden, bias); |
117 | 84 | } |
118 | 84 | ops::gelu(&mut ffn_hidden); |
119 | | |
120 | 84 | let mut ffn_output = self.fused_matmul(&ffn_hidden, &layer.ffn_down_weight)?0 ; |
121 | 84 | if let Some(ref bias0 ) = layer.ffn_down_bias { |
122 | 0 | ops::add_bias(&mut ffn_output, bias); |
123 | 84 | } |
124 | | |
125 | | // Residual |
126 | 17.6k | for i in 0..hidden_dim84 { |
127 | 17.6k | hidden[i] += ffn_output[i]; |
128 | 17.6k | } |
129 | | } |
130 | | } |
131 | | |
132 | | // Advance cache position for all processed tokens |
133 | 84 | for _ in 0..seq_len5 { |
134 | 84 | cache.advance(); |
135 | 84 | } |
136 | | |
137 | | // 3. Final layer norm and LM head for LAST token only |
138 | 5 | let last_hidden = &hidden_states[seq_len - 1]; |
139 | 5 | let normed = ops::layer_norm( |
140 | 5 | last_hidden, |
141 | 5 | &self.output_norm_weight, |
142 | 5 | self.output_norm_bias.as_deref(), |
143 | 5 | self.config.eps, |
144 | | ); |
145 | | |
146 | | // 4. LM head projection |
147 | 5 | let mut logits = self.fused_matmul(&normed, &self.lm_head_weight)?0 ; |
148 | 5 | if let Some(ref bias0 ) = self.lm_head_bias { |
149 | 0 | ops::add_bias(&mut logits, bias); |
150 | 5 | } |
151 | | |
152 | 5 | Ok(logits) |
153 | 5 | } |
154 | | |
155 | | /// Batched attention computation with GPU acceleration (PARITY-002) |
156 | | /// |
157 | | /// Computes attention for all positions at once, enabling GPU dispatch |
158 | | /// when the workload (seq_len * hidden_dim * seq_len) exceeds the threshold. |
159 | | /// |
160 | | /// KEY OPTIMIZATION: Uses GPU matmul for Q @ K^T when workload is large enough. |
161 | | /// This is the critical path for GPU acceleration - previous implementation only |
162 | | /// recorded metrics without actually using GPU. |
163 | | #[cfg(feature = "gpu")] |
164 | 5 | fn batched_attention_with_cache( |
165 | 5 | &self, |
166 | 5 | all_q: &[Vec<f32>], |
167 | 5 | all_k: &[Vec<f32>], |
168 | 5 | all_v: &[Vec<f32>], |
169 | 5 | cache: &OwnedQuantizedKVCache, |
170 | 5 | layer_idx: usize, |
171 | 5 | metrics: &std::sync::Arc<DispatchMetrics>, |
172 | 5 | ) -> Result<Vec<Vec<f32>>> { |
173 | 5 | let seq_len = all_q.len(); |
174 | 5 | let hidden_dim = self.config.hidden_dim; |
175 | 5 | let num_heads = self.config.num_heads; |
176 | 5 | let head_dim = hidden_dim / num_heads; |
177 | | |
178 | | // Get any cached K/V from previous sequences |
179 | 5 | let cached_k = cache.get_k(layer_idx); |
180 | 5 | let cached_v = cache.get_v(layer_idx); |
181 | 5 | let cache_len = cached_k.len() / hidden_dim; |
182 | | |
183 | | // Build full K/V sequences: [cache + current] |
184 | 5 | let total_len = cache_len + seq_len; |
185 | | |
186 | | // Determine if we should use GPU based on workload size |
187 | | // |
188 | | // IMPORTANT FINDING (IMP-600, PARITY-002): |
189 | | // GPU is 2.7x SLOWER for MATVEC operations (per-head attention is MATVEC) |
190 | | // GPU is 57x FASTER for large GEMM (batch) operations |
191 | | // |
192 | | // For GPU to be beneficial, we need LARGE matrices. Per-head attention |
193 | | // uses tiny matrices: Q[1, head_dim] @ K^T[head_dim, seq_len] = [1, seq_len] |
194 | | // This is a MATVEC operation where GPU transfer overhead dominates. |
195 | | // |
196 | | // Measured result with GPU matmul: 0.20 tok/s (vs 5.31 tok/s CPU) |
197 | | // GPU path is 26x SLOWER due to per-head matmul overhead. |
198 | | // |
199 | | // For true GPU acceleration, need: |
200 | | // - FlashAttention (fused kernel, not yet available in trueno) |
201 | | // - Batched multi-request inference (process multiple prompts together) |
202 | | // |
203 | | // For now, use optimized CPU path which is faster for single-request inference. |
204 | 5 | let workload = num_heads * seq_len * head_dim * total_len; |
205 | 5 | let _ = workload; // Document: GPU not used because MATVEC is slower on GPU |
206 | | |
207 | | // Always use CPU path - it's faster for per-head attention MATVEC |
208 | 5 | metrics.record_cpu_dispatch(); |
209 | 5 | self.cpu_batched_attention( |
210 | 5 | all_q, all_k, all_v, cached_k, cached_v, cache_len, hidden_dim, num_heads, head_dim, |
211 | | ) |
212 | 5 | } |
213 | | |
214 | | /// CPU-based batched attention (fallback for small workloads) |
215 | | #[cfg(feature = "gpu")] |
216 | | #[allow(clippy::too_many_arguments)] // Attention requires all these parameters |
217 | 5 | fn cpu_batched_attention( |
218 | 5 | &self, |
219 | 5 | all_q: &[Vec<f32>], |
220 | 5 | all_k: &[Vec<f32>], |
221 | 5 | all_v: &[Vec<f32>], |
222 | 5 | cached_k: &[f32], |
223 | 5 | cached_v: &[f32], |
224 | 5 | cache_len: usize, |
225 | 5 | hidden_dim: usize, |
226 | 5 | _num_heads: usize, |
227 | 5 | head_dim: usize, |
228 | 5 | ) -> Result<Vec<Vec<f32>>> { |
229 | 5 | let seq_len = all_q.len(); |
230 | 5 | let mut outputs = Vec::with_capacity(seq_len); |
231 | | |
232 | 84 | for (q_pos, q) in all_q5 .iter5 ().enumerate5 () { |
233 | 84 | let attend_len = cache_len + q_pos + 1; |
234 | 84 | let mut k_vecs: Vec<&[f32]> = Vec::with_capacity(attend_len); |
235 | 84 | let mut v_vecs: Vec<&[f32]> = Vec::with_capacity(attend_len); |
236 | | |
237 | | // Add cached K/V |
238 | 84 | for i0 in 0..cache_len { |
239 | 0 | let start = i * hidden_dim; |
240 | 0 | let end = start + hidden_dim; |
241 | 0 | k_vecs.push(&cached_k[start..end]); |
242 | 0 | v_vecs.push(&cached_v[start..end]); |
243 | 0 | } |
244 | | |
245 | | // Add current sequence K/V up to and including current position |
246 | 2.14k | for i in 0..=q_pos84 { |
247 | 2.14k | k_vecs.push(&all_k[i]); |
248 | 2.14k | v_vecs.push(&all_v[i]); |
249 | 2.14k | } |
250 | | |
251 | 84 | let output = self.compute_attention_output(q, &k_vecs, &v_vecs, head_dim)?0 ; |
252 | 84 | outputs.push(output); |
253 | | } |
254 | | |
255 | 5 | Ok(outputs) |
256 | 5 | } |
257 | | |
258 | | /// Compute attention output for a single query against K/V vectors |
259 | | #[cfg(feature = "gpu")] |
260 | 84 | fn compute_attention_output( |
261 | 84 | &self, |
262 | 84 | q: &[f32], |
263 | 84 | k_vecs: &[&[f32]], |
264 | 84 | v_vecs: &[&[f32]], |
265 | 84 | head_dim: usize, |
266 | 84 | ) -> Result<Vec<f32>> { |
267 | 84 | let hidden_dim = q.len(); |
268 | 84 | let num_heads = hidden_dim / head_dim; |
269 | 84 | let seq_len = k_vecs.len(); |
270 | | |
271 | 84 | if seq_len == 0 { |
272 | | // No keys to attend to - return zeros (will be replaced by first attention) |
273 | 0 | return Ok(vec![0.0; hidden_dim]); |
274 | 84 | } |
275 | | |
276 | 84 | let scale = 1.0 / (head_dim as f32).sqrt(); |
277 | 84 | let mut output = vec![0.0; hidden_dim]; |
278 | | |
279 | | // Process each head independently |
280 | 592 | for head in 0..num_heads84 { |
281 | 592 | let head_start = head * head_dim; |
282 | 592 | let head_end = head_start + head_dim; |
283 | | |
284 | 592 | let q_head = &q[head_start..head_end]; |
285 | | |
286 | | // Compute attention scores for this head |
287 | 592 | let mut scores = Vec::with_capacity(seq_len); |
288 | 17.4k | for k16.9k in k_vecs { |
289 | 16.9k | let k_head = &k[head_start..head_end]; |
290 | 536k | let score16.9k : f3216.9k = q_head16.9k .iter16.9k ().zip16.9k (k_head16.9k .iter16.9k ()).map16.9k (|(a, b)| a * b).sum16.9k (); |
291 | 16.9k | scores.push(score * scale); |
292 | | } |
293 | | |
294 | | // Softmax (SIMD-optimized, in-place) |
295 | 592 | crate::quantize::softmax_simd(&mut scores); |
296 | | |
297 | | // Weighted sum of values |
298 | 16.9k | for (attn, v) in scores.iter()592 .zip592 (v_vecs592 .iter592 ()) { |
299 | 16.9k | let v_head = &v[head_start..head_end]; |
300 | 536k | for (i, &v_val) in v_head16.9k .iter16.9k ().enumerate16.9k () { |
301 | 536k | output[head_start + i] += attn * v_val; |
302 | 536k | } |
303 | | } |
304 | | } |
305 | | |
306 | 84 | Ok(output) |
307 | 84 | } |
308 | | |
309 | | /// Generate tokens with batched prompt prefill (PARITY-002) |
310 | | /// |
311 | | /// Uses `forward_batch_with_cache` for initial prompt processing (GPU-accelerated), |
312 | | /// then falls back to single-token generation for autoregressive decoding. |
313 | | /// |
314 | | /// # Arguments |
315 | | /// * `prompt` - Initial token IDs (processed in batch) |
316 | | /// * `config` - Generation configuration |
317 | | /// * `metrics` - Dispatch metrics tracker |
318 | | /// |
319 | | /// # Returns |
320 | | /// Generated token sequence including prompt |
321 | | /// |
322 | | /// # Errors |
323 | | /// Returns error if generation fails |
324 | | #[cfg(feature = "gpu")] |
325 | 1 | pub fn generate_with_batched_prefill( |
326 | 1 | &self, |
327 | 1 | prompt: &[u32], |
328 | 1 | config: &QuantizedGenerateConfig, |
329 | 1 | metrics: &std::sync::Arc<DispatchMetrics>, |
330 | 1 | ) -> Result<Vec<u32>> { |
331 | 1 | if prompt.is_empty() { |
332 | 0 | return Err(RealizarError::InvalidShape { |
333 | 0 | reason: "Prompt cannot be empty".to_string(), |
334 | 0 | }); |
335 | 1 | } |
336 | | |
337 | 1 | let max_seq_len = prompt.len() + config.max_tokens; |
338 | 1 | let mut cache = OwnedQuantizedKVCache::from_config(&self.config, max_seq_len); |
339 | 1 | let mut tokens = prompt.to_vec(); |
340 | | |
341 | | // PARITY-002: Process ALL prompt tokens at once (batched prefill) |
342 | | // This enables GPU acceleration for the attention computation |
343 | 1 | let mut logits = self.forward_batch_with_cache(prompt, &mut cache, metrics)?0 ; |
344 | | |
345 | | // Generate new tokens one at a time (autoregressive) |
346 | 5 | for gen_idx in 0..config.max_tokens1 { |
347 | | // Sample next token from logits |
348 | 5 | let next_token = if config.temperature == 0.0 || config.top_k == 10 { |
349 | 5 | ops::argmax(&logits) |
350 | | } else { |
351 | 0 | crate::gguf::OwnedQuantizedModel::sample_topk(&logits, config.temperature, config.top_k) |
352 | | }; |
353 | | |
354 | | // Check stop condition |
355 | 5 | if config.stop_tokens.contains(&next_token) { |
356 | 0 | break; |
357 | 5 | } |
358 | | |
359 | 5 | tokens.push(next_token); |
360 | | |
361 | | // Check max length |
362 | 5 | if tokens.len() >= max_seq_len { |
363 | 1 | break; |
364 | 4 | } |
365 | | |
366 | | // Forward pass for the new token (single-token, uses CPU) |
367 | 4 | let position = prompt.len() + gen_idx; |
368 | 4 | logits = |
369 | 4 | self.forward_single_with_cache_adaptive(next_token, &mut cache, position, metrics)?0 ; |
370 | | } |
371 | | |
372 | 1 | Ok(tokens) |
373 | 1 | } |
374 | | |
375 | | /// Generate tokens with SmallVec optimization (IMP-117) |
376 | | /// |
377 | | /// Uses SmallVec for token storage to avoid heap allocations when: |
378 | | /// - Prompt + max_tokens <= TOKEN_BUFFER_INLINE_CAP |
379 | | /// |
380 | | /// # Arguments |
381 | | /// * `prompt` - Input token buffer (can be SmallVec or slice) |
382 | | /// * `config` - Generation configuration |
383 | | /// |
384 | | /// # Returns |
385 | | /// Generated token sequence as TokenBuffer (SmallVec) |
386 | | /// |
387 | | /// # Errors |
388 | | /// Returns error if forward pass fails |
389 | 1 | pub fn generate_with_smallvec( |
390 | 1 | &self, |
391 | 1 | prompt: &[u32], |
392 | 1 | config: &QuantizedGenerateConfig, |
393 | 1 | ) -> Result<TokenBuffer> { |
394 | 1 | if prompt.is_empty() { |
395 | 0 | return Err(RealizarError::InvalidShape { |
396 | 0 | reason: "Prompt cannot be empty".to_string(), |
397 | 0 | }); |
398 | 1 | } |
399 | | |
400 | 1 | let max_seq_len = prompt.len() + config.max_tokens; |
401 | 1 | let mut cache = OwnedQuantizedKVCache::from_config(&self.config, max_seq_len); |
402 | | |
403 | | // Use SmallVec for token storage - inline for small sequences |
404 | 1 | let mut tokens: TokenBuffer = TokenBuffer::from_slice(prompt); |
405 | | |
406 | | // Process prompt tokens (prefill) |
407 | 5 | for (pos, &token_id) in prompt1 .iter1 ().enumerate1 () { |
408 | 5 | let _ = self.forward_single_with_cache(token_id, &mut cache, pos)?0 ; |
409 | | } |
410 | | |
411 | | // Generate new tokens |
412 | 10 | for gen_idx in 0..config.max_tokens1 { |
413 | 10 | let position = prompt.len() + gen_idx; |
414 | 10 | let last_token = *tokens.last().ok_or_else(|| RealizarError::InvalidShape { |
415 | 0 | reason: "Token buffer empty during generation".to_string(), |
416 | 0 | })?; |
417 | | |
418 | 10 | let logits = self.forward_single_with_cache(last_token, &mut cache, position)?0 ; |
419 | | |
420 | | // Sample next token |
421 | 10 | let next_token = if config.temperature == 0.0 || config.top_k == 10 { |
422 | 10 | ops::argmax(&logits) |
423 | | } else { |
424 | 0 | crate::gguf::OwnedQuantizedModel::sample_topk(&logits, config.temperature, config.top_k) |
425 | | }; |
426 | | |
427 | | // Check stop condition |
428 | 10 | if config.stop_tokens.contains(&next_token) { |
429 | 0 | break; |
430 | 10 | } |
431 | | |
432 | 10 | tokens.push(next_token); |
433 | | |
434 | | // Check max length |
435 | 10 | if tokens.len() >= max_seq_len { |
436 | 1 | break; |
437 | 9 | } |
438 | | } |
439 | | |
440 | 1 | Ok(tokens) |
441 | 1 | } |
442 | | |
443 | | // ======================================================================== |
444 | | // PARITY-006: Batch Processing - Parallel Token Generation |
445 | | // ======================================================================== |
446 | | |
447 | | /// Generate tokens for multiple requests in parallel (PARITY-006) |
448 | | /// |
449 | | /// This processes multiple independent requests together, enabling GPU GEMM |
450 | | /// acceleration. When batch_size > 1, the matmul operations become: |
451 | | /// `[batch_size, hidden_dim] @ [hidden_dim, output_dim]` which is GEMM. |
452 | | /// |
453 | | /// Per IMP-600: GPU is 57x faster for GEMM vs 2.7x slower for MATVEC. |
454 | | /// Batch inference is the key to utilizing GPU acceleration effectively. |
455 | | /// |
456 | | /// # Arguments |
457 | | /// * `prompts` - Vector of prompts (each prompt is a slice of token IDs) |
458 | | /// * `config` - Generation configuration (shared across all requests) |
459 | | /// |
460 | | /// # Returns |
461 | | /// Vector of generated token sequences (one per input prompt) |
462 | | /// |
463 | | /// # Performance |
464 | | /// - batch_size=1: Falls back to single-request path (CPU optimal) |
465 | | /// - batch_size>1: Uses batched matmul for GPU GEMM acceleration |
466 | | /// |
467 | | /// # Errors |
468 | | /// Returns error if any request fails |
469 | 5 | pub fn batch_generate( |
470 | 5 | &self, |
471 | 5 | prompts: &[&[u32]], |
472 | 5 | config: &QuantizedGenerateConfig, |
473 | 5 | ) -> Result<Vec<Vec<u32>>> { |
474 | 5 | if prompts.is_empty() { |
475 | 1 | return Err(RealizarError::InvalidShape { |
476 | 1 | reason: "Prompts cannot be empty".to_string(), |
477 | 1 | }); |
478 | 4 | } |
479 | | |
480 | | // For single request, use optimized single-request path |
481 | 4 | if prompts.len() == 1 { |
482 | 2 | return Ok(vec![self.generate_with_cache(prompts[0], config)?0 ]); |
483 | 2 | } |
484 | | |
485 | 2 | let batch_size = prompts.len(); |
486 | 7 | let max_prompt_len2 = prompts2 .iter2 ().map2 (|p| p.len()).max2 ().unwrap_or2 (0); |
487 | 2 | let max_seq_len = max_prompt_len + config.max_tokens; |
488 | | |
489 | | // Create KV caches for each request |
490 | 2 | let mut caches: Vec<OwnedQuantizedKVCache> = (0..batch_size) |
491 | 7 | .map2 (|_| OwnedQuantizedKVCache::from_config(&self.config, max_seq_len)) |
492 | 2 | .collect(); |
493 | | |
494 | | // Initialize token sequences with prompts |
495 | 7 | let mut all_tokens2 : Vec<Vec<u32>>2 = prompts2 .iter2 ().map2 (|p| p.to_vec()).collect2 (); |
496 | | |
497 | | // Track which requests are still generating |
498 | 2 | let mut active: Vec<bool> = vec![true; batch_size]; |
499 | | |
500 | | // Prefill phase: process each prompt (can be batched in future) |
501 | 7 | for (req_idx, prompt) in prompts2 .iter2 ().enumerate2 () { |
502 | 21 | for (pos, &token_id) in prompt7 .iter7 ().enumerate7 () { |
503 | 21 | let _ = self.forward_single_with_cache(token_id, &mut caches[req_idx], pos)?0 ; |
504 | | } |
505 | | } |
506 | | |
507 | | // Generation phase: process all active requests together |
508 | 10 | for gen_idx in 0..config.max_tokens2 { |
509 | | // Count active requests |
510 | 10 | let active_count = active.iter().filter(|&&a| a).count(); |
511 | 10 | if active_count == 0 { |
512 | 0 | break; |
513 | 10 | } |
514 | | |
515 | | // Collect last tokens from active requests |
516 | 10 | let active_indices: Vec<usize> = active |
517 | 10 | .iter() |
518 | 10 | .enumerate() |
519 | 10 | .filter(|(_, &a)| a) |
520 | 10 | .map(|(i, _)| i) |
521 | 10 | .collect(); |
522 | | |
523 | | // Process active requests - batched forward pass |
524 | 10 | let mut next_tokens = Vec::with_capacity(active_count); |
525 | | |
526 | 45 | for &req_idx35 in &active_indices { |
527 | 35 | let position = prompts[req_idx].len() + gen_idx; |
528 | 35 | let last_token = *all_tokens[req_idx] |
529 | 35 | .last() |
530 | 35 | .expect("tokens must be non-empty"); |
531 | | |
532 | 35 | let logits = |
533 | 35 | self.forward_single_with_cache(last_token, &mut caches[req_idx], position)?0 ; |
534 | | |
535 | | // Sample next token |
536 | 35 | let next_token = if config.temperature == 0.0 || config.top_k == 10 { |
537 | 35 | ops::argmax(&logits) |
538 | | } else { |
539 | 0 | crate::gguf::OwnedQuantizedModel::sample_topk(&logits, config.temperature, config.top_k) |
540 | | }; |
541 | | |
542 | 35 | next_tokens.push((req_idx, next_token)); |
543 | | } |
544 | | |
545 | | // Apply next tokens and check stop conditions |
546 | 45 | for (req_idx35 , next_token35 ) in next_tokens { |
547 | 35 | if config.stop_tokens.contains(&next_token) { |
548 | 0 | active[req_idx] = false; |
549 | 0 | continue; |
550 | 35 | } |
551 | | |
552 | 35 | all_tokens[req_idx].push(next_token); |
553 | | |
554 | 35 | if all_tokens[req_idx].len() >= max_seq_len { |
555 | 5 | active[req_idx] = false; |
556 | 30 | } |
557 | | } |
558 | | } |
559 | | |
560 | 2 | Ok(all_tokens) |
561 | 5 | } |
562 | | |
563 | | /// Get the batch throughput improvement factor (PARITY-006) |
564 | | /// |
565 | | /// Per IMP-600: GPU GEMM is 57x faster than MATVEC. |
566 | | /// Batch inference converts MATVEC to GEMM when batch_size > 1. |
567 | | /// |
568 | | /// # Arguments |
569 | | /// * `batch_size` - Number of concurrent requests |
570 | | /// |
571 | | /// # Returns |
572 | | /// Estimated throughput multiplier vs single-request |
573 | | #[must_use] |
574 | 3 | pub const fn batch_throughput_factor(batch_size: usize) -> f64 { |
575 | 3 | match batch_size { |
576 | 1 | 0 | 1 => 1.0, |
577 | 2 | 2..=4 => 1.81 , // ~2x throughput with small batch |
578 | 1 | 5..=8 => 2.50 , // GPU GEMM starts to help |
579 | 1 | 9..=16 => 3.5, // Good GPU utilization |
580 | 0 | 17..=32 => 5.0, // Near-optimal batch |
581 | 0 | _ => 6.0, // Large batch, GPU-limited |
582 | | } |
583 | 3 | } |
584 | | |
585 | | /// Forward pass for a batch of tokens (IMP-106) |
586 | | /// |
587 | | /// Processes multiple tokens through the transformer in parallel. |
588 | | /// This is more efficient than sequential processing for prefill. |
589 | | /// |
590 | | /// # Arguments |
591 | | /// * `token_ids` - Batch of input token IDs [batch_size] |
592 | | /// |
593 | | /// # Returns |
594 | | /// Logits for all positions [batch_size * vocab_size] |
595 | | /// |
596 | | /// # Errors |
597 | | /// Returns error if tensor operations fail |
598 | 2 | pub fn forward_batch(&self, token_ids: &[u32]) -> Result<Vec<f32>> { |
599 | 2 | let batch_size = token_ids.len(); |
600 | 2 | let hidden_dim = self.config.hidden_dim; |
601 | | |
602 | | // 1. Token embedding lookup for all tokens |
603 | 2 | let mut hidden = self.embed(token_ids); |
604 | | |
605 | | // 2. Process through transformer layers |
606 | 4 | for layer2 in &self.layers { |
607 | | // Pre-attention LayerNorm |
608 | 2 | let normed = ops::layer_norm( |
609 | 2 | &hidden, |
610 | 2 | &layer.attn_norm_weight, |
611 | 2 | layer.attn_norm_bias.as_deref(), |
612 | 2 | self.config.eps, |
613 | | ); |
614 | | |
615 | | // QKV projection (batched) |
616 | 2 | let qkv = self.qkv_matmul(&normed, &layer.qkv_weight)?0 ; |
617 | | |
618 | | // Split Q, K, V for batch - simplified attention (no causal mask for batch) |
619 | 2 | let qkv_dim = qkv.len() / batch_size; |
620 | 2 | let q_dim = hidden_dim; |
621 | 2 | let kv_dim = (qkv_dim - q_dim) / 2; |
622 | | |
623 | | // Process attention for each position (simplified for batch) |
624 | 2 | let mut attn_out = Vec::with_capacity(batch_size * hidden_dim); |
625 | 8 | for pos in 0..batch_size2 { |
626 | 8 | let qkv_start = pos * qkv_dim; |
627 | 8 | let q = &qkv[qkv_start..qkv_start + q_dim]; |
628 | 8 | let k = &qkv[qkv_start + q_dim..qkv_start + q_dim + kv_dim]; |
629 | 8 | let v = &qkv[qkv_start + q_dim + kv_dim..qkv_start + qkv_dim]; |
630 | | |
631 | | // Simple self-attention for current position (attend to itself only for simplicity) |
632 | | // Full causal attention would require attending to all previous positions |
633 | 8 | let head_dim = hidden_dim / self.config.num_heads; |
634 | 8 | let scale = 1.0 / (head_dim as f32).sqrt(); |
635 | | |
636 | 8 | let mut out = vec![0.0f32; hidden_dim]; |
637 | 32 | for h in 0..self.config.num_heads8 { |
638 | 32 | let kv_h = h * self.config.num_kv_heads / self.config.num_heads; |
639 | 32 | let q_h = &q[h * head_dim..(h + 1) * head_dim]; |
640 | 32 | let k_h = &k[kv_h * head_dim..(kv_h + 1) * head_dim]; |
641 | 32 | let v_h = &v[kv_h * head_dim..(kv_h + 1) * head_dim]; |
642 | | |
643 | | // Score and softmax (single position = 1.0 weight) |
644 | 32 | let mut score = 0.0f32; |
645 | 256 | for d in 0..head_dim32 { |
646 | 256 | score += q_h[d] * k_h[d]; |
647 | 256 | } |
648 | 32 | let _weight = (score * scale).exp(); // softmax of single value = 1.0 |
649 | | |
650 | | // Apply value |
651 | 256 | for d in 0..head_dim32 { |
652 | 256 | out[h * head_dim + d] = v_h[d]; |
653 | 256 | } |
654 | | } |
655 | 8 | attn_out.extend_from_slice(&out); |
656 | | } |
657 | | |
658 | | // Output projection |
659 | 2 | let projected = self.fused_matmul(&attn_out, &layer.attn_output_weight)?0 ; |
660 | | |
661 | | // Residual connection |
662 | 256 | for i in 0..hidden2 .len2 () { |
663 | 256 | hidden[i] += projected[i]; |
664 | 256 | } |
665 | | |
666 | | // FFN (pre-norm style) |
667 | 2 | let ffn_normed = |
668 | 2 | ops::layer_norm(&hidden, &layer.attn_norm_weight, None, self.config.eps); |
669 | 2 | let up = self.fused_matmul(&ffn_normed, &layer.ffn_up_weight)?0 ; |
670 | | |
671 | | // GELU activation |
672 | 2 | let gelu: Vec<f32> = up |
673 | 2 | .iter() |
674 | 512 | .map2 (|&x| 0.5 * x * (1.0 + (0.797_884_6 * (x + 0.044_715 * x.powi(3))).tanh())) |
675 | 2 | .collect(); |
676 | | |
677 | 2 | let down = self.fused_matmul(&gelu, &layer.ffn_down_weight)?0 ; |
678 | | |
679 | | // Residual connection |
680 | 256 | for i in 0..hidden2 .len2 () { |
681 | 256 | hidden[i] += down[i]; |
682 | 256 | } |
683 | | } |
684 | | |
685 | | // 3. Final LayerNorm |
686 | 2 | let normed = ops::layer_norm( |
687 | 2 | &hidden, |
688 | 2 | &self.output_norm_weight, |
689 | 2 | self.output_norm_bias.as_deref(), |
690 | 2 | self.config.eps, |
691 | | ); |
692 | | |
693 | | // 4. LM head projection to vocab logits |
694 | 2 | let logits = self.fused_matmul(&normed, &self.lm_head_weight)?0 ; |
695 | | |
696 | 2 | Ok(logits) |
697 | 2 | } |
698 | | |
699 | | /// Prefill prompt tokens with batched forward pass (IMP-106) |
700 | | /// |
701 | | /// Efficiently processes all prompt tokens and populates the KV cache. |
702 | | /// Returns the last position's logits for sampling. |
703 | | /// |
704 | | /// # Arguments |
705 | | /// * `prompt` - Prompt token IDs |
706 | | /// * `cache` - KV cache to populate |
707 | | /// |
708 | | /// # Returns |
709 | | /// Logits for the last position [vocab_size] |
710 | | /// |
711 | | /// # Errors |
712 | | /// Returns error if forward pass fails |
713 | 1 | pub fn prefill_batch( |
714 | 1 | &self, |
715 | 1 | prompt: &[u32], |
716 | 1 | cache: &mut OwnedQuantizedKVCache, |
717 | 1 | ) -> Result<Vec<f32>> { |
718 | 1 | if prompt.is_empty() { |
719 | 0 | return Err(RealizarError::InvalidShape { |
720 | 0 | reason: "Prompt cannot be empty".to_string(), |
721 | 0 | }); |
722 | 1 | } |
723 | | |
724 | | // Process each position to populate KV cache |
725 | | // (True batch prefill would compute all positions at once with causal attention) |
726 | 1 | let mut last_logits = Vec::new(); |
727 | 4 | for (pos, &token_id) in prompt1 .iter1 ().enumerate1 () { |
728 | 4 | last_logits = self.forward_single_with_cache(token_id, cache, pos)?0 ; |
729 | | } |
730 | | |
731 | 1 | Ok(last_logits) |
732 | 1 | } |
733 | | |
734 | | /// Forward pass for a batch of tokens with GPU acceleration (IMP-107) |
735 | | /// |
736 | | /// Uses HybridScheduler to route matmuls to GPU when batch_size > 1 |
737 | | /// and matrix size exceeds threshold. Falls back to CPU for small batches. |
738 | | /// |
739 | | /// # Arguments |
740 | | /// * `token_ids` - Batch of input token IDs [batch_size] |
741 | | /// |
742 | | /// # Returns |
743 | | /// Logits for all positions [batch_size * vocab_size] |
744 | | /// |
745 | | /// # Errors |
746 | | /// Returns error if GPU initialization or tensor operations fail |
747 | | #[cfg(feature = "gpu")] |
748 | 3 | pub fn forward_batch_gpu(&self, token_ids: &[u32]) -> Result<Vec<f32>> { |
749 | | use crate::gpu::HybridScheduler; |
750 | | |
751 | 3 | let batch_size = token_ids.len(); |
752 | 3 | let hidden_dim = self.config.hidden_dim; |
753 | 3 | let vocab_size = self.config.vocab_size; |
754 | | |
755 | | // Initialize HybridScheduler with reasonable threshold |
756 | | // Threshold of 1000 means: batch_size * hidden_dim * out_dim > 1000 uses GPU |
757 | 3 | let mut scheduler = HybridScheduler::with_threshold(1000).map_err(|e| {0 |
758 | 0 | RealizarError::UnsupportedOperation { |
759 | 0 | operation: "HybridScheduler::with_threshold".to_string(), |
760 | 0 | reason: format!("GPU scheduler initialization failed: {e}"), |
761 | 0 | } |
762 | 0 | })?; |
763 | | |
764 | | // 1. Token embedding lookup for all tokens |
765 | 3 | let mut hidden = self.embed(token_ids); |
766 | | |
767 | | // 2. Process through transformer layers |
768 | 6 | for layer3 in &self.layers { |
769 | | // Pre-attention LayerNorm |
770 | 3 | let normed = ops::layer_norm( |
771 | 3 | &hidden, |
772 | 3 | &layer.attn_norm_weight, |
773 | 3 | layer.attn_norm_bias.as_deref(), |
774 | 3 | self.config.eps, |
775 | | ); |
776 | | |
777 | | // QKV projection - use GPU for batch ops |
778 | 3 | let qkv = self.batch_qkv_matmul_gpu_with_scheduler( |
779 | 3 | &normed, |
780 | 3 | &layer.qkv_weight, |
781 | 3 | batch_size, |
782 | 3 | hidden_dim, |
783 | 3 | &mut scheduler, |
784 | 0 | )?; |
785 | | |
786 | | // Split Q, K, V for batch - PARITY-114: use proper batched causal attention |
787 | 3 | let qkv_dim = qkv.len() / batch_size; |
788 | 3 | let q_dim = hidden_dim; |
789 | 3 | let kv_dim = (qkv_dim - q_dim) / 2; |
790 | | |
791 | | // Collect Q, K, V for all positions |
792 | 3 | let mut q_all = Vec::with_capacity(batch_size * q_dim); |
793 | 3 | let mut k_all = Vec::with_capacity(batch_size * kv_dim); |
794 | 3 | let mut v_all = Vec::with_capacity(batch_size * kv_dim); |
795 | | |
796 | 20 | for pos in 0..batch_size3 { |
797 | 20 | let qkv_start = pos * qkv_dim; |
798 | 20 | q_all.extend_from_slice(&qkv[qkv_start..qkv_start + q_dim]); |
799 | 20 | k_all.extend_from_slice(&qkv[qkv_start + q_dim..qkv_start + q_dim + kv_dim]); |
800 | 20 | v_all.extend_from_slice(&qkv[qkv_start + q_dim + kv_dim..qkv_start + qkv_dim]); |
801 | 20 | } |
802 | | |
803 | | // Proper batched causal attention (PARITY-114: matches cached forward path) |
804 | 3 | let attn_out = self.batched_causal_attention_gpu(&q_all, &k_all, &v_all, batch_size)?0 ; |
805 | | |
806 | | // Output projection - use GPU for batch ops |
807 | 3 | let projected = self.batch_matmul_gpu( |
808 | 3 | &attn_out, |
809 | 3 | &layer.attn_output_weight, |
810 | 3 | batch_size, |
811 | 3 | hidden_dim, |
812 | 3 | layer.attn_output_weight.out_dim, |
813 | 3 | &mut scheduler, |
814 | 0 | )?; |
815 | | |
816 | | // Residual connection |
817 | 1.28k | for i in 0..hidden3 .len3 () { |
818 | 1.28k | hidden[i] += projected[i]; |
819 | 1.28k | } |
820 | | |
821 | | // FFN (pre-norm style) |
822 | 3 | let ffn_normed = ops::layer_norm( |
823 | 3 | &hidden, |
824 | 3 | &layer.attn_norm_weight, |
825 | 3 | layer.attn_norm_bias.as_deref(), |
826 | 3 | self.config.eps, |
827 | | ); |
828 | | |
829 | | // FFN up projection - use GPU |
830 | 3 | let mut ffn_hidden = self.batch_matmul_gpu( |
831 | 3 | &ffn_normed, |
832 | 3 | &layer.ffn_up_weight, |
833 | 3 | batch_size, |
834 | 3 | hidden_dim, |
835 | 3 | layer.ffn_up_weight.out_dim, |
836 | 3 | &mut scheduler, |
837 | 0 | )?; |
838 | | |
839 | | // GELU activation |
840 | 3 | ops::gelu(&mut ffn_hidden); |
841 | | |
842 | | // FFN down projection - use GPU |
843 | 3 | let ffn_output = self.batch_matmul_gpu( |
844 | 3 | &ffn_hidden, |
845 | 3 | &layer.ffn_down_weight, |
846 | 3 | batch_size, |
847 | 3 | layer.ffn_up_weight.out_dim, |
848 | 3 | hidden_dim, |
849 | 3 | &mut scheduler, |
850 | 0 | )?; |
851 | | |
852 | | // Residual |
853 | 1.28k | for i in 0..hidden3 .len3 () { |
854 | 1.28k | hidden[i] += ffn_output[i]; |
855 | 1.28k | } |
856 | | } |
857 | | |
858 | | // 3. Final layer norm |
859 | 3 | let normed = ops::layer_norm( |
860 | 3 | &hidden, |
861 | 3 | &self.output_norm_weight, |
862 | 3 | self.output_norm_bias.as_deref(), |
863 | 3 | self.config.eps, |
864 | | ); |
865 | | |
866 | | // 4. LM head projection - use GPU for large vocab |
867 | 3 | let logits = self.batch_matmul_gpu( |
868 | 3 | &normed, |
869 | 3 | &self.lm_head_weight, |
870 | 3 | batch_size, |
871 | 3 | hidden_dim, |
872 | 3 | vocab_size, |
873 | 3 | &mut scheduler, |
874 | 0 | )?; |
875 | | |
876 | 3 | Ok(logits) |
877 | 3 | } |
878 | | |
879 | | /// Batch matmul with GPU acceleration via HybridScheduler (IMP-107) |
880 | | /// |
881 | | /// Dequantizes weights and uses GPU for large operations. |
882 | | #[cfg(feature = "gpu")] |
883 | 15 | fn batch_matmul_gpu( |
884 | 15 | &self, |
885 | 15 | input: &[f32], |
886 | 15 | weight: &OwnedQuantizedTensor, |
887 | 15 | m: usize, |
888 | 15 | k: usize, |
889 | 15 | n: usize, |
890 | 15 | scheduler: &mut crate::gpu::HybridScheduler, |
891 | 15 | ) -> Result<Vec<f32>> { |
892 | | // Dequantize weight to f32 |
893 | 15 | let weight_f32 = self.dequantize_weight(weight)?0 ; |
894 | | |
895 | | // Use HybridScheduler for GPU/CPU dispatch |
896 | | // A: [m, k], B: [k, n] -> C: [m, n] |
897 | 15 | scheduler.matmul(input, &weight_f32, m, k, n).map_err(|e| {0 |
898 | 0 | RealizarError::UnsupportedOperation { |
899 | 0 | operation: "HybridScheduler::matmul".to_string(), |
900 | 0 | reason: format!("GPU matmul failed: {e}"), |
901 | 0 | } |
902 | 0 | }) |
903 | 15 | } |
904 | | |
905 | | /// Batch QKV matmul with GPU acceleration via HybridScheduler |
906 | | /// |
907 | | /// Five Whys Root Cause Fix: Handles both fused and separate Q/K/V formats |
908 | | #[cfg(feature = "gpu")] |
909 | 3 | fn batch_qkv_matmul_gpu_with_scheduler( |
910 | 3 | &self, |
911 | 3 | input: &[f32], |
912 | 3 | qkv: &OwnedQKVWeights, |
913 | 3 | batch_size: usize, |
914 | 3 | hidden_dim: usize, |
915 | 3 | scheduler: &mut crate::gpu::HybridScheduler, |
916 | 3 | ) -> Result<Vec<f32>> { |
917 | 3 | match qkv { |
918 | 3 | OwnedQKVWeights::Fused(ref weight) => self.batch_matmul_gpu( |
919 | 3 | input, |
920 | 3 | weight, |
921 | 3 | batch_size, |
922 | 3 | hidden_dim, |
923 | 3 | weight.out_dim, |
924 | 3 | scheduler, |
925 | | ), |
926 | | OwnedQKVWeights::Separate { |
927 | 0 | ref q, |
928 | 0 | ref k, |
929 | 0 | ref v, |
930 | | } => { |
931 | | // Compute Q, K, V separately then concatenate |
932 | 0 | let q_out = |
933 | 0 | self.batch_matmul_gpu(input, q, batch_size, hidden_dim, q.out_dim, scheduler)?; |
934 | 0 | let k_out = |
935 | 0 | self.batch_matmul_gpu(input, k, batch_size, hidden_dim, k.out_dim, scheduler)?; |
936 | 0 | let v_out = |
937 | 0 | self.batch_matmul_gpu(input, v, batch_size, hidden_dim, v.out_dim, scheduler)?; |
938 | | |
939 | | // Interleave Q, K, V for each position in batch |
940 | 0 | let qkv_dim = q.out_dim + k.out_dim + v.out_dim; |
941 | 0 | let mut output = Vec::with_capacity(batch_size * qkv_dim); |
942 | 0 | for b in 0..batch_size { |
943 | 0 | output.extend_from_slice(&q_out[b * q.out_dim..(b + 1) * q.out_dim]); |
944 | 0 | output.extend_from_slice(&k_out[b * k.out_dim..(b + 1) * k.out_dim]); |
945 | 0 | output.extend_from_slice(&v_out[b * v.out_dim..(b + 1) * v.out_dim]); |
946 | 0 | } |
947 | 0 | Ok(output) |
948 | | }, |
949 | | } |
950 | 3 | } |
951 | | |
952 | | /// Dequantize a weight tensor to f32 |
953 | | #[cfg(feature = "gpu")] |
954 | 47 | pub(crate) fn dequantize_weight(&self, weight: &OwnedQuantizedTensor) -> Result<Vec<f32>> { |
955 | | use crate::quantize::{dequantize_q4_k_simd, dequantize_q5_k, dequantize_q6_k, QK_K}; |
956 | | |
957 | 47 | let in_dim = weight.in_dim; |
958 | 47 | let out_dim = weight.out_dim; |
959 | 47 | let total_elements = in_dim * out_dim; |
960 | | |
961 | 47 | match weight.qtype { |
962 | | GGUF_TYPE_Q4_K => { |
963 | 47 | let super_blocks_per_row = in_dim.div_ceil(QK_K); |
964 | 47 | let mut output = Vec::with_capacity(total_elements); |
965 | 5.95k | for row in 0..out_dim47 { |
966 | 5.95k | let row_start = row * super_blocks_per_row * 144; |
967 | 5.95k | let row_end = row_start + super_blocks_per_row * 144; |
968 | 5.95k | let row_data = &weight.data[row_start..row_end]; |
969 | 5.95k | let row_dequant = dequantize_q4_k_simd(row_data)?0 ; |
970 | | // Take only in_dim values (may have padding due to super-block alignment) |
971 | 5.95k | output.extend_from_slice(&row_dequant[..in_dim.min(row_dequant.len())]); |
972 | | } |
973 | 47 | Ok(output) |
974 | | }, |
975 | | GGUF_TYPE_Q5_K => { |
976 | 0 | let super_blocks_per_row = in_dim.div_ceil(QK_K); |
977 | 0 | let mut output = Vec::with_capacity(total_elements); |
978 | 0 | for row in 0..out_dim { |
979 | 0 | let row_start = row * super_blocks_per_row * 176; |
980 | 0 | let row_end = row_start + super_blocks_per_row * 176; |
981 | 0 | let row_data = &weight.data[row_start..row_end]; |
982 | 0 | let row_dequant = dequantize_q5_k(row_data)?; |
983 | 0 | output.extend_from_slice(&row_dequant[..in_dim.min(row_dequant.len())]); |
984 | | } |
985 | 0 | Ok(output) |
986 | | }, |
987 | | GGUF_TYPE_Q6_K => { |
988 | 0 | let super_blocks_per_row = in_dim.div_ceil(QK_K); |
989 | 0 | let mut output = Vec::with_capacity(total_elements); |
990 | 0 | for row in 0..out_dim { |
991 | 0 | let row_start = row * super_blocks_per_row * 210; |
992 | 0 | let row_end = row_start + super_blocks_per_row * 210; |
993 | 0 | let row_data = &weight.data[row_start..row_end]; |
994 | 0 | let row_dequant = dequantize_q6_k(row_data)?; |
995 | 0 | output.extend_from_slice(&row_dequant[..in_dim.min(row_dequant.len())]); |
996 | | } |
997 | 0 | Ok(output) |
998 | | }, |
999 | | _ => { |
1000 | | // F32 or unsupported - interpret raw bytes as f32 |
1001 | 0 | let num_floats = weight.data.len() / 4; |
1002 | 0 | let mut output = vec![0.0f32; num_floats]; |
1003 | 0 | for (i, chunk) in weight.data.chunks_exact(4).enumerate() { |
1004 | 0 | output[i] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); |
1005 | 0 | } |
1006 | 0 | Ok(output) |
1007 | | }, |
1008 | | } |
1009 | 47 | } |
1010 | | |
1011 | | /// Dequantize QKV weights - handles both fused and separate formats |
1012 | | /// |
1013 | | /// Five Whys Root Cause Fix: This method handles both tensor layouts for dequantization |
1014 | | #[cfg(feature = "gpu")] |
1015 | 0 | pub fn dequantize_qkv(&self, qkv: &OwnedQKVWeights) -> Result<Vec<f32>> { |
1016 | 0 | match qkv { |
1017 | 0 | OwnedQKVWeights::Fused(ref weight) => self.dequantize_weight(weight), |
1018 | | OwnedQKVWeights::Separate { |
1019 | 0 | ref q, |
1020 | 0 | ref k, |
1021 | 0 | ref v, |
1022 | | } => { |
1023 | | // Dequantize each separately and concatenate |
1024 | 0 | let q_out = self.dequantize_weight(q)?; |
1025 | 0 | let k_out = self.dequantize_weight(k)?; |
1026 | 0 | let v_out = self.dequantize_weight(v)?; |
1027 | | |
1028 | 0 | let mut output = Vec::with_capacity(q_out.len() + k_out.len() + v_out.len()); |
1029 | 0 | output.extend_from_slice(&q_out); |
1030 | 0 | output.extend_from_slice(&k_out); |
1031 | 0 | output.extend_from_slice(&v_out); |
1032 | 0 | Ok(output) |
1033 | | }, |
1034 | | } |
1035 | 0 | } |
1036 | | |
1037 | | /// Fused batch matmul with GPU acceleration (IMP-109) |
1038 | | /// |
1039 | | /// Performs batched matrix multiplication with fused dequantization. |
1040 | | /// Uses the same weight layout interpretation as `batch_matmul_gpu` for |
1041 | | /// consistency within the codebase. |
1042 | | /// |
1043 | | /// Key optimization: Dequantizes weight matrix once for all batch elements, |
1044 | | /// reducing memory bandwidth for repeated operations in transformer layers. |
1045 | | /// |
1046 | | /// # Arguments |
1047 | | /// * `input` - Input tensor [batch_size, in_dim] |
1048 | | /// * `weight` - Quantized weight tensor [out_dim, in_dim] |
1049 | | /// * `batch_size` - Number of input vectors |
1050 | | /// |
1051 | | /// # Returns |
1052 | | /// Output tensor [batch_size, out_dim] |
1053 | | /// |
1054 | | /// # Errors |
1055 | | /// Returns error if GPU operations fail or dimensions mismatch |
1056 | | #[cfg(feature = "gpu")] |
1057 | 2 | pub fn fused_batch_matmul_gpu( |
1058 | 2 | &self, |
1059 | 2 | input: &[f32], |
1060 | 2 | weight: &OwnedQuantizedTensor, |
1061 | 2 | batch_size: usize, |
1062 | 2 | ) -> Result<Vec<f32>> { |
1063 | | use crate::gpu::HybridScheduler; |
1064 | | |
1065 | 2 | let in_dim = weight.in_dim; |
1066 | 2 | let out_dim = weight.out_dim; |
1067 | | |
1068 | | // Validate input dimensions |
1069 | 2 | if input.len() != batch_size * in_dim { |
1070 | 0 | return Err(RealizarError::InvalidShape { |
1071 | 0 | reason: format!( |
1072 | 0 | "Input size {} doesn't match batch_size={} * in_dim={}={}", |
1073 | 0 | input.len(), |
1074 | 0 | batch_size, |
1075 | 0 | in_dim, |
1076 | 0 | batch_size * in_dim |
1077 | 0 | ), |
1078 | 0 | }); |
1079 | 2 | } |
1080 | | |
1081 | | // Dequantize weight once (key optimization: reuse across batch elements) |
1082 | 2 | let weight_f32 = self.dequantize_weight(weight)?0 ; |
1083 | | |
1084 | | // Use HybridScheduler for CPU/GPU dispatch based on workload size |
1085 | 2 | let mut scheduler = HybridScheduler::with_threshold(1000).map_err(|e| {0 |
1086 | 0 | RealizarError::UnsupportedOperation { |
1087 | 0 | operation: "HybridScheduler::with_threshold".to_string(), |
1088 | 0 | reason: format!("GPU scheduler initialization failed: {e}"), |
1089 | 0 | } |
1090 | 0 | })?; |
1091 | | |
1092 | | // Use same matmul approach as batch_matmul_gpu for consistency |
1093 | 2 | scheduler |
1094 | 2 | .matmul(input, &weight_f32, batch_size, in_dim, out_dim) |
1095 | 2 | .map_err(|e| RealizarError::UnsupportedOperation { |
1096 | 0 | operation: "HybridScheduler::matmul".to_string(), |
1097 | 0 | reason: format!("GPU batched matmul failed: {e}"), |
1098 | 0 | }) |
1099 | 2 | } |
1100 | | |
1101 | | /// Batched causal attention with GPU acceleration (IMP-108) |
1102 | | /// |
1103 | | /// Computes causal self-attention using matrix multiplications that can be |
1104 | | /// GPU-accelerated for large sequence lengths. Uses HybridScheduler for |
1105 | | /// automatic CPU/GPU dispatch. |
1106 | | /// |
1107 | | /// Algorithm: |
1108 | | /// 1. For each head: scores = Q @ K^T / sqrt(head_dim) |
1109 | | /// 2. Apply causal mask: scores[i,j] = -inf for j > i |
1110 | | /// 3. Softmax per row |
1111 | | /// 4. Output = softmax(scores) @ V |
1112 | | /// |
1113 | | /// # Arguments |
1114 | | /// * `q` - Query tensor [seq_len, hidden_dim] |
1115 | | /// * `k` - Key tensor [seq_len, hidden_dim] |
1116 | | /// * `v` - Value tensor [seq_len, hidden_dim] |
1117 | | /// * `seq_len` - Sequence length |
1118 | | /// |
1119 | | /// # Returns |
1120 | | /// Attention output [seq_len, hidden_dim] |
1121 | | /// |
1122 | | /// # Errors |
1123 | | /// Returns error if GPU operations fail |
1124 | | #[cfg(feature = "gpu")] |
1125 | 7 | pub fn batched_causal_attention_gpu( |
1126 | 7 | &self, |
1127 | 7 | q: &[f32], |
1128 | 7 | k: &[f32], |
1129 | 7 | v: &[f32], |
1130 | 7 | seq_len: usize, |
1131 | 7 | ) -> Result<Vec<f32>> { |
1132 | | use crate::gpu::HybridScheduler; |
1133 | | |
1134 | 7 | let hidden_dim = self.config.hidden_dim; |
1135 | 7 | let num_heads = self.config.num_heads; |
1136 | 7 | let head_dim = hidden_dim / num_heads; |
1137 | 7 | let scale = 1.0 / (head_dim as f32).sqrt(); |
1138 | | |
1139 | 7 | let mut scheduler = HybridScheduler::with_threshold(1000).map_err(|e| {0 |
1140 | 0 | RealizarError::UnsupportedOperation { |
1141 | 0 | operation: "HybridScheduler::with_threshold".to_string(), |
1142 | 0 | reason: format!("GPU scheduler initialization failed: {e}"), |
1143 | 0 | } |
1144 | 0 | })?; |
1145 | | |
1146 | 7 | let mut output = vec![0.0f32; seq_len * hidden_dim]; |
1147 | | |
1148 | | // Process each head |
1149 | 24 | for head in 0..num_heads7 { |
1150 | 24 | let head_offset = head * head_dim; |
1151 | | |
1152 | | // Extract Q_h, K_h, V_h for this head: [seq_len, head_dim] |
1153 | 24 | let mut q_h = Vec::with_capacity(seq_len * head_dim); |
1154 | 24 | let mut k_h = Vec::with_capacity(seq_len * head_dim); |
1155 | 24 | let mut v_h = Vec::with_capacity(seq_len * head_dim); |
1156 | | |
1157 | 144 | for pos in 0..seq_len24 { |
1158 | 144 | let start = pos * hidden_dim + head_offset; |
1159 | 144 | q_h.extend_from_slice(&q[start..start + head_dim]); |
1160 | 144 | k_h.extend_from_slice(&k[start..start + head_dim]); |
1161 | 144 | v_h.extend_from_slice(&v[start..start + head_dim]); |
1162 | 144 | } |
1163 | | |
1164 | | // Compute attention scores: Q_h @ K_h^T -> [seq_len, seq_len] |
1165 | | // Use GPU for large sequences (seq_len^2 * head_dim ops) |
1166 | 24 | let scores = |
1167 | 24 | self.batched_qk_scores(&q_h, &k_h, seq_len, head_dim, scale, &mut scheduler)?0 ; |
1168 | | |
1169 | | // Apply causal mask and softmax |
1170 | 24 | let attn_weights = self.apply_causal_mask_softmax(&scores, seq_len); |
1171 | | |
1172 | | // Compute output: attn_weights @ V_h -> [seq_len, head_dim] |
1173 | 24 | let head_output = |
1174 | 24 | self.batched_attn_v(&attn_weights, &v_h, seq_len, head_dim, &mut scheduler)?0 ; |
1175 | | |
1176 | | // Copy head output to final output |
1177 | 144 | for pos in 0..seq_len24 { |
1178 | 144 | let out_start = pos * hidden_dim + head_offset; |
1179 | 144 | let head_start = pos * head_dim; |
1180 | 144 | output[out_start..out_start + head_dim] |
1181 | 144 | .copy_from_slice(&head_output[head_start..head_start + head_dim]); |
1182 | 144 | } |
1183 | | } |
1184 | | |
1185 | 7 | Ok(output) |
1186 | 7 | } |
1187 | | |
1188 | | /// Compute Q @ K^T attention scores with GPU acceleration |
1189 | | #[cfg(feature = "gpu")] |
1190 | 24 | fn batched_qk_scores( |
1191 | 24 | &self, |
1192 | 24 | q: &[f32], |
1193 | 24 | k: &[f32], |
1194 | 24 | seq_len: usize, |
1195 | 24 | head_dim: usize, |
1196 | 24 | scale: f32, |
1197 | 24 | scheduler: &mut crate::gpu::HybridScheduler, |
1198 | 24 | ) -> Result<Vec<f32>> { |
1199 | | // Q: [seq_len, head_dim], K: [seq_len, head_dim] |
1200 | | // scores = Q @ K^T -> [seq_len, seq_len] |
1201 | | |
1202 | | // Transpose K: [head_dim, seq_len] |
1203 | 24 | let mut k_t = vec![0.0f32; head_dim * seq_len]; |
1204 | 144 | for i in 0..seq_len24 { |
1205 | 2.04k | for j in 0..head_dim144 { |
1206 | 2.04k | k_t[j * seq_len + i] = k[i * head_dim + j]; |
1207 | 2.04k | } |
1208 | | } |
1209 | | |
1210 | | // Matmul: Q[seq_len, head_dim] @ K_T[head_dim, seq_len] -> [seq_len, seq_len] |
1211 | 24 | let scores = scheduler |
1212 | 24 | .matmul(q, &k_t, seq_len, head_dim, seq_len) |
1213 | 24 | .map_err(|e| RealizarError::UnsupportedOperation { |
1214 | 0 | operation: "batched_qk_scores".to_string(), |
1215 | 0 | reason: format!("GPU matmul failed: {e}"), |
1216 | 0 | })?; |
1217 | | |
1218 | | // Apply scale |
1219 | 960 | let scaled24 : Vec<f32>24 = scores.iter()24 .map24 (|&s| s * scale).collect24 (); |
1220 | 24 | Ok(scaled) |
1221 | 24 | } |
1222 | | |
1223 | | /// Apply causal mask and softmax to attention scores |
1224 | | #[cfg(feature = "gpu")] |
1225 | 64 | pub(crate) fn apply_causal_mask_softmax(&self, scores: &[f32], seq_len: usize) -> Vec<f32> { |
1226 | 64 | let mut weights = vec![0.0f32; seq_len * seq_len]; |
1227 | | |
1228 | 344 | for i in 0..seq_len64 { |
1229 | | // Apply causal mask: set j > i to -inf |
1230 | 344 | let mut max_score = f32::NEG_INFINITY; |
1231 | 1.28k | for j in 0..=i344 { |
1232 | 1.28k | let idx = i * seq_len + j; |
1233 | 1.28k | max_score = max_score.max(scores[idx]); |
1234 | 1.28k | } |
1235 | | |
1236 | | // Compute softmax for causal positions only |
1237 | 344 | let mut exp_sum = 0.0f32; |
1238 | 1.28k | for j in 0..=i344 { |
1239 | 1.28k | let idx = i * seq_len + j; |
1240 | 1.28k | let exp_val = (scores[idx] - max_score).exp(); |
1241 | 1.28k | weights[idx] = exp_val; |
1242 | 1.28k | exp_sum += exp_val; |
1243 | 1.28k | } |
1244 | | |
1245 | | // Normalize |
1246 | 1.28k | for j in 0..=i344 { |
1247 | 1.28k | let idx = i * seq_len + j; |
1248 | 1.28k | weights[idx] /= exp_sum; |
1249 | 1.28k | } |
1250 | | // j > i remains 0 (masked out) |
1251 | | } |
1252 | | |
1253 | 64 | weights |
1254 | 64 | } |
1255 | | |
1256 | | /// Compute attention_weights @ V with GPU acceleration |
1257 | | #[cfg(feature = "gpu")] |
1258 | 24 | fn batched_attn_v( |
1259 | 24 | &self, |
1260 | 24 | attn_weights: &[f32], |
1261 | 24 | v: &[f32], |
1262 | 24 | seq_len: usize, |
1263 | 24 | head_dim: usize, |
1264 | 24 | scheduler: &mut crate::gpu::HybridScheduler, |
1265 | 24 | ) -> Result<Vec<f32>> { |
1266 | | // attn_weights: [seq_len, seq_len], V: [seq_len, head_dim] |
1267 | | // output = attn_weights @ V -> [seq_len, head_dim] |
1268 | 24 | scheduler |
1269 | 24 | .matmul(attn_weights, v, seq_len, seq_len, head_dim) |
1270 | 24 | .map_err(|e| RealizarError::UnsupportedOperation { |
1271 | 0 | operation: "batched_attn_v".to_string(), |
1272 | 0 | reason: format!("GPU matmul failed: {e}"), |
1273 | 0 | }) |
1274 | 24 | } |
1275 | | |
1276 | | // ========================================================================= |
1277 | | // IMP-110: Multi-Head Parallel Attention |
1278 | | // ========================================================================= |
1279 | | |
1280 | | /// Reshape tensor from [seq_len, hidden_dim] to [num_heads, seq_len, head_dim] |
1281 | | /// |
1282 | | /// IMP-110b: Prepares Q/K/V tensors for parallel multi-head processing. |
1283 | | /// Original layout stores all head features contiguously per position. |
1284 | | /// New layout groups by head for batched matmul operations. |
1285 | | /// |
1286 | | /// # Arguments |
1287 | | /// * `input` - Input tensor [seq_len, hidden_dim] |
1288 | | /// * `seq_len` - Sequence length |
1289 | | /// * `num_heads` - Number of attention heads |
1290 | | /// * `head_dim` - Dimension per head (hidden_dim / num_heads) |
1291 | | /// |
1292 | | /// # Returns |
1293 | | /// Reshaped tensor [num_heads, seq_len, head_dim] |
1294 | | #[cfg(feature = "gpu")] |
1295 | 42 | pub fn reshape_for_parallel_heads( |
1296 | 42 | &self, |
1297 | 42 | input: &[f32], |
1298 | 42 | seq_len: usize, |
1299 | 42 | num_heads: usize, |
1300 | 42 | head_dim: usize, |
1301 | 42 | ) -> Result<Vec<f32>> { |
1302 | 42 | let hidden_dim = num_heads * head_dim; |
1303 | 42 | let expected_len = seq_len * hidden_dim; |
1304 | | |
1305 | 42 | if input.len() != expected_len { |
1306 | 0 | return Err(RealizarError::InvalidShape { |
1307 | 0 | reason: format!( |
1308 | 0 | "Input size {} doesn't match seq_len={} * hidden_dim={}={}", |
1309 | 0 | input.len(), |
1310 | 0 | seq_len, |
1311 | 0 | hidden_dim, |
1312 | 0 | expected_len |
1313 | 0 | ), |
1314 | 0 | }); |
1315 | 42 | } |
1316 | | |
1317 | 42 | let mut reshaped = vec![0.0f32; num_heads * seq_len * head_dim]; |
1318 | | |
1319 | | // Transform: input[pos * hidden_dim + h * head_dim + d] |
1320 | | // -> reshaped[h * seq_len * head_dim + pos * head_dim + d] |
1321 | 204 | for h in 0..num_heads42 { |
1322 | 5.90k | for pos in 0..seq_len204 { |
1323 | 91.0k | for d in 0..head_dim5.90k { |
1324 | 91.0k | let orig_idx = pos * hidden_dim + h * head_dim + d; |
1325 | 91.0k | let new_idx = h * seq_len * head_dim + pos * head_dim + d; |
1326 | 91.0k | reshaped[new_idx] = input[orig_idx]; |
1327 | 91.0k | } |
1328 | | } |
1329 | | } |
1330 | | |
1331 | 42 | Ok(reshaped) |
1332 | 42 | } |
1333 | | |
1334 | | /// Compute batched Q@K^T scores for all heads in parallel |
1335 | | /// |
1336 | | /// IMP-110c: Computes attention scores for all heads in a single batch. |
1337 | | /// Takes Q, K in original [seq_len, hidden_dim] layout and computes |
1338 | | /// Q@K^T for each head. |
1339 | | /// |
1340 | | /// # Arguments |
1341 | | /// * `q` - Query tensor [seq_len, hidden_dim] |
1342 | | /// * `k` - Key tensor [seq_len, hidden_dim] |
1343 | | /// * `seq_len` - Sequence length |
1344 | | /// * `num_heads` - Number of attention heads |
1345 | | /// * `head_dim` - Dimension per head |
1346 | | /// * `scale` - Attention scale (1/sqrt(head_dim)) |
1347 | | /// |
1348 | | /// # Returns |
1349 | | /// Batched scores [num_heads, seq_len, seq_len] |
1350 | | #[cfg(feature = "gpu")] |
1351 | 3 | pub fn parallel_batched_qk_scores( |
1352 | 3 | &self, |
1353 | 3 | q: &[f32], |
1354 | 3 | k: &[f32], |
1355 | 3 | seq_len: usize, |
1356 | 3 | num_heads: usize, |
1357 | 3 | head_dim: usize, |
1358 | 3 | scale: f32, |
1359 | 3 | ) -> Result<Vec<f32>> { |
1360 | | use crate::gpu::HybridScheduler; |
1361 | | |
1362 | | // Reshape Q and K to [num_heads, seq_len, head_dim] |
1363 | 3 | let q_reshaped = self.reshape_for_parallel_heads(q, seq_len, num_heads, head_dim)?0 ; |
1364 | 3 | let k_reshaped = self.reshape_for_parallel_heads(k, seq_len, num_heads, head_dim)?0 ; |
1365 | | |
1366 | 3 | let mut scheduler = HybridScheduler::with_threshold(1000).map_err(|e| {0 |
1367 | 0 | RealizarError::UnsupportedOperation { |
1368 | 0 | operation: "HybridScheduler::with_threshold".to_string(), |
1369 | 0 | reason: format!("GPU scheduler initialization failed: {e}"), |
1370 | 0 | } |
1371 | 0 | })?; |
1372 | | |
1373 | | // For each head: Q_h @ K_h^T -> [seq_len, seq_len] |
1374 | | // Total output: [num_heads, seq_len, seq_len] |
1375 | 3 | let mut all_scores = Vec::with_capacity(num_heads * seq_len * seq_len); |
1376 | | |
1377 | 12 | for h in 0..num_heads3 { |
1378 | 12 | let head_start = h * seq_len * head_dim; |
1379 | 12 | let q_h = &q_reshaped[head_start..head_start + seq_len * head_dim]; |
1380 | 12 | let k_h = &k_reshaped[head_start..head_start + seq_len * head_dim]; |
1381 | | |
1382 | | // Transpose K_h: [seq_len, head_dim] -> [head_dim, seq_len] |
1383 | 12 | let mut k_t = vec![0.0f32; head_dim * seq_len]; |
1384 | 80 | for i in 0..seq_len12 { |
1385 | 1.15k | for j in 0..head_dim80 { |
1386 | 1.15k | k_t[j * seq_len + i] = k_h[i * head_dim + j]; |
1387 | 1.15k | } |
1388 | | } |
1389 | | |
1390 | | // Q_h @ K_h^T: [seq_len, head_dim] @ [head_dim, seq_len] -> [seq_len, seq_len] |
1391 | 12 | let scores = scheduler |
1392 | 12 | .matmul(q_h, &k_t, seq_len, head_dim, seq_len) |
1393 | 12 | .map_err(|e| RealizarError::UnsupportedOperation { |
1394 | 0 | operation: "parallel_batched_qk_scores".to_string(), |
1395 | 0 | reason: format!("GPU matmul failed: {e}"), |
1396 | 0 | })?; |
1397 | | |
1398 | | // Apply scale and accumulate |
1399 | 588 | for s576 in &scores { |
1400 | 576 | all_scores.push(s * scale); |
1401 | 576 | } |
1402 | | } |
1403 | | |
1404 | 3 | Ok(all_scores) |
1405 | 3 | } |
1406 | | |
1407 | | /// Multi-head attention with parallel head processing |
1408 | | /// |
1409 | | /// IMP-110a: Processes all attention heads in parallel batches instead |
1410 | | /// of iterating head-by-head. This enables better GPU utilization. |
1411 | | /// |
1412 | | /// # Arguments |
1413 | | /// * `q` - Query tensor [seq_len, hidden_dim] |
1414 | | /// * `k` - Key tensor [seq_len, hidden_dim] |
1415 | | /// * `v` - Value tensor [seq_len, hidden_dim] |
1416 | | /// * `seq_len` - Sequence length |
1417 | | /// |
1418 | | /// # Returns |
1419 | | /// Attention output [seq_len, hidden_dim] |
1420 | | #[cfg(feature = "gpu")] |
1421 | 2 | pub fn parallel_multihead_attention_gpu( |
1422 | 2 | &self, |
1423 | 2 | q: &[f32], |
1424 | 2 | k: &[f32], |
1425 | 2 | v: &[f32], |
1426 | 2 | seq_len: usize, |
1427 | 2 | ) -> Result<Vec<f32>> { |
1428 | | use crate::gpu::HybridScheduler; |
1429 | | |
1430 | 2 | let hidden_dim = self.config.hidden_dim; |
1431 | 2 | let num_heads = self.config.num_heads; |
1432 | 2 | let head_dim = hidden_dim / num_heads; |
1433 | 2 | let scale = 1.0 / (head_dim as f32).sqrt(); |
1434 | | |
1435 | | // Get batched scores for all heads: [num_heads, seq_len, seq_len] |
1436 | 2 | let batched_scores = |
1437 | 2 | self.parallel_batched_qk_scores(q, k, seq_len, num_heads, head_dim, scale)?0 ; |
1438 | | |
1439 | | // Apply causal mask and softmax per head |
1440 | 2 | let mut batched_weights = vec![0.0f32; num_heads * seq_len * seq_len]; |
1441 | 8 | for h in 0..num_heads2 { |
1442 | 8 | let head_offset = h * seq_len * seq_len; |
1443 | 8 | let head_scores = &batched_scores[head_offset..head_offset + seq_len * seq_len]; |
1444 | 8 | let head_weights = self.apply_causal_mask_softmax(head_scores, seq_len); |
1445 | 8 | batched_weights[head_offset..head_offset + seq_len * seq_len] |
1446 | 8 | .copy_from_slice(&head_weights); |
1447 | 8 | } |
1448 | | |
1449 | | // Reshape V to [num_heads, seq_len, head_dim] |
1450 | 2 | let v_reshaped = self.reshape_for_parallel_heads(v, seq_len, num_heads, head_dim)?0 ; |
1451 | | |
1452 | | // Compute attention output for all heads |
1453 | 2 | let mut scheduler = HybridScheduler::with_threshold(1000).map_err(|e| {0 |
1454 | 0 | RealizarError::UnsupportedOperation { |
1455 | 0 | operation: "HybridScheduler::with_threshold".to_string(), |
1456 | 0 | reason: format!("GPU scheduler initialization failed: {e}"), |
1457 | 0 | } |
1458 | 0 | })?; |
1459 | | |
1460 | | // Output: [seq_len, hidden_dim] |
1461 | 2 | let mut output = vec![0.0f32; seq_len * hidden_dim]; |
1462 | | |
1463 | 8 | for h in 0..num_heads2 { |
1464 | 8 | let weights_offset = h * seq_len * seq_len; |
1465 | 8 | let v_offset = h * seq_len * head_dim; |
1466 | | |
1467 | 8 | let head_weights = &batched_weights[weights_offset..weights_offset + seq_len * seq_len]; |
1468 | 8 | let v_h = &v_reshaped[v_offset..v_offset + seq_len * head_dim]; |
1469 | | |
1470 | | // weights @ V_h: [seq_len, seq_len] @ [seq_len, head_dim] -> [seq_len, head_dim] |
1471 | 8 | let head_output = scheduler |
1472 | 8 | .matmul(head_weights, v_h, seq_len, seq_len, head_dim) |
1473 | 8 | .map_err(|e| RealizarError::UnsupportedOperation { |
1474 | 0 | operation: "parallel_multihead_attention_gpu".to_string(), |
1475 | 0 | reason: format!("GPU matmul failed: {e}"), |
1476 | 0 | })?; |
1477 | | |
1478 | | // Copy to output in original layout |
1479 | 64 | for pos in 0..seq_len8 { |
1480 | 64 | let out_start = pos * hidden_dim + h * head_dim; |
1481 | 64 | let head_start = pos * head_dim; |
1482 | 64 | output[out_start..out_start + head_dim] |
1483 | 64 | .copy_from_slice(&head_output[head_start..head_start + head_dim]); |
1484 | 64 | } |
1485 | | } |
1486 | | |
1487 | 2 | Ok(output) |
1488 | 2 | } |
1489 | | |
1490 | | // ========================================================================= |
1491 | | // IMP-111: Flash Attention-style Tiled Computation |
1492 | | // ========================================================================= |
1493 | | |
1494 | | /// Standard softmax (reference implementation) |
1495 | | /// |
1496 | | /// IMP-111a: Reference implementation for testing online softmax. |
1497 | | /// Computes softmax in the standard way: exp(x - max) / sum(exp(x - max)) |
1498 | 9 | pub fn standard_softmax(&self, scores: &[f32]) -> Vec<f32> { |
1499 | 9 | if scores.is_empty() { |
1500 | 0 | return Vec::new(); |
1501 | 9 | } |
1502 | | |
1503 | | // Find max for numerical stability |
1504 | 9 | let max_score = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max); |
1505 | | |
1506 | | // Compute exp(x - max) and sum |
1507 | 80 | let exp_scores9 : Vec<f32>9 = scores9 .iter9 ().map9 (|&s| (s - max_score).exp()).collect9 (); |
1508 | 9 | let sum: f32 = exp_scores.iter().sum(); |
1509 | | |
1510 | | // Normalize |
1511 | 80 | exp_scores.iter()9 .map9 (|&e| e / sum).collect9 () |
1512 | 9 | } |
1513 | | |
1514 | | /// Online softmax with tiled processing (O(1) memory per tile) |
1515 | | /// |
1516 | | /// IMP-111a: Implements the "online softmax" algorithm that processes |
1517 | | /// data in tiles without materializing the full softmax denominator. |
1518 | | /// |
1519 | | /// Algorithm: |
1520 | | /// 1. Process tiles, tracking running max (m) and denominator (d) |
1521 | | /// 2. When new tile has larger max, rescale previous denominator |
1522 | | /// 3. Final pass normalizes all values |
1523 | | /// |
1524 | | /// # Arguments |
1525 | | /// * `scores` - Input scores to apply softmax |
1526 | | /// * `tile_size` - Size of each tile for processing |
1527 | | /// |
1528 | | /// # Returns |
1529 | | /// Softmax probabilities |
1530 | 1 | pub fn online_softmax(&self, scores: &[f32], tile_size: usize) -> Result<Vec<f32>> { |
1531 | 1 | if scores.is_empty() { |
1532 | 0 | return Ok(Vec::new()); |
1533 | 1 | } |
1534 | | |
1535 | 1 | let n = scores.len(); |
1536 | 1 | let tile_size = tile_size.max(1); |
1537 | | |
1538 | | // Running statistics |
1539 | 1 | let mut global_max = f32::NEG_INFINITY; |
1540 | 1 | let mut global_sum = 0.0f32; |
1541 | | |
1542 | | // First pass: compute global max and sum using online algorithm |
1543 | 4 | for tile_start in (0..n)1 .step_by1 (tile_size1 ) { |
1544 | 4 | let tile_end = (tile_start + tile_size).min(n); |
1545 | | |
1546 | | // Find local max in this tile |
1547 | 4 | let local_max = scores[tile_start..tile_end] |
1548 | 4 | .iter() |
1549 | 4 | .cloned() |
1550 | 4 | .fold(f32::NEG_INFINITY, f32::max); |
1551 | | |
1552 | 4 | if local_max > global_max { |
1553 | 2 | // Rescale previous sum when we find a new max |
1554 | 2 | let rescale = (global_max - local_max).exp(); |
1555 | 2 | global_sum *= rescale; |
1556 | 2 | global_max = local_max; |
1557 | 2 | } |
1558 | | |
1559 | | // Add this tile's contribution to sum |
1560 | 16 | for &s in &scores4 [tile_start..tile_end]4 { |
1561 | 16 | global_sum += (s - global_max).exp(); |
1562 | 16 | } |
1563 | | } |
1564 | | |
1565 | | // Second pass: compute final softmax values |
1566 | 1 | let mut result = Vec::with_capacity(n); |
1567 | 17 | for &s16 in scores { |
1568 | 16 | result.push((s - global_max).exp() / global_sum); |
1569 | 16 | } |
1570 | | |
1571 | 1 | Ok(result) |
1572 | 1 | } |
1573 | | |
1574 | | /// Standard single-head attention (reference implementation) |
1575 | | /// |
1576 | | /// IMP-111b: Reference implementation that materializes full attention matrix. |
1577 | | /// Used to verify tiled attention correctness. |
1578 | | /// |
1579 | | /// # Arguments |
1580 | | /// * `q` - Query tensor [seq_len, head_dim] |
1581 | | /// * `k` - Key tensor [seq_len, head_dim] |
1582 | | /// * `v` - Value tensor [seq_len, head_dim] |
1583 | | /// * `seq_len` - Sequence length |
1584 | | /// * `head_dim` - Dimension per head |
1585 | | /// * `scale` - Attention scale (1/sqrt(head_dim)) |
1586 | 1 | pub fn standard_single_head_attention( |
1587 | 1 | &self, |
1588 | 1 | q: &[f32], |
1589 | 1 | k: &[f32], |
1590 | 1 | v: &[f32], |
1591 | 1 | seq_len: usize, |
1592 | 1 | head_dim: usize, |
1593 | 1 | scale: f32, |
1594 | 1 | ) -> Result<Vec<f32>> { |
1595 | | // Compute attention scores: Q @ K^T -> [seq_len, seq_len] |
1596 | 1 | let mut scores = vec![0.0f32; seq_len * seq_len]; |
1597 | 8 | for i in 0..seq_len1 { |
1598 | 64 | for j in 0..seq_len8 { |
1599 | 64 | let mut dot = 0.0f32; |
1600 | 512 | for d in 0..head_dim64 { |
1601 | 512 | dot += q[i * head_dim + d] * k[j * head_dim + d]; |
1602 | 512 | } |
1603 | 64 | scores[i * seq_len + j] = dot * scale; |
1604 | | } |
1605 | | } |
1606 | | |
1607 | | // Apply softmax per row |
1608 | 1 | let mut weights = vec![0.0f32; seq_len * seq_len]; |
1609 | 8 | for i in 0..seq_len1 { |
1610 | 8 | let row_start = i * seq_len; |
1611 | 8 | let row = &scores[row_start..row_start + seq_len]; |
1612 | 8 | let softmax = self.standard_softmax(row); |
1613 | 8 | weights[row_start..row_start + seq_len].copy_from_slice(&softmax); |
1614 | 8 | } |
1615 | | |
1616 | | // Compute output: weights @ V -> [seq_len, head_dim] |
1617 | 1 | let mut output = vec![0.0f32; seq_len * head_dim]; |
1618 | 8 | for i in 0..seq_len1 { |
1619 | 64 | for d in 0..head_dim8 { |
1620 | 64 | let mut acc = 0.0f32; |
1621 | 512 | for j in 0..seq_len64 { |
1622 | 512 | acc += weights[i * seq_len + j] * v[j * head_dim + d]; |
1623 | 512 | } |
1624 | 64 | output[i * head_dim + d] = acc; |
1625 | | } |
1626 | | } |
1627 | | |
1628 | 1 | Ok(output) |
1629 | 1 | } |
1630 | | |
1631 | | /// Tiled single-head attention (non-causal) |
1632 | | /// |
1633 | | /// IMP-111b: Flash Attention-style tiled computation. |
1634 | | /// Processes K/V in tiles, maintaining running softmax statistics. |
1635 | | #[allow(clippy::too_many_arguments)] |
1636 | 1 | pub fn tiled_single_head_attention( |
1637 | 1 | &self, |
1638 | 1 | q: &[f32], |
1639 | 1 | k: &[f32], |
1640 | 1 | v: &[f32], |
1641 | 1 | seq_len: usize, |
1642 | 1 | head_dim: usize, |
1643 | 1 | scale: f32, |
1644 | 1 | tile_size: usize, |
1645 | 1 | ) -> Result<Vec<f32>> { |
1646 | 1 | let tile_size = tile_size.max(1); |
1647 | 1 | let mut output = vec![0.0f32; seq_len * head_dim]; |
1648 | | |
1649 | | // Process each query position |
1650 | 8 | for i in 0..seq_len1 { |
1651 | 8 | let q_i = &q[i * head_dim..(i + 1) * head_dim]; |
1652 | | |
1653 | | // Running statistics for online softmax |
1654 | 8 | let mut running_max = f32::NEG_INFINITY; |
1655 | 8 | let mut running_sum = 0.0f32; |
1656 | 8 | let mut running_output = vec![0.0f32; head_dim]; |
1657 | | |
1658 | | // Process K/V in tiles |
1659 | 16 | for tile_start in (0..seq_len)8 .step_by8 (tile_size8 ) { |
1660 | 16 | let tile_end = (tile_start + tile_size).min(seq_len); |
1661 | | |
1662 | | // Compute scores for this tile: q_i @ K_tile^T |
1663 | 16 | let mut tile_scores = Vec::with_capacity(tile_end - tile_start); |
1664 | 64 | for j in tile_start16 ..tile_end16 { |
1665 | 64 | let mut dot = 0.0f32; |
1666 | 512 | for d in 0..head_dim64 { |
1667 | 512 | dot += q_i[d] * k[j * head_dim + d]; |
1668 | 512 | } |
1669 | 64 | tile_scores.push(dot * scale); |
1670 | | } |
1671 | | |
1672 | | // Find tile max |
1673 | 16 | let tile_max = tile_scores |
1674 | 16 | .iter() |
1675 | 16 | .cloned() |
1676 | 16 | .fold(f32::NEG_INFINITY, f32::max); |
1677 | | |
1678 | | // Update running statistics |
1679 | 16 | let new_max = running_max.max(tile_max); |
1680 | | |
1681 | | // Rescale previous output and sum |
1682 | 16 | if new_max > running_max && running_sum > 0.011 { |
1683 | 3 | let rescale = (running_max - new_max).exp(); |
1684 | 3 | running_sum *= rescale; |
1685 | 27 | for out_val24 in &mut running_output { |
1686 | 24 | *out_val *= rescale; |
1687 | 24 | } |
1688 | 13 | } |
1689 | 16 | running_max = new_max; |
1690 | | |
1691 | | // Accumulate this tile's contribution |
1692 | 64 | for (idx, &score) in tile_scores.iter()16 .enumerate16 () { |
1693 | 64 | let j = tile_start + idx; |
1694 | 64 | let weight = (score - running_max).exp(); |
1695 | 64 | running_sum += weight; |
1696 | 512 | for d in 0..head_dim64 { |
1697 | 512 | running_output[d] += weight * v[j * head_dim + d]; |
1698 | 512 | } |
1699 | | } |
1700 | | } |
1701 | | |
1702 | | // Normalize output |
1703 | 64 | for d in 0..head_dim8 { |
1704 | 64 | output[i * head_dim + d] = running_output[d] / running_sum; |
1705 | 64 | } |
1706 | | } |
1707 | | |
1708 | 1 | Ok(output) |
1709 | 1 | } |
1710 | | |
1711 | | /// Tiled causal attention |
1712 | | /// |
1713 | | /// IMP-111c: Flash Attention with causal masking. |
1714 | | /// For position i, only attends to positions 0..=i. |
1715 | | #[allow(clippy::too_many_arguments)] |
1716 | 29 | pub fn tiled_causal_attention( |
1717 | 29 | &self, |
1718 | 29 | q: &[f32], |
1719 | 29 | k: &[f32], |
1720 | 29 | v: &[f32], |
1721 | 29 | seq_len: usize, |
1722 | 29 | head_dim: usize, |
1723 | 29 | scale: f32, |
1724 | 29 | tile_size: usize, |
1725 | 29 | ) -> Result<Vec<f32>> { |
1726 | 29 | let tile_size = tile_size.max(1); |
1727 | 29 | let mut output = vec![0.0f32; seq_len * head_dim]; |
1728 | | |
1729 | | // Process each query position |
1730 | 540 | for i in 0..seq_len29 { |
1731 | 540 | let q_i = &q[i * head_dim..(i + 1) * head_dim]; |
1732 | | |
1733 | | // Running statistics for online softmax |
1734 | 540 | let mut running_max = f32::NEG_INFINITY; |
1735 | 540 | let mut running_sum = 0.0f32; |
1736 | 540 | let mut running_output = vec![0.0f32; head_dim]; |
1737 | | |
1738 | | // Only process K/V up to position i (causal) |
1739 | 540 | let causal_len = i + 1; |
1740 | | |
1741 | | // Process K/V in tiles |
1742 | 2.00k | for tile_start in (0..causal_len)540 .step_by540 (tile_size540 ) { |
1743 | 2.00k | let tile_end = (tile_start + tile_size).min(causal_len); |
1744 | | |
1745 | | // Compute scores for this tile: q_i @ K_tile^T |
1746 | 2.00k | let mut tile_scores = Vec::with_capacity(tile_end - tile_start); |
1747 | 6.83k | for j in tile_start2.00k ..tile_end2.00k { |
1748 | 6.83k | let mut dot = 0.0f32; |
1749 | 103k | for d in 0..head_dim6.83k { |
1750 | 103k | dot += q_i[d] * k[j * head_dim + d]; |
1751 | 103k | } |
1752 | 6.83k | tile_scores.push(dot * scale); |
1753 | | } |
1754 | | |
1755 | | // Find tile max |
1756 | 2.00k | let tile_max = tile_scores |
1757 | 2.00k | .iter() |
1758 | 2.00k | .cloned() |
1759 | 2.00k | .fold(f32::NEG_INFINITY, f32::max); |
1760 | | |
1761 | | // Update running statistics |
1762 | 2.00k | let new_max = running_max.max(tile_max); |
1763 | | |
1764 | | // Rescale previous output and sum |
1765 | 2.00k | if new_max > running_max && running_sum > 0.0920 { |
1766 | 380 | let rescale = (running_max - new_max).exp(); |
1767 | 380 | running_sum *= rescale; |
1768 | 6.11k | for out_val5.73k in &mut running_output { |
1769 | 5.73k | *out_val *= rescale; |
1770 | 5.73k | } |
1771 | 1.62k | } |
1772 | 2.00k | running_max = new_max; |
1773 | | |
1774 | | // Accumulate this tile's contribution |
1775 | 6.83k | for (idx, &score) in tile_scores.iter()2.00k .enumerate2.00k () { |
1776 | 6.83k | let j = tile_start + idx; |
1777 | 6.83k | let weight = (score - running_max).exp(); |
1778 | 6.83k | running_sum += weight; |
1779 | 103k | for d in 0..head_dim6.83k { |
1780 | 103k | running_output[d] += weight * v[j * head_dim + d]; |
1781 | 103k | } |
1782 | | } |
1783 | | } |
1784 | | |
1785 | | // Normalize output |
1786 | 540 | if running_sum > 0.0 { |
1787 | 7.84k | for d in 0..head_dim540 { |
1788 | 7.84k | output[i * head_dim + d] = running_output[d] / running_sum; |
1789 | 7.84k | } |
1790 | 0 | } |
1791 | | } |
1792 | | |
1793 | 29 | Ok(output) |
1794 | 29 | } |
1795 | | } |