1use std::any::Any;
2use std::cell::Cell;
3use std::ffi::CStr;
4use std::marker::PhantomData;
5use std::os::raw::{c_char, c_void};
6use std::panic::{catch_unwind, AssertUnwindSafe};
7use std::ptr::{self, NonNull};
8use std::sync::{mpsc, Arc, Mutex, OnceLock};
9use std::thread::{self, ThreadId};
10
11use vllm_cpp_sys as ffi;
12
13use crate::callback::{StreamControl, StreamEvent};
14use crate::engine::{Engine, EngineInner};
15use crate::error::{status_result, Error};
16use crate::params::{to_cstring, LogitsProcessorRegistration, SamplingParams};
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24#[non_exhaustive]
25pub enum RequestOutcome {
26 Completed,
28 StoppedByCallback,
32 Cancelled,
34}
35
36pub struct Request {
41 raw: Option<NonNull<ffi::vllm_request>>,
42 callback: Option<Box<AsyncCallbackState>>,
43 logits_processor: Option<LogitsProcessorRegistration>,
44 engine: Option<Arc<EngineInner>>,
45 cancellation_requested: bool,
46 _not_sync: PhantomData<Cell<()>>,
47}
48
49impl std::fmt::Debug for Request {
50 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 formatter
52 .debug_struct("Request")
53 .field("raw", &self.raw)
54 .field("cancellation_requested", &self.cancellation_requested)
55 .finish_non_exhaustive()
56 }
57}
58
59impl Engine {
60 pub fn submit<F>(
66 &self,
67 prompt: &str,
68 params: &SamplingParams,
69 callback: F,
70 ) -> Result<Request, Error>
71 where
72 F: FnMut(StreamEvent) -> StreamControl + Send + 'static,
73 {
74 cleanup_sender()?;
75 let prompt = to_cstring(prompt, "prompt")?;
76 let mut params = params.marshal()?;
77 let mut callback = Box::new(AsyncCallbackState::new(callback));
78 let mut output = ptr::null_mut();
79 let status = unsafe {
83 ffi::vllm_request_submit(
84 self.inner.raw.as_ptr(),
85 prompt.as_ptr(),
86 params.raw(),
87 Some(async_callback_trampoline),
88 ptr::from_mut(&mut *callback).cast(),
89 &mut output,
90 )
91 };
92 if status != ffi::vllm_status_VLLM_OK {
93 status_result(status)?;
94 unreachable!("non-OK native status unexpectedly succeeded");
95 }
96 let raw = match NonNull::new(output) {
97 Some(raw) => raw,
98 None => {
99 return Err(Error::Runtime {
100 message: "vllm_request_submit succeeded without a request handle".to_owned(),
101 });
102 }
103 };
104 Ok(Request {
105 raw: Some(raw),
106 callback: Some(callback),
107 logits_processor: params.take_logits_processor(),
108 engine: Some(Arc::clone(&self.inner)),
109 cancellation_requested: false,
110 _not_sync: PhantomData,
111 })
112 }
113}
114
115impl Request {
116 #[must_use]
118 pub fn is_done(&self) -> bool {
119 self.native_done()
120 }
121
122 pub fn cancel(&mut self) -> Result<(), Error> {
130 let was_done = self.is_done();
131 let status = unsafe { ffi::vllm_request_cancel(self.raw().as_ptr()) };
133 status_result(status)?;
134 self.cancellation_requested |= !was_done;
135 Ok(())
136 }
137
138 pub fn wait(&mut self) -> Result<RequestOutcome, Error> {
143 if self.is_native_callback_thread() {
144 return Err(Error::RequestCallbackThread { operation: "wait" });
145 }
146 let status = unsafe { ffi::vllm_request_wait(self.raw().as_ptr()) };
149 let native_result = status_result(status);
150 if let Some(error) = self.logits_processor_error() {
151 return Err(error);
152 }
153 let callback_result = self.callback().result(self.cancellation_requested);
154 match callback_result {
155 Err(error) => Err(error),
156 Ok(Some(outcome)) => native_result.map(|()| outcome),
157 Ok(None) => {
158 native_result?;
159 Err(Error::Runtime {
160 message: "request completed without a terminal callback or locally observable stop/cancellation"
161 .to_owned(),
162 })
163 }
164 }
165 }
166
167 pub fn native_error(&self) -> Result<Option<String>, Error> {
173 if !self.is_done() {
174 return Ok(None);
175 }
176 let pointer = unsafe { ffi::vllm_request_error(self.raw().as_ptr()) };
179 if pointer.is_null() {
180 return Err(Error::Runtime {
181 message: "vllm_request_error returned a null pointer".to_owned(),
182 });
183 }
184 let error = unsafe { CStr::from_ptr(pointer) }
187 .to_str()
188 .map_err(|_| Error::InvalidUtf8 {
189 field: "request error",
190 })?
191 .to_owned();
192 Ok((!error.is_empty()).then_some(error))
193 }
194
195 fn raw(&self) -> NonNull<ffi::vllm_request> {
196 self.raw.expect("live Request always has a native handle")
197 }
198
199 fn native_done(&self) -> bool {
200 unsafe { ffi::vllm_request_done(self.raw().as_ptr()) }
203 }
204
205 fn callback(&self) -> &AsyncCallbackState {
206 self.callback
207 .as_deref()
208 .expect("live Request always has callback state")
209 }
210
211 fn logits_processor_error(&self) -> Option<Error> {
212 self.logits_processor
213 .as_ref()
214 .and_then(LogitsProcessorRegistration::error)
215 }
216
217 fn is_native_callback_thread(&self) -> bool {
218 self.callback().is_delivery_thread()
219 || self
220 .logits_processor
221 .as_ref()
222 .is_some_and(LogitsProcessorRegistration::is_active_on_current_thread)
223 }
224}
225
226impl Drop for Request {
227 fn drop(&mut self) {
228 let parts = (
229 self.raw.take(),
230 self.callback.take(),
231 self.logits_processor.take(),
232 self.engine.take(),
233 );
234 match parts {
235 (Some(raw), Some(callback), logits_processor, Some(engine)) => {
236 CleanupJob::new(raw, callback, logits_processor, engine).run();
237 }
238 parts => {
239 std::mem::forget(parts);
243 std::process::abort();
244 }
245 }
246 }
247}
248
249unsafe impl Send for Request {}
253
254struct CallbackOutcome {
255 stopped: bool,
256 saw_finished: bool,
257 error: Option<Error>,
258 panic: Option<Box<dyn Any + Send>>,
259 delivery_thread: Option<ThreadId>,
260}
261
262struct AsyncCallbackState {
263 callback: Mutex<Box<dyn FnMut(StreamEvent) -> StreamControl + Send + 'static>>,
264 outcome: Mutex<CallbackOutcome>,
265}
266
267impl AsyncCallbackState {
268 fn new<F>(callback: F) -> Self
269 where
270 F: FnMut(StreamEvent) -> StreamControl + Send + 'static,
271 {
272 Self {
273 callback: Mutex::new(Box::new(callback)),
274 outcome: Mutex::new(CallbackOutcome {
275 stopped: false,
276 saw_finished: false,
277 error: None,
278 panic: None,
279 delivery_thread: None,
280 }),
281 }
282 }
283
284 fn record_delivery_thread(&self) {
285 lock_unpoisoned(&self.outcome).delivery_thread = Some(thread::current().id());
290 }
291
292 fn is_delivery_thread(&self) -> bool {
293 lock_unpoisoned(&self.outcome)
294 .delivery_thread
295 .as_ref()
296 .is_some_and(|id| *id == thread::current().id())
297 }
298
299 fn record_error(&self, error: Error) {
300 let mut outcome = lock_unpoisoned(&self.outcome);
301 outcome.error = Some(error);
302 outcome.stopped = true;
303 }
304
305 fn record_result(
306 &self,
307 result: Result<StreamControl, Box<dyn Any + Send>>,
308 finished: bool,
309 ) -> bool {
310 let mut outcome = lock_unpoisoned(&self.outcome);
311 outcome.saw_finished |= finished;
312 match result {
313 Ok(StreamControl::Continue) => true,
314 Ok(StreamControl::Stop) => {
315 outcome.stopped = true;
316 false
317 }
318 Err(payload) => {
319 outcome.panic = Some(payload);
320 outcome.stopped = true;
321 false
322 }
323 }
324 }
325
326 fn result(&self, cancellation_requested: bool) -> Result<Option<RequestOutcome>, Error> {
327 let outcome = lock_unpoisoned(&self.outcome);
328 if outcome.panic.is_some() {
329 return Err(Error::CallbackPanicked);
330 }
331 if let Some(error) = &outcome.error {
332 return Err(error.clone());
333 }
334 if outcome.stopped {
335 return Ok(Some(RequestOutcome::StoppedByCallback));
336 }
337 if outcome.saw_finished {
338 return Ok(Some(RequestOutcome::Completed));
339 }
340 if cancellation_requested {
341 return Ok(Some(RequestOutcome::Cancelled));
342 }
343 Ok(None)
344 }
345}
346
347unsafe extern "C" fn async_callback_trampoline(
348 delta_text: *const c_char,
349 finished: bool,
350 user_data: *mut c_void,
351) -> bool {
352 let state = unsafe { &*user_data.cast::<AsyncCallbackState>() };
355 state.record_delivery_thread();
356 if delta_text.is_null() {
357 state.record_error(Error::InvalidUtf8 {
358 field: "stream delta",
359 });
360 return false;
361 }
362 let delta = match unsafe { CStr::from_ptr(delta_text) }.to_str() {
364 Ok(delta) => delta.to_owned(),
365 Err(_) => {
366 state.record_error(Error::InvalidUtf8 {
367 field: "stream delta",
368 });
369 return false;
370 }
371 };
372 let event = StreamEvent { delta, finished };
373 let result = catch_unwind(AssertUnwindSafe(|| {
374 let mut callback = lock_unpoisoned(&state.callback);
375 callback(event)
376 }));
377 state.record_result(result, finished)
378}
379
380fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
381 mutex
382 .lock()
383 .unwrap_or_else(std::sync::PoisonError::into_inner)
384}
385
386struct CleanupJob {
387 state: CleanupState,
388 context: CleanupContext,
389}
390
391enum CleanupContext {
392 Caller,
393 Reaper,
394}
395
396enum CleanupState {
397 Armed {
398 raw: NonNull<ffi::vllm_request>,
399 callback: Box<AsyncCallbackState>,
400 logits_processor: Option<LogitsProcessorRegistration>,
401 engine: Arc<EngineInner>,
402 },
403 Disarmed,
404}
405
406impl CleanupJob {
407 fn new(
408 raw: NonNull<ffi::vllm_request>,
409 callback: Box<AsyncCallbackState>,
410 logits_processor: Option<LogitsProcessorRegistration>,
411 engine: Arc<EngineInner>,
412 ) -> Self {
413 Self {
414 state: CleanupState::Armed {
415 raw,
416 callback,
417 logits_processor,
418 engine,
419 },
420 context: CleanupContext::Caller,
421 }
422 }
423
424 fn run(mut self) {
425 self.finish();
426 }
427
428 fn finish(&mut self) {
429 if matches!(self.state, CleanupState::Disarmed) {
430 return;
431 }
432 let needs_deferral = match self.context {
433 CleanupContext::Caller => match &self.state {
434 CleanupState::Armed {
435 callback,
436 logits_processor,
437 ..
438 } => {
439 callback.is_delivery_thread()
440 || logits_processor
441 .as_ref()
442 .is_some_and(LogitsProcessorRegistration::is_active_on_current_thread)
443 }
444 CleanupState::Disarmed => return,
445 },
446 CleanupContext::Reaper => false,
449 };
450 if needs_deferral {
451 self.defer_to_reaper();
452 } else if let Err(payload) = catch_unwind(AssertUnwindSafe(|| self.cleanup_now())) {
453 self.leak_armed();
456 std::mem::forget(payload);
457 }
458 }
459
460 fn cleanup_now(&mut self) {
461 let state = std::mem::replace(&mut self.state, CleanupState::Disarmed);
462 let CleanupState::Armed {
463 raw,
464 callback,
465 logits_processor,
466 engine,
467 } = state
468 else {
469 return;
470 };
471 let mut owners = std::mem::ManuallyDrop::new((callback, logits_processor, engine));
475 unsafe { ffi::vllm_request_free(raw.as_ptr()) };
483 let (callback, logits_processor, engine) =
485 unsafe { std::mem::ManuallyDrop::take(&mut owners) };
486 if let Err(payload) = catch_unwind(AssertUnwindSafe(|| drop(callback))) {
489 std::mem::forget(payload);
490 }
491 if let Err(payload) = catch_unwind(AssertUnwindSafe(|| drop(logits_processor))) {
492 std::mem::forget(payload);
493 }
494 drop(engine);
495 }
496
497 fn leak_armed(&mut self) {
498 let state = std::mem::replace(&mut self.state, CleanupState::Disarmed);
499 std::mem::forget(state);
500 }
501
502 fn defer_to_reaper(&mut self) {
503 let sender = match CLEANUP_REAPER.get() {
504 Some(Ok(sender)) => sender,
505 _ => std::process::abort(),
508 };
509 let job = Self {
510 state: std::mem::replace(&mut self.state, CleanupState::Disarmed),
511 context: CleanupContext::Reaper,
512 };
513 if let Err(error) = sender.send(job) {
514 std::mem::forget(error);
518 std::process::abort();
519 }
520 }
521}
522
523impl Drop for CleanupJob {
524 fn drop(&mut self) {
525 self.finish();
529 }
530}
531
532unsafe impl Send for CleanupJob {}
536
537static CLEANUP_REAPER: OnceLock<Result<mpsc::Sender<CleanupJob>, String>> = OnceLock::new();
538
539fn cleanup_sender() -> Result<&'static mpsc::Sender<CleanupJob>, Error> {
540 match CLEANUP_REAPER.get_or_init(|| {
541 let (sender, receiver) = mpsc::channel::<CleanupJob>();
542 thread::Builder::new()
543 .name("vllm-request-reaper".to_owned())
544 .spawn(move || {
545 while let Ok(job) = receiver.recv() {
546 job.run();
547 }
548 })
549 .map(|_| sender)
550 .map_err(|error| error.to_string())
551 }) {
552 Ok(sender) => Ok(sender),
553 Err(message) => Err(Error::Runtime {
554 message: format!("failed to start request cleanup reaper: {message}"),
555 }),
556 }
557}