/home/noah/src/realizar/src/generate/sampler.rs
Line | Count | Source |
1 | | //! Advanced Sampling Strategies (PMAT-802) |
2 | | //! |
3 | | //! Extracted from generate/mod.rs - Advanced sampling algorithms. |
4 | | //! |
5 | | //! ## Contents |
6 | | //! - Stop sequence detection |
7 | | //! - Repetition, presence/frequency penalties |
8 | | //! - Min-p, Mirostat, TFS, typical sampling |
9 | | //! - DRY, XTC, Eta sampling |
10 | | //! - Token healing |
11 | | |
12 | | use crate::tensor::Tensor; |
13 | | use crate::error::Result; |
14 | | use super::{ |
15 | | GenerationConfig, apply_temperature, sample_greedy, sample_token, |
16 | | }; |
17 | | use std::collections::HashMap; |
18 | | |
19 | | // ==================== Advanced Sampling Features ==================== |
20 | | |
21 | | use serde::{Deserialize, Serialize}; |
22 | | |
23 | | /// Stop sequence detector for generation termination |
24 | | /// |
25 | | /// Detects when generated text matches stop sequences and signals termination. |
26 | | /// Supports both token ID sequences and string patterns. |
27 | | #[derive(Debug, Clone, Default)] |
28 | | pub struct StopSequenceDetector { |
29 | | /// Token ID sequences to stop on |
30 | | token_sequences: Vec<Vec<usize>>, |
31 | | /// String patterns to stop on |
32 | | string_patterns: Vec<String>, |
33 | | /// Buffer for partial matches (token-based) |
34 | | token_buffer: Vec<usize>, |
35 | | /// Maximum sequence length to track |
36 | | max_seq_len: usize, |
37 | | } |
38 | | |
39 | | impl StopSequenceDetector { |
40 | | /// Create new stop sequence detector |
41 | 10 | pub fn new() -> Self { |
42 | 10 | Self { |
43 | 10 | token_sequences: Vec::new(), |
44 | 10 | string_patterns: Vec::new(), |
45 | 10 | token_buffer: Vec::new(), |
46 | 10 | max_seq_len: 0, |
47 | 10 | } |
48 | 10 | } |
49 | | |
50 | | /// Add a token ID sequence as stop condition |
51 | | #[must_use] |
52 | 6 | pub fn with_token_sequence(mut self, sequence: Vec<usize>) -> Self { |
53 | 6 | if !sequence.is_empty() { |
54 | 6 | self.max_seq_len = self.max_seq_len.max(sequence.len()); |
55 | 6 | self.token_sequences.push(sequence); |
56 | 6 | }0 |
57 | 6 | self |
58 | 6 | } |
59 | | |
60 | | /// Add a string pattern as stop condition |
61 | | #[must_use] |
62 | 3 | pub fn with_string_pattern(mut self, pattern: impl Into<String>) -> Self { |
63 | 3 | let pattern = pattern.into(); |
64 | 3 | if !pattern.is_empty() { |
65 | 3 | self.string_patterns.push(pattern); |
66 | 3 | }0 |
67 | 3 | self |
68 | 3 | } |
69 | | |
70 | | /// Add multiple stop sequences from strings |
71 | | #[must_use] |
72 | 2 | pub fn with_stop_strings(mut self, stops: Vec<String>) -> Self { |
73 | 5 | for stop3 in stops { |
74 | 3 | if !stop.is_empty() { |
75 | 3 | self.string_patterns.push(stop); |
76 | 3 | }0 |
77 | | } |
78 | 2 | self |
79 | 2 | } |
80 | | |
81 | | /// Check if a new token triggers a stop condition |
82 | | /// |
83 | | /// Returns true if generation should stop. |
84 | 109 | pub fn check_token(&mut self, token_id: usize) -> bool { |
85 | | // Add to buffer |
86 | 109 | self.token_buffer.push(token_id); |
87 | | |
88 | | // Trim buffer to max sequence length |
89 | 109 | if self.token_buffer.len() > self.max_seq_len && self.max_seq_len > 098 { |
90 | 98 | self.token_buffer.remove(0); |
91 | 98 | }11 |
92 | | |
93 | | // Check token sequences |
94 | 216 | for seq109 in &self.token_sequences { |
95 | 109 | if self.token_buffer.ends_with(seq) { |
96 | 2 | return true; |
97 | 107 | } |
98 | | } |
99 | | |
100 | 107 | false |
101 | 109 | } |
102 | | |
103 | | /// Check if generated text contains a stop string |
104 | | /// |
105 | | /// Returns Some(position) if stop found, None otherwise. |
106 | 6 | pub fn check_text(&self, text: &str) -> Option<usize> { |
107 | 10 | for pattern8 in &self.string_patterns { |
108 | 8 | if let Some(pos4 ) = text.find(pattern) { |
109 | 4 | return Some(pos); |
110 | 4 | } |
111 | | } |
112 | 2 | None |
113 | 6 | } |
114 | | |
115 | | /// Reset detector state |
116 | 1 | pub fn reset(&mut self) { |
117 | 1 | self.token_buffer.clear(); |
118 | 1 | } |
119 | | |
120 | | /// Check if detector has any stop conditions configured |
121 | 4 | pub fn has_conditions(&self) -> bool { |
122 | 4 | !self.token_sequences.is_empty() || !self.string_patterns.is_empty()2 |
123 | 4 | } |
124 | | } |
125 | | |
126 | | /// Repetition penalty configuration |
127 | | /// |
128 | | /// Penalizes tokens that have appeared in the context to reduce repetition. |
129 | | /// Higher values = stronger penalty (1.0 = no penalty). |
130 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
131 | | pub struct RepetitionPenaltyConfig { |
132 | | /// Penalty multiplier for repeated tokens (1.0 = no penalty, >1.0 = penalty) |
133 | | pub penalty: f32, |
134 | | /// Number of recent tokens to consider (0 = all) |
135 | | pub window_size: usize, |
136 | | } |
137 | | |
138 | | impl Default for RepetitionPenaltyConfig { |
139 | 1 | fn default() -> Self { |
140 | 1 | Self { |
141 | 1 | penalty: 1.0, // No penalty by default |
142 | 1 | window_size: 64, |
143 | 1 | } |
144 | 1 | } |
145 | | } |
146 | | |
147 | | impl RepetitionPenaltyConfig { |
148 | | /// Create with specified penalty |
149 | 9 | pub fn new(penalty: f32) -> Self { |
150 | 9 | Self { |
151 | 9 | penalty, |
152 | 9 | window_size: 64, |
153 | 9 | } |
154 | 9 | } |
155 | | |
156 | | /// Set window size for context |
157 | | #[must_use] |
158 | 2 | pub fn with_window(mut self, window_size: usize) -> Self { |
159 | 2 | self.window_size = window_size; |
160 | 2 | self |
161 | 2 | } |
162 | | |
163 | | /// Check if penalty is enabled |
164 | 6 | pub fn is_enabled(&self) -> bool { |
165 | 6 | (self.penalty - 1.0).abs() > 1e-6 |
166 | 6 | } |
167 | | } |
168 | | |
169 | | /// Apply repetition penalty to logits |
170 | | /// |
171 | | /// Divides logits of tokens that appear in context by the penalty factor. |
172 | | /// |
173 | | /// # Arguments |
174 | | /// |
175 | | /// * `logits` - Raw logits from model |
176 | | /// * `context_tokens` - List of previously generated token IDs |
177 | | /// * `config` - Repetition penalty configuration |
178 | | /// |
179 | | /// # Returns |
180 | | /// |
181 | | /// Logits with repetition penalty applied |
182 | 4 | pub fn apply_repetition_penalty( |
183 | 4 | logits: &Tensor<f32>, |
184 | 4 | context_tokens: &[usize], |
185 | 4 | config: &RepetitionPenaltyConfig, |
186 | 4 | ) -> Tensor<f32> { |
187 | 4 | if !config.is_enabled() || context_tokens3 .is_empty3 () { |
188 | 1 | return logits.clone(); |
189 | 3 | } |
190 | | |
191 | 3 | let data = logits.data(); |
192 | 3 | let mut penalized = data.to_vec(); |
193 | 3 | let vocab_size = data.len(); |
194 | | |
195 | | // Get relevant context window |
196 | 3 | let window_start = if config.window_size > 0 && context_tokens.len() > config.window_size { |
197 | 1 | context_tokens.len() - config.window_size |
198 | | } else { |
199 | 2 | 0 |
200 | | }; |
201 | 3 | let relevant_tokens = &context_tokens[window_start..]; |
202 | | |
203 | | // Apply penalty to each token in context |
204 | 11 | for &token_id8 in relevant_tokens { |
205 | 8 | if token_id < vocab_size { |
206 | 8 | let logit = penalized[token_id]; |
207 | | // For positive logits, divide by penalty |
208 | | // For negative logits, multiply by penalty |
209 | 8 | penalized[token_id] = if logit > 0.0 { |
210 | 7 | logit / config.penalty |
211 | | } else { |
212 | 1 | logit * config.penalty |
213 | | }; |
214 | 0 | } |
215 | | } |
216 | | |
217 | 3 | Tensor::from_vec(logits.shape().to_vec(), penalized) |
218 | 3 | .expect("Shape should match original logits") |
219 | 4 | } |
220 | | |
221 | | /// Presence and frequency penalty configuration (OpenAI-style) |
222 | | /// |
223 | | /// - Presence penalty: Constant penalty for tokens that appear at least once |
224 | | /// - Frequency penalty: Penalty proportional to token frequency |
225 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
226 | | pub struct PresenceFrequencyPenalty { |
227 | | /// Presence penalty (penalty if token appeared at all) |
228 | | pub presence_penalty: f32, |
229 | | /// Frequency penalty (penalty per occurrence) |
230 | | pub frequency_penalty: f32, |
231 | | } |
232 | | |
233 | | impl Default for PresenceFrequencyPenalty { |
234 | 1 | fn default() -> Self { |
235 | 1 | Self { |
236 | 1 | presence_penalty: 0.0, |
237 | 1 | frequency_penalty: 0.0, |
238 | 1 | } |
239 | 1 | } |
240 | | } |
241 | | |
242 | | impl PresenceFrequencyPenalty { |
243 | | /// Create new penalty config |
244 | 8 | pub fn new(presence: f32, frequency: f32) -> Self { |
245 | 8 | Self { |
246 | 8 | presence_penalty: presence, |
247 | 8 | frequency_penalty: frequency, |
248 | 8 | } |
249 | 8 | } |
250 | | |
251 | | /// Check if any penalty is enabled |
252 | 6 | pub fn is_enabled(&self) -> bool { |
253 | 6 | self.presence_penalty.abs() > 1e-6 || self.frequency_penalty.abs() > 1e-62 |
254 | 6 | } |
255 | | } |
256 | | |
257 | | /// Apply presence and frequency penalties to logits |
258 | | /// |
259 | | /// Formula: logit -= presence_penalty * (1 if token in context else 0) |
260 | | /// Formula: logit -= frequency_penalty * count(token in context) |
261 | | /// |
262 | | /// # Arguments |
263 | | /// |
264 | | /// * `logits` - Raw logits from model |
265 | | /// * `context_tokens` - List of previously generated token IDs |
266 | | /// * `config` - Presence/frequency penalty configuration |
267 | | /// |
268 | | /// # Returns |
269 | | /// |
270 | | /// Logits with penalties applied |
271 | 4 | pub fn apply_presence_frequency_penalty( |
272 | 4 | logits: &Tensor<f32>, |
273 | 4 | context_tokens: &[usize], |
274 | 4 | config: &PresenceFrequencyPenalty, |
275 | 4 | ) -> Tensor<f32> { |
276 | 4 | if !config.is_enabled() || context_tokens.is_empty() { |
277 | 0 | return logits.clone(); |
278 | 4 | } |
279 | | |
280 | 4 | let data = logits.data(); |
281 | 4 | let mut penalized = data.to_vec(); |
282 | 4 | let vocab_size = data.len(); |
283 | | |
284 | | // Count token frequencies |
285 | 4 | let mut token_counts: HashMap<usize, usize> = HashMap::new(); |
286 | 17 | for &token_id13 in context_tokens { |
287 | 13 | if token_id < vocab_size { |
288 | 13 | *token_counts.entry(token_id).or_insert(0) += 1; |
289 | 13 | }0 |
290 | | } |
291 | | |
292 | | // Apply penalties |
293 | 12 | for (token_id8 , count8 ) in token_counts { |
294 | 8 | let presence = if count > 0 { 1.0 } else { 0.00 }; |
295 | 8 | penalized[token_id] -= config.presence_penalty * presence; |
296 | 8 | penalized[token_id] -= config.frequency_penalty * (count as f32); |
297 | | } |
298 | | |
299 | 4 | Tensor::from_vec(logits.shape().to_vec(), penalized) |
300 | 4 | .expect("Shape should match original logits") |
301 | 4 | } |
302 | | |
303 | | /// Logit bias configuration |
304 | | /// |
305 | | /// Allows adjusting specific token probabilities before sampling. |
306 | | #[derive(Debug, Clone, Default, Serialize, Deserialize)] |
307 | | pub struct LogitBias { |
308 | | /// Map of token ID to bias value (added to logit) |
309 | | biases: HashMap<usize, f32>, |
310 | | } |
311 | | |
312 | | impl LogitBias { |
313 | | /// Create empty logit bias |
314 | 5 | pub fn new() -> Self { |
315 | 5 | Self { |
316 | 5 | biases: HashMap::new(), |
317 | 5 | } |
318 | 5 | } |
319 | | |
320 | | /// Add bias for a specific token |
321 | | #[must_use] |
322 | 8 | pub fn with_bias(mut self, token_id: usize, bias: f32) -> Self { |
323 | 8 | self.biases.insert(token_id, bias); |
324 | 8 | self |
325 | 8 | } |
326 | | |
327 | | /// Add multiple biases from a map |
328 | | #[must_use] |
329 | 0 | pub fn with_biases(mut self, biases: HashMap<usize, f32>) -> Self { |
330 | 0 | self.biases.extend(biases); |
331 | 0 | self |
332 | 0 | } |
333 | | |
334 | | /// Check if any biases are configured |
335 | 5 | pub fn is_empty(&self) -> bool { |
336 | 5 | self.biases.is_empty() |
337 | 5 | } |
338 | | |
339 | | /// Get bias for a token (0.0 if not set) |
340 | 3 | pub fn get(&self, token_id: usize) -> f32 { |
341 | 3 | self.biases.get(&token_id).copied().unwrap_or(0.0) |
342 | 3 | } |
343 | | } |
344 | | |
345 | | /// Apply logit bias to logits |
346 | | /// |
347 | | /// # Arguments |
348 | | /// |
349 | | /// * `logits` - Raw logits from model |
350 | | /// * `bias` - Logit bias configuration |
351 | | /// |
352 | | /// # Returns |
353 | | /// |
354 | | /// Logits with biases applied |
355 | 3 | pub fn apply_logit_bias(logits: &Tensor<f32>, bias: &LogitBias) -> Tensor<f32> { |
356 | 3 | if bias.is_empty() { |
357 | 0 | return logits.clone(); |
358 | 3 | } |
359 | | |
360 | 3 | let data = logits.data(); |
361 | 3 | let mut biased = data.to_vec(); |
362 | 3 | let vocab_size = data.len(); |
363 | | |
364 | 8 | for (&token_id5 , &bias_value5 ) in &bias.biases { |
365 | 5 | if token_id < vocab_size { |
366 | 4 | biased[token_id] += bias_value; |
367 | 4 | }1 |
368 | | } |
369 | | |
370 | 3 | Tensor::from_vec(logits.shape().to_vec(), biased).expect("Shape should match original logits") |
371 | 3 | } |
372 | | |
373 | | // ===== Prompt Caching ===== |
374 | | |
375 | | /// Prompt cache entry |
376 | | #[derive(Debug, Clone)] |
377 | | pub struct PromptCacheEntry { |
378 | | /// Token sequence |
379 | | pub tokens: Vec<usize>, |
380 | | /// Cached KV state (simplified - in practice would be actual KV tensors) |
381 | | pub kv_hash: u64, |
382 | | /// Number of times this entry has been hit |
383 | | pub hit_count: usize, |
384 | | /// Last access timestamp |
385 | | pub last_access: std::time::Instant, |
386 | | } |
387 | | |
388 | | /// Prompt cache for efficient prefix reuse |
389 | | /// |
390 | | /// Caches prompt prefixes to avoid recomputation when generating multiple |
391 | | /// completions with the same prefix. |
392 | | #[derive(Debug)] |
393 | | pub struct PromptCache { |
394 | | /// Cache entries keyed by token sequence hash |
395 | | entries: std::collections::HashMap<u64, PromptCacheEntry>, |
396 | | /// Maximum cache size |
397 | | max_entries: usize, |
398 | | } |
399 | | |
400 | | impl Default for PromptCache { |
401 | 1 | fn default() -> Self { |
402 | 1 | Self::new(100) |
403 | 1 | } |
404 | | } |
405 | | |
406 | | impl PromptCache { |
407 | | /// Create new prompt cache |
408 | 8 | pub fn new(max_entries: usize) -> Self { |
409 | 8 | Self { |
410 | 8 | entries: std::collections::HashMap::new(), |
411 | 8 | max_entries, |
412 | 8 | } |
413 | 8 | } |
414 | | |
415 | | /// Compute hash for token sequence |
416 | 18 | fn hash_tokens(tokens: &[usize]) -> u64 { |
417 | | use std::hash::{Hash, Hasher}; |
418 | 18 | let mut hasher = std::collections::hash_map::DefaultHasher::new(); |
419 | 18 | tokens.hash(&mut hasher); |
420 | 18 | hasher.finish() |
421 | 18 | } |
422 | | |
423 | | /// Find longest matching prefix in cache |
424 | 5 | pub fn find_prefix(&mut self, tokens: &[usize]) -> Option<(usize, u64)> { |
425 | | // Try progressively shorter prefixes |
426 | 8 | for len in (1..=tokens.len()5 ).rev5 () { |
427 | 8 | let prefix = &tokens[..len]; |
428 | 8 | let hash = Self::hash_tokens(prefix); |
429 | 8 | if let Some(entry4 ) = self.entries.get_mut(&hash) { |
430 | 4 | entry.hit_count += 1; |
431 | 4 | entry.last_access = std::time::Instant::now(); |
432 | 4 | return Some((len, entry.kv_hash)); |
433 | 4 | } |
434 | | } |
435 | 1 | None |
436 | 5 | } |
437 | | |
438 | | /// Add entry to cache |
439 | 10 | pub fn add(&mut self, tokens: Vec<usize>, kv_hash: u64) { |
440 | | // Evict if at capacity |
441 | 10 | if self.entries.len() >= self.max_entries { |
442 | 1 | self.evict_lru(); |
443 | 9 | } |
444 | | |
445 | 10 | let hash = Self::hash_tokens(&tokens); |
446 | 10 | self.entries.insert( |
447 | 10 | hash, |
448 | 10 | PromptCacheEntry { |
449 | 10 | tokens, |
450 | 10 | kv_hash, |
451 | 10 | hit_count: 0, |
452 | 10 | last_access: std::time::Instant::now(), |
453 | 10 | }, |
454 | | ); |
455 | 10 | } |
456 | | |
457 | | /// Evict least recently used entry |
458 | 1 | fn evict_lru(&mut self) { |
459 | 1 | if let Some((&key, _)) = self.entries.iter().min_by_key(|(_, v)| v.last_access) { |
460 | 1 | self.entries.remove(&key); |
461 | 1 | }0 |
462 | 1 | } |
463 | | |
464 | | /// Get cache size |
465 | 5 | pub fn len(&self) -> usize { |
466 | 5 | self.entries.len() |
467 | 5 | } |
468 | | |
469 | | /// Check if cache is empty |
470 | 3 | pub fn is_empty(&self) -> bool { |
471 | 3 | self.entries.is_empty() |
472 | 3 | } |
473 | | |
474 | | /// Clear all entries |
475 | 1 | pub fn clear(&mut self) { |
476 | 1 | self.entries.clear(); |
477 | 1 | } |
478 | | |
479 | | /// Get cache statistics |
480 | 1 | pub fn stats(&self) -> PromptCacheStats { |
481 | 1 | let total_hits: usize = self.entries.values().map(|e| e.hit_count).sum(); |
482 | 1 | PromptCacheStats { |
483 | 1 | entries: self.entries.len(), |
484 | 1 | total_hits, |
485 | 1 | max_entries: self.max_entries, |
486 | 1 | } |
487 | 1 | } |
488 | | } |
489 | | |
490 | | /// Prompt cache statistics |
491 | | #[derive(Debug, Clone)] |
492 | | pub struct PromptCacheStats { |
493 | | /// Number of entries in cache |
494 | | pub entries: usize, |
495 | | /// Total cache hits |
496 | | pub total_hits: usize, |
497 | | /// Maximum cache size |
498 | | pub max_entries: usize, |
499 | | } |
500 | | |
501 | | /// Beam search state for a single hypothesis |
502 | | #[derive(Debug, Clone)] |
503 | | pub struct BeamHypothesis { |
504 | | /// Token sequence generated so far |
505 | | pub tokens: Vec<usize>, |
506 | | /// Cumulative log probability |
507 | | pub score: f32, |
508 | | /// Whether this hypothesis has finished (hit EOS) |
509 | | pub finished: bool, |
510 | | } |
511 | | |
512 | | impl BeamHypothesis { |
513 | | /// Create a new hypothesis starting with given tokens |
514 | 12 | pub fn new(tokens: Vec<usize>, score: f32) -> Self { |
515 | 12 | Self { |
516 | 12 | tokens, |
517 | 12 | score, |
518 | 12 | finished: false, |
519 | 12 | } |
520 | 12 | } |
521 | | |
522 | | /// Extend hypothesis with a new token |
523 | | #[must_use] |
524 | 6 | pub fn extend(&self, token: usize, log_prob: f32, is_eos: bool) -> Self { |
525 | 6 | let mut new_tokens = self.tokens.clone(); |
526 | 6 | new_tokens.push(token); |
527 | 6 | Self { |
528 | 6 | tokens: new_tokens, |
529 | 6 | score: self.score + log_prob, |
530 | 6 | finished: is_eos, |
531 | 6 | } |
532 | 6 | } |
533 | | |
534 | | /// Get length-normalized score |
535 | 8 | pub fn normalized_score(&self, length_penalty: f32) -> f32 { |
536 | 8 | let len = self.tokens.len() as f32; |
537 | 8 | self.score / len.powf(length_penalty) |
538 | 8 | } |
539 | | } |
540 | | |
541 | | /// Beam search configuration |
542 | | #[derive(Debug, Clone)] |
543 | | pub struct BeamSearchConfig { |
544 | | /// Number of beams (hypotheses) to keep |
545 | | pub num_beams: usize, |
546 | | /// Length penalty (>1.0 favors longer sequences, <1.0 favors shorter) |
547 | | pub length_penalty: f32, |
548 | | /// Early stopping: stop when num_beams hypotheses are finished |
549 | | pub early_stopping: bool, |
550 | | /// Number of beams to return |
551 | | pub num_return: usize, |
552 | | } |
553 | | |
554 | | impl Default for BeamSearchConfig { |
555 | 9 | fn default() -> Self { |
556 | 9 | Self { |
557 | 9 | num_beams: 4, |
558 | 9 | length_penalty: 1.0, |
559 | 9 | early_stopping: true, |
560 | 9 | num_return: 1, |
561 | 9 | } |
562 | 9 | } |
563 | | } |
564 | | |
565 | | impl BeamSearchConfig { |
566 | | /// Create new beam search config |
567 | 8 | pub fn new(num_beams: usize) -> Self { |
568 | 8 | Self { |
569 | 8 | num_beams, |
570 | 8 | ..Default::default() |
571 | 8 | } |
572 | 8 | } |
573 | | |
574 | | /// Set length penalty |
575 | | #[must_use] |
576 | 2 | pub fn with_length_penalty(mut self, penalty: f32) -> Self { |
577 | 2 | self.length_penalty = penalty; |
578 | 2 | self |
579 | 2 | } |
580 | | |
581 | | /// Set early stopping |
582 | | #[must_use] |
583 | 5 | pub fn with_early_stopping(mut self, early: bool) -> Self { |
584 | 5 | self.early_stopping = early; |
585 | 5 | self |
586 | 5 | } |
587 | | |
588 | | /// Set number of sequences to return |
589 | | #[must_use] |
590 | 2 | pub fn with_num_return(mut self, n: usize) -> Self { |
591 | 2 | self.num_return = n; |
592 | 2 | self |
593 | 2 | } |
594 | | } |
595 | | |
596 | | /// Beam search state manager |
597 | | #[derive(Debug, Clone)] |
598 | | pub struct BeamSearchState { |
599 | | /// Current hypotheses |
600 | | pub hypotheses: Vec<BeamHypothesis>, |
601 | | /// Finished hypotheses |
602 | | pub finished: Vec<BeamHypothesis>, |
603 | | /// Configuration |
604 | | pub config: BeamSearchConfig, |
605 | | } |
606 | | |
607 | | impl BeamSearchState { |
608 | | /// Create new beam search state |
609 | 6 | pub fn new(config: BeamSearchConfig, initial_tokens: Vec<usize>) -> Self { |
610 | 6 | let hypotheses = vec![BeamHypothesis::new(initial_tokens, 0.0)]; |
611 | 6 | Self { |
612 | 6 | hypotheses, |
613 | 6 | finished: Vec::new(), |
614 | 6 | config, |
615 | 6 | } |
616 | 6 | } |
617 | | |
618 | | /// Process a step with log probabilities for each hypothesis |
619 | | /// |
620 | | /// # Arguments |
621 | | /// |
622 | | /// * `log_probs_per_hyp` - Log probabilities for each token, for each hypothesis |
623 | | /// * `eos_token` - Optional end-of-sequence token ID |
624 | 1 | pub fn step(&mut self, log_probs_per_hyp: &[Vec<f32>], eos_token: Option<usize>) { |
625 | 1 | let mut candidates: Vec<BeamHypothesis> = Vec::new(); |
626 | | |
627 | 1 | for (hyp_idx, hyp) in self.hypotheses.iter().enumerate() { |
628 | 1 | if hyp.finished { |
629 | 0 | candidates.push(hyp.clone()); |
630 | 0 | continue; |
631 | 1 | } |
632 | | |
633 | 1 | let log_probs = &log_probs_per_hyp[hyp_idx]; |
634 | | |
635 | | // Get top-k tokens for this hypothesis (k = num_beams * 2 for safety) |
636 | 1 | let mut indexed: Vec<(usize, f32)> = log_probs |
637 | 1 | .iter() |
638 | 1 | .enumerate() |
639 | 5 | .map1 (|(i, &lp)| (i, lp)) |
640 | 1 | .collect(); |
641 | 4 | indexed1 .sort_by1 (|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); |
642 | | |
643 | 4 | for &(token, log_prob) in indexed.iter()1 .take1 (self.config.num_beams * 21 ) { |
644 | 4 | let is_eos = eos_token == Some(token); |
645 | 4 | let new_hyp = hyp.extend(token, log_prob, is_eos); |
646 | | |
647 | 4 | if is_eos { |
648 | 0 | self.finished.push(new_hyp); |
649 | 4 | } else { |
650 | 4 | candidates.push(new_hyp); |
651 | 4 | } |
652 | | } |
653 | | } |
654 | | |
655 | | // Select top num_beams hypotheses by normalized score |
656 | 3 | candidates1 .sort_by1 (|a, b| { |
657 | 3 | let score_a = a.normalized_score(self.config.length_penalty); |
658 | 3 | let score_b = b.normalized_score(self.config.length_penalty); |
659 | 3 | score_b |
660 | 3 | .partial_cmp(&score_a) |
661 | 3 | .unwrap_or(std::cmp::Ordering::Equal) |
662 | 3 | }); |
663 | | |
664 | 1 | self.hypotheses = candidates.into_iter().take(self.config.num_beams).collect(); |
665 | 1 | } |
666 | | |
667 | | /// Check if search should stop |
668 | 4 | pub fn should_stop(&self) -> bool { |
669 | 4 | if self.config.early_stopping && self.finished2 .len() >= self.config.num_beams { |
670 | 1 | return true; |
671 | 3 | } |
672 | 3 | self.hypotheses.is_empty() || self.hypotheses.iter()2 .all2 (|h| h.finished) |
673 | 4 | } |
674 | | |
675 | | /// Get best completed hypotheses |
676 | 0 | pub fn best_hypotheses(&self) -> Vec<BeamHypothesis> { |
677 | 0 | let mut all: Vec<_> = self |
678 | 0 | .finished |
679 | 0 | .iter() |
680 | 0 | .chain(self.hypotheses.iter()) |
681 | 0 | .cloned() |
682 | 0 | .collect(); |
683 | 0 | all.sort_by(|a, b| { |
684 | 0 | let score_a = a.normalized_score(self.config.length_penalty); |
685 | 0 | let score_b = b.normalized_score(self.config.length_penalty); |
686 | 0 | score_b |
687 | 0 | .partial_cmp(&score_a) |
688 | 0 | .unwrap_or(std::cmp::Ordering::Equal) |
689 | 0 | }); |
690 | 0 | all.into_iter().take(self.config.num_return).collect() |
691 | 0 | } |
692 | | } |
693 | | |
694 | | /// Streaming generation callback type |
695 | | /// |
696 | | /// The callback receives: |
697 | | /// - token_id: The generated token ID |
698 | | /// - token_text: Optional decoded text for the token |
699 | | /// - is_final: Whether this is the last token |
700 | | /// |
701 | | /// Returns `true` to continue, `false` to stop generation |
702 | | pub type StreamCallback = Box<dyn FnMut(usize, Option<&str>, bool) -> bool + Send>; |
703 | | |
704 | | /// Streaming generation state |
705 | | #[derive(Debug)] |
706 | | pub struct StreamingGenerator { |
707 | | /// Tokens generated so far |
708 | | pub tokens: Vec<usize>, |
709 | | /// Generated text so far |
710 | | pub text: String, |
711 | | /// Whether generation is complete |
712 | | pub finished: bool, |
713 | | /// Total tokens generated |
714 | | pub total_tokens: usize, |
715 | | } |
716 | | |
717 | | impl StreamingGenerator { |
718 | | /// Create new streaming generator |
719 | 8 | pub fn new() -> Self { |
720 | 8 | Self { |
721 | 8 | tokens: Vec::new(), |
722 | 8 | text: String::new(), |
723 | 8 | finished: false, |
724 | 8 | total_tokens: 0, |
725 | 8 | } |
726 | 8 | } |
727 | | |
728 | | /// Add a generated token |
729 | 15 | pub fn add_token(&mut self, token_id: usize, token_text: Option<&str>) { |
730 | 15 | self.tokens.push(token_id); |
731 | 15 | if let Some(text9 ) = token_text { |
732 | 9 | self.text.push_str(text); |
733 | 9 | }6 |
734 | 15 | self.total_tokens += 1; |
735 | 15 | } |
736 | | |
737 | | /// Mark generation as finished |
738 | 1 | pub fn finish(&mut self) { |
739 | 1 | self.finished = true; |
740 | 1 | } |
741 | | |
742 | | /// Get current token count |
743 | 4 | pub fn token_count(&self) -> usize { |
744 | 4 | self.total_tokens |
745 | 4 | } |
746 | | } |
747 | | |
748 | | impl Default for StreamingGenerator { |
749 | 1 | fn default() -> Self { |
750 | 1 | Self::new() |
751 | 1 | } |
752 | | } |
753 | | |
754 | | /// Extended generation configuration with advanced sampling options |
755 | | #[derive(Debug, Clone, Default)] |
756 | | pub struct AdvancedGenerationConfig { |
757 | | /// Base generation config |
758 | | pub base: GenerationConfig, |
759 | | /// Stop sequence detector |
760 | | pub stop_detector: Option<StopSequenceDetector>, |
761 | | /// Repetition penalty config |
762 | | pub repetition_penalty: Option<RepetitionPenaltyConfig>, |
763 | | /// Presence/frequency penalties |
764 | | pub presence_frequency: Option<PresenceFrequencyPenalty>, |
765 | | /// Logit bias |
766 | | pub logit_bias: Option<LogitBias>, |
767 | | } |
768 | | |
769 | | impl AdvancedGenerationConfig { |
770 | | /// Create with base config |
771 | 2 | pub fn new(base: GenerationConfig) -> Self { |
772 | 2 | Self { |
773 | 2 | base, |
774 | 2 | ..Default::default() |
775 | 2 | } |
776 | 2 | } |
777 | | |
778 | | /// Add stop sequences |
779 | | #[must_use] |
780 | 1 | pub fn with_stop_sequences(mut self, stops: Vec<String>) -> Self { |
781 | 1 | self.stop_detector = Some(StopSequenceDetector::new().with_stop_strings(stops)); |
782 | 1 | self |
783 | 1 | } |
784 | | |
785 | | /// Add repetition penalty |
786 | | #[must_use] |
787 | 2 | pub fn with_repetition_penalty(mut self, penalty: f32) -> Self { |
788 | 2 | self.repetition_penalty = Some(RepetitionPenaltyConfig::new(penalty)); |
789 | 2 | self |
790 | 2 | } |
791 | | |
792 | | /// Add presence/frequency penalties |
793 | | #[must_use] |
794 | 2 | pub fn with_presence_frequency(mut self, presence: f32, frequency: f32) -> Self { |
795 | 2 | self.presence_frequency = Some(PresenceFrequencyPenalty::new(presence, frequency)); |
796 | 2 | self |
797 | 2 | } |
798 | | |
799 | | /// Add logit bias |
800 | | #[must_use] |
801 | 2 | pub fn with_logit_bias(mut self, bias: LogitBias) -> Self { |
802 | 2 | self.logit_bias = Some(bias); |
803 | 2 | self |
804 | 2 | } |
805 | | } |
806 | | |
807 | | /// Apply all configured penalties and biases to logits |
808 | | /// |
809 | | /// # Arguments |
810 | | /// |
811 | | /// * `logits` - Raw logits from model |
812 | | /// * `context_tokens` - Previously generated tokens |
813 | | /// * `config` - Advanced generation configuration |
814 | | /// |
815 | | /// # Returns |
816 | | /// |
817 | | /// Logits with all penalties applied |
818 | 2 | pub fn apply_all_penalties( |
819 | 2 | logits: &Tensor<f32>, |
820 | 2 | context_tokens: &[usize], |
821 | 2 | config: &AdvancedGenerationConfig, |
822 | 2 | ) -> Tensor<f32> { |
823 | 2 | let mut result = logits.clone(); |
824 | | |
825 | | // Apply repetition penalty |
826 | 2 | if let Some(ref rep_config1 ) = config.repetition_penalty { |
827 | 1 | result = apply_repetition_penalty(&result, context_tokens, rep_config); |
828 | 1 | } |
829 | | |
830 | | // Apply presence/frequency penalty |
831 | 2 | if let Some(ref pf_config1 ) = config.presence_frequency { |
832 | 1 | result = apply_presence_frequency_penalty(&result, context_tokens, pf_config); |
833 | 1 | } |
834 | | |
835 | | // Apply logit bias |
836 | 2 | if let Some(ref bias1 ) = config.logit_bias { |
837 | 1 | result = apply_logit_bias(&result, bias); |
838 | 1 | } |
839 | | |
840 | 2 | result |
841 | 2 | } |
842 | | |
843 | | // ============================================================================ |
844 | | // Dynamic Temperature (temp_ext) - Entropy-based temperature adjustment |
845 | | // ============================================================================ |
846 | | |
847 | | /// Configuration for dynamic temperature (temp_ext) |
848 | | /// |
849 | | /// Adjusts temperature based on the entropy of the probability distribution. |
850 | | /// When entropy is low (confident), uses higher temperature to increase diversity. |
851 | | /// When entropy is high (uncertain), uses lower temperature to focus on likely tokens. |
852 | | /// |
853 | | /// Reference: llama.cpp `llama_sampler_init_temp_ext` |
854 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
855 | | pub struct DynTempConfig { |
856 | | /// Base temperature |
857 | | pub temp: f32, |
858 | | /// Range around base temperature (min = temp - delta, max = temp + delta) |
859 | | pub delta: f32, |
860 | | /// Exponent for entropy mapping (higher = more aggressive adjustment) |
861 | | pub exponent: f32, |
862 | | } |
863 | | |
864 | | impl Default for DynTempConfig { |
865 | 1 | fn default() -> Self { |
866 | 1 | Self { |
867 | 1 | temp: 1.0, |
868 | 1 | delta: 0.0, |
869 | 1 | exponent: 1.0, |
870 | 1 | } |
871 | 1 | } |
872 | | } |
873 | | |
874 | | impl DynTempConfig { |
875 | | /// Create a new dynamic temperature config |
876 | 7 | pub fn new(temp: f32, delta: f32, exponent: f32) -> Self { |
877 | 7 | Self { |
878 | 7 | temp, |
879 | 7 | delta, |
880 | 7 | exponent, |
881 | 7 | } |
882 | 7 | } |
883 | | |
884 | | /// Create with just temperature (no dynamic adjustment) |
885 | 2 | pub fn static_temp(temp: f32) -> Self { |
886 | 2 | Self { |
887 | 2 | temp, |
888 | 2 | delta: 0.0, |
889 | 2 | exponent: 1.0, |
890 | 2 | } |
891 | 2 | } |
892 | | } |
893 | | |
894 | | /// Apply dynamic temperature based on entropy |
895 | | /// |
896 | | /// The algorithm: |
897 | | /// 1. Calculate max possible entropy: -log(1/n) |
898 | | /// 2. Calculate actual entropy: -sum(p * log(p)) |
899 | | /// 3. Normalize entropy to [0, 1] |
900 | | /// 4. Map to temperature: min_temp + (max_temp - min_temp) * pow(norm_entropy, exponent) |
901 | | /// 5. Apply calculated temperature to logits |
902 | | /// |
903 | | /// # Arguments |
904 | | /// |
905 | | /// * `logits` - Raw logits from model |
906 | | /// * `config` - Dynamic temperature configuration |
907 | | /// |
908 | | /// # Returns |
909 | | /// |
910 | | /// Logits with dynamic temperature applied |
911 | 6 | pub fn apply_dynamic_temperature(logits: &Tensor<f32>, config: &DynTempConfig) -> Tensor<f32> { |
912 | | // If no delta, just apply static temperature |
913 | 6 | if config.delta <= 0.0 { |
914 | 1 | return apply_temperature(logits, config.temp).unwrap_or_else(|_| logits0 .clone0 ()); |
915 | 5 | } |
916 | | |
917 | 5 | let data = logits.data(); |
918 | 5 | if data.len() <= 1 { |
919 | 1 | return logits.clone(); |
920 | 4 | } |
921 | | |
922 | | // Calculate softmax probabilities |
923 | 4 | let max_logit = data.iter().copied().fold(f32::NEG_INFINITY, f32::max); |
924 | 20 | let exp_sum4 : f324 = data4 .iter4 ().map4 (|x| (x - max_logit).exp()).sum4 (); |
925 | 4 | let probs: Vec<f32> = data |
926 | 4 | .iter() |
927 | 20 | .map4 (|x| (x - max_logit).exp() / exp_sum) |
928 | 4 | .collect(); |
929 | | |
930 | | // Calculate maximum possible entropy: -log(1/n) = log(n) |
931 | 4 | let max_entropy = (data.len() as f32).ln(); |
932 | | |
933 | | // Calculate actual entropy: -sum(p * log(p)) |
934 | 4 | let entropy: f32 = probs |
935 | 4 | .iter() |
936 | 20 | .filter4 (|&&p| p > 0.0) |
937 | 20 | .map4 (|&p| -p * p.ln()) |
938 | 4 | .sum(); |
939 | | |
940 | | // Normalize entropy to [0, 1] |
941 | 4 | let normalized_entropy = if max_entropy > 0.0 { |
942 | 4 | (entropy / max_entropy).clamp(0.0, 1.0) |
943 | | } else { |
944 | 0 | 0.0 |
945 | | }; |
946 | | |
947 | | // Calculate dynamic temperature |
948 | 4 | let min_temp = (config.temp - config.delta).max(0.0); |
949 | 4 | let max_temp = config.temp + config.delta; |
950 | 4 | let dyn_temp = min_temp + (max_temp - min_temp) * normalized_entropy.powf(config.exponent); |
951 | | |
952 | | // Apply calculated temperature |
953 | 4 | apply_temperature(logits, dyn_temp).unwrap_or_else(|_| logits0 .clone0 ()) |
954 | 6 | } |
955 | | |
956 | | // ============================================================================ |
957 | | // Infill/FIM Sampler - Fill-in-the-Middle for code completion |
958 | | // ============================================================================ |
959 | | |
960 | | /// Configuration for infill/FIM (Fill-in-the-Middle) sampling |
961 | | /// |
962 | | /// Used for code completion where the model generates text to fill a gap. |
963 | | /// Handles EOG (End-of-Generation) tokens specially to determine when to stop. |
964 | | /// |
965 | | /// Reference: llama.cpp `llama_sampler_init_infill` |
966 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
967 | | pub struct InfillConfig { |
968 | | /// EOG (End-of-Generation) token IDs |
969 | | pub eog_tokens: Vec<usize>, |
970 | | /// Ratio threshold: if 3*p_eog*n > p_txt, force EOG |
971 | | pub eog_ratio_threshold: f32, |
972 | | } |
973 | | |
974 | | impl Default for InfillConfig { |
975 | 2 | fn default() -> Self { |
976 | 2 | Self { |
977 | 2 | eog_tokens: vec![], |
978 | 2 | eog_ratio_threshold: 3.0, |
979 | 2 | } |
980 | 2 | } |
981 | | } |
982 | | |
983 | | impl InfillConfig { |
984 | | /// Create a new infill config with EOG tokens |
985 | 7 | pub fn new(eog_tokens: Vec<usize>) -> Self { |
986 | 7 | Self { |
987 | 7 | eog_tokens, |
988 | 7 | eog_ratio_threshold: 3.0, |
989 | 7 | } |
990 | 7 | } |
991 | | |
992 | | /// Set the EOG ratio threshold |
993 | | #[must_use] |
994 | 1 | pub fn with_threshold(mut self, threshold: f32) -> Self { |
995 | 1 | self.eog_ratio_threshold = threshold; |
996 | 1 | self |
997 | 1 | } |
998 | | } |
999 | | |
1000 | | /// Result of infill sampling |
1001 | | #[derive(Debug, Clone)] |
1002 | | pub struct InfillResult { |
1003 | | /// Modified logits (with non-EOG tokens potentially zeroed) |
1004 | | pub logits: Tensor<f32>, |
1005 | | /// Whether to force EOG token |
1006 | | pub force_eog: bool, |
1007 | | /// Probability sum of text tokens |
1008 | | pub p_txt: f32, |
1009 | | /// Probability sum of EOG tokens |
1010 | | pub p_eog: f32, |
1011 | | } |
1012 | | |
1013 | | /// Apply infill sampling logic |
1014 | | /// |
1015 | | /// This determines if the model should stop generating (emit EOG) based on |
1016 | | /// the relative probabilities of EOG vs text tokens. |
1017 | | /// |
1018 | | /// # Arguments |
1019 | | /// |
1020 | | /// * `logits` - Raw logits from model |
1021 | | /// * `config` - Infill configuration |
1022 | | /// |
1023 | | /// # Returns |
1024 | | /// |
1025 | | /// `InfillResult` with modified logits and EOG decision |
1026 | 5 | pub fn apply_infill_sampling(logits: &Tensor<f32>, config: &InfillConfig) -> InfillResult { |
1027 | 5 | let data = logits.data(); |
1028 | 5 | if data.is_empty() || config.eog_tokens.is_empty() { |
1029 | 1 | return InfillResult { |
1030 | 1 | logits: logits.clone(), |
1031 | 1 | force_eog: false, |
1032 | 1 | p_txt: 1.0, |
1033 | 1 | p_eog: 0.0, |
1034 | 1 | }; |
1035 | 4 | } |
1036 | | |
1037 | | // Calculate softmax probabilities |
1038 | 4 | let max_logit = data.iter().copied().fold(f32::NEG_INFINITY, f32::max); |
1039 | 20 | let exp_sum4 : f324 = data4 .iter4 ().map4 (|x| (x - max_logit).exp()).sum4 (); |
1040 | 4 | let probs: Vec<f32> = data |
1041 | 4 | .iter() |
1042 | 20 | .map4 (|x| (x - max_logit).exp() / exp_sum) |
1043 | 4 | .collect(); |
1044 | | |
1045 | | // Calculate p_eog and p_txt |
1046 | 4 | let mut p_eog: f32 = 0.0; |
1047 | 4 | let mut p_txt: f32 = 0.0; |
1048 | | |
1049 | 20 | for (i, &p) in probs.iter()4 .enumerate4 () { |
1050 | 20 | if config.eog_tokens.contains(&i) { |
1051 | 5 | p_eog += p; |
1052 | 15 | } else { |
1053 | 15 | p_txt += p; |
1054 | 15 | } |
1055 | | } |
1056 | | |
1057 | | // Check if we should force EOG |
1058 | | // Condition: 3 * p_eog * n > p_txt |
1059 | 4 | let n = data.len() as f32; |
1060 | 4 | let force_eog = config.eog_ratio_threshold * p_eog * n > p_txt; |
1061 | | |
1062 | 4 | if force_eog { |
1063 | | // Keep only EOG tokens |
1064 | 3 | let mut new_data = vec![f32::NEG_INFINITY; data.len()]; |
1065 | 3 | let mut eog_sum = 0.0; |
1066 | | |
1067 | 7 | for &eog_id4 in &config.eog_tokens { |
1068 | 4 | if eog_id < data.len() { |
1069 | 4 | new_data[eog_id] = data[eog_id]; |
1070 | 4 | eog_sum += probs[eog_id]; |
1071 | 4 | }0 |
1072 | | } |
1073 | | |
1074 | | // Renormalize EOG tokens |
1075 | 3 | if eog_sum > 0.0 { |
1076 | 7 | for &eog_id4 in &config.eog_tokens { |
1077 | 4 | if eog_id < data.len() && new_data[eog_id] > f32::NEG_INFINITY { |
1078 | 4 | // Convert back to logit scale |
1079 | 4 | let normalized_p = probs[eog_id] / eog_sum; |
1080 | 4 | new_data[eog_id] = normalized_p.ln(); |
1081 | 4 | }0 |
1082 | | } |
1083 | 0 | } |
1084 | | |
1085 | | InfillResult { |
1086 | 3 | logits: Tensor::from_vec(logits.shape().to_vec(), new_data) |
1087 | 3 | .unwrap_or_else(|_| logits0 .clone0 ()), |
1088 | | force_eog: true, |
1089 | 3 | p_txt, |
1090 | 3 | p_eog, |
1091 | | } |
1092 | | } else { |
1093 | 1 | InfillResult { |
1094 | 1 | logits: logits.clone(), |
1095 | 1 | force_eog: false, |
1096 | 1 | p_txt, |
1097 | 1 | p_eog, |
1098 | 1 | } |
1099 | | } |
1100 | 5 | } |
1101 | | |
1102 | | // ============================================================================ |
1103 | | // Sampler Chain - Composable sampler pipeline |
1104 | | // ============================================================================ |
1105 | | |
1106 | | /// Trait for samplers that can be chained together |
1107 | | pub trait Sampler: Send + Sync { |
1108 | | /// Get the sampler name |
1109 | | fn name(&self) -> &'static str; |
1110 | | |
1111 | | /// Apply the sampler to logits (in-place modification) |
1112 | | fn apply(&self, logits: &mut Tensor<f32>, context: &SamplerContext); |
1113 | | |
1114 | | /// Clone the sampler (for use in chains) |
1115 | | fn clone_box(&self) -> Box<dyn Sampler>; |
1116 | | } |
1117 | | |
1118 | | /// Context passed to samplers during application |
1119 | | #[derive(Debug, Clone, Default)] |
1120 | | pub struct SamplerContext { |
1121 | | /// Previously generated tokens |
1122 | | pub tokens: Vec<usize>, |
1123 | | /// Random value for stochastic samplers [0, 1) |
1124 | | pub rng_value: f32, |
1125 | | /// Current generation step |
1126 | | pub step: usize, |
1127 | | } |
1128 | | |
1129 | | impl SamplerContext { |
1130 | | /// Create a new sampler context |
1131 | 6 | pub fn new() -> Self { |
1132 | 6 | Self::default() |
1133 | 6 | } |
1134 | | |
1135 | | /// Set tokens |
1136 | | #[must_use] |
1137 | 1 | pub fn with_tokens(mut self, tokens: Vec<usize>) -> Self { |
1138 | 1 | self.tokens = tokens; |
1139 | 1 | self |
1140 | 1 | } |
1141 | | |
1142 | | /// Set RNG value |
1143 | | #[must_use] |
1144 | 1 | pub fn with_rng(mut self, rng_value: f32) -> Self { |
1145 | 1 | self.rng_value = rng_value; |
1146 | 1 | self |
1147 | 1 | } |
1148 | | |
1149 | | /// Set step |
1150 | | #[must_use] |
1151 | 1 | pub fn with_step(mut self, step: usize) -> Self { |
1152 | 1 | self.step = step; |
1153 | 1 | self |
1154 | 1 | } |
1155 | | } |
1156 | | |
1157 | | /// A chain of samplers applied in sequence |
1158 | | pub struct SamplerChain { |
1159 | | samplers: Vec<Box<dyn Sampler>>, |
1160 | | } |
1161 | | |
1162 | | impl Default for SamplerChain { |
1163 | 1 | fn default() -> Self { |
1164 | 1 | Self::new() |
1165 | 1 | } |
1166 | | } |
1167 | | |
1168 | | impl SamplerChain { |
1169 | | /// Create a new empty sampler chain |
1170 | 9 | pub fn new() -> Self { |
1171 | 9 | Self { samplers: vec![] } |
1172 | 9 | } |
1173 | | |
1174 | | /// Add a sampler to the chain (builder pattern) |
1175 | | #[must_use] |
1176 | 11 | pub fn with_sampler<S: Sampler + 'static>(mut self, sampler: S) -> Self { |
1177 | 11 | self.samplers.push(Box::new(sampler)); |
1178 | 11 | self |
1179 | 11 | } |
1180 | | |
1181 | | /// Push a boxed sampler to the chain |
1182 | 1 | pub fn push(&mut self, sampler: Box<dyn Sampler>) { |
1183 | 1 | self.samplers.push(sampler); |
1184 | 1 | } |
1185 | | |
1186 | | /// Get the number of samplers in the chain |
1187 | 5 | pub fn len(&self) -> usize { |
1188 | 5 | self.samplers.len() |
1189 | 5 | } |
1190 | | |
1191 | | /// Check if the chain is empty |
1192 | 2 | pub fn is_empty(&self) -> bool { |
1193 | 2 | self.samplers.is_empty() |
1194 | 2 | } |
1195 | | |
1196 | | /// Get sampler names in order |
1197 | 3 | pub fn names(&self) -> Vec<&'static str> { |
1198 | 6 | self.samplers.iter()3 .map3 (|s| s.name()).collect3 () |
1199 | 3 | } |
1200 | | |
1201 | | /// Apply all samplers in sequence |
1202 | 3 | pub fn apply(&self, logits: &mut Tensor<f32>, context: &SamplerContext) { |
1203 | 8 | for sampler5 in &self.samplers { |
1204 | 5 | sampler.apply(logits, context); |
1205 | 5 | } |
1206 | 3 | } |
1207 | | |
1208 | | /// Sample a token after applying all samplers |
1209 | | /// |
1210 | | /// # Errors |
1211 | | /// |
1212 | | /// Returns error if sampling fails |
1213 | 2 | pub fn sample(&self, logits: &Tensor<f32>, context: &SamplerContext) -> Result<usize> { |
1214 | 2 | let mut modified = logits.clone(); |
1215 | 2 | self.apply(&mut modified, context); |
1216 | 2 | sample_greedy(&modified) |
1217 | 2 | } |
1218 | | } |
1219 | | |
1220 | | impl Clone for SamplerChain { |
1221 | 1 | fn clone(&self) -> Self { |
1222 | | Self { |
1223 | 2 | samplers: self.samplers.iter()1 .map1 (|s| s.clone_box()).collect1 (), |
1224 | | } |
1225 | 1 | } |
1226 | | } |
1227 | | |
1228 | | // Concrete sampler implementations for the chain |
1229 | | |
1230 | | /// Temperature sampler |
1231 | | #[derive(Debug, Clone)] |
1232 | | pub struct TemperatureSampler { |
1233 | | /// Temperature value (1.0 = no change) |
1234 | | pub temp: f32, |
1235 | | } |
1236 | | |
1237 | | impl TemperatureSampler { |
1238 | | /// Create a new temperature sampler |
1239 | 8 | pub fn new(temp: f32) -> Self { |
1240 | 8 | Self { temp } |
1241 | 8 | } |
1242 | | } |
1243 | | |
1244 | | impl Sampler for TemperatureSampler { |
1245 | 4 | fn name(&self) -> &'static str { |
1246 | 4 | "temperature" |
1247 | 4 | } |
1248 | | |
1249 | 3 | fn apply(&self, logits: &mut Tensor<f32>, _context: &SamplerContext) { |
1250 | 3 | if let Ok(result) = apply_temperature(logits, self.temp) { |
1251 | 3 | *logits = result; |
1252 | 3 | }0 |
1253 | 3 | } |
1254 | | |
1255 | 1 | fn clone_box(&self) -> Box<dyn Sampler> { |
1256 | 1 | Box::new(self.clone()) |
1257 | 1 | } |
1258 | | } |
1259 | | |
1260 | | /// Dynamic temperature sampler |
1261 | | #[derive(Debug, Clone)] |
1262 | | pub struct DynTempSampler { |
1263 | | /// Dynamic temperature configuration |
1264 | | pub config: DynTempConfig, |
1265 | | } |
1266 | | |
1267 | | impl DynTempSampler { |
1268 | | /// Create a new dynamic temperature sampler |
1269 | 1 | pub fn new(config: DynTempConfig) -> Self { |
1270 | 1 | Self { config } |
1271 | 1 | } |
1272 | | } |
1273 | | |
1274 | | impl Sampler for DynTempSampler { |
1275 | 1 | fn name(&self) -> &'static str { |
1276 | 1 | "dyn_temp" |
1277 | 1 | } |
1278 | | |
1279 | 0 | fn apply(&self, logits: &mut Tensor<f32>, _context: &SamplerContext) { |
1280 | 0 | *logits = apply_dynamic_temperature(logits, &self.config); |
1281 | 0 | } |
1282 | | |
1283 | 0 | fn clone_box(&self) -> Box<dyn Sampler> { |
1284 | 0 | Box::new(self.clone()) |
1285 | 0 | } |
1286 | | } |
1287 | | |
1288 | | /// Top-K sampler |
1289 | | #[derive(Debug, Clone)] |
1290 | | pub struct TopKSampler { |
1291 | | /// Number of top tokens to consider |
1292 | | pub k: usize, |
1293 | | } |
1294 | | |
1295 | | impl TopKSampler { |
1296 | | /// Create a new top-k sampler |
1297 | 5 | pub fn new(k: usize) -> Self { |
1298 | 5 | Self { k } |
1299 | 5 | } |
1300 | | } |
1301 | | |
1302 | | impl Sampler for TopKSampler { |
1303 | 3 | fn name(&self) -> &'static str { |
1304 | 3 | "top_k" |
1305 | 3 | } |
1306 | | |
1307 | 2 | fn apply(&self, logits: &mut Tensor<f32>, _context: &SamplerContext) { |
1308 | | // Apply top-k by zeroing out tokens outside top-k |
1309 | 2 | let data = logits.data(); |
1310 | 2 | let mut indexed: Vec<(usize, f32)> = data.iter().copied().enumerate().collect(); |
1311 | 54 | indexed2 .sort_by2 (|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); |
1312 | | |
1313 | 2 | let mut new_data = vec![f32::NEG_INFINITY; data.len()]; |
1314 | 12 | for (idx, logit) in indexed.iter()2 .take2 (self.k2 ) { |
1315 | 12 | new_data[*idx] = *logit; |
1316 | 12 | } |
1317 | | |
1318 | 2 | if let Ok(result) = Tensor::from_vec(logits.shape().to_vec(), new_data) { |
1319 | 2 | *logits = result; |
1320 | 2 | }0 |
1321 | 2 | } |
1322 | | |
1323 | 1 | fn clone_box(&self) -> Box<dyn Sampler> { |
1324 | 1 | Box::new(self.clone()) |
1325 | 1 | } |
1326 | | } |
1327 | | |
1328 | | /// Top-P (nucleus) sampler |
1329 | | #[derive(Debug, Clone)] |
1330 | | pub struct TopPSampler { |
1331 | | /// Cumulative probability threshold (0.0 to 1.0) |
1332 | | pub p: f32, |
1333 | | } |
1334 | | |
1335 | | impl TopPSampler { |
1336 | | /// Create a new top-p sampler |
1337 | 4 | pub fn new(p: f32) -> Self { |
1338 | 4 | Self { p } |
1339 | 4 | } |
1340 | | } |
1341 | | |
1342 | | impl Sampler for TopPSampler { |
1343 | 2 | fn name(&self) -> &'static str { |
1344 | 2 | "top_p" |
1345 | 2 | } |
1346 | | |
1347 | 2 | fn apply(&self, logits: &mut Tensor<f32>, _context: &SamplerContext) { |
1348 | 2 | let data = logits.data(); |
1349 | | |
1350 | | // Calculate softmax |
1351 | 2 | let max_logit = data.iter().copied().fold(f32::NEG_INFINITY, f32::max); |
1352 | 15 | let exp_sum2 : f322 = data2 .iter2 ().map2 (|x| (x - max_logit).exp()).sum2 (); |
1353 | 2 | let mut indexed: Vec<(usize, f32, f32)> = data |
1354 | 2 | .iter() |
1355 | 2 | .enumerate() |
1356 | 15 | .map2 (|(i, &logit)| (i, logit, (logit - max_logit).exp() / exp_sum)) |
1357 | 2 | .collect(); |
1358 | | |
1359 | | // Sort by probability descending |
1360 | 50 | indexed2 .sort_by2 (|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); |
1361 | | |
1362 | | // Find cutoff |
1363 | 2 | let mut cumsum = 0.0; |
1364 | 2 | let mut cutoff_idx = indexed.len(); |
1365 | 4 | for (i, (_, _, prob)) in indexed.iter()2 .enumerate2 () { |
1366 | 4 | cumsum += prob; |
1367 | 4 | if cumsum >= self.p { |
1368 | 2 | cutoff_idx = i + 1; |
1369 | 2 | break; |
1370 | 2 | } |
1371 | | } |
1372 | | |
1373 | | // Zero out tokens below cutoff |
1374 | 2 | let mut new_data = vec![f32::NEG_INFINITY; data.len()]; |
1375 | 4 | for (idx, logit, _) in indexed.iter()2 .take2 (cutoff_idx2 ) { |
1376 | 4 | new_data[*idx] = *logit; |
1377 | 4 | } |
1378 | | |
1379 | 2 | if let Ok(result) = Tensor::from_vec(logits.shape().to_vec(), new_data) { |
1380 | 2 | *logits = result; |
1381 | 2 | }0 |
1382 | 2 | } |
1383 | | |
1384 | 0 | fn clone_box(&self) -> Box<dyn Sampler> { |
1385 | 0 | Box::new(self.clone()) |
1386 | 0 | } |
1387 | | } |
1388 | | |
1389 | | /// Repetition penalty sampler |
1390 | | #[derive(Debug, Clone)] |
1391 | | pub struct RepetitionPenaltySampler { |
1392 | | /// Repetition penalty configuration |
1393 | | pub config: RepetitionPenaltyConfig, |
1394 | | } |
1395 | | |
1396 | | impl RepetitionPenaltySampler { |
1397 | | /// Create a new repetition penalty sampler |
1398 | 1 | pub fn new(config: RepetitionPenaltyConfig) -> Self { |
1399 | 1 | Self { config } |
1400 | 1 | } |
1401 | | } |
1402 | | |
1403 | | impl Sampler for RepetitionPenaltySampler { |
1404 | 1 | fn name(&self) -> &'static str { |
1405 | 1 | "repetition_penalty" |
1406 | 1 | } |
1407 | | |
1408 | 0 | fn apply(&self, logits: &mut Tensor<f32>, context: &SamplerContext) { |
1409 | 0 | *logits = apply_repetition_penalty(logits, &context.tokens, &self.config); |
1410 | 0 | } |
1411 | | |
1412 | 0 | fn clone_box(&self) -> Box<dyn Sampler> { |
1413 | 0 | Box::new(self.clone()) |
1414 | 0 | } |
1415 | | } |
1416 | | |
1417 | | /// Infill sampler |
1418 | | #[derive(Debug, Clone)] |
1419 | | pub struct InfillSampler { |
1420 | | /// Infill/FIM configuration |
1421 | | pub config: InfillConfig, |
1422 | | } |
1423 | | |
1424 | | impl InfillSampler { |
1425 | | /// Create a new infill sampler |
1426 | 1 | pub fn new(config: InfillConfig) -> Self { |
1427 | 1 | Self { config } |
1428 | 1 | } |
1429 | | } |
1430 | | |
1431 | | impl Sampler for InfillSampler { |
1432 | 1 | fn name(&self) -> &'static str { |
1433 | 1 | "infill" |
1434 | 1 | } |
1435 | | |
1436 | 0 | fn apply(&self, logits: &mut Tensor<f32>, _context: &SamplerContext) { |
1437 | 0 | let result = apply_infill_sampling(logits, &self.config); |
1438 | 0 | *logits = result.logits; |
1439 | 0 | } |
1440 | | |
1441 | 0 | fn clone_box(&self) -> Box<dyn Sampler> { |
1442 | 0 | Box::new(self.clone()) |
1443 | 0 | } |
1444 | | } |
1445 | | |
1446 | | // ============================================================================= |
1447 | | // LogitProcessor Trait (RLZR-GEN-001) |
1448 | | // ============================================================================= |
1449 | | // |
1450 | | // Composable logit processing for text generation pipelines. |
1451 | | // Based on HuggingFace Transformers LogitsProcessor pattern. |
1452 | | // |
1453 | | // References: |
1454 | | // - Holtzman et al. (2020) "The Curious Case of Neural Text Degeneration" |
1455 | | // - Wolf et al. (2020) "Transformers: State-of-the-Art NLP" |
1456 | | // ============================================================================= |
1457 | | |
1458 | | /// Context available during logit processing |
1459 | | /// |
1460 | | /// Provides information about the current generation state to processors. |
1461 | | #[derive(Debug, Clone)] |
1462 | | pub struct LogitProcessorContext<'a> { |
1463 | | /// Previously generated tokens (including initial prompt) |
1464 | | pub tokens: &'a [u32], |
1465 | | /// Current generation step (0-indexed, after initial tokens) |
1466 | | pub step: usize, |
1467 | | /// Vocabulary size |
1468 | | pub n_vocab: usize, |
1469 | | } |
1470 | | |
1471 | | impl<'a> LogitProcessorContext<'a> { |
1472 | | /// Create a new context |
1473 | | #[must_use] |
1474 | 21 | pub fn new(tokens: &'a [u32], step: usize, n_vocab: usize) -> Self { |
1475 | 21 | Self { |
1476 | 21 | tokens, |
1477 | 21 | step, |
1478 | 21 | n_vocab, |
1479 | 21 | } |
1480 | 21 | } |
1481 | | } |
1482 | | |
1483 | | /// Logit processor trait for composable pre-sampling transforms |
1484 | | /// |
1485 | | /// Processors are applied in order before sampling. They can: |
1486 | | /// - Set logits to -inf to suppress tokens |
1487 | | /// - Add penalties (repetition, length) |
1488 | | /// - Scale logits (temperature) |
1489 | | /// |
1490 | | /// # Example |
1491 | | /// |
1492 | | /// ```rust,ignore |
1493 | | /// use realizar::generate::{LogitProcessor, LogitProcessorContext}; |
1494 | | /// |
1495 | | /// struct MyProcessor; |
1496 | | /// |
1497 | | /// impl LogitProcessor for MyProcessor { |
1498 | | /// fn process(&self, logits: &mut [f32], ctx: &LogitProcessorContext) { |
1499 | | /// // Suppress token 0 |
1500 | | /// logits[0] = f32::NEG_INFINITY; |
1501 | | /// } |
1502 | | /// } |
1503 | | /// ``` |
1504 | | pub trait LogitProcessor: Send + Sync { |
1505 | | /// Process logits in-place before sampling |
1506 | | /// |
1507 | | /// # Arguments |
1508 | | /// |
1509 | | /// * `logits` - Mutable slice of logits to modify |
1510 | | /// * `ctx` - Context with token history and generation state |
1511 | | fn process(&self, logits: &mut [f32], ctx: &LogitProcessorContext); |
1512 | | |
1513 | | /// Human-readable name for debugging and tracing |
1514 | 0 | fn name(&self) -> &'static str { |
1515 | 0 | "unnamed" |
1516 | 0 | } |
1517 | | } |
1518 | | |
1519 | | /// Suppress specific tokens by setting their logits to -inf |
1520 | | /// |
1521 | | /// Use this to prevent certain tokens from being generated, such as: |
1522 | | /// - Special tokens (SOT, PREV, SOLM in Whisper) |
1523 | | /// - Profanity or sensitive content |
1524 | | /// - Invalid tokens for the current context |
1525 | | #[derive(Debug, Clone)] |
1526 | | pub struct TokenSuppressor { |
1527 | | /// Token IDs to suppress |
1528 | | suppress_ids: Vec<u32>, |
1529 | | } |
1530 | | |
1531 | | impl TokenSuppressor { |
1532 | | /// Create a new token suppressor |
1533 | | /// |
1534 | | /// # Arguments |
1535 | | /// |
1536 | | /// * `suppress_ids` - Token IDs to suppress (set to -inf) |
1537 | | #[must_use] |
1538 | 9 | pub fn new(suppress_ids: Vec<u32>) -> Self { |
1539 | 9 | Self { suppress_ids } |
1540 | 9 | } |
1541 | | |
1542 | | /// Create from a slice of token IDs |
1543 | | #[must_use] |
1544 | 0 | pub fn from_slice(suppress_ids: &[u32]) -> Self { |
1545 | 0 | Self { |
1546 | 0 | suppress_ids: suppress_ids.to_vec(), |
1547 | 0 | } |
1548 | 0 | } |
1549 | | } |
1550 | | |
1551 | | impl LogitProcessor for TokenSuppressor { |
1552 | 9 | fn process(&self, logits: &mut [f32], _ctx: &LogitProcessorContext) { |
1553 | 21 | for &token_id12 in &self.suppress_ids { |
1554 | 12 | if (token_id as usize) < logits.len() { |
1555 | 10 | logits[token_id as usize] = f32::NEG_INFINITY; |
1556 | 10 | }2 |
1557 | | } |
1558 | 9 | } |
1559 | | |
1560 | 2 | fn name(&self) -> &'static str { |
1561 | 2 | "token_suppressor" |
1562 | 2 | } |
1563 | | } |
1564 | | |
1565 | | /// Penalize repeated tokens to reduce repetitive generation |
1566 | | /// |
1567 | | /// Applies a penalty to tokens that have appeared in the recent context. |
1568 | | /// Penalty > 1.0 reduces probability, < 1.0 increases it. |
1569 | | /// |
1570 | | /// Based on: Keskar et al. (2019) "CTRL: A Conditional Transformer Language Model" |
1571 | | #[derive(Debug, Clone)] |
1572 | | pub struct RepetitionPenalty { |
1573 | | /// Penalty multiplier (> 1.0 to penalize, < 1.0 to encourage) |
1574 | | penalty: f32, |
1575 | | /// Look-back window size (0 = entire history) |
1576 | | window: usize, |
1577 | | } |
1578 | | |
1579 | | impl RepetitionPenalty { |
1580 | | /// Create a new repetition penalty processor |
1581 | | /// |
1582 | | /// # Arguments |
1583 | | /// |
1584 | | /// * `penalty` - Penalty multiplier (typical: 1.0-2.0) |
1585 | | /// * `window` - Look-back window (0 = use all tokens) |
1586 | | #[must_use] |
1587 | 1 | pub fn new(penalty: f32, window: usize) -> Self { |
1588 | 1 | Self { penalty, window } |
1589 | 1 | } |
1590 | | |
1591 | | /// Create with default window (entire history) |
1592 | | #[must_use] |
1593 | 4 | pub fn with_penalty(penalty: f32) -> Self { |
1594 | 4 | Self { penalty, window: 0 } |
1595 | 4 | } |
1596 | | } |
1597 | | |
1598 | | impl LogitProcessor for RepetitionPenalty { |
1599 | 3 | fn process(&self, logits: &mut [f32], ctx: &LogitProcessorContext) { |
1600 | | // Determine which tokens to consider |
1601 | 3 | let tokens = if self.window > 0 && ctx.tokens1 .len() > self.window { |
1602 | 1 | &ctx.tokens[ctx.tokens.len() - self.window..] |
1603 | | } else { |
1604 | 2 | ctx.tokens |
1605 | | }; |
1606 | | |
1607 | | // Apply penalty to tokens that have appeared |
1608 | 9 | for &token_id6 in tokens { |
1609 | 6 | if (token_id as usize) < logits.len() { |
1610 | 6 | let logit = logits[token_id as usize]; |
1611 | | // Apply penalty: divide positive logits, multiply negative logits |
1612 | 6 | logits[token_id as usize] = if logit > 0.0 { |
1613 | 5 | logit / self.penalty |
1614 | | } else { |
1615 | 1 | logit * self.penalty |
1616 | | }; |
1617 | 0 | } |
1618 | | } |
1619 | 3 | } |
1620 | | |
1621 | 1 | fn name(&self) -> &'static str { |
1622 | 1 | "repetition_penalty" |
1623 | 1 | } |
1624 | | } |
1625 | | |
1626 | | /// Scale logits by temperature |
1627 | | /// |
1628 | | /// Temperature > 1.0 increases randomness (flatter distribution) |
1629 | | /// Temperature < 1.0 decreases randomness (sharper distribution) |
1630 | | /// Temperature = 1.0 has no effect |
1631 | | #[derive(Debug, Clone)] |
1632 | | pub struct TemperatureScaler { |
1633 | | /// Temperature value (must be > 0) |
1634 | | temperature: f32, |
1635 | | } |
1636 | | |
1637 | | impl TemperatureScaler { |
1638 | | /// Create a new temperature scaler |
1639 | | /// |
1640 | | /// # Arguments |
1641 | | /// |
1642 | | /// * `temperature` - Temperature value (> 0) |
1643 | | /// |
1644 | | /// # Panics |
1645 | | /// |
1646 | | /// Panics if temperature <= 0 |
1647 | | #[must_use] |
1648 | 5 | pub fn new(temperature: f32) -> Self { |
1649 | 5 | assert!(temperature > 0.0, "Temperature must be positive"1 ); |
1650 | 4 | Self { temperature } |
1651 | 4 | } |
1652 | | } |
1653 | | |
1654 | | impl LogitProcessor for TemperatureScaler { |
1655 | 3 | fn process(&self, logits: &mut [f32], _ctx: &LogitProcessorContext) { |
1656 | 3 | if (self.temperature - 1.0).abs() > 1e-6 { |
1657 | 6 | for logit in logits2 .iter_mut2 () { |
1658 | 6 | *logit /= self.temperature; |
1659 | 6 | } |
1660 | 1 | } |
1661 | 3 | } |
1662 | | |
1663 | 1 | fn name(&self) -> &'static str { |
1664 | 1 | "temperature_scaler" |
1665 | 1 | } |
1666 | | } |
1667 | | |
1668 | | /// Chain of logit processors applied in order |
1669 | | /// |
1670 | | /// Allows composing multiple processors into a single processing step. |
1671 | | #[derive(Default)] |
1672 | | pub struct LogitProcessorChain { |
1673 | | processors: Vec<Box<dyn LogitProcessor>>, |
1674 | | } |
1675 | | |
1676 | | impl LogitProcessorChain { |
1677 | | /// Create an empty processor chain |
1678 | | #[must_use] |
1679 | 9 | pub fn new() -> Self { |
1680 | 9 | Self { |
1681 | 9 | processors: Vec::new(), |
1682 | 9 | } |
1683 | 9 | } |
1684 | | |
1685 | | /// Add a processor to the chain (builder pattern) |
1686 | | #[must_use] |
1687 | 10 | pub fn with_processor<P: LogitProcessor + 'static>(mut self, processor: P) -> Self { |
1688 | 10 | self.processors.push(Box::new(processor)); |
1689 | 10 | self |
1690 | 10 | } |
1691 | | |
1692 | | /// Add a boxed processor to the chain (builder pattern) |
1693 | | #[must_use] |
1694 | 0 | pub fn with_boxed_processor(mut self, processor: Box<dyn LogitProcessor>) -> Self { |
1695 | 0 | self.processors.push(processor); |
1696 | 0 | self |
1697 | 0 | } |
1698 | | |
1699 | | /// Process logits through all processors in order |
1700 | 13 | pub fn process(&self, logits: &mut [f32], ctx: &LogitProcessorContext) { |
1701 | 21 | for processor8 in &self.processors { |
1702 | 8 | processor.process(logits, ctx); |
1703 | 8 | } |
1704 | 13 | } |
1705 | | |
1706 | | /// Get the number of processors in the chain |
1707 | | #[must_use] |
1708 | 2 | pub fn len(&self) -> usize { |
1709 | 2 | self.processors.len() |
1710 | 2 | } |
1711 | | |
1712 | | /// Check if the chain is empty |
1713 | | #[must_use] |
1714 | 2 | pub fn is_empty(&self) -> bool { |
1715 | 2 | self.processors.is_empty() |
1716 | 2 | } |
1717 | | |
1718 | | /// Get processor names for debugging |
1719 | | #[must_use] |
1720 | 1 | pub fn processor_names(&self) -> Vec<&str> { |
1721 | 3 | self.processors.iter()1 .map1 (|p| p.name()).collect1 () |
1722 | 1 | } |
1723 | | } |
1724 | | |
1725 | | impl LogitProcessor for LogitProcessorChain { |
1726 | 1 | fn process(&self, logits: &mut [f32], ctx: &LogitProcessorContext) { |
1727 | 1 | LogitProcessorChain::process(self, logits, ctx); |
1728 | 1 | } |
1729 | | |
1730 | 1 | fn name(&self) -> &'static str { |
1731 | 1 | "processor_chain" |
1732 | 1 | } |
1733 | | } |
1734 | | |
1735 | | /// Model trait for generation pipeline |
1736 | | /// |
1737 | | /// Implement this trait to use your model with GenerationPipeline. |
1738 | | pub trait GenerativeModel { |
1739 | | /// Forward pass producing logits for next token |
1740 | | /// |
1741 | | /// # Arguments |
1742 | | /// |
1743 | | /// * `tokens` - Current token sequence |
1744 | | /// |
1745 | | /// # Returns |
1746 | | /// |
1747 | | /// Logits for vocabulary (shape: [vocab_size]) |
1748 | | fn forward(&mut self, tokens: &[u32]) -> Result<Vec<f32>>; |
1749 | | |
1750 | | /// Get vocabulary size |
1751 | | fn vocab_size(&self) -> usize; |
1752 | | |
1753 | | /// Reset any cached state (e.g., KV cache) |
1754 | 0 | fn reset(&mut self) {} |
1755 | | } |
1756 | | |
1757 | | /// Generation pipeline with processor chain |
1758 | | /// |
1759 | | /// Orchestrates the generation loop with: |
1760 | | /// 1. Model forward pass |
1761 | | /// 2. Logit processing |
1762 | | /// 3. Token sampling |
1763 | | /// 4. EOS detection |
1764 | | /// |
1765 | | /// # Example |
1766 | | /// |
1767 | | /// ```rust,ignore |
1768 | | /// use realizar::generate::{GenerationPipeline, TokenSuppressor, GenerationConfig}; |
1769 | | /// |
1770 | | /// let pipeline = GenerationPipeline::new(model) |
1771 | | /// .add_processor(TokenSuppressor::new(vec![0, 1, 2])) |
1772 | | /// .with_config(GenerationConfig::greedy().with_eos_token_id(50256)); |
1773 | | /// |
1774 | | /// let tokens = pipeline.generate(&[1, 2, 3])?; |
1775 | | /// ``` |
1776 | | pub struct GenerationPipeline<M: GenerativeModel> { |
1777 | | model: M, |
1778 | | processors: LogitProcessorChain, |
1779 | | config: GenerationConfig, |
1780 | | } |
1781 | | |
1782 | | impl<M: GenerativeModel> GenerationPipeline<M> { |
1783 | | /// Create a new generation pipeline |
1784 | | #[must_use] |
1785 | 4 | pub fn new(model: M) -> Self { |
1786 | 4 | Self { |
1787 | 4 | model, |
1788 | 4 | processors: LogitProcessorChain::new(), |
1789 | 4 | config: GenerationConfig::default(), |
1790 | 4 | } |
1791 | 4 | } |
1792 | | |
1793 | | /// Add a logit processor to the pipeline |
1794 | | #[must_use] |
1795 | 2 | pub fn add_processor<P: LogitProcessor + 'static>(mut self, processor: P) -> Self { |
1796 | 2 | self.processors = self.processors.with_processor(processor); |
1797 | 2 | self |
1798 | 2 | } |
1799 | | |
1800 | | /// Set generation configuration |
1801 | | #[must_use] |
1802 | 4 | pub fn with_config(mut self, config: GenerationConfig) -> Self { |
1803 | 4 | self.config = config; |
1804 | 4 | self |
1805 | 4 | } |
1806 | | |
1807 | | /// Generate tokens starting from initial sequence |
1808 | | /// |
1809 | | /// # Arguments |
1810 | | /// |
1811 | | /// * `initial_tokens` - Starting token sequence (prompt) |
1812 | | /// |
1813 | | /// # Returns |
1814 | | /// |
1815 | | /// Generated token sequence (including initial tokens) |
1816 | 4 | pub fn generate(&mut self, initial_tokens: &[u32]) -> Result<Vec<u32>> { |
1817 | 4 | let mut tokens = initial_tokens.to_vec(); |
1818 | 4 | let n_vocab = self.model.vocab_size(); |
1819 | 4 | let eos_token = self.config.eos_token_id; |
1820 | | |
1821 | | // Simple PRNG for sampling (deterministic with seed) |
1822 | 4 | let mut rng_state = self.config.seed.unwrap_or(42); |
1823 | | |
1824 | 11 | for step in 0..self.config.max_tokens4 { |
1825 | | // Forward pass |
1826 | 11 | let mut logits = self.model.forward(&tokens)?0 ; |
1827 | | |
1828 | | // Apply logit processors |
1829 | 11 | let ctx = LogitProcessorContext::new(&tokens, step, n_vocab); |
1830 | 11 | self.processors.process(&mut logits, &ctx); |
1831 | | |
1832 | | // Sample next token |
1833 | 11 | let logits_tensor = Tensor::from_vec(vec![logits.len()], logits)?0 ; |
1834 | | |
1835 | | // Simple LCG for RNG |
1836 | 11 | rng_state = rng_state |
1837 | 11 | .wrapping_mul(6_364_136_223_846_793_005) |
1838 | 11 | .wrapping_add(1); |
1839 | 11 | let rng_value = (rng_state >> 33) as f32 / (1u64 << 31) as f32; |
1840 | | |
1841 | 11 | let next_token = sample_token(&logits_tensor, &self.config, rng_value)?0 as u32; |
1842 | | |
1843 | 11 | tokens.push(next_token); |
1844 | | |
1845 | | // Check for EOS |
1846 | 11 | if let Some(eos7 ) = eos_token { |
1847 | 7 | if next_token == eos as u32 { |
1848 | 2 | break; |
1849 | 5 | } |
1850 | 4 | } |
1851 | | } |
1852 | | |
1853 | 4 | Ok(tokens) |
1854 | 4 | } |
1855 | | |
1856 | | /// Get reference to the model |
1857 | | #[must_use] |
1858 | 0 | pub fn model(&self) -> &M { |
1859 | 0 | &self.model |
1860 | 0 | } |
1861 | | |
1862 | | /// Get mutable reference to the model |
1863 | 0 | pub fn model_mut(&mut self) -> &mut M { |
1864 | 0 | &mut self.model |
1865 | 0 | } |
1866 | | |
1867 | | /// Get reference to the processor chain |
1868 | | #[must_use] |
1869 | 0 | pub fn processors(&self) -> &LogitProcessorChain { |
1870 | 0 | &self.processors |
1871 | 0 | } |
1872 | | |
1873 | | /// Get reference to the config |
1874 | | #[must_use] |
1875 | 0 | pub fn config(&self) -> &GenerationConfig { |
1876 | 0 | &self.config |
1877 | 0 | } |
1878 | | } |
1879 | | |
1880 | | // Tests extracted to tests.rs (PMAT-802) |