1use std::collections::HashMap;
2use std::ffi::CString;
3use std::fmt;
4use std::mem::{align_of, size_of};
5use std::os::raw::{c_char, c_void};
6use std::panic::{catch_unwind, AssertUnwindSafe};
7use std::ptr;
8use std::slice;
9use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
10use std::sync::{Arc, Mutex, OnceLock, Weak};
11use std::thread::{self, ThreadId};
12
13use vllm_cpp_sys as ffi;
14
15use crate::error::{invalid_configuration, Error};
16
17const NATIVE_DEFAULT_MAX_TOKENS: u32 = 16;
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
27pub enum SchedulerPolicy {
28 #[default]
30 Fcfs,
31 Priority,
36
37 LongestPrefixMatch,
39}
40
41impl SchedulerPolicy {
42 pub(crate) const fn as_str(self) -> &'static str {
43 match self {
44 Self::Fcfs => "fcfs",
45 Self::Priority => "priority",
46 Self::LongestPrefixMatch => "lpm",
47 }
48 }
49}
50
51#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
53pub enum Toggle {
54 #[default]
56 Default,
57 On,
59 Off,
61}
62
63impl Toggle {
64 pub(crate) const fn as_native(self) -> i32 {
65 match self {
66 Self::Default => 0,
67 Self::On => 1,
68 Self::Off => 2,
69 }
70 }
71}
72
73type DynLogitsProcessor = dyn Fn(&[i32], &mut [f32]) + Send + Sync + 'static;
74
75#[derive(Clone)]
76struct LogitsProcessor {
77 callback: Arc<DynLogitsProcessor>,
78}
79
80impl fmt::Debug for LogitsProcessor {
81 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82 formatter.write_str("LogitsProcessor { .. }")
83 }
84}
85
86impl PartialEq for LogitsProcessor {
87 fn eq(&self, other: &Self) -> bool {
88 Arc::ptr_eq(&self.callback, &other.callback)
89 }
90}
91
92#[derive(Clone, Debug, Eq, PartialEq)]
94pub enum StructuredOutput {
95 JsonSchema(String),
96 Regex(String),
97 Choice(Vec<String>),
98 Grammar(String),
99 JsonObject,
100}
101
102#[derive(Clone, Debug, PartialEq)]
104pub struct SamplingParams {
105 temperature: f32,
106 top_p: f32,
107 top_k: i32,
108 min_p: f32,
109 max_tokens: Option<u32>,
110 seed: Option<u64>,
111 presence_penalty: f32,
112 frequency_penalty: f32,
113 repetition_penalty: f32,
114 min_tokens: u32,
115 ignore_eos: bool,
116 stop: Vec<String>,
117 structured_output: Option<StructuredOutput>,
118 logits_processor: Option<LogitsProcessor>,
119}
120
121impl Default for SamplingParams {
122 fn default() -> Self {
123 Self {
124 temperature: 1.0,
125 top_p: 1.0,
126 top_k: 0,
127 min_p: 0.0,
128 max_tokens: Some(NATIVE_DEFAULT_MAX_TOKENS),
129 seed: None,
130 presence_penalty: 0.0,
131 frequency_penalty: 0.0,
132 repetition_penalty: 1.0,
133 min_tokens: 0,
134 ignore_eos: false,
135 stop: Vec::new(),
136 structured_output: None,
137 logits_processor: None,
138 }
139 }
140}
141
142impl SamplingParams {
143 #[must_use]
145 pub fn greedy() -> Self {
146 Self::default().temperature(0.0)
147 }
148
149 #[must_use]
150 pub fn temperature(mut self, value: f32) -> Self {
151 self.temperature = value;
152 self
153 }
154
155 #[must_use]
156 pub fn top_p(mut self, value: f32) -> Self {
157 self.top_p = value;
158 self
159 }
160
161 #[must_use]
162 pub fn top_k(mut self, value: i32) -> Self {
163 self.top_k = value;
164 self
165 }
166
167 #[must_use]
168 pub fn min_p(mut self, value: f32) -> Self {
169 self.min_p = value;
170 self
171 }
172
173 #[must_use]
178 pub fn max_tokens(mut self, value: u32) -> Self {
179 self.max_tokens = Some(value);
180 self
181 }
182
183 #[must_use]
185 pub fn unbounded(mut self) -> Self {
186 self.max_tokens = None;
187 self
188 }
189
190 #[must_use]
191 pub fn seed(mut self, value: u64) -> Self {
192 self.seed = Some(value);
193 self
194 }
195
196 #[must_use]
197 pub fn clear_seed(mut self) -> Self {
198 self.seed = None;
199 self
200 }
201
202 #[must_use]
203 pub fn presence_penalty(mut self, value: f32) -> Self {
204 self.presence_penalty = value;
205 self
206 }
207
208 #[must_use]
209 pub fn frequency_penalty(mut self, value: f32) -> Self {
210 self.frequency_penalty = value;
211 self
212 }
213
214 #[must_use]
215 pub fn repetition_penalty(mut self, value: f32) -> Self {
216 self.repetition_penalty = value;
217 self
218 }
219
220 #[must_use]
221 pub fn min_tokens(mut self, value: u32) -> Self {
222 self.min_tokens = value;
223 self
224 }
225
226 #[must_use]
227 pub fn ignore_eos(mut self, value: bool) -> Self {
228 self.ignore_eos = value;
229 self
230 }
231
232 #[must_use]
233 pub fn stop(mut self, value: impl Into<String>) -> Self {
234 self.stop.push(value.into());
235 self
236 }
237
238 #[must_use]
239 pub fn stop_all<I, S>(mut self, values: I) -> Self
240 where
241 I: IntoIterator<Item = S>,
242 S: Into<String>,
243 {
244 self.stop.extend(values.into_iter().map(Into::into));
245 self
246 }
247
248 #[must_use]
249 pub fn structured_output(mut self, value: StructuredOutput) -> Self {
250 self.structured_output = Some(value);
251 self
252 }
253
254 #[must_use]
264 pub fn logits_processor<F>(mut self, processor: F) -> Self
265 where
266 F: Fn(&[i32], &mut [f32]) + Send + Sync + 'static,
267 {
268 self.logits_processor = Some(LogitsProcessor {
269 callback: Arc::new(processor),
270 });
271 self
272 }
273
274 #[must_use]
276 pub fn clear_logits_processor(mut self) -> Self {
277 self.logits_processor = None;
278 self
279 }
280
281 pub(crate) fn marshal(&self) -> Result<MarshaledSamplingParams, Error> {
282 MarshaledSamplingParams::new(self)
283 }
284}
285
286pub(crate) struct MarshaledSamplingParams {
287 raw: ffi::vllm_sampling_params,
288 _stop: Vec<CString>,
289 _stop_pointers: Vec<*const c_char>,
290 _structured_string: Option<CString>,
291 _choices: Vec<CString>,
292 _choice_pointers: Vec<*const c_char>,
293 logits_processor: Option<LogitsProcessorRegistration>,
294}
295
296impl MarshaledSamplingParams {
297 fn new(params: &SamplingParams) -> Result<Self, Error> {
298 let mut raw = unsafe { ffi::vllm_sampling_params_default() };
300 raw.temperature = params.temperature;
301 raw.top_p = params.top_p;
302 raw.top_k = params.top_k;
303 raw.min_p = params.min_p;
304 raw.max_tokens = optional_u32_to_i32(params.max_tokens, "max_tokens")?;
305 raw.seed = params.seed.unwrap_or(0);
306 raw.has_seed = i32::from(params.seed.is_some());
307 raw.presence_penalty = params.presence_penalty;
308 raw.frequency_penalty = params.frequency_penalty;
309 raw.repetition_penalty = params.repetition_penalty;
310 raw.min_tokens = u32_to_i32(params.min_tokens, "min_tokens")?;
311 raw.ignore_eos = i32::from(params.ignore_eos);
312
313 let stop = strings_to_cstrings(¶ms.stop, "stop string")?;
314 let stop_pointers = stop.iter().map(|value| value.as_ptr()).collect::<Vec<_>>();
315 raw.stop = pointer_or_null(&stop_pointers);
316 raw.n_stop = length_to_i32(stop_pointers.len(), "stop strings")?;
317
318 let mut structured_string = None;
319 let mut choices = Vec::new();
320 let mut choice_pointers = Vec::new();
321 if let Some(structured) = ¶ms.structured_output {
322 match structured {
323 StructuredOutput::JsonSchema(value) => {
324 structured_string = Some(to_cstring(value, "JSON schema")?);
325 raw.structured_json = structured_string.as_ref().unwrap().as_ptr();
326 }
327 StructuredOutput::Regex(value) => {
328 structured_string = Some(to_cstring(value, "structured regex")?);
329 raw.structured_regex = structured_string.as_ref().unwrap().as_ptr();
330 }
331 StructuredOutput::Choice(values) => {
332 if values.is_empty() {
333 return Err(invalid_configuration(
334 "structured choices must contain at least one value",
335 ));
336 }
337 choices = strings_to_cstrings(values, "structured choice")?;
338 choice_pointers = choices
339 .iter()
340 .map(|value| value.as_ptr())
341 .collect::<Vec<_>>();
342 raw.structured_choice = pointer_or_null(&choice_pointers);
343 raw.n_structured_choice =
344 length_to_i32(choice_pointers.len(), "structured choices")?;
345 }
346 StructuredOutput::Grammar(value) => {
347 structured_string = Some(to_cstring(value, "structured grammar")?);
348 raw.structured_grammar = structured_string.as_ref().unwrap().as_ptr();
349 }
350 StructuredOutput::JsonObject => raw.structured_json_object = 1,
351 }
352 }
353
354 let mut logits_processor = None;
355 if let Some(processor) = ¶ms.logits_processor {
356 if params.max_tokens.is_none() || params.max_tokens == Some(0) {
357 return Err(invalid_configuration(
358 "custom logits processors require bounded max_tokens because the native callback cannot abort generation",
359 ));
360 }
361 let registration = LogitsProcessorRegistration::new(Arc::clone(&processor.callback));
362 raw.logits_processor = Some(logits_processor_trampoline);
363 raw.logits_processor_user_data = registration.user_data();
364 logits_processor = Some(registration);
365 }
366
367 Ok(Self {
368 raw,
369 _stop: stop,
370 _stop_pointers: stop_pointers,
371 _structured_string: structured_string,
372 _choices: choices,
373 _choice_pointers: choice_pointers,
374 logits_processor,
375 })
376 }
377
378 pub(crate) const fn raw(&self) -> &ffi::vllm_sampling_params {
379 &self.raw
380 }
381
382 pub(crate) fn logits_processor_error(&self) -> Option<Error> {
383 self.logits_processor
384 .as_ref()
385 .and_then(LogitsProcessorRegistration::error)
386 }
387
388 pub(crate) fn take_logits_processor(&mut self) -> Option<LogitsProcessorRegistration> {
389 self.logits_processor.take()
390 }
391}
392
393const PROCESSOR_OK: u8 = 0;
394const PROCESSOR_PANICKED: u8 = 1;
395const PROCESSOR_INVALID_INPUT: u8 = 2;
396
397static NEXT_PROCESSOR_ID: AtomicUsize = AtomicUsize::new(1);
398static LOGITS_PROCESSORS: OnceLock<Mutex<HashMap<usize, Weak<LogitsProcessorState>>>> =
399 OnceLock::new();
400
401pub(crate) struct LogitsProcessorRegistration {
402 id: usize,
403 state: Arc<LogitsProcessorState>,
404}
405
406impl LogitsProcessorRegistration {
407 fn new(callback: Arc<DynLogitsProcessor>) -> Self {
408 let id = NEXT_PROCESSOR_ID
409 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
410 .unwrap_or_else(|_| std::process::abort());
411 let state = Arc::new(LogitsProcessorState::new(callback));
412 lock_unpoisoned(logits_processor_registry()).insert(id, Arc::downgrade(&state));
413 Self { id, state }
414 }
415
416 fn user_data(&self) -> *mut c_void {
417 ptr::without_provenance_mut(self.id)
418 }
419
420 pub(crate) fn error(&self) -> Option<Error> {
421 self.state.error()
422 }
423
424 pub(crate) fn is_active_on_current_thread(&self) -> bool {
425 self.state.is_active_on_current_thread()
426 }
427}
428
429impl Drop for LogitsProcessorRegistration {
430 fn drop(&mut self) {
431 lock_unpoisoned(logits_processor_registry()).remove(&self.id);
432 }
433}
434
435fn logits_processor_registry() -> &'static Mutex<HashMap<usize, Weak<LogitsProcessorState>>> {
436 LOGITS_PROCESSORS.get_or_init(|| Mutex::new(HashMap::new()))
437}
438
439fn registered_logits_processor(id: usize) -> Option<Arc<LogitsProcessorState>> {
440 lock_unpoisoned(logits_processor_registry())
441 .get(&id)
442 .and_then(Weak::upgrade)
443}
444
445struct LogitsProcessorState {
446 callback: Arc<DynLogitsProcessor>,
447 failure: AtomicU8,
448 active_threads: Mutex<Vec<ThreadId>>,
449}
450
451impl LogitsProcessorState {
452 fn new(callback: Arc<DynLogitsProcessor>) -> Self {
453 Self {
454 callback,
455 failure: AtomicU8::new(PROCESSOR_OK),
456 active_threads: Mutex::new(Vec::new()),
457 }
458 }
459
460 pub(crate) fn error(&self) -> Option<Error> {
461 match self.failure.load(Ordering::Acquire) {
462 PROCESSOR_OK => None,
463 PROCESSOR_PANICKED => Some(Error::LogitsProcessorPanicked),
464 PROCESSOR_INVALID_INPUT => Some(Error::Runtime {
465 message: "native logits processor callback received invalid pointers or lengths"
466 .to_owned(),
467 }),
468 _ => Some(Error::Runtime {
469 message: "native logits processor callback entered an unknown failure state"
470 .to_owned(),
471 }),
472 }
473 }
474
475 pub(crate) fn is_active_on_current_thread(&self) -> bool {
476 let current = thread::current().id();
477 lock_unpoisoned(&self.active_threads).contains(¤t)
478 }
479
480 fn record_failure(&self, failure: u8) {
481 let _ = self.failure.compare_exchange(
482 PROCESSOR_OK,
483 failure,
484 Ordering::AcqRel,
485 Ordering::Acquire,
486 );
487 }
488}
489
490struct ActiveProcessorGuard<'state> {
491 state: &'state LogitsProcessorState,
492 thread_id: ThreadId,
493}
494
495impl<'state> ActiveProcessorGuard<'state> {
496 fn enter(state: &'state LogitsProcessorState) -> Self {
497 let thread_id = thread::current().id();
498 lock_unpoisoned(&state.active_threads).push(thread_id);
499 Self { state, thread_id }
500 }
501}
502
503impl Drop for ActiveProcessorGuard<'_> {
504 fn drop(&mut self) {
505 let mut active = lock_unpoisoned(&self.state.active_threads);
506 if let Some(index) = active.iter().rposition(|id| *id == self.thread_id) {
507 active.swap_remove(index);
508 }
509 }
510}
511
512unsafe extern "C" fn logits_processor_trampoline(
513 token_ids: *const i32,
514 n_token_ids: i32,
515 logits: *mut f32,
516 vocab_size: i32,
517 user_data: *mut c_void,
518) {
519 if user_data.is_null() {
520 return;
521 }
522 let Some(state) = registered_logits_processor(user_data.addr()) else {
523 return;
524 };
525 if state.failure.load(Ordering::Acquire) != PROCESSOR_OK {
526 return;
527 }
528 if n_token_ids < 0
529 || vocab_size <= 0
530 || (n_token_ids > 0 && token_ids.is_null())
531 || logits.is_null()
532 || (n_token_ids > 0 && !valid_slice_layout(token_ids, n_token_ids as usize))
533 || !valid_slice_layout(logits, vocab_size as usize)
534 {
535 state.record_failure(PROCESSOR_INVALID_INPUT);
536 return;
537 }
538
539 let _active = ActiveProcessorGuard::enter(&state);
540 let tokens = if n_token_ids == 0 {
541 &[]
542 } else {
543 unsafe { slice::from_raw_parts(token_ids, n_token_ids as usize) }
546 };
547 let logits = unsafe { slice::from_raw_parts_mut(logits, vocab_size as usize) };
550 if let Err(payload) = catch_unwind(AssertUnwindSafe(|| (state.callback)(tokens, logits))) {
551 state.record_failure(PROCESSOR_PANICKED);
552 discard_panic_payload(payload);
553 }
554}
555
556fn valid_slice_layout<T>(pointer: *const T, length: usize) -> bool {
557 !pointer.is_null()
558 && (pointer as usize) % align_of::<T>() == 0
559 && length <= (isize::MAX as usize) / size_of::<T>()
560}
561
562fn discard_panic_payload(payload: Box<dyn std::any::Any + Send>) {
563 if let Err(payload) = catch_unwind(AssertUnwindSafe(|| drop(payload))) {
564 std::mem::forget(payload);
565 }
566}
567
568fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
569 mutex
570 .lock()
571 .unwrap_or_else(std::sync::PoisonError::into_inner)
572}
573
574pub(crate) fn to_cstring(value: &str, field: &'static str) -> Result<CString, Error> {
575 CString::new(value).map_err(|_| Error::InteriorNul { field })
576}
577
578fn strings_to_cstrings(values: &[String], field: &'static str) -> Result<Vec<CString>, Error> {
579 values
580 .iter()
581 .map(|value| to_cstring(value, field))
582 .collect()
583}
584
585fn pointer_or_null(values: &[*const c_char]) -> *const *const c_char {
586 if values.is_empty() {
587 ptr::null()
588 } else {
589 values.as_ptr()
590 }
591}
592
593fn optional_u32_to_i32(value: Option<u32>, field: &'static str) -> Result<i32, Error> {
594 match value {
595 Some(0) => Err(invalid_configuration(format!(
596 "{field} must be greater than zero; use unbounded() for no limit"
597 ))),
598 Some(value) => u32_to_i32(value, field),
599 None => Ok(0),
600 }
601}
602
603fn u32_to_i32(value: u32, field: &'static str) -> Result<i32, Error> {
604 i32::try_from(value)
605 .map_err(|_| invalid_configuration(format!("{field} exceeds native i32 range")))
606}
607
608fn length_to_i32(value: usize, field: &'static str) -> Result<i32, Error> {
609 i32::try_from(value).map_err(|_| invalid_configuration(format!("too many {field}")))
610}
611
612#[cfg(test)]
613mod tests {
614 use super::{logits_processor_trampoline, SamplingParams};
615 use crate::Error;
616 use std::sync::atomic::{AtomicUsize, Ordering};
617 use std::sync::Arc;
618
619 #[test]
620 fn marshals_and_invokes_custom_logits_processor() {
621 let params = SamplingParams::default()
622 .max_tokens(2)
623 .logits_processor(|tokens, logits| {
624 assert_eq!(tokens, &[3, 5]);
625 logits[1] = 9.0;
626 });
627 let marshaled = params.marshal().expect("marshal processor");
628 let callback = marshaled
629 .raw()
630 .logits_processor
631 .expect("processor callback");
632 let mut logits = [1.0, 2.0, 3.0];
633 let tokens = [3, 5];
634 unsafe {
635 callback(
636 tokens.as_ptr(),
637 tokens.len() as i32,
638 logits.as_mut_ptr(),
639 logits.len() as i32,
640 marshaled.raw().logits_processor_user_data,
641 );
642 }
643 assert_eq!(logits, [1.0, 9.0, 3.0]);
644 assert_eq!(marshaled.logits_processor_error(), None);
645 }
646
647 #[test]
648 fn stale_processor_user_data_is_a_noop() {
649 let calls = Arc::new(AtomicUsize::new(0));
650 let user_data = {
651 let calls = Arc::clone(&calls);
652 let params = SamplingParams::default()
653 .max_tokens(1)
654 .logits_processor(move |_, _| {
655 calls.fetch_add(1, Ordering::Relaxed);
656 });
657 let marshaled = params.marshal().expect("marshal processor");
658 marshaled.raw().logits_processor_user_data
659 };
660 let mut logits = [1.0];
661 unsafe {
662 logits_processor_trampoline(
663 std::ptr::null(),
664 0,
665 logits.as_mut_ptr(),
666 logits.len() as i32,
667 user_data,
668 );
669 }
670 assert_eq!(calls.load(Ordering::Relaxed), 0);
671 }
672
673 #[test]
674 fn contains_processor_panic_and_skips_later_calls() {
675 let params = SamplingParams::default()
676 .max_tokens(2)
677 .logits_processor(|_, _| panic!("processor panic"));
678 let marshaled = params.marshal().expect("marshal processor");
679 let mut logits = [1.0, 2.0];
680 unsafe {
681 logits_processor_trampoline(
682 std::ptr::null(),
683 0,
684 logits.as_mut_ptr(),
685 logits.len() as i32,
686 marshaled.raw().logits_processor_user_data,
687 );
688 }
689 assert_eq!(
690 marshaled.logits_processor_error(),
691 Some(Error::LogitsProcessorPanicked)
692 );
693 unsafe {
694 logits_processor_trampoline(
695 std::ptr::null(),
696 0,
697 logits.as_mut_ptr(),
698 logits.len() as i32,
699 marshaled.raw().logits_processor_user_data,
700 );
701 }
702 }
703
704 #[test]
705 fn rejects_zero_or_unbounded_processor_and_invalid_native_shape() {
706 for params in [
707 SamplingParams::default().max_tokens(0),
708 SamplingParams::default()
709 .unbounded()
710 .logits_processor(|_, _| {}),
711 ] {
712 let error = params.marshal().err().expect("invalid bounds rejection");
713 assert!(matches!(error, Error::InvalidConfiguration { .. }));
714 }
715
716 let params = SamplingParams::default().logits_processor(|_, _| {});
717 let marshaled = params.marshal().expect("marshal processor");
718 unsafe {
719 logits_processor_trampoline(
720 std::ptr::null(),
721 -1,
722 std::ptr::null_mut(),
723 0,
724 marshaled.raw().logits_processor_user_data,
725 );
726 }
727 assert!(matches!(
728 marshaled.logits_processor_error(),
729 Some(Error::Runtime { .. })
730 ));
731 }
732}