/home/noah/src/realizar/src/generate/mod.rs
Line | Count | Source |
1 | | //! Text generation and sampling strategies |
2 | | //! |
3 | | //! This module provides the generation loop for autoregressive text generation |
4 | | //! and various sampling strategies for token selection. |
5 | | //! |
6 | | //! # Sampling Strategies |
7 | | //! |
8 | | //! - **Greedy**: Always select the most probable token |
9 | | //! - **Top-k**: Sample from the k most probable tokens |
10 | | //! - **Top-p (nucleus)**: Sample from tokens with cumulative probability ≤ p |
11 | | //! - **Temperature**: Scale logits before softmax to control randomness |
12 | | |
13 | | use crate::{ |
14 | | error::{RealizarError, Result}, |
15 | | layers::softmax, |
16 | | tensor::Tensor, |
17 | | }; |
18 | | |
19 | | // Submodules |
20 | | mod algorithms; |
21 | | mod sampler; |
22 | | |
23 | | // Re-exports from algorithms (unique sampling algorithms) |
24 | | pub use algorithms::{ |
25 | | sample_min_p, MirostatState, sample_mirostat, sample_tfs, sample_typical, |
26 | | DryConfig, apply_dry_penalty, XtcConfig, apply_xtc, EtaConfig, sample_eta, |
27 | | TokenHealingConfig, TokenHealingResult, analyze_token_healing, CfgConfig, apply_cfg, |
28 | | }; |
29 | | |
30 | | // Re-exports from sampler (advanced sampling infrastructure) |
31 | | pub use sampler::{ |
32 | | StopSequenceDetector, RepetitionPenaltyConfig, apply_repetition_penalty, |
33 | | PresenceFrequencyPenalty, apply_presence_frequency_penalty, LogitBias, apply_logit_bias, |
34 | | PromptCache, PromptCacheEntry, PromptCacheStats, BeamHypothesis, BeamSearchConfig, |
35 | | BeamSearchState, StreamingGenerator, AdvancedGenerationConfig, apply_all_penalties, |
36 | | DynTempConfig, apply_dynamic_temperature, InfillConfig, InfillResult, apply_infill_sampling, |
37 | | SamplerContext, SamplerChain, Sampler, TemperatureSampler, DynTempSampler, TopKSampler, |
38 | | TopPSampler, RepetitionPenaltySampler, InfillSampler, LogitProcessorContext, |
39 | | LogitProcessor, TokenSuppressor, RepetitionPenalty, TemperatureScaler, |
40 | | LogitProcessorChain, GenerativeModel, GenerationPipeline, |
41 | | }; |
42 | | |
43 | | /// Sample from a probability distribution using a random value |
44 | | /// |
45 | | /// # Arguments |
46 | | /// |
47 | | /// * `probs` - Probabilities (must sum to 1) |
48 | | /// * `indices` - Corresponding indices for each probability |
49 | | /// * `rng_value` - Random value in [0, 1) |
50 | | /// |
51 | | /// # Returns |
52 | | /// |
53 | | /// Selected index |
54 | 322 | pub(crate) fn sample_from_distribution(probs: &[f32], indices: &[usize], rng_value: f32) -> usize { |
55 | 322 | let mut cumsum = 0.0; |
56 | 11.8k | for (i, &prob) in probs322 .iter322 ().enumerate322 () { |
57 | 11.8k | cumsum += prob; |
58 | 11.8k | if rng_value < cumsum { |
59 | 322 | return indices[i]; |
60 | 11.5k | } |
61 | | } |
62 | | // Fallback to last token |
63 | 0 | indices[indices.len() - 1] |
64 | 322 | } |
65 | | |
66 | | /// Convert logits to softmax probabilities for a subset |
67 | | /// |
68 | | /// # Arguments |
69 | | /// |
70 | | /// * `indexed` - Pairs of (index, logit) sorted by logit descending |
71 | | /// |
72 | | /// # Returns |
73 | | /// |
74 | | /// Probabilities for the subset |
75 | 19 | pub(crate) fn logits_to_probs(indexed: &[(usize, f32)]) -> Vec<f32> { |
76 | 19 | let max_logit = indexed[0].1; |
77 | 96 | let exp_vals19 : Vec<f32>19 = indexed19 .iter19 ().map19 (|(_, l)| (l - max_logit).exp()).collect19 (); |
78 | 19 | let sum_exp: f32 = exp_vals.iter().sum(); |
79 | 96 | exp_vals.iter()19 .map19 (|e| e / sum_exp).collect19 () |
80 | 19 | } |
81 | | |
82 | | /// Build nucleus for top-p sampling |
83 | | /// |
84 | | /// # Arguments |
85 | | /// |
86 | | /// * `indexed` - Pairs of (index, prob) sorted by prob descending |
87 | | /// * `p` - Cumulative probability threshold |
88 | | /// |
89 | | /// # Returns |
90 | | /// |
91 | | /// Nucleus of (index, prob) pairs with cumulative probability >= p |
92 | 271 | pub(crate) fn build_nucleus(indexed: &[(usize, f32)], p: f32) -> Vec<(usize, f32)> { |
93 | 271 | let mut cumsum = 0.0; |
94 | 271 | let mut nucleus = Vec::new(); |
95 | 24.3k | for &(idx, prob) in indexed { |
96 | 24.3k | nucleus.push((idx, prob)); |
97 | 24.3k | cumsum += prob; |
98 | 24.3k | if cumsum >= p { |
99 | 271 | break; |
100 | 24.0k | } |
101 | | } |
102 | 271 | nucleus |
103 | 271 | } |
104 | | |
105 | | /// Sampling strategy for token selection |
106 | | #[derive(Debug, Clone, Copy, PartialEq)] |
107 | | pub enum SamplingStrategy { |
108 | | /// Always select the most probable token |
109 | | Greedy, |
110 | | /// Sample from the k most probable tokens |
111 | | TopK { |
112 | | /// Number of top tokens to consider |
113 | | k: usize, |
114 | | }, |
115 | | /// Sample from tokens with cumulative probability ≤ p |
116 | | TopP { |
117 | | /// Cumulative probability threshold |
118 | | p: f32, |
119 | | }, |
120 | | } |
121 | | |
122 | | /// Configuration for text generation |
123 | | #[derive(Debug, Clone)] |
124 | | pub struct GenerationConfig { |
125 | | /// Maximum number of tokens to generate |
126 | | pub max_tokens: usize, |
127 | | /// Sampling strategy |
128 | | pub strategy: SamplingStrategy, |
129 | | /// Temperature for scaling logits (1.0 = no scaling) |
130 | | pub temperature: f32, |
131 | | /// Token ID for end-of-sequence |
132 | | pub eos_token_id: Option<usize>, |
133 | | /// Random seed for reproducibility |
134 | | pub seed: Option<u64>, |
135 | | } |
136 | | |
137 | | impl Default for GenerationConfig { |
138 | 59 | fn default() -> Self { |
139 | 59 | Self { |
140 | 59 | max_tokens: 100, |
141 | 59 | strategy: SamplingStrategy::Greedy, |
142 | 59 | temperature: 1.0, |
143 | 59 | eos_token_id: None, |
144 | 59 | seed: None, |
145 | 59 | } |
146 | 59 | } |
147 | | } |
148 | | |
149 | | impl GenerationConfig { |
150 | | /// Create a new generation config with greedy sampling |
151 | | #[must_use] |
152 | 16 | pub fn greedy() -> Self { |
153 | 16 | Self { |
154 | 16 | strategy: SamplingStrategy::Greedy, |
155 | 16 | ..Default::default() |
156 | 16 | } |
157 | 16 | } |
158 | | |
159 | | /// Create a new generation config with top-k sampling |
160 | | #[must_use] |
161 | 3 | pub fn top_k(k: usize) -> Self { |
162 | 3 | Self { |
163 | 3 | strategy: SamplingStrategy::TopK { k }, |
164 | 3 | ..Default::default() |
165 | 3 | } |
166 | 3 | } |
167 | | |
168 | | /// Create a new generation config with top-p (nucleus) sampling |
169 | | #[must_use] |
170 | 2 | pub fn top_p(p: f32) -> Self { |
171 | 2 | Self { |
172 | 2 | strategy: SamplingStrategy::TopP { p }, |
173 | 2 | ..Default::default() |
174 | 2 | } |
175 | 2 | } |
176 | | |
177 | | /// Set temperature |
178 | | #[must_use] |
179 | 31 | pub fn with_temperature(mut self, temperature: f32) -> Self { |
180 | 31 | self.temperature = temperature; |
181 | 31 | self |
182 | 31 | } |
183 | | |
184 | | /// Set maximum tokens |
185 | | #[must_use] |
186 | 41 | pub fn with_max_tokens(mut self, max_tokens: usize) -> Self { |
187 | 41 | self.max_tokens = max_tokens; |
188 | 41 | self |
189 | 41 | } |
190 | | |
191 | | /// Set end-of-sequence token ID |
192 | | #[must_use] |
193 | 4 | pub fn with_eos_token_id(mut self, eos_token_id: usize) -> Self { |
194 | 4 | self.eos_token_id = Some(eos_token_id); |
195 | 4 | self |
196 | 4 | } |
197 | | |
198 | | /// Set random seed |
199 | | #[must_use] |
200 | 13 | pub fn with_seed(mut self, seed: u64) -> Self { |
201 | 13 | self.seed = Some(seed); |
202 | 13 | self |
203 | 13 | } |
204 | | } |
205 | | |
206 | | /// Apply temperature scaling to logits |
207 | | /// |
208 | | /// # Arguments |
209 | | /// |
210 | | /// * `logits` - Raw logits from model |
211 | | /// * `temperature` - Temperature value (> 0) |
212 | | /// |
213 | | /// # Returns |
214 | | /// |
215 | | /// Scaled logits |
216 | | /// |
217 | | /// # Errors |
218 | | /// |
219 | | /// Returns error if temperature is not positive |
220 | 1.36k | pub fn apply_temperature(logits: &Tensor<f32>, temperature: f32) -> Result<Tensor<f32>> { |
221 | 1.36k | if temperature <= 0.0 { |
222 | 3 | return Err(RealizarError::InvalidShape { |
223 | 3 | reason: "Temperature must be positive".to_string(), |
224 | 3 | }); |
225 | 1.35k | } |
226 | | |
227 | 1.35k | if (temperature - 1.0).abs() < 1e-6 { |
228 | | // No scaling needed |
229 | 540 | return Ok(logits.clone()); |
230 | 819 | } |
231 | | |
232 | 819 | let data = logits.data(); |
233 | 80.8k | let scaled819 : Vec<f32>819 = data819 .iter819 ().map819 (|&x| x / temperature).collect819 (); |
234 | 819 | Tensor::from_vec(logits.shape().to_vec(), scaled) |
235 | 1.36k | } |
236 | | |
237 | | /// Greedy sampling: select the token with highest probability |
238 | | /// |
239 | | /// # Arguments |
240 | | /// |
241 | | /// * `logits` - Logits for the vocabulary |
242 | | /// |
243 | | /// # Returns |
244 | | /// |
245 | | /// Index of the selected token |
246 | | /// |
247 | | /// # Errors |
248 | | /// |
249 | | /// Returns error if logits are empty |
250 | 1.07k | pub fn sample_greedy(logits: &Tensor<f32>) -> Result<usize> { |
251 | 1.07k | let data = logits.data(); |
252 | 1.07k | if data.is_empty() { |
253 | 0 | return Err(RealizarError::InvalidShape { |
254 | 0 | reason: "Logits cannot be empty".to_string(), |
255 | 0 | }); |
256 | 1.07k | } |
257 | | |
258 | 1.07k | let mut max_idx = 0; |
259 | 1.07k | let mut max_val = data[0]; |
260 | 301k | for (i, &val) in data1.07k .iter1.07k ().enumerate1.07k ().skip1.07k (1) { |
261 | 301k | if val > max_val { |
262 | 28 | max_val = val; |
263 | 28 | max_idx = i; |
264 | 301k | } |
265 | | } |
266 | | |
267 | 1.07k | Ok(max_idx) |
268 | 1.07k | } |
269 | | |
270 | | /// Top-k sampling: sample from the k most probable tokens |
271 | | /// |
272 | | /// # Arguments |
273 | | /// |
274 | | /// * `logits` - Logits for the vocabulary |
275 | | /// * `k` - Number of top tokens to consider |
276 | | /// * `rng_value` - Random value in [0, 1) for sampling |
277 | | /// |
278 | | /// # Returns |
279 | | /// |
280 | | /// Index of the selected token |
281 | | /// |
282 | | /// # Errors |
283 | | /// |
284 | | /// Returns error if k is 0 or logits are empty |
285 | 20 | pub fn sample_top_k(logits: &Tensor<f32>, k: usize, rng_value: f32) -> Result<usize> { |
286 | 20 | let data = logits.data(); |
287 | 20 | if data.is_empty() { |
288 | 0 | return Err(RealizarError::InvalidShape { |
289 | 0 | reason: "Logits cannot be empty".to_string(), |
290 | 0 | }); |
291 | 20 | } |
292 | 20 | if k == 0 { |
293 | 1 | return Err(RealizarError::InvalidShape { |
294 | 1 | reason: "k must be > 0".to_string(), |
295 | 1 | }); |
296 | 19 | } |
297 | | |
298 | | // Create (index, logit) pairs and sort by logit descending |
299 | 19 | let mut indexed: Vec<(usize, f32)> = data.iter().copied().enumerate().collect(); |
300 | 1.16k | indexed19 .sort_by19 (|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); |
301 | | |
302 | | // Take top k |
303 | 19 | let top_k: Vec<(usize, f32)> = indexed.into_iter().take(k.min(data.len())).collect(); |
304 | | |
305 | | // Convert to probabilities and sample |
306 | 19 | let probs = logits_to_probs(&top_k); |
307 | 19 | let indices: Vec<usize> = top_k.iter().map(|(idx, _)| *idx).collect(); |
308 | 19 | Ok(sample_from_distribution(&probs, &indices, rng_value)) |
309 | 20 | } |
310 | | |
311 | | /// Top-p (nucleus) sampling: sample from tokens with cumulative probability ≤ p |
312 | | /// |
313 | | /// # Arguments |
314 | | /// |
315 | | /// * `logits` - Logits for the vocabulary |
316 | | /// * `p` - Cumulative probability threshold |
317 | | /// * `rng_value` - Random value in [0, 1) for sampling |
318 | | /// |
319 | | /// # Returns |
320 | | /// |
321 | | /// Index of the selected token |
322 | | /// |
323 | | /// # Errors |
324 | | /// |
325 | | /// Returns error if p is not in (0, 1] or logits are empty |
326 | 274 | pub fn sample_top_p(logits: &Tensor<f32>, p: f32, rng_value: f32) -> Result<usize> { |
327 | 274 | let data = logits.data(); |
328 | 274 | if data.is_empty() { |
329 | 0 | return Err(RealizarError::InvalidShape { |
330 | 0 | reason: "Logits cannot be empty".to_string(), |
331 | 0 | }); |
332 | 274 | } |
333 | 274 | if p <= 0.0 || p > 1.0272 { |
334 | 3 | return Err(RealizarError::InvalidShape { |
335 | 3 | reason: "p must be in (0, 1]".to_string(), |
336 | 3 | }); |
337 | 271 | } |
338 | | |
339 | | // Convert logits to probabilities |
340 | 271 | let probs_tensor = softmax(logits)?0 ; |
341 | 271 | let probs = probs_tensor.data(); |
342 | | |
343 | | // Create (index, prob) pairs and sort by prob descending |
344 | 271 | let mut indexed: Vec<(usize, f32)> = probs.iter().copied().enumerate().collect(); |
345 | 26.4k | indexed271 .sort_by271 (|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); |
346 | | |
347 | | // Build nucleus (cumulative probability <= p) |
348 | 271 | let nucleus = build_nucleus(&indexed, p); |
349 | | |
350 | | // Renormalize and sample |
351 | 271 | let nucleus_sum: f32 = nucleus.iter().map(|(_, prob)| prob).sum(); |
352 | 24.3k | let normalized_probs271 : Vec<f32>271 = nucleus.iter()271 .map271 (|(_, prob)| prob / nucleus_sum).collect271 (); |
353 | 271 | let indices: Vec<usize> = nucleus.iter().map(|(idx, _)| *idx).collect(); |
354 | | |
355 | 271 | Ok(sample_from_distribution( |
356 | 271 | &normalized_probs, |
357 | 271 | &indices, |
358 | 271 | rng_value, |
359 | 271 | )) |
360 | 274 | } |
361 | | |
362 | | /// Sample a token based on the sampling strategy |
363 | | /// |
364 | | /// # Arguments |
365 | | /// |
366 | | /// * `logits` - Logits for the vocabulary |
367 | | /// * `config` - Generation configuration |
368 | | /// * `rng_value` - Random value in [0, 1) for sampling (ignored for greedy) |
369 | | /// |
370 | | /// # Returns |
371 | | /// |
372 | | /// Index of the selected token |
373 | | /// |
374 | | /// # Errors |
375 | | /// |
376 | | /// Returns error if temperature is invalid or sampling fails |
377 | 1.34k | pub fn sample_token( |
378 | 1.34k | logits: &Tensor<f32>, |
379 | 1.34k | config: &GenerationConfig, |
380 | 1.34k | rng_value: f32, |
381 | 1.34k | ) -> Result<usize> { |
382 | | // Apply temperature |
383 | 1.34k | let scaled_logits1.34k = apply_temperature(logits, config.temperature)?1 ; |
384 | | |
385 | 1.34k | match config.strategy { |
386 | 1.06k | SamplingStrategy::Greedy => sample_greedy(&scaled_logits), |
387 | 15 | SamplingStrategy::TopK { k } => sample_top_k(&scaled_logits, k, rng_value), |
388 | 268 | SamplingStrategy::TopP { p } => sample_top_p(&scaled_logits, p, rng_value), |
389 | | } |
390 | 1.34k | } |
391 | | |
392 | | |
393 | | // Tests extracted to tests.rs (PMAT-802) |
394 | | #[cfg(test)] |
395 | | #[path = "tests.rs"] |
396 | | mod generate_tests; |