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(¶ms, &[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(¶ms, &[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(¶ms, &[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(¶ms, &[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}