Skip to main content

cherenkov/
sampling.rs

1//! Request-owned sampling state. GPU logits are borrowed after synchronization.
2
3use anyhow::{Result, ensure};
4use clap::Args;
5use rand::{Rng, SeedableRng, rngs::StdRng};
6use serde::{Deserialize, Serialize};
7
8#[derive(Args, Clone, Debug, PartialEq, Serialize, Deserialize)]
9#[serde(default, deny_unknown_fields)]
10pub struct Sampling {
11    /// Sampling temperature; 0 keeps greedy decoding
12    #[arg(long, default_value_t = 0.0)]
13    pub temperature: f64,
14    /// Nucleus probability, in (0, 1]
15    #[arg(long, default_value_t = 1.0)]
16    pub top_p: f64,
17    /// Keep the highest K logits; 0 keeps the whole vocabulary
18    #[arg(long, default_value_t = 0)]
19    pub top_k: usize,
20    /// Seed for this request's random stream
21    #[arg(long)]
22    pub seed: Option<u64>,
23    /// Penalty applied once to tokens present in the prompt or output
24    #[arg(long, default_value_t = 0.0, allow_hyphen_values = true)]
25    pub presence_penalty: f64,
26    /// Penalty multiplied by each token's occurrence count
27    #[arg(long, default_value_t = 0.0, allow_hyphen_values = true)]
28    pub frequency_penalty: f64,
29}
30
31impl Default for Sampling {
32    fn default() -> Self {
33        Self {
34            temperature: 0.0,
35            top_p: 1.0,
36            top_k: 0,
37            seed: None,
38            presence_penalty: 0.0,
39            frequency_penalty: 0.0,
40        }
41    }
42}
43
44impl Sampling {
45    pub fn validate(&self) -> Result<()> {
46        ensure!(
47            self.temperature.is_finite() && (0.0..=2.0).contains(&self.temperature),
48            "temperature must be finite and in 0..2"
49        );
50        ensure!(
51            self.top_p.is_finite() && self.top_p > 0.0 && self.top_p <= 1.0,
52            "top_p must be finite and in (0, 1]"
53        );
54        ensure!(self.top_k <= 1_000_000, "top_k must be 0..1000000");
55
56        for (name, value) in [
57            ("presence_penalty", self.presence_penalty),
58            ("frequency_penalty", self.frequency_penalty),
59        ] {
60            ensure!(
61                value.is_finite() && (-2.0..=2.0).contains(&value),
62                "{name} must be finite and in -2..2"
63            );
64        }
65
66        Ok(())
67    }
68
69    /// Only the unmodified greedy distribution can use argmax MTP verification.
70    pub fn greedy(&self) -> bool {
71        self.temperature == 0.0 && self.presence_penalty == 0.0 && self.frequency_penalty == 0.0
72    }
73
74    pub(crate) fn from_request(value: &serde_json::Value, defaults: &Self) -> Result<Self> {
75        let mut fields = serde_json::to_value(defaults)?;
76
77        for key in [
78            "temperature",
79            "top_p",
80            "top_k",
81            "seed",
82            "presence_penalty",
83            "frequency_penalty",
84        ] {
85            if let Some(v) = value.get(key).filter(|v| !v.is_null()) {
86                fields[key] = v.clone();
87            }
88        }
89
90        let result: Self = serde_json::from_value(fields)?;
91
92        result.validate()?;
93
94        Ok(result)
95    }
96}
97
98pub(crate) struct Sampler {
99    options: Sampling,
100    rng: StdRng,
101    counts: Vec<u32>,
102    candidates: Vec<(u32, f64)>,
103}
104
105impl Sampler {
106    pub(crate) fn set_rng(&mut self, rng: StdRng) {
107        self.rng = rng;
108    }
109
110    pub(crate) fn rng(&self) -> StdRng {
111        self.rng.clone()
112    }
113
114    pub(crate) fn new(options: &Sampling, prompt: &[u32], vocab: usize) -> Result<Self> {
115        options.validate()?;
116
117        let mut sampler = Self {
118            options: options.clone(),
119            rng: options
120                .seed
121                .map(StdRng::seed_from_u64)
122                .unwrap_or_else(StdRng::from_os_rng),
123            counts: vec![0; vocab],
124            candidates: Vec::with_capacity(vocab),
125        };
126
127        for &token in prompt {
128            sampler.accept(token)?;
129        }
130
131        Ok(sampler)
132    }
133
134    pub(crate) fn accept(&mut self, token: u32) -> Result<()> {
135        let count = self
136            .counts
137            .get_mut(token as usize)
138            .ok_or_else(|| anyhow::anyhow!("token exceeds vocabulary"))?;
139        *count = count.saturating_add(1);
140
141        Ok(())
142    }
143
144    pub(crate) fn sample(&mut self, logits: &[f32]) -> Result<u32> {
145        ensure!(
146            logits.len() == self.counts.len(),
147            "logit vocabulary mismatch"
148        );
149        self.candidates.clear();
150
151        for (id, (&logit, &count)) in logits.iter().zip(&self.counts).enumerate() {
152            ensure!(
153                !logit.is_nan() && logit != f32::INFINITY,
154                "non-finite model logits"
155            );
156
157            let penalty = self.options.frequency_penalty * f64::from(count)
158                + if count > 0 {
159                    self.options.presence_penalty
160                } else {
161                    0.0
162                };
163
164            self.candidates
165                .push((id as u32, f64::from(logit) - penalty));
166        }
167
168        let best = self
169            .candidates
170            .iter()
171            .min_by(|a, b| rank(a, b))
172            .copied()
173            .ok_or_else(|| anyhow::anyhow!("empty vocabulary"))?;
174
175        ensure!(best.1.is_finite(), "no finite candidate logits");
176
177        if self.options.temperature == 0.0 {
178            return Ok(best.0);
179        }
180
181        let k = self.options.top_k;
182
183        if k > 0 && k < self.candidates.len() {
184            self.candidates.select_nth_unstable_by(k, rank);
185            self.candidates.truncate(k);
186        }
187
188        if self.options.top_p < 1.0 {
189            self.candidates.sort_unstable_by(rank);
190        }
191
192        let mut total = 0.0;
193
194        for (_, weight) in &mut self.candidates {
195            *weight = ((*weight - best.1) / self.options.temperature).exp();
196            total += *weight;
197        }
198
199        let (keep, mass) = nucleus(&self.candidates, total * self.options.top_p);
200        let draw = self.rng.random::<f64>() * mass;
201        let mut cumulative = 0.0;
202
203        for &(token, weight) in &self.candidates[..keep] {
204            cumulative += weight;
205
206            if draw < cumulative {
207                return Ok(token);
208            }
209        }
210
211        Ok(self.candidates[keep - 1].0)
212    }
213}
214
215fn rank(a: &(u32, f64), b: &(u32, f64)) -> std::cmp::Ordering {
216    b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0))
217}
218
219fn nucleus(candidates: &[(u32, f64)], threshold: f64) -> (usize, f64) {
220    let mut mass = 0.0;
221
222    for (i, &(_, weight)) in candidates.iter().enumerate() {
223        mass += weight;
224
225        if mass >= threshold {
226            return (i + 1, mass);
227        }
228    }
229
230    (candidates.len(), mass)
231}
232
233#[cfg(test)]
234#[path = "../tests/unit/sampling.rs"]
235mod tests;