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(¶ms).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, ¶ms, PenaltyWindow::new(&[], &[]));
296 let probs = sampling_distribution(&logits, ¶ms, 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, ¶ms, PenaltyWindow::new(&[], &[]));
333 let (loud, probs) =
334 Sampler::new(7).sample_reporting(&logits, ¶ms, 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, ¶ms, 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, ¶ms, 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], ¶ms, 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], ¶ms, 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, ¶ms, PenaltyWindow::new(&[], &[])),
429 1
430 );
431 // Must be deterministic regardless of RNG state advancing.
432 assert_eq!(
433 sampler.sample(&logits, ¶ms, 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, ¶ms, 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, ¶ms, 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, ¶ms, 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, ¶ms, 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, ¶ms, 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, ¶ms, 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, ¶ms, 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, ¶ms, 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, ¶ms, 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, ¶ms, PenaltyWindow::new(&[], &[])),
628 1
629 );
630 }
631}