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