Skip to main content

abaddon/
sampler.rs

1//! Token sampling implementations.
2
3use infernum_core::SamplingParams;
4
5/// Token sampler for text generation.
6pub struct Sampler {
7    params: SamplingParams,
8    rng: Option<fastrand::Rng>,
9}
10
11impl Sampler {
12    /// Creates a new sampler with the given parameters.
13    #[must_use]
14    pub fn new(params: SamplingParams) -> Self {
15        let rng = params.seed.map(|seed| {
16            let mut rng = fastrand::Rng::new();
17            rng.seed(seed);
18            rng
19        });
20
21        Self { params, rng }
22    }
23
24    /// Samples a token from the logits.
25    #[must_use]
26    pub fn sample(&mut self, logits: &[f32]) -> u32 {
27        if self.params.temperature == 0.0 {
28            // Greedy sampling
29            return self.argmax(logits);
30        }
31
32        // Apply temperature
33        let scaled: Vec<f32> = logits
34            .iter()
35            .map(|&l| l / self.params.temperature)
36            .collect();
37
38        // Apply top-k
39        let filtered = if self.params.top_k > 0 {
40            self.top_k_filter(&scaled, self.params.top_k as usize)
41        } else {
42            scaled
43        };
44
45        // Apply top-p
46        let filtered = if self.params.top_p < 1.0 {
47            self.top_p_filter(&filtered, self.params.top_p)
48        } else {
49            filtered
50        };
51
52        // Apply min-p
53        let filtered = if self.params.min_p > 0.0 {
54            self.min_p_filter(&filtered, self.params.min_p)
55        } else {
56            filtered
57        };
58
59        // Sample from distribution
60        self.categorical_sample(&filtered)
61    }
62
63    /// Returns the index of the maximum value.
64    fn argmax(&self, logits: &[f32]) -> u32 {
65        logits
66            .iter()
67            .enumerate()
68            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
69            .map(|(i, _)| i as u32)
70            .unwrap_or(0)
71    }
72
73    /// Applies top-k filtering.
74    fn top_k_filter(&self, logits: &[f32], k: usize) -> Vec<f32> {
75        let mut indexed: Vec<(usize, f32)> = logits.iter().copied().enumerate().collect();
76        indexed.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
77
78        let mut result = vec![f32::NEG_INFINITY; logits.len()];
79        for (i, v) in indexed.into_iter().take(k) {
80            result[i] = v;
81        }
82        result
83    }
84
85    /// Applies top-p (nucleus) filtering.
86    fn top_p_filter(&self, logits: &[f32], p: f32) -> Vec<f32> {
87        let probs = self.softmax(logits);
88        let mut indexed: Vec<(usize, f32)> = probs.iter().copied().enumerate().collect();
89        indexed.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
90
91        let mut cumsum = 0.0;
92        let mut result = vec![f32::NEG_INFINITY; logits.len()];
93
94        for (i, prob) in indexed {
95            if cumsum < p {
96                result[i] = logits[i];
97                cumsum += prob;
98            }
99        }
100
101        result
102    }
103
104    /// Applies min-p filtering.
105    fn min_p_filter(&self, logits: &[f32], min_p: f32) -> Vec<f32> {
106        let probs = self.softmax(logits);
107        let max_prob = probs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
108        let threshold = max_prob * min_p;
109
110        logits
111            .iter()
112            .zip(probs.iter())
113            .map(|(&l, &p)| if p >= threshold { l } else { f32::NEG_INFINITY })
114            .collect()
115    }
116
117    /// Computes softmax probabilities.
118    fn softmax(&self, logits: &[f32]) -> Vec<f32> {
119        let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
120        let exp: Vec<f32> = logits.iter().map(|&l| (l - max).exp()).collect();
121        let sum: f32 = exp.iter().sum();
122        exp.iter().map(|&e| e / sum).collect()
123    }
124
125    /// Samples from a categorical distribution.
126    fn categorical_sample(&mut self, logits: &[f32]) -> u32 {
127        let probs = self.softmax(logits);
128        let r = self.random_f32();
129
130        let mut cumsum = 0.0;
131        for (i, &p) in probs.iter().enumerate() {
132            cumsum += p;
133            if r < cumsum {
134                return i as u32;
135            }
136        }
137
138        (probs.len() - 1) as u32
139    }
140
141    /// Generates a random f32 in [0, 1).
142    fn random_f32(&mut self) -> f32 {
143        if let Some(rng) = &mut self.rng {
144            rng.f32()
145        } else {
146            fastrand::f32()
147        }
148    }
149
150    /// Returns the sampling parameters.
151    #[must_use]
152    pub fn params(&self) -> &SamplingParams {
153        &self.params
154    }
155
156    /// Checks if a token matches any stop sequence.
157    #[must_use]
158    pub fn is_stop_token(&self, text: &str) -> bool {
159        self.params
160            .stop_sequences
161            .iter()
162            .any(|stop| text.contains(stop))
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_greedy_sampling() {
172        let params = SamplingParams::greedy();
173        let mut sampler = Sampler::new(params);
174
175        let logits = vec![1.0, 5.0, 2.0, 0.5];
176        assert_eq!(sampler.sample(&logits), 1);
177    }
178
179    #[test]
180    fn test_deterministic_with_seed() {
181        let params = SamplingParams::balanced().with_seed(42);
182        let mut sampler1 = Sampler::new(params.clone());
183        let mut sampler2 = Sampler::new(params);
184
185        let logits = vec![1.0, 1.0, 1.0, 1.0];
186        assert_eq!(sampler1.sample(&logits), sampler2.sample(&logits));
187    }
188}