Skip to main content

frink_models/sampling/
rng.rs

1//! The seeded generator every draw in a generation comes off, and the
2//! entry points that use it.
3//!
4//! Split out of `sampling.rs` so the chain runner beside it stays about
5//! the chain. The RNG is one concept and a small one, but it is the
6//! concept the whole run's reproducibility rests on: a request that
7//! passed `seed: 42` must draw the same tokens on every machine, so
8//! every draw -- the token, speculative decoding's accept coin, and
9//! XTC's -- has to come off this one stream in this one order.
10
11use super::penalties::apply_history_penalties;
12use super::{argmax, filtered_distribution, greedy_choice, SamplingParams};
13use crate::penalty_window::PenaltyWindow;
14
15/// Sets logits a caller wants to forbid to `-inf`, in place, before the
16/// sampler looks at them.
17///
18/// Two callers today, and they COMPOSE rather than exclude each other --
19/// a masked logit stays masked, so the order they run in cannot matter:
20/// JSON-object mode's character-class filter
21/// (`frink_server::json_mode`), and grammar-constrained decoding
22/// ([`crate::grammar_sampler::GrammarSampler::mask_logits`]).
23///
24/// The signature returns nothing because the callback runs from inside
25/// the sampler, which has no error to return one through. A mask that
26/// CAN fail -- a grammar that dead-ends leaves every logit at `-inf`,
27/// and sampling from that is how an "impossible" request becomes
28/// arbitrary text with a 200 -- records its refusal in the closure's own
29/// captured state, and the decode loop reads it after the sample and
30/// throws the token away. `frink_server::sample_step::sample_next` is
31/// the one place that pairing lives.
32pub type LogitMask<'a> = &'a mut dyn FnMut(&mut [f32]);
33
34/// A small, seedable xorshift64* generator. Not cryptographically
35/// secure -- sampling doesn't need that -- but reproducible given a
36/// seed, which greedy argmax already was for free.
37pub struct Sampler {
38    state: u64,
39}
40
41impl Sampler {
42    pub fn new(seed: u64) -> Self {
43        // xorshift64* requires a nonzero seed.
44        Sampler {
45            state: if seed == 0 { 0x9E3779B97F4A7C15 } else { seed },
46        }
47    }
48
49    fn next_u64(&mut self) -> u64 {
50        self.state ^= self.state << 13;
51        self.state ^= self.state >> 7;
52        self.state ^= self.state << 17;
53        // The `*` in xorshift64*. Without it this is plain xorshift64,
54        // whose state IS its output, and a small seed's first output is
55        // therefore still small: for every seed below ~4000 the first
56        // draw landed in the bottom eighth of [0, 1), so a request that
57        // asked for `seed: 42` always got its first token from the
58        // bottom of the CDF. The multiply is what decorrelates the
59        // output from a low-entropy state; see
60        // `low_seeds_do_not_bias_the_first_draw`.
61        self.state.wrapping_mul(0x2545F491_4F6CDD1D)
62    }
63
64    /// Uniform float in [0.0, 1.0).
65    fn next_f32(&mut self) -> f32 {
66        (self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
67    }
68
69    /// The one uniform draw XTC needs for this token, or `None` when XTC
70    /// cannot fire for these parameters.
71    ///
72    /// XTC is the only sampler in the chain that is itself stochastic
73    /// (`llama_sample_xtc_apply` draws from its own `std::mt19937`,
74    /// `src/llama-sampler.cpp:2146`), which is why the chain below takes
75    /// the roll as an argument instead of owning an RNG: the chain is
76    /// also what `sampling_distribution` runs for speculative
77    /// verification, and a filter that drew its own randomness there
78    /// would make "the distribution the sampler draws from" a different
79    /// distribution every time it was asked.
80    ///
81    /// **The draw is skipped when XTC cannot fire**, and that is not an
82    /// optimisation. Every draw advances the seeded stream, so drawing
83    /// unconditionally would shift every subsequent token of every
84    /// existing seeded generation, on every run that never asked for
85    /// XTC. [`SamplingParams::xtc_can_fire`] is the single predicate
86    /// this and [`Candidates::xtc`] share.
87    pub fn xtc_roll(&mut self, params: &SamplingParams) -> Option<f32> {
88        if params.xtc_can_fire() {
89            Some(self.next_f32())
90        } else {
91            None
92        }
93    }
94
95    /// Samples one token id from `logits`, given `params` and the
96    /// [`PenaltyWindow`] the penalties look back over. Falls back to
97    /// plain greedy argmax when `params.temperature <= 0.0`.
98    ///
99    /// `history` is a window and not a slice on purpose: it carries the
100    /// PROMPT as well as the generated tokens, which is what llama.cpp
101    /// penalises over. See [`crate::penalty_window`].
102    ///
103    /// A length-1 `logits` vector is treated as a precomputed greedy token
104    /// id (`logits[0] as usize`) — used by the Metal dense-stack path that
105    /// returns GPU argmax instead of downloading the full vocab.
106    pub fn sample(
107        &mut self,
108        logits: &[f32],
109        params: &SamplingParams,
110        history: PenaltyWindow<'_>,
111    ) -> usize {
112        self.sample_with_mask(logits, params, history, None)
113    }
114
115    /// Like [`Self::sample`], but optionally zeroes disallowed logits via
116    /// `mask` before argmax / nucleus sampling (used for JSON-object mode).
117    pub fn sample_with_mask(
118        &mut self,
119        logits: &[f32],
120        params: &SamplingParams,
121        history: PenaltyWindow<'_>,
122        mask: Option<LogitMask<'_>>,
123    ) -> usize {
124        self.sample_inner(logits, params, history, mask, false).0
125    }
126
127    /// The token AND the distribution it was drawn from, normalised to
128    /// sum to 1 over the whole vocabulary.
129    ///
130    /// This is what `logprobs` has to report: not the raw logits, but
131    /// the distribution the sampler actually drew from, with the
132    /// penalties applied over the `penalty_last_n` window and
133    /// llama.cpp's chain run in `params.sampler_order`. A filtered-out
134    /// candidate is a zero, which is what "this token could not have
135    /// been chosen" means.
136    ///
137    /// `None` for a vocabulary this sampler never saw: a backend that
138    /// folded `lm_head` and `argmax` onto the device hands back a
139    /// one-element vector holding the chosen id, and there is no
140    /// distribution to report for it. A caller that needs one must ask
141    /// for the vocabulary (`GenerationParams::needs_vocab_logits`)
142    /// rather than be given a fabricated single-candidate answer.
143    ///
144    /// It is the SAME vector [`Self::sample_with_mask`] draws from --
145    /// both go through `sample_inner` -- so a reported logprob cannot
146    /// describe a distribution other than the one that was sampled.
147    ///
148    /// **Not `sampling_distribution`**, and the difference is the
149    /// point. That function recomputes the pipeline from the logits
150    /// WITHOUT drawing, which is right for speculative verification
151    /// (it needs `p_target(x)` for a token someone else proposed) and
152    /// wrong here for two reasons: it takes `xtc_roll` as an argument,
153    /// so a caller who passed a fresh roll would report a distribution
154    /// the draw never saw; and for a greedy request it returns a
155    /// ONE-HOT, which as a logprob would claim the model was certain
156    /// when nobody asked it. This reports the real distribution in
157    /// both cases, because "how confident was the model" is a question
158    /// a greedy caller is entitled to ask.
159    pub fn sample_reporting(
160        &mut self,
161        logits: &[f32],
162        params: &SamplingParams,
163        history: PenaltyWindow<'_>,
164        mask: Option<LogitMask<'_>>,
165    ) -> (usize, Option<Vec<f32>>) {
166        self.sample_inner(logits, params, history, mask, true)
167    }
168
169    /// One pipeline, parameterised by whether the caller wants the
170    /// distribution back.
171    ///
172    /// `want_probs` costs the greedy fast path: with it set, even a
173    /// chain that keeps the argmax builds the full distribution,
174    /// because there is nothing to report otherwise. Unset, every path
175    /// is exactly what it was.
176    fn sample_inner(
177        &mut self,
178        logits: &[f32],
179        params: &SamplingParams,
180        history: PenaltyWindow<'_>,
181        mut mask: Option<LogitMask<'_>>,
182        want_probs: bool,
183    ) -> (usize, Option<Vec<f32>>) {
184        let xtc_roll = self.xtc_roll(params);
185        // A device-folded argmax: one element holding the chosen id,
186        // no vocabulary behind it.
187        //
188        // Gated on GREEDY, and that gate is load-bearing rather than
189        // incidental: only the greedy device fold produces this shape,
190        // and a SAMPLED request with a one-token vocabulary is a real
191        // distribution whose only candidate is token 0. Hoisting this
192        // check above the temperature test made `sample(&[42.0])` at
193        // temperature 0.8 answer 42 instead of 0, which
194        // `temperature_zero_accepts_precomputed_argmax_singleton`
195        // catches.
196        if params.temperature <= 0.0 && mask.is_none() && logits.len() == 1 {
197            return (logits[0] as usize, None);
198        }
199
200        let mut scores: Vec<f32> = logits.to_vec();
201        apply_history_penalties(&mut scores, params, history);
202
203        if let Some(m) = mask.as_mut() {
204            m(&mut scores);
205        }
206
207        if params.temperature <= 0.0 {
208            if scores.len() == 1 {
209                return (scores[0] as usize, None);
210            }
211            if !want_probs {
212                return (greedy_choice(scores, params, history, xtc_roll), None);
213            }
214            // The greedy answer read off the distribution it is the
215            // argmax OF, so the reported probabilities and the chosen
216            // token cannot disagree.
217            let probs = filtered_distribution(scores, params, history, xtc_roll);
218            return (argmax(&probs), Some(probs));
219        }
220
221        let probs = filtered_distribution(scores, params, history, xtc_roll);
222        let chosen = self.sample_from(&probs);
223        (chosen, want_probs.then_some(probs))
224    }
225
226    /// A uniform draw in `[0.0, 1.0)`.
227    ///
228    /// Exposed because speculative decoding's accept test is a coin
229    /// flip against `p_target(x) / p_draft(x)` rather than a draw from
230    /// a distribution, and it must come off the same seeded stream as
231    /// every other draw in the run or a "reproducible given a seed"
232    /// generation stops being reproducible.
233    pub fn uniform(&mut self) -> f32 {
234        self.next_f32()
235    }
236
237    /// Draws one index from an already-normalised distribution.
238    ///
239    /// Split out of [`Self::sample_with_mask`] so speculative decoding
240    /// can sample from a distribution it had to compute anyway (the
241    /// rejection rule needs `p_target` itself, not just a draw from it)
242    /// and still go through *exactly* the same draw as ordinary
243    /// sampling. Two separate copies of this loop would be two chances
244    /// to be subtly non-lossless.
245    pub fn sample_from(&mut self, probs: &[f32]) -> usize {
246        let draw = self.next_f32();
247        let mut cumulative = 0.0f32;
248        for (i, &p) in probs.iter().enumerate() {
249            cumulative += p;
250            if draw < cumulative {
251                return i;
252            }
253        }
254        // Floating-point rounding may leave `draw` fractionally above
255        // the final cumulative sum; the last nonzero-probability token
256        // is the correct fallback, not index 0.
257        probs
258            .iter()
259            .enumerate()
260            .rev()
261            .find(|&(_, &p)| p > 0.0)
262            .map(|(i, _)| i)
263            .unwrap_or(0)
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::sampling::{sampling_distribution, spread_logits};
271
272    ///
273    /// This is the reason [`Sampler::xtc_roll`] is conditional rather
274    /// than unconditional. Delete the `xtc_can_fire` guard there and
275    /// this goes red on the first token.
276    #[test]
277    fn a_chain_without_xtc_does_not_consume_a_draw_for_it() {
278        let params = SamplingParams {
279            temperature: 1.0,
280            ..SamplingParams::default()
281        };
282        let logits = spread_logits(32);
283        assert!(
284            Sampler::new(99).xtc_roll(&params).is_none(),
285            "the guard must refuse the draw, not merely ignore it"
286        );
287
288        // ONE draw per token, taken by hand off a generator that never
289        // heard of XTC. An unconditional roll makes `sample` consume
290        // two values per token, so the very first token comes off the
291        // SECOND draw and this diverges immediately.
292        let mut sampled_by_chain = Sampler::new(99);
293        let mut by_hand = Sampler::new(99);
294        for step in 0..16 {
295            let sampled = sampled_by_chain.sample(&logits, &params, PenaltyWindow::new(&[], &[]));
296            let probs = sampling_distribution(&logits, &params, PenaltyWindow::new(&[], &[]), None);
297            assert_eq!(
298                sampled,
299                by_hand.sample_from(&probs),
300                "token {step} came off a different position in the stream"
301            );
302        }
303        // And the two generators are still in lockstep afterwards.
304        assert_eq!(sampled_by_chain.uniform(), by_hand.uniform());
305    }
306
307    /// **The flag does something.** A chain that runs the temperature
308    /// before top-p keeps a different candidate set than the default,
309    /// which is the whole reason the order is worth exposing -- and the
310    /// reason getting it wrong is a silent quality regression rather
311    /// than an error.
312    ///
313    /// A hot temperature flattens the distribution, so a top-p applied
314    /// after it sums smaller probabilities and reaches `p` later,
315    /// keeping MORE candidates.
316    ///
317    /// The reported distribution must be the one that was DRAWN from,
318    /// not a second opinion computed beside it. Both go through
319    /// `sample_inner`, and this pins the consequence: the same seed
320    /// gives the same token whether or not the caller asked to see the
321    /// probabilities, and the token always has nonzero probability in
322    /// what is reported.
323    #[test]
324    fn the_reported_distribution_is_the_one_that_was_sampled() {
325        let logits: Vec<f32> = (0..64).map(|i| ((i * 7) % 13) as f32 * 0.4).collect();
326        let params = SamplingParams {
327            temperature: 0.9,
328            top_p: 0.95,
329            ..SamplingParams::default()
330        };
331
332        let quiet = Sampler::new(7).sample(&logits, &params, PenaltyWindow::new(&[], &[]));
333        let (loud, probs) =
334            Sampler::new(7).sample_reporting(&logits, &params, PenaltyWindow::new(&[], &[]), None);
335        assert_eq!(
336            quiet, loud,
337            "asking for the probabilities changed which token was drawn"
338        );
339
340        let probs = probs.expect("a real vocabulary reports a distribution");
341        assert_eq!(probs.len(), logits.len(), "one entry per vocabulary slot");
342        let total: f32 = probs.iter().sum();
343        assert!(
344            (total - 1.0).abs() < 1e-4,
345            "must be normalised, got {total}"
346        );
347        assert!(
348            probs[loud] > 0.0,
349            "the chosen token has zero probability in the distribution it came from"
350        );
351        // A filtered-out candidate is a zero, which is what "could not
352        // have been chosen" means, so top-p really did remove some.
353        assert!(
354            probs.contains(&0.0),
355            "top_p 0.95 kept every candidate, so this proves nothing"
356        );
357    }
358
359    /// Greedy reports too, and the token it reports is the argmax OF
360    /// the reported distribution -- read off the same vector rather
361    /// than decided separately, so the two cannot disagree.
362    #[test]
363    fn greedy_reports_the_distribution_its_answer_is_the_argmax_of() {
364        let logits = vec![0.1f32, 3.0, 0.2, 2.9];
365        let params = SamplingParams::default();
366        assert!(params.temperature <= 0.0, "default is greedy");
367
368        let (chosen, probs) =
369            Sampler::new(1).sample_reporting(&logits, &params, PenaltyWindow::new(&[], &[]), None);
370        let probs = probs.expect("a real vocabulary reports a distribution");
371        assert_eq!(chosen, 1, "the largest logit wins");
372        let best = probs
373            .iter()
374            .enumerate()
375            .max_by(|a, b| a.1.total_cmp(b.1))
376            .map(|(i, _)| i)
377            .unwrap();
378        assert_eq!(chosen, best, "the answer is not the argmax of the report");
379        // And it agrees with the plain entry point.
380        assert_eq!(
381            Sampler::new(1).sample(&logits, &params, PenaltyWindow::new(&[], &[])),
382            chosen
383        );
384    }
385
386    /// A device-folded argmax has no vocabulary behind it, so there is
387    /// nothing to report. `None` rather than a fabricated
388    /// single-candidate distribution, which would read as "the model
389    /// was certain" when nobody asked the model.
390    #[test]
391    fn a_device_folded_argmax_reports_no_distribution() {
392        let params = SamplingParams::default();
393        let (chosen, probs) =
394            Sampler::new(1).sample_reporting(&[42.0], &params, PenaltyWindow::new(&[], &[]), None);
395        assert_eq!(chosen, 42, "the singleton is the chosen id");
396        assert!(
397            probs.is_none(),
398            "a folded argmax must not fabricate a distribution"
399        );
400    }
401
402    #[test]
403    fn temperature_zero_accepts_precomputed_argmax_singleton() {
404        let mut sampler = Sampler::new(1);
405        let params = SamplingParams::default();
406        assert_eq!(
407            sampler.sample(&[42.0], &params, PenaltyWindow::new(&[], &[])),
408            42
409        );
410        // Non-greedy must not treat a singleton as a token id.
411        let sampled = SamplingParams {
412            temperature: 0.8,
413            ..SamplingParams::default()
414        };
415        // Softmax of a single logit → only token 0 is eligible.
416        assert_eq!(
417            sampler.sample(&[42.0], &sampled, PenaltyWindow::new(&[], &[])),
418            0
419        );
420    }
421
422    #[test]
423    fn temperature_zero_is_deterministic_greedy_argmax() {
424        let logits = vec![0.1, 0.9, 0.3, -0.2];
425        let params = SamplingParams::default();
426        let mut sampler = Sampler::new(42);
427        assert_eq!(
428            sampler.sample(&logits, &params, PenaltyWindow::new(&[], &[])),
429            1
430        );
431        // Must be deterministic regardless of RNG state advancing.
432        assert_eq!(
433            sampler.sample(&logits, &params, PenaltyWindow::new(&[], &[])),
434            1
435        );
436    }
437
438    #[test]
439    fn high_temperature_can_pick_a_non_argmax_token_over_many_draws() {
440        let logits = vec![1.0, 1.0, 1.0, 1.0];
441        let params = SamplingParams {
442            temperature: 1.0,
443            ..SamplingParams::default()
444        };
445        let mut sampler = Sampler::new(7);
446        let mut seen = std::collections::HashSet::new();
447        for _ in 0..200 {
448            seen.insert(sampler.sample(&logits, &params, PenaltyWindow::new(&[], &[])));
449        }
450        assert!(
451            seen.len() > 1,
452            "uniform logits at temperature=1.0 must produce more than one distinct token across 200 draws"
453        );
454    }
455
456    #[test]
457    fn top_k_one_is_equivalent_to_greedy() {
458        let logits = vec![0.1, 0.9, 0.3, -0.2];
459        let params = SamplingParams {
460            temperature: 1.0,
461            top_k: 1,
462            ..SamplingParams::default()
463        };
464        let mut sampler = Sampler::new(123);
465        for _ in 0..20 {
466            assert_eq!(
467                sampler.sample(&logits, &params, PenaltyWindow::new(&[], &[])),
468                1
469            );
470        }
471    }
472
473    #[test]
474    fn top_p_near_zero_is_equivalent_to_greedy() {
475        let logits = vec![0.1, 5.0, 0.3, -0.2];
476        let params = SamplingParams {
477            temperature: 1.0,
478            top_p: 0.001,
479            ..SamplingParams::default()
480        };
481        let mut sampler = Sampler::new(9);
482        for _ in 0..20 {
483            assert_eq!(
484                sampler.sample(&logits, &params, PenaltyWindow::new(&[], &[])),
485                1
486            );
487        }
488    }
489
490    #[test]
491    fn presence_and_frequency_penalties_reduce_seen_token_logits() {
492        let logits = vec![0.0, 5.0, 0.0];
493        let params = SamplingParams {
494            temperature: 1.0,
495            presence_penalty: 10.0,
496            frequency_penalty: 0.0,
497            ..SamplingParams::default()
498        };
499        let mut sampler = Sampler::new(1);
500        let mut counts = [0usize; 3];
501        for _ in 0..500 {
502            counts[sampler.sample(&logits, &params, PenaltyWindow::new(&[], &[1]))] += 1;
503        }
504        assert!(
505            counts[1] < 250,
506            "presence_penalty should discourage token 1; counts={counts:?}"
507        );
508
509        let params = SamplingParams {
510            temperature: 1.0,
511            presence_penalty: 0.0,
512            frequency_penalty: 10.0,
513            ..SamplingParams::default()
514        };
515        let mut sampler = Sampler::new(2);
516        counts = [0; 3];
517        for _ in 0..500 {
518            counts[sampler.sample(&logits, &params, PenaltyWindow::new(&[], &[1, 1, 1]))] += 1;
519        }
520        assert!(
521            counts[1] < 250,
522            "frequency_penalty should discourage repeated token 1; counts={counts:?}"
523        );
524    }
525
526    #[test]
527    fn repetition_penalty_reduces_probability_of_recently_seen_token() {
528        let logits = vec![0.0, 5.0, 0.0];
529        let params = SamplingParams {
530            temperature: 1.0,
531            repetition_penalty: 1000.0,
532            ..SamplingParams::default()
533        };
534        let mut sampler = Sampler::new(3);
535        let mut counts = [0usize; 3];
536        for _ in 0..500 {
537            counts[sampler.sample(&logits, &params, PenaltyWindow::new(&[], &[1]))] += 1;
538        }
539        assert!(
540            counts[1] < 250,
541            "heavily penalizing token 1 (already in history) should make it far less likely than its raw logit alone would suggest; got counts={counts:?}"
542        );
543    }
544
545    #[test]
546    fn low_seeds_do_not_bias_the_first_draw() {
547        // Every generation seeds a fresh `Sampler` (the server does it
548        // per request, from the caller's `seed`), so the FIRST draw off
549        // a freshly seeded generator is the one users actually see.
550        // Plain xorshift64 returns its own state, so seeds 1..4000 all
551        // produced a first draw in the bottom eighth of [0, 1) -- the
552        // first sampled token of every seeded request came off the
553        // bottom of the CDF.
554        let vocab = 8;
555        let logits = vec![0.0f32; vocab];
556        let params = SamplingParams {
557            temperature: 1.0,
558            ..SamplingParams::default()
559        };
560        let seeds = 4_000u64;
561        let mut counts = vec![0usize; vocab];
562        for seed in 1..=seeds {
563            counts[Sampler::new(seed).sample(&logits, &params, PenaltyWindow::new(&[], &[]))] += 1;
564        }
565        let expected = seeds as f64 / vocab as f64;
566        for (token, &c) in counts.iter().enumerate() {
567            assert!(
568                (c as f64 - expected).abs() < expected * 0.25,
569                "uniform logits: token {token} came up {c} times across {seeds} seeds, \
570                 expected about {expected:.0} (counts={counts:?})"
571            );
572        }
573    }
574
575    #[test]
576    fn the_published_distribution_is_the_one_sample_actually_draws_from() {
577        // `sampling_distribution` is load-bearing for lossless
578        // speculative verification: if it disagreed with what `sample`
579        // draws from, every accept/reject decision would be measured
580        // against the wrong target. Check them against each other
581        // empirically, with filters on so the two code paths have
582        // something to disagree about.
583        let logits = vec![0.4, 2.0, -1.0, 1.2, 0.9, -0.3];
584        let params = SamplingParams {
585            temperature: 0.8,
586            top_p: 0.9,
587            top_k: 4,
588            repetition_penalty: 1.3,
589            ..SamplingParams::default()
590        };
591        let history = [1usize, 4];
592        let claimed =
593            sampling_distribution(&logits, &params, PenaltyWindow::new(&[], &history), None);
594        assert!((claimed.iter().sum::<f32>() - 1.0).abs() < 1e-5);
595
596        let draws = 100_000;
597        let mut counts = vec![0usize; logits.len()];
598        let mut sampler = Sampler::new(0xC0FFEE);
599        for _ in 0..draws {
600            counts[sampler.sample(&logits, &params, PenaltyWindow::new(&[], &history))] += 1;
601        }
602        for (i, &c) in counts.iter().enumerate() {
603            let empirical = c as f64 / draws as f64;
604            assert!(
605                (empirical - claimed[i] as f64).abs() < 0.01,
606                "token {i}: sample() draws it {empirical:.4} of the time but \
607                 sampling_distribution claims {:.4}",
608                claimed[i]
609            );
610        }
611    }
612
613    #[test]
614    fn degenerate_all_zero_probability_falls_back_to_greedy() {
615        // top_k=1 combined with a top_p that would exclude even that
616        // one surviving token is a contradictory/degenerate
617        // configuration; must not panic or sample index 0 blindly.
618        let logits = vec![0.1, 0.9, 0.3, -0.2];
619        let params = SamplingParams {
620            temperature: 1.0,
621            top_k: 1,
622            top_p: 1.0,
623            ..SamplingParams::default()
624        };
625        let mut sampler = Sampler::new(1);
626        assert_eq!(
627            sampler.sample(&logits, &params, PenaltyWindow::new(&[], &[])),
628            1
629        );
630    }
631}