1use ferrum_types::{Result, SamplingParams, TokenId};
8use rand::{RngCore, SeedableRng};
9use rand_chacha::ChaCha12Rng;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fmt;
13
14pub const SAMPLING_RNG_ALGORITHM_ID: &str = "chacha12-rand-core-pcg32-u64-v1";
20
21#[derive(Clone, Debug)]
22pub struct SamplingRng {
23 inner: ChaCha12Rng,
24}
25
26impl SamplingRng {
27 pub fn seeded(seed: u64) -> Self {
28 Self {
29 inner: ChaCha12Rng::seed_from_u64(seed),
30 }
31 }
32
33 pub fn from_seed_bytes(seed: [u8; 32]) -> Self {
34 Self {
35 inner: ChaCha12Rng::from_seed(seed),
36 }
37 }
38
39 pub fn from_entropy() -> Self {
40 let mut entropy = rand::rng();
41 Self {
42 inner: ChaCha12Rng::from_rng(&mut entropy),
43 }
44 }
45
46 pub const fn algorithm_id() -> &'static str {
47 SAMPLING_RNG_ALGORITHM_ID
48 }
49}
50
51impl RngCore for SamplingRng {
52 fn next_u32(&mut self) -> u32 {
53 self.inner.next_u32()
54 }
55
56 fn next_u64(&mut self) -> u64 {
57 self.inner.next_u64()
58 }
59
60 fn fill_bytes(&mut self, dest: &mut [u8]) {
61 self.inner.fill_bytes(dest);
62 }
63}
64
65#[derive(Debug)]
67pub struct SamplingContext<'a> {
68 pub step: usize,
70 pub sampling_params: &'a SamplingParams,
72 pub logits: &'a mut [f32],
74 pub previous_tokens: &'a [TokenId],
76 pub token_frequencies: &'a HashMap<TokenId, usize>,
78 pub vocab_size: usize,
80 pub metadata: HashMap<String, f32>,
82}
83
84impl<'a> SamplingContext<'a> {
85 pub fn new(
87 step: usize,
88 sampling_params: &'a SamplingParams,
89 logits: &'a mut [f32],
90 previous_tokens: &'a [TokenId],
91 token_frequencies: &'a HashMap<TokenId, usize>,
92 vocab_size: usize,
93 ) -> Self {
94 Self {
95 step,
96 sampling_params,
97 logits,
98 previous_tokens,
99 token_frequencies,
100 vocab_size,
101 metadata: HashMap::new(),
102 }
103 }
104
105 pub fn get_logit(&self, token_id: TokenId) -> Option<f32> {
107 if usize::from(token_id) < self.logits.len() {
108 Some(self.logits[usize::from(token_id)])
109 } else {
110 None
111 }
112 }
113
114 pub fn set_logit(&mut self, token_id: TokenId, value: f32) -> bool {
116 if usize::from(token_id) < self.logits.len() {
117 self.logits[usize::from(token_id)] = value;
118 true
119 } else {
120 false
121 }
122 }
123
124 pub fn mask_tokens(&mut self, token_ids: &[TokenId]) {
126 for &token_id in token_ids {
127 if usize::from(token_id) < self.logits.len() {
128 self.logits[usize::from(token_id)] = f32::NEG_INFINITY;
129 }
130 }
131 }
132}
133
134pub trait LogitsProcessor: Send + Sync {
136 fn process(&self, ctx: &mut SamplingContext) -> Result<()>;
138
139 fn name(&self) -> &str;
141
142 fn priority(&self) -> ProcessorPriority {
144 ProcessorPriority::Normal
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
150pub enum ProcessorPriority {
151 High = 3,
153 Normal = 2,
155 Low = 1,
157}
158
159pub trait Sampler: Send + Sync {
161 fn sample(&self, logits: &[f32], rng: &mut dyn RngCore) -> Result<TokenId>;
163
164 fn sample_with_context(&self, ctx: &SamplingContext, rng: &mut dyn RngCore) -> Result<TokenId> {
166 self.sample(ctx.logits, rng)
167 }
168
169 fn name(&self) -> &str;
171
172 fn is_deterministic(&self) -> bool;
174}
175
176pub trait MultiSampler: Sampler {
178 fn sample_multiple(
180 &self,
181 logits: &[f32],
182 num_samples: usize,
183 rng: &mut dyn RngCore,
184 ) -> Result<Vec<TokenId>>;
185
186 fn sample_with_probabilities(
188 &self,
189 logits: &[f32],
190 rng: &mut dyn RngCore,
191 ) -> Result<(TokenId, Vec<f32>)>;
192}
193
194pub struct LogitsProcessorChain {
196 processors: Vec<Box<dyn LogitsProcessor>>,
197}
198
199impl LogitsProcessorChain {
200 pub fn new() -> Self {
202 Self {
203 processors: Vec::new(),
204 }
205 }
206
207 pub fn add_processor(mut self, processor: Box<dyn LogitsProcessor>) -> Self {
209 self.processors.push(processor);
210 self.processors
212 .sort_by(|a, b| b.priority().cmp(&a.priority()));
213 self
214 }
215
216 pub fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
218 for processor in &self.processors {
219 processor.process(ctx)?;
220 }
221 Ok(())
222 }
223
224 pub fn processor_names(&self) -> Vec<&str> {
226 self.processors.iter().map(|p| p.name()).collect()
227 }
228
229 pub fn is_empty(&self) -> bool {
231 self.processors.is_empty()
232 }
233}
234
235impl fmt::Debug for LogitsProcessorChain {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 f.debug_list()
238 .entries(self.processors.iter().map(|processor| processor.name()))
239 .finish()
240 }
241}
242
243impl Default for LogitsProcessorChain {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249pub struct TemperatureProcessor {
253 pub temperature: f32,
254}
255
256impl TemperatureProcessor {
257 pub fn new(temperature: f32) -> Self {
258 Self { temperature }
259 }
260}
261
262impl LogitsProcessor for TemperatureProcessor {
263 fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
264 if self.temperature > 0.0 && self.temperature != 1.0 {
265 for logit in ctx.logits.iter_mut() {
266 *logit /= self.temperature;
267 }
268 }
269 Ok(())
270 }
271
272 fn name(&self) -> &str {
273 "temperature"
274 }
275
276 fn priority(&self) -> ProcessorPriority {
277 ProcessorPriority::Normal
280 }
281}
282
283pub struct TopKProcessor {
285 pub k: usize,
286}
287
288impl TopKProcessor {
289 pub fn new(k: usize) -> Self {
290 Self { k }
291 }
292}
293
294impl LogitsProcessor for TopKProcessor {
295 fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
296 if self.k > 0 && self.k < ctx.logits.len() {
297 let mut indices: Vec<usize> = (0..ctx.logits.len()).collect();
299 indices.sort_by(|&a, &b| {
300 ctx.logits[b]
301 .partial_cmp(&ctx.logits[a])
302 .unwrap_or(std::cmp::Ordering::Equal)
303 });
304
305 let threshold = ctx.logits[indices[self.k - 1]];
306
307 for logit in ctx.logits.iter_mut() {
309 if *logit < threshold {
310 *logit = f32::NEG_INFINITY;
311 }
312 }
313 }
314 Ok(())
315 }
316
317 fn name(&self) -> &str {
318 "top_k"
319 }
320
321 fn priority(&self) -> ProcessorPriority {
322 ProcessorPriority::Low
323 }
324}
325
326pub struct TopPProcessor {
328 pub p: f32,
329}
330
331impl TopPProcessor {
332 pub fn new(p: f32) -> Self {
333 Self { p }
334 }
335}
336
337impl LogitsProcessor for TopPProcessor {
338 fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
339 if self.p < 1.0 && self.p > 0.0 {
340 let mut candidates = ctx
344 .logits
345 .iter()
346 .copied()
347 .enumerate()
348 .filter(|(_, logit)| logit.is_finite())
349 .collect::<Vec<_>>();
350 if candidates.is_empty() {
351 return Ok(());
352 }
353
354 candidates.sort_by(|(left_idx, left), (right_idx, right)| {
355 right.total_cmp(left).then_with(|| left_idx.cmp(right_idx))
356 });
357
358 let max_logit = candidates[0].1;
359 let sum = candidates
360 .iter()
361 .map(|(_, logit)| (*logit - max_logit).exp())
362 .sum::<f32>();
363 let mut cum_prob = 0.0;
364 let mut cutoff_idx = candidates.len();
365 for (i, (_, logit)) in candidates.iter().enumerate() {
366 cum_prob += (*logit - max_logit).exp() / sum;
367 if cum_prob >= self.p {
368 cutoff_idx = i + 1;
369 break;
370 }
371 }
372
373 for (idx, _) in candidates.into_iter().skip(cutoff_idx) {
374 ctx.logits[idx] = f32::NEG_INFINITY;
375 }
376 }
377 Ok(())
378 }
379
380 fn name(&self) -> &str {
381 "top_p"
382 }
383
384 fn priority(&self) -> ProcessorPriority {
385 ProcessorPriority::Low
386 }
387}
388
389pub struct MinPProcessor {
395 pub min_p: f32,
396}
397
398impl MinPProcessor {
399 pub fn new(min_p: f32) -> Self {
400 Self { min_p }
401 }
402}
403
404impl LogitsProcessor for MinPProcessor {
405 fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
406 if self.min_p > 0.0 && self.min_p <= 1.0 {
407 let max_logit = ctx.logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
408 let threshold = max_logit + self.min_p.ln();
409 for logit in ctx.logits.iter_mut() {
410 if *logit < threshold {
411 *logit = f32::NEG_INFINITY;
412 }
413 }
414 }
415 Ok(())
416 }
417
418 fn name(&self) -> &str {
419 "min_p"
420 }
421
422 fn priority(&self) -> ProcessorPriority {
423 ProcessorPriority::Low
424 }
425}
426
427pub struct RepetitionPenaltyProcessor {
429 pub penalty: f32,
430}
431
432impl RepetitionPenaltyProcessor {
433 pub fn new(penalty: f32) -> Self {
434 Self { penalty }
435 }
436}
437
438impl LogitsProcessor for RepetitionPenaltyProcessor {
439 fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
440 if self.penalty != 1.0 {
441 for &token_id in ctx.token_frequencies.keys() {
442 if usize::from(token_id) >= ctx.logits.len() {
443 continue;
444 }
445 let idx = usize::from(token_id);
446 let current_logit = ctx.logits[idx];
447 if current_logit > 0.0 {
448 ctx.logits[idx] = current_logit / self.penalty;
449 } else {
450 ctx.logits[idx] = current_logit * self.penalty;
451 }
452 }
453 }
454 Ok(())
455 }
456
457 fn name(&self) -> &str {
458 "repetition_penalty"
459 }
460
461 fn priority(&self) -> ProcessorPriority {
462 ProcessorPriority::High }
464}
465
466pub struct PresenceFrequencyPenaltyProcessor {
472 pub presence_penalty: f32,
473 pub frequency_penalty: f32,
474}
475
476impl PresenceFrequencyPenaltyProcessor {
477 pub fn new(presence_penalty: f32, frequency_penalty: f32) -> Self {
478 Self {
479 presence_penalty,
480 frequency_penalty,
481 }
482 }
483}
484
485impl LogitsProcessor for PresenceFrequencyPenaltyProcessor {
486 fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
487 if self.presence_penalty == 0.0 && self.frequency_penalty == 0.0 {
488 return Ok(());
489 }
490 for (&token_id, &count) in ctx.token_frequencies {
491 let idx = usize::from(token_id);
492 if idx >= ctx.logits.len() || count == 0 {
493 continue;
494 }
495 ctx.logits[idx] -= self.presence_penalty + self.frequency_penalty * count as f32;
496 }
497 Ok(())
498 }
499
500 fn name(&self) -> &str {
501 "presence_frequency_penalty"
502 }
503
504 fn priority(&self) -> ProcessorPriority {
505 ProcessorPriority::High
506 }
507}
508
509pub struct GreedySampler;
513
514impl Sampler for GreedySampler {
515 fn sample(&self, logits: &[f32], _rng: &mut dyn RngCore) -> Result<TokenId> {
516 let max_idx = logits
517 .iter()
518 .enumerate()
519 .filter(|(_, logit)| logit.is_finite())
520 .reduce(|best, candidate| match candidate.1.total_cmp(best.1) {
521 std::cmp::Ordering::Greater => candidate,
522 std::cmp::Ordering::Equal if candidate.0 < best.0 => candidate,
523 _ => best,
524 })
525 .map(|(idx, _)| idx)
526 .ok_or_else(|| {
527 ferrum_types::FerrumError::backend("No finite logits available for sampling")
528 })?;
529
530 Ok(TokenId::new(max_idx as u32))
531 }
532
533 fn name(&self) -> &str {
534 "greedy"
535 }
536
537 fn is_deterministic(&self) -> bool {
538 true
539 }
540}
541
542#[cfg(test)]
543mod greedy_sampler_tests {
544 use super::{GreedySampler, Sampler, SamplingRng};
545
546 #[test]
547 fn ties_choose_the_lowest_token_id() {
548 let mut rng = SamplingRng::seeded(1);
549 let token = GreedySampler
550 .sample(&[-1.0, 4.0, 4.0, f32::NAN], &mut rng)
551 .unwrap();
552 assert_eq!(token.get(), 1);
553 }
554
555 #[test]
556 fn non_finite_logits_are_never_selected() {
557 let mut rng = SamplingRng::seeded(1);
558 let token = GreedySampler
559 .sample(&[f32::NAN, f32::INFINITY, -2.0], &mut rng)
560 .unwrap();
561 assert_eq!(token.get(), 2);
562 assert!(GreedySampler
563 .sample(&[f32::NAN, f32::INFINITY], &mut rng)
564 .is_err());
565 }
566}
567
568pub struct MultinomialSampler;
570
571impl Sampler for MultinomialSampler {
572 fn sample(&self, logits: &[f32], rng: &mut dyn RngCore) -> Result<TokenId> {
573 let max_logit = logits.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
575
576 let mut probs: Vec<f32> = logits
577 .iter()
578 .map(|&logit| {
579 if logit.is_finite() && logit > f32::NEG_INFINITY {
580 (logit - max_logit).exp()
581 } else {
582 0.0
583 }
584 })
585 .collect();
586
587 let sum: f32 = probs.iter().sum();
588 if sum <= 0.0 {
589 return Err(ferrum_types::FerrumError::backend(
590 "No valid tokens for sampling",
591 ));
592 }
593
594 for prob in probs.iter_mut() {
595 *prob /= sum;
596 }
597
598 let threshold = rng.next_u32() as f32 / u32::MAX as f32;
600 let mut cumulative = 0.0;
601
602 for (idx, prob) in probs.iter().enumerate() {
603 cumulative += prob;
604 if cumulative >= threshold {
605 return Ok(TokenId::new(idx as u32));
606 }
607 }
608
609 Ok(TokenId::new((probs.len() - 1) as u32))
611 }
612
613 fn name(&self) -> &str {
614 "multinomial"
615 }
616
617 fn is_deterministic(&self) -> bool {
618 false
619 }
620}
621
622pub struct SamplingConfigBuilder {
624 processors: Vec<Box<dyn LogitsProcessor>>,
625 sampler: Option<Box<dyn Sampler>>,
626}
627
628impl SamplingConfigBuilder {
629 pub fn new() -> Self {
631 Self {
632 processors: Vec::new(),
633 sampler: None,
634 }
635 }
636
637 pub fn with_temperature(mut self, temperature: f32) -> Self {
639 if temperature > 0.0 && temperature != 1.0 {
640 self.processors
641 .push(Box::new(TemperatureProcessor::new(temperature)));
642 }
643 self
644 }
645
646 pub fn with_top_k(mut self, k: usize) -> Self {
648 if k > 0 {
649 self.processors.push(Box::new(TopKProcessor::new(k)));
650 }
651 self
652 }
653
654 pub fn with_top_p(mut self, p: f32) -> Self {
656 if p > 0.0 && p < 1.0 {
657 self.processors.push(Box::new(TopPProcessor::new(p)));
658 }
659 self
660 }
661
662 pub fn with_min_p(mut self, min_p: f32) -> Self {
664 if min_p > 0.0 && min_p <= 1.0 {
665 self.processors.push(Box::new(MinPProcessor::new(min_p)));
666 }
667 self
668 }
669
670 pub fn with_repetition_penalty(mut self, penalty: f32) -> Self {
672 if penalty != 1.0 {
673 self.processors
674 .push(Box::new(RepetitionPenaltyProcessor::new(penalty)));
675 }
676 self
677 }
678
679 pub fn with_presence_frequency_penalty(
681 mut self,
682 presence_penalty: f32,
683 frequency_penalty: f32,
684 ) -> Self {
685 if presence_penalty != 0.0 || frequency_penalty != 0.0 {
686 self.processors
687 .push(Box::new(PresenceFrequencyPenaltyProcessor::new(
688 presence_penalty,
689 frequency_penalty,
690 )));
691 }
692 self
693 }
694
695 pub fn with_sampler(mut self, sampler: Box<dyn Sampler>) -> Self {
697 self.sampler = Some(sampler);
698 self
699 }
700
701 pub fn build(self) -> SamplingConfig {
703 let mut chain = LogitsProcessorChain::new();
704 for processor in self.processors {
705 chain = chain.add_processor(processor);
706 }
707
708 let sampler = self.sampler.unwrap_or_else(|| Box::new(MultinomialSampler));
709
710 SamplingConfig {
711 processor_chain: chain,
712 sampler,
713 }
714 }
715}
716
717impl Default for SamplingConfigBuilder {
718 fn default() -> Self {
719 Self::new()
720 }
721}
722
723pub struct SamplingConfig {
725 pub processor_chain: LogitsProcessorChain,
726 pub sampler: Box<dyn Sampler>,
727}
728
729impl fmt::Debug for SamplingConfig {
730 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
731 f.debug_struct("SamplingConfig")
732 .field("processor_chain", &self.processor_chain)
733 .field("sampler", &self.sampler.name())
734 .finish()
735 }
736}
737
738impl SamplingConfig {
739 pub fn from_params(params: &SamplingParams) -> Self {
741 let mut builder = SamplingConfigBuilder::new()
742 .with_temperature(params.temperature)
743 .with_repetition_penalty(params.repetition_penalty)
744 .with_presence_frequency_penalty(params.presence_penalty, params.frequency_penalty);
745
746 if let Some(min_p) = params.min_p {
747 builder = builder.with_min_p(min_p);
748 }
749
750 if let Some(top_k) = params.top_k {
751 builder = builder.with_top_k(top_k);
752 }
753
754 if params.top_p < 1.0 {
755 builder = builder.with_top_p(params.top_p);
756 }
757
758 let sampler: Box<dyn Sampler> = if params.temperature == 0.0 {
760 Box::new(GreedySampler)
761 } else {
762 Box::new(MultinomialSampler)
763 };
764
765 builder.with_sampler(sampler).build()
766 }
767
768 pub fn supports_raw_greedy_speculation(&self) -> bool {
773 self.sampler.is_deterministic() && self.processor_chain.is_empty()
774 }
775
776 pub fn sample(&self, mut ctx: SamplingContext, rng: &mut dyn RngCore) -> Result<TokenId> {
778 self.processor_chain.process(&mut ctx)?;
780
781 self.sampler.sample_with_context(&ctx, rng)
783 }
784}
785
786#[derive(Debug, Clone, Serialize, Deserialize)]
788pub struct SamplingStats {
789 pub total_samples: u64,
791 pub avg_sample_time_us: f64,
793 pub token_distribution: HashMap<TokenId, u64>,
795 pub effective_temperature: f32,
797 pub processor_times: HashMap<String, f64>,
799}