1use crate::stream::StreamEvent;
25use crate::tasks::generate::GenerateRequest;
26use crate::{InferenceError, InferenceResult};
27use serde::{Deserialize, Serialize};
28use std::future::Future;
29use std::path::PathBuf;
30use std::sync::atomic::{AtomicBool, Ordering};
31use std::sync::{Arc, OnceLock, RwLock};
32
33#[derive(Clone, Debug)]
34struct InferenceControlScope {
35 inference_id: String,
36 termination: ControlledTerminationToken,
37}
38
39tokio::task_local! {
40 static INFERENCE_CONTROL_SCOPE: InferenceControlScope;
41}
42
43#[derive(Clone, Debug, Default)]
47pub struct ControlledTerminationToken(Arc<AtomicBool>);
48
49impl ControlledTerminationToken {
50 pub fn confirm_exact_backend_termination(&self) {
54 self.0.store(true, Ordering::Release);
55 }
56
57 pub fn is_confirmed(&self) -> bool {
58 self.0.load(Ordering::Acquire)
59 }
60
61 pub fn error_if_confirmed(&self) -> Result<(), InferenceError> {
62 if self.is_confirmed() {
63 Err(InferenceError::ControlledTermination)
64 } else {
65 Ok(())
66 }
67 }
68}
69
70pub async fn scope_inference_control_id<F>(inference_id: String, future: F) -> F::Output
74where
75 F: Future,
76{
77 INFERENCE_CONTROL_SCOPE
78 .scope(
79 InferenceControlScope {
80 inference_id,
81 termination: ControlledTerminationToken::default(),
82 },
83 future,
84 )
85 .await
86}
87
88pub fn current_inference_control_id() -> Option<String> {
90 INFERENCE_CONTROL_SCOPE
91 .try_with(|scope| scope.inference_id.clone())
92 .ok()
93}
94
95static REMOTE_DEADLINES: OnceLock<RwLock<std::collections::HashMap<String, RemoteDeadline>>> =
104 OnceLock::new();
105
106#[derive(Clone, Copy, Debug)]
110pub struct RemoteDeadline {
111 pub expires_at: std::time::Instant,
112 pub applied_ms: u64,
113}
114
115impl RemoteDeadline {
116 pub fn remaining(&self) -> std::time::Duration {
117 self.expires_at
118 .saturating_duration_since(std::time::Instant::now())
119 }
120}
121
122fn remote_deadlines() -> &'static RwLock<std::collections::HashMap<String, RemoteDeadline>> {
123 REMOTE_DEADLINES.get_or_init(|| RwLock::new(std::collections::HashMap::new()))
124}
125
126pub fn set_remote_deadline(inference_id: &str, timeout: std::time::Duration) {
129 if let Ok(mut map) = remote_deadlines().write() {
130 map.insert(
131 inference_id.to_string(),
132 RemoteDeadline {
133 expires_at: std::time::Instant::now() + timeout,
134 applied_ms: timeout.as_millis() as u64,
135 },
136 );
137 }
138}
139
140pub fn clear_remote_deadline(inference_id: &str) {
144 if let Ok(mut map) = remote_deadlines().write() {
145 map.remove(inference_id);
146 }
147}
148
149pub fn current_remote_deadline() -> Option<RemoteDeadline> {
153 let id = current_inference_control_id()?;
154 remote_deadlines().read().ok()?.get(&id).copied()
155}
156
157pub fn current_controlled_termination_token() -> Option<ControlledTerminationToken> {
160 INFERENCE_CONTROL_SCOPE
161 .try_with(|scope| scope.termination.clone())
162 .ok()
163}
164
165pub fn ensure_not_controlled_terminated() -> Result<(), InferenceError> {
167 match current_controlled_termination_token() {
168 Some(token) => token.error_if_confirmed(),
169 None => Ok(()),
170 }
171}
172
173#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
174pub struct LocalWorkerAdmission {
175 pub policy: crate::resource_policy::ResourcePolicy,
176 pub policy_generation: u64,
177 pub state_root: PathBuf,
178 pub measured_weights_bytes: u64,
179}
180
181#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
182pub struct LocalWorkerResidency {
183 pub model_id: String,
184 pub measured_weights_bytes: u64,
185}
186
187pub struct LocalOffloadResult {
188 pub result: InferenceResult,
189 pub residency: LocalWorkerResidency,
190 pub retention: crate::backend_cache::BackendRetention,
191}
192
193pub struct LocalOffloadStream {
194 pub events: tokio::sync::mpsc::Receiver<StreamEvent>,
195 pub residency: LocalWorkerResidency,
196 pub retention: crate::backend_cache::BackendRetention,
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Eq)]
203pub enum InferenceTerminationAck {
204 Confirmed,
205 Unconfirmed,
206}
207
208#[async_trait::async_trait]
215pub trait LocalGenerationOffload: Send + Sync {
216 async fn generate(&self, request: GenerateRequest) -> Result<InferenceResult, InferenceError>;
218
219 async fn stream(
224 &self,
225 request: GenerateRequest,
226 ) -> Result<tokio::sync::mpsc::Receiver<StreamEvent>, InferenceError>;
227
228 async fn generate_admitted(
233 &self,
234 request: GenerateRequest,
235 _admission: LocalWorkerAdmission,
236 ) -> Result<LocalOffloadResult, InferenceError> {
237 let model_id = request.model.clone().unwrap_or_default();
238 let result = self.generate(request).await?;
239 Ok(LocalOffloadResult {
240 result,
241 residency: LocalWorkerResidency {
242 model_id,
243 measured_weights_bytes: 0,
244 },
245 retention: crate::backend_cache::BackendRetention::Transient,
246 })
247 }
248
249 async fn stream_admitted(
250 &self,
251 request: GenerateRequest,
252 _admission: LocalWorkerAdmission,
253 ) -> Result<LocalOffloadStream, InferenceError> {
254 let model_id = request.model.clone().unwrap_or_default();
255 let events = self.stream(request).await?;
256 Ok(LocalOffloadStream {
257 events,
258 residency: LocalWorkerResidency {
259 model_id,
260 measured_weights_bytes: 0,
261 },
262 retention: crate::backend_cache::BackendRetention::Transient,
263 })
264 }
265
266 fn refresh_resource_policy(&self, _generation: u64) {}
267
268 async fn resident_models(&self) -> Vec<String> {
269 Vec::new()
270 }
271
272 fn resident_allocation_id(&self, _model_id: &str) -> Option<String> {
275 None
276 }
277
278 async fn release_model(&self, _model_id: &str) -> Result<bool, InferenceError> {
281 Ok(false)
282 }
283
284 async fn terminate_inference(&self, _inference_id: &str) -> InferenceTerminationAck {
288 InferenceTerminationAck::Unconfirmed
289 }
290}
291
292fn offload_slot() -> &'static RwLock<Option<Arc<dyn LocalGenerationOffload>>> {
293 static SLOT: OnceLock<RwLock<Option<Arc<dyn LocalGenerationOffload>>>> = OnceLock::new();
294 SLOT.get_or_init(|| RwLock::new(None))
295}
296
297pub fn set_local_offload(offload: Option<Arc<dyn LocalGenerationOffload>>) {
300 let mut guard = offload_slot().write().expect("local offload slot poisoned");
301 *guard = offload;
302}
303
304pub fn current_local_offload() -> Option<Arc<dyn LocalGenerationOffload>> {
310 if is_offload_worker() {
311 return None;
312 }
313 offload_slot()
314 .read()
315 .expect("local offload slot poisoned")
316 .clone()
317}
318
319pub fn is_offload_worker() -> bool {
325 std::env::var_os("CAR_INFERENCE_WORKER").is_some()
326}
327
328#[cfg(test)]
329pub(crate) fn test_offload_lock() -> &'static tokio::sync::Mutex<()> {
330 static LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
331 LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 struct NoopOffload;
339
340 #[tokio::test]
341 async fn legacy_or_nonisolated_offload_never_confirms_termination() {
342 let offload = NoopOffload;
343 assert_eq!(
344 offload.terminate_inference("opaque").await,
345 InferenceTerminationAck::Unconfirmed
346 );
347 }
348
349 #[async_trait::async_trait]
350 impl LocalGenerationOffload for NoopOffload {
351 async fn generate(
352 &self,
353 _request: GenerateRequest,
354 ) -> Result<InferenceResult, InferenceError> {
355 Err(InferenceError::InferenceFailed("noop".into()))
356 }
357 async fn stream(
358 &self,
359 _request: GenerateRequest,
360 ) -> Result<tokio::sync::mpsc::Receiver<StreamEvent>, InferenceError> {
361 Err(InferenceError::InferenceFailed("noop".into()))
362 }
363 }
364
365 #[allow(dead_code)]
366 fn external_legacy_trait_contract_still_compiles(
367 implementation: Arc<dyn LocalGenerationOffload>,
368 ) -> Arc<dyn LocalGenerationOffload> {
369 implementation
370 }
371
372 #[tokio::test]
373 async fn slot_set_and_clear() {
374 let _guard = test_offload_lock().lock().await;
375 assert!(current_local_offload().is_none());
378 set_local_offload(Some(Arc::new(NoopOffload)));
379 assert!(current_local_offload().is_some());
382 set_local_offload(None);
383 assert!(current_local_offload().is_none());
384 }
385}