Skip to main content

eredu_runtime/
generation.rs

1//! Backend-neutral causal-model and token-sampling contracts.
2
3use eredu_core::{
4    generation::ResolvedGenerationConfig, SpeculativeTokenFilterController, TokenFilter,
5    TokenFilterController,
6};
7use eredu_nn::Tensor;
8
9/// Monomorphized causal model used by generation sessions.
10pub trait CausalModel<S> {
11    /// Backend-native tensor handle containing logits and decode token ids.
12    type Tensor: Tensor;
13    /// Borrowed, tokenizer/media-prepared prefill input.
14    type Input<'a>: Copy;
15    /// Concrete model or backend failure.
16    type Error;
17
18    /// Computes initial logits and updates mutable state.
19    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    /// Computes logits for decode tokens using existing mutable state.
27    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    /// Adjusts prefill logits before backend-native sampling.
35    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/// Portable cardinality of one zero-based token-id domain.
46///
47/// Backends validate native token tensors without copying their values to the
48/// host. Architectures select the exact domain for each prediction boundary.
49#[derive(Debug, Clone, Copy, Eq, PartialEq)]
50pub struct TokenDomain {
51    cardinality: usize,
52}
53
54impl TokenDomain {
55    /// Creates the token IDs `0..cardinality`.
56    pub const fn new(cardinality: usize) -> Self {
57        Self { cardinality }
58    }
59
60    /// Number of valid zero-based token IDs.
61    pub const fn cardinality(self) -> usize {
62        self.cardinality
63    }
64}
65
66/// Backend primitives required by generic token-sampling policies.
67///
68/// The runtime owns ordering, history, adaptive state, and constraint rollback.
69/// Implementations operate directly on native logits and random state without
70/// copying values through a neutral tensor representation.
71pub trait SamplingBackend {
72    /// Backend-native logits tensor.
73    type Logits: Clone;
74    /// Backend-native sampled-token tensor.
75    type Token: Clone;
76    /// Backend-native random-key stream.
77    type RandomState;
78    /// Execution context, such as a stream.
79    type Context: ?Sized;
80    /// Backend failure.
81    type Error;
82
83    /// Creates a backend error for a portable policy or constraint failure.
84    fn error(message: String) -> Self::Error;
85
86    /// Validates a native token tensor against one architecture-selected domain.
87    ///
88    /// The returned token must retain a backend-native dependency on the range
89    /// check so lazy backends cannot commit an unchecked forced token.
90    fn validate_token(
91        token: &Self::Token,
92        domain: TokenDomain,
93        context: &Self::Context,
94    ) -> Result<Self::Token, Self::Error>;
95
96    /// Scales logits by inverse temperature, preserving the native tensor.
97    fn scale_temperature(
98        logits: &Self::Logits,
99        temperature: f32,
100        context: &Self::Context,
101    ) -> Result<Self::Logits, Self::Error>;
102
103    /// Applies repetition, frequency, and presence penalties.
104    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    /// Masks all but the highest `top_k` logits. Non-positive values disable it.
112    fn apply_top_k(
113        logits: Self::Logits,
114        top_k: i32,
115        context: &Self::Context,
116    ) -> Result<Self::Logits, Self::Error>;
117
118    /// Applies nucleus filtering while retaining canonical vocabulary order.
119    fn apply_top_p(
120        logits: Self::Logits,
121        top_p: f32,
122        context: &Self::Context,
123    ) -> Result<Self::Logits, Self::Error>;
124
125    /// Applies minimum-relative-probability filtering.
126    fn apply_min_p(
127        logits: Self::Logits,
128        min_p: f32,
129        context: &Self::Context,
130    ) -> Result<Self::Logits, Self::Error>;
131
132    /// Masks tokens rejected by a portable vocabulary filter.
133    fn apply_token_filter(
134        logits: &Self::Logits,
135        filter: &TokenFilter,
136        context: &Self::Context,
137    ) -> Result<Self::Logits, Self::Error>;
138
139    /// Applies Mirostat's surprise cutoff after penalties and temperature.
140    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    /// Selects from raw logits, applying temperature for stochastic sampling.
150    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    /// Selects from logits already scaled by the policy.
158    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    /// Materializes only the selected scalar token identifier.
166    fn token_id(token: &Self::Token, context: &Self::Context) -> Result<u32, Self::Error>;
167
168    /// Materializes one committed token probability from processed logits.
169    fn token_probability(
170        logits: &Self::Logits,
171        token: u32,
172        context: &Self::Context,
173    ) -> Result<f32, Self::Error>;
174}
175
176/// Backend-neutral repetition/frequency/presence controls.
177#[derive(Debug, Clone, Copy, PartialEq)]
178pub struct PenaltyConfig {
179    /// Repetition multiplier; `1.0` disables it.
180    pub repeat_penalty: f32,
181    /// Number of recent tokens considered; negative means all.
182    pub repeat_last_n: i32,
183    /// Per-occurrence logit penalty.
184    pub frequency_penalty: f32,
185    /// One-time penalty for every present token.
186    pub presence_penalty: f32,
187}
188
189impl PenaltyConfig {
190    /// Returns whether every penalty is disabled.
191    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
207/// Sampling policy suitable for lossless speculative decoding.
208pub trait SpeculativeSampler<B: SamplingBackend> {
209    /// Whether loaded checkpoint defaults should wrap this policy.
210    fn uses_checkpoint_defaults(&self) -> bool {
211        false
212    }
213
214    /// Whether optimistic draft work is an exact discardable fork.
215    fn supports_exact_optimistic_promotion(&self) -> bool {
216        false
217    }
218
219    /// Whether the committed generation grammar is complete.
220    fn grammar_is_complete(&mut self) -> Result<bool, B::Error> {
221        Ok(false)
222    }
223
224    /// Whether an uncommitted logical prefix completes the grammar.
225    fn prefix_is_complete(&self, _history: &[u32]) -> Result<bool, B::Error> {
226        Ok(false)
227    }
228
229    /// Applies penalties, filters, and temperature.
230    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    /// Selects from already processed logits.
239    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    /// Commits a token selected from a processed target distribution.
250    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
260/// Strategy for choosing a token from model logits.
261pub trait Sampler<B: SamplingBackend> {
262    /// Whether loaded checkpoint defaults should wrap this policy.
263    fn uses_checkpoint_defaults(&self) -> bool {
264        false
265    }
266
267    /// Selects one token from raw model logits.
268    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
277/// Grammar-aware wrapper around a backend-neutral sampling policy.
278pub 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    /// Wraps a policy with a portable canonical constraint controller.
299    pub fn new(policy: S, controller: C) -> Self {
300        Self { policy, controller }
301    }
302
303    /// Returns the wrapped policy.
304    pub const fn policy(&self) -> &S {
305        &self.policy
306    }
307
308    /// Returns the portable constraint controller.
309    pub const fn controller(&self) -> &C {
310        &self.controller
311    }
312
313    /// Returns the portable constraint controller mutably.
314    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/// Stateless greedy/categorical sampler.
432#[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/// Configurable backend-neutral text sampler.
476#[derive(Debug, Clone)]
477pub struct GenerationSampler {
478    /// Keep only the `top_k` highest-logit tokens when positive.
479    pub top_k: i32,
480    /// Nucleus probability mass.
481    pub top_p: f32,
482    /// Minimum probability relative to the most probable token.
483    pub min_p: f32,
484    /// Repetition multiplier.
485    pub repeat_penalty: f32,
486    /// Number of recent tokens considered by penalties.
487    pub repeat_last_n: i32,
488    /// Per-occurrence penalty.
489    pub frequency_penalty: f32,
490    /// One-time presence penalty.
491    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    /// Creates a sampler with default controls.
512    pub fn new() -> Self {
513        Self::default()
514    }
515
516    /// Creates a sampler from a resolved portable generation configuration.
517    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    /// Seeds accepted-token history.
531    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    /// Sets top-k filtering.
537    pub fn top_k(mut self, value: i32) -> Self {
538        self.top_k = value;
539        self
540    }
541
542    /// Sets nucleus filtering.
543    pub fn top_p(mut self, value: f32) -> Self {
544        self.top_p = value;
545        self
546    }
547
548    /// Sets minimum-relative-probability filtering.
549    pub fn min_p(mut self, value: f32) -> Self {
550        self.min_p = value;
551        self
552    }
553
554    /// Sets repetition, frequency, and presence penalties.
555    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    /// Returns accepted-token history.
570    pub fn generated_tokens(&self) -> &[u32] {
571        &self.generated_tokens
572    }
573
574    /// Replaces accepted-token history.
575    pub fn set_generated_tokens(&mut self, tokens: impl IntoIterator<Item = u32>) {
576        self.generated_tokens = tokens.into_iter().collect();
577    }
578
579    /// Records a token accepted outside this sampler.
580    pub fn accept_token(&mut self, token: u32) {
581        self.generated_tokens.push(token);
582    }
583
584    /// Clears accepted-token history.
585    pub fn clear_generated_tokens(&mut self) {
586        self.generated_tokens.clear();
587    }
588
589    /// Returns the portable penalty controls.
590    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/// Adaptive Mirostat V2 policy with backend-neutral state.
649#[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    /// Creates a sampler targeting `tau` bits of surprise.
670    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    /// Sets penalties applied before adaptive truncation.
682    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    /// Target surprise in bits.
699    pub const fn tau(&self) -> f32 {
700        self.tau
701    }
702
703    /// Adaptation rate.
704    pub const fn eta(&self) -> f32 {
705        self.eta
706    }
707
708    /// Current adaptive surprise limit.
709    pub const fn mu(&self) -> f32 {
710        self.mu
711    }
712
713    /// Accepted-token history.
714    pub fn generated_tokens(&self) -> &[u32] {
715        self.penalties.generated_tokens()
716    }
717
718    /// Records an externally accepted token and its normalized probability.
719    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    /// Resets adaptive state and history.
735    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/// Invalid backend-neutral sampling configuration.
820#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
821pub enum SamplingConfigurationError {
822    /// A numeric or probability control is invalid.
823    #[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}