Skip to main content

ferrum_sampler/
lib.rs

1//! # Ferrum Sampler
2//!
3//! MVP sampler implementation for Ferrum inference stack.
4//!
5//! This crate provides a thin wrapper around the sampling interfaces defined in
6//! `ferrum-interfaces`, offering convenient factory functions and utilities for
7//! building sampling pipelines from `SamplingParams`.
8//!
9//! ## Design
10//!
11//! - **Re-export Interface Types**: All core types from `ferrum-interfaces::sampler`
12//! - **Factory Pattern**: Simple factory for creating samplers and configs
13//! - **Zero Overhead**: Direct delegation to interface implementations
14//!
15//! ## Usage
16//!
17//! ```no_run
18//! use ferrum_sampler::{build_sampling_config, sampler_from_params};
19//! use ferrum_types::SamplingParams;
20//!
21//! let params = SamplingParams::default();
22//! let config = build_sampling_config(&params);
23//! let sampler = sampler_from_params(&params);
24//! ```
25
26pub mod guided;
27pub mod json_mode;
28pub mod schema_to_regex;
29
30// Re-export all sampler types from ferrum-interfaces
31pub use ferrum_interfaces::sampler::{
32    GreedySampler, LogitsProcessor, LogitsProcessorChain, MultiSampler, MultinomialSampler,
33    ProcessorPriority, RepetitionPenaltyProcessor, Sampler, SamplingConfig, SamplingConfigBuilder,
34    SamplingContext, SamplingStats, TemperatureProcessor, TopKProcessor, TopPProcessor,
35};
36
37// Re-export types from ferrum-types
38pub use ferrum_types::{Result, SamplingParams, TokenId};
39
40use rand::RngCore;
41use std::collections::HashMap;
42
43/// Default sampler factory for creating samplers and configurations.
44#[derive(Debug, Clone, Default)]
45pub struct DefaultSamplerFactory;
46
47impl DefaultSamplerFactory {
48    /// Create new factory instance
49    pub fn new() -> Self {
50        Self
51    }
52
53    /// Build sampling configuration from parameters
54    pub fn build_config(&self, params: &SamplingParams) -> SamplingConfig {
55        SamplingConfig::from_params(params)
56    }
57
58    /// Create sampler instance based on temperature
59    /// - temperature == 0.0 → GreedySampler (deterministic)
60    /// - temperature > 0.0 → MultinomialSampler (stochastic)
61    pub fn create_sampler(&self, params: &SamplingParams) -> Box<dyn Sampler + Send + Sync> {
62        if params.temperature == 0.0 {
63            Box::new(GreedySampler)
64        } else {
65            Box::new(MultinomialSampler)
66        }
67    }
68
69    /// Create sampling pipeline with config and sampler
70    pub fn build_pipeline(&self, params: &SamplingParams) -> SamplingPipeline {
71        let config = self.build_config(params);
72        SamplingPipeline { config }
73    }
74}
75
76/// Sampling pipeline that combines config and execution logic.
77///
78/// This struct holds a `SamplingConfig` and provides a convenient interface
79/// for sampling tokens with context.
80pub struct SamplingPipeline {
81    config: SamplingConfig,
82}
83
84impl SamplingPipeline {
85    /// Create new pipeline from parameters
86    pub fn new(params: &SamplingParams) -> Self {
87        let config = SamplingConfig::from_params(params);
88        Self { config }
89    }
90
91    /// Get reference to sampling config
92    pub fn config(&self) -> &SamplingConfig {
93        &self.config
94    }
95
96    /// Sample next token with full context
97    ///
98    /// # Arguments
99    /// * `step` - Current generation step (0-based)
100    /// * `logits` - Mutable logits array to process
101    /// * `previous_tokens` - Previously generated tokens
102    /// * `token_frequencies` - Token frequency map for penalties
103    /// * `sampling_params` - Sampling parameters for this step
104    /// * `rng` - Random number generator
105    pub fn sample_next(
106        &self,
107        step: usize,
108        logits: &mut [f32],
109        previous_tokens: &[TokenId],
110        token_frequencies: &HashMap<TokenId, usize>,
111        sampling_params: &SamplingParams,
112        rng: &mut dyn RngCore,
113    ) -> Result<TokenId> {
114        let vocab_size = logits.len();
115        let ctx = SamplingContext::new(
116            step,
117            sampling_params,
118            logits,
119            previous_tokens,
120            token_frequencies,
121            vocab_size,
122        );
123        self.config.sample(ctx, rng)
124    }
125
126    /// Simple sampling without context (uses default params)
127    pub fn sample_simple(&self, logits: &mut [f32], rng: &mut dyn RngCore) -> Result<TokenId> {
128        let params = SamplingParams::default();
129        let empty_tokens = Vec::new();
130        let empty_freqs = HashMap::new();
131        self.sample_next(0, logits, &empty_tokens, &empty_freqs, &params, rng)
132    }
133}
134
135// ============================================================================
136// Convenience Functions
137// ============================================================================
138
139/// Build sampling configuration from parameters.
140///
141/// This is the primary entry point for creating a `SamplingConfig`.
142pub fn build_sampling_config(params: &SamplingParams) -> SamplingConfig {
143    SamplingConfig::from_params(params)
144}
145
146/// Create sampler instance from parameters.
147///
148/// Returns a boxed `Sampler` trait object based on the temperature setting.
149pub fn sampler_from_params(params: &SamplingParams) -> Box<dyn Sampler + Send + Sync> {
150    DefaultSamplerFactory::new().create_sampler(params)
151}
152
153/// Build complete sampling pipeline from parameters.
154pub fn pipeline_from_params(params: &SamplingParams) -> SamplingPipeline {
155    DefaultSamplerFactory::new().build_pipeline(params)
156}
157
158/// Create a greedy sampler (always picks highest logit).
159pub fn greedy_sampler() -> Box<dyn Sampler + Send + Sync> {
160    Box::new(GreedySampler)
161}
162
163/// Create a multinomial sampler (probabilistic sampling).
164pub fn multinomial_sampler() -> Box<dyn Sampler + Send + Sync> {
165    Box::new(MultinomialSampler)
166}
167
168// ============================================================================
169// Tests
170// ============================================================================
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use rand::rngs::StdRng;
176    use rand::SeedableRng;
177
178    #[test]
179    fn test_factory_creates_greedy_for_zero_temp() {
180        let factory = DefaultSamplerFactory::new();
181        let params = SamplingParams {
182            temperature: 0.0,
183            ..Default::default()
184        };
185        let sampler = factory.create_sampler(&params);
186        assert!(sampler.is_deterministic());
187    }
188
189    #[test]
190    fn test_factory_creates_multinomial_for_nonzero_temp() {
191        let factory = DefaultSamplerFactory::new();
192        let params = SamplingParams {
193            temperature: 1.0,
194            ..Default::default()
195        };
196        let sampler = factory.create_sampler(&params);
197        assert!(!sampler.is_deterministic());
198    }
199
200    #[test]
201    fn test_build_sampling_config() {
202        let params = SamplingParams {
203            temperature: 0.8,
204            top_k: Some(50),
205            top_p: 0.95,
206            repetition_penalty: 1.1,
207            ..Default::default()
208        };
209        let config = build_sampling_config(&params);
210        // Config should be created successfully
211        // Should have: temperature, top_k, top_p, repetition_penalty processors
212        assert_eq!(config.processor_chain.processor_names().len(), 4);
213    }
214
215    #[test]
216    fn test_pipeline_sample_simple() {
217        let params = SamplingParams::greedy();
218        let pipeline = pipeline_from_params(&params);
219        let mut rng = StdRng::seed_from_u64(42);
220
221        let mut logits = vec![1.0, 5.0, 2.0, 0.5];
222        let token = pipeline.sample_simple(&mut logits, &mut rng).unwrap();
223
224        // Should select index 1 (highest logit)
225        assert_eq!(token.get(), 1);
226    }
227
228    #[test]
229    fn test_greedy_sampler_deterministic() {
230        let sampler = greedy_sampler();
231        assert!(sampler.is_deterministic());
232        assert_eq!(sampler.name(), "greedy");
233    }
234
235    #[test]
236    fn test_multinomial_sampler_stochastic() {
237        let sampler = multinomial_sampler();
238        assert!(!sampler.is_deterministic());
239        assert_eq!(sampler.name(), "multinomial");
240    }
241
242    #[test]
243    fn test_pipeline_with_context() {
244        let params = SamplingParams {
245            temperature: 1.0,
246            repetition_penalty: 1.2,
247            ..Default::default()
248        };
249        let pipeline = SamplingPipeline::new(&params);
250        let mut rng = StdRng::seed_from_u64(42);
251
252        let mut logits = vec![1.0, 2.0, 3.0, 2.0];
253        let previous_tokens = vec![TokenId::new(2)]; // Token 2 was generated before
254        let mut freqs = HashMap::new();
255        freqs.insert(TokenId::new(2), 1);
256
257        let token = pipeline
258            .sample_next(0, &mut logits, &previous_tokens, &freqs, &params, &mut rng)
259            .unwrap();
260
261        // Token should be valid
262        assert!(token.get() < 4);
263    }
264}