1use rlx_ir::Philox4x32;
32use rlx_runtime::samplers::{
33 Dry, DynamicTemperature, MirostatV1, MirostatV2, RepetitionPenalty, SamplerChain, SamplerState,
34 Temperature, TopK, TopNSigma, TopP, TypicalP, Xtc,
35};
36
37#[derive(Debug, Clone, Copy, PartialEq, Default)]
40pub enum MirostatMode {
41 #[default]
42 Off,
43 V1,
44 V2,
45}
46
47pub const DRY_BREAKERS_MAX: usize = 32;
67
68#[derive(Debug, Clone, Copy)]
69pub struct SampleOpts {
70 pub temperature: f32,
71 pub top_k: usize,
72 pub top_p: f32,
73 pub seed: u64,
74 pub greedy: bool,
75
76 pub dynamic_temp: Option<(f32, f32)>,
78 pub dynamic_temp_exponent: f32,
79 pub typical_p: f32,
80 pub top_n_sigma: f32,
81 pub xtc_threshold: f32,
82 pub xtc_prob: f32,
83 pub dry_multiplier: f32,
84 pub dry_base: f32,
85 pub dry_allowed_length: usize,
86 pub dry_max_ngram: usize,
87 pub dry_sequence_breakers: [u32; DRY_BREAKERS_MAX],
91 pub dry_sequence_breakers_len: u8,
92 pub mirostat: MirostatMode,
93 pub mirostat_tau: f32,
94 pub mirostat_eta: f32,
95 pub mirostat_m: usize,
96 pub repetition_penalty: f32,
97 pub frequency_penalty: f32,
98 pub presence_penalty: f32,
99 pub repetition_window: usize,
100 pub min_keep: usize,
101}
102
103impl SampleOpts {
104 pub fn greedy() -> Self {
105 Self {
106 temperature: 1.0,
107 top_k: 0,
108 top_p: 1.0,
109 seed: 0,
110 greedy: true,
111 dynamic_temp: None,
112 dynamic_temp_exponent: 1.0,
113 typical_p: 1.0,
114 top_n_sigma: 0.0,
115 xtc_threshold: 0.0,
116 xtc_prob: 0.0,
117 dry_multiplier: 0.0,
118 dry_base: 1.75,
119 dry_allowed_length: 2,
120 dry_max_ngram: 32,
121 dry_sequence_breakers: [0; DRY_BREAKERS_MAX],
122 dry_sequence_breakers_len: 0,
123 mirostat: MirostatMode::Off,
124 mirostat_tau: 5.0,
125 mirostat_eta: 0.1,
126 mirostat_m: 100,
127 repetition_penalty: 1.0,
128 frequency_penalty: 0.0,
129 presence_penalty: 0.0,
130 repetition_window: 64,
131 min_keep: 1,
132 }
133 }
134
135 pub fn temperature(temp: f32, seed: u64) -> Self {
136 Self {
137 temperature: temp,
138 top_k: 0,
139 top_p: 1.0,
140 seed,
141 greedy: false,
142 ..Self::greedy()
143 }
144 }
145
146 pub fn with_top_k(mut self, k: usize) -> Self {
147 self.top_k = k;
148 self
149 }
150
151 pub fn with_top_p(mut self, p: f32) -> Self {
152 self.top_p = p;
153 self
154 }
155
156 pub fn with_dynamic_temp(mut self, min: f32, max: f32) -> Self {
159 self.dynamic_temp = Some((min, max));
160 self.greedy = false;
161 self
162 }
163
164 pub fn with_typical_p(mut self, p: f32) -> Self {
165 self.typical_p = p;
166 self.greedy = false;
167 self
168 }
169
170 pub fn with_top_n_sigma(mut self, n: f32) -> Self {
171 self.top_n_sigma = n;
172 self.greedy = false;
173 self
174 }
175
176 pub fn with_xtc(mut self, threshold: f32, prob: f32) -> Self {
177 self.xtc_threshold = threshold;
178 self.xtc_prob = prob;
179 self.greedy = false;
180 self
181 }
182
183 pub fn with_dry(mut self, multiplier: f32, base: f32, allowed_length: usize) -> Self {
184 self.dry_multiplier = multiplier;
185 self.dry_base = base;
186 self.dry_allowed_length = allowed_length;
187 self.greedy = false;
188 self
189 }
190
191 pub fn with_mirostat_v1(mut self, tau: f32, eta: f32) -> Self {
192 self.mirostat = MirostatMode::V1;
193 self.mirostat_tau = tau;
194 self.mirostat_eta = eta;
195 self.greedy = false;
196 self
197 }
198
199 pub fn with_mirostat_v2(mut self, tau: f32, eta: f32) -> Self {
200 self.mirostat = MirostatMode::V2;
201 self.mirostat_tau = tau;
202 self.mirostat_eta = eta;
203 self.greedy = false;
204 self
205 }
206
207 pub fn with_repetition_penalty(mut self, p: f32) -> Self {
208 self.repetition_penalty = p;
209 self
210 }
211
212 pub fn with_frequency_presence(mut self, frequency: f32, presence: f32) -> Self {
213 self.frequency_penalty = frequency;
214 self.presence_penalty = presence;
215 self
216 }
217
218 pub fn is_classic(&self) -> bool {
223 self.dynamic_temp.is_none()
224 && self.typical_p >= 1.0
225 && self.top_n_sigma <= 0.0
226 && self.xtc_prob <= 0.0
227 && self.dry_multiplier <= 0.0
228 && self.mirostat == MirostatMode::Off
229 && self.frequency_penalty == 0.0
230 && self.presence_penalty == 0.0
231 && (self.repetition_penalty - 1.0).abs() < f32::EPSILON
232 }
233
234 pub fn into_chain(&self) -> SamplerChain {
237 let mut b = SamplerChain::builder();
238 if (self.repetition_penalty - 1.0).abs() > f32::EPSILON
239 || self.frequency_penalty != 0.0
240 || self.presence_penalty != 0.0
241 {
242 b = b.push(RepetitionPenalty {
243 penalty: self.repetition_penalty,
244 frequency: self.frequency_penalty,
245 presence: self.presence_penalty,
246 last_n: self.repetition_window,
247 });
248 }
249 if self.dry_multiplier > 0.0 {
250 b = b.push(Dry {
251 multiplier: self.dry_multiplier,
252 base: self.dry_base,
253 allowed_length: self.dry_allowed_length,
254 max_ngram: self.dry_max_ngram,
255 sequence_breakers: self.dry_sequence_breakers
256 [..self.dry_sequence_breakers_len as usize]
257 .to_vec(),
258 });
259 }
260 if self.mirostat == MirostatMode::Off {
261 if let Some((mn, mx)) = self.dynamic_temp {
262 b = b.push(DynamicTemperature {
263 min: mn,
264 max: mx,
265 exponent: self.dynamic_temp_exponent,
266 });
267 } else if self.temperature > 0.0 && (self.temperature - 1.0).abs() > f32::EPSILON {
268 b = b.push(Temperature {
269 t: self.temperature,
270 });
271 }
272 }
273 if self.top_k > 0 {
274 b = b.push(TopK { k: self.top_k });
275 }
276 if self.typical_p < 1.0 && self.typical_p > 0.0 {
277 b = b.push(TypicalP {
278 p: self.typical_p,
279 min_keep: self.min_keep,
280 });
281 }
282 if self.top_p < 1.0 && self.top_p > 0.0 {
283 b = b.push(TopP {
284 p: self.top_p,
285 min_keep: self.min_keep,
286 });
287 }
288 if self.top_n_sigma > 0.0 {
289 b = b.push(TopNSigma {
290 n: self.top_n_sigma,
291 });
292 }
293 if self.xtc_prob > 0.0 && self.xtc_threshold > 0.0 {
294 b = b.push(Xtc {
295 threshold: self.xtc_threshold,
296 prob: self.xtc_prob,
297 min_keep: self.min_keep,
298 });
299 }
300 match self.mirostat {
301 MirostatMode::Off => {}
302 MirostatMode::V1 => {
303 b = b.push(MirostatV1 {
304 tau: self.mirostat_tau,
305 eta: self.mirostat_eta,
306 m: self.mirostat_m,
307 });
308 }
309 MirostatMode::V2 => {
310 b = b.push(MirostatV2 {
311 tau: self.mirostat_tau,
312 eta: self.mirostat_eta,
313 });
314 }
315 }
316 b.build()
317 }
318}
319
320pub fn sample_token(logits: &[f32], opts: SampleOpts) -> usize {
324 sample_token_at(logits, opts, 0)
325}
326
327pub fn sample_token_at(logits: &[f32], opts: SampleOpts, step: u64) -> usize {
331 if opts.is_classic() {
334 return sample_token_inner(logits, opts, step);
335 }
336 sample_token_with_history(logits, &opts, &[], step) as usize
337}
338
339pub fn sample_token_with_history(
347 logits: &[f32],
348 opts: &SampleOpts,
349 history: &[u32],
350 step: u64,
351) -> u32 {
352 let mut state = SamplerState::new();
353 sample_token_stateful(logits, opts, history, step, &mut state)
354}
355
356pub fn sample_token_stateful(
360 logits: &[f32],
361 opts: &SampleOpts,
362 history: &[u32],
363 step: u64,
364 state: &mut SamplerState,
365) -> u32 {
366 let mut work = logits.to_vec();
367 let chain = opts.into_chain();
368 let mut rng = Philox4x32::new(opts.seed.wrapping_add(step));
369 chain.sample(&mut work, history, state, &mut rng)
370}
371
372fn sample_token_inner(logits: &[f32], opts: SampleOpts, step: u64) -> usize {
373 assert!(!logits.is_empty(), "sample_token: empty logits");
374
375 if opts.greedy {
376 return argmax(logits);
377 }
378
379 let mut work: Vec<f32> = if opts.temperature > 0.0 && opts.temperature != 1.0 {
381 logits.iter().map(|&l| l / opts.temperature).collect()
382 } else {
383 logits.to_vec()
384 };
385
386 if opts.top_k > 0 && opts.top_k < work.len() {
388 let mut indexed: Vec<(usize, f32)> =
389 work.iter().enumerate().map(|(i, &v)| (i, v)).collect();
390 indexed.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
392 let cutoff = indexed[opts.top_k - 1].1;
393 for v in work.iter_mut() {
394 if *v < cutoff {
395 *v = f32::NEG_INFINITY;
396 }
397 }
398 }
399
400 let max = work.iter().copied().fold(f32::NEG_INFINITY, f32::max);
402 let mut probs: Vec<f32> = work.iter().map(|&l| (l - max).exp()).collect();
403 let sum: f32 = probs.iter().sum();
404 if sum > 0.0 {
405 for p in probs.iter_mut() {
406 *p /= sum;
407 }
408 } else {
409 return argmax(logits);
411 }
412
413 if opts.top_p < 1.0 && opts.top_p > 0.0 {
415 let mut order: Vec<usize> = (0..probs.len()).collect();
416 order.sort_unstable_by(|&a, &b| {
417 probs[b]
418 .partial_cmp(&probs[a])
419 .unwrap_or(std::cmp::Ordering::Equal)
420 });
421 let mut cum = 0.0f32;
422 let mut keep = vec![false; probs.len()];
423 for &i in &order {
424 cum += probs[i];
425 keep[i] = true;
426 if cum >= opts.top_p {
427 break;
428 }
429 }
430 let mut renorm = 0.0f32;
431 for (i, p) in probs.iter_mut().enumerate() {
432 if !keep[i] {
433 *p = 0.0;
434 } else {
435 renorm += *p;
436 }
437 }
438 if renorm > 0.0 {
439 for p in probs.iter_mut() {
440 *p /= renorm;
441 }
442 }
443 }
444
445 let mut rng = Philox4x32::new(opts.seed.wrapping_add(step));
447 let u = rng.next_f32();
448 let mut acc = 0.0f32;
449 for (i, &p) in probs.iter().enumerate() {
450 acc += p;
451 if u < acc {
452 return i;
453 }
454 }
455 probs.len() - 1
456}
457
458fn argmax(xs: &[f32]) -> usize {
459 let mut best = 0usize;
460 let mut best_v = f32::NEG_INFINITY;
461 for (i, &v) in xs.iter().enumerate() {
462 if v > best_v {
463 best_v = v;
464 best = i;
465 }
466 }
467 best
468}
469
470pub fn apply_repetition_penalty(
472 logits: &mut [f32],
473 counts: &std::collections::HashMap<u32, u32>,
474 penalty: f32,
475) {
476 if penalty <= 1.0 || counts.is_empty() {
477 return;
478 }
479 for (&tok, &count) in counts {
480 let idx = tok as usize;
481 if idx >= logits.len() {
482 continue;
483 }
484 let factor = penalty.powi(count as i32);
485 if logits[idx] > 0.0 {
486 logits[idx] /= factor;
487 } else {
488 logits[idx] *= factor;
489 }
490 }
491}
492
493pub fn softmax_logits(logits: &[f32]) -> Vec<f32> {
497 let max = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
498 let mut p: Vec<f32> = logits.iter().map(|&l| (l - max).exp()).collect();
499 let sum: f32 = p.iter().sum();
500 if sum > 0.0 {
501 for v in p.iter_mut() {
502 *v /= sum;
503 }
504 }
505 p
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
513 fn greedy_matches_argmax() {
514 let logits = vec![0.1, 0.5, 0.2, -1.0, 0.49];
515 let t = sample_token(&logits, SampleOpts::greedy());
516 assert_eq!(t, 1);
517 }
518
519 #[test]
520 fn top_k_one_equals_greedy() {
521 let logits = vec![0.1, 0.5, 0.2, -1.0, 0.49];
522 let opts = SampleOpts::temperature(1.0, 42).with_top_k(1);
523 assert_eq!(sample_token(&logits, opts), 1);
524 }
525
526 #[test]
527 fn top_p_full_equals_unrestricted_multinomial() {
528 let logits = vec![1.0, 2.0, 0.5, 0.0];
531 let opts = SampleOpts::temperature(1.0, 7).with_top_p(1.0);
532 let t = sample_token(&logits, opts);
533 assert!(t < logits.len());
534 }
535
536 #[test]
537 fn deterministic_for_same_seed() {
538 let logits: Vec<f32> = (0..32).map(|i| (i as f32) * 0.01).collect();
539 let opts = SampleOpts::temperature(0.7, 123).with_top_k(4);
540 let a = sample_token(&logits, opts);
541 let b = sample_token(&logits, opts);
542 assert_eq!(a, b);
543 }
544
545 #[test]
546 fn top_p_truncates_low_mass() {
547 let mut logits = vec![-10.0f32; 16];
550 logits[7] = 10.0;
551 let opts = SampleOpts::temperature(1.0, 999).with_top_p(0.5);
552 assert_eq!(sample_token(&logits, opts), 7);
553 }
554
555 #[test]
556 fn high_temperature_still_returns_valid_id() {
557 let logits = vec![0.0; 10];
558 let opts = SampleOpts::temperature(100.0, 1);
559 let t = sample_token(&logits, opts);
560 assert!(t < 10);
561 }
562
563 #[test]
564 fn sample_token_at_varies_rng_by_step() {
565 let logits: Vec<f32> = (0..64).map(|i| (i as f32) * 0.05).collect();
566 let opts = SampleOpts::temperature(0.9, 100).with_top_p(1.0);
567 let a = sample_token_at(&logits, opts, 0);
568 let b = sample_token_at(&logits, opts, 1);
569 assert_ne!(a, b);
570 }
571
572 #[test]
573 fn mirostat_v2_routes_through_chain() {
574 let logits: Vec<f32> = (0..32).map(|i| (i as f32) * 0.1).collect();
578 let opts = SampleOpts::temperature(0.7, 42).with_mirostat_v2(5.0, 0.1);
579 assert!(!opts.is_classic());
580 let t = sample_token(&logits, opts);
581 assert!(t < 32);
582 }
583
584 #[test]
585 fn dynamic_temp_routes_through_chain() {
586 let logits: Vec<f32> = (0..32).map(|i| (i as f32) * 0.1).collect();
587 let opts = SampleOpts::temperature(1.0, 7).with_dynamic_temp(0.5, 1.5);
588 assert!(!opts.is_classic());
589 let t = sample_token(&logits, opts);
590 assert!(t < 32);
591 }
592
593 #[test]
594 fn classic_path_unchanged_with_new_fields() {
595 let opts = SampleOpts::greedy();
598 assert!(opts.is_classic());
599 let opts = SampleOpts::temperature(0.7, 1)
600 .with_top_k(40)
601 .with_top_p(0.9);
602 assert!(opts.is_classic());
603 }
604
605 #[test]
606 fn sample_token_with_history_threads_dry() {
607 let history = vec![0u32, 1, 0, 1, 0];
610 let logits = vec![0.0, 0.0];
611 let opts = SampleOpts::temperature(1.0, 0).with_dry(1.0, 2.0, 2);
612 let mut state = SamplerState::new();
613 let t = sample_token_stateful(&logits, &opts, &history, 0, &mut state);
614 assert!(t < 2);
615 }
616}