Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/realizar/src/gpu/scheduler/model.rs
Line
Count
Source
1
//! GPU Model and Configuration (PMAT-802)
2
//!
3
//! GpuModel, GpuModelConfig, GpuGenerateConfig, AttentionBuffers
4
5
use crate::error::{RealizarError, Result};
6
use super::super::{
7
    HybridScheduler, StreamingKVCache, exceeds_gpu_buffer_limit,
8
    cpu_matmul_transposed_simd, cpu_matmul,
9
};
10
#[cfg(feature = "cuda")]
11
use super::core::CudaScheduler;
12
13
/// GPU-accelerated model for M3 parity (128 tok/s target)
14
///
15
/// Wraps standard Model and uses HybridScheduler for GPU-accelerated
16
/// matrix multiplications in the forward pass.
17
pub struct GpuModel {
18
    /// Embedding weights (vocab_size x hidden_dim)
19
    pub(crate) embedding_weights: Vec<f32>,
20
    /// Linear layer weights for each block
21
    /// Each block has: attn_q, attn_k, attn_v, attn_out, ffn_fc1, ffn_fc2
22
    pub(crate) block_weights: Vec<BlockWeights>,
23
    /// Final layer norm weights
24
    pub(crate) final_norm_weight: Vec<f32>,
25
    pub(crate) final_norm_bias: Vec<f32>,
26
    /// LM head weights (hidden_dim x vocab_size)
27
    pub(crate) lm_head_weight: Vec<f32>,
28
    /// LM head weights transposed (vocab_size x hidden_dim) for fast CPU inference
29
    pub(crate) lm_head_weight_t: Vec<f32>,
30
    pub(crate) lm_head_bias: Vec<f32>,
31
    /// GPU scheduler (HybridScheduler - may force CPU for m=1)
32
    pub(crate) scheduler: HybridScheduler,
33
    /// IMP-1003: Optional CUDA-only scheduler that ALWAYS uses GPU
34
    /// When present, this scheduler is preferred over HybridScheduler for matmul
35
    #[cfg(feature = "cuda")]
36
    pub(crate) cuda_scheduler: Option<CudaScheduler>,
37
    /// Model configuration
38
    pub config: GpuModelConfig,
39
    /// Pre-allocated attention buffers for optimized incremental decoding (M17)
40
    pub(crate) attention_buffers: Option<AttentionBuffers>,
41
}
42
43
/// Weights for a single transformer block
44
///
45
/// Used by adapters (PMAT-106) to construct GpuModel from various formats.
46
pub struct BlockWeights {
47
    /// Attention layer norm weight
48
    pub attn_norm_weight: Vec<f32>,
49
    /// Attention layer norm bias
50
    pub attn_norm_bias: Vec<f32>,
51
    /// Combined QKV projection weights (hidden_dim x 3*hidden_dim)
52
    pub qkv_weight: Vec<f32>,
53
    /// QKV projection bias (reserved for future use)
54
    #[allow(dead_code)]
55
    pub qkv_bias: Vec<f32>,
56
    /// Output projection weight (hidden_dim x hidden_dim)
57
    pub out_weight: Vec<f32>,
58
    /// Output projection bias
59
    pub out_bias: Vec<f32>,
60
    /// FFN layer norm weight
61
    pub ffn_norm_weight: Vec<f32>,
62
    /// FFN layer norm bias
63
    pub ffn_norm_bias: Vec<f32>,
64
    /// FFN first layer weight (up projection)
65
    pub ffn_fc1_weight: Vec<f32>,
66
    /// FFN first layer bias
67
    pub ffn_fc1_bias: Vec<f32>,
68
    /// FFN second layer weight (down projection)
69
    pub ffn_fc2_weight: Vec<f32>,
70
    /// FFN second layer bias
71
    pub ffn_fc2_bias: Vec<f32>,
72
    /// FFN gate projection weight for SwiGLU (optional)
73
    /// When present, FFN uses SwiGLU: down(SiLU(gate(x)) * up(x))
74
    /// When None, FFN uses GELU: down(GELU(up(x)))
75
    pub ffn_gate_weight: Option<Vec<f32>>,
76
}
77
78
/// IMP-1007: Weight type for split-borrow matmul
79
///
80
/// This enum specifies which weight matrix to use in matmul_split,
81
/// enabling zero-clone matmul operations by using Rust's split borrow pattern.
82
#[derive(Debug, Clone, Copy)]
83
pub enum WeightType {
84
    /// QKV projection: [hidden_dim, qkv_dim]
85
    Qkv,
86
    /// Output projection: [hidden_dim, hidden_dim]
87
    Output,
88
    /// FFN FC1: [hidden_dim, intermediate_dim]
89
    FfnFc1,
90
    /// FFN FC2: [intermediate_dim, hidden_dim]
91
    FfnFc2,
92
    /// LM head: [hidden_dim, vocab_size]
93
    LmHead,
94
}
95
96
/// Configuration for GPU model
97
#[derive(Debug, Clone)]
98
pub struct GpuModelConfig {
99
    /// Vocabulary size
100
    pub vocab_size: usize,
101
    /// Hidden dimension
102
    pub hidden_dim: usize,
103
    /// Number of attention heads (Q heads)
104
    pub num_heads: usize,
105
    /// Number of key-value heads for GQA (IMP-088)
106
    /// For standard MHA: num_kv_heads == num_heads
107
    /// For GQA (Qwen, Llama-3): num_kv_heads < num_heads
108
    pub num_kv_heads: usize,
109
    /// Number of transformer blocks
110
    pub num_layers: usize,
111
    /// FFN intermediate dimension
112
    pub intermediate_dim: usize,
113
    /// Layer normalization epsilon
114
    pub eps: f32,
115
    /// RoPE theta for rotary position embeddings (Phase 21)
116
    /// Default: 10000.0 (standard LLaMA)
117
    pub rope_theta: f32,
118
}
119
120
impl GpuModelConfig {
121
    /// Head dimension (hidden_dim / num_heads)
122
    #[inline]
123
5.36k
    pub fn head_dim(&self) -> usize {
124
5.36k
        self.hidden_dim / self.num_heads
125
5.36k
    }
126
127
    /// K/V dimension for GQA (num_kv_heads * head_dim)
128
    #[inline]
129
3.58k
    pub fn kv_dim(&self) -> usize {
130
3.58k
        self.num_kv_heads * self.head_dim()
131
3.58k
    }
132
133
    /// Total QKV projection output dimension
134
    /// For MHA: 3 * hidden_dim
135
    /// For GQA: hidden_dim + 2 * kv_dim
136
    #[inline]
137
1.79k
    pub fn qkv_dim(&self) -> usize {
138
1.79k
        self.hidden_dim + 2 * self.kv_dim()
139
1.79k
    }
140
141
    /// Whether this is a GQA model (num_kv_heads < num_heads)
142
    #[inline]
143
2
    pub fn is_gqa(&self) -> bool {
144
2
        self.num_kv_heads < self.num_heads
145
2
    }
146
}
147
148
/// Configuration for GPU text generation (M14: E2E Inference)
149
#[derive(Debug, Clone)]
150
pub struct GpuGenerateConfig {
151
    /// Maximum tokens to generate
152
    pub max_tokens: usize,
153
    /// Sampling temperature (0.0 = greedy)
154
    pub temperature: f32,
155
    /// Top-k sampling (1 = greedy)
156
    pub top_k: usize,
157
    /// Stop token IDs
158
    pub stop_tokens: Vec<usize>,
159
}
160
161
impl Default for GpuGenerateConfig {
162
2
    fn default() -> Self {
163
2
        Self {
164
2
            max_tokens: 64,
165
2
            temperature: 0.0,
166
2
            top_k: 1,
167
2
            stop_tokens: Vec::new(),
168
2
        }
169
2
    }
170
}
171
172
impl GpuGenerateConfig {
173
    /// Create config for deterministic (greedy) generation
174
    #[must_use]
175
11
    pub fn deterministic(max_tokens: usize) -> Self {
176
11
        Self {
177
11
            max_tokens,
178
11
            temperature: 0.0,
179
11
            top_k: 1,
180
11
            stop_tokens: Vec::new(),
181
11
        }
182
11
    }
183
184
    /// Create config with temperature and top-k sampling
185
    #[must_use]
186
3
    pub fn with_sampling(max_tokens: usize, temperature: f32, top_k: usize) -> Self {
187
3
        Self {
188
3
            max_tokens,
189
3
            temperature,
190
3
            top_k,
191
3
            stop_tokens: Vec::new(),
192
3
        }
193
3
    }
194
195
    /// Add stop tokens to config
196
    #[must_use]
197
4
    pub fn with_stop_tokens(mut self, stop_tokens: Vec<usize>) -> Self {
198
4
        self.stop_tokens = stop_tokens;
199
4
        self
200
4
    }
201
}
202
203
/// Pre-allocated attention buffers for optimized incremental decoding (M17)
204
///
205
/// Eliminates per-token memory allocation during incremental generation by
206
/// reusing pre-allocated buffers for Q, attention scores, and output.
207
#[derive(Debug)]
208
pub struct AttentionBuffers {
209
    /// Q buffer for single-token attention [hidden_dim]
210
    pub q_buffer: Vec<f32>,
211
    /// Attention scores buffer [num_heads * max_seq_len]
212
    pub scores_buffer: Vec<f32>,
213
    /// Attention output buffer [hidden_dim]
214
    pub output_buffer: Vec<f32>,
215
    /// K/V projection buffer [hidden_dim]
216
    pub kv_proj_buffer: Vec<f32>,
217
    /// Intermediate FFN buffer [intermediate_dim]
218
    pub ffn_buffer: Vec<f32>,
219
    /// Max sequence length these buffers support
220
    pub max_seq_len: usize,
221
}
222
223
impl AttentionBuffers {
224
    /// Create pre-allocated attention buffers from model config
225
    ///
226
    /// # Arguments
227
    ///
228
    /// * `config` - Model configuration
229
    /// * `max_seq_len` - Maximum sequence length to support
230
    #[must_use]
231
8
    pub fn new(config: &GpuModelConfig, max_seq_len: usize) -> Self {
232
8
        Self {
233
8
            q_buffer: vec![0.0; config.hidden_dim],
234
8
            scores_buffer: vec![0.0; config.num_heads * max_seq_len],
235
8
            output_buffer: vec![0.0; config.hidden_dim],
236
8
            kv_proj_buffer: vec![0.0; config.hidden_dim],
237
8
            ffn_buffer: vec![0.0; config.intermediate_dim],
238
8
            max_seq_len,
239
8
        }
240
8
    }
241
242
    /// Reset all buffers to zero (for reuse)
243
1
    pub fn reset(&mut self) {
244
1
        self.q_buffer.fill(0.0);
245
1
        self.scores_buffer.fill(0.0);
246
1
        self.output_buffer.fill(0.0);
247
1
        self.kv_proj_buffer.fill(0.0);
248
1
        self.ffn_buffer.fill(0.0);
249
1
    }
250
}
251
252
impl GpuModel {
253
    /// Create a new GPU-accelerated model with random initialization
254
    ///
255
    /// # Errors
256
    ///
257
    /// Returns error if GPU initialization fails
258
19
    pub fn new(config: GpuModelConfig) -> Result<Self> {
259
19
        let scheduler = HybridScheduler::new()
?0
;
260
261
        // Initialize weights (small random values for testing)
262
19
        let embedding_weights = vec![0.01f32; config.vocab_size * config.hidden_dim];
263
264
19
        let mut block_weights = Vec::with_capacity(config.num_layers);
265
46
        for _ in 0..
config.num_layers19
{
266
46
            block_weights.push(BlockWeights {
267
46
                attn_norm_weight: vec![1.0f32; config.hidden_dim],
268
46
                attn_norm_bias: vec![0.0f32; config.hidden_dim],
269
46
                qkv_weight: vec![0.01f32; config.hidden_dim * 3 * config.hidden_dim],
270
46
                qkv_bias: vec![0.0f32; 3 * config.hidden_dim],
271
46
                out_weight: vec![0.01f32; config.hidden_dim * config.hidden_dim],
272
46
                out_bias: vec![0.0f32; config.hidden_dim],
273
46
                ffn_norm_weight: vec![1.0f32; config.hidden_dim],
274
46
                ffn_norm_bias: vec![0.0f32; config.hidden_dim],
275
46
                ffn_fc1_weight: vec![0.01f32; config.hidden_dim * config.intermediate_dim],
276
46
                ffn_fc1_bias: vec![0.0f32; config.intermediate_dim],
277
46
                ffn_fc2_weight: vec![0.01f32; config.intermediate_dim * config.hidden_dim],
278
46
                ffn_fc2_bias: vec![0.0f32; config.hidden_dim],
279
46
                ffn_gate_weight: None, // No SwiGLU in test models
280
46
            });
281
46
        }
282
283
19
        let final_norm_weight = vec![1.0f32; config.hidden_dim];
284
19
        let final_norm_bias = vec![0.0f32; config.hidden_dim];
285
19
        let lm_head_weight = vec![0.01f32; config.hidden_dim * config.vocab_size];
286
19
        let lm_head_bias = vec![0.0f32; config.vocab_size];
287
288
        // Pre-compute transposed LM head for fast CPU inference
289
        // Original: [hidden_dim, vocab_size] -> Transposed: [vocab_size, hidden_dim]
290
19
        let lm_head_weight_t =
291
19
            Self::transpose_weights(&lm_head_weight, config.hidden_dim, config.vocab_size);
292
293
19
        Ok(Self {
294
19
            embedding_weights,
295
19
            block_weights,
296
19
            final_norm_weight,
297
19
            final_norm_bias,
298
19
            lm_head_weight,
299
19
            lm_head_weight_t,
300
19
            lm_head_bias,
301
19
            scheduler,
302
19
            #[cfg(feature = "cuda")]
303
19
            cuda_scheduler: None,
304
19
            config,
305
19
            attention_buffers: None,
306
19
        })
307
19
    }
308
309
    /// IMP-1003: Create GPU model with CUDA-only scheduler
310
    ///
311
    /// Unlike `new()`, this constructor creates a model that ALWAYS uses CUDA
312
    /// for matmul operations, even for m=1 (single-token generation).
313
    ///
314
    /// # Errors
315
    ///
316
    /// Returns error if GPU or CUDA initialization fails
317
    #[cfg(feature = "cuda")]
318
    pub fn new_with_cuda(config: GpuModelConfig) -> Result<Self> {
319
        let scheduler = HybridScheduler::new()?;
320
        let cuda_scheduler = Some(CudaScheduler::new()?);
321
322
        // Initialize weights (small random values for testing)
323
        let embedding_weights = vec![0.01f32; config.vocab_size * config.hidden_dim];
324
325
        let mut block_weights = Vec::with_capacity(config.num_layers);
326
        for _ in 0..config.num_layers {
327
            block_weights.push(BlockWeights {
328
                attn_norm_weight: vec![1.0f32; config.hidden_dim],
329
                attn_norm_bias: vec![0.0f32; config.hidden_dim],
330
                qkv_weight: vec![0.01f32; config.hidden_dim * config.qkv_dim()],
331
                qkv_bias: vec![0.0f32; config.qkv_dim()],
332
                out_weight: vec![0.01f32; config.hidden_dim * config.hidden_dim],
333
                out_bias: vec![0.0f32; config.hidden_dim],
334
                ffn_norm_weight: vec![1.0f32; config.hidden_dim],
335
                ffn_norm_bias: vec![0.0f32; config.hidden_dim],
336
                ffn_fc1_weight: vec![0.01f32; config.hidden_dim * config.intermediate_dim],
337
                ffn_fc1_bias: vec![0.0f32; config.intermediate_dim],
338
                ffn_fc2_weight: vec![0.01f32; config.intermediate_dim * config.hidden_dim],
339
                ffn_fc2_bias: vec![0.0f32; config.hidden_dim],
340
                ffn_gate_weight: None, // No SwiGLU in test models
341
            });
342
        }
343
344
        let final_norm_weight = vec![1.0f32; config.hidden_dim];
345
        let final_norm_bias = vec![0.0f32; config.hidden_dim];
346
        let lm_head_weight = vec![0.01f32; config.hidden_dim * config.vocab_size];
347
        let lm_head_bias = vec![0.0f32; config.vocab_size];
348
349
        let lm_head_weight_t =
350
            Self::transpose_weights(&lm_head_weight, config.hidden_dim, config.vocab_size);
351
352
        Ok(Self {
353
            embedding_weights,
354
            block_weights,
355
            final_norm_weight,
356
            final_norm_bias,
357
            lm_head_weight,
358
            lm_head_weight_t,
359
            lm_head_bias,
360
            scheduler,
361
            cuda_scheduler,
362
            config,
363
            attention_buffers: None,
364
        })
365
    }
366
367
    /// IMP-1003: Check if this model has CUDA scheduler enabled
368
    #[cfg(feature = "cuda")]
369
    #[must_use]
370
    pub fn has_cuda_scheduler(&self) -> bool {
371
        self.cuda_scheduler.is_some()
372
    }
373
374
    /// IMP-1003: Perform matmul using CUDA scheduler (always GPU, even for m=1)
375
    ///
376
    /// # Errors
377
    ///
378
    /// Returns error if CUDA scheduler is not available or matmul fails
379
    #[cfg(feature = "cuda")]
380
    #[allow(clippy::many_single_char_names)]
381
    pub fn cuda_matmul(
382
        &mut self,
383
        a: &[f32],
384
        b: &[f32],
385
        m: usize,
386
        k: usize,
387
        n: usize,
388
    ) -> Result<Vec<f32>> {
389
        if let Some(ref mut cuda_sched) = self.cuda_scheduler {
390
            cuda_sched.matmul(a, b, m, k, n)
391
        } else {
392
            // Fallback to HybridScheduler
393
            self.scheduler.matmul(a, b, m, k, n)
394
        }
395
    }
396
397
    /// IMP-1005: Unified matmul dispatch that prefers CudaScheduler when available
398
    ///
399
    /// This method is used throughout forward_gpu() and forward_block_idx() to
400
    /// ensure CUDA is used for all matmul operations when cuda_scheduler is present.
401
    ///
402
    /// # Errors
403
    ///
404
    /// Returns error if matmul fails
405
    #[allow(clippy::many_single_char_names)]
406
6.76k
    pub fn do_matmul(
407
6.76k
        &mut self,
408
6.76k
        a: &[f32],
409
6.76k
        b: &[f32],
410
6.76k
        m: usize,
411
6.76k
        k: usize,
412
6.76k
        n: usize,
413
6.76k
    ) -> Result<Vec<f32>> {
414
        #[cfg(feature = "cuda")]
415
        if let Some(ref mut cuda_sched) = self.cuda_scheduler {
416
            return cuda_sched.matmul(a, b, m, k, n);
417
        }
418
        // Fallback to HybridScheduler (or always use it when cuda feature disabled)
419
6.76k
        self.scheduler.matmul(a, b, m, k, n)
420
6.76k
    }
421
422
    /// IMP-1007: Zero-clone matmul using split borrow pattern
423
    ///
424
    /// This method eliminates weight cloning by using Rust's split borrow pattern.
425
    /// It directly borrows weights from block_weights while mutably borrowing schedulers.
426
    ///
427
    /// # Arguments
428
    ///
429
    /// * `input` - Input tensor
430
    /// * `block_idx` - Block index for block weights (ignored for LmHead)
431
    /// * `op` - Which matmul operation/weight to use
432
    ///
433
    /// # Errors
434
    ///
435
    /// Returns error if matmul fails
436
0
    pub fn matmul_split(
437
0
        &mut self,
438
0
        input: &[f32],
439
0
        block_idx: usize,
440
0
        op: WeightType,
441
0
    ) -> Result<Vec<f32>> {
442
        // IMP-1007: Use split borrowing to avoid weight cloning
443
        // Extract dimensions from config (Copy types, no borrow conflict)
444
0
        let hidden_dim = self.config.hidden_dim;
445
0
        let qkv_dim = self.config.qkv_dim();
446
0
        let intermediate_dim = self.config.intermediate_dim;
447
0
        let vocab_size = self.config.vocab_size;
448
449
        // Get weight reference and dimensions based on operation
450
0
        let (weight, m, k, n) = match op {
451
0
            WeightType::Qkv => (
452
0
                &self.block_weights[block_idx].qkv_weight,
453
0
                1,
454
0
                hidden_dim,
455
0
                qkv_dim,
456
0
            ),
457
0
            WeightType::Output => (
458
0
                &self.block_weights[block_idx].out_weight,
459
0
                1,
460
0
                hidden_dim,
461
0
                hidden_dim,
462
0
            ),
463
0
            WeightType::FfnFc1 => (
464
0
                &self.block_weights[block_idx].ffn_fc1_weight,
465
0
                1,
466
0
                hidden_dim,
467
0
                intermediate_dim,
468
0
            ),
469
0
            WeightType::FfnFc2 => (
470
0
                &self.block_weights[block_idx].ffn_fc2_weight,
471
0
                1,
472
0
                intermediate_dim,
473
0
                hidden_dim,
474
0
            ),
475
0
            WeightType::LmHead => (&self.lm_head_weight, 1, hidden_dim, vocab_size),
476
        };
477
478
        // Clone weight to work around borrow checker - this is the safe fallback.
479
        // For zero-clone operations, use matmul_zero_clone() instead (IMP-1007).
480
0
        let weight_clone = weight.clone();
481
482
        // Now call do_matmul with cloned weight
483
0
        self.do_matmul(input, &weight_clone, m, k, n)
484
0
    }
485
486
    /// IMP-1007: Zero-clone matmul helper using explicit scheduler extraction
487
    ///
488
    /// This is a more aggressive optimization that temporarily extracts the
489
    /// cuda_scheduler to enable truly zero-clone matmul operations.
490
    ///
491
    /// # Safety
492
    ///
493
    /// This method uses `Option::take()` to temporarily move the scheduler,
494
    /// which is safe but requires careful handling to restore it.
495
    #[cfg(feature = "cuda")]
496
    pub fn matmul_zero_clone(
497
        &mut self,
498
        input: &[f32],
499
        block_idx: usize,
500
        op: WeightType,
501
    ) -> Result<Vec<f32>> {
502
        // Extract dimensions
503
        let hidden_dim = self.config.hidden_dim;
504
        let qkv_dim = self.config.qkv_dim();
505
        let intermediate_dim = self.config.intermediate_dim;
506
        let vocab_size = self.config.vocab_size;
507
508
        // Temporarily take cuda_scheduler out of self
509
        let mut cuda_sched: Option<CudaScheduler> = self.cuda_scheduler.take();
510
511
        // Now we can borrow block_weights freely
512
        let (weight, m, k, n) = match op {
513
            WeightType::Qkv => (
514
                &self.block_weights[block_idx].qkv_weight,
515
                1,
516
                hidden_dim,
517
                qkv_dim,
518
            ),
519
            WeightType::Output => (
520
                &self.block_weights[block_idx].out_weight,
521
                1,
522
                hidden_dim,
523
                hidden_dim,
524
            ),
525
            WeightType::FfnFc1 => (
526
                &self.block_weights[block_idx].ffn_fc1_weight,
527
                1,
528
                hidden_dim,
529
                intermediate_dim,
530
            ),
531
            WeightType::FfnFc2 => (
532
                &self.block_weights[block_idx].ffn_fc2_weight,
533
                1,
534
                intermediate_dim,
535
                hidden_dim,
536
            ),
537
            WeightType::LmHead => (&self.lm_head_weight, 1, hidden_dim, vocab_size),
538
        };
539
540
        // Perform matmul with extracted scheduler
541
        let result: Result<Vec<f32>> = if let Some(sched) = cuda_sched.as_mut() {
542
            CudaScheduler::matmul(sched, input, weight, m, k, n)
543
        } else {
544
            self.scheduler.matmul(input, weight, m, k, n)
545
        };
546
547
        // Restore cuda_scheduler
548
        self.cuda_scheduler = cuda_sched;
549
550
        result
551
    }
552
553
    // =========================================================================
554
    // IMP-1008: RefCell-based zero-clone matmul (interior mutability pattern)
555
    // =========================================================================
556
557
    /// IMP-1008: Zero-clone matmul using interior mutability
558
    ///
559
    /// This method takes `&self` instead of `&mut self` by wrapping scheduler
560
    /// access in RefCell. This eliminates the need to clone weights.
561
    ///
562
    /// # Errors
563
    ///
564
    /// Returns error if matmul fails or RefCell is already borrowed.
565
    #[cfg(feature = "cuda")]
566
    #[allow(clippy::many_single_char_names)]
567
    pub fn matmul_refcell(
568
        &self,
569
        a: &[f32],
570
        b: &[f32],
571
        m: usize,
572
        k: usize,
573
        n: usize,
574
    ) -> Result<Vec<f32>> {
575
        // IMP-1008: For RefCell pattern, we need to use a different approach
576
        // Since cuda_scheduler is Option<CudaScheduler>, we use UnsafeCell
577
        // pattern with explicit unsafe block to avoid changing struct layout.
578
        //
579
        // This is safe because:
580
        // 1. We only access cuda_scheduler mutably here
581
        // 2. No other code paths access it during matmul
582
        // 3. This is single-threaded execution
583
584
        // Use raw pointer to bypass borrow checker (safe in single-threaded context)
585
        // SAFETY: This is safe because:
586
        // - We're in single-threaded context (LLM inference)
587
        // - cuda_scheduler is only accessed through this method during matmul
588
        // - The borrow is released before returning
589
        let cuda_sched_ptr = std::ptr::addr_of!(self.cuda_scheduler).cast_mut();
590
591
        // SAFETY: Memory safety ensured by bounds checking and alignment
592
        let result: Result<Vec<f32>> = unsafe {
593
            if let Some(sched) = (*cuda_sched_ptr).as_mut() {
594
                CudaScheduler::matmul(sched, a, b, m, k, n)
595
            } else {
596
                // Fallback to HybridScheduler (also needs mut access)
597
                let sched_ptr = std::ptr::addr_of!(self.scheduler).cast_mut();
598
                (*sched_ptr).matmul(a, b, m, k, n)
599
            }
600
        };
601
        result
602
    }
603
604
    /// IMP-1008: Forward single block without weight cloning
605
    ///
606
    /// Uses interior mutability pattern to avoid cloning weights on each matmul.
607
    /// This method takes `&self` instead of `&mut self`.
608
    ///
609
    /// # Errors
610
    ///
611
    /// Returns error if forward pass fails.
612
    #[cfg(feature = "cuda")]
613
    pub fn forward_block_refcell(
614
        &self,
615
        input: &[f32],
616
        block_idx: usize,
617
        kv_cache: &mut StreamingKVCache,
618
    ) -> Result<Vec<f32>> {
619
        // Phase 21 Debug: trace first forward call only
620
        static DEBUG_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
621
        let call_count = DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
622
        let debug_this_call = block_idx == 0 && call_count == 0;  // Only first call to block 0
623
624
        // Extract config values (Copy types, no borrow conflict)
625
        let hidden_dim = self.config.hidden_dim;
626
        let num_heads = self.config.num_heads;
627
        let head_dim = self.config.head_dim();
628
        let kv_dim = self.config.kv_dim();
629
        let qkv_dim = self.config.qkv_dim();
630
        let intermediate_dim = self.config.intermediate_dim;
631
        let eps = self.config.eps;
632
        let num_kv_heads = self.config.num_kv_heads;
633
634
        if debug_this_call {
635
            eprintln!("[PHASE21] forward_block_refcell START block_idx={}", block_idx);
636
            eprintln!("[PHASE21] input L2: {:.4}", input.iter().map(|x| x*x).sum::<f32>().sqrt());
637
        }
638
639
        // IMP-1008: No cloning! Direct reference to weights
640
        // Pre-attention layer norm (static function avoids &self borrow)
641
        let normed = Self::layer_norm_static(
642
            input,
643
            &self.block_weights[block_idx].attn_norm_weight,
644
            &self.block_weights[block_idx].attn_norm_bias,
645
            hidden_dim,
646
            eps,
647
        );
648
649
        // QKV projection - NO CLONE!
650
        let mut qkv = self.matmul_refcell(
651
            &normed,
652
            &self.block_weights[block_idx].qkv_weight,
653
            1,
654
            hidden_dim,
655
            qkv_dim,
656
        )?;
657
658
        if debug_this_call {
659
            eprintln!("[PHASE21] QKV L2: {:.4}", qkv.iter().map(|x| x*x).sum::<f32>().sqrt());
660
        }
661
662
        // Get current position BEFORE caching (Phase 21)
663
        let (cached_k_ref, _) = kv_cache.get_valid(block_idx);
664
        let current_pos = cached_k_ref.len() / kv_dim;
665
666
        // Phase 21: Apply RoPE to Q and K BEFORE caching
667
        // Without RoPE, attention has no position information and produces garbage
668
        let rope_theta = self.config.rope_theta;
669
        Self::apply_rope_inline(&mut qkv[0..hidden_dim], num_heads, head_dim, rope_theta, current_pos);
670
        Self::apply_rope_inline(&mut qkv[hidden_dim..hidden_dim + kv_dim], num_kv_heads, head_dim, rope_theta, current_pos);
671
672
        // Split QKV (GQA: K/V have kv_dim, not hidden_dim) - after RoPE
673
        let q = qkv[0..hidden_dim].to_vec();
674
        let k_new = qkv[hidden_dim..hidden_dim + kv_dim].to_vec();
675
        let v_new = qkv[hidden_dim + kv_dim..].to_vec();
676
677
        // Get cached K/V and clone to avoid borrow issues with kv_cache
678
        let (cached_k, cached_v) = kv_cache.get_valid(block_idx);
679
        let keys_cached = cached_k.to_vec();
680
        let vals_cached = cached_v.to_vec();
681
682
        // Append new K/V (with RoPE applied) to cache
683
        kv_cache.append(block_idx, &k_new, &v_new);
684
685
        // Build full K/V (cached + new)
686
        let kv_len = keys_cached.len() / kv_dim + 1;
687
        let mut full_k = keys_cached;
688
        full_k.extend_from_slice(&k_new);
689
        let mut full_v = vals_cached;
690
        full_v.extend_from_slice(&v_new);
691
692
        // GQA attention (IMP-089): static method to avoid borrow conflicts
693
        let attn_output = Self::gqa_multihead_attention(
694
            &q,
695
            &full_k,
696
            &full_v,
697
            kv_len,
698
            num_heads,
699
            num_kv_heads,
700
            head_dim,
701
        );
702
703
        if debug_this_call {
704
            eprintln!("[PHASE21] attn_output L2: {:.4}", attn_output.iter().map(|x| x*x).sum::<f32>().sqrt());
705
        }
706
707
        // Output projection - NO CLONE!
708
        let attn_proj = self.matmul_refcell(
709
            &attn_output,
710
            &self.block_weights[block_idx].out_weight,
711
            1,
712
            hidden_dim,
713
            hidden_dim,
714
        )?;
715
716
        // Add residual and bias
717
        let out_bias = &self.block_weights[block_idx].out_bias;
718
        let post_attn: Vec<f32> = input
719
            .iter()
720
            .zip(attn_proj.iter())
721
            .zip(out_bias.iter())
722
            .map(|((&i, &a), &b)| i + a + b)
723
            .collect();
724
725
        // FFN with layer norm (static function)
726
        let ffn_normed = Self::layer_norm_static(
727
            &post_attn,
728
            &self.block_weights[block_idx].ffn_norm_weight,
729
            &self.block_weights[block_idx].ffn_norm_bias,
730
            hidden_dim,
731
            eps,
732
        );
733
734
        // FFN: SwiGLU when gate weight exists, otherwise GELU
735
        let fc1_activated: Vec<f32> = if let Some(ref gate_weight) = self.block_weights[block_idx].ffn_gate_weight {
736
            // SwiGLU: silu(gate(x)) * up(x)
737
            // Up projection
738
            let up_out = self.matmul_refcell(
739
                &ffn_normed,
740
                &self.block_weights[block_idx].ffn_fc1_weight,
741
                1,
742
                hidden_dim,
743
                intermediate_dim,
744
            )?;
745
746
            // Gate projection
747
            let gate_out = self.matmul_refcell(
748
                &ffn_normed,
749
                gate_weight,
750
                1,
751
                hidden_dim,
752
                intermediate_dim,
753
            )?;
754
755
            // SwiGLU: silu(gate) * up
756
            // silu(x) = x * sigmoid(x) = x / (1 + exp(-x))
757
            up_out
758
                .iter()
759
                .zip(gate_out.iter())
760
                .map(|(&u, &g)| {
761
                    let silu_g = g / (1.0 + (-g).exp());
762
                    silu_g * u
763
                })
764
                .collect()
765
        } else {
766
            // Standard GELU FFN
767
            let fc1_out = self.matmul_refcell(
768
                &ffn_normed,
769
                &self.block_weights[block_idx].ffn_fc1_weight,
770
                1,
771
                hidden_dim,
772
                intermediate_dim,
773
            )?;
774
775
            // Add bias and GELU activation
776
            let ffn_fc1_bias = &self.block_weights[block_idx].ffn_fc1_bias;
777
            fc1_out
778
                .iter()
779
                .zip(ffn_fc1_bias.iter())
780
                .map(|(&x, &b)| {
781
                    let x_b = x + b;
782
                    x_b * 0.5 + x_b * 0.5 * (0.797_884_6 * (x_b + 0.044_715 * x_b.powi(3))).tanh()
783
                })
784
                .collect()
785
        };
786
787
        // FFN FC2 (down projection) - NO CLONE!
788
        let fc2_out = self.matmul_refcell(
789
            &fc1_activated,
790
            &self.block_weights[block_idx].ffn_fc2_weight,
791
            1,
792
            intermediate_dim,
793
            hidden_dim,
794
        )?;
795
796
        // Add bias and residual
797
        let ffn_fc2_bias = &self.block_weights[block_idx].ffn_fc2_bias;
798
        let output: Vec<f32> = post_attn
799
            .iter()
800
            .zip(fc2_out.iter())
801
            .zip(ffn_fc2_bias.iter())
802
            .map(|((&h, &f), &b)| h + f + b)
803
            .collect();
804
805
        if debug_this_call {
806
            eprintln!("[PHASE21] block output L2: {:.4}", output.iter().map(|x| x*x).sum::<f32>().sqrt());
807
        }
808
809
        Ok(output)
810
    }
811
812
    /// IMP-1008: Full incremental forward pass without weight cloning
813
    ///
814
    /// Uses interior mutability pattern throughout for zero-clone operation.
815
    ///
816
    /// # Errors
817
    ///
818
    /// Returns error if forward pass fails.
819
    #[cfg(feature = "cuda")]
820
    pub fn forward_refcell(
821
        &self,
822
        token_id: usize,
823
        kv_cache: &mut StreamingKVCache,
824
    ) -> Result<Vec<f32>> {
825
        // Phase 21: Debug first call only
826
        static FWD_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
827
        let fwd_count = FWD_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
828
        let debug_this_fwd = fwd_count == 0;
829
830
        if token_id >= self.config.vocab_size {
831
            return Err(RealizarError::InvalidShape {
832
                reason: format!(
833
                    "Token ID {} out of bounds (vocab_size={})",
834
                    token_id, self.config.vocab_size
835
                ),
836
            });
837
        }
838
839
        let hidden_dim = self.config.hidden_dim;
840
841
        // Embed single token
842
        let offset = token_id * hidden_dim;
843
        let mut hidden = self.embedding_weights[offset..offset + hidden_dim].to_vec();
844
845
        // Process through all blocks - NO CLONE!
846
        for block_idx in 0..self.config.num_layers {
847
            hidden = self.forward_block_refcell(&hidden, block_idx, kv_cache)?;
848
        }
849
850
        // Final layer norm
851
        hidden = self.layer_norm_refcell(&hidden, &self.final_norm_weight, &self.final_norm_bias);
852
853
        // LM head projection
854
        let lm_head_elements = hidden_dim * self.config.vocab_size;
855
        let output = if exceeds_gpu_buffer_limit(lm_head_elements) {
856
            // CPU path with transposed weights + SIMD + fused bias
857
            cpu_matmul_transposed_simd(
858
                &hidden,
859
                &self.lm_head_weight_t,
860
                &self.lm_head_bias,
861
                hidden_dim,
862
                self.config.vocab_size,
863
            )
864
        } else {
865
            // GPU path - NO CLONE!
866
            let vocab_size = self.config.vocab_size;
867
            let logits =
868
                self.matmul_refcell(&hidden, &self.lm_head_weight, 1, hidden_dim, vocab_size)?;
869
            // Add bias
870
            logits
871
                .into_iter()
872
                .zip(self.lm_head_bias.iter())
873
                .map(|(l, &b)| l + b)
874
                .collect()
875
        };
876
877
        if debug_this_fwd {
878
            // Find argmax
879
            let (argmax_idx, argmax_val) = output.iter()
880
                .enumerate()
881
                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
882
                .unwrap_or((0, &0.0));
883
            eprintln!("[PHASE21] forward_refcell: final hidden L2: {:.4}", hidden.iter().map(|x| x*x).sum::<f32>().sqrt());
884
            eprintln!("[PHASE21] forward_refcell: logits argmax: {} (val: {:.4})", argmax_idx, argmax_val);
885
        }
886
887
        Ok(output)
888
    }
889
890
    /// IMP-1008: Layer norm with RefCell pattern (takes &self)
891
    #[cfg(feature = "cuda")]
892
    fn layer_norm_refcell(&self, input: &[f32], weight: &[f32], bias: &[f32]) -> Vec<f32> {
893
        Self::layer_norm_static(input, weight, bias, self.config.hidden_dim, self.config.eps)
894
    }
895
896
    /// IMP-1008: Generate tokens without weight cloning
897
    ///
898
    /// Uses interior mutability pattern for zero-clone inference.
899
    ///
900
    /// # Errors
901
    ///
902
    /// Returns error if generation fails.
903
    #[cfg(feature = "cuda")]
904
    pub fn generate_refcell(
905
        &self,
906
        prompt: &[usize],
907
        config: &GpuGenerateConfig,
908
    ) -> Result<Vec<usize>> {
909
        if prompt.is_empty() {
910
            return Err(RealizarError::InvalidShape {
911
                reason: "Prompt cannot be empty".to_string(),
912
            });
913
        }
914
915
        let num_kv_heads = self.config.num_kv_heads;
916
        let head_dim = self.config.head_dim();
917
        let max_seq_len = prompt.len() + config.max_tokens;
918
919
        // Initialize KV cache
920
        let mut kv_cache =
921
            StreamingKVCache::new(self.config.num_layers, max_seq_len, num_kv_heads, head_dim);
922
923
        let mut tokens = prompt.to_vec();
924
925
        // Process prompt tokens to populate KV cache
926
        for &token_id in prompt {
927
            let _ = self.forward_refcell(token_id, &mut kv_cache)?;
928
        }
929
930
        // Generate new tokens
931
        for _ in 0..config.max_tokens {
932
            let last_token = *tokens.last().unwrap_or(&0);
933
            let logits = self.forward_refcell(last_token, &mut kv_cache)?;
934
935
            // Sample next token (greedy when temperature=0, otherwise top-k)
936
            let next_token = if config.temperature == 0.0 || config.top_k == 1 {
937
                // Greedy decoding
938
                logits
939
                    .iter()
940
                    .enumerate()
941
                    .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
942
                    .map_or(0, |(idx, _)| idx)
943
            } else {
944
                // Top-k sampling with temperature
945
                Self::sample_topk_generate(&logits, config.temperature, config.top_k)
946
            };
947
948
            tokens.push(next_token);
949
950
            // Check for stop tokens
951
            if config.stop_tokens.contains(&next_token) {
952
                break;
953
            }
954
        }
955
956
        Ok(tokens)
957
    }
958
959
    /// Create GPU model from GGUF config (M13: Real Model Loading)
960
    ///
961
    /// This is a convenience constructor that creates a model with zero-initialized
962
    /// weights from a config. Use `from_mapped_gguf()` to load actual weights.
963
    ///
964
    /// # Arguments
965
    ///
966
    /// * `config` - Model configuration
967
    ///
968
    /// # Errors
969
    ///
970
    /// Returns error if GPU initialization fails
971
    ///
972
    /// # Examples
973
    ///
974
    /// ```rust,ignore
975
    /// let config = GpuModelConfig {
976
    ///     vocab_size: 32000,
977
    ///     hidden_dim: 4096,
978
    ///     num_heads: 32,
979
    ///     num_kv_heads: 32,
980
    ///     num_layers: 32,
981
    ///     intermediate_dim: 11008,
982
    ///     eps: 1e-5,
983
    ///     rope_theta: 10000.0,
984
    /// };
985
    /// let model = GpuModel::from_gguf_config(config)?;
986
    /// ```
987
9
    pub fn from_gguf_config(config: GpuModelConfig) -> Result<Self> {
988
        // Delegate to new() which handles initialization
989
9
        Self::new(config)
990
9
    }
991
992
    /// Load GPU model from memory-mapped GGUF file (M13: Real Model Loading)
993
    ///
994
    /// This is the primary method for loading real GGUF models to GPU.
995
    /// It dequantizes weights on-the-fly and uploads them to GPU buffers.
996
    ///
997
    /// # Arguments
998
    ///
999
    /// * `mapped` - Memory-mapped GGUF model
1000
    ///
1001
    /// # Errors
1002
    ///
1003
    /// Returns error if:
1004
    /// - Required tensors are missing
1005
    /// - Tensor shapes don't match expected dimensions
1006
    /// - GPU initialization or upload fails
1007
    ///
1008
    /// # Examples
1009
    ///
1010
    /// ```rust,ignore
1011
    /// let mapped = MappedGGUFModel::from_path("model.gguf")?;
1012
    /// let model = GpuModel::from_mapped_gguf(&mapped)?;
1013
    /// let logits = model.forward_gpu_owned(&[1, 2, 3])?;
1014
    /// ```
1015
0
    pub fn from_mapped_gguf(mapped: &crate::gguf::MappedGGUFModel) -> Result<Self> {
1016
        use crate::gguf::GGUFConfig;
1017
1018
        // Extract config from GGUF metadata
1019
0
        let gguf_config = GGUFConfig::from_gguf(&mapped.model)?;
1020
1021
0
        let config = GpuModelConfig {
1022
0
            vocab_size: gguf_config.vocab_size,
1023
0
            hidden_dim: gguf_config.hidden_dim,
1024
0
            num_heads: gguf_config.num_heads,
1025
0
            num_kv_heads: gguf_config.num_kv_heads, // IMP-088: GQA support
1026
0
            num_layers: gguf_config.num_layers,
1027
0
            intermediate_dim: gguf_config.intermediate_dim,
1028
0
            eps: gguf_config.eps,
1029
0
            rope_theta: gguf_config.rope_theta, // Phase 21: RoPE support
1030
0
        };
1031
1032
0
        let scheduler = HybridScheduler::new()?;
1033
0
        let data = mapped.data();
1034
1035
        // Load token embeddings (always dequantized for fast lookup)
1036
0
        let embedding_weights = mapped.model.get_tensor_f32("token_embd.weight", data)?;
1037
1038
        // Load transformer blocks
1039
0
        let mut block_weights = Vec::with_capacity(config.num_layers);
1040
0
        for layer_idx in 0..config.num_layers {
1041
0
            let prefix = format!("blk.{}", layer_idx);
1042
1043
            // Attention norm (small, keep as f32)
1044
0
            let attn_norm_weight = mapped
1045
0
                .model
1046
0
                .get_tensor_f32(&format!("{}.attn_norm.weight", prefix), data)?;
1047
0
            let attn_norm_bias = mapped
1048
0
                .model
1049
0
                .get_tensor_f32(&format!("{}.attn_norm.bias", prefix), data)
1050
0
                .unwrap_or_else(|_| vec![0.0f32; config.hidden_dim]);
1051
1052
            // QKV projection - try fused QKV first (LLaMA), then separate Q/K/V (Qwen)
1053
0
            let (qkv_weight, qkv_bias) = if let Ok(fused_qkv) = mapped
1054
0
                .model
1055
0
                .get_tensor_f32(&format!("{}.attn_qkv.weight", prefix), data)
1056
            {
1057
                // Fused QKV (LLaMA-style)
1058
0
                let bias = mapped
1059
0
                    .model
1060
0
                    .get_tensor_f32(&format!("{}.attn_qkv.bias", prefix), data)
1061
0
                    .unwrap_or_else(|_| vec![0.0f32; 3 * config.hidden_dim]);
1062
0
                (fused_qkv, bias)
1063
            } else {
1064
                // Separate Q/K/V (Qwen-style) - concatenate into fused format
1065
                // For GQA: Q has num_heads * head_dim, K/V have num_kv_heads * head_dim
1066
0
                let head_dim = config.hidden_dim / config.num_heads;
1067
0
                let kv_dim = config.num_kv_heads * head_dim; // K/V dimension for GQA
1068
1069
0
                let q_weight = mapped
1070
0
                    .model
1071
0
                    .get_tensor_f32(&format!("{}.attn_q.weight", prefix), data)?;
1072
0
                let k_weight = mapped
1073
0
                    .model
1074
0
                    .get_tensor_f32(&format!("{}.attn_k.weight", prefix), data)?;
1075
0
                let v_weight = mapped
1076
0
                    .model
1077
0
                    .get_tensor_f32(&format!("{}.attn_v.weight", prefix), data)?;
1078
1079
                // Concatenate Q, K, V weights
1080
0
                let mut qkv_weight =
1081
0
                    Vec::with_capacity(q_weight.len() + k_weight.len() + v_weight.len());
1082
0
                qkv_weight.extend_from_slice(&q_weight);
1083
0
                qkv_weight.extend_from_slice(&k_weight);
1084
0
                qkv_weight.extend_from_slice(&v_weight);
1085
1086
                // Load biases if available (use correct dimensions for GQA)
1087
0
                let q_bias = mapped
1088
0
                    .model
1089
0
                    .get_tensor_f32(&format!("{}.attn_q.bias", prefix), data)
1090
0
                    .unwrap_or_else(|_| vec![0.0f32; config.hidden_dim]);
1091
0
                let k_bias = mapped
1092
0
                    .model
1093
0
                    .get_tensor_f32(&format!("{}.attn_k.bias", prefix), data)
1094
0
                    .unwrap_or_else(|_| vec![0.0f32; kv_dim]); // GQA: K/V use num_kv_heads
1095
0
                let v_bias = mapped
1096
0
                    .model
1097
0
                    .get_tensor_f32(&format!("{}.attn_v.bias", prefix), data)
1098
0
                    .unwrap_or_else(|_| vec![0.0f32; kv_dim]); // GQA: K/V use num_kv_heads
1099
1100
                // Total bias size: Q (hidden_dim) + K (kv_dim) + V (kv_dim)
1101
0
                let total_bias_dim = config.hidden_dim + 2 * kv_dim;
1102
0
                let mut qkv_bias = Vec::with_capacity(total_bias_dim);
1103
0
                qkv_bias.extend_from_slice(&q_bias);
1104
0
                qkv_bias.extend_from_slice(&k_bias);
1105
0
                qkv_bias.extend_from_slice(&v_bias);
1106
1107
0
                (qkv_weight, qkv_bias)
1108
            };
1109
1110
            // Output projection
1111
0
            let out_weight = mapped
1112
0
                .model
1113
0
                .get_tensor_f32(&format!("{}.attn_output.weight", prefix), data)?;
1114
0
            let out_bias = mapped
1115
0
                .model
1116
0
                .get_tensor_f32(&format!("{}.attn_output.bias", prefix), data)
1117
0
                .unwrap_or_else(|_| vec![0.0f32; config.hidden_dim]);
1118
1119
            // FFN norm
1120
0
            let ffn_norm_weight = mapped
1121
0
                .model
1122
0
                .get_tensor_f32(&format!("{}.ffn_norm.weight", prefix), data)
1123
0
                .unwrap_or_else(|_| vec![1.0f32; config.hidden_dim]);
1124
0
            let ffn_norm_bias = mapped
1125
0
                .model
1126
0
                .get_tensor_f32(&format!("{}.ffn_norm.bias", prefix), data)
1127
0
                .unwrap_or_else(|_| vec![0.0f32; config.hidden_dim]);
1128
1129
            // FFN projections
1130
0
            let ffn_fc1_weight = mapped
1131
0
                .model
1132
0
                .get_tensor_f32(&format!("{}.ffn_up.weight", prefix), data)?;
1133
0
            let ffn_fc1_bias = mapped
1134
0
                .model
1135
0
                .get_tensor_f32(&format!("{}.ffn_up.bias", prefix), data)
1136
0
                .unwrap_or_else(|_| vec![0.0f32; config.intermediate_dim]);
1137
1138
0
            let ffn_fc2_weight = mapped
1139
0
                .model
1140
0
                .get_tensor_f32(&format!("{}.ffn_down.weight", prefix), data)?;
1141
0
            let ffn_fc2_bias = mapped
1142
0
                .model
1143
0
                .get_tensor_f32(&format!("{}.ffn_down.bias", prefix), data)
1144
0
                .unwrap_or_else(|_| vec![0.0f32; config.hidden_dim]);
1145
1146
            // Try to load gate weight for SwiGLU (optional)
1147
0
            let ffn_gate_weight = mapped
1148
0
                .model
1149
0
                .get_tensor_f32(&format!("{}.ffn_gate.weight", prefix), data)
1150
0
                .ok();
1151
1152
0
            block_weights.push(BlockWeights {
1153
0
                attn_norm_weight,
1154
0
                attn_norm_bias,
1155
0
                qkv_weight,
1156
0
                qkv_bias,
1157
0
                out_weight,
1158
0
                out_bias,
1159
0
                ffn_norm_weight,
1160
0
                ffn_norm_bias,
1161
0
                ffn_fc1_weight,
1162
0
                ffn_fc1_bias,
1163
0
                ffn_fc2_weight,
1164
0
                ffn_fc2_bias,
1165
0
                ffn_gate_weight,
1166
0
            });
1167
        }
1168
1169
        // Final layer norm
1170
0
        let final_norm_weight = mapped.model.get_tensor_f32("output_norm.weight", data)?;
1171
0
        let final_norm_bias = mapped
1172
0
            .model
1173
0
            .get_tensor_f32("output_norm.bias", data)
1174
0
            .unwrap_or_else(|_| vec![0.0f32; config.hidden_dim]);
1175
1176
        // LM head
1177
0
        let lm_head_weight = mapped.model.get_tensor_f32("output.weight", data)?;
1178
0
        let lm_head_bias = mapped
1179
0
            .model
1180
0
            .get_tensor_f32("output.bias", data)
1181
0
            .unwrap_or_else(|_| vec![0.0f32; config.vocab_size]);
1182
1183
        // Pre-compute transposed LM head for fast CPU inference
1184
0
        let lm_head_weight_t =
1185
0
            Self::transpose_weights(&lm_head_weight, config.hidden_dim, config.vocab_size);
1186
1187
0
        Ok(Self {
1188
0
            embedding_weights,
1189
0
            block_weights,
1190
0
            final_norm_weight,
1191
0
            final_norm_bias,
1192
0
            lm_head_weight,
1193
0
            lm_head_weight_t,
1194
0
            lm_head_bias,
1195
0
            scheduler,
1196
0
            #[cfg(feature = "cuda")]
1197
0
            cuda_scheduler: None,
1198
0
            config,
1199
0
            attention_buffers: None,
1200
0
        })
1201
0
    }
1202
1203
    /// Create GpuModel from pre-extracted APR weights (PMAT-106)
1204
    ///
1205
    /// This constructor is used by `AprToGpuAdapter` to create a `GpuModel`
1206
    /// from dequantized APR weights.
1207
    ///
1208
    /// # Arguments
1209
    ///
1210
    /// * `config` - GPU model configuration
1211
    /// * `embedding_weights` - Token embedding weights
1212
    /// * `block_weights` - Transformer block weights
1213
    /// * `final_norm_weight` - Final layer norm weight
1214
    /// * `final_norm_bias` - Final layer norm bias
1215
    /// * `lm_head_weight` - LM head weight (row-major)
1216
    /// * `lm_head_weight_t` - LM head weight transposed (for fast CPU inference)
1217
    /// * `lm_head_bias` - LM head bias
1218
    ///
1219
    /// # Errors
1220
    ///
1221
    /// Returns error if GPU scheduler initialization fails
1222
    #[allow(clippy::too_many_arguments)]
1223
0
    pub fn from_apr_weights(
1224
0
        config: GpuModelConfig,
1225
0
        embedding_weights: Vec<f32>,
1226
0
        block_weights: Vec<BlockWeights>,
1227
0
        final_norm_weight: Vec<f32>,
1228
0
        final_norm_bias: Vec<f32>,
1229
0
        lm_head_weight: Vec<f32>,
1230
0
        lm_head_weight_t: Vec<f32>,
1231
0
        lm_head_bias: Vec<f32>,
1232
0
    ) -> Result<Self> {
1233
0
        let scheduler = HybridScheduler::new()?;
1234
1235
        // Phase 21: Initialize CudaScheduler for GPU-accelerated matmul
1236
        #[cfg(feature = "cuda")]
1237
        let cuda_scheduler = match CudaScheduler::new() {
1238
            Ok(cs) => {
1239
                eprintln!("[PHASE21] CudaScheduler initialized for APR model");
1240
                Some(cs)
1241
            }
1242
            Err(e) => {
1243
                eprintln!("[PHASE21] CudaScheduler init failed (using HybridScheduler fallback): {}", e);
1244
                None
1245
            }
1246
        };
1247
1248
0
        Ok(Self {
1249
0
            embedding_weights,
1250
0
            block_weights,
1251
0
            final_norm_weight,
1252
0
            final_norm_bias,
1253
0
            lm_head_weight,
1254
0
            lm_head_weight_t,
1255
0
            lm_head_bias,
1256
0
            scheduler,
1257
0
            #[cfg(feature = "cuda")]
1258
0
            cuda_scheduler,
1259
0
            config,
1260
0
            attention_buffers: None,
1261
0
        })
1262
0
    }
1263
1264
    /// Get model configuration (M13: Real Model Loading)
1265
    #[must_use]
1266
4
    pub fn config(&self) -> &GpuModelConfig {
1267
4
        &self.config
1268
4
    }
1269
1270
    // ============================================================================
1271
    // Phase 8: Optimized Incremental Decoding (M17)
1272
    // ============================================================================
1273
1274
    /// Create GPU model with pre-allocated attention buffers (M17)
1275
    ///
1276
    /// Allocates reusable buffers for incremental decoding, eliminating
1277
    /// per-token memory allocation overhead.
1278
    ///
1279
    /// # Arguments
1280
    ///
1281
    /// * `config` - Model configuration
1282
    /// * `max_seq_len` - Maximum sequence length to support
1283
    ///
1284
    /// # Errors
1285
    ///
1286
    /// Returns error if GPU initialization fails
1287
5
    pub fn with_attention_buffers(config: GpuModelConfig, max_seq_len: usize) -> Result<Self> {
1288
5
        let buffers = AttentionBuffers::new(&config, max_seq_len);
1289
5
        let mut model = Self::new(config)
?0
;
1290
5
        model.attention_buffers = Some(buffers);
1291
5
        Ok(model)
1292
5
    }
1293
1294
    /// Check if model has pre-allocated attention buffers (M17)
1295
    #[must_use]
1296
1
    pub fn has_attention_buffers(&self) -> bool {
1297
1
        self.attention_buffers.is_some()
1298
1
    }
1299
1300
    /// Optimized text generation using pre-allocated buffers (M17)
1301
    ///
1302
    /// Uses the optimized incremental forward pass with pre-allocated buffers
1303
    /// and batched multi-head attention for better performance.
1304
    ///
1305
    /// # Arguments
1306
    ///
1307
    /// * `prompt` - Initial token IDs
1308
    /// * `config` - Generation configuration
1309
    ///
1310
    /// # Errors
1311
    ///
1312
    /// Returns error if generation fails
1313
32
    pub fn generate_optimized(
1314
32
        &mut self,
1315
32
        prompt: &[usize],
1316
32
        config: &GpuGenerateConfig,
1317
32
    ) -> Result<Vec<usize>> {
1318
32
        if prompt.is_empty() {
1319
1
            return Err(RealizarError::InvalidShape {
1320
1
                reason: "Prompt cannot be empty".to_string(),
1321
1
            });
1322
31
        }
1323
1324
        // Initialize KV cache
1325
        // IMP-093: For GQA, use num_kv_heads since K/V have fewer heads than Q
1326
31
        let head_dim = self.config.hidden_dim / self.config.num_heads;
1327
31
        let max_seq_len = self
1328
31
            .attention_buffers
1329
31
            .as_ref()
1330
31
            .map_or(512, |b| b.max_seq_len);
1331
31
        let mut kv_cache = StreamingKVCache::new(
1332
31
            self.config.num_layers,
1333
31
            max_seq_len,
1334
31
            self.config.num_kv_heads, // GQA: K/V have fewer heads
1335
31
            head_dim,
1336
        );
1337
1338
31
        let mut tokens = prompt.to_vec();
1339
1340
        // Process prompt with cache - returns logits for final position only [vocab_size]
1341
31
        let logits = self.forward_gpu_with_cache(prompt, &mut kv_cache)
?0
;
1342
1343
        // Sample first token (logits is already for last position only)
1344
31
        let mut next_token = if config.temperature == 0.0 || 
config.top_k == 11
{
1345
30
            Self::argmax(&logits)
1346
        } else {
1347
1
            Self::sample_topk_generate(&logits, config.temperature, config.top_k)
1348
        };
1349
1350
31
        if config.stop_tokens.contains(&next_token) {
1351
2
            return Ok(tokens);
1352
29
        }
1353
1354
29
        tokens.push(next_token);
1355
1356
        // Generate remaining tokens using optimized incremental forward
1357
29
        for _ in 1..config.max_tokens {
1358
472
            let logits = self.forward_gpu_incremental_optimized(next_token, &mut kv_cache)
?0
;
1359
1360
472
            next_token = if config.temperature == 0.0 || 
config.top_k == 12
{
1361
470
                Self::argmax(&logits)
1362
            } else {
1363
2
                Self::sample_topk_generate(&logits, config.temperature, config.top_k)
1364
            };
1365
1366
472
            if config.stop_tokens.contains(&next_token) {
1367
0
                break;
1368
472
            }
1369
1370
472
            tokens.push(next_token);
1371
        }
1372
1373
29
        Ok(tokens)
1374
32
    }
1375
1376
    /// Optimized incremental forward pass using pre-allocated buffers (M17)
1377
    ///
1378
    /// Single-token forward pass optimized by:
1379
    /// - Reusing pre-allocated attention buffers
1380
    /// - Direct KV cache access without copying
1381
    /// - Batched multi-head attention computation
1382
    ///
1383
    /// # Arguments
1384
    ///
1385
    /// * `token_id` - Single token to process
1386
    /// * `kv_cache` - Mutable reference to KV cache
1387
    ///
1388
    /// # Errors
1389
    ///
1390
    /// Returns error if forward pass fails
1391
524
    pub fn forward_gpu_incremental_optimized(
1392
524
        &mut self,
1393
524
        token_id: usize,
1394
524
        kv_cache: &mut StreamingKVCache,
1395
524
    ) -> Result<Vec<f32>> {
1396
524
        if token_id >= self.config.vocab_size {
1397
0
            return Err(RealizarError::InvalidShape {
1398
0
                reason: format!(
1399
0
                    "Token ID {} out of bounds (vocab_size={})",
1400
0
                    token_id, self.config.vocab_size
1401
0
                ),
1402
0
            });
1403
524
        }
1404
1405
524
        let hidden_dim = self.config.hidden_dim;
1406
1407
        // Get embedding for single token
1408
524
        let offset = token_id * hidden_dim;
1409
524
        let mut hidden: Vec<f32> = self.embedding_weights[offset..offset + hidden_dim].to_vec();
1410
1411
        // Process through all blocks with optimized attention
1412
1.55k
        for block_idx in 0..
self.block_weights524
.
len524
() {
1413
1.55k
            hidden = self.forward_block_incremental_optimized(&hidden, block_idx, kv_cache)
?0
;
1414
        }
1415
1416
        // Final layer norm
1417
524
        hidden = self.layer_norm(&hidden, &self.final_norm_weight, &self.final_norm_bias);
1418
1419
        // LM head projection (single token)
1420
        // IMP-090, IMP-096: Use CPU fallback with SIMD for large vocab
1421
524
        let lm_head_elements = hidden_dim * self.config.vocab_size;
1422
524
        let output = if exceeds_gpu_buffer_limit(lm_head_elements) {
1423
            // IMP-096: CPU path with transposed weights + SIMD + fused bias
1424
0
            cpu_matmul_transposed_simd(
1425
0
                &hidden,
1426
0
                &self.lm_head_weight_t,
1427
0
                &self.lm_head_bias,
1428
0
                hidden_dim,
1429
0
                self.config.vocab_size,
1430
            )
1431
        } else {
1432
            // IMP-1006: Use do_matmul to route to CudaScheduler when available
1433
524
            let lm_weight = self.lm_head_weight.clone();
1434
524
            let vocab_size = self.config.vocab_size;
1435
524
            let logits = self.do_matmul(&hidden, &lm_weight, 1, hidden_dim, vocab_size)
?0
;
1436
            // Add bias
1437
524
            logits
1438
524
                .into_iter()
1439
524
                .zip(self.lm_head_bias.iter())
1440
134k
                .
map524
(|(l, &b)| l + b)
1441
524
                .collect()
1442
        };
1443
1444
524
        Ok(output)
1445
524
    }
1446
1447
    /// Optimized block forward with batched multi-head attention (M17, IMP-092)
1448
    ///
1449
    /// IMP-092: Eliminated weight cloning (~130MB per layer) by using explicit
1450
    /// field borrowing. Previous version cloned 3.7GB per token across 28 layers.
1451
1.55k
    pub fn forward_block_incremental_optimized(
1452
1.55k
        &mut self,
1453
1.55k
        input: &[f32],
1454
1.55k
        block_idx: usize,
1455
1.55k
        kv_cache: &mut StreamingKVCache,
1456
1.55k
    ) -> Result<Vec<f32>> {
1457
        // Extract config values (Copy types, no borrow conflict)
1458
1.55k
        let hidden_dim = self.config.hidden_dim;
1459
1.55k
        let num_heads = self.config.num_heads;
1460
1.55k
        let head_dim = self.config.head_dim();
1461
1.55k
        let kv_dim = self.config.kv_dim();
1462
1.55k
        let qkv_dim = self.config.qkv_dim();
1463
1.55k
        let intermediate_dim = self.config.intermediate_dim;
1464
1.55k
        let eps = self.config.eps;
1465
1.55k
        let num_kv_heads = self.config.num_kv_heads;
1466
1467
        // IMP-092: Use REFERENCES instead of cloning 130MB of weights per layer
1468
        // Pre-attention layer norm (static function avoids &self borrow)
1469
1.55k
        let normed = Self::layer_norm_static(
1470
1.55k
            input,
1471
1.55k
            &self.block_weights[block_idx].attn_norm_weight,
1472
1.55k
            &self.block_weights[block_idx].attn_norm_bias,
1473
1.55k
            hidden_dim,
1474
1.55k
            eps,
1475
        );
1476
1477
        // QKV projection for single token [1, hidden_dim] @ [hidden_dim, qkv_dim]
1478
        // For GQA: qkv_dim = hidden_dim + 2*kv_dim (K/V have fewer heads)
1479
        // IMP-1006: Use do_matmul to route to CudaScheduler when available
1480
1.55k
        let qkv_weight = self.block_weights[block_idx].qkv_weight.clone();
1481
1.55k
        let mut qkv = self.do_matmul(&normed, &qkv_weight, 1, hidden_dim, qkv_dim)
?0
;
1482
1483
        // Get current position BEFORE caching (Phase 21)
1484
1.55k
        let (cached_k_ref, _) = kv_cache.get_valid(block_idx);
1485
1.55k
        let current_pos = cached_k_ref.len() / kv_dim;
1486
1487
        // Phase 21: Apply RoPE to Q and K BEFORE caching
1488
        // Without RoPE, attention has no position information and produces garbage
1489
1.55k
        let rope_theta = self.config.rope_theta;
1490
1.55k
        Self::apply_rope_inline(&mut qkv[0..hidden_dim], num_heads, head_dim, rope_theta, current_pos);
1491
1.55k
        Self::apply_rope_inline(&mut qkv[hidden_dim..hidden_dim + kv_dim], num_kv_heads, head_dim, rope_theta, current_pos);
1492
1493
        // Split QKV (GQA: K/V have kv_dim, not hidden_dim) - after RoPE
1494
1.55k
        let q = qkv[0..hidden_dim].to_vec();
1495
1.55k
        let k_new = qkv[hidden_dim..hidden_dim + kv_dim].to_vec();
1496
1.55k
        let v_new = qkv[hidden_dim + kv_dim..].to_vec();
1497
1498
        // Get cached K/V and clone to avoid borrow issues with kv_cache
1499
1.55k
        let (cached_k, cached_v) = kv_cache.get_valid(block_idx);
1500
1.55k
        let keys_cached = cached_k.to_vec();
1501
1.55k
        let vals_cached = cached_v.to_vec();
1502
1503
        // Append new K/V (with RoPE applied) to cache
1504
1.55k
        kv_cache.append(block_idx, &k_new, &v_new);
1505
1506
        // Build full K/V (cached + new)
1507
        // GQA: K/V have kv_dim per position, not hidden_dim
1508
1.55k
        let kv_len = keys_cached.len() / kv_dim + 1;
1509
1.55k
        let mut full_k = keys_cached;
1510
1.55k
        full_k.extend_from_slice(&k_new);
1511
1.55k
        let mut full_v = vals_cached;
1512
1.55k
        full_v.extend_from_slice(&v_new);
1513
1514
        // GQA attention (IMP-089): static method to avoid borrow conflicts
1515
1.55k
        let attn_output = Self::gqa_multihead_attention(
1516
1.55k
            &q,
1517
1.55k
            &full_k,
1518
1.55k
            &full_v,
1519
1.55k
            kv_len,
1520
1.55k
            num_heads,
1521
1.55k
            num_kv_heads,
1522
1.55k
            head_dim,
1523
        );
1524
1525
        // Output projection
1526
        // IMP-1006: Use do_matmul to route to CudaScheduler when available
1527
1.55k
        let out_weight = self.block_weights[block_idx].out_weight.clone();
1528
1.55k
        let attn_proj = self.do_matmul(&attn_output, &out_weight, 1, hidden_dim, hidden_dim)
?0
;
1529
1530
        // Add residual and bias
1531
1.55k
        let out_bias = &self.block_weights[block_idx].out_bias;
1532
1.55k
        let mut post_attn: Vec<f32> = input
1533
1.55k
            .iter()
1534
1.55k
            .zip(attn_proj.iter())
1535
1.55k
            .zip(out_bias.iter())
1536
163k
            .
map1.55k
(|((&i, &a), &b)| i + a + b)
1537
1.55k
            .collect();
1538
1539
        // FFN with layer norm (static function)
1540
1.55k
        let ffn_normed = Self::layer_norm_static(
1541
1.55k
            &post_attn,
1542
1.55k
            &self.block_weights[block_idx].ffn_norm_weight,
1543
1.55k
            &self.block_weights[block_idx].ffn_norm_bias,
1544
1.55k
            hidden_dim,
1545
1.55k
            eps,
1546
        );
1547
1548
        // FFN: SwiGLU when gate weight exists, otherwise GELU
1549
        // IMP-1006: Use do_matmul to route to CudaScheduler when available
1550
1.55k
        let fc1_activated: Vec<f32> = if let Some(
ref gate_weight0
) = self.block_weights[block_idx].ffn_gate_weight {
1551
            // SwiGLU: silu(gate(x)) * up(x)
1552
0
            let fc1_weight = self.block_weights[block_idx].ffn_fc1_weight.clone();
1553
0
            let gate_weight = gate_weight.clone();
1554
1555
0
            let up_out = self.do_matmul(&ffn_normed, &fc1_weight, 1, hidden_dim, intermediate_dim)?;
1556
0
            let gate_out = self.do_matmul(&ffn_normed, &gate_weight, 1, hidden_dim, intermediate_dim)?;
1557
1558
            // SwiGLU: silu(gate) * up
1559
0
            up_out
1560
0
                .iter()
1561
0
                .zip(gate_out.iter())
1562
0
                .map(|(&u, &g)| {
1563
0
                    let silu_g = g / (1.0 + (-g).exp());
1564
0
                    silu_g * u
1565
0
                })
1566
0
                .collect()
1567
        } else {
1568
            // Standard GELU FFN
1569
1.55k
            let fc1_weight = self.block_weights[block_idx].ffn_fc1_weight.clone();
1570
1.55k
            let fc1_out = self.do_matmul(&ffn_normed, &fc1_weight, 1, hidden_dim, intermediate_dim)
?0
;
1571
1572
1.55k
            let ffn_fc1_bias = &self.block_weights[block_idx].ffn_fc1_bias;
1573
1.55k
            fc1_out
1574
1.55k
                .iter()
1575
1.55k
                .zip(ffn_fc1_bias.iter())
1576
326k
                .
map1.55k
(|(&x, &b)| {
1577
326k
                    let x_b = x + b;
1578
326k
                    x_b * 0.5 + x_b * 0.5 * (0.797_884_6 * (x_b + 0.044_715 * x_b.powi(3))).tanh()
1579
326k
                })
1580
1.55k
                .collect()
1581
        };
1582
1583
        // FFN FC2 (down projection)
1584
        // IMP-1006: Use do_matmul to route to CudaScheduler when available
1585
1.55k
        let fc2_weight = self.block_weights[block_idx].ffn_fc2_weight.clone();
1586
1.55k
        let fc2_out =
1587
1.55k
            self.do_matmul(&fc1_activated, &fc2_weight, 1, intermediate_dim, hidden_dim)
?0
;
1588
1589
        // Add residual and bias
1590
1.55k
        let ffn_fc2_bias = &self.block_weights[block_idx].ffn_fc2_bias;
1591
163k
        for i in 0..
hidden_dim1.55k
{
1592
163k
            post_attn[i] += fc2_out[i] + ffn_fc2_bias[i];
1593
163k
        }
1594
1595
1.55k
        Ok(post_attn)
1596
1.55k
    }
1597
1598
    /// Apply Rotary Position Embedding (RoPE) inline (Phase 21)
1599
    ///
1600
    /// RoPE encodes position information by rotating pairs of elements
1601
    /// with position-dependent angles. This is CRITICAL for transformer attention.
1602
    ///
1603
    /// # Arguments
1604
    /// * `x` - Mutable slice of Q or K vectors for a single position [num_heads * head_dim]
1605
    /// * `num_heads` - Number of attention heads
1606
    /// * `head_dim` - Dimension per head
1607
    /// * `rope_theta` - Base frequency (typically 10000.0)
1608
    /// * `position` - Token position for RoPE encoding
1609
3.10k
    fn apply_rope_inline(
1610
3.10k
        x: &mut [f32],
1611
3.10k
        num_heads: usize,
1612
3.10k
        head_dim: usize,
1613
3.10k
        rope_theta: f32,
1614
3.10k
        position: usize,
1615
3.10k
    ) {
1616
3.10k
        let half_dim = head_dim / 2;
1617
3.10k
        let head_dim_f32 = head_dim as f32;
1618
3.10k
        let pos_f32 = position as f32;
1619
1620
20.4k
        for h in 0..
num_heads3.10k
{
1621
20.4k
            let head_start = h * head_dim;
1622
20.4k
            let idx2_start = head_start + half_dim;
1623
1624
163k
            for i in 0..
half_dim20.4k
{
1625
163k
                let freq = 1.0 / rope_theta.powf(2.0 * i as f32 / head_dim_f32);
1626
163k
                let angle = pos_f32 * freq;
1627
163k
                let (sin_val, cos_val) = angle.sin_cos();
1628
163k
1629
163k
                let x1 = x[head_start + i];
1630
163k
                let x2 = x[idx2_start + i];
1631
163k
1632
163k
                // Apply rotation: [cos -sin; sin cos] * [x1; x2]
1633
163k
                x[head_start + i] = x1 * cos_val - x2 * sin_val;
1634
163k
                x[idx2_start + i] = x1 * sin_val + x2 * cos_val;
1635
163k
            }
1636
        }
1637
3.10k
    }
1638
1639
    /// GQA multi-head attention (IMP-089, IMP-092, IMP-094)
1640
    ///
1641
    /// Grouped Query Attention where K/V have fewer heads than Q.
1642
    /// Each KV head serves (num_heads / num_kv_heads) Q heads.
1643
    ///
1644
    /// IMP-094: Uses trueno SIMD-accelerated dot product and softmax
1645
    /// for ~10x speedup over scalar implementation.
1646
    ///
1647
    /// Static method to avoid borrow conflicts with scheduler and weights.
1648
1.55k
    fn gqa_multihead_attention(
1649
1.55k
        q: &[f32], // Q: [num_heads * head_dim]
1650
1.55k
        k: &[f32], // K: [kv_len * num_kv_heads * head_dim]
1651
1.55k
        v: &[f32], // V: [kv_len * num_kv_heads * head_dim]
1652
1.55k
        kv_len: usize,
1653
1.55k
        num_heads: usize,    // Number of Q heads
1654
1.55k
        num_kv_heads: usize, // Number of K/V heads (for GQA, < num_heads)
1655
1.55k
        head_dim: usize,
1656
1.55k
    ) -> Vec<f32> {
1657
        use trueno::Vector;
1658
1659
1.55k
        let hidden_dim = num_heads * head_dim;
1660
1.55k
        let kv_dim = num_kv_heads * head_dim;
1661
1.55k
        let scale = 1.0 / (head_dim as f32).sqrt();
1662
1663
        // Number of Q heads per KV head
1664
1.55k
        let heads_per_kv = num_heads / num_kv_heads;
1665
1666
1.55k
        let mut output = vec![0.0; hidden_dim];
1667
1668
        // Compute attention for all Q heads
1669
10.2k
        for h in 0..
num_heads1.55k
{
1670
10.2k
            let q_head = &q[h * head_dim..(h + 1) * head_dim];
1671
            // IMP-094: Create trueno vector for SIMD dot product
1672
10.2k
            let q_vec = Vector::from_slice(q_head);
1673
1674
            // Map Q head to KV head (GQA: multiple Q heads share one KV head)
1675
10.2k
            let kv_head = h / heads_per_kv;
1676
1677
            // Compute attention scores for this head using SIMD dot product
1678
10.2k
            let mut scores = Vec::with_capacity(kv_len);
1679
225k
            for pos in 0..
kv_len10.2k
{
1680
225k
                // K offset: pos * kv_dim + kv_head * head_dim
1681
225k
                let k_offset = pos * kv_dim + kv_head * head_dim;
1682
225k
                let cached_key = &k[k_offset..k_offset + head_dim];
1683
225k
1684
225k
                // IMP-094: SIMD dot product via trueno
1685
225k
                let k_vec = Vector::from_slice(cached_key);
1686
225k
                let score = q_vec.dot(&k_vec).unwrap_or(0.0) * scale;
1687
225k
                scores.push(score);
1688
225k
            }
1689
1690
            // IMP-094: SIMD softmax via trueno
1691
10.2k
            let scores_vec = Vector::from_slice(&scores);
1692
10.2k
            let attn_weights: Vec<f32> = scores_vec.softmax().map_or_else(
1693
0
                |_| {
1694
                    // Fallback to scalar softmax
1695
0
                    let max_score = scores.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1696
0
                    let exp_scores: Vec<f32> =
1697
0
                        scores.iter().map(|&s| (s - max_score).exp()).collect();
1698
0
                    let sum_exp: f32 = exp_scores.iter().sum();
1699
0
                    exp_scores.iter().map(|&e| e / sum_exp).collect()
1700
0
                },
1701
10.2k
                |v| v.as_slice().to_vec(),
1702
            );
1703
1704
            // Weighted sum of values (still scalar - SIMD benefit is marginal for small head_dim)
1705
225k
            for (pos, &weight) in 
attn_weights.iter()10.2k
.
enumerate10.2k
() {
1706
                // V offset: pos * kv_dim + kv_head * head_dim
1707
225k
                let v_offset = pos * kv_dim + kv_head * head_dim;
1708
225k
                let v_head = &v[v_offset..v_offset + head_dim];
1709
1710
3.61M
                for d in 0..
head_dim225k
{
1711
3.61M
                    output[h * head_dim + d] += weight * v_head[d];
1712
3.61M
                }
1713
            }
1714
        }
1715
1716
1.55k
        output
1717
1.55k
    }
1718
1719
    // ============================================================================
1720
    // Phase 9: Fused Kernels & Vectorization (M18)
1721
    // ============================================================================
1722
1723
    /// Check if model has fused QKV projection (M18 - IMP-037)
1724
    ///
1725
    /// Fused QKV uses a single matmul instead of three separate projections.
1726
    /// This is always true for GpuModel as QKV weights are stored combined.
1727
    #[must_use]
1728
1
    pub fn has_fused_qkv(&self) -> bool {
1729
        // QKV weights are stored as [hidden_dim, 3*hidden_dim] for fused projection
1730
1
        !self.block_weights.is_empty()
1731
1
            && self.block_weights[0].qkv_weight.len()
1732
1
                == self.config.hidden_dim * 3 * self.config.hidden_dim
1733
1
    }
1734
1735
    /// Fused QKV projection (M18 - IMP-037)
1736
    ///
1737
    /// Performs Q, K, V projection in a single matmul operation.
1738
    ///
1739
    /// # Arguments
1740
    ///
1741
    /// * `input` - Input tensor [hidden_dim]
1742
    ///
1743
    /// # Returns
1744
    ///
1745
    /// Tuple of (Q, K, V) tensors, each [hidden_dim]
1746
    ///
1747
    /// # Errors
1748
    ///
1749
    /// Returns error if matmul fails
1750
1
    pub fn fused_qkv_projection(
1751
1
        &mut self,
1752
1
        input: &[f32],
1753
1
    ) -> Result<(Vec<f32>, Vec<f32>, Vec<f32>)> {
1754
1
        let hidden_dim = self.config.hidden_dim;
1755
1
        let kv_dim = self.config.kv_dim();
1756
1
        let qkv_dim = self.config.qkv_dim();
1757
1758
        // Use first block's QKV weights for projection
1759
1
        let qkv_weight = &self.block_weights[0].qkv_weight;
1760
1761
        // Single matmul: [1, hidden_dim] @ [hidden_dim, qkv_dim] -> [1, qkv_dim]
1762
        // For GQA: qkv_dim = hidden_dim + 2*kv_dim
1763
1
        let qkv = self
1764
1
            .scheduler
1765
1
            .matmul(input, qkv_weight, 1, hidden_dim, qkv_dim)
?0
;
1766
1767
        // Split into Q, K, V (GQA: K/V have kv_dim, not hidden_dim)
1768
1
        let q = qkv[0..hidden_dim].to_vec();
1769
1
        let k = qkv[hidden_dim..hidden_dim + kv_dim].to_vec();
1770
1
        let v = qkv[hidden_dim + kv_dim..].to_vec();
1771
1772
1
        Ok((q, k, v))
1773
1
    }
1774
1775
    /// Generation with fused QKV projection (M18 - IMP-037)
1776
    ///
1777
    /// Uses fused QKV projection for improved performance.
1778
    ///
1779
    /// # Errors
1780
    ///
1781
    /// Returns error if generation fails due to invalid input or model state.
1782
1
    pub fn generate_with_fused_qkv(
1783
1
        &mut self,
1784
1
        prompt: &[usize],
1785
1
        config: &GpuGenerateConfig,
1786
1
    ) -> Result<Vec<usize>> {
1787
        // Fused QKV is already used in generate_optimized via forward_block_incremental_optimized
1788
        // This method provides explicit API for benchmarking
1789
1
        self.generate_optimized(prompt, config)
1790
1
    }
1791
1792
    /// Check if model has fused attention projection (M18 - IMP-039)
1793
    #[must_use]
1794
1
    pub fn has_fused_attn_proj(&self) -> bool {
1795
        // Attention output projection is stored in block_weights
1796
1
        !self.block_weights.is_empty()
1797
1
            && self.block_weights[0].out_weight.len()
1798
1
                == self.config.hidden_dim * self.config.hidden_dim
1799
1
    }
1800
1801
    /// Forward pass with fused attention projection (M18 - IMP-039)
1802
    ///
1803
    /// Uses fused attention output projection for improved performance.
1804
    ///
1805
    /// # Errors
1806
    ///
1807
    /// Returns error if forward pass fails due to invalid token or cache state.
1808
10
    pub fn forward_with_fused_attn_proj(
1809
10
        &mut self,
1810
10
        token_id: usize,
1811
10
        kv_cache: &mut StreamingKVCache,
1812
10
    ) -> Result<Vec<f32>> {
1813
        // Fused attention projection is already used in forward_gpu_incremental_optimized
1814
        // This method provides explicit API for benchmarking
1815
10
        self.forward_gpu_incremental_optimized(token_id, kv_cache)
1816
10
    }
1817
1818
    /// Check if model has fused output residual capability (M19 - IMP-042)
1819
    #[must_use]
1820
1
    pub fn has_fused_output_residual(&self) -> bool {
1821
        // Fused output residual requires attention buffers and block weights
1822
1
        self.attention_buffers.is_some() && !self.block_weights.is_empty()
1823
1
    }
1824
1825
    /// Forward pass with fused output projection + residual (M19 - IMP-042)
1826
    ///
1827
    /// Combines the output projection matrix multiplication with residual
1828
    /// connection in a single fused operation.
1829
    ///
1830
    /// # Errors
1831
    ///
1832
    /// Returns error if forward pass fails due to invalid token or cache state.
1833
11
    pub fn forward_with_fused_output_residual(
1834
11
        &mut self,
1835
11
        token_id: usize,
1836
11
        kv_cache: &mut StreamingKVCache,
1837
11
    ) -> Result<Vec<f32>> {
1838
        // Currently uses the optimized forward path
1839
        // The fused operation is implemented in forward_block_incremental_optimized
1840
        // This method provides explicit API for benchmarking
1841
11
        self.forward_gpu_incremental_optimized(token_id, kv_cache)
1842
11
    }
1843
1844
    /// Forward pass taking ownership of token_ids (convenience wrapper)
1845
    ///
1846
    /// This is useful when you don't need to keep the token_ids after the call.
1847
    ///
1848
    /// # Arguments
1849
    ///
1850
    /// * `token_ids` - Input token IDs (as Vec for owned semantics in tests)
1851
    ///
1852
    /// # Errors
1853
    ///
1854
    /// Returns error if forward pass fails
1855
1
    pub fn forward_gpu_owned(&mut self, token_ids: &[usize]) -> Result<Vec<f32>> {
1856
1
        self.forward_gpu(token_ids)
1857
1
    }
1858
1859
    /// Generate text tokens using GPU-accelerated inference (M14: E2E Inference)
1860
    ///
1861
    /// Performs autoregressive token generation starting from a prompt.
1862
    /// Uses GPU for forward passes and CPU for sampling.
1863
    ///
1864
    /// # Arguments
1865
    ///
1866
    /// * `prompt` - Initial token IDs to start generation from
1867
    /// * `config` - Generation configuration (max tokens, temperature, etc.)
1868
    ///
1869
    /// # Returns
1870
    ///
1871
    /// Vector of generated token IDs (including the prompt)
1872
    ///
1873
    /// # Errors
1874
    ///
1875
    /// Returns error if:
1876
    /// - Prompt is empty
1877
    /// - Forward pass fails
1878
    ///
1879
    /// # Examples
1880
    ///
1881
    /// ```rust,ignore
1882
    /// let config = GpuGenerateConfig::deterministic(32);
1883
    /// let tokens = model.generate(&[1, 2, 3], &config)?;
1884
    /// ```
1885
22
    pub fn generate(&mut self, prompt: &[usize], config: &GpuGenerateConfig) -> Result<Vec<usize>> {
1886
        // IMP-1009: Use zero-clone RefCell path when CUDA is available
1887
        // This provides ~7x speedup by eliminating weight cloning
1888
        #[cfg(feature = "cuda")]
1889
        if self.cuda_scheduler.is_some() {
1890
            return self.generate_refcell(prompt, config);
1891
        }
1892
1893
        // Fallback to clone-based path for non-CUDA or HybridScheduler
1894
        // IMP-091: Uses KV cache for O(n) generation
1895
22
        self.generate_optimized(prompt, config)
1896
22
    }
1897
1898
    // =========================================================================
1899
    // Phase 7: KV Cache Integration - Wrappers (extracted to kv.rs)
1900
    // =========================================================================
1901
1902
    /// Forward pass with KV cache population (IMP-031) - delegates to kv module
1903
35
    pub fn forward_gpu_with_cache(
1904
35
        &mut self,
1905
35
        token_ids: &[usize],
1906
35
        kv_cache: &mut StreamingKVCache,
1907
35
    ) -> Result<Vec<f32>> {
1908
35
        super::kv::forward_gpu_with_cache(self, token_ids, kv_cache)
1909
35
    }
1910
1911
    /// Incremental forward pass using cached KV (IMP-032) - delegates to kv module
1912
4
    pub fn forward_gpu_incremental(
1913
4
        &mut self,
1914
4
        token_id: usize,
1915
4
        kv_cache: &mut StreamingKVCache,
1916
4
    ) -> Result<Vec<f32>> {
1917
4
        super::kv::forward_gpu_incremental(self, token_id, kv_cache)
1918
4
    }
1919
1920
    /// Generate with KV cache (IMP-033) - delegates to kv module
1921
1
    pub fn generate_with_cache(
1922
1
        &mut self,
1923
1
        prompt: &[usize],
1924
1
        config: &GpuGenerateConfig,
1925
1
    ) -> Result<Vec<usize>> {
1926
1
        super::kv::generate_with_cache(self, prompt, config)
1927
1
    }
1928
1929
    /// Top-k sampling with temperature (returns highest prob token in top-k for determinism)
1930
3
    fn sample_topk_generate(logits: &[f32], temperature: f32, top_k: usize) -> usize {
1931
        // Apply temperature
1932
768
        let 
scaled3
:
Vec<f32>3
=
logits3
.
iter3
().
map3
(|&x| x / temperature).
collect3
();
1933
1934
        // Softmax with numerical stability
1935
3
        let max_logit = scaled.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1936
768
        let 
exp_logits3
:
Vec<f32>3
=
scaled.iter()3
.
map3
(|&x| (x - max_logit).exp()).
collect3
();
1937
3
        let sum: f32 = exp_logits.iter().sum();
1938
768
        let 
probs3
:
Vec<f32>3
=
exp_logits.iter()3
.
map3
(|&x| x / sum).
collect3
();
1939
1940
        // Get top-k indices by sorting
1941
3
        let mut indexed: Vec<(usize, f32)> =
1942
768
            
probs.iter()3
.
enumerate3
().
map3
(|(i, &p)| (i, p)).
collect3
();
1943
765
        
indexed3
.
sort_by3
(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1944
1945
        // Truncate to top_k and return highest probability token (deterministic)
1946
3
        indexed.truncate(top_k);
1947
3
        indexed.first().map_or(0, |&(idx, _)| idx)
1948
3
    }
1949
1950
    /// Transpose weight matrix from [rows, cols] to [cols, rows]
1951
19
    fn transpose_weights(weights: &[f32], rows: usize, cols: usize) -> Vec<f32> {
1952
19
        let mut transposed = vec![0.0f32; rows * cols];
1953
1.72k
        for i in 0..
rows19
{
1954
25.9M
            for j in 0..
cols1.72k
{
1955
25.9M
                transposed[j * rows + i] = weights[i * cols + j];
1956
25.9M
            }
1957
        }
1958
19
        transposed
1959
19
    }
1960
1961
    /// Check if GPU is being used
1962
    #[must_use]
1963
1
    pub fn has_gpu(&self) -> bool {
1964
1
        self.scheduler.has_gpu()
1965
1
    }
1966
1967
    /// GPU-accelerated forward pass
1968
    ///
1969
    /// Uses HybridScheduler for matrix multiplications.
1970
    ///
1971
    /// # Arguments
1972
    ///
1973
    /// * `token_ids` - Input token IDs
1974
    ///
1975
    /// # Returns
1976
    ///
1977
    /// Logits tensor with shape `[seq_len, vocab_size]`
1978
    ///
1979
    /// # Errors
1980
    ///
1981
    /// Returns error if forward pass fails
1982
5
    pub fn forward_gpu(&mut self, token_ids: &[usize]) -> Result<Vec<f32>> {
1983
5
        if token_ids.is_empty() {
1984
0
            return Err(RealizarError::InvalidShape {
1985
0
                reason: "Token IDs cannot be empty".to_string(),
1986
0
            });
1987
5
        }
1988
1989
5
        let seq_len = token_ids.len();
1990
5
        let hidden_dim = self.config.hidden_dim;
1991
1992
        // Step 1: Embed tokens
1993
5
        let mut hidden = Vec::with_capacity(seq_len * hidden_dim);
1994
21
        for &
token_id16
in token_ids {
1995
16
            if token_id >= self.config.vocab_size {
1996
0
                return Err(RealizarError::InvalidShape {
1997
0
                    reason: format!(
1998
0
                        "Token ID {} out of bounds (vocab_size={})",
1999
0
                        token_id, self.config.vocab_size
2000
0
                    ),
2001
0
                });
2002
16
            }
2003
16
            let offset = token_id * hidden_dim;
2004
16
            hidden.extend_from_slice(&self.embedding_weights[offset..offset + hidden_dim]);
2005
        }
2006
2007
        // Step 2: Pass through transformer blocks
2008
10
        for block_idx in 0..
self.block_weights5
.
len5
() {
2009
10
            hidden = self.forward_block_idx(&hidden, seq_len, block_idx)
?0
;
2010
        }
2011
2012
        // Step 3: Final layer norm
2013
5
        hidden = self.layer_norm(&hidden, &self.final_norm_weight, &self.final_norm_bias);
2014
2015
        // Step 4: LM head projection
2016
        // [seq_len, hidden_dim] @ [hidden_dim, vocab_size] -> [seq_len, vocab_size]
2017
        // Phase 22 FIX: Use lm_head_weight_t (transposed) which is [hidden_dim, vocab_size]
2018
        // The original lm_head_weight is [vocab_size, hidden_dim] (APR convention)
2019
        // IMP-090: Use CPU fallback for large vocab to avoid GPU buffer overflow
2020
5
        let lm_head_elements = hidden_dim * self.config.vocab_size;
2021
5
        let logits = if exceeds_gpu_buffer_limit(lm_head_elements) {
2022
            // CPU fallback for large vocab (>256MB weight matrix)
2023
0
            cpu_matmul(
2024
0
                &hidden,
2025
0
                &self.lm_head_weight_t,
2026
0
                seq_len,
2027
0
                hidden_dim,
2028
0
                self.config.vocab_size,
2029
            )
2030
        } else {
2031
            // GPU path for smaller vocab (IMP-1005: use do_matmul for CUDA)
2032
            // Clone weights to avoid borrow conflict with &mut self in do_matmul
2033
5
            let lm_weight = self.lm_head_weight_t.clone();
2034
5
            self.do_matmul(
2035
5
                &hidden,
2036
5
                &lm_weight,
2037
5
                seq_len,
2038
5
                hidden_dim,
2039
5
                self.config.vocab_size,
2040
0
            )?
2041
        };
2042
2043
        // Add bias
2044
5
        let mut output = logits;
2045
16
        for i in 0..
seq_len5
{
2046
403k
            for j in 0..
self.config.vocab_size16
{
2047
403k
                output[i * self.config.vocab_size + j] += self.lm_head_bias[j];
2048
403k
            }
2049
        }
2050
2051
5
        Ok(output)
2052
5
    }
2053
2054
    /// Forward pass through a single transformer block by index
2055
10
    pub fn forward_block_idx(
2056
10
        &mut self,
2057
10
        input: &[f32],
2058
10
        seq_len: usize,
2059
10
        block_idx: usize,
2060
10
    ) -> Result<Vec<f32>> {
2061
10
        let hidden_dim = self.config.hidden_dim;
2062
10
        let intermediate_dim = self.config.intermediate_dim;
2063
10
        let qkv_dim = self.config.qkv_dim();
2064
2065
        // Get references to block weights (avoid cloning)
2066
10
        let block = &self.block_weights[block_idx];
2067
10
        let attn_norm_weight = &block.attn_norm_weight;
2068
10
        let attn_norm_bias = &block.attn_norm_bias;
2069
2070
        // Pre-norm (uses references, no clone)
2071
10
        let normed = Self::layer_norm_static(
2072
10
            input,
2073
10
            attn_norm_weight,
2074
10
            attn_norm_bias,
2075
10
            hidden_dim,
2076
10
            self.config.eps,
2077
        );
2078
2079
        // IMP-1005: Clone weights to avoid borrow conflict with &mut self in do_matmul
2080
10
        let qkv_weight = self.block_weights[block_idx].qkv_weight.clone();
2081
2082
        // QKV projection (IMP-1005: use do_matmul for CUDA)
2083
        // [seq_len, hidden_dim] @ [hidden_dim, qkv_dim] -> [seq_len, qkv_dim]
2084
10
        let qkv = self.do_matmul(&normed, &qkv_weight, seq_len, hidden_dim, qkv_dim)
?0
;
2085
2086
        // Optimized GQA attention with GPU matmul for scores
2087
10
        let attn_out = self.optimized_gqa_attention(&qkv, seq_len)
?0
;
2088
2089
        // IMP-1005: Clone weights to avoid borrow conflict
2090
10
        let out_weight = self.block_weights[block_idx].out_weight.clone();
2091
10
        let out_bias = self.block_weights[block_idx].out_bias.clone();
2092
2093
        // Output projection (IMP-1005: use do_matmul for CUDA)
2094
10
        let projected = self.do_matmul(&attn_out, &out_weight, seq_len, hidden_dim, hidden_dim)
?0
;
2095
2096
        // Residual 1 (vectorized)
2097
10
        let mut residual1: Vec<f32> = input
2098
10
            .iter()
2099
10
            .zip(projected.iter())
2100
10
            .enumerate()
2101
3.96k
            .
map10
(|(i, (&inp, &proj))| inp + proj + out_bias[i % hidden_dim])
2102
10
            .collect();
2103
2104
        // IMP-1005: Clone weights to avoid borrow conflict
2105
10
        let ffn_norm_weight = self.block_weights[block_idx].ffn_norm_weight.clone();
2106
10
        let ffn_norm_bias = self.block_weights[block_idx].ffn_norm_bias.clone();
2107
2108
        // FFN pre-norm
2109
10
        let ffn_normed = Self::layer_norm_static(
2110
10
            &residual1,
2111
10
            &ffn_norm_weight,
2112
10
            &ffn_norm_bias,
2113
10
            hidden_dim,
2114
10
            self.config.eps,
2115
        );
2116
2117
        // IMP-1005: Clone weights to avoid borrow conflict
2118
10
        let ffn_fc1_weight = self.block_weights[block_idx].ffn_fc1_weight.clone();
2119
10
        let ffn_fc1_bias = self.block_weights[block_idx].ffn_fc1_bias.clone();
2120
10
        let ffn_gate_weight = self.block_weights[block_idx].ffn_gate_weight.clone();
2121
2122
        // FFN: SwiGLU when gate weight exists, otherwise GELU
2123
10
        let activated: Vec<f32> = if let Some(
gate_weight0
) = ffn_gate_weight {
2124
            // SwiGLU: silu(gate(x)) * up(x)
2125
0
            let up_out = self.do_matmul(&ffn_normed, &ffn_fc1_weight, seq_len, hidden_dim, intermediate_dim)?;
2126
0
            let gate_out = self.do_matmul(&ffn_normed, &gate_weight, seq_len, hidden_dim, intermediate_dim)?;
2127
2128
            // SwiGLU: silu(gate) * up
2129
0
            up_out
2130
0
                .iter()
2131
0
                .zip(gate_out.iter())
2132
0
                .map(|(&u, &g)| {
2133
0
                    let silu_g = g / (1.0 + (-g).exp());
2134
0
                    silu_g * u
2135
0
                })
2136
0
                .collect()
2137
        } else {
2138
            // Standard GELU FFN
2139
10
            let fc1_out = self.do_matmul(
2140
10
                &ffn_normed,
2141
10
                &ffn_fc1_weight,
2142
10
                seq_len,
2143
10
                hidden_dim,
2144
10
                intermediate_dim,
2145
0
            )?;
2146
2147
            // GELU activation + bias (vectorized)
2148
10
            fc1_out
2149
10
                .iter()
2150
10
                .enumerate()
2151
7.93k
                .
map10
(|(i, &x)| {
2152
7.93k
                    let x = x + ffn_fc1_bias[i % intermediate_dim];
2153
                    // GELU approximation
2154
7.93k
                    0.5 * x
2155
7.93k
                        * (1.0
2156
7.93k
                            + ((2.0f32 / std::f32::consts::PI).sqrt() * (x + 0.044_715 * x.powi(3)))
2157
7.93k
                                .tanh())
2158
7.93k
                })
2159
10
                .collect()
2160
        };
2161
2162
        // IMP-1005: Clone weights to avoid borrow conflict
2163
10
        let ffn_fc2_weight = self.block_weights[block_idx].ffn_fc2_weight.clone();
2164
10
        let ffn_fc2_bias = self.block_weights[block_idx].ffn_fc2_bias.clone();
2165
2166
        // FFN: fc2 (IMP-1005: use do_matmul for CUDA)
2167
10
        let fc2_out = self.do_matmul(
2168
10
            &activated,
2169
10
            &ffn_fc2_weight,
2170
10
            seq_len,
2171
10
            intermediate_dim,
2172
10
            hidden_dim,
2173
0
        )?;
2174
2175
        // Residual 2 (vectorized, in-place)
2176
3.96k
        for (i, x) in 
residual1.iter_mut()10
.
enumerate10
() {
2177
3.96k
            *x += fc2_out[i] + ffn_fc2_bias[i % hidden_dim];
2178
3.96k
        }
2179
2180
10
        Ok(residual1)
2181
10
    }
2182
2183
    /// RMSNorm (Root Mean Square Layer Normalization)
2184
    ///
2185
    /// PMAT-094 FIX: Qwen2, LLaMA, Mistral use RMSNorm, NOT LayerNorm.
2186
    /// Formula: output = x / sqrt(mean(x^2) + eps) * weight + bias
2187
    #[allow(clippy::cast_precision_loss)]
2188
4.17k
    pub(crate) fn layer_norm_static(
2189
4.17k
        input: &[f32],
2190
4.17k
        weight: &[f32],
2191
4.17k
        bias: &[f32],
2192
4.17k
        hidden_dim: usize,
2193
4.17k
        eps: f32,
2194
4.17k
    ) -> Vec<f32> {
2195
4.17k
        let num_rows = input.len() / hidden_dim;
2196
4.17k
        let mut output = Vec::with_capacity(input.len());
2197
2198
5.39k
        for row in 0..
num_rows4.17k
{
2199
5.39k
            let start = row * hidden_dim;
2200
5.39k
            let row_data = &input[start..start + hidden_dim];
2201
2202
            // RMSNorm: compute root mean square (no mean subtraction!)
2203
567k
            let 
sum_sq5.39k
:
f325.39k
=
row_data5.39k
.
iter5.39k
().
map5.39k
(|&x| x * x).
sum5.39k
();
2204
5.39k
            let rms = (sum_sq / hidden_dim as f32 + eps).sqrt();
2205
2206
            // Normalize and scale
2207
567k
            for (i, &x) in 
row_data5.39k
.
iter5.39k
().
enumerate5.39k
() {
2208
567k
                let normalized = x / rms;
2209
567k
                output.push(normalized * weight[i] + bias[i]);
2210
567k
            }
2211
        }
2212
2213
4.17k
        output
2214
4.17k
    }
2215
2216
    /// Layer normalization (instance method)
2217
529
    fn layer_norm(&self, input: &[f32], weight: &[f32], bias: &[f32]) -> Vec<f32> {
2218
529
        Self::layer_norm_static(input, weight, bias, self.config.hidden_dim, self.config.eps)
2219
529
    }
2220
2221
    /// Generate tokens using GPU-accelerated forward pass with incremental decoding (wrapper)
2222
0
    pub fn generate_gpu(&mut self, prompt: &[usize], max_tokens: usize) -> Result<Vec<usize>> {
2223
0
        super::batch::generate_gpu(self, prompt, max_tokens)
2224
0
    }
2225
2226
    /// Argmax helper for sampling (wrapper)
2227
500
    fn argmax(logits: &[f32]) -> usize {
2228
500
        super::batch::argmax(logits)
2229
500
    }
2230
2231
    /// Optimized GQA attention using GPU for matmul operations (wrapper)
2232
10
    fn optimized_gqa_attention(&mut self, qkv: &[f32], seq_len: usize) -> Result<Vec<f32>> {
2233
10
        super::batch::optimized_gqa_attention(self, qkv, seq_len)
2234
10
    }
2235
}