1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone)]
11pub struct SplitMix64 {
12 state: u64,
13}
14
15#[derive(Debug, Default)]
19pub struct SamplerScratch {
20 seen_epoch: Vec<u32>,
21 epoch: u32,
22}
23
24impl SamplerScratch {
25 fn begin_seen(&mut self, vocab_size: usize) -> u32 {
26 if self.seen_epoch.len() < vocab_size {
27 self.seen_epoch.resize(vocab_size, 0);
28 }
29 self.epoch = self.epoch.wrapping_add(1);
30 if self.epoch == 0 {
31 self.seen_epoch.fill(0);
32 self.epoch = 1;
33 }
34 self.epoch
35 }
36}
37
38impl SplitMix64 {
39 pub fn new(seed: u64) -> Self {
40 Self { state: seed }
41 }
42
43 pub fn from_entropy() -> Self {
45 let t = std::time::SystemTime::now()
46 .duration_since(std::time::UNIX_EPOCH)
47 .unwrap_or_default();
48 let addr = Box::into_raw(Box::new(0u8)) as u64;
49 unsafe { drop(Box::from_raw(addr as *mut u8)) };
51 Self::new(t.as_nanos() as u64 ^ addr.rotate_left(17) ^ 0x9E3779B97F4A7C15)
52 }
53
54 #[inline]
55 pub fn next_u64(&mut self) -> u64 {
56 self.state = self.state.wrapping_add(0x9E3779B97F4A7C15);
57 let mut z = self.state;
58 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
59 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
60 z ^ (z >> 31)
61 }
62
63 #[inline]
65 pub fn next_f32(&mut self) -> f32 {
66 (self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct SamplerConfig {
73 pub temperature: f32,
74 pub top_p: f32,
75 pub top_k: u32,
76 pub repetition_penalty: f32,
77 pub min_p: f32,
78 #[serde(default)]
80 pub seed: Option<u64>,
81}
82
83impl Default for SamplerConfig {
84 fn default() -> Self {
85 Self {
86 temperature: 0.7,
87 top_p: 0.9,
88 top_k: 40,
89 repetition_penalty: 1.1,
90 min_p: 0.05,
91 seed: None,
92 }
93 }
94}
95
96pub fn sample(
99 logits: &[f32],
100 config: &SamplerConfig,
101 past_tokens: &[u32],
102 rng: &mut SplitMix64,
103) -> u32 {
104 let mut scratch = SamplerScratch::default();
105 sample_with_scratch(logits, config, past_tokens, rng, &mut scratch)
106}
107
108pub fn sample_with_scratch(
110 logits: &[f32],
111 config: &SamplerConfig,
112 past_tokens: &[u32],
113 rng: &mut SplitMix64,
114 scratch: &mut SamplerScratch,
115) -> u32 {
116 if config.temperature < 1e-6 && config.repetition_penalty == 1.0 {
120 return argmax(logits);
121 }
122 let mut probs = logits.to_vec();
123
124 if config.repetition_penalty != 1.0 {
125 apply_repetition_penalty(&mut probs, past_tokens, config.repetition_penalty, scratch);
126 }
127
128 if config.temperature < 1e-6 {
129 return argmax(&probs); }
131 if config.temperature != 1.0 {
132 for p in probs.iter_mut() {
133 *p /= config.temperature;
134 }
135 }
136
137 softmax_inplace(&mut probs);
138
139 if config.min_p > 0.0 {
140 let max_prob = probs.iter().cloned().fold(0.0f32, f32::max);
141 let threshold = max_prob * config.min_p;
142 for p in probs.iter_mut() {
143 if *p < threshold {
144 *p = 0.0;
145 }
146 }
147 }
148
149 if config.top_k > 0 && (config.top_k as usize) < probs.len() {
150 apply_top_k(&mut probs, config.top_k as usize);
151 }
152
153 if config.top_p < 1.0 && config.top_p > 0.0 {
154 apply_top_p(&mut probs, config.top_p);
155 }
156
157 let sum: f32 = probs.iter().sum();
158 if sum > 0.0 {
159 for p in probs.iter_mut() {
160 *p /= sum;
161 }
162 } else {
163 return argmax(logits);
165 }
166
167 categorical_sample(&probs, rng.next_f32())
168}
169
170pub fn argmax(values: &[f32]) -> u32 {
172 values
173 .iter()
174 .enumerate()
175 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
176 .map(|(i, _)| i as u32)
177 .unwrap_or(0)
178}
179
180fn softmax_inplace(logits: &mut [f32]) {
181 let max_val = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
182 let mut sum = 0.0f32;
183 for v in logits.iter_mut() {
184 *v = (*v - max_val).exp();
185 sum += *v;
186 }
187 if sum > 0.0 {
188 for v in logits.iter_mut() {
189 *v /= sum;
190 }
191 }
192}
193
194fn apply_repetition_penalty(
195 logits: &mut [f32],
196 past_tokens: &[u32],
197 penalty: f32,
198 scratch: &mut SamplerScratch,
199) {
200 let epoch = scratch.begin_seen(logits.len());
201 for &tok in past_tokens {
202 let idx = tok as usize;
203 if idx < logits.len() && scratch.seen_epoch[idx] != epoch {
204 scratch.seen_epoch[idx] = epoch;
205 if logits[idx] > 0.0 {
206 logits[idx] /= penalty;
207 } else {
208 logits[idx] *= penalty;
209 }
210 }
211 }
212}
213
214fn apply_top_k(probs: &mut [f32], k: usize) {
219 if k == 0 || k >= probs.len() {
220 return;
221 }
222 let mut sel: Vec<f32> = probs.to_vec();
223 let (_, kth, _) = sel.select_nth_unstable_by(k - 1, |a, b| {
225 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
226 });
227 let threshold = *kth;
228 for p in probs.iter_mut() {
229 if *p < threshold {
230 *p = 0.0;
231 }
232 }
233}
234
235fn apply_top_p(probs: &mut [f32], top_p: f32) {
240 let mut indexed: Vec<(usize, f32)> = probs
241 .iter()
242 .copied()
243 .enumerate()
244 .filter(|&(_, p)| p > 0.0)
245 .collect();
246 indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
247
248 let mut cumsum = 0.0f32;
249 let mut cutoff_idx = indexed.len();
250 for (i, &(_, prob)) in indexed.iter().enumerate() {
251 cumsum += prob;
252 if cumsum >= top_p {
253 cutoff_idx = i + 1;
254 break;
255 }
256 }
257
258 for &(i, _) in &indexed[cutoff_idx..] {
260 probs[i] = 0.0;
261 }
262}
263
264fn categorical_sample(probs: &[f32], r: f32) -> u32 {
266 let mut cumsum = 0.0f32;
267 for (i, &p) in probs.iter().enumerate() {
268 cumsum += p;
269 if r < cumsum {
270 return i as u32;
271 }
272 }
273 probs.iter().rposition(|&p| p > 0.0).unwrap_or(0) as u32
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_argmax() {
282 let logits = vec![0.1, 0.5, 0.3, 0.9, 0.2];
283 assert_eq!(argmax(&logits), 3);
284 }
285
286 #[test]
287 fn test_greedy_sampling() {
288 let logits = vec![1.0, 5.0, 2.0, 3.0];
289 let config = SamplerConfig {
290 temperature: 0.0,
291 ..Default::default()
292 };
293 let mut rng = SplitMix64::new(1);
294 assert_eq!(sample(&logits, &config, &[], &mut rng), 1);
295 }
296
297 #[test]
298 fn test_softmax() {
299 let mut logits = vec![1.0, 2.0, 3.0];
300 softmax_inplace(&mut logits);
301 let sum: f32 = logits.iter().sum();
302 assert!((sum - 1.0).abs() < 1e-5);
303 assert!(logits[2] > logits[1] && logits[1] > logits[0]);
304 }
305
306 #[test]
307 fn test_repetition_penalty() {
308 let mut logits = vec![1.0, 2.0, 3.0, 4.0];
309 let mut scratch = SamplerScratch::default();
310 apply_repetition_penalty(&mut logits, &[1, 3], 2.0, &mut scratch);
311 assert_eq!(logits, vec![1.0, 1.0, 3.0, 2.0]);
312 }
313
314 #[test]
315 fn repetition_penalty_applies_once_per_unique_token() {
316 let mut logits = vec![1.0, 4.0, -6.0];
317 let mut scratch = SamplerScratch::default();
318 apply_repetition_penalty(&mut logits, &[1, 1, 2, 1, 2], 2.0, &mut scratch);
319 assert_eq!(logits, vec![1.0, 2.0, -12.0]);
320 }
321
322 #[test]
323 fn top_k_keeps_exactly_k() {
324 let mut probs = vec![0.1, 0.4, 0.05, 0.3, 0.15];
325 apply_top_k(&mut probs, 2);
326 let kept = probs.iter().filter(|&&p| p > 0.0).count();
327 assert_eq!(kept, 2, "top-k must keep exactly k (was k+1 in v1)");
328 assert!(probs[1] > 0.0 && probs[3] > 0.0);
329 }
330
331 #[test]
332 fn rng_reaches_full_cdf() {
333 let probs = vec![0.25f32; 4];
336 let mut rng = SplitMix64::new(42);
337 let mut hits = [0usize; 4];
338 for _ in 0..4000 {
339 let i = categorical_sample(&probs, rng.next_f32()) as usize;
340 hits[i] += 1;
341 }
342 for (i, &h) in hits.iter().enumerate() {
343 assert!(h > 700, "index {i} sampled only {h}/4000 — biased RNG");
344 }
345 }
346
347 #[test]
348 fn same_seed_same_sequence() {
349 let logits: Vec<f32> = (0..32).map(|i| (i as f32 * 0.37).sin()).collect();
350 let config = SamplerConfig {
351 temperature: 1.0,
352 seed: Some(7),
353 ..Default::default()
354 };
355 let run = |seed: u64| -> Vec<u32> {
356 let mut rng = SplitMix64::new(seed);
357 (0..16)
358 .map(|_| sample(&logits, &config, &[], &mut rng))
359 .collect()
360 };
361 assert_eq!(run(7), run(7), "same seed must reproduce");
362 assert_ne!(run(7), run(8), "different seed must differ");
363 }
364}