use crate::error::{InferenceError, InferenceResult};
use scirs2_core::ndarray::{Array1, Array2};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SamplingConfig {
pub strategy: SamplingStrategy,
pub temperature: f32,
pub top_k: Option<usize>,
pub top_p: Option<f32>,
pub beam_width: usize,
pub seed: Option<u64>,
}
impl Default for SamplingConfig {
fn default() -> Self {
Self {
strategy: SamplingStrategy::Greedy,
temperature: 1.0,
top_k: None,
top_p: None,
beam_width: 1,
seed: None,
}
}
}
impl SamplingConfig {
pub fn new() -> Self {
Self::default()
}
pub fn strategy(mut self, strategy: SamplingStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn temperature(mut self, temp: f32) -> Self {
self.temperature = temp;
self
}
pub fn top_k(mut self, k: usize) -> Self {
self.strategy = SamplingStrategy::TopK;
self.top_k = Some(k);
self
}
pub fn top_p(mut self, p: f32) -> Self {
self.strategy = SamplingStrategy::TopP;
self.top_p = Some(p);
self
}
pub fn beam_search(mut self, width: usize) -> Self {
self.strategy = SamplingStrategy::BeamSearch;
self.beam_width = width;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum SamplingStrategy {
Greedy,
Temperature,
TopK,
TopP,
BeamSearch,
Custom,
}
pub type CustomSamplingFn = Arc<dyn Fn(&Array1<f32>, f32) -> InferenceResult<f32> + Send + Sync>;
pub struct Sampler {
config: SamplingConfig,
custom_fn: Option<CustomSamplingFn>,
}
impl Sampler {
pub fn new(config: SamplingConfig) -> Self {
Self {
config,
custom_fn: None,
}
}
pub fn with_custom_fn(mut config: SamplingConfig, custom_fn: CustomSamplingFn) -> Self {
config.strategy = SamplingStrategy::Custom;
Self {
config,
custom_fn: Some(custom_fn),
}
}
pub fn set_custom_fn(&mut self, custom_fn: CustomSamplingFn) {
self.custom_fn = Some(custom_fn);
self.config.strategy = SamplingStrategy::Custom;
}
pub fn sample(&mut self, logits: &Array1<f32>) -> InferenceResult<f32> {
if logits.is_empty() {
return Err(InferenceError::DimensionMismatch {
expected: 1,
got: 0,
});
}
match self.config.strategy {
SamplingStrategy::Greedy => Ok(self.greedy_sample(logits)),
SamplingStrategy::Temperature => self.temperature_sample(logits),
SamplingStrategy::TopK => self.top_k_sample(logits),
SamplingStrategy::TopP => self.top_p_sample(logits),
SamplingStrategy::BeamSearch => {
Ok(self.greedy_sample(logits))
}
SamplingStrategy::Custom => {
if let Some(ref custom_fn) = self.custom_fn {
custom_fn(logits, self.config.temperature)
} else {
Ok(self.greedy_sample(logits))
}
}
}
}
pub fn sample_batch(&mut self, logits: &Array2<f32>) -> InferenceResult<Array1<f32>> {
let batch_size = logits.nrows();
let mut results = Vec::with_capacity(batch_size);
for i in 0..batch_size {
let logit_row = logits.row(i).to_owned();
results.push(self.sample(&logit_row)?);
}
Ok(Array1::from_vec(results))
}
fn greedy_sample(&self, logits: &Array1<f32>) -> f32 {
logits
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx as f32)
.unwrap_or(0.0)
}
fn temperature_sample(&mut self, logits: &Array1<f32>) -> InferenceResult<f32> {
let scaled = if (self.config.temperature - 1.0).abs() > 1e-6 {
logits.mapv(|x| x / self.config.temperature)
} else {
logits.clone()
};
let probs = softmax(&scaled);
self.sample_categorical(&probs)
}
fn top_k_sample(&mut self, logits: &Array1<f32>) -> InferenceResult<f32> {
let k = self.config.top_k.unwrap_or(10);
let mut indexed: Vec<_> = logits.iter().enumerate().collect();
indexed.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let top_k_indices: Vec<usize> = indexed.iter().take(k).map(|(idx, _)| *idx).collect();
let mut filtered = Array1::from_elem(logits.len(), f32::NEG_INFINITY);
for &idx in &top_k_indices {
filtered[idx] = logits[idx];
}
let probs = softmax(&filtered);
self.sample_categorical(&probs)
}
fn top_p_sample(&mut self, logits: &Array1<f32>) -> InferenceResult<f32> {
let p = self.config.top_p.unwrap_or(0.9);
let probs = softmax(logits);
let mut indexed: Vec<_> = probs.iter().enumerate().collect();
indexed.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let mut cumsum = 0.0;
let mut nucleus_size = 0;
for (_, &prob) in &indexed {
cumsum += prob;
nucleus_size += 1;
if cumsum >= p {
break;
}
}
let nucleus_indices: Vec<usize> = indexed
.iter()
.take(nucleus_size)
.map(|(idx, _)| *idx)
.collect();
let mut filtered = Array1::from_elem(logits.len(), f32::NEG_INFINITY);
for &idx in &nucleus_indices {
filtered[idx] = logits[idx];
}
let filtered_probs = softmax(&filtered);
self.sample_categorical(&filtered_probs)
}
fn sample_categorical(&mut self, probs: &Array1<f32>) -> InferenceResult<f32> {
use scirs2_core::random::{rng, RngExt};
let mut rng_gen = rng();
let uniform: f32 = rng_gen.random();
let mut cumsum = 0.0;
for (idx, &prob) in probs.iter().enumerate() {
cumsum += prob;
if uniform < cumsum {
return Ok(idx as f32);
}
}
Ok((probs.len() - 1) as f32)
}
pub fn config(&self) -> &SamplingConfig {
&self.config
}
}
fn softmax(logits: &Array1<f32>) -> Array1<f32> {
let max_logit = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let exp_logits = logits.mapv(|x| (x - max_logit).exp());
let sum_exp: f32 = exp_logits.sum();
if sum_exp > 0.0 {
exp_logits / sum_exp
} else {
Array1::from_elem(logits.len(), 1.0 / logits.len() as f32)
}
}
#[derive(Debug, Clone)]
pub struct Beam {
pub sequence: Vec<f32>,
pub log_prob: f32,
pub states: Vec<kizzasi_core::HiddenState>,
}
impl Beam {
pub fn new() -> Self {
Self {
sequence: Vec::new(),
log_prob: 0.0,
states: Vec::new(),
}
}
pub fn extend(&mut self, value: f32, log_prob: f32) {
self.sequence.push(value);
self.log_prob += log_prob;
}
pub fn avg_log_prob(&self) -> f32 {
if self.sequence.is_empty() {
0.0
} else {
self.log_prob / self.sequence.len() as f32
}
}
}
impl Default for Beam {
fn default() -> Self {
Self::new()
}
}
pub struct BeamSearch {
beam_width: usize,
beams: Vec<Beam>,
}
impl BeamSearch {
pub fn new(beam_width: usize) -> Self {
let beams = vec![Beam::new()];
Self { beam_width, beams }
}
pub fn expand(&mut self, logits: &Array2<f32>) -> InferenceResult<()> {
if logits.nrows() != self.beams.len() {
return Err(InferenceError::DimensionMismatch {
expected: self.beams.len(),
got: logits.nrows(),
});
}
let mut candidates = Vec::new();
for (beam_idx, beam) in self.beams.iter().enumerate() {
let beam_logits = logits.row(beam_idx).to_owned();
let probs = softmax(&beam_logits);
let mut indexed: Vec<_> = probs.iter().enumerate().collect();
indexed.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
for (idx, &prob) in indexed.iter().take(self.beam_width) {
let mut new_beam = beam.clone();
new_beam.extend(*idx as f32, prob.ln());
candidates.push(new_beam);
}
}
candidates.sort_by(|a, b| {
b.avg_log_prob()
.partial_cmp(&a.avg_log_prob())
.unwrap_or(std::cmp::Ordering::Equal)
});
self.beams = candidates.into_iter().take(self.beam_width).collect();
Ok(())
}
pub fn best(&self) -> Option<&Beam> {
self.beams.first()
}
pub fn beams(&self) -> &[Beam] {
&self.beams
}
}
pub type ConstraintFn = Arc<dyn Fn(&[f32]) -> bool + Send + Sync>;
pub struct ConstrainedBeamSearch {
beam_search: BeamSearch,
constraints: Vec<ConstraintFn>,
soft_constraints: bool,
constraint_penalty: f32,
}
impl ConstrainedBeamSearch {
pub fn new(beam_width: usize) -> Self {
Self {
beam_search: BeamSearch::new(beam_width),
constraints: Vec::new(),
soft_constraints: false,
constraint_penalty: 1.0,
}
}
pub fn add_constraint(mut self, constraint: ConstraintFn) -> Self {
self.constraints.push(constraint);
self
}
pub fn with_soft_constraints(mut self, penalty: f32) -> Self {
self.soft_constraints = true;
self.constraint_penalty = penalty;
self
}
fn satisfies_constraints(&self, sequence: &[f32]) -> bool {
self.constraints.iter().all(|c| c(sequence))
}
pub fn expand(&mut self, logits: &Array2<f32>) -> InferenceResult<()> {
self.beam_search.expand(logits)?;
if self.soft_constraints {
let violations: Vec<bool> = self
.beam_search
.beams
.iter()
.map(|beam| !self.satisfies_constraints(&beam.sequence))
.collect();
let penalty = self.constraint_penalty;
for (beam, &violates) in self.beam_search.beams.iter_mut().zip(violations.iter()) {
if violates {
beam.log_prob -= penalty;
}
}
self.beam_search.beams.sort_by(|a, b| {
b.log_prob
.partial_cmp(&a.log_prob)
.unwrap_or(std::cmp::Ordering::Equal)
});
} else {
let valid_beams: Vec<Beam> = self
.beam_search
.beams
.iter()
.filter(|beam| self.satisfies_constraints(&beam.sequence))
.cloned()
.collect();
if !valid_beams.is_empty() {
self.beam_search.beams = valid_beams;
}
}
Ok(())
}
pub fn best(&self) -> Option<&Beam> {
self.beam_search.best()
}
pub fn beams(&self) -> &[Beam] {
self.beam_search.beams()
}
pub fn num_constraints(&self) -> usize {
self.constraints.len()
}
}
use std::sync::Arc;
pub struct RejectionSampler {
base_sampler: Sampler,
constraints: Vec<ConstraintFn>,
max_attempts: usize,
fallback_strategy: FallbackStrategy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FallbackStrategy {
BestCandidate,
Greedy,
Error,
}
impl RejectionSampler {
pub fn new(config: SamplingConfig) -> Self {
Self {
base_sampler: Sampler::new(config),
constraints: Vec::new(),
max_attempts: 100,
fallback_strategy: FallbackStrategy::BestCandidate,
}
}
pub fn add_constraint(mut self, constraint: ConstraintFn) -> Self {
self.constraints.push(constraint);
self
}
pub fn max_attempts(mut self, attempts: usize) -> Self {
self.max_attempts = attempts;
self
}
pub fn fallback_strategy(mut self, strategy: FallbackStrategy) -> Self {
self.fallback_strategy = strategy;
self
}
pub fn sample_with_rejection(
&mut self,
logits: &Array1<f32>,
context: &[f32],
) -> InferenceResult<f32> {
if self.constraints.is_empty() {
return self.base_sampler.sample(logits);
}
let mut best_candidate = None;
let mut min_violations = usize::MAX;
for attempt in 0..self.max_attempts {
let candidate = self.base_sampler.sample(logits)?;
let mut test_sequence = context.to_vec();
test_sequence.push(candidate);
let violations = self.count_violations(&test_sequence);
if violations == 0 {
return Ok(candidate);
}
if violations < min_violations {
min_violations = violations;
best_candidate = Some(candidate);
}
if attempt > self.max_attempts / 2 && violations < self.constraints.len() / 2 {
break;
}
}
match self.fallback_strategy {
FallbackStrategy::BestCandidate => best_candidate.ok_or_else(|| {
InferenceError::ForwardError(
"Rejection sampling failed: no candidates generated".to_string(),
)
}),
FallbackStrategy::Greedy => {
let greedy_config = SamplingConfig::new().strategy(SamplingStrategy::Greedy);
let mut greedy_sampler = Sampler::new(greedy_config);
greedy_sampler.sample(logits)
}
FallbackStrategy::Error => Err(InferenceError::ForwardError(format!(
"Rejection sampling failed after {} attempts",
self.max_attempts
))),
}
}
fn count_violations(&self, sequence: &[f32]) -> usize {
self.constraints
.iter()
.filter(|constraint| !constraint(sequence))
.count()
}
pub fn base_sampler(&self) -> &Sampler {
&self.base_sampler
}
pub fn base_sampler_mut(&mut self) -> &mut Sampler {
&mut self.base_sampler
}
pub fn num_constraints(&self) -> usize {
self.constraints.len()
}
}
pub struct AdaptiveRejectionSampler {
rejection_sampler: RejectionSampler,
rejection_counts: Vec<usize>,
total_samples: usize,
}
impl AdaptiveRejectionSampler {
pub fn new(config: SamplingConfig, vocab_size: usize) -> Self {
Self {
rejection_sampler: RejectionSampler::new(config),
rejection_counts: vec![0; vocab_size],
total_samples: 0,
}
}
pub fn add_constraint(mut self, constraint: ConstraintFn) -> Self {
self.rejection_sampler = self.rejection_sampler.add_constraint(constraint);
self
}
pub fn sample_adaptive(
&mut self,
logits: &Array1<f32>,
context: &[f32],
) -> InferenceResult<f32> {
self.total_samples += 1;
let mut adjusted_logits = logits.clone();
if self.total_samples > 10 {
let max_rejections = *self.rejection_counts.iter().max().unwrap_or(&1) as f32;
for (i, &count) in self.rejection_counts.iter().enumerate() {
if i < adjusted_logits.len() && count > 0 {
let penalty = (count as f32 / max_rejections) * 2.0;
adjusted_logits[i] -= penalty;
}
}
}
let result = self
.rejection_sampler
.sample_with_rejection(&adjusted_logits, context);
if let Ok(value) = result {
Ok(value)
} else {
let greedy_config = SamplingConfig::new().strategy(SamplingStrategy::Greedy);
let mut greedy_sampler = Sampler::new(greedy_config);
if let Ok(fallback) = greedy_sampler.sample(&adjusted_logits) {
let idx = fallback as usize;
if idx < self.rejection_counts.len() {
self.rejection_counts[idx] += 1;
}
}
Err(InferenceError::ForwardError(
"Adaptive rejection sampling failed".to_string(),
))
}
}
pub fn rejection_rate(&self) -> f32 {
if self.total_samples == 0 {
return 0.0;
}
let total_rejections: usize = self.rejection_counts.iter().sum();
total_rejections as f32 / self.total_samples as f32
}
pub fn reset_stats(&mut self) {
self.rejection_counts.fill(0);
self.total_samples = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greedy_sampling() {
let config = SamplingConfig::new().strategy(SamplingStrategy::Greedy);
let mut sampler = Sampler::new(config);
let logits = Array1::from_vec(vec![0.1, 0.5, 0.3, 0.8, 0.2]);
let result = sampler.sample(&logits).unwrap();
assert_eq!(result, 3.0); }
#[test]
fn test_temperature_sampling() {
let config = SamplingConfig::new()
.strategy(SamplingStrategy::Temperature)
.temperature(0.5)
.seed(42);
let mut sampler = Sampler::new(config);
let logits = Array1::from_vec(vec![0.1, 0.5, 0.3, 0.8, 0.2]);
let result = sampler.sample(&logits);
assert!(result.is_ok());
}
#[test]
fn test_top_k_sampling() {
let config = SamplingConfig::new().top_k(3).seed(42);
let mut sampler = Sampler::new(config);
let logits = Array1::from_vec(vec![0.1, 0.5, 0.3, 0.8, 0.2]);
let result = sampler.sample(&logits);
assert!(result.is_ok());
}
#[test]
fn test_top_p_sampling() {
let config = SamplingConfig::new().top_p(0.9).seed(42);
let mut sampler = Sampler::new(config);
let logits = Array1::from_vec(vec![0.1, 0.5, 0.3, 0.8, 0.2]);
let result = sampler.sample(&logits);
assert!(result.is_ok());
}
#[test]
fn test_softmax() {
let logits = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let probs = softmax(&logits);
let sum: f32 = probs.sum();
assert!((sum - 1.0).abs() < 1e-6);
let max_idx = probs
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(idx, _)| idx)
.unwrap();
assert_eq!(max_idx, 2);
}
#[test]
fn test_beam_search() {
let mut bs = BeamSearch::new(2);
let logits1 = Array2::from_shape_vec((1, 3), vec![0.5, 0.3, 0.2]).unwrap();
bs.expand(&logits1).unwrap();
assert_eq!(bs.beams().len(), 2);
let logits2 = Array2::from_shape_vec((2, 3), vec![0.4, 0.3, 0.3, 0.5, 0.3, 0.2]).unwrap();
bs.expand(&logits2).unwrap();
assert_eq!(bs.beams().len(), 2);
let best = bs.best().unwrap();
assert_eq!(best.sequence.len(), 2);
}
#[test]
fn test_beam_avg_log_prob() {
let mut beam = Beam::new();
beam.extend(1.0, -0.5);
beam.extend(2.0, -0.3);
let avg = beam.avg_log_prob();
assert!((avg - (-0.4)).abs() < 1e-6);
}
#[test]
fn test_sample_batch() {
let config = SamplingConfig::new().strategy(SamplingStrategy::Greedy);
let mut sampler = Sampler::new(config);
let logits = Array2::from_shape_vec(
(3, 4),
vec![
0.1, 0.5, 0.3, 0.2, 0.8, 0.2, 0.1, 0.3, 0.2, 0.3, 0.9, 0.1, ],
)
.unwrap();
let results = sampler.sample_batch(&logits).unwrap();
assert_eq!(results[0], 1.0);
assert_eq!(results[1], 0.0);
assert_eq!(results[2], 2.0);
}
}