1use eredu_core::{
4 generation::ResolvedGenerationConfig, SpeculativeTokenFilterController, TokenFilter,
5 TokenFilterController,
6};
7use eredu_nn::Tensor;
8
9pub trait CausalModel<S> {
11 type Tensor: Tensor;
13 type Input<'a>: Copy;
15 type Error;
17
18 fn prefill_input_logits(
20 &mut self,
21 input: Self::Input<'_>,
22 state: &mut S,
23 context: &<Self::Tensor as Tensor>::Context,
24 ) -> Result<Self::Tensor, Self::Error>;
25
26 fn decode_logits(
28 &mut self,
29 input_tokens: &Self::Tensor,
30 state: &mut S,
31 context: &<Self::Tensor as Tensor>::Context,
32 ) -> Result<Self::Tensor, Self::Error>;
33
34 fn adjust_prefill_logits(
36 &mut self,
37 logits: Self::Tensor,
38 _state: &mut S,
39 _context: &<Self::Tensor as Tensor>::Context,
40 ) -> Result<Self::Tensor, Self::Error> {
41 Ok(logits)
42 }
43}
44
45#[derive(Debug, Clone, Copy, Eq, PartialEq)]
50pub struct TokenDomain {
51 cardinality: usize,
52}
53
54impl TokenDomain {
55 pub const fn new(cardinality: usize) -> Self {
57 Self { cardinality }
58 }
59
60 pub const fn cardinality(self) -> usize {
62 self.cardinality
63 }
64}
65
66pub trait SamplingBackend {
72 type Logits: Clone;
74 type Token: Clone;
76 type RandomState;
78 type Context: ?Sized;
80 type Error;
82
83 fn error(message: String) -> Self::Error;
85
86 fn validate_token(
91 token: &Self::Token,
92 domain: TokenDomain,
93 context: &Self::Context,
94 ) -> Result<Self::Token, Self::Error>;
95
96 fn scale_temperature(
98 logits: &Self::Logits,
99 temperature: f32,
100 context: &Self::Context,
101 ) -> Result<Self::Logits, Self::Error>;
102
103 fn apply_penalties(
105 logits: &Self::Logits,
106 history: &[u32],
107 penalties: PenaltyConfig,
108 context: &Self::Context,
109 ) -> Result<Self::Logits, Self::Error>;
110
111 fn apply_top_k(
113 logits: Self::Logits,
114 top_k: i32,
115 context: &Self::Context,
116 ) -> Result<Self::Logits, Self::Error>;
117
118 fn apply_top_p(
120 logits: Self::Logits,
121 top_p: f32,
122 context: &Self::Context,
123 ) -> Result<Self::Logits, Self::Error>;
124
125 fn apply_min_p(
127 logits: Self::Logits,
128 min_p: f32,
129 context: &Self::Context,
130 ) -> Result<Self::Logits, Self::Error>;
131
132 fn apply_token_filter(
134 logits: &Self::Logits,
135 filter: &TokenFilter,
136 context: &Self::Context,
137 ) -> Result<Self::Logits, Self::Error>;
138
139 fn apply_mirostat(
141 logits: &Self::Logits,
142 history: &[u32],
143 penalties: PenaltyConfig,
144 temperature: f32,
145 mu: f32,
146 context: &Self::Context,
147 ) -> Result<Self::Logits, Self::Error>;
148
149 fn sample_raw(
151 logits: &Self::Logits,
152 temperature: f32,
153 random: Option<&mut Self::RandomState>,
154 context: &Self::Context,
155 ) -> Result<Self::Token, Self::Error>;
156
157 fn sample_processed(
159 logits: &Self::Logits,
160 temperature: f32,
161 random: Option<&mut Self::RandomState>,
162 context: &Self::Context,
163 ) -> Result<Self::Token, Self::Error>;
164
165 fn token_id(token: &Self::Token, context: &Self::Context) -> Result<u32, Self::Error>;
167
168 fn token_probability(
170 logits: &Self::Logits,
171 token: u32,
172 context: &Self::Context,
173 ) -> Result<f32, Self::Error>;
174}
175
176#[derive(Debug, Clone, Copy, PartialEq)]
178pub struct PenaltyConfig {
179 pub repeat_penalty: f32,
181 pub repeat_last_n: i32,
183 pub frequency_penalty: f32,
185 pub presence_penalty: f32,
187}
188
189impl PenaltyConfig {
190 pub fn is_identity(self) -> bool {
192 self.repeat_penalty == 1.0 && self.frequency_penalty == 0.0 && self.presence_penalty == 0.0
193 }
194}
195
196impl Default for PenaltyConfig {
197 fn default() -> Self {
198 Self {
199 repeat_penalty: 1.0,
200 repeat_last_n: 64,
201 frequency_penalty: 0.0,
202 presence_penalty: 0.0,
203 }
204 }
205}
206
207pub trait SpeculativeSampler<B: SamplingBackend> {
209 fn uses_checkpoint_defaults(&self) -> bool {
211 false
212 }
213
214 fn supports_exact_optimistic_promotion(&self) -> bool {
216 false
217 }
218
219 fn grammar_is_complete(&mut self) -> Result<bool, B::Error> {
221 Ok(false)
222 }
223
224 fn prefix_is_complete(&self, _history: &[u32]) -> Result<bool, B::Error> {
226 Ok(false)
227 }
228
229 fn process_logits(
231 &mut self,
232 logits: &B::Logits,
233 temperature: f32,
234 history: &[u32],
235 context: &B::Context,
236 ) -> Result<B::Logits, B::Error>;
237
238 fn sample_processed(
240 &self,
241 logits: &B::Logits,
242 temperature: f32,
243 random: Option<&mut B::RandomState>,
244 context: &B::Context,
245 ) -> Result<B::Token, B::Error> {
246 B::sample_processed(logits, temperature, random, context)
247 }
248
249 fn commit_token(
251 &mut self,
252 _processed_logits: &B::Logits,
253 _token: u32,
254 _context: &B::Context,
255 ) -> Result<(), B::Error> {
256 Ok(())
257 }
258}
259
260pub trait Sampler<B: SamplingBackend> {
262 fn uses_checkpoint_defaults(&self) -> bool {
264 false
265 }
266
267 fn sample(
269 &mut self,
270 logits: &B::Logits,
271 temperature: f32,
272 random: Option<&mut B::RandomState>,
273 context: &B::Context,
274 ) -> Result<B::Token, B::Error>;
275}
276
277pub struct ConstrainedSampler<S, C> {
279 policy: S,
280 controller: C,
281}
282
283struct ConstraintCheckpoint<S, C> {
284 policy: S,
285 controller: C,
286}
287
288impl<S: Clone, C: Clone> Clone for ConstrainedSampler<S, C> {
289 fn clone(&self) -> Self {
290 Self {
291 policy: self.policy.clone(),
292 controller: self.controller.clone(),
293 }
294 }
295}
296
297impl<S, C> ConstrainedSampler<S, C> {
298 pub fn new(policy: S, controller: C) -> Self {
300 Self { policy, controller }
301 }
302
303 pub const fn policy(&self) -> &S {
305 &self.policy
306 }
307
308 pub const fn controller(&self) -> &C {
310 &self.controller
311 }
312
313 pub fn controller_mut(&mut self) -> &mut C {
315 &mut self.controller
316 }
317}
318
319impl<S: Clone, C: Clone> ConstrainedSampler<S, C> {
320 fn checkpoint(&self) -> ConstraintCheckpoint<S, C> {
321 ConstraintCheckpoint {
322 policy: self.policy.clone(),
323 controller: self.controller.clone(),
324 }
325 }
326}
327
328impl<B, S, C> SpeculativeSampler<B> for ConstrainedSampler<S, C>
329where
330 B: SamplingBackend,
331 S: SpeculativeSampler<B> + Clone,
332 C: SpeculativeTokenFilterController,
333{
334 fn supports_exact_optimistic_promotion(&self) -> bool {
335 self.policy.supports_exact_optimistic_promotion()
336 }
337
338 fn grammar_is_complete(&mut self) -> Result<bool, B::Error> {
339 self.controller
340 .is_complete()
341 .map_err(|error| B::error(error.to_string()))
342 }
343
344 fn prefix_is_complete(&self, history: &[u32]) -> Result<bool, B::Error> {
345 self.controller
346 .prefix_is_complete(history)
347 .map_err(|error| B::error(error.to_string()))
348 }
349
350 fn process_logits(
351 &mut self,
352 logits: &B::Logits,
353 temperature: f32,
354 history: &[u32],
355 context: &B::Context,
356 ) -> Result<B::Logits, B::Error> {
357 let filter = self
358 .controller
359 .filter_at(history)
360 .map_err(|error| B::error(error.to_string()))?;
361 let masked = B::apply_token_filter(logits, &filter, context)?;
362 self.policy
363 .process_logits(&masked, temperature, history, context)
364 }
365
366 fn sample_processed(
367 &self,
368 logits: &B::Logits,
369 temperature: f32,
370 random: Option<&mut B::RandomState>,
371 context: &B::Context,
372 ) -> Result<B::Token, B::Error> {
373 self.policy
374 .sample_processed(logits, temperature, random, context)
375 }
376
377 fn commit_token(
378 &mut self,
379 processed_logits: &B::Logits,
380 token: u32,
381 context: &B::Context,
382 ) -> Result<(), B::Error> {
383 let checkpoint = self.checkpoint();
384 if let Err(error) = self
385 .policy
386 .commit_token(processed_logits, token, context)
387 .and_then(|()| {
388 self.controller
389 .commit_token(token)
390 .map_err(|error| B::error(error.to_string()))
391 })
392 {
393 self.policy = checkpoint.policy;
394 self.controller = checkpoint.controller;
395 return Err(error);
396 }
397 Ok(())
398 }
399}
400
401impl<B, S, C> Sampler<B> for ConstrainedSampler<S, C>
402where
403 B: SamplingBackend,
404 S: Sampler<B> + Clone,
405 C: TokenFilterController + Clone,
406{
407 fn sample(
408 &mut self,
409 logits: &B::Logits,
410 temperature: f32,
411 random: Option<&mut B::RandomState>,
412 context: &B::Context,
413 ) -> Result<B::Token, B::Error> {
414 let checkpoint = self.checkpoint();
415 let filter = self
416 .controller
417 .current_filter()
418 .map_err(|error| B::error(error.to_string()))?;
419 let masked = B::apply_token_filter(logits, &filter, context)?;
420 let token = self.policy.sample(&masked, temperature, random, context)?;
421 let token_id = B::token_id(&token, context)?;
422 if let Err(error) = self.controller.commit_token(token_id) {
423 self.policy = checkpoint.policy;
424 self.controller = checkpoint.controller;
425 return Err(B::error(error.to_string()));
426 }
427 Ok(token)
428 }
429}
430
431#[derive(Debug, Clone, Copy)]
433pub struct DefaultSampler;
434
435impl<B: SamplingBackend> SpeculativeSampler<B> for DefaultSampler {
436 fn uses_checkpoint_defaults(&self) -> bool {
437 true
438 }
439
440 fn supports_exact_optimistic_promotion(&self) -> bool {
441 true
442 }
443
444 fn process_logits(
445 &mut self,
446 logits: &B::Logits,
447 temperature: f32,
448 _history: &[u32],
449 context: &B::Context,
450 ) -> Result<B::Logits, B::Error> {
451 if temperature == 0.0 {
452 Ok(logits.clone())
453 } else {
454 B::scale_temperature(logits, temperature, context)
455 }
456 }
457}
458
459impl<B: SamplingBackend> Sampler<B> for DefaultSampler {
460 fn uses_checkpoint_defaults(&self) -> bool {
461 true
462 }
463
464 fn sample(
465 &mut self,
466 logits: &B::Logits,
467 temperature: f32,
468 random: Option<&mut B::RandomState>,
469 context: &B::Context,
470 ) -> Result<B::Token, B::Error> {
471 B::sample_raw(logits, temperature, random, context)
472 }
473}
474
475#[derive(Debug, Clone)]
477pub struct GenerationSampler {
478 pub top_k: i32,
480 pub top_p: f32,
482 pub min_p: f32,
484 pub repeat_penalty: f32,
486 pub repeat_last_n: i32,
488 pub frequency_penalty: f32,
490 pub presence_penalty: f32,
492 generated_tokens: Vec<u32>,
493}
494
495impl Default for GenerationSampler {
496 fn default() -> Self {
497 Self {
498 top_k: 40,
499 top_p: 0.95,
500 min_p: 0.05,
501 repeat_penalty: 1.0,
502 repeat_last_n: 64,
503 frequency_penalty: 0.0,
504 presence_penalty: 0.0,
505 generated_tokens: Vec::new(),
506 }
507 }
508}
509
510impl GenerationSampler {
511 pub fn new() -> Self {
513 Self::default()
514 }
515
516 pub fn from_resolved(config: ResolvedGenerationConfig) -> Self {
518 Self::new()
519 .top_k(config.top_k)
520 .top_p(config.top_p)
521 .min_p(config.min_p)
522 .penalties(
523 config.repetition_penalty,
524 config.repeat_last_n,
525 config.frequency_penalty,
526 config.presence_penalty,
527 )
528 }
529
530 pub fn with_generated_tokens(mut self, tokens: impl IntoIterator<Item = u32>) -> Self {
532 self.generated_tokens = tokens.into_iter().collect();
533 self
534 }
535
536 pub fn top_k(mut self, value: i32) -> Self {
538 self.top_k = value;
539 self
540 }
541
542 pub fn top_p(mut self, value: f32) -> Self {
544 self.top_p = value;
545 self
546 }
547
548 pub fn min_p(mut self, value: f32) -> Self {
550 self.min_p = value;
551 self
552 }
553
554 pub fn penalties(
556 mut self,
557 repeat_penalty: f32,
558 repeat_last_n: i32,
559 frequency_penalty: f32,
560 presence_penalty: f32,
561 ) -> Self {
562 self.repeat_penalty = repeat_penalty;
563 self.repeat_last_n = repeat_last_n;
564 self.frequency_penalty = frequency_penalty;
565 self.presence_penalty = presence_penalty;
566 self
567 }
568
569 pub fn generated_tokens(&self) -> &[u32] {
571 &self.generated_tokens
572 }
573
574 pub fn set_generated_tokens(&mut self, tokens: impl IntoIterator<Item = u32>) {
576 self.generated_tokens = tokens.into_iter().collect();
577 }
578
579 pub fn accept_token(&mut self, token: u32) {
581 self.generated_tokens.push(token);
582 }
583
584 pub fn clear_generated_tokens(&mut self) {
586 self.generated_tokens.clear();
587 }
588
589 pub const fn penalty_config(&self) -> PenaltyConfig {
591 PenaltyConfig {
592 repeat_penalty: self.repeat_penalty,
593 repeat_last_n: self.repeat_last_n,
594 frequency_penalty: self.frequency_penalty,
595 presence_penalty: self.presence_penalty,
596 }
597 }
598
599 fn process_for<B: SamplingBackend>(
600 &self,
601 logits: &B::Logits,
602 history: &[u32],
603 context: &B::Context,
604 ) -> Result<B::Logits, B::Error> {
605 let logits = B::apply_penalties(logits, history, self.penalty_config(), context)?;
606 let logits = B::apply_top_k(logits, self.top_k, context)?;
607 let logits = B::apply_top_p(logits, self.top_p, context)?;
608 B::apply_min_p(logits, self.min_p, context)
609 }
610}
611
612impl<B: SamplingBackend> SpeculativeSampler<B> for GenerationSampler {
613 fn supports_exact_optimistic_promotion(&self) -> bool {
614 true
615 }
616
617 fn process_logits(
618 &mut self,
619 logits: &B::Logits,
620 temperature: f32,
621 history: &[u32],
622 context: &B::Context,
623 ) -> Result<B::Logits, B::Error> {
624 let logits = self.process_for::<B>(logits, history, context)?;
625 if temperature == 0.0 {
626 Ok(logits)
627 } else {
628 B::scale_temperature(&logits, temperature, context)
629 }
630 }
631}
632
633impl<B: SamplingBackend> Sampler<B> for GenerationSampler {
634 fn sample(
635 &mut self,
636 logits: &B::Logits,
637 temperature: f32,
638 random: Option<&mut B::RandomState>,
639 context: &B::Context,
640 ) -> Result<B::Token, B::Error> {
641 let logits = self.process_for::<B>(logits, &self.generated_tokens, context)?;
642 let token = B::sample_raw(&logits, temperature, random, context)?;
643 self.generated_tokens.push(B::token_id(&token, context)?);
644 Ok(token)
645 }
646}
647
648#[derive(Debug, Clone)]
650pub struct MirostatV2Sampler {
651 tau: f32,
652 eta: f32,
653 mu: f32,
654 penalties: GenerationSampler,
655}
656
657impl Default for MirostatV2Sampler {
658 fn default() -> Self {
659 Self {
660 tau: 5.0,
661 eta: 0.1,
662 mu: 10.0,
663 penalties: GenerationSampler::new().top_k(0).top_p(1.0).min_p(0.0),
664 }
665 }
666}
667
668impl MirostatV2Sampler {
669 pub fn new(tau: f32, eta: f32) -> Result<Self, SamplingConfigurationError> {
671 validate_positive_finite("Mirostat V2 tau", tau)?;
672 validate_positive_finite("Mirostat V2 eta", eta)?;
673 Ok(Self {
674 tau,
675 eta,
676 mu: 2.0 * tau,
677 penalties: GenerationSampler::new().top_k(0).top_p(1.0).min_p(0.0),
678 })
679 }
680
681 pub fn penalties(
683 mut self,
684 repeat_penalty: f32,
685 repeat_last_n: i32,
686 frequency_penalty: f32,
687 presence_penalty: f32,
688 ) -> Self {
689 self.penalties = self.penalties.penalties(
690 repeat_penalty,
691 repeat_last_n,
692 frequency_penalty,
693 presence_penalty,
694 );
695 self
696 }
697
698 pub const fn tau(&self) -> f32 {
700 self.tau
701 }
702
703 pub const fn eta(&self) -> f32 {
705 self.eta
706 }
707
708 pub const fn mu(&self) -> f32 {
710 self.mu
711 }
712
713 pub fn generated_tokens(&self) -> &[u32] {
715 self.penalties.generated_tokens()
716 }
717
718 pub fn accept_token(
720 &mut self,
721 token: u32,
722 probability: f32,
723 ) -> Result<(), SamplingConfigurationError> {
724 if !probability.is_finite() || probability <= 0.0 || probability > 1.0 {
725 return Err(SamplingConfigurationError::Invalid(
726 "accepted Mirostat V2 token probability must be finite and in (0, 1]".into(),
727 ));
728 }
729 self.update_mu(-probability.log2());
730 self.penalties.accept_token(token);
731 Ok(())
732 }
733
734 pub fn reset(&mut self) {
736 self.mu = 2.0 * self.tau;
737 self.penalties.clear_generated_tokens();
738 }
739
740 fn update_mu(&mut self, observed_surprise: f32) {
741 self.mu -= self.eta * (observed_surprise - self.tau);
742 }
743
744 fn process_for<B: SamplingBackend>(
745 &self,
746 logits: &B::Logits,
747 temperature: f32,
748 history: &[u32],
749 context: &B::Context,
750 ) -> Result<B::Logits, B::Error> {
751 if !temperature.is_finite() || temperature <= 0.0 {
752 return Err(B::error(
753 "Mirostat V2 requires a finite temperature greater than zero".into(),
754 ));
755 }
756 B::apply_mirostat(
757 logits,
758 history,
759 self.penalties.penalty_config(),
760 temperature,
761 self.mu,
762 context,
763 )
764 }
765
766 fn commit_for<B: SamplingBackend>(
767 &mut self,
768 logits: &B::Logits,
769 token: u32,
770 context: &B::Context,
771 ) -> Result<(), B::Error> {
772 let probability = B::token_probability(logits, token, context)?;
773 self.accept_token(token, probability)
774 .map_err(|error| B::error(error.to_string()))
775 }
776}
777
778impl<B: SamplingBackend> Sampler<B> for MirostatV2Sampler {
779 fn sample(
780 &mut self,
781 logits: &B::Logits,
782 temperature: f32,
783 random: Option<&mut B::RandomState>,
784 context: &B::Context,
785 ) -> Result<B::Token, B::Error> {
786 let processed = self.process_for::<B>(
787 logits,
788 temperature,
789 self.penalties.generated_tokens(),
790 context,
791 )?;
792 let token = B::sample_processed(&processed, temperature, random, context)?;
793 self.commit_for::<B>(&processed, B::token_id(&token, context)?, context)?;
794 Ok(token)
795 }
796}
797
798impl<B: SamplingBackend> SpeculativeSampler<B> for MirostatV2Sampler {
799 fn process_logits(
800 &mut self,
801 logits: &B::Logits,
802 temperature: f32,
803 history: &[u32],
804 context: &B::Context,
805 ) -> Result<B::Logits, B::Error> {
806 self.process_for::<B>(logits, temperature, history, context)
807 }
808
809 fn commit_token(
810 &mut self,
811 processed_logits: &B::Logits,
812 token: u32,
813 context: &B::Context,
814 ) -> Result<(), B::Error> {
815 self.commit_for::<B>(processed_logits, token, context)
816 }
817}
818
819#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
821pub enum SamplingConfigurationError {
822 #[error("{0}")]
824 Invalid(String),
825}
826
827fn validate_positive_finite(name: &str, value: f32) -> Result<(), SamplingConfigurationError> {
828 if value.is_finite() && value > 0.0 {
829 Ok(())
830 } else {
831 Err(SamplingConfigurationError::Invalid(format!(
832 "{name} must be finite and greater than zero"
833 )))
834 }
835}
836
837#[cfg(test)]
838mod tests {
839 use super::{GenerationSampler, MirostatV2Sampler};
840
841 #[test]
842 fn generation_history_is_backend_neutral() {
843 let mut sampler = GenerationSampler::new().with_generated_tokens([1, 2]);
844 sampler.accept_token(3);
845 assert_eq!(sampler.generated_tokens(), &[1, 2, 3]);
846 sampler.set_generated_tokens([5, 8]);
847 assert_eq!(sampler.generated_tokens(), &[5, 8]);
848 sampler.clear_generated_tokens();
849 assert!(sampler.generated_tokens().is_empty());
850 }
851
852 #[test]
853 fn mirostat_state_is_backend_neutral() {
854 let mut sampler = MirostatV2Sampler::default();
855 sampler.accept_token(42, 2.0f32.powi(-7)).unwrap();
856 assert!((sampler.mu() - 9.8).abs() < 1e-6);
857 assert_eq!(sampler.generated_tokens(), &[42]);
858 sampler.reset();
859 assert_eq!(sampler.mu(), 10.0);
860 assert!(sampler.generated_tokens().is_empty());
861 }
862}