Skip to main content

ferrox_models/
dry.rs

1//! The DRY repetition sampler ("Don't Repeat Yourself"), ported from
2//! llama.cpp's `llama_sampler_dry` (`src/llama-sampler.cpp:3078-3400`),
3//! which is itself a port of Koboldcpp PR 982 by pi6am.
4//!
5//! # What it does that the repetition penalty cannot
6//!
7//! `penalties` looks at SINGLE tokens: a token that occurred in the
8//! window is made less likely, once. DRY looks at SEQUENCES. It asks,
9//! for every token the model could emit next, "how long a repetition of
10//! earlier context would emitting this token continue?", and penalises
11//! by `multiplier * base ^ (that length - allowed_length)`. A model
12//! looping on a four-token phrase is not emitting one over-represented
13//! token, so the repetition penalty barely moves it while DRY's
14//! exponential grows every time round the loop.
15//!
16//! # The three parts, and where each one comes from
17//!
18//! * [`DryBreakers`] is `get_overlapping_token_sequences`
19//!   (`src/llama-sampler.cpp:3095`): the sequence breakers a caller
20//!   gives as STRINGS, tokenised against the loaded vocabulary. A
21//!   breaker is a point the repetition detector refuses to look past,
22//!   so `"\n"` stops one paragraph's phrasing from penalising the next.
23//!   It has to be done against the model's own vocabulary because a
24//!   breaker string is usually not a whole token: `":"` may only ever
25//!   appear as the tail of `"foo:"`, so the entry is keyed on the token
26//!   that CONTAINS it and carries whatever tokens must follow.
27//! * [`DryParams::penalties`] is `llama_sampler_dry_apply`
28//!   (`src/llama-sampler.cpp:3151`), including the reverse Z-algorithm
29//!   that finds, in one linear pass, how long a suffix ending at each
30//!   position also occurs elsewhere in the window.
31//! * [`crate::sampler_chain::Candidates::dry`] subtracts the result.
32//!
33//! # Why the parameters are a struct with private fields
34//!
35//! Because enabling DRY without tokenising its breakers is a silent
36//! wrong answer, not an error: the sampler runs, penalises across
37//! newlines it was told to stop at, and nothing says so. So there is no
38//! way to build an ENABLED [`DryParams`] without handing it a
39//! [`DryBreakers`], and the only way to build a non-empty
40//! [`DryBreakers`] is [`DryBreakers::from_vocab`], which needs a
41//! vocabulary. A caller that genuinely wants none writes
42//! [`DryBreakers::none`] and that is visible in the diff.
43
44use std::collections::HashMap;
45use std::fmt;
46use std::sync::Arc;
47
48use crate::penalty_window::PenaltyWindow;
49
50/// llama.cpp's `MAX_CHAR_LEN` (`src/llama-sampler.cpp:3382`): a
51/// sequence breaker longer than this is truncated rather than refused.
52const MAX_CHAR_LEN: usize = 40;
53
54/// llama.cpp's `MAX_SEQ_LEN` (`src/llama-sampler.cpp:3383`): the tail
55/// of a breaker is clamped to this many tokens, which is what keeps the
56/// restart-sequence scan in [`DryParams::penalties`] linear in the
57/// window length rather than quadratic.
58const MAX_SEQ_LEN: usize = 20;
59
60/// llama.cpp's `FLOAT_MAX_LOG` (`src/llama-sampler.cpp:3312`), the
61/// approximate natural log of `FLT_MAX`. The exponent is clamped to
62/// `FLOAT_MAX_LOG / ln(base)` so `base ^ exponent` cannot overflow to
63/// infinity on a long repetition.
64const FLOAT_MAX_LOG: f32 = 88.722_84;
65
66/// llama.cpp's default sequence breakers
67/// (`common/common.h:259`, `dry_sequence_breakers = {"\n", ":", "\"", "*"}`).
68///
69/// One definition, read by the CLI flag's default and by the server's,
70/// because two lists that must agree about four strings is this repo's
71/// dominant defect shape at its smallest.
72pub const DEFAULT_SEQUENCE_BREAKERS: [&str; 4] = ["\n", ":", "\"", "*"];
73
74/// What DRY needs from a loaded model's vocabulary, and nothing else.
75///
76/// Three methods rather than a dependency on any particular tokenizer
77/// type: `ferrox-models` has four of those and `ferrox-server` wraps
78/// them in a fifth, so a concrete type here would either pick one or
79/// force an enum that grows with every new vocabulary format.
80pub trait DryVocab {
81    /// The number of token ids in the vocabulary. Every id in
82    /// `0..n_tokens()` must be valid for [`Self::detokenize`].
83    fn n_tokens(&self) -> usize;
84
85    /// The text of ONE token, special tokens rendered rather than
86    /// hidden -- llama.cpp's `vocab.detokenize({token_id}, true)`
87    /// (`src/llama-sampler.cpp:3097`).
88    fn detokenize(&self, token: usize) -> String;
89
90    /// `text` as token ids, with no BOS and no special-token parsing --
91    /// llama.cpp's `vocab.tokenize(str.substr(i), false, false)`
92    /// (`src/llama-sampler.cpp:3114`).
93    fn tokenize(&self, text: &str) -> Vec<usize>;
94}
95
96/// Sequence breakers, tokenised against one model's vocabulary.
97///
98/// Keyed on the token that HOLDS the start of the breaker string, with
99/// the tokens that must follow it as the value. An empty tail means the
100/// token by itself is a whole breaker, which is both the common case and
101/// the one that suppresses a penalty entirely (see
102/// [`DryParams::penalties`] step 4).
103///
104/// A token may head more than one breaker -- llama.cpp keeps a
105/// `std::unordered_multimap` for exactly that -- so the value is a list
106/// of tails, deduplicated as upstream deduplicates it.
107#[derive(Debug, Clone, Default, PartialEq, Eq)]
108pub struct DryBreakers {
109    /// The strings this was built from, kept so a cache key can name
110    /// the configuration without hashing the whole tokenised map.
111    raw: Vec<String>,
112    heads: HashMap<usize, Vec<Vec<usize>>>,
113}
114
115impl DryBreakers {
116    /// No breakers: the repetition scan may look back across anything.
117    ///
118    /// This is what `--dry-sequence-breaker none` asks for upstream, and
119    /// what a caller with no vocabulary to tokenise against must write
120    /// explicitly rather than get by accident.
121    pub fn none() -> Self {
122        DryBreakers::default()
123    }
124
125    /// The strings these breakers were built from, in the order given.
126    pub fn raw(&self) -> &[String] {
127        &self.raw
128    }
129
130    pub fn is_empty(&self) -> bool {
131        self.heads.is_empty()
132    }
133
134    /// Tokenise `breakers` against `vocab`.
135    ///
136    /// This is `get_overlapping_token_sequences`
137    /// (`src/llama-sampler.cpp:3095-3134`), and the "overlapping" is the
138    /// whole point: a breaker is rarely a token of its own. The scan
139    /// walks every token in the vocabulary and asks either
140    ///
141    /// * does this token's text CONTAIN the breaker? Then the token
142    ///   alone breaks the sequence (an empty tail); or
143    /// * does this token's text END with a prefix of the breaker? Then
144    ///   the token breaks the sequence only when the REST of the breaker
145    ///   follows, and that rest is tokenised and kept as the tail.
146    ///
147    /// **Cost.** One `detokenize` per vocabulary entry per breaker
148    /// string, which is upstream's cost too (`llama_sampler_init_dry` at
149    /// `:3399` runs it per sampler, and llama.cpp's server builds a
150    /// sampler per request). It is paid only when DRY is switched on --
151    /// [`DryParams::new`] is the only caller and it is only reached for
152    /// a non-zero multiplier.
153    ///
154    /// An empty breaker is skipped rather than refused, as upstream
155    /// skips it (`:3388`); one longer than [`MAX_CHAR_LEN`] bytes is
156    /// truncated.
157    pub fn from_vocab(vocab: &dyn DryVocab, breakers: &[String]) -> Self {
158        let mut out = DryBreakers {
159            raw: breakers.to_vec(),
160            heads: HashMap::new(),
161        };
162        for breaker in breakers {
163            if breaker.is_empty() {
164                continue;
165            }
166            let mut bytes = breaker.as_bytes();
167            if bytes.len() > MAX_CHAR_LEN {
168                bytes = &bytes[..MAX_CHAR_LEN];
169            }
170            out.add_overlapping(vocab, bytes);
171        }
172        out
173    }
174
175    /// Breakers given directly as token sequences.
176    ///
177    /// This is `llama_sampler_init_dry_testing`
178    /// (`src/llama-sampler.cpp:3420`), the entry point llama.cpp's own
179    /// `tests/test-sampling.cpp` uses, and it exists here for the same
180    /// reason: the golden values in [`mod tests`](self::tests) were
181    /// produced by running that function against `libllama`, and a test
182    /// that had to build a vocabulary first would be testing the
183    /// tokenizer as well as the sampler.
184    ///
185    /// The first token of each sequence is the head, the rest the tail,
186    /// exactly as upstream splits it.
187    pub fn from_token_sequences(sequences: &[Vec<usize>]) -> Self {
188        let mut heads: HashMap<usize, Vec<Vec<usize>>> = HashMap::new();
189        for sequence in sequences {
190            let Some((&head, tail)) = sequence.split_first() else {
191                continue;
192            };
193            let mut tail = tail.to_vec();
194            tail.truncate(MAX_SEQ_LEN);
195            let entry = heads.entry(head).or_default();
196            if !entry.contains(&tail) {
197                entry.push(tail);
198            }
199        }
200        DryBreakers {
201            raw: Vec::new(),
202            heads,
203        }
204    }
205
206    fn tails(&self, token: usize) -> Option<&[Vec<usize>]> {
207        self.heads.get(&token).map(|v| v.as_slice())
208    }
209
210    /// True when this token is a breaker all by itself, which is what
211    /// makes upstream skip its penalty entirely (`:3325-3334`).
212    fn is_single_token_breaker(&self, token: usize) -> bool {
213        self.heads
214            .get(&token)
215            .is_some_and(|tails| tails.iter().any(|tail| tail.is_empty()))
216    }
217
218    fn push(&mut self, head: usize, tail: Vec<usize>) {
219        let entry = self.heads.entry(head).or_default();
220        // Upstream's `equal_range` duplicate check (`:3117-3126`).
221        if !entry.contains(&tail) {
222            entry.push(tail);
223        }
224    }
225
226    /// One breaker string against the whole vocabulary.
227    ///
228    /// Byte-wise rather than char-wise because upstream is:
229    /// `word.find(str[0], pos + 1)` indexes `std::string` by byte, and a
230    /// token's text is not guaranteed to split on a character boundary
231    /// where the breaker does. The tail is only tokenised when the
232    /// remaining bytes are valid UTF-8, which they are whenever the
233    /// breaker itself is.
234    fn add_overlapping(&mut self, vocab: &dyn DryVocab, breaker: &[u8]) {
235        for token in 0..vocab.n_tokens() {
236            let word = vocab.detokenize(token);
237            let word = word.as_bytes();
238            if contains_subslice(word, breaker) {
239                self.push(token, Vec::new());
240                continue;
241            }
242            let mut from = 0usize;
243            while from < word.len() {
244                let Some(offset) = word[from..].iter().position(|&c| c == breaker[0]) else {
245                    break;
246                };
247                let pos = from + offset;
248                from = pos + 1;
249                // How many bytes of the breaker this occurrence
250                // matched before running off the end of the token.
251                let mut i = 1usize;
252                let mut matched = true;
253                while i < breaker.len() && i + pos < word.len() {
254                    if word[pos + i] != breaker[i] {
255                        matched = false;
256                        break;
257                    }
258                    i += 1;
259                }
260                if !matched {
261                    continue;
262                }
263                let Ok(rest) = std::str::from_utf8(&breaker[i..]) else {
264                    // A breaker whose tail starts mid-character cannot
265                    // be tokenised; upstream would hand the tokenizer
266                    // the same broken bytes and get nothing useful.
267                    continue;
268                };
269                let mut tail = vocab.tokenize(rest);
270                tail.truncate(MAX_SEQ_LEN);
271                self.push(token, tail);
272            }
273        }
274    }
275}
276
277fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
278    if needle.is_empty() || needle.len() > haystack.len() {
279        return needle.is_empty();
280    }
281    haystack.windows(needle.len()).any(|w| w == needle)
282}
283
284/// One request's DRY configuration.
285///
286/// Fields are private and there is no `Default` that can be enabled:
287/// [`DryParams::off`] is the only way to get a disabled one and
288/// [`DryParams::new`] the only way to get an enabled one, and it takes
289/// the breakers. See the module docs for why that is a type invariant
290/// rather than a convention.
291#[derive(Debug, Clone)]
292pub struct DryParams {
293    /// llama.cpp's `dry_multiplier` (`common/common.h:242`), `0.0` off.
294    multiplier: f32,
295    /// llama.cpp's `dry_base` (`common/common.h:243`), default 1.75.
296    /// Below 1.0 disables DRY, exactly as upstream's guard reads
297    /// (`src/llama-sampler.cpp:3154`).
298    base: f32,
299    /// llama.cpp's `dry_allowed_length` (`common/common.h:244`),
300    /// default 2: a repetition this long or shorter is free.
301    allowed_length: i32,
302    /// llama.cpp's `dry_penalty_last_n` (`common/common.h:245`),
303    /// default `-1` meaning "the whole context"; `0` disables.
304    penalty_last_n: i32,
305    /// llama.cpp's `n_ctx_train`, the ceiling every window is clamped
306    /// to (`src/llama-sampler.cpp:3158-3159`).
307    total_context_size: usize,
308    /// Shared rather than cloned: this is a map over the vocabulary and
309    /// [`crate::sampling::SamplingParams`] is cloned per request.
310    breakers: Arc<DryBreakers>,
311}
312
313impl DryParams {
314    /// DRY switched off, which is llama.cpp's default
315    /// (`dry_multiplier = 0.0f`, `common/common.h:242`).
316    pub fn off() -> Self {
317        DryParams {
318            multiplier: 0.0,
319            base: 1.75,
320            allowed_length: 2,
321            penalty_last_n: -1,
322            total_context_size: 0,
323            breakers: Arc::new(DryBreakers::none()),
324        }
325    }
326
327    /// DRY with the given configuration and breakers.
328    ///
329    /// `total_context_size` is llama.cpp's `n_ctx_train`: the ceiling a
330    /// `penalty_last_n` of `-1` resolves to, and the clamp every window
331    /// is passed through.
332    pub fn new(
333        multiplier: f32,
334        base: f32,
335        allowed_length: i32,
336        penalty_last_n: i32,
337        total_context_size: usize,
338        breakers: DryBreakers,
339    ) -> Self {
340        DryParams {
341            multiplier,
342            base,
343            allowed_length,
344            penalty_last_n,
345            total_context_size,
346            breakers: Arc::new(breakers),
347        }
348    }
349
350    /// The single predicate for "DRY does nothing", transcribed from the
351    /// guard both `llama_sampler_dry_accept` (`:3143`) and
352    /// `llama_sampler_dry_apply` (`:3154`) open with, and the same three
353    /// conditions `llama_sampler_init_dry` reads to return an empty
354    /// sampler (`:3387`).
355    ///
356    /// One function rather than the three copies upstream has, because
357    /// this is read by the chain, by the CLI banner and by the server's
358    /// refusal, and a disagreement between them is a sampler that is
359    /// reported on but not run.
360    pub fn is_enabled(&self) -> bool {
361        self.multiplier != 0.0 && self.base >= 1.0 && self.penalty_last_n != 0
362    }
363
364    pub fn multiplier(&self) -> f32 {
365        self.multiplier
366    }
367
368    pub fn base(&self) -> f32 {
369        self.base
370    }
371
372    pub fn allowed_length(&self) -> i32 {
373        self.allowed_length
374    }
375
376    pub fn penalty_last_n(&self) -> i32 {
377        self.penalty_last_n
378    }
379
380    pub fn total_context_size(&self) -> usize {
381        self.total_context_size
382    }
383
384    pub fn breakers(&self) -> &DryBreakers {
385        &self.breakers
386    }
387
388    /// How many of the most recent tokens the scan looks at:
389    /// `dry_penalty_last_n` resolved against the context size, exactly
390    /// as `src/llama-sampler.cpp:3158` resolves it.
391    fn effective_last_n(&self) -> usize {
392        if self.penalty_last_n == -1 {
393            self.total_context_size
394        } else {
395            self.penalty_last_n.max(0) as usize
396        }
397    }
398
399    /// The penalty to SUBTRACT from each token's logit, keyed by token
400    /// id. Empty when DRY is off or has nothing to say.
401    ///
402    /// `llama_sampler_dry_apply` (`src/llama-sampler.cpp:3151-3345`),
403    /// in its four documented steps:
404    ///
405    /// 1. walk backwards for a sequence breaker, and clamp how far a
406    ///    repetition may reach (`rep_limit`);
407    /// 2. the reverse Z-algorithm, which fills `repeat_count[i]` with
408    ///    the length of the window's suffix that also occurs ending at
409    ///    position `i`;
410    /// 3. for each non-zero count, the token that WOULD extend that
411    ///    repetition is the one to penalise, and the longest repetition
412    ///    ending in it wins;
413    /// 4. `multiplier * base ^ (length - allowed_length)`, skipped for a
414    ///    token that is a whole sequence breaker by itself.
415    ///
416    /// `history` is the same [`PenaltyWindow`] the repetition penalties
417    /// use, for the same reason: llama.cpp's DRY sampler is fed by
418    /// `common_sampler_accept`, which both front ends call for prompt
419    /// tokens as well as generated ones (see [`crate::penalty_window`]).
420    pub fn penalties(&self, history: PenaltyWindow<'_>) -> HashMap<usize, f32> {
421        let empty = HashMap::new();
422        if !self.is_enabled() {
423            return empty;
424        }
425        // `last_tokens` is a ring buffer of `effective_last_n` capacity
426        // upstream, so its size is already clamped; here the window is
427        // asked for that many and clamped again by the context size,
428        // which is upstream's second `std::min` (`:3159`).
429        let effective = self.effective_last_n();
430        let recent: Vec<usize> = history
431            .recent(effective.min(self.total_context_size))
432            .collect();
433        let n = recent.len();
434        if n as i32 <= self.allowed_length {
435            return empty;
436        }
437        // `rat(i)`: llama.cpp's `ring_buffer::rat`, i tokens back from
438        // the most recent. `recent` is oldest-first.
439        let rat = |i: usize| recent[n - 1 - i];
440
441        // Step 1: how far back a repetition may reach before it hits a
442        // sequence breaker.
443        let mut rep_limit = n as i32;
444        for i in 0..n {
445            let Some(tails) = self.breakers.tails(rat(i)) else {
446                continue;
447            };
448            let mut longest_match: i32 = -1;
449            for tail in tails {
450                let seq_len = tail.len() as i32;
451                // `<= i`: the tail has to fit in the window behind the
452                // head, and the head itself is already matched.
453                if seq_len <= longest_match || seq_len > i as i32 {
454                    continue;
455                }
456                let matched = (0..tail.len()).all(|offset| tail[offset] == rat(i - offset - 1));
457                if matched {
458                    longest_match = seq_len;
459                }
460            }
461            if longest_match >= 0 {
462                rep_limit = i as i32 - longest_match;
463                break;
464            }
465        }
466        if rep_limit < self.allowed_length {
467            return empty;
468        }
469
470        // Step 2: the reverse Z-algorithm (`:3243-3283`).
471        let mut repeat_count = vec![0i32; n];
472        let last = n - 1;
473        let mut lt = 0usize;
474        let mut rt = 0usize;
475        for k in 1..n {
476            if k > rt {
477                // Outside the current Z-box: compare naively.
478                let mut matched = 0usize;
479                while matched + k < n && rat(matched) == rat(matched + k) {
480                    matched += 1;
481                }
482                repeat_count[last - k] = (matched as i32).min(rep_limit);
483                if matched > 0 {
484                    lt = k;
485                    rt = k + matched - 1;
486                }
487            } else {
488                let p = k - lt;
489                let right_part_len = (rt - k + 1) as i32;
490                if repeat_count[last - p] < right_part_len {
491                    repeat_count[last - k] = repeat_count[last - p].min(rep_limit);
492                } else {
493                    let mut i = rt + 1;
494                    while i < n && rat(i) == rat(i - k) {
495                        i += 1;
496                    }
497                    repeat_count[last - k] = ((i - k) as i32).min(rep_limit);
498                    lt = k;
499                    rt = i - 1;
500                }
501            }
502        }
503
504        // Step 3: the token that would EXTEND each repetition
505        // (`:3296-3308`). `repeat_count[i]` is about the window position
506        // `i`, so the token that follows it is `recent[i + 1]`, which is
507        // upstream's `rat(last_n_repeat - 2 - i)`.
508        let mut max_token_repeat: HashMap<usize, i32> = HashMap::new();
509        for i in 0..n - 1 {
510            let repeat_len = repeat_count[i];
511            if repeat_len < self.allowed_length {
512                continue;
513            }
514            let token = recent[i + 1];
515            let slot = max_token_repeat.entry(token).or_insert(repeat_len);
516            if *slot < repeat_len {
517                *slot = repeat_len;
518            }
519        }
520
521        // Step 4: the exponential (`:3310-3343`).
522        let mut max_exponent = 0i32;
523        if self.base > 1.000_001 {
524            max_exponent = (FLOAT_MAX_LOG / self.base.ln()) as i32;
525        }
526        let mut out = HashMap::with_capacity(max_token_repeat.len());
527        for (&token, &max_repeat) in &max_token_repeat {
528            // A token that is a whole sequence breaker is exempt: it is
529            // the thing repetition is allowed to run into.
530            if self.breakers.is_single_token_breaker(token) {
531                continue;
532            }
533            let mut repeat_exp = max_repeat - self.allowed_length;
534            if max_exponent > 0 && repeat_exp > max_exponent {
535                repeat_exp = max_exponent;
536            }
537            // `std::pow(float, int)` promotes to double upstream, and
538            // the product is narrowed back to float on assignment;
539            // computing it in f32 throughout drifts on long repeats.
540            let penalty = (self.multiplier as f64) * (self.base as f64).powi(repeat_exp);
541            out.insert(token, penalty as f32);
542        }
543        out
544    }
545}
546
547/// DRY as a caller SPELLS it: four numbers and a list of strings, with
548/// the breakers not yet tokenised.
549///
550/// This is the shape a CLI flag set and an HTTP request body arrive in,
551/// and it exists so that both front ends resolve them the same way.
552/// [`Self::resolve`] is the only bridge to [`DryParams`], and it is the
553/// one place that decides what happens when DRY is asked for and there
554/// is no vocabulary to tokenise the breakers against.
555#[derive(Debug, Clone, PartialEq)]
556pub struct DryRequest {
557    pub multiplier: f32,
558    pub base: f32,
559    pub allowed_length: i32,
560    pub penalty_last_n: i32,
561    pub sequence_breakers: Vec<String>,
562}
563
564impl Default for DryRequest {
565    /// llama.cpp's defaults (`common/common.h:242-245, 259`), which are
566    /// DISABLED: `dry_multiplier` is 0.
567    fn default() -> Self {
568        DryRequest {
569            multiplier: 0.0,
570            base: 1.75,
571            allowed_length: 2,
572            penalty_last_n: -1,
573            sequence_breakers: DEFAULT_SEQUENCE_BREAKERS
574                .iter()
575                .map(|s| s.to_string())
576                .collect(),
577        }
578    }
579}
580
581/// DRY was asked for and there is no vocabulary to tokenise its
582/// sequence breakers against.
583///
584/// A refusal rather than a silent fallback to no breakers, because DRY
585/// without its breakers is not DRY with fewer features: it scans across
586/// the newline the caller told it to stop at, and the output looks like
587/// working DRY. The only checkpoints this can happen on are the ones
588/// with no real vocabulary at all (`ByteTokenizer`, the synthetic-weight
589/// demo model), where no sampler configured by text could mean what it
590/// says anyway.
591#[derive(Debug, Clone, Copy, PartialEq, Eq)]
592pub struct DryVocabMissing;
593
594impl fmt::Display for DryVocabMissing {
595    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596        f.write_str(
597            "the DRY sampler needs the model's vocabulary to tokenise its sequence \
598             breakers, and this checkpoint has none loaded. Pass \
599             `--dry-sequence-breaker none` to run DRY with no breakers, or \
600             `--dry-multiplier 0` to switch DRY off",
601        )
602    }
603}
604
605impl std::error::Error for DryVocabMissing {}
606
607impl DryRequest {
608    /// The same three conditions [`DryParams::is_enabled`] reads, asked
609    /// before the breakers have been tokenised -- so the expensive
610    /// vocabulary walk is skipped for the overwhelming majority of
611    /// requests, which leave DRY off.
612    pub fn is_enabled(&self) -> bool {
613        self.multiplier != 0.0 && self.base >= 1.0 && self.penalty_last_n != 0
614    }
615
616    /// Tokenise the breakers and build the sampler's parameters.
617    ///
618    /// `vocab` is `None` when the loaded checkpoint has no real
619    /// vocabulary. That is fine while DRY is off and a refusal when it
620    /// is on; see [`DryVocabMissing`].
621    ///
622    /// An EMPTY breaker list needs no vocabulary either: it is the
623    /// caller's `--dry-sequence-breaker none`, and there is nothing to
624    /// tokenise.
625    pub fn resolve(
626        &self,
627        vocab: Option<&dyn DryVocab>,
628        total_context_size: usize,
629    ) -> Result<DryParams, DryVocabMissing> {
630        if !self.is_enabled() {
631            return Ok(DryParams::off());
632        }
633        let breakers = match (self.sequence_breakers.is_empty(), vocab) {
634            (true, _) => DryBreakers::none(),
635            (false, Some(vocab)) => DryBreakers::from_vocab(vocab, &self.sequence_breakers),
636            (false, None) => return Err(DryVocabMissing),
637        };
638        Ok(DryParams::new(
639            self.multiplier,
640            self.base,
641            self.allowed_length,
642            self.penalty_last_n,
643            total_context_size,
644            breakers,
645        ))
646    }
647}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652
653    /// Every expected value below was produced by calling the real
654    /// `llama_sampler_init_dry_testing` in `libllama` (llama.cpp b7650,
655    /// `.scratch/llama.cpp`) from a small C++ harness and reading the
656    /// logits back, not by reasoning about the algorithm. The harness
657    /// applied the sampler to logits `ln(p)` for the probabilities
658    /// llama.cpp's own `tests/test-sampling.cpp:357-361` uses, so these
659    /// cases are upstream's cases with the arithmetic checked against
660    /// upstream's code as it actually runs.
661    fn dry(
662        multiplier: f32,
663        base: f32,
664        allowed: i32,
665        last_n: i32,
666        breakers: &[Vec<usize>],
667    ) -> DryParams {
668        DryParams::new(
669            multiplier,
670            base,
671            allowed,
672            last_n,
673            1024,
674            DryBreakers::from_token_sequences(breakers),
675        )
676    }
677
678    fn penalties_of(params: &DryParams, history: &[usize]) -> Vec<(usize, f32)> {
679        let mut out: Vec<(usize, f32)> = params
680            .penalties(PenaltyWindow::new(&[], history))
681            .into_iter()
682            .collect();
683        out.sort_unstable_by_key(|&(token, _)| token);
684        out
685    }
686
687    /// The window `a b c a b` repeats its own two-token suffix `a b` at
688    /// its head, so emitting `c` would extend that repetition to three.
689    /// At `allowed_length = 2` the exponent is `2 - 2 = 0`, so the
690    /// penalty is the bare multiplier.
691    ///
692    /// libllama: logit `ln(0.25) = -1.3862944` became `-2.3862944` for
693    /// token 2 and nothing else moved.
694    #[test]
695    fn a_repeated_suffix_penalises_only_the_token_that_would_extend_it() {
696        let params = dry(1.0, 1.1, 2, 5, &[]);
697        assert_eq!(penalties_of(&params, &[0, 1, 2, 0, 1]), vec![(2, 1.0)]);
698
699        // The multiplier scales it linearly: libllama gave -3.6094379
700        // from ln(0.2) = -1.6094379, i.e. exactly 2.0.
701        let doubled = dry(2.0, 1.1, 2, 5, &[]);
702        assert_eq!(penalties_of(&doubled, &[0, 1, 2, 0, 1]), vec![(2, 2.0)]);
703    }
704
705    /// A window no longer than `allowed_length` cannot contain a
706    /// repetition worth penalising, and upstream returns before it looks
707    /// (`src/llama-sampler.cpp:3161`).
708    ///
709    /// libllama left all four logits at `ln(0.25)`.
710    #[test]
711    fn a_window_within_the_allowed_length_is_never_penalised() {
712        let params = dry(1.0, 1.1, 2, 4, &[]);
713        assert!(penalties_of(&params, &[0, 1]).is_empty());
714    }
715
716    /// `allowed_length` really is a free allowance: the window
717    /// `0 1 2 3 4 0 1` repeats a two-token suffix, and at
718    /// `allowed_length = 4` that is below the threshold, so nothing is
719    /// penalised at all.
720    ///
721    /// libllama left all five logits at `ln(0.2)`.
722    #[test]
723    fn a_repetition_shorter_than_the_allowed_length_is_free() {
724        let params = dry(1.0, 1.1, 4, 7, &[]);
725        assert!(penalties_of(&params, &[0, 1, 2, 3, 4, 0, 1]).is_empty());
726    }
727
728    /// The exponent is the repetition length MINUS the allowance, and it
729    /// grows the penalty geometrically -- which is the whole reason DRY
730    /// exists where a flat repetition penalty does not.
731    ///
732    /// Window `0 1 2 3 0 1 2 3 0 1 2` (11 tokens): its seven-token
733    /// suffix `0 1 2 3 0 1 2` also occurs at the head, so emitting `3`
734    /// would extend a repetition of length 7. At `allowed_length = 2`
735    /// and llama.cpp's default `base = 1.75` that is
736    /// `0.8 * 1.75^5 = 13.130469`.
737    ///
738    /// libllama: logit `0.0` became `-13.1304693` for token 3, and no
739    /// other logit moved.
740    #[test]
741    fn the_penalty_is_multiplier_times_base_to_the_length_over_the_allowance() {
742        let params = dry(0.8, 1.75, 2, -1, &[]);
743        let got = penalties_of(&params, &[0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2]);
744        assert_eq!(got.len(), 1, "only token 3 extends the repetition: {got:?}");
745        assert_eq!(got[0].0, 3);
746        assert!(
747            (got[0].1 - 13.130_469).abs() < 1e-4,
748            "0.8 * 1.75^5 = 13.130469, got {}",
749            got[0].1
750        );
751    }
752
753    /// A sequence breaker stops the scan, and a breaker that is a whole
754    /// token by itself is additionally exempt from being penalised.
755    ///
756    /// Window `0 1 3 4 0 1` with `3` as a single-token breaker: the
757    /// suffix `0 1` does repeat, and the token that would extend it is
758    /// `3` -- which is the breaker, so upstream skips it
759    /// (`src/llama-sampler.cpp:3325-3334`) and NOTHING is penalised.
760    ///
761    /// libllama left all five logits at `ln(0.2)`. Without the breaker
762    /// the same window penalises token 3, which is what the second half
763    /// asserts: a test that only checked the breaker case would pass for
764    /// an implementation that penalised nothing ever.
765    #[test]
766    fn a_single_token_sequence_breaker_is_never_itself_penalised() {
767        let with_breaker = dry(1.0, 1.1, 2, 6, &[vec![3]]);
768        assert!(penalties_of(&with_breaker, &[0, 1, 3, 4, 0, 1]).is_empty());
769
770        let without = dry(1.0, 1.1, 2, 6, &[]);
771        assert_eq!(penalties_of(&without, &[0, 1, 3, 4, 0, 1]), vec![(3, 1.0)]);
772    }
773
774    /// Each of the three switches upstream reads turns DRY off on its
775    /// own (`src/llama-sampler.cpp:3154`), and
776    /// [`DryParams::is_enabled`] is the one place that decides.
777    #[test]
778    fn a_zero_multiplier_a_base_below_one_or_a_zero_window_all_disable_it() {
779        let history = [0usize, 1, 2, 0, 1];
780        for params in [
781            DryParams::off(),
782            dry(0.0, 1.75, 2, -1, &[]),
783            dry(1.0, 0.5, 2, -1, &[]),
784            dry(1.0, 1.75, 2, 0, &[]),
785        ] {
786            assert!(!params.is_enabled(), "{params:?} must be disabled");
787            assert!(
788                params
789                    .penalties(PenaltyWindow::new(&[], &history))
790                    .is_empty(),
791                "{params:?} penalised something while disabled"
792            );
793        }
794        assert!(dry(1.0, 1.75, 2, -1, &[]).is_enabled());
795    }
796
797    /// `dry_penalty_last_n` bounds how far back the scan looks, so a
798    /// repetition that has fallen out of the window stops being
799    /// penalised. `-1` means the whole context.
800    #[test]
801    fn the_scan_only_sees_the_last_n_tokens() {
802        let history = [0usize, 1, 2, 0, 1];
803        // The whole window: the `0 1` prefix is visible, so token 2 is
804        // penalised (the case pinned against libllama above).
805        assert_eq!(
806            penalties_of(&dry(1.0, 1.1, 2, 5, &[]), &history),
807            vec![(2, 1.0)]
808        );
809        // Only the last three tokens `2 0 1`: no repetition left.
810        assert!(penalties_of(&dry(1.0, 1.1, 2, 3, &[]), &history).is_empty());
811        // `-1` resolves to the context size, which is wide enough here.
812        assert_eq!(
813            penalties_of(&dry(1.0, 1.1, 2, -1, &[]), &history),
814            vec![(2, 1.0)]
815        );
816    }
817
818    /// The window is the tail of PROMPT ++ GENERATED, like every other
819    /// penalty in this engine: llama.cpp feeds prompt tokens to the DRY
820    /// sampler through `common_sampler_accept` exactly as it feeds
821    /// generated ones (see [`crate::penalty_window`]).
822    ///
823    /// The same five tokens split differently across the seam must give
824    /// the same answer, or the seam is being treated as a boundary DRY
825    /// does not have.
826    #[test]
827    fn the_scan_reads_across_the_prompt_and_generation_seam() {
828        let params = dry(1.0, 1.1, 2, 5, &[]);
829        let whole = params.penalties(PenaltyWindow::new(&[], &[0, 1, 2, 0, 1]));
830        let split = params.penalties(PenaltyWindow::new(&[0, 1, 2], &[0, 1]));
831        let all_prompt = params.penalties(PenaltyWindow::new(&[0, 1, 2, 0, 1], &[]));
832        assert_eq!(whole, split);
833        assert_eq!(whole, all_prompt);
834        assert_eq!(whole.len(), 1);
835    }
836
837    /// The exponent is clamped so `base ^ exponent` cannot overflow to
838    /// infinity, which would make the penalty NaN once subtracted from
839    /// a `-inf` logit (`src/llama-sampler.cpp:3310-3318`).
840    ///
841    /// A 400-token repetition at base 1.75 would ask for `1.75 ^ 398`,
842    /// which is `inf` in an f32.
843    #[test]
844    fn a_very_long_repetition_clamps_the_exponent_rather_than_overflowing() {
845        let params = dry(1.0, 1.75, 2, -1, &[]);
846        let mut history: Vec<usize> = vec![0usize; 400];
847        history.push(1);
848        history.extend(std::iter::repeat_n(0usize, 400));
849        let penalties = params.penalties(PenaltyWindow::new(&[], &history));
850        for (token, penalty) in penalties {
851            assert!(
852                penalty.is_finite(),
853                "token {token} got a non-finite penalty {penalty}"
854            );
855        }
856    }
857
858    /// A vocabulary whose tokens are whole words, so a breaker string
859    /// can be contained in one token, span the end of one, or be absent.
860    struct WordVocab {
861        words: Vec<&'static str>,
862    }
863
864    impl DryVocab for WordVocab {
865        fn n_tokens(&self) -> usize {
866            self.words.len()
867        }
868        fn detokenize(&self, token: usize) -> String {
869            self.words[token].to_string()
870        }
871        fn tokenize(&self, text: &str) -> Vec<usize> {
872            // Longest-match-first over the same word list, which is
873            // enough to tokenise the short tails a breaker leaves.
874            let mut out = Vec::new();
875            let mut rest = text;
876            'outer: while !rest.is_empty() {
877                let mut candidates: Vec<usize> = (0..self.words.len()).collect();
878                candidates.sort_by_key(|&i| std::cmp::Reverse(self.words[i].len()));
879                for i in candidates {
880                    let w = self.words[i];
881                    if !w.is_empty() && rest.starts_with(w) {
882                        out.push(i);
883                        rest = &rest[w.len()..];
884                        continue 'outer;
885                    }
886                }
887                break;
888            }
889            out
890        }
891    }
892
893    /// A breaker that is a whole token gets an EMPTY tail, and one that
894    /// only ever appears at the end of a longer token gets the tokens
895    /// that must follow it.
896    ///
897    /// This is the half of `get_overlapping_token_sequences` a naive
898    /// "is this token equal to the breaker" implementation misses
899    /// entirely: with a vocabulary where `":"` is only ever the tail of
900    /// `"foo:"`, such an implementation finds no breakers at all and DRY
901    /// silently scans across every boundary it was told to stop at.
902    #[test]
903    fn a_breaker_is_found_inside_a_token_and_across_a_token_boundary() {
904        let vocab = WordVocab {
905            words: vec!["foo", "bar", ":", "ab", "c", "abc", "xab"],
906        };
907        // ":" is token 2 outright.
908        let colon = DryBreakers::from_vocab(&vocab, &[":".to_string()]);
909        assert_eq!(colon.tails(2), Some([Vec::new()].as_slice()));
910        assert_eq!(colon.tails(0), None, "`foo` does not contain a colon");
911        assert!(colon.is_single_token_breaker(2));
912
913        // "abc" is token 5 outright, is CONTAINED by nothing else, and
914        // OVERLAPS the end of "ab" (token 3, tail "c" = token 4) and of
915        // "xab" (token 6, same tail).
916        let abc = DryBreakers::from_vocab(&vocab, &["abc".to_string()]);
917        assert_eq!(abc.tails(5), Some([Vec::new()].as_slice()));
918        assert_eq!(abc.tails(3), Some([vec![4usize]].as_slice()));
919        assert_eq!(abc.tails(6), Some([vec![4usize]].as_slice()));
920        assert!(!abc.is_single_token_breaker(3), "`ab` needs `c` to follow");
921        assert!(abc.is_single_token_breaker(5));
922        assert_eq!(abc.tails(0), None);
923    }
924
925    /// A multi-token breaker only fires when its tail actually FOLLOWS,
926    /// which is the reason the tail is stored at all.
927    ///
928    /// Two windows differing in one token, and the token differs only in
929    /// whether it completes the breaker:
930    ///
931    /// * `foo bar : ab c foo bar : ab c` -- `ab`(3) is immediately
932    ///   followed by `c`(4), so the breaker matches one token from the
933    ///   end and `rep_limit` collapses to 0, below `allowed_length`;
934    /// * `foo bar : ab xab foo bar : ab xab` -- `ab` is followed by
935    ///   `xab`(6) instead, which is not its tail, so no breaker matches,
936    ///   the five-token suffix is found repeating, and `foo` is
937    ///   penalised for extending it.
938    ///
939    /// The third case is the control: the FIRST window with no breakers
940    /// configured at all penalises too, so the emptiness above is the
941    /// breaker's doing and not the window's.
942    #[test]
943    fn a_breaker_with_a_tail_only_stops_the_scan_when_the_tail_follows() {
944        let vocab = WordVocab {
945            words: vec!["foo", "bar", ":", "ab", "c", "abc", "xab"],
946        };
947        let breakers = DryBreakers::from_vocab(&vocab, &["abc".to_string()]);
948        assert_eq!(breakers.tails(3), Some([vec![4usize]].as_slice()));
949        let params = DryParams::new(1.0, 1.1, 2, -1, 1024, breakers);
950
951        let broken = [0usize, 1, 2, 3, 4, 0, 1, 2, 3, 4];
952        let unbroken = [0usize, 1, 2, 3, 6, 0, 1, 2, 3, 6];
953        assert!(
954            params
955                .penalties(PenaltyWindow::new(&[], &broken))
956                .is_empty(),
957            "`ab` followed by `c` is the breaker, one token from the end"
958        );
959        assert!(
960            !params
961                .penalties(PenaltyWindow::new(&[], &unbroken))
962                .is_empty(),
963            "`ab` followed by `xab` is not the breaker, so the scan runs"
964        );
965
966        let no_breakers = DryParams::new(1.0, 1.1, 2, -1, 1024, DryBreakers::none());
967        assert!(
968            !no_breakers
969                .penalties(PenaltyWindow::new(&[], &broken))
970                .is_empty(),
971            "the same window without breakers repeats, so the first \
972             assertion is about the breaker and not about the window"
973        );
974    }
975
976    /// An empty breaker string is skipped rather than turning every
977    /// token into a breaker: `word.find("")` is 0 for every string, so a
978    /// missing guard here would exempt the entire vocabulary from DRY
979    /// while leaving it switched on.
980    #[test]
981    fn an_empty_breaker_string_is_skipped() {
982        let vocab = WordVocab {
983            words: vec!["foo", "bar"],
984        };
985        let breakers = DryBreakers::from_vocab(&vocab, &[String::new()]);
986        assert!(breakers.is_empty());
987        assert_eq!(breakers.raw(), &[String::new()]);
988    }
989
990    /// llama.cpp's defaults, spelled once and read by both front ends.
991    #[test]
992    fn the_default_breakers_are_llama_cpps_four() {
993        assert_eq!(DEFAULT_SEQUENCE_BREAKERS, ["\n", ":", "\"", "*"]);
994    }
995}