Skip to main content

dynamo_mocker/scheduler/
mod.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Dynamo-facing protocol for the shared AISimulate generalized engine.
5//!
6//! Engine scheduling, native KV accounting, preemption, and timing live in
7//! `aisimulate_core::engine`. This module retains only the asynchronous compatibility
8//! contract consumed by Dynamo's Live Mocker and handoff driver.
9
10mod metrics;
11mod protocol;
12
13use crate::common::protocols::{DirectRequest, OutputSignal};
14use tokio::sync::{mpsc, oneshot};
15use tokio_util::sync::CancellationToken;
16use uuid::Uuid;
17
18pub use crate::common::protocols::ForwardPassSnapshot;
19pub use metrics::MockerMetrics;
20pub use protocol::{
21    SchedulerCommand, SchedulerCommandEffects, SchedulerCommandResult, SchedulerLifecycleEvent,
22};
23
24#[derive(Debug, Clone)]
25pub(crate) struct AdmissionEvent {
26    pub(crate) uuid: Uuid,
27    pub(crate) reused_input_tokens: usize,
28}
29
30pub struct SchedulerCommandEnvelope {
31    pub command: SchedulerCommand,
32    pub reply: oneshot::Sender<anyhow::Result<SchedulerCommandEffects>>,
33}
34
35#[derive(Debug)]
36pub(crate) enum LiveEngineEvent {
37    Admissions(Vec<AdmissionEvent>),
38    Outputs {
39        signals: Vec<OutputSignal>,
40        /// Acknowledge only after the request-route dispatcher has attempted
41        /// delivery. The grouped pass boundary waits on this signal, so the
42        /// next pass cannot overtake route cleanup for the current one.
43        delivered: oneshot::Sender<Vec<OutputSignal>>,
44    },
45}
46
47/// Visibility point retained by Dynamo's replay-artifact adapter. Native
48/// engine observations are captured at the generalized-engine boundary; this
49/// enum only selects the timestamp used when rendering legacy artifacts.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub(crate) enum RouterEventVisibility {
52    PassStart,
53    PassEnd,
54}
55
56#[derive(Clone)]
57pub(crate) enum SchedulerEventSender {
58    Outputs(mpsc::UnboundedSender<Vec<OutputSignal>>),
59    Ordered {
60        tx: mpsc::Sender<LiveEngineEvent>,
61        forward_admissions: bool,
62        cancel: CancellationToken,
63    },
64}
65
66#[derive(Debug)]
67pub(crate) enum SchedulerEventSendError {
68    OutputClosed(Vec<OutputSignal>),
69    OrderedLaneClosed,
70    Cancelled,
71}
72
73impl SchedulerEventSender {
74    pub(crate) async fn send_admissions(
75        &self,
76        admissions: &[AdmissionEvent],
77    ) -> Result<(), SchedulerEventSendError> {
78        if admissions.is_empty() {
79            return Ok(());
80        }
81        match self {
82            Self::Outputs(_) => Ok(()),
83            Self::Ordered {
84                forward_admissions: false,
85                ..
86            } => Ok(()),
87            Self::Ordered { tx, cancel, .. } => {
88                tokio::select! {
89                    biased;
90                    result = tx.send(LiveEngineEvent::Admissions(admissions.to_vec())) => {
91                        result.map_err(|_| {
92                            if cancel.is_cancelled() {
93                                SchedulerEventSendError::Cancelled
94                            } else {
95                                SchedulerEventSendError::OrderedLaneClosed
96                            }
97                        })
98                    }
99                    _ = cancel.cancelled() => Err(SchedulerEventSendError::Cancelled),
100                }
101            }
102        }
103    }
104
105    pub(crate) async fn send_outputs(
106        &self,
107        signals: Vec<OutputSignal>,
108    ) -> Result<(), SchedulerEventSendError> {
109        match self {
110            Self::Outputs(tx) => tx
111                .send(signals)
112                .map_err(|error| SchedulerEventSendError::OutputClosed(error.0)),
113            Self::Ordered { tx, cancel, .. } => {
114                let (delivered, acknowledged) = oneshot::channel();
115                tokio::select! {
116                    biased;
117                    result = tx.send(LiveEngineEvent::Outputs { signals, delivered }) => {
118                        result.map_err(|_| {
119                            if cancel.is_cancelled() {
120                                SchedulerEventSendError::Cancelled
121                            } else {
122                                SchedulerEventSendError::OrderedLaneClosed
123                            }
124                        })?;
125                    }
126                    _ = cancel.cancelled() => return Err(SchedulerEventSendError::Cancelled),
127                }
128                let failed = tokio::select! {
129                    biased;
130                    result = acknowledged => {
131                        result.map_err(|_| {
132                            if cancel.is_cancelled() {
133                                SchedulerEventSendError::Cancelled
134                            } else {
135                                SchedulerEventSendError::OrderedLaneClosed
136                            }
137                        })?
138                    }
139                    _ = cancel.cancelled() => return Err(SchedulerEventSendError::Cancelled),
140                };
141                if failed.is_empty() {
142                    Ok(())
143                } else {
144                    Err(SchedulerEventSendError::OutputClosed(failed))
145                }
146            }
147        }
148    }
149}
150
151impl From<mpsc::UnboundedSender<Vec<OutputSignal>>> for SchedulerEventSender {
152    fn from(tx: mpsc::UnboundedSender<Vec<OutputSignal>>) -> Self {
153        Self::Outputs(tx)
154    }
155}
156
157pub struct SchedulerCancellationEnvelope {
158    pub request_id: Uuid,
159    pub discard_pending_output: bool,
160    pub reply: oneshot::Sender<anyhow::Result<SchedulerCommandEffects>>,
161}
162
163impl From<SchedulerCancellationEnvelope> for SchedulerCommandEnvelope {
164    fn from(cancellation: SchedulerCancellationEnvelope) -> Self {
165        Self {
166            command: SchedulerCommand::CancelRequest {
167                request_id: cancellation.request_id,
168            },
169            reply: cancellation.reply,
170        }
171    }
172}
173
174/// Engine-agnostic asynchronous scheduler interface retained for Dynamo.
175pub trait SchedulerHandle: Send + Sync {
176    /// Send a request to the scheduler's waiting queue.
177    fn receive(&self, request: DirectRequest);
178
179    /// Get a clone of the compatibility request sender channel.
180    fn request_sender(&self) -> mpsc::UnboundedSender<DirectRequest>;
181
182    fn metrics_receiver(&self) -> tokio::sync::watch::Receiver<MockerMetrics>;
183
184    fn command_sender(&self) -> mpsc::Sender<SchedulerCommandEnvelope>;
185
186    fn cancellation_sender(&self) -> mpsc::Sender<SchedulerCancellationEnvelope>;
187
188    fn take_lifecycle_receiver(&mut self) -> Option<mpsc::Receiver<SchedulerLifecycleEvent>>;
189}
190
191pub(crate) fn handoff_channel_capacity(args: &crate::common::protocols::MockEngineArgs) -> usize {
192    args.effective_handoff_capacity()
193        .checked_mul(2)
194        .expect("mocker handoff channel capacity overflow")
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[tokio::test]
202    async fn ordered_output_send_waits_for_route_delivery_ack() {
203        let (tx, mut rx) = mpsc::channel(1);
204        let sender = SchedulerEventSender::Ordered {
205            tx,
206            forward_admissions: false,
207            cancel: CancellationToken::new(),
208        };
209        let send = tokio::spawn(async move {
210            sender
211                .send_outputs(vec![OutputSignal {
212                    uuid: Uuid::from_u128(1),
213                    token_id: Some(2),
214                    completed: true,
215                    rejected: false,
216                    handoff_delay_ms: None,
217                    cached_tokens: None,
218                }])
219                .await
220        });
221
222        let Some(LiveEngineEvent::Outputs { signals, delivered }) = rx.recv().await else {
223            panic!("expected an ordered output batch");
224        };
225        assert_eq!(signals.len(), 1);
226        tokio::task::yield_now().await;
227        assert!(
228            !send.is_finished(),
229            "enqueueing the output must not acknowledge route delivery"
230        );
231
232        delivered.send(Vec::new()).unwrap();
233        send.await.unwrap().unwrap();
234    }
235
236    #[tokio::test]
237    async fn dropped_ordered_output_ack_is_orderly_after_cancellation() {
238        let (tx, mut rx) = mpsc::channel(1);
239        let cancel = CancellationToken::new();
240        let sender = SchedulerEventSender::Ordered {
241            tx,
242            forward_admissions: false,
243            cancel: cancel.clone(),
244        };
245        let send = tokio::spawn(async move {
246            sender
247                .send_outputs(vec![OutputSignal {
248                    uuid: Uuid::from_u128(2),
249                    token_id: Some(3),
250                    completed: true,
251                    rejected: false,
252                    handoff_delay_ms: None,
253                    cached_tokens: None,
254                }])
255                .await
256        });
257
258        let Some(LiveEngineEvent::Outputs { delivered, .. }) = rx.recv().await else {
259            panic!("expected an ordered output batch");
260        };
261        cancel.cancel();
262        drop(delivered);
263        assert!(matches!(
264            send.await.unwrap(),
265            Err(SchedulerEventSendError::Cancelled)
266        ));
267    }
268
269    #[tokio::test]
270    async fn dropped_ordered_output_ack_without_cancellation_is_an_error() {
271        let (tx, mut rx) = mpsc::channel(1);
272        let sender = SchedulerEventSender::Ordered {
273            tx,
274            forward_admissions: false,
275            cancel: CancellationToken::new(),
276        };
277        let send = tokio::spawn(async move {
278            sender
279                .send_outputs(vec![OutputSignal {
280                    uuid: Uuid::from_u128(3),
281                    token_id: Some(4),
282                    completed: true,
283                    rejected: false,
284                    handoff_delay_ms: None,
285                    cached_tokens: None,
286                }])
287                .await
288        });
289
290        let Some(LiveEngineEvent::Outputs { delivered, .. }) = rx.recv().await else {
291            panic!("expected an ordered output batch");
292        };
293        drop(delivered);
294        assert!(matches!(
295            send.await.unwrap(),
296            Err(SchedulerEventSendError::OrderedLaneClosed)
297        ));
298    }
299}