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(
138 logits: &Self::Logits,
139 filter: &TokenFilter,
140 context: &Self::Context,
141 ) -> Result<Self::Logits, Self::Error>;
142
143 fn apply_mirostat(
145 logits: &Self::Logits,
146 history: &[u32],
147 penalties: PenaltyConfig,
148 temperature: f32,
149 mu: f32,
150 context: &Self::Context,
151 ) -> Result<Self::Logits, Self::Error>;
152
153 fn sample_raw(
155 logits: &Self::Logits,
156 temperature: f32,
157 random: Option<&mut Self::RandomState>,
158 context: &Self::Context,
159 ) -> Result<Self::Token, Self::Error>;
160
161 fn sample_processed(
163 logits: &Self::Logits,
164 temperature: f32,
165 random: Option<&mut Self::RandomState>,
166 context: &Self::Context,
167 ) -> Result<Self::Token, Self::Error>;
168
169 fn token_id(token: &Self::Token, context: &Self::Context) -> Result<u32, Self::Error>;
171
172 fn token_probability(
174 logits: &Self::Logits,
175 token: u32,
176 context: &Self::Context,
177 ) -> Result<f32, Self::Error>;
178}
179
180#[derive(Debug, Clone, Copy, PartialEq)]
182pub struct PenaltyConfig {
183 pub repeat_penalty: f32,
185 pub repeat_last_n: i32,
187 pub frequency_penalty: f32,
189 pub presence_penalty: f32,
191}
192
193impl PenaltyConfig {
194 pub fn is_identity(self) -> bool {
196 self.repeat_penalty == 1.0 && self.frequency_penalty == 0.0 && self.presence_penalty == 0.0
197 }
198}
199
200impl Default for PenaltyConfig {
201 fn default() -> Self {
202 Self {
203 repeat_penalty: 1.0,
204 repeat_last_n: 64,
205 frequency_penalty: 0.0,
206 presence_penalty: 0.0,
207 }
208 }
209}
210
211pub trait SpeculativeSampler<B: SamplingBackend> {
213 fn uses_checkpoint_defaults(&self) -> bool {
215 false
216 }
217
218 fn supports_exact_optimistic_promotion(&self) -> bool {
220 false
221 }
222
223 fn grammar_is_complete(&mut self) -> Result<bool, B::Error> {
225 Ok(false)
226 }
227
228 fn prefix_is_complete(&self, _history: &[u32]) -> Result<bool, B::Error> {
230 Ok(false)
231 }
232
233 fn process_logits(
235 &mut self,
236 logits: &B::Logits,
237 temperature: f32,
238 history: &[u32],
239 context: &B::Context,
240 ) -> Result<B::Logits, B::Error>;
241
242 fn sample_processed(
244 &self,
245 logits: &B::Logits,
246 temperature: f32,
247 random: Option<&mut B::RandomState>,
248 context: &B::Context,
249 ) -> Result<B::Token, B::Error> {
250 B::sample_processed(logits, temperature, random, context)
251 }
252
253 fn commit_token(
255 &mut self,
256 _processed_logits: &B::Logits,
257 _token: u32,
258 _context: &B::Context,
259 ) -> Result<(), B::Error> {
260 Ok(())
261 }
262}
263
264pub trait Sampler<B: SamplingBackend> {
266 fn uses_checkpoint_defaults(&self) -> bool {
268 false
269 }
270
271 fn sample(
273 &mut self,
274 logits: &B::Logits,
275 temperature: f32,
276 random: Option<&mut B::RandomState>,
277 context: &B::Context,
278 ) -> Result<B::Token, B::Error>;
279}
280
281pub struct ConstrainedSampler<S, C> {
283 policy: S,
284 controller: C,
285}
286
287struct ConstraintCheckpoint<S, C> {
288 policy: S,
289 controller: C,
290}
291
292impl<S: Clone, C: Clone> Clone for ConstrainedSampler<S, C> {
293 fn clone(&self) -> Self {
294 Self {
295 policy: self.policy.clone(),
296 controller: self.controller.clone(),
297 }
298 }
299}
300
301impl<S, C> ConstrainedSampler<S, C> {
302 pub fn new(policy: S, controller: C) -> Self {
304 Self { policy, controller }
305 }
306
307 pub const fn policy(&self) -> &S {
309 &self.policy
310 }
311
312 pub const fn controller(&self) -> &C {
314 &self.controller
315 }
316
317 pub fn controller_mut(&mut self) -> &mut C {
319 &mut self.controller
320 }
321}
322
323impl<S: Clone, C: Clone> ConstrainedSampler<S, C> {
324 fn checkpoint(&self) -> ConstraintCheckpoint<S, C> {
325 ConstraintCheckpoint {
326 policy: self.policy.clone(),
327 controller: self.controller.clone(),
328 }
329 }
330}
331
332impl<B, S, C> SpeculativeSampler<B> for ConstrainedSampler<S, C>
333where
334 B: SamplingBackend,
335 S: SpeculativeSampler<B> + Clone,
336 C: SpeculativeTokenFilterController,
337{
338 fn supports_exact_optimistic_promotion(&self) -> bool {
339 self.policy.supports_exact_optimistic_promotion()
340 }
341
342 fn grammar_is_complete(&mut self) -> Result<bool, B::Error> {
343 self.controller
344 .is_complete()
345 .map_err(|error| B::error(error.to_string()))
346 }
347
348 fn prefix_is_complete(&self, history: &[u32]) -> Result<bool, B::Error> {
349 self.controller
350 .prefix_is_complete(history)
351 .map_err(|error| B::error(error.to_string()))
352 }
353
354 fn process_logits(
355 &mut self,
356 logits: &B::Logits,
357 temperature: f32,
358 history: &[u32],
359 context: &B::Context,
360 ) -> Result<B::Logits, B::Error> {
361 let filter = self
362 .controller
363 .filter_at(history)
364 .map_err(|error| B::error(error.to_string()))?;
365 let masked = B::apply_token_filter(logits, &filter, context)?;
366 self.policy
367 .process_logits(&masked, temperature, history, context)
368 }
369
370 fn sample_processed(
371 &self,
372 logits: &B::Logits,
373 temperature: f32,
374 random: Option<&mut B::RandomState>,
375 context: &B::Context,
376 ) -> Result<B::Token, B::Error> {
377 self.policy
378 .sample_processed(logits, temperature, random, context)
379 }
380
381 fn commit_token(
382 &mut self,
383 processed_logits: &B::Logits,
384 token: u32,
385 context: &B::Context,
386 ) -> Result<(), B::Error> {
387 let checkpoint = self.checkpoint();
388 if let Err(error) = self
389 .policy
390 .commit_token(processed_logits, token, context)
391 .and_then(|()| {
392 self.controller
393 .commit_token(token)
394 .map_err(|error| B::error(error.to_string()))
395 })
396 {
397 self.policy = checkpoint.policy;
398 self.controller = checkpoint.controller;
399 return Err(error);
400 }
401 Ok(())
402 }
403}
404
405impl<B, S, C> Sampler<B> for ConstrainedSampler<S, C>
406where
407 B: SamplingBackend,
408 S: Sampler<B> + Clone,
409 C: TokenFilterController + Clone,
410{
411 fn sample(
412 &mut self,
413 logits: &B::Logits,
414 temperature: f32,
415 random: Option<&mut B::RandomState>,
416 context: &B::Context,
417 ) -> Result<B::Token, B::Error> {
418 let checkpoint = self.checkpoint();
419 let filter = self
420 .controller
421 .current_filter()
422 .map_err(|error| B::error(error.to_string()))?;
423 let masked = B::apply_token_filter(logits, &filter, context)?;
424 let token = self.policy.sample(&masked, temperature, random, context)?;
425 let token_id = B::token_id(&token, context)?;
426 if let Err(error) = self.controller.commit_token(token_id) {
427 self.policy = checkpoint.policy;
428 self.controller = checkpoint.controller;
429 return Err(B::error(error.to_string()));
430 }
431 Ok(token)
432 }
433}
434
435#[derive(Debug, Clone, Copy)]
437pub struct DefaultSampler;
438
439impl<B: SamplingBackend> SpeculativeSampler<B> for DefaultSampler {
440 fn uses_checkpoint_defaults(&self) -> bool {
441 true
442 }
443
444 fn supports_exact_optimistic_promotion(&self) -> bool {
445 true
446 }
447
448 fn process_logits(
449 &mut self,
450 logits: &B::Logits,
451 temperature: f32,
452 _history: &[u32],
453 context: &B::Context,
454 ) -> Result<B::Logits, B::Error> {
455 if temperature == 0.0 {
456 Ok(logits.clone())
457 } else {
458 B::scale_temperature(logits, temperature, context)
459 }
460 }
461}
462
463impl<B: SamplingBackend> Sampler<B> for DefaultSampler {
464 fn uses_checkpoint_defaults(&self) -> bool {
465 true
466 }
467
468 fn sample(
469 &mut self,
470 logits: &B::Logits,
471 temperature: f32,
472 random: Option<&mut B::RandomState>,
473 context: &B::Context,
474 ) -> Result<B::Token, B::Error> {
475 B::sample_raw(logits, temperature, random, context)
476 }
477}
478
479#[derive(Debug, Clone)]
481pub struct GenerationSampler {
482 pub top_k: i32,
484 pub top_p: f32,
486 pub min_p: f32,
488 pub repeat_penalty: f32,
490 pub repeat_last_n: i32,
492 pub frequency_penalty: f32,
494 pub presence_penalty: f32,
496 generated_tokens: Vec<u32>,
497}
498
499impl Default for GenerationSampler {
500 fn default() -> Self {
501 Self {
502 top_k: 40,
503 top_p: 0.95,
504 min_p: 0.05,
505 repeat_penalty: 1.0,
506 repeat_last_n: 64,
507 frequency_penalty: 0.0,
508 presence_penalty: 0.0,
509 generated_tokens: Vec::new(),
510 }
511 }
512}
513
514impl GenerationSampler {
515 pub fn new() -> Self {
517 Self::default()
518 }
519
520 pub fn from_resolved(config: ResolvedGenerationConfig) -> Self {
522 Self::new()
523 .top_k(config.top_k)
524 .top_p(config.top_p)
525 .min_p(config.min_p)
526 .penalties(
527 config.repetition_penalty,
528 config.repeat_last_n,
529 config.frequency_penalty,
530 config.presence_penalty,
531 )
532 }
533
534 pub fn with_generated_tokens(mut self, tokens: impl IntoIterator<Item = u32>) -> Self {
536 self.generated_tokens = tokens.into_iter().collect();
537 self
538 }
539
540 pub fn top_k(mut self, value: i32) -> Self {
542 self.top_k = value;
543 self
544 }
545
546 pub fn top_p(mut self, value: f32) -> Self {
548 self.top_p = value;
549 self
550 }
551
552 pub fn min_p(mut self, value: f32) -> Self {
554 self.min_p = value;
555 self
556 }
557
558 pub fn penalties(
560 mut self,
561 repeat_penalty: f32,
562 repeat_last_n: i32,
563 frequency_penalty: f32,
564 presence_penalty: f32,
565 ) -> Self {
566 self.repeat_penalty = repeat_penalty;
567 self.repeat_last_n = repeat_last_n;
568 self.frequency_penalty = frequency_penalty;
569 self.presence_penalty = presence_penalty;
570 self
571 }
572
573 pub fn generated_tokens(&self) -> &[u32] {
575 &self.generated_tokens
576 }
577
578 pub fn set_generated_tokens(&mut self, tokens: impl IntoIterator<Item = u32>) {
580 self.generated_tokens = tokens.into_iter().collect();
581 }
582
583 pub fn accept_token(&mut self, token: u32) {
585 self.generated_tokens.push(token);
586 }
587
588 pub fn clear_generated_tokens(&mut self) {
590 self.generated_tokens.clear();
591 }
592
593 pub const fn penalty_config(&self) -> PenaltyConfig {
595 PenaltyConfig {
596 repeat_penalty: self.repeat_penalty,
597 repeat_last_n: self.repeat_last_n,
598 frequency_penalty: self.frequency_penalty,
599 presence_penalty: self.presence_penalty,
600 }
601 }
602
603 fn process_for<B: SamplingBackend>(
604 &self,
605 logits: &B::Logits,
606 history: &[u32],
607 context: &B::Context,
608 ) -> Result<B::Logits, B::Error> {
609 let logits = B::apply_penalties(logits, history, self.penalty_config(), context)?;
610 let logits = B::apply_top_k(logits, self.top_k, context)?;
611 let logits = B::apply_top_p(logits, self.top_p, context)?;
612 B::apply_min_p(logits, self.min_p, context)
613 }
614}
615
616impl<B: SamplingBackend> SpeculativeSampler<B> for GenerationSampler {
617 fn supports_exact_optimistic_promotion(&self) -> bool {
618 true
619 }
620
621 fn process_logits(
622 &mut self,
623 logits: &B::Logits,
624 temperature: f32,
625 history: &[u32],
626 context: &B::Context,
627 ) -> Result<B::Logits, B::Error> {
628 let logits = self.process_for::<B>(logits, history, context)?;
629 if temperature == 0.0 {
630 Ok(logits)
631 } else {
632 B::scale_temperature(&logits, temperature, context)
633 }
634 }
635}
636
637impl<B: SamplingBackend> Sampler<B> for GenerationSampler {
638 fn sample(
639 &mut self,
640 logits: &B::Logits,
641 temperature: f32,
642 random: Option<&mut B::RandomState>,
643 context: &B::Context,
644 ) -> Result<B::Token, B::Error> {
645 let logits = self.process_for::<B>(logits, &self.generated_tokens, context)?;
646 let token = B::sample_raw(&logits, temperature, random, context)?;
647 self.generated_tokens.push(B::token_id(&token, context)?);
648 Ok(token)
649 }
650}
651
652#[derive(Debug, Clone)]
654pub struct MirostatV2Sampler {
655 tau: f32,
656 eta: f32,
657 mu: f32,
658 penalties: GenerationSampler,
659}
660
661impl Default for MirostatV2Sampler {
662 fn default() -> Self {
663 Self {
664 tau: 5.0,
665 eta: 0.1,
666 mu: 10.0,
667 penalties: GenerationSampler::new().top_k(0).top_p(1.0).min_p(0.0),
668 }
669 }
670}
671
672impl MirostatV2Sampler {
673 pub fn new(tau: f32, eta: f32) -> Result<Self, SamplingConfigurationError> {
675 validate_positive_finite("Mirostat V2 tau", tau)?;
676 validate_positive_finite("Mirostat V2 eta", eta)?;
677 Ok(Self {
678 tau,
679 eta,
680 mu: 2.0 * tau,
681 penalties: GenerationSampler::new().top_k(0).top_p(1.0).min_p(0.0),
682 })
683 }
684
685 pub fn penalties(
687 mut self,
688 repeat_penalty: f32,
689 repeat_last_n: i32,
690 frequency_penalty: f32,
691 presence_penalty: f32,
692 ) -> Self {
693 self.penalties = self.penalties.penalties(
694 repeat_penalty,
695 repeat_last_n,
696 frequency_penalty,
697 presence_penalty,
698 );
699 self
700 }
701
702 pub const fn tau(&self) -> f32 {
704 self.tau
705 }
706
707 pub const fn eta(&self) -> f32 {
709 self.eta
710 }
711
712 pub const fn mu(&self) -> f32 {
714 self.mu
715 }
716
717 pub fn generated_tokens(&self) -> &[u32] {
719 self.penalties.generated_tokens()
720 }
721
722 pub fn accept_token(
724 &mut self,
725 token: u32,
726 probability: f32,
727 ) -> Result<(), SamplingConfigurationError> {
728 if !probability.is_finite() || probability <= 0.0 || probability > 1.0 {
729 return Err(SamplingConfigurationError::Invalid(
730 "accepted Mirostat V2 token probability must be finite and in (0, 1]".into(),
731 ));
732 }
733 self.update_mu(-probability.log2());
734 self.penalties.accept_token(token);
735 Ok(())
736 }
737
738 pub fn reset(&mut self) {
740 self.mu = 2.0 * self.tau;
741 self.penalties.clear_generated_tokens();
742 }
743
744 fn update_mu(&mut self, observed_surprise: f32) {
745 self.mu -= self.eta * (observed_surprise - self.tau);
746 }
747
748 fn process_for<B: SamplingBackend>(
749 &self,
750 logits: &B::Logits,
751 temperature: f32,
752 history: &[u32],
753 context: &B::Context,
754 ) -> Result<B::Logits, B::Error> {
755 if !temperature.is_finite() || temperature <= 0.0 {
756 return Err(B::error(
757 "Mirostat V2 requires a finite temperature greater than zero".into(),
758 ));
759 }
760 B::apply_mirostat(
761 logits,
762 history,
763 self.penalties.penalty_config(),
764 temperature,
765 self.mu,
766 context,
767 )
768 }
769
770 fn commit_for<B: SamplingBackend>(
771 &mut self,
772 logits: &B::Logits,
773 token: u32,
774 context: &B::Context,
775 ) -> Result<(), B::Error> {
776 let probability = B::token_probability(logits, token, context)?;
777 self.accept_token(token, probability)
778 .map_err(|error| B::error(error.to_string()))
779 }
780}
781
782impl<B: SamplingBackend> Sampler<B> for MirostatV2Sampler {
783 fn sample(
784 &mut self,
785 logits: &B::Logits,
786 temperature: f32,
787 random: Option<&mut B::RandomState>,
788 context: &B::Context,
789 ) -> Result<B::Token, B::Error> {
790 let processed = self.process_for::<B>(
791 logits,
792 temperature,
793 self.penalties.generated_tokens(),
794 context,
795 )?;
796 let token = B::sample_processed(&processed, temperature, random, context)?;
797 self.commit_for::<B>(&processed, B::token_id(&token, context)?, context)?;
798 Ok(token)
799 }
800}
801
802impl<B: SamplingBackend> SpeculativeSampler<B> for MirostatV2Sampler {
803 fn process_logits(
804 &mut self,
805 logits: &B::Logits,
806 temperature: f32,
807 history: &[u32],
808 context: &B::Context,
809 ) -> Result<B::Logits, B::Error> {
810 self.process_for::<B>(logits, temperature, history, context)
811 }
812
813 fn commit_token(
814 &mut self,
815 processed_logits: &B::Logits,
816 token: u32,
817 context: &B::Context,
818 ) -> Result<(), B::Error> {
819 self.commit_for::<B>(processed_logits, token, context)
820 }
821}
822
823#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
825pub enum SamplingConfigurationError {
826 #[error("{0}")]
828 Invalid(String),
829}
830
831fn validate_positive_finite(name: &str, value: f32) -> Result<(), SamplingConfigurationError> {
832 if value.is_finite() && value > 0.0 {
833 Ok(())
834 } else {
835 Err(SamplingConfigurationError::Invalid(format!(
836 "{name} must be finite and greater than zero"
837 )))
838 }
839}
840
841#[cfg(test)]
842mod tests {
843 use super::{GenerationSampler, MirostatV2Sampler};
844
845 #[test]
846 fn generation_history_is_backend_neutral() {
847 let mut sampler = GenerationSampler::new().with_generated_tokens([1, 2]);
848 sampler.accept_token(3);
849 assert_eq!(sampler.generated_tokens(), &[1, 2, 3]);
850 sampler.set_generated_tokens([5, 8]);
851 assert_eq!(sampler.generated_tokens(), &[5, 8]);
852 sampler.clear_generated_tokens();
853 assert!(sampler.generated_tokens().is_empty());
854 }
855
856 #[test]
857 fn mirostat_state_is_backend_neutral() {
858 let mut sampler = MirostatV2Sampler::default();
859 sampler.accept_token(42, 2.0f32.powi(-7)).unwrap();
860 assert!((sampler.mu() - 9.8).abs() < 1e-6);
861 assert_eq!(sampler.generated_tokens(), &[42]);
862 sampler.reset();
863 assert_eq!(sampler.mu(), 10.0);
864 assert!(sampler.generated_tokens().is_empty());
865 }
866}