use eredu_core::{
generation::ResolvedGenerationConfig, SpeculativeTokenFilterController, TokenFilter,
TokenFilterController,
};
use eredu_nn::Tensor;
pub trait CausalModel<S> {
type Tensor: Tensor;
type Input<'a>: Copy;
type Error;
fn prefill_input_logits(
&mut self,
input: Self::Input<'_>,
state: &mut S,
context: &<Self::Tensor as Tensor>::Context,
) -> Result<Self::Tensor, Self::Error>;
fn decode_logits(
&mut self,
input_tokens: &Self::Tensor,
state: &mut S,
context: &<Self::Tensor as Tensor>::Context,
) -> Result<Self::Tensor, Self::Error>;
fn adjust_prefill_logits(
&mut self,
logits: Self::Tensor,
_state: &mut S,
_context: &<Self::Tensor as Tensor>::Context,
) -> Result<Self::Tensor, Self::Error> {
Ok(logits)
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct TokenDomain {
cardinality: usize,
}
impl TokenDomain {
pub const fn new(cardinality: usize) -> Self {
Self { cardinality }
}
pub const fn cardinality(self) -> usize {
self.cardinality
}
}
pub trait SamplingBackend {
type Logits: Clone;
type Token: Clone;
type RandomState;
type Context: ?Sized;
type Error;
fn error(message: String) -> Self::Error;
fn validate_token(
token: &Self::Token,
domain: TokenDomain,
context: &Self::Context,
) -> Result<Self::Token, Self::Error>;
fn scale_temperature(
logits: &Self::Logits,
temperature: f32,
context: &Self::Context,
) -> Result<Self::Logits, Self::Error>;
fn apply_penalties(
logits: &Self::Logits,
history: &[u32],
penalties: PenaltyConfig,
context: &Self::Context,
) -> Result<Self::Logits, Self::Error>;
fn apply_top_k(
logits: Self::Logits,
top_k: i32,
context: &Self::Context,
) -> Result<Self::Logits, Self::Error>;
fn apply_top_p(
logits: Self::Logits,
top_p: f32,
context: &Self::Context,
) -> Result<Self::Logits, Self::Error>;
fn apply_min_p(
logits: Self::Logits,
min_p: f32,
context: &Self::Context,
) -> Result<Self::Logits, Self::Error>;
fn apply_token_filter(
logits: &Self::Logits,
filter: &TokenFilter,
context: &Self::Context,
) -> Result<Self::Logits, Self::Error>;
fn apply_mirostat(
logits: &Self::Logits,
history: &[u32],
penalties: PenaltyConfig,
temperature: f32,
mu: f32,
context: &Self::Context,
) -> Result<Self::Logits, Self::Error>;
fn sample_raw(
logits: &Self::Logits,
temperature: f32,
random: Option<&mut Self::RandomState>,
context: &Self::Context,
) -> Result<Self::Token, Self::Error>;
fn sample_processed(
logits: &Self::Logits,
temperature: f32,
random: Option<&mut Self::RandomState>,
context: &Self::Context,
) -> Result<Self::Token, Self::Error>;
fn token_id(token: &Self::Token, context: &Self::Context) -> Result<u32, Self::Error>;
fn token_probability(
logits: &Self::Logits,
token: u32,
context: &Self::Context,
) -> Result<f32, Self::Error>;
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PenaltyConfig {
pub repeat_penalty: f32,
pub repeat_last_n: i32,
pub frequency_penalty: f32,
pub presence_penalty: f32,
}
impl PenaltyConfig {
pub fn is_identity(self) -> bool {
self.repeat_penalty == 1.0 && self.frequency_penalty == 0.0 && self.presence_penalty == 0.0
}
}
impl Default for PenaltyConfig {
fn default() -> Self {
Self {
repeat_penalty: 1.0,
repeat_last_n: 64,
frequency_penalty: 0.0,
presence_penalty: 0.0,
}
}
}
pub trait SpeculativeSampler<B: SamplingBackend> {
fn uses_checkpoint_defaults(&self) -> bool {
false
}
fn supports_exact_optimistic_promotion(&self) -> bool {
false
}
fn grammar_is_complete(&mut self) -> Result<bool, B::Error> {
Ok(false)
}
fn prefix_is_complete(&self, _history: &[u32]) -> Result<bool, B::Error> {
Ok(false)
}
fn process_logits(
&mut self,
logits: &B::Logits,
temperature: f32,
history: &[u32],
context: &B::Context,
) -> Result<B::Logits, B::Error>;
fn sample_processed(
&self,
logits: &B::Logits,
temperature: f32,
random: Option<&mut B::RandomState>,
context: &B::Context,
) -> Result<B::Token, B::Error> {
B::sample_processed(logits, temperature, random, context)
}
fn commit_token(
&mut self,
_processed_logits: &B::Logits,
_token: u32,
_context: &B::Context,
) -> Result<(), B::Error> {
Ok(())
}
}
pub trait Sampler<B: SamplingBackend> {
fn uses_checkpoint_defaults(&self) -> bool {
false
}
fn sample(
&mut self,
logits: &B::Logits,
temperature: f32,
random: Option<&mut B::RandomState>,
context: &B::Context,
) -> Result<B::Token, B::Error>;
}
pub struct ConstrainedSampler<S, C> {
policy: S,
controller: C,
}
struct ConstraintCheckpoint<S, C> {
policy: S,
controller: C,
}
impl<S: Clone, C: Clone> Clone for ConstrainedSampler<S, C> {
fn clone(&self) -> Self {
Self {
policy: self.policy.clone(),
controller: self.controller.clone(),
}
}
}
impl<S, C> ConstrainedSampler<S, C> {
pub fn new(policy: S, controller: C) -> Self {
Self { policy, controller }
}
pub const fn policy(&self) -> &S {
&self.policy
}
pub const fn controller(&self) -> &C {
&self.controller
}
pub fn controller_mut(&mut self) -> &mut C {
&mut self.controller
}
}
impl<S: Clone, C: Clone> ConstrainedSampler<S, C> {
fn checkpoint(&self) -> ConstraintCheckpoint<S, C> {
ConstraintCheckpoint {
policy: self.policy.clone(),
controller: self.controller.clone(),
}
}
}
impl<B, S, C> SpeculativeSampler<B> for ConstrainedSampler<S, C>
where
B: SamplingBackend,
S: SpeculativeSampler<B> + Clone,
C: SpeculativeTokenFilterController,
{
fn supports_exact_optimistic_promotion(&self) -> bool {
self.policy.supports_exact_optimistic_promotion()
}
fn grammar_is_complete(&mut self) -> Result<bool, B::Error> {
self.controller
.is_complete()
.map_err(|error| B::error(error.to_string()))
}
fn prefix_is_complete(&self, history: &[u32]) -> Result<bool, B::Error> {
self.controller
.prefix_is_complete(history)
.map_err(|error| B::error(error.to_string()))
}
fn process_logits(
&mut self,
logits: &B::Logits,
temperature: f32,
history: &[u32],
context: &B::Context,
) -> Result<B::Logits, B::Error> {
let filter = self
.controller
.filter_at(history)
.map_err(|error| B::error(error.to_string()))?;
let masked = B::apply_token_filter(logits, &filter, context)?;
self.policy
.process_logits(&masked, temperature, history, context)
}
fn sample_processed(
&self,
logits: &B::Logits,
temperature: f32,
random: Option<&mut B::RandomState>,
context: &B::Context,
) -> Result<B::Token, B::Error> {
self.policy
.sample_processed(logits, temperature, random, context)
}
fn commit_token(
&mut self,
processed_logits: &B::Logits,
token: u32,
context: &B::Context,
) -> Result<(), B::Error> {
let checkpoint = self.checkpoint();
if let Err(error) = self
.policy
.commit_token(processed_logits, token, context)
.and_then(|()| {
self.controller
.commit_token(token)
.map_err(|error| B::error(error.to_string()))
})
{
self.policy = checkpoint.policy;
self.controller = checkpoint.controller;
return Err(error);
}
Ok(())
}
}
impl<B, S, C> Sampler<B> for ConstrainedSampler<S, C>
where
B: SamplingBackend,
S: Sampler<B> + Clone,
C: TokenFilterController + Clone,
{
fn sample(
&mut self,
logits: &B::Logits,
temperature: f32,
random: Option<&mut B::RandomState>,
context: &B::Context,
) -> Result<B::Token, B::Error> {
let checkpoint = self.checkpoint();
let filter = self
.controller
.current_filter()
.map_err(|error| B::error(error.to_string()))?;
let masked = B::apply_token_filter(logits, &filter, context)?;
let token = self.policy.sample(&masked, temperature, random, context)?;
let token_id = B::token_id(&token, context)?;
if let Err(error) = self.controller.commit_token(token_id) {
self.policy = checkpoint.policy;
self.controller = checkpoint.controller;
return Err(B::error(error.to_string()));
}
Ok(token)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DefaultSampler;
impl<B: SamplingBackend> SpeculativeSampler<B> for DefaultSampler {
fn uses_checkpoint_defaults(&self) -> bool {
true
}
fn supports_exact_optimistic_promotion(&self) -> bool {
true
}
fn process_logits(
&mut self,
logits: &B::Logits,
temperature: f32,
_history: &[u32],
context: &B::Context,
) -> Result<B::Logits, B::Error> {
if temperature == 0.0 {
Ok(logits.clone())
} else {
B::scale_temperature(logits, temperature, context)
}
}
}
impl<B: SamplingBackend> Sampler<B> for DefaultSampler {
fn uses_checkpoint_defaults(&self) -> bool {
true
}
fn sample(
&mut self,
logits: &B::Logits,
temperature: f32,
random: Option<&mut B::RandomState>,
context: &B::Context,
) -> Result<B::Token, B::Error> {
B::sample_raw(logits, temperature, random, context)
}
}
#[derive(Debug, Clone)]
pub struct GenerationSampler {
pub top_k: i32,
pub top_p: f32,
pub min_p: f32,
pub repeat_penalty: f32,
pub repeat_last_n: i32,
pub frequency_penalty: f32,
pub presence_penalty: f32,
generated_tokens: Vec<u32>,
}
impl Default for GenerationSampler {
fn default() -> Self {
Self {
top_k: 40,
top_p: 0.95,
min_p: 0.05,
repeat_penalty: 1.0,
repeat_last_n: 64,
frequency_penalty: 0.0,
presence_penalty: 0.0,
generated_tokens: Vec::new(),
}
}
}
impl GenerationSampler {
pub fn new() -> Self {
Self::default()
}
pub fn from_resolved(config: ResolvedGenerationConfig) -> Self {
Self::new()
.top_k(config.top_k)
.top_p(config.top_p)
.min_p(config.min_p)
.penalties(
config.repetition_penalty,
config.repeat_last_n,
config.frequency_penalty,
config.presence_penalty,
)
}
pub fn with_generated_tokens(mut self, tokens: impl IntoIterator<Item = u32>) -> Self {
self.generated_tokens = tokens.into_iter().collect();
self
}
pub fn top_k(mut self, value: i32) -> Self {
self.top_k = value;
self
}
pub fn top_p(mut self, value: f32) -> Self {
self.top_p = value;
self
}
pub fn min_p(mut self, value: f32) -> Self {
self.min_p = value;
self
}
pub fn penalties(
mut self,
repeat_penalty: f32,
repeat_last_n: i32,
frequency_penalty: f32,
presence_penalty: f32,
) -> Self {
self.repeat_penalty = repeat_penalty;
self.repeat_last_n = repeat_last_n;
self.frequency_penalty = frequency_penalty;
self.presence_penalty = presence_penalty;
self
}
pub fn generated_tokens(&self) -> &[u32] {
&self.generated_tokens
}
pub fn set_generated_tokens(&mut self, tokens: impl IntoIterator<Item = u32>) {
self.generated_tokens = tokens.into_iter().collect();
}
pub fn accept_token(&mut self, token: u32) {
self.generated_tokens.push(token);
}
pub fn clear_generated_tokens(&mut self) {
self.generated_tokens.clear();
}
pub const fn penalty_config(&self) -> PenaltyConfig {
PenaltyConfig {
repeat_penalty: self.repeat_penalty,
repeat_last_n: self.repeat_last_n,
frequency_penalty: self.frequency_penalty,
presence_penalty: self.presence_penalty,
}
}
fn process_for<B: SamplingBackend>(
&self,
logits: &B::Logits,
history: &[u32],
context: &B::Context,
) -> Result<B::Logits, B::Error> {
let logits = B::apply_penalties(logits, history, self.penalty_config(), context)?;
let logits = B::apply_top_k(logits, self.top_k, context)?;
let logits = B::apply_top_p(logits, self.top_p, context)?;
B::apply_min_p(logits, self.min_p, context)
}
}
impl<B: SamplingBackend> SpeculativeSampler<B> for GenerationSampler {
fn supports_exact_optimistic_promotion(&self) -> bool {
true
}
fn process_logits(
&mut self,
logits: &B::Logits,
temperature: f32,
history: &[u32],
context: &B::Context,
) -> Result<B::Logits, B::Error> {
let logits = self.process_for::<B>(logits, history, context)?;
if temperature == 0.0 {
Ok(logits)
} else {
B::scale_temperature(&logits, temperature, context)
}
}
}
impl<B: SamplingBackend> Sampler<B> for GenerationSampler {
fn sample(
&mut self,
logits: &B::Logits,
temperature: f32,
random: Option<&mut B::RandomState>,
context: &B::Context,
) -> Result<B::Token, B::Error> {
let logits = self.process_for::<B>(logits, &self.generated_tokens, context)?;
let token = B::sample_raw(&logits, temperature, random, context)?;
self.generated_tokens.push(B::token_id(&token, context)?);
Ok(token)
}
}
#[derive(Debug, Clone)]
pub struct MirostatV2Sampler {
tau: f32,
eta: f32,
mu: f32,
penalties: GenerationSampler,
}
impl Default for MirostatV2Sampler {
fn default() -> Self {
Self {
tau: 5.0,
eta: 0.1,
mu: 10.0,
penalties: GenerationSampler::new().top_k(0).top_p(1.0).min_p(0.0),
}
}
}
impl MirostatV2Sampler {
pub fn new(tau: f32, eta: f32) -> Result<Self, SamplingConfigurationError> {
validate_positive_finite("Mirostat V2 tau", tau)?;
validate_positive_finite("Mirostat V2 eta", eta)?;
Ok(Self {
tau,
eta,
mu: 2.0 * tau,
penalties: GenerationSampler::new().top_k(0).top_p(1.0).min_p(0.0),
})
}
pub fn penalties(
mut self,
repeat_penalty: f32,
repeat_last_n: i32,
frequency_penalty: f32,
presence_penalty: f32,
) -> Self {
self.penalties = self.penalties.penalties(
repeat_penalty,
repeat_last_n,
frequency_penalty,
presence_penalty,
);
self
}
pub const fn tau(&self) -> f32 {
self.tau
}
pub const fn eta(&self) -> f32 {
self.eta
}
pub const fn mu(&self) -> f32 {
self.mu
}
pub fn generated_tokens(&self) -> &[u32] {
self.penalties.generated_tokens()
}
pub fn accept_token(
&mut self,
token: u32,
probability: f32,
) -> Result<(), SamplingConfigurationError> {
if !probability.is_finite() || probability <= 0.0 || probability > 1.0 {
return Err(SamplingConfigurationError::Invalid(
"accepted Mirostat V2 token probability must be finite and in (0, 1]".into(),
));
}
self.update_mu(-probability.log2());
self.penalties.accept_token(token);
Ok(())
}
pub fn reset(&mut self) {
self.mu = 2.0 * self.tau;
self.penalties.clear_generated_tokens();
}
fn update_mu(&mut self, observed_surprise: f32) {
self.mu -= self.eta * (observed_surprise - self.tau);
}
fn process_for<B: SamplingBackend>(
&self,
logits: &B::Logits,
temperature: f32,
history: &[u32],
context: &B::Context,
) -> Result<B::Logits, B::Error> {
if !temperature.is_finite() || temperature <= 0.0 {
return Err(B::error(
"Mirostat V2 requires a finite temperature greater than zero".into(),
));
}
B::apply_mirostat(
logits,
history,
self.penalties.penalty_config(),
temperature,
self.mu,
context,
)
}
fn commit_for<B: SamplingBackend>(
&mut self,
logits: &B::Logits,
token: u32,
context: &B::Context,
) -> Result<(), B::Error> {
let probability = B::token_probability(logits, token, context)?;
self.accept_token(token, probability)
.map_err(|error| B::error(error.to_string()))
}
}
impl<B: SamplingBackend> Sampler<B> for MirostatV2Sampler {
fn sample(
&mut self,
logits: &B::Logits,
temperature: f32,
random: Option<&mut B::RandomState>,
context: &B::Context,
) -> Result<B::Token, B::Error> {
let processed = self.process_for::<B>(
logits,
temperature,
self.penalties.generated_tokens(),
context,
)?;
let token = B::sample_processed(&processed, temperature, random, context)?;
self.commit_for::<B>(&processed, B::token_id(&token, context)?, context)?;
Ok(token)
}
}
impl<B: SamplingBackend> SpeculativeSampler<B> for MirostatV2Sampler {
fn process_logits(
&mut self,
logits: &B::Logits,
temperature: f32,
history: &[u32],
context: &B::Context,
) -> Result<B::Logits, B::Error> {
self.process_for::<B>(logits, temperature, history, context)
}
fn commit_token(
&mut self,
processed_logits: &B::Logits,
token: u32,
context: &B::Context,
) -> Result<(), B::Error> {
self.commit_for::<B>(processed_logits, token, context)
}
}
#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
pub enum SamplingConfigurationError {
#[error("{0}")]
Invalid(String),
}
fn validate_positive_finite(name: &str, value: f32) -> Result<(), SamplingConfigurationError> {
if value.is_finite() && value > 0.0 {
Ok(())
} else {
Err(SamplingConfigurationError::Invalid(format!(
"{name} must be finite and greater than zero"
)))
}
}
#[cfg(test)]
mod tests {
use super::{GenerationSampler, MirostatV2Sampler};
#[test]
fn generation_history_is_backend_neutral() {
let mut sampler = GenerationSampler::new().with_generated_tokens([1, 2]);
sampler.accept_token(3);
assert_eq!(sampler.generated_tokens(), &[1, 2, 3]);
sampler.set_generated_tokens([5, 8]);
assert_eq!(sampler.generated_tokens(), &[5, 8]);
sampler.clear_generated_tokens();
assert!(sampler.generated_tokens().is_empty());
}
#[test]
fn mirostat_state_is_backend_neutral() {
let mut sampler = MirostatV2Sampler::default();
sampler.accept_token(42, 2.0f32.powi(-7)).unwrap();
assert!((sampler.mu() - 9.8).abs() < 1e-6);
assert_eq!(sampler.generated_tokens(), &[42]);
sampler.reset();
assert_eq!(sampler.mu(), 10.0);
assert!(sampler.generated_tokens().is_empty());
}
}