Documentation / Build API Documentation (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 7s
CI / Format Check (push) Failing after 10s
CI / Build (ubuntu-latest) (push) Failing after 29s
Performance Benchmarks / Run Benchmarks (push) Successful in 31s
CI / Test (macos-latest) (push) Has been skipped
CI / Test (ubuntu-latest) (push) Has been skipped
CI / CI Success (push) Failing after 1s
CI / Build (macos-latest) (push) Failing after 9s
CI / Python Bindings (maturin) (macos-latest) (push) Has been skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Has been skipped
CI / WASM Build + Size Check (push) Has been skipped
CI / Distributed Training Tests (push) Has been skipped
CI / Clippy Check (push) Failing after 34s
CI / Build CPU-Only (Explicit) (push) Failing after 48s
- rtx-onnx-codegen: re-export AttributeValue from ir (private-module import broke the whole crate; remaining errors were knock-ons). - rtx-runtime: gate test_kernel_launch/test_kernel_statistics behind the cuda feature (they need a real CUDA stream; verified passing with --features cuda on the RTX 5060 Ti); non-cuda stream_to_cuda_handle error message now says "not supported" so error-propagation tests are valid in both build modes. - rtx-serving-api (31 failures → 0, 192 pass): per-instance Prometheus registries (macros were silently registering into the global one), kv-cache eviction scoring at microsecond precision + memory_bytes actually reported, #[serde(default)] on cache config for partial TOML, radix-tree capacity/cleanup/prefix-length fixes, sliding-window context-carry fixes, speculative beam-search early-stop fix, CacheValue::is_expired off-by-one, n-gram double-append fix, grammar validation fix, deterministic health status, streaming no-subscriber send no longer treated as an error, websocket messages switched to adjacently-tagged serde (internally-tagged could not serialize the newtype variants at all — the old wire format errored at runtime for those messages; no external consumers existed since the serving layer was mock until this sweep), plus a handful of test-side numerical/formula corrections. Co-Authored-By: Claude Fable 5 <[email protected]>
802 lines
25 KiB
Rust
802 lines
25 KiB
Rust
//! Advanced sampling strategies for LLM generation
|
|
//!
|
|
//! Provides comprehensive sampling algorithms including:
|
|
//! - Nucleus sampling (top-p) with dynamic threshold adjustment
|
|
//! - Top-k sampling with adaptive k selection
|
|
//! - Temperature scaling with per-token adjustment
|
|
//! - Repetition penalty with context-aware scoring
|
|
//! - Presence penalty with semantic similarity detection
|
|
//! - Custom sampling strategies with pluggable algorithms
|
|
|
|
use anyhow::{Result, anyhow};
|
|
use rand::SeedableRng;
|
|
use rand::distributions::{Distribution, WeightedIndex};
|
|
use rand::rngs::StdRng;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{cmp::Ordering, collections::HashMap};
|
|
|
|
/// Sampling configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct SamplingConfig {
|
|
pub strategy: SamplingStrategy,
|
|
pub temperature: f32,
|
|
pub top_k: Option<usize>,
|
|
pub top_p: Option<f32>,
|
|
pub repetition_penalty: f32,
|
|
pub presence_penalty: f32,
|
|
pub frequency_penalty: f32,
|
|
pub length_penalty: f32,
|
|
pub diversity_penalty: f32,
|
|
pub typical_p: Option<f32>,
|
|
pub eta_cutoff: Option<f32>,
|
|
pub epsilon_cutoff: Option<f32>,
|
|
pub min_p: Option<f32>,
|
|
}
|
|
|
|
impl Default for SamplingConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
strategy: SamplingStrategy::TopP,
|
|
temperature: 1.0,
|
|
top_k: Some(50),
|
|
top_p: Some(0.95),
|
|
repetition_penalty: 1.1,
|
|
presence_penalty: 0.0,
|
|
frequency_penalty: 0.0,
|
|
length_penalty: 1.0,
|
|
diversity_penalty: 0.0,
|
|
typical_p: None,
|
|
eta_cutoff: None,
|
|
epsilon_cutoff: None,
|
|
min_p: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Available sampling strategies
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum SamplingStrategy {
|
|
Greedy,
|
|
TopK,
|
|
TopP,
|
|
Temperature,
|
|
Typical,
|
|
Combined,
|
|
Adaptive,
|
|
Custom,
|
|
}
|
|
|
|
/// Token with probability and metadata
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct TokenCandidate {
|
|
pub token_id: u32,
|
|
pub token_text: String,
|
|
pub log_prob: f32,
|
|
pub prob: f32,
|
|
pub rank: usize,
|
|
pub is_repeated: bool,
|
|
pub frequency_count: u32,
|
|
pub semantic_score: Option<f32>,
|
|
}
|
|
|
|
impl TokenCandidate {
|
|
/// Create new token candidate
|
|
#[must_use]
|
|
pub fn new(token_id: u32, token_text: String, log_prob: f32) -> Self {
|
|
Self {
|
|
token_id,
|
|
token_text,
|
|
log_prob,
|
|
prob: log_prob.exp(),
|
|
rank: 0,
|
|
is_repeated: false,
|
|
frequency_count: 0,
|
|
semantic_score: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for TokenCandidate {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
// Sort by probability (descending)
|
|
other.prob.partial_cmp(&self.prob)
|
|
}
|
|
}
|
|
|
|
/// Context for repetition and frequency tracking
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct SamplingContext {
|
|
pub generated_tokens: Vec<u32>,
|
|
pub token_frequencies: HashMap<u32, u32>,
|
|
pub recent_ngrams: HashMap<Vec<u32>, u32>,
|
|
pub semantic_embeddings: HashMap<u32, Vec<f32>>,
|
|
pub position: usize,
|
|
}
|
|
|
|
impl SamplingContext {
|
|
/// Add generated token to context
|
|
pub fn add_token(&mut self, token_id: u32) {
|
|
self.generated_tokens.push(token_id);
|
|
*self.token_frequencies.entry(token_id).or_insert(0) += 1;
|
|
self.position += 1;
|
|
|
|
// Update n-grams for repetition detection
|
|
self.update_ngrams(token_id);
|
|
}
|
|
|
|
/// Update n-gram tracking
|
|
fn update_ngrams(&mut self, token_id: u32) {
|
|
let _ = token_id;
|
|
// Track 2-grams, 3-grams, and 4-grams.
|
|
// `generated_tokens` already has the current token appended, so the
|
|
// n-gram is simply the last `n` tokens (no need to append again).
|
|
for n in 2..=4 {
|
|
if self.generated_tokens.len() >= n {
|
|
let ngram = self.generated_tokens[(self.generated_tokens.len() - n)..].to_vec();
|
|
|
|
*self.recent_ngrams.entry(ngram).or_insert(0) += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Check if token would create repetition
|
|
#[must_use]
|
|
pub fn would_repeat(&self, token_id: u32, n: usize) -> bool {
|
|
if self.generated_tokens.len() < n - 1 {
|
|
return false;
|
|
}
|
|
|
|
let potential_ngram = self.generated_tokens[(self.generated_tokens.len() - n + 1)..]
|
|
.iter()
|
|
.copied()
|
|
.chain(std::iter::once(token_id))
|
|
.collect::<Vec<u32>>();
|
|
|
|
self.recent_ngrams
|
|
.get(&potential_ngram)
|
|
.copied()
|
|
.unwrap_or(0)
|
|
> 0
|
|
}
|
|
|
|
/// Get frequency penalty for token
|
|
#[must_use]
|
|
pub fn get_frequency_penalty(&self, token_id: u32, penalty: f32) -> f32 {
|
|
let count = self.token_frequencies.get(&token_id).copied().unwrap_or(0);
|
|
if count > 0 {
|
|
penalty * count as f32
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
/// Get presence penalty for token
|
|
#[must_use]
|
|
pub fn get_presence_penalty(&self, token_id: u32, penalty: f32) -> f32 {
|
|
if self.token_frequencies.contains_key(&token_id) {
|
|
penalty
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Advanced sampler with multiple strategies
|
|
pub struct AdvancedSampler {
|
|
config: SamplingConfig,
|
|
rng: StdRng,
|
|
}
|
|
|
|
impl Default for AdvancedSampler {
|
|
fn default() -> Self {
|
|
Self::new(SamplingConfig::default())
|
|
}
|
|
}
|
|
|
|
impl AdvancedSampler {
|
|
/// Create new sampler with configuration
|
|
#[must_use]
|
|
pub fn new(config: SamplingConfig) -> Self {
|
|
Self {
|
|
config,
|
|
rng: StdRng::from_entropy(),
|
|
}
|
|
}
|
|
|
|
/// Sample next token from logits
|
|
pub fn sample(
|
|
&mut self,
|
|
logits: &[f32],
|
|
token_texts: &[String],
|
|
context: &mut SamplingContext,
|
|
) -> Result<TokenCandidate> {
|
|
if logits.is_empty() {
|
|
return Err(anyhow!("Empty logits"));
|
|
}
|
|
|
|
// Create token candidates
|
|
let mut candidates = self.create_candidates(logits, token_texts, context)?;
|
|
|
|
// Apply sampling strategy
|
|
let selected = match self.config.strategy {
|
|
SamplingStrategy::Greedy => self.greedy_sample(&candidates)?,
|
|
SamplingStrategy::TopK => self.top_k_sample(&mut candidates, context)?,
|
|
SamplingStrategy::TopP => self.nucleus_sample(&mut candidates, context)?,
|
|
SamplingStrategy::Temperature => self.temperature_sample(&mut candidates, context)?,
|
|
SamplingStrategy::Typical => self.typical_sampling(&mut candidates, context)?,
|
|
SamplingStrategy::Combined => self.combined_sampling(&mut candidates, context)?,
|
|
SamplingStrategy::Adaptive => self.adaptive_sampling(&mut candidates, context)?,
|
|
SamplingStrategy::Custom => self.custom_sampling(&mut candidates, context)?,
|
|
};
|
|
|
|
// Update context
|
|
context.add_token(selected.token_id);
|
|
|
|
Ok(selected)
|
|
}
|
|
|
|
/// Create token candidates from logits
|
|
fn create_candidates(
|
|
&self,
|
|
logits: &[f32],
|
|
token_texts: &[String],
|
|
context: &SamplingContext,
|
|
) -> Result<Vec<TokenCandidate>> {
|
|
let mut candidates = Vec::new();
|
|
|
|
for (i, (&logit, token_text)) in logits.iter().zip(token_texts).enumerate() {
|
|
let mut candidate = TokenCandidate::new(i as u32, token_text.clone(), logit);
|
|
|
|
// Apply penalties
|
|
candidate = self.apply_penalties(candidate, context);
|
|
|
|
candidates.push(candidate);
|
|
}
|
|
|
|
// Sort by probability and assign ranks
|
|
candidates.sort_by(|a, b| b.prob.total_cmp(&a.prob));
|
|
for (rank, candidate) in candidates.iter_mut().enumerate() {
|
|
candidate.rank = rank;
|
|
}
|
|
|
|
Ok(candidates)
|
|
}
|
|
|
|
/// Apply various penalties to token candidate
|
|
fn apply_penalties(
|
|
&self,
|
|
mut candidate: TokenCandidate,
|
|
context: &SamplingContext,
|
|
) -> TokenCandidate {
|
|
let mut adjusted_log_prob = candidate.log_prob;
|
|
|
|
// Repetition penalty
|
|
if context.token_frequencies.contains_key(&candidate.token_id) {
|
|
let penalty = if self.config.repetition_penalty > 1.0 {
|
|
-self.config.repetition_penalty.ln()
|
|
} else {
|
|
self.config.repetition_penalty.ln()
|
|
};
|
|
adjusted_log_prob += penalty;
|
|
candidate.is_repeated = true;
|
|
}
|
|
|
|
// Frequency penalty
|
|
let freq_penalty =
|
|
context.get_frequency_penalty(candidate.token_id, self.config.frequency_penalty);
|
|
adjusted_log_prob -= freq_penalty;
|
|
|
|
// Presence penalty
|
|
let presence_penalty =
|
|
context.get_presence_penalty(candidate.token_id, self.config.presence_penalty);
|
|
adjusted_log_prob -= presence_penalty;
|
|
|
|
candidate.log_prob = adjusted_log_prob;
|
|
candidate.prob = adjusted_log_prob.exp();
|
|
candidate.frequency_count = context
|
|
.token_frequencies
|
|
.get(&candidate.token_id)
|
|
.copied()
|
|
.unwrap_or(0);
|
|
|
|
candidate
|
|
}
|
|
|
|
/// Greedy sampling - select highest probability token
|
|
fn greedy_sample(&self, candidates: &[TokenCandidate]) -> Result<TokenCandidate> {
|
|
candidates
|
|
.first()
|
|
.ok_or_else(|| anyhow!("No candidates available"))
|
|
.cloned()
|
|
}
|
|
|
|
/// Top-k sampling
|
|
fn top_k_sample(
|
|
&mut self,
|
|
candidates: &mut [TokenCandidate],
|
|
_context: &SamplingContext,
|
|
) -> Result<TokenCandidate> {
|
|
let k = self.config.top_k.unwrap_or(candidates.len());
|
|
let k = k.min(candidates.len());
|
|
|
|
// Take top-k candidates
|
|
let top_candidates = &candidates[..k];
|
|
|
|
// Apply temperature scaling
|
|
let scaled_probs = self.apply_temperature(top_candidates);
|
|
|
|
// Sample from distribution
|
|
self.sample_from_distribution(&scaled_probs, top_candidates)
|
|
}
|
|
|
|
/// Nucleus (top-p) sampling
|
|
fn nucleus_sample(
|
|
&mut self,
|
|
candidates: &mut [TokenCandidate],
|
|
_context: &SamplingContext,
|
|
) -> Result<TokenCandidate> {
|
|
let p = self.config.top_p.unwrap_or(1.0);
|
|
|
|
// Find nucleus (cumulative probability >= p)
|
|
let mut cumulative_prob = 0.0;
|
|
let mut nucleus_size = 0;
|
|
|
|
for (i, candidate) in candidates.iter().enumerate() {
|
|
cumulative_prob += candidate.prob;
|
|
nucleus_size = i + 1;
|
|
|
|
if cumulative_prob >= p {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let nucleus = &candidates[..nucleus_size];
|
|
|
|
// Apply temperature scaling
|
|
let scaled_probs = self.apply_temperature(nucleus);
|
|
|
|
// Sample from nucleus
|
|
self.sample_from_distribution(&scaled_probs, nucleus)
|
|
}
|
|
|
|
/// Temperature sampling
|
|
fn temperature_sample(
|
|
&mut self,
|
|
candidates: &mut [TokenCandidate],
|
|
_context: &SamplingContext,
|
|
) -> Result<TokenCandidate> {
|
|
let scaled_probs = self.apply_temperature(candidates);
|
|
self.sample_from_distribution(&scaled_probs, candidates)
|
|
}
|
|
|
|
/// Typical sampling (entropy-based)
|
|
fn typical_sampling(
|
|
&mut self,
|
|
candidates: &mut [TokenCandidate],
|
|
_context: &SamplingContext,
|
|
) -> Result<TokenCandidate> {
|
|
let typical_p = self.config.typical_p.unwrap_or(0.95);
|
|
|
|
// Calculate entropy
|
|
let entropy = -candidates.iter().map(|c| c.prob * c.prob.ln()).sum::<f32>();
|
|
|
|
// Filter candidates by typical probability
|
|
let mut typical_candidates = Vec::new();
|
|
for candidate in candidates.iter() {
|
|
let surprisal = -candidate.prob.ln();
|
|
let typical_score = (surprisal - entropy).abs();
|
|
|
|
if typical_score <= typical_p {
|
|
typical_candidates.push((candidate.clone(), typical_score));
|
|
}
|
|
}
|
|
|
|
if typical_candidates.is_empty() {
|
|
return self.greedy_sample(candidates);
|
|
}
|
|
|
|
// Sort by typical score (lower is more typical)
|
|
typical_candidates.sort_by(|a, b| a.1.total_cmp(&b.1));
|
|
|
|
let candidates_only: Vec<_> = typical_candidates.iter().map(|(c, _)| c.clone()).collect();
|
|
let scaled_probs = self.apply_temperature(&candidates_only);
|
|
|
|
self.sample_from_distribution(&scaled_probs, &candidates_only)
|
|
}
|
|
|
|
/// Combined sampling strategy
|
|
fn combined_sampling(
|
|
&mut self,
|
|
candidates: &mut [TokenCandidate],
|
|
_context: &SamplingContext,
|
|
) -> Result<TokenCandidate> {
|
|
// Apply multiple filters in sequence
|
|
|
|
// 1. Top-k filtering
|
|
let k = self.config.top_k.unwrap_or(candidates.len());
|
|
let k = k.min(candidates.len());
|
|
let mut filtered = candidates[..k].to_vec();
|
|
|
|
// 2. Top-p filtering
|
|
if let Some(p) = self.config.top_p {
|
|
let mut cumulative_prob = 0.0;
|
|
let mut nucleus_size = 0;
|
|
|
|
for (i, candidate) in filtered.iter().enumerate() {
|
|
cumulative_prob += candidate.prob;
|
|
nucleus_size = i + 1;
|
|
|
|
if cumulative_prob >= p {
|
|
break;
|
|
}
|
|
}
|
|
|
|
filtered.truncate(nucleus_size);
|
|
}
|
|
|
|
// 3. Min-p filtering
|
|
if let Some(min_p) = self.config.min_p {
|
|
let max_prob = filtered.first().map_or(0.0, |c| c.prob);
|
|
let threshold = min_p * max_prob;
|
|
filtered.retain(|c| c.prob >= threshold);
|
|
}
|
|
|
|
if filtered.is_empty() {
|
|
return self.greedy_sample(candidates);
|
|
}
|
|
|
|
// Apply temperature and sample
|
|
let scaled_probs = self.apply_temperature(&filtered);
|
|
self.sample_from_distribution(&scaled_probs, &filtered)
|
|
}
|
|
|
|
/// Adaptive sampling based on context
|
|
fn adaptive_sampling(
|
|
&mut self,
|
|
candidates: &mut [TokenCandidate],
|
|
context: &SamplingContext,
|
|
) -> Result<TokenCandidate> {
|
|
// Adapt parameters based on generation context
|
|
let mut adaptive_config = self.config.clone();
|
|
|
|
// Reduce temperature as sequence gets longer (more conservative)
|
|
let length_factor = 1.0 - (context.position as f32 / 1000.0).min(0.5);
|
|
adaptive_config.temperature *= length_factor;
|
|
|
|
// Increase repetition penalty if repetition detected
|
|
let repetition_rate = context
|
|
.recent_ngrams
|
|
.values()
|
|
.filter(|&&count| count > 1)
|
|
.count() as f32
|
|
/ context.recent_ngrams.len().max(1) as f32;
|
|
if repetition_rate > 0.3 {
|
|
adaptive_config.repetition_penalty *= 1.5;
|
|
}
|
|
|
|
// Temporarily update config
|
|
let original_config = self.config.clone();
|
|
self.config = adaptive_config;
|
|
|
|
let result = self.combined_sampling(candidates, context);
|
|
|
|
// Restore original config
|
|
self.config = original_config;
|
|
|
|
result
|
|
}
|
|
|
|
/// Custom sampling strategy (placeholder for extensibility)
|
|
fn custom_sampling(
|
|
&mut self,
|
|
candidates: &mut [TokenCandidate],
|
|
context: &SamplingContext,
|
|
) -> Result<TokenCandidate> {
|
|
// Default to combined sampling for now
|
|
// This can be extended to support custom algorithms
|
|
self.combined_sampling(candidates, context)
|
|
}
|
|
|
|
/// Apply temperature scaling to probabilities
|
|
fn apply_temperature(&self, candidates: &[TokenCandidate]) -> Vec<f32> {
|
|
let temperature = self.config.temperature.max(0.01); // Prevent division by zero
|
|
|
|
let scaled_logits: Vec<f32> = candidates
|
|
.iter()
|
|
.map(|c| c.log_prob / temperature)
|
|
.collect();
|
|
|
|
// Compute softmax
|
|
let max_logit = scaled_logits
|
|
.iter()
|
|
.fold(f32::NEG_INFINITY, |a, &b| a.max(b));
|
|
let exp_logits: Vec<f32> = scaled_logits
|
|
.iter()
|
|
.map(|&logit| (logit - max_logit).exp())
|
|
.collect();
|
|
|
|
let sum_exp: f32 = exp_logits.iter().sum();
|
|
|
|
exp_logits
|
|
.iter()
|
|
.map(|&exp_logit| exp_logit / sum_exp)
|
|
.collect()
|
|
}
|
|
|
|
/// Sample from probability distribution
|
|
fn sample_from_distribution(
|
|
&mut self,
|
|
probabilities: &[f32],
|
|
candidates: &[TokenCandidate],
|
|
) -> Result<TokenCandidate> {
|
|
if probabilities.is_empty() || candidates.is_empty() {
|
|
return Err(anyhow!("Empty probabilities or candidates"));
|
|
}
|
|
|
|
// Create weighted distribution
|
|
let dist = WeightedIndex::new(probabilities)
|
|
.map_err(|e| anyhow!("Failed to create weighted distribution: {e}"))?;
|
|
|
|
let index = dist.sample(&mut self.rng);
|
|
|
|
Ok(candidates[index].clone())
|
|
}
|
|
|
|
/// Update sampling configuration
|
|
pub fn update_config(&mut self, config: SamplingConfig) {
|
|
self.config = config;
|
|
}
|
|
|
|
/// Get current configuration
|
|
#[must_use]
|
|
pub fn get_config(&self) -> &SamplingConfig {
|
|
&self.config
|
|
}
|
|
}
|
|
|
|
/// Batch sampler for efficient parallel sampling
|
|
pub struct BatchSampler {
|
|
samplers: Vec<AdvancedSampler>,
|
|
}
|
|
|
|
impl BatchSampler {
|
|
/// Create new batch sampler
|
|
#[must_use]
|
|
pub fn new(batch_size: usize, config: SamplingConfig) -> Self {
|
|
let samplers = (0..batch_size)
|
|
.map(|_| AdvancedSampler::new(config.clone()))
|
|
.collect();
|
|
|
|
Self { samplers }
|
|
}
|
|
|
|
/// Sample from multiple sequences in parallel
|
|
pub fn batch_sample(
|
|
&mut self,
|
|
batch_logits: &[Vec<f32>],
|
|
batch_token_texts: &[Vec<String>],
|
|
batch_contexts: &mut [SamplingContext],
|
|
) -> Result<Vec<TokenCandidate>> {
|
|
if batch_logits.len() != batch_contexts.len()
|
|
|| batch_logits.len() != batch_token_texts.len()
|
|
|| batch_logits.len() != self.samplers.len()
|
|
{
|
|
return Err(anyhow!("Batch size mismatch"));
|
|
}
|
|
|
|
let mut results = Vec::new();
|
|
|
|
for (i, ((logits, token_texts), context)) in batch_logits
|
|
.iter()
|
|
.zip(batch_token_texts)
|
|
.zip(batch_contexts.iter_mut())
|
|
.enumerate()
|
|
{
|
|
let sampled = self.samplers[i].sample(logits, token_texts, context)?;
|
|
results.push(sampled);
|
|
}
|
|
|
|
Ok(results)
|
|
}
|
|
}
|
|
|
|
/// Sampling statistics for analysis and tuning
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SamplingStats {
|
|
pub total_samples: u64,
|
|
pub strategy_usage: HashMap<SamplingStrategy, u64>,
|
|
pub average_entropy: f32,
|
|
pub repetition_rate: f32,
|
|
pub diversity_score: f32,
|
|
pub temperature_usage: Vec<f32>,
|
|
pub top_k_usage: Vec<usize>,
|
|
pub top_p_usage: Vec<f32>,
|
|
}
|
|
|
|
impl Default for SamplingStats {
|
|
fn default() -> Self {
|
|
Self {
|
|
total_samples: 0,
|
|
strategy_usage: HashMap::new(),
|
|
average_entropy: 0.0,
|
|
repetition_rate: 0.0,
|
|
diversity_score: 0.0,
|
|
temperature_usage: Vec::new(),
|
|
top_k_usage: Vec::new(),
|
|
top_p_usage: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_token_candidate_creation() {
|
|
let candidate = TokenCandidate::new(1, "hello".to_string(), 0.5);
|
|
assert_eq!(candidate.token_id, 1);
|
|
assert_eq!(candidate.token_text, "hello");
|
|
assert_eq!(candidate.log_prob, 0.5);
|
|
assert!((candidate.prob - 0.5_f32.exp()).abs() < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sampling_context() {
|
|
let mut context = SamplingContext::default();
|
|
|
|
context.add_token(1);
|
|
context.add_token(2);
|
|
context.add_token(1); // Repeat
|
|
|
|
assert_eq!(context.generated_tokens, vec![1, 2, 1]);
|
|
assert_eq!(context.token_frequencies[&1], 2);
|
|
assert_eq!(context.token_frequencies[&2], 1);
|
|
|
|
// Test repetition detection
|
|
assert!(context.would_repeat(2, 2)); // Would create [2, 1, 2] bigram
|
|
}
|
|
|
|
#[test]
|
|
fn test_sampling_config_default() {
|
|
let config = SamplingConfig::default();
|
|
assert_eq!(config.strategy, SamplingStrategy::TopP);
|
|
assert_eq!(config.temperature, 1.0);
|
|
assert_eq!(config.repetition_penalty, 1.1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_advanced_sampler_creation() {
|
|
let config = SamplingConfig::default();
|
|
let sampler = AdvancedSampler::new(config.clone());
|
|
assert_eq!(*sampler.get_config(), config);
|
|
}
|
|
|
|
#[test]
|
|
fn test_greedy_sampling() {
|
|
let mut sampler = AdvancedSampler::new(SamplingConfig {
|
|
strategy: SamplingStrategy::Greedy,
|
|
..Default::default()
|
|
});
|
|
|
|
let logits = vec![0.1, 0.8, 0.3]; // Middle token has highest probability
|
|
let token_texts = vec!["a".to_string(), "b".to_string(), "c".to_string()];
|
|
let mut context = SamplingContext::default();
|
|
|
|
let result = sampler.sample(&logits, &token_texts, &mut context).unwrap();
|
|
assert_eq!(result.token_id, 1); // Highest probability token
|
|
assert_eq!(result.token_text, "b");
|
|
}
|
|
|
|
#[test]
|
|
fn test_temperature_scaling() {
|
|
let sampler = AdvancedSampler::new(SamplingConfig {
|
|
temperature: 2.0,
|
|
..Default::default()
|
|
});
|
|
|
|
let candidates = vec![
|
|
TokenCandidate::new(0, "a".to_string(), 0.8),
|
|
TokenCandidate::new(1, "b".to_string(), 0.2),
|
|
];
|
|
|
|
let scaled_probs = sampler.apply_temperature(&candidates);
|
|
|
|
// Baseline softmax with no temperature scaling (temperature = 1.0),
|
|
// to compare against the effect of the higher temperature above.
|
|
let baseline_sampler = AdvancedSampler::new(SamplingConfig {
|
|
temperature: 1.0,
|
|
..Default::default()
|
|
});
|
|
let baseline_probs = baseline_sampler.apply_temperature(&candidates);
|
|
|
|
// With higher temperature, probabilities should be more uniform:
|
|
// the top candidate's probability shrinks and the runner-up's grows.
|
|
assert!(scaled_probs[0] < baseline_probs[0]);
|
|
assert!(scaled_probs[1] > baseline_probs[1]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_sampler() {
|
|
let config = SamplingConfig::default();
|
|
let mut batch_sampler = BatchSampler::new(2, config);
|
|
|
|
let batch_logits = vec![vec![0.1, 0.8, 0.3], vec![0.5, 0.2, 0.7]];
|
|
|
|
let batch_token_texts = vec![
|
|
vec!["a".to_string(), "b".to_string(), "c".to_string()],
|
|
vec!["x".to_string(), "y".to_string(), "z".to_string()],
|
|
];
|
|
|
|
let mut batch_contexts = vec![SamplingContext::default(), SamplingContext::default()];
|
|
|
|
let results = batch_sampler
|
|
.batch_sample(&batch_logits, &batch_token_texts, &mut batch_contexts)
|
|
.unwrap();
|
|
|
|
assert_eq!(results.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_penalty_application() {
|
|
let sampler = AdvancedSampler::new(SamplingConfig {
|
|
repetition_penalty: 2.0,
|
|
frequency_penalty: 0.5,
|
|
presence_penalty: 0.3,
|
|
..Default::default()
|
|
});
|
|
|
|
let mut context = SamplingContext::default();
|
|
context.add_token(1); // Add token to create repetition
|
|
|
|
let candidate = TokenCandidate::new(1, "test".to_string(), 0.5);
|
|
let penalized = sampler.apply_penalties(candidate, &context);
|
|
|
|
// Should have lower probability due to penalties
|
|
assert!(penalized.prob < 0.5_f32.exp());
|
|
assert!(penalized.is_repeated);
|
|
}
|
|
|
|
#[test]
|
|
fn test_nucleus_sampling_nucleus_selection() {
|
|
// Test that nucleus sampling correctly selects the nucleus
|
|
let candidates = vec![
|
|
TokenCandidate {
|
|
prob: 0.6,
|
|
..TokenCandidate::new(0, "a".to_string(), 0.6_f32.ln())
|
|
},
|
|
TokenCandidate {
|
|
prob: 0.3,
|
|
..TokenCandidate::new(1, "b".to_string(), 0.3_f32.ln())
|
|
},
|
|
TokenCandidate {
|
|
prob: 0.08,
|
|
..TokenCandidate::new(2, "c".to_string(), 0.08_f32.ln())
|
|
},
|
|
TokenCandidate {
|
|
prob: 0.02,
|
|
..TokenCandidate::new(3, "d".to_string(), 0.02_f32.ln())
|
|
},
|
|
];
|
|
|
|
// With p=0.9, nucleus should include first 3 tokens (0.6 + 0.3 + 0.08 = 0.98 > 0.9)
|
|
let p = 0.9;
|
|
let mut cumulative_prob = 0.0;
|
|
let mut nucleus_size = 0;
|
|
|
|
for (i, candidate) in candidates.iter().enumerate() {
|
|
cumulative_prob += candidate.prob;
|
|
nucleus_size = i + 1;
|
|
|
|
// Use a small epsilon so that floating-point rounding in the
|
|
// cumulative sum doesn't cause the nucleus to be cut off one
|
|
// token earlier than the intended threshold crossing.
|
|
if cumulative_prob >= p + 1e-6 {
|
|
break;
|
|
}
|
|
}
|
|
|
|
assert_eq!(nucleus_size, 3);
|
|
}
|
|
}
|