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/gguf/inference/generation.rs
Line
Count
Source
1
//! Token generation for OwnedQuantizedModel
2
//!
3
//! Contains generate, generate_with_cache, generate_with_cache_streaming,
4
//! generate_with_scratch, and sampling methods.
5
6
use crate::error::{RealizarError, Result};
7
use crate::gguf::ops;
8
use crate::gguf::{
9
    InferenceScratchBuffer, OwnedQuantizedKVCache, OwnedQuantizedModel, QuantizedGenerateConfig,
10
};
11
#[cfg(feature = "gpu")]
12
use crate::gguf::DispatchMetrics;
13
use rand::Rng;
14
15
impl OwnedQuantizedModel {
16
    /// Get most likely next token
17
    ///
18
    /// # Errors
19
    ///
20
    /// Returns error if forward pass fails
21
0
    pub fn predict_next(&self, token_ids: &[u32]) -> Result<u32> {
22
0
        let logits = self.forward(token_ids)?;
23
0
        let (max_idx, _) = logits
24
0
            .iter()
25
0
            .enumerate()
26
0
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
27
0
            .ok_or_else(|| RealizarError::InvalidShape {
28
0
                reason: "Empty logits".to_string(),
29
0
            })?;
30
0
        Ok(max_idx as u32)
31
0
    }
32
33
    /// Generate tokens using fused Q4_K operations (IMP-100)
34
    ///
35
    /// This is the HTTP serving entry point for quantized inference.
36
    ///
37
    /// # Arguments
38
    ///
39
    /// * `prompt` - Initial token IDs
40
    /// * `config` - Generation configuration
41
    ///
42
    /// # Returns
43
    ///
44
    /// Generated token sequence including prompt
45
    ///
46
    /// # Errors
47
    ///
48
    /// Returns error if forward pass fails
49
0
    pub fn generate(&self, prompt: &[u32], config: &QuantizedGenerateConfig) -> Result<Vec<u32>> {
50
0
        if prompt.is_empty() {
51
0
            return Err(RealizarError::InvalidShape {
52
0
                reason: "Prompt cannot be empty".to_string(),
53
0
            });
54
0
        }
55
56
0
        let mut tokens = prompt.to_vec();
57
0
        let max_len = prompt.len() + config.max_tokens;
58
59
0
        for _ in 0..config.max_tokens {
60
            // Forward pass with fused Q4_K ops (1.37x faster)
61
0
            let logits = self.forward(&tokens)?;
62
63
            // Sample next token
64
0
            let next_token = if config.temperature == 0.0 || config.top_k == 1 {
65
                // Greedy decoding
66
0
                Self::argmax(&logits)
67
            } else {
68
                // Temperature + top-k sampling
69
0
                Self::sample_topk(&logits, config.temperature, config.top_k)
70
            };
71
72
            // Check stop condition
73
0
            if config.stop_tokens.contains(&next_token) {
74
0
                break;
75
0
            }
76
77
0
            tokens.push(next_token);
78
79
            // Check max length
80
0
            if tokens.len() >= max_len {
81
0
                break;
82
0
            }
83
        }
84
85
0
        Ok(tokens)
86
0
    }
87
88
    /// Greedy argmax over logits
89
0
    pub(crate) fn argmax(logits: &[f32]) -> u32 {
90
0
        logits
91
0
            .iter()
92
0
            .enumerate()
93
0
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
94
0
            .map_or(0, |(idx, _)| idx as u32)
95
0
    }
96
97
    /// Top-k sampling with temperature
98
0
    pub fn sample_topk(logits: &[f32], temperature: f32, top_k: usize) -> u32 {
99
        // Apply temperature
100
0
        let scaled: Vec<f32> = logits.iter().map(|&x| x / temperature).collect();
101
102
        // Get top-k indices
103
0
        let mut indexed: Vec<(usize, f32)> = scaled.iter().copied().enumerate().collect();
104
0
        indexed.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
105
0
        indexed.truncate(top_k);
106
107
        // Softmax over top-k
108
0
        let max_val = indexed.first().map_or(0.0, |(_, v)| *v);
109
0
        let exp_sum: f32 = indexed.iter().map(|(_, v)| (v - max_val).exp()).sum();
110
0
        let probs: Vec<(usize, f32)> = indexed
111
0
            .iter()
112
0
            .map(|(i, v)| (*i, (v - max_val).exp() / exp_sum))
113
0
            .collect();
114
115
        // Sample from probability distribution with proper randomness
116
0
        let mut rng = rand::thread_rng();
117
0
        let r: f32 = rng.gen();
118
119
0
        let mut cumulative = 0.0;
120
0
        for &(idx, prob) in &probs {
121
0
            cumulative += prob;
122
0
            if cumulative >= r {
123
0
                return idx as u32;
124
0
            }
125
        }
126
127
0
        probs.last().map_or(0, |(idx, _)| *idx as u32)
128
0
    }
129
130
    /// Generate tokens using KV cache for efficient autoregressive decoding (IMP-101)
131
    ///
132
    /// This is O(n) per token instead of O(n²) due to KV cache reuse.
133
    ///
134
    /// # Arguments
135
    /// * `prompt` - Input token IDs
136
    /// * `config` - Generation configuration
137
    ///
138
    /// # Returns
139
    /// Generated token sequence including prompt
140
    ///
141
    /// # Errors
142
    /// Returns error if forward pass fails
143
16
    pub fn generate_with_cache(
144
16
        &self,
145
16
        prompt: &[u32],
146
16
        config: &QuantizedGenerateConfig,
147
16
    ) -> Result<Vec<u32>> {
148
16
        if prompt.is_empty() {
149
0
            return Err(RealizarError::InvalidShape {
150
0
                reason: "Prompt cannot be empty".to_string(),
151
0
            });
152
16
        }
153
154
16
        let max_seq_len = prompt.len() + config.max_tokens;
155
16
        let mut cache = OwnedQuantizedKVCache::from_config(&self.config, max_seq_len);
156
16
        let mut tokens = prompt.to_vec();
157
158
        // Process prompt tokens (prefill), keeping the logits from the last position
159
        // The logits from processing token[n-1] at position n-1 predict token[n]
160
16
        let mut logits = Vec::new();
161
60
        for (pos, &token_id) in 
prompt16
.
iter16
().
enumerate16
() {
162
60
            logits = self.forward_single_with_cache(token_id, &mut cache, pos)
?0
;
163
        }
164
165
        // Generate new tokens
166
        // First iteration uses logits from prefill, subsequent use logits from forward pass
167
120
        for gen_idx in 0..
config.max_tokens16
{
168
            // DEBUG: Print logits info for first generated token
169
120
            if gen_idx == 0 && 
std::env::var("REALIZAR_DEBUG_LOGITS")16
.
is_ok16
() {
170
0
                let sum: f32 = logits.iter().sum();
171
0
                let max_val = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
172
0
                let min_val = logits.iter().copied().fold(f32::INFINITY, f32::min);
173
0
                let top_5: Vec<(usize, f32)> = {
174
0
                    let mut indexed: Vec<_> =
175
0
                        logits.iter().enumerate().map(|(i, &v)| (i, v)).collect();
176
0
                    indexed.sort_by(|(_, a), (_, b)| {
177
0
                        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
178
0
                    });
179
0
                    indexed.into_iter().take(5).collect()
180
                };
181
0
                eprintln!(
182
0
                    "[DEBUG-LOGITS] len={}, sum={:.4}, min={:.4}, max={:.4}",
183
0
                    logits.len(),
184
                    sum,
185
                    min_val,
186
                    max_val
187
                );
188
0
                eprintln!("[DEBUG-LOGITS] top 5 token ids and logits: {:?}", top_5);
189
0
                eprintln!(
190
0
                    "[DEBUG-LOGITS] logits[0..5]: {:?}",
191
0
                    &logits[..5.min(logits.len())]
192
                );
193
120
            }
194
195
            // Sample next token
196
120
            let next_token = if config.temperature == 0.0 || 
config.top_k == 10
{
197
120
                ops::argmax(&logits)
198
            } else {
199
0
                crate::gguf::OwnedQuantizedModel::sample_topk(&logits, config.temperature, config.top_k)
200
            };
201
202
            // DEBUG: Print selected token
203
120
            if gen_idx == 0 && 
std::env::var("REALIZAR_DEBUG_LOGITS")16
.
is_ok16
() {
204
0
                eprintln!(
205
0
                    "[DEBUG-LOGITS] selected token: {} (logit={:.4})",
206
0
                    next_token,
207
0
                    logits.get(next_token as usize).copied().unwrap_or(f32::NAN)
208
0
                );
209
120
            }
210
211
            // Check stop condition
212
120
            if config.stop_tokens.contains(&next_token) {
213
0
                break;
214
120
            }
215
216
120
            tokens.push(next_token);
217
218
            // Check max length
219
120
            if tokens.len() >= max_seq_len {
220
16
                break;
221
104
            }
222
223
            // Get logits for next iteration by forwarding the newly sampled token
224
            // Position is prompt.len() + gen_idx (where token was just added)
225
104
            let position = prompt.len() + gen_idx;
226
104
            logits = self.forward_single_with_cache(next_token, &mut cache, position)
?0
;
227
        }
228
229
16
        Ok(tokens)
230
16
    }
231
232
    /// Generate tokens with streaming callback (PMAT-087)
233
    ///
234
    /// Same as `generate_with_cache` but calls `on_token` after each token
235
    /// is generated, enabling true streaming to clients.
236
    ///
237
    /// # Arguments
238
    /// * `prompt` - Input token IDs
239
    /// * `config` - Generation configuration
240
    /// * `on_token` - Callback called for each generated token. Return `false` to stop.
241
    ///
242
    /// # Returns
243
    /// Generated token sequence including prompt
244
    ///
245
    /// # Errors
246
    /// Returns error if generation fails
247
0
    pub fn generate_with_cache_streaming<F>(
248
0
        &self,
249
0
        prompt: &[u32],
250
0
        config: &QuantizedGenerateConfig,
251
0
        mut on_token: F,
252
0
    ) -> Result<Vec<u32>>
253
0
    where
254
0
        F: FnMut(u32) -> bool,
255
    {
256
0
        if prompt.is_empty() {
257
0
            return Err(RealizarError::InvalidShape {
258
0
                reason: "Prompt cannot be empty".to_string(),
259
0
            });
260
0
        }
261
262
0
        let max_seq_len = prompt.len() + config.max_tokens;
263
0
        let mut cache = OwnedQuantizedKVCache::from_config(&self.config, max_seq_len);
264
0
        let mut tokens = prompt.to_vec();
265
266
        // Process prompt tokens (prefill)
267
0
        let mut logits = Vec::new();
268
0
        for (pos, &token_id) in prompt.iter().enumerate() {
269
0
            logits = self.forward_single_with_cache(token_id, &mut cache, pos)?;
270
        }
271
272
        // Generate new tokens with streaming
273
0
        for gen_idx in 0..config.max_tokens {
274
            // Sample next token
275
0
            let next_token = if config.temperature == 0.0 || config.top_k == 1 {
276
0
                ops::argmax(&logits)
277
            } else {
278
0
                crate::gguf::OwnedQuantizedModel::sample_topk(&logits, config.temperature, config.top_k)
279
            };
280
281
            // Check stop condition
282
0
            if config.stop_tokens.contains(&next_token) {
283
0
                break;
284
0
            }
285
286
0
            tokens.push(next_token);
287
288
            // PMAT-087: Call streaming callback - stop if it returns false
289
0
            if !on_token(next_token) {
290
0
                break;
291
0
            }
292
293
            // Check max length
294
0
            if tokens.len() >= max_seq_len {
295
0
                break;
296
0
            }
297
298
            // Get logits for next iteration
299
0
            let position = prompt.len() + gen_idx;
300
0
            logits = self.forward_single_with_cache(next_token, &mut cache, position)?;
301
        }
302
303
0
        Ok(tokens)
304
0
    }
305
306
    /// Generate tokens with zero-allocation inference (IMP-131)
307
    ///
308
    /// This is the highest-performance generation path. Uses pre-allocated
309
    /// scratch buffers to eliminate per-token allocations, providing ~3-4x
310
    /// speedup over allocating variants.
311
    ///
312
    /// Performance characteristics:
313
    /// - Single allocation at start (scratch buffer + KV cache)
314
    /// - Zero allocations per generated token
315
    /// - ~500KB saved per token for TinyLlama-1.1B
316
    ///
317
    /// # Arguments
318
    /// * `prompt` - Input token IDs
319
    /// * `config` - Generation configuration
320
    ///
321
    /// # Returns
322
    /// Generated token sequence including prompt
323
    ///
324
    /// # Errors
325
    /// Returns error if forward pass fails
326
0
    pub fn generate_with_scratch(
327
0
        &self,
328
0
        prompt: &[u32],
329
0
        config: &QuantizedGenerateConfig,
330
0
    ) -> Result<Vec<u32>> {
331
0
        if prompt.is_empty() {
332
0
            return Err(RealizarError::InvalidShape {
333
0
                reason: "Prompt cannot be empty".to_string(),
334
0
            });
335
0
        }
336
337
0
        let max_seq_len = prompt.len() + config.max_tokens;
338
0
        let mut cache = OwnedQuantizedKVCache::from_config(&self.config, max_seq_len);
339
0
        let mut scratch = InferenceScratchBuffer::from_config(&self.config);
340
0
        let mut tokens = prompt.to_vec();
341
342
        // Process prompt tokens (prefill) - uses scratch buffers
343
0
        for (pos, &token_id) in prompt.iter().enumerate() {
344
0
            self.forward_single_with_scratch(token_id, &mut cache, pos, &mut scratch)?;
345
        }
346
347
        // Generate new tokens - zero allocations per token
348
        // PAR-126: Fixed loop structure to match generate_with_cache:
349
        // 1. Sample from current logits (prefill on first iter, previous forward otherwise)
350
        // 2. Then run forward on the new token to get logits for next iteration
351
0
        for gen_idx in 0..config.max_tokens {
352
            // Sample next token from current logits (prefill logits on first iter)
353
0
            let next_token = if config.temperature == 0.0 || config.top_k == 1 {
354
0
                ops::argmax(&scratch.logits)
355
            } else {
356
0
                crate::gguf::OwnedQuantizedModel::sample_topk(&scratch.logits, config.temperature, config.top_k)
357
            };
358
359
            // Check stop condition
360
0
            if config.stop_tokens.contains(&next_token) {
361
0
                break;
362
0
            }
363
364
0
            tokens.push(next_token);
365
366
            // Check max length
367
0
            if tokens.len() >= max_seq_len {
368
0
                break;
369
0
            }
370
371
            // Get logits for next iteration by forwarding the new token
372
0
            let position = prompt.len() + gen_idx;
373
0
            self.forward_single_with_scratch(next_token, &mut cache, position, &mut scratch)?;
374
        }
375
376
0
        Ok(tokens)
377
0
    }
378
379
    /// Generate tokens with adaptive CPU/GPU attention (IMP-125)
380
    ///
381
    /// This variant of `generate_with_cache` uses `forward_single_with_cache_adaptive`
382
    /// to automatically select between CPU and GPU backends based on cache length.
383
    /// It also records dispatch decisions to the provided metrics tracker.
384
    ///
385
    /// # Arguments
386
    /// * `prompt` - Initial token IDs
387
    /// * `config` - Generation configuration
388
    /// * `metrics` - Dispatch metrics tracker for CPU/GPU decision recording
389
    ///
390
    /// # Returns
391
    /// Generated token sequence including prompt
392
    ///
393
    /// # Errors
394
    /// Returns error if forward pass fails
395
    #[cfg(feature = "gpu")]
396
9
    pub fn generate_with_cache_adaptive(
397
9
        &self,
398
9
        prompt: &[u32],
399
9
        config: &QuantizedGenerateConfig,
400
9
        metrics: &std::sync::Arc<DispatchMetrics>,
401
9
    ) -> Result<Vec<u32>> {
402
9
        if prompt.is_empty() {
403
0
            return Err(RealizarError::InvalidShape {
404
0
                reason: "Prompt cannot be empty".to_string(),
405
0
            });
406
9
        }
407
408
9
        let max_seq_len = prompt.len() + config.max_tokens;
409
9
        let mut cache = OwnedQuantizedKVCache::from_config(&self.config, max_seq_len);
410
9
        let mut tokens = prompt.to_vec();
411
412
        // Process prompt tokens (prefill) with adaptive attention
413
        // Keep the logits from the last position for the first generated token
414
9
        let mut logits = Vec::new();
415
33
        for (pos, &token_id) in 
prompt9
.
iter9
().
enumerate9
() {
416
33
            logits = self.forward_single_with_cache_adaptive(token_id, &mut cache, pos, metrics)
?0
;
417
        }
418
419
        // Generate new tokens with adaptive attention
420
226
        for gen_idx in 0..
config.max_tokens9
{
421
            // Sample next token from current logits
422
226
            let next_token = if config.temperature == 0.0 || 
config.top_k == 10
{
423
226
                ops::argmax(&logits)
424
            } else {
425
0
                crate::gguf::OwnedQuantizedModel::sample_topk(&logits, config.temperature, config.top_k)
426
            };
427
428
            // Check stop condition
429
226
            if config.stop_tokens.contains(&next_token) {
430
0
                break;
431
226
            }
432
433
226
            tokens.push(next_token);
434
435
            // Check max length
436
226
            if tokens.len() >= max_seq_len {
437
9
                break;
438
217
            }
439
440
            // Get logits for next iteration by forwarding the newly sampled token
441
217
            let position = prompt.len() + gen_idx;
442
217
            logits =
443
217
                self.forward_single_with_cache_adaptive(next_token, &mut cache, position, metrics)
?0
;
444
        }
445
446
9
        Ok(tokens)
447
9
    }
448
}