1mod kv_event_sink;
7mod live_boundary;
8#[path = "sglang/mod.rs"]
9pub mod sglang;
10mod source_holds;
11pub mod vllm;
12
13pub use crate::common::protocols::ForwardPassSnapshot;
14use crate::common::protocols::{DirectRequest, OutputSignal};
15use dynamo_kv_router::protocols::RouterEvent;
16pub(crate) use kv_event_sink::{CapturedRouterEventBuffer, capture_router_event_sink};
17pub(crate) use live_boundary::{
18 LiveBoundaryCore, LivePassExecution, LiveSchedulerState, spawn_live_scheduler,
19};
20pub(crate) use source_holds::{
21 ActiveHandoffRequests, DestinationHolds, PendingDestinations, RemovedSource, SourceCompletion,
22 SourceHolds,
23};
24pub use source_holds::{
25 SchedulerCommand, SchedulerCommandEffects, SchedulerCommandResult, SchedulerLifecycleEvent,
26};
27use tokio::sync::{mpsc, oneshot};
28use uuid::Uuid;
29
30#[cfg(feature = "kvbm-offload")]
31pub(crate) struct OffloadTickEffects {
32 pub kv_events: Vec<RouterEvent>,
33 pub lifecycle_events: Vec<SchedulerLifecycleEvent>,
34}
35
36#[derive(Default)]
40pub(crate) struct WelfordAcc {
41 pub(crate) count: u32,
42 pub(crate) sum: f64,
43 mean: f64,
44 m2: f64,
45}
46
47impl WelfordAcc {
48 pub(crate) fn add(&mut self, v: f64) {
49 self.count += 1;
50 self.sum += v;
51 let delta = v - self.mean;
52 self.mean += delta / self.count as f64;
53 let delta2 = v - self.mean;
54 self.m2 += delta * delta2;
55 }
56
57 pub(crate) fn variance(&self) -> f64 {
58 if self.count == 0 {
59 return 0.0;
60 }
61 self.m2 / self.count as f64
62 }
63}
64
65pub(crate) fn build_fpm_snapshot(
75 scheduled_prefills: impl Iterator<Item = (u64, u64, u64)>,
76 scheduled_decodes: impl Iterator<Item = u64>,
77 queued_prefills: impl Iterator<Item = u64>,
78 queued_decodes: impl Iterator<Item = u64>,
79 wall_time_secs: f64,
80) -> ForwardPassSnapshot {
81 let mut prefill_acc = WelfordAcc::default();
82 let mut decode_acc = WelfordAcc::default();
83 let mut sum_prefill_tokens: u64 = 0;
84 let mut sum_prefill_kv_tokens: u64 = 0;
85
86 for (prompt_len, prefix_tokens, tokens_computed) in scheduled_prefills {
87 sum_prefill_tokens += tokens_computed;
88 sum_prefill_kv_tokens += prefix_tokens;
89 prefill_acc.add(prompt_len as f64);
90 }
91
92 for sequence_len in scheduled_decodes {
93 decode_acc.add(sequence_len as f64);
94 }
95
96 let mut queued_prefill_acc = WelfordAcc::default();
97 let mut queued_decode_acc = WelfordAcc::default();
98
99 for prompt_len in queued_prefills {
100 queued_prefill_acc.add(prompt_len as f64);
101 }
102
103 for kv_tokens in queued_decodes {
104 queued_decode_acc.add(kv_tokens as f64);
105 }
106
107 ForwardPassSnapshot {
108 num_prefill_requests: prefill_acc.count,
109 sum_prefill_tokens,
110 var_prefill_length: prefill_acc.variance(),
111 sum_prefill_kv_tokens,
112 num_decode_requests: decode_acc.count,
113 sum_decode_kv_tokens: decode_acc.sum as u64,
114 var_decode_kv_tokens: decode_acc.variance(),
115 num_queued_prefill: queued_prefill_acc.count,
116 sum_queued_prefill_tokens: queued_prefill_acc.sum as u64,
117 var_queued_prefill_length: queued_prefill_acc.variance(),
118 num_queued_decode: queued_decode_acc.count,
119 sum_queued_decode_kv_tokens: queued_decode_acc.sum as u64,
120 var_queued_decode_kv_tokens: queued_decode_acc.variance(),
121 wall_time_secs,
122 ..Default::default()
123 }
124}
125
126pub(crate) fn accept_length_sample(output_signals: &[OutputSignal]) -> (usize, usize) {
130 let visible_tokens = output_signals
131 .iter()
132 .filter(|signal| !signal.rejected && signal.token_id.is_some())
133 .count();
134 if visible_tokens == 0 {
135 return (0, 0);
136 }
137
138 let request_forwards = output_signals
139 .iter()
140 .filter(|signal| !signal.rejected && signal.token_id.is_some())
141 .map(|signal| signal.uuid)
142 .collect::<std::collections::HashSet<_>>()
143 .len();
144 (visible_tokens, request_forwards)
145}
146
147pub(crate) use sglang::SglangCore;
148pub use sglang::SglangScheduler;
149pub(crate) use vllm::VllmCore;
150pub use vllm::{MockerMetrics, Scheduler};
151
152#[derive(Debug, Clone)]
153pub(crate) struct AdmissionEvent {
154 pub(crate) uuid: Uuid,
155 pub(crate) reused_input_tokens: usize,
156}
157
158#[derive(Debug, Clone)]
159pub(crate) struct EnginePassResult {
160 pub(crate) end_ms: f64,
161 pub(crate) completed_requests: usize,
162 pub(crate) output_signals: Vec<OutputSignal>,
163 pub(crate) admissions: Vec<AdmissionEvent>,
164 pub(crate) lifecycle_events: Vec<SchedulerLifecycleEvent>,
165 pub(crate) mocker_metrics: MockerMetrics,
166 pub(crate) router_event_visibility: RouterEventVisibility,
169 pub(crate) kv_events: Vec<RouterEvent>,
171 pub(crate) fpm: Option<ForwardPassSnapshot>,
173 pub(crate) accept_length_output_tokens: usize,
175 pub(crate) accept_length_decode_forwards: usize,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub(crate) enum RouterEventVisibility {
181 PassStart,
183 PassEnd,
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub(crate) enum AdmissionStage {
189 Materialized,
190 PendingDestinationHead,
191 FreshKv,
192}
193
194#[derive(Debug, Clone, Copy)]
195pub(crate) struct AdmissionInvariant {
196 pending_destination: bool,
197}
198
199impl AdmissionInvariant {
200 pub(crate) fn new(pending_destination: bool) -> Self {
201 Self {
202 pending_destination,
203 }
204 }
205
206 pub(crate) fn stage_for(self, materialized: bool) -> AdmissionStage {
207 if materialized {
208 AdmissionStage::Materialized
209 } else if self.pending_destination {
210 AdmissionStage::PendingDestinationHead
211 } else {
212 AdmissionStage::FreshKv
213 }
214 }
215}
216
217#[allow(clippy::large_enum_variant)]
218pub(crate) enum EngineCore {
219 Vllm(VllmCore),
220 Sglang(SglangCore),
221}
222
223impl EngineCore {
224 pub(crate) fn receive(&mut self, request: DirectRequest) -> Uuid {
225 match self {
226 Self::Vllm(core) => core.receive(request),
227 Self::Sglang(core) => core.receive(request),
228 }
229 }
230
231 pub(crate) fn is_empty(&self) -> bool {
232 match self {
233 Self::Vllm(core) => core.is_empty(),
234 Self::Sglang(core) => core.is_empty(),
235 }
236 }
237
238 #[allow(dead_code)]
239 pub(crate) fn is_drained(&self) -> bool {
240 match self {
241 Self::Vllm(core) => core.is_drained(),
242 Self::Sglang(core) => core.is_drained(),
243 }
244 }
245
246 #[allow(dead_code)]
247 pub(crate) fn apply_command(
248 &mut self,
249 command: SchedulerCommand,
250 ) -> anyhow::Result<SchedulerCommandResult> {
251 match self {
252 Self::Vllm(core) => core.apply_command(command),
253 Self::Sglang(core) => core.apply_command(command),
254 }
255 }
256
257 pub(crate) fn apply_command_effects(
258 &mut self,
259 command: SchedulerCommand,
260 allow_destination_admission: bool,
261 ) -> anyhow::Result<SchedulerCommandEffects> {
262 match self {
263 Self::Vllm(core) => core.apply_command_effects(command, allow_destination_admission),
264 Self::Sglang(core) => core.apply_command_effects(command, allow_destination_admission),
265 }
266 }
267
268 pub(crate) fn retry_pending_destinations(&mut self) -> Vec<SchedulerLifecycleEvent> {
269 match self {
270 Self::Vllm(core) => core.retry_pending_destinations(),
271 Self::Sglang(core) => core.retry_pending_destinations(),
272 }
273 }
274
275 pub(crate) fn drain_kv_events(&self) -> Vec<dynamo_kv_router::protocols::RouterEvent> {
276 match self {
277 Self::Vllm(core) => core.drain_kv_events(),
278 Self::Sglang(core) => core.drain_kv_events(),
279 }
280 }
281
282 pub(crate) fn num_requests(&self) -> usize {
283 match self {
284 Self::Vllm(core) => core.num_requests(),
285 Self::Sglang(core) => core.num_requests(),
286 }
287 }
288
289 pub(crate) fn try_execute_pass(
290 &mut self,
291 collector: &mut crate::replay::TraceCollector,
292 now_ms: f64,
293 ) -> anyhow::Result<EnginePassResult> {
294 match self {
295 Self::Vllm(core) => core.try_execute_pass(collector, now_ms),
296 Self::Sglang(core) => core.try_execute_pass(collector, now_ms),
297 }
298 }
299
300 #[cfg(test)]
301 pub(crate) fn execute_hidden_pass(&mut self, now_ms: f64) -> EnginePassResult {
302 self.try_execute_hidden_pass(now_ms)
303 .expect("engine hidden scheduler pass failed")
304 }
305
306 pub(crate) fn try_execute_hidden_pass(
307 &mut self,
308 now_ms: f64,
309 ) -> anyhow::Result<EnginePassResult> {
310 match self {
311 Self::Vllm(core) => core.try_execute_hidden_pass(now_ms),
312 Self::Sglang(core) => core.try_execute_hidden_pass(now_ms),
313 }
314 }
315
316 #[cfg(feature = "kvbm-offload")]
317 pub(crate) fn tick_offload_only(&mut self, now_ms: f64) -> OffloadTickEffects {
318 match self {
319 Self::Vllm(core) => core.tick_offload_only(now_ms),
320 Self::Sglang(_) => OffloadTickEffects {
321 kv_events: Vec::new(),
322 lifecycle_events: Vec::new(),
323 },
324 }
325 }
326
327 #[cfg(feature = "kvbm-offload")]
328 pub(crate) fn tick_offload_transport_only(&mut self, now_ms: f64) -> OffloadTickEffects {
329 match self {
330 Self::Vllm(core) => core.tick_offload_transport_only(now_ms),
331 Self::Sglang(_) => OffloadTickEffects {
332 kv_events: Vec::new(),
333 lifecycle_events: Vec::new(),
334 },
335 }
336 }
337
338 #[cfg(feature = "kvbm-offload")]
339 pub(crate) fn earliest_offload_deadline(&self) -> Option<f64> {
340 match self {
341 Self::Vllm(core) => core.earliest_offload_deadline(),
342 Self::Sglang(_) => None,
343 }
344 }
345}
346
347pub struct SchedulerCommandEnvelope {
348 pub command: SchedulerCommand,
349 pub reply: oneshot::Sender<anyhow::Result<SchedulerCommandEffects>>,
350}
351
352#[derive(Clone)]
355pub(crate) enum SchedulerOutputSender {
356 Unbounded(mpsc::UnboundedSender<Vec<OutputSignal>>),
357}
358
359impl SchedulerOutputSender {
360 pub(crate) async fn send(&self, signals: Vec<OutputSignal>) -> Result<(), Vec<OutputSignal>> {
361 match self {
362 Self::Unbounded(tx) => tx.send(signals).map_err(|error| error.0),
363 }
364 }
365}
366
367impl From<mpsc::UnboundedSender<Vec<OutputSignal>>> for SchedulerOutputSender {
368 fn from(tx: mpsc::UnboundedSender<Vec<OutputSignal>>) -> Self {
369 Self::Unbounded(tx)
370 }
371}
372
373#[derive(Debug)]
374pub(crate) enum LiveEngineEvent {
375 Admissions(Vec<AdmissionEvent>),
376 Outputs(Vec<OutputSignal>),
377}
378
379#[derive(Clone)]
380pub(crate) enum SchedulerEventSender {
381 Outputs(SchedulerOutputSender),
382 Ordered(mpsc::Sender<LiveEngineEvent>),
383}
384
385pub(crate) enum SchedulerEventSendError {
386 OutputClosed(Vec<OutputSignal>),
387 OrderedLaneClosed,
388}
389
390impl SchedulerEventSender {
391 pub(crate) async fn send_admissions(
392 &self,
393 admissions: &[AdmissionEvent],
394 ) -> Result<(), SchedulerEventSendError> {
395 if admissions.is_empty() {
396 return Ok(());
397 }
398 match self {
399 Self::Outputs(_) => {
400 Ok(())
402 }
403 Self::Ordered(tx) => tx
404 .send(LiveEngineEvent::Admissions(admissions.to_vec()))
405 .await
406 .map_err(|_| SchedulerEventSendError::OrderedLaneClosed),
407 }
408 }
409
410 pub(crate) async fn send_outputs(
411 &self,
412 signals: Vec<OutputSignal>,
413 ) -> Result<(), SchedulerEventSendError> {
414 match self {
415 Self::Outputs(tx) => tx
416 .send(signals)
417 .await
418 .map_err(SchedulerEventSendError::OutputClosed),
419 Self::Ordered(tx) => tx
420 .send(LiveEngineEvent::Outputs(signals))
421 .await
422 .map_err(|_| SchedulerEventSendError::OrderedLaneClosed),
423 }
424 }
425}
426
427impl From<SchedulerOutputSender> for SchedulerEventSender {
428 fn from(tx: SchedulerOutputSender) -> Self {
429 Self::Outputs(tx)
430 }
431}
432
433pub struct SchedulerCancellationEnvelope {
434 pub request_id: Uuid,
435 pub discard_pending_output: bool,
436 pub reply: oneshot::Sender<anyhow::Result<SchedulerCommandEffects>>,
437}
438
439impl From<SchedulerCancellationEnvelope> for SchedulerCommandEnvelope {
440 fn from(cancellation: SchedulerCancellationEnvelope) -> Self {
441 Self {
442 command: SchedulerCommand::CancelRequest {
443 request_id: cancellation.request_id,
444 },
445 reply: cancellation.reply,
446 }
447 }
448}
449
450pub trait SchedulerHandle: Send + Sync {
455 fn receive(&self, request: DirectRequest);
457
458 fn request_sender(&self) -> mpsc::UnboundedSender<DirectRequest>;
460
461 fn metrics_receiver(&self) -> tokio::sync::watch::Receiver<MockerMetrics>;
463
464 fn command_sender(&self) -> mpsc::Sender<SchedulerCommandEnvelope>;
466
467 fn cancellation_sender(&self) -> mpsc::Sender<SchedulerCancellationEnvelope>;
473
474 fn take_lifecycle_receiver(&mut self) -> Option<mpsc::Receiver<SchedulerLifecycleEvent>>;
476}
477
478pub(crate) fn handoff_channel_capacity(args: &crate::common::protocols::MockEngineArgs) -> usize {
479 args.effective_handoff_capacity()
480 .checked_mul(2)
481 .expect("mocker handoff channel capacity overflow")
482}
483
484#[cfg(feature = "kvbm-offload")]
489pub async fn init_kvbm_live(
490 args: &crate::common::protocols::MockEngineArgs,
491 kv_manager: &mut crate::kv_manager::G1Manager,
492) -> anyhow::Result<Option<std::sync::Arc<std::sync::Mutex<crate::kvbm_offload::MockOffloadEngine>>>>
493{
494 use crate::kvbm_offload::KvbmOffloadConfig;
495 let Some(config) = KvbmOffloadConfig::from_args(args)? else {
496 return Ok(None);
497 };
498 let engine = std::thread::spawn(move || build_owned_offload_engine(config))
499 .join()
500 .map_err(|_| anyhow::anyhow!("kvbm-offload live init thread panicked"))??;
501 Ok(Some(kv_manager.attach_new_offload_engine(engine)))
502}
503
504#[cfg(feature = "kvbm-offload")]
508pub fn init_kvbm_offline(
509 args: &crate::common::protocols::MockEngineArgs,
510 kv_manager: &mut crate::kv_manager::G1Manager,
511) -> anyhow::Result<Option<std::sync::Arc<std::sync::Mutex<crate::kvbm_offload::MockOffloadEngine>>>>
512{
513 use crate::kvbm_offload::KvbmOffloadConfig;
514 let Some(config) = KvbmOffloadConfig::from_args(args)? else {
515 return Ok(None);
516 };
517 tracing::debug!(
518 num_g2_blocks = config.num_g2_blocks,
519 num_g3_blocks = config.num_g3_blocks,
520 g4_enabled = config.enable_g4_storage,
521 offload_batch_size = config.offload_batch_size,
522 bw_g1_to_g2_gbps = config.bandwidth_g1_to_g2_gbps,
523 bw_g2_to_g1_gbps = config.bandwidth_g2_to_g1_gbps,
524 bw_g2_to_g3_gbps = config.bandwidth_g2_to_g3_gbps,
525 bw_g3_to_g2_gbps = config.bandwidth_g3_to_g2_gbps,
526 bw_g2_to_g4_gbps = config.bandwidth_g2_to_g4_gbps,
527 bw_g4_to_g2_gbps = config.bandwidth_g4_to_g2_gbps,
528 "kvbm-offload: init_kvbm_offline attaching engine"
529 );
530 let engine = build_owned_offload_engine(config)?;
531 Ok(Some(kv_manager.attach_new_offload_engine(engine)))
532}
533
534#[cfg(feature = "kvbm-offload")]
540fn build_owned_offload_engine(
541 config: crate::kvbm_offload::KvbmOffloadConfig,
542) -> anyhow::Result<crate::kvbm_offload::MockOffloadEngine> {
543 let rt = tokio::runtime::Builder::new_multi_thread()
544 .worker_threads(1)
545 .enable_all()
546 .build()?;
547 let mut engine = rt.block_on(crate::kvbm_offload::MockOffloadEngine::new(config))?;
548 engine.attach_runtime(rt);
549 Ok(engine)
550}
551
552#[cfg(test)]
554pub(crate) mod test_utils;
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::common::handoff::HandoffId;
560 use crate::common::protocols::{EngineType, MockEngineArgs, WorkerType};
561
562 fn core(engine_type: EngineType, worker_type: WorkerType, blocks: usize) -> EngineCore {
563 let args = MockEngineArgs::builder()
564 .engine_type(engine_type)
565 .block_size(4)
566 .num_gpu_blocks(blocks)
567 .max_num_batched_tokens(Some(16))
568 .max_num_seqs(Some(1))
569 .enable_prefix_caching(true)
570 .worker_type(worker_type)
571 .speedup_ratio(0.0)
572 .build()
573 .unwrap();
574 match engine_type {
575 EngineType::Vllm | EngineType::Trtllm => EngineCore::Vllm(VllmCore::new(args)),
576 EngineType::Sglang => EngineCore::Sglang(SglangCore::new(args)),
577 }
578 }
579
580 fn request(uuid: Uuid, tokens: Vec<u32>) -> DirectRequest {
581 DirectRequest {
582 tokens,
583 max_output_tokens: 2,
584 uuid: Some(uuid),
585 dp_rank: 0,
586 arrival_timestamp_ms: None,
587 ..Default::default()
588 }
589 }
590
591 fn destination_reservation_attempts(core: &EngineCore) -> usize {
592 match core {
593 EngineCore::Vllm(core) => core.destination_reservation_attempts(),
594 EngineCore::Sglang(core) => core.destination_reservation_attempts(),
595 }
596 }
597
598 fn request_metrics(core: &EngineCore) -> MockerMetrics {
599 match core {
600 EngineCore::Vllm(core) => core.mocker_metrics(),
601 EngineCore::Sglang(core) => core.mocker_metrics(),
602 }
603 }
604
605 #[test]
606 fn request_cancellation_removes_waiting_and_running_requests_for_each_engine() {
607 for (case, engine_type) in [EngineType::Vllm, EngineType::Sglang]
608 .into_iter()
609 .enumerate()
610 {
611 let mut core = core(engine_type, WorkerType::Aggregated, 16);
612 let waiting_id = Uuid::from_u128(20_000 + case as u128);
613 core.receive(request(waiting_id, (0..4).collect()));
614 assert_eq!(request_metrics(&core).waiting_requests, 1);
615 assert_eq!(
616 core.apply_command(SchedulerCommand::CancelRequest {
617 request_id: waiting_id,
618 })
619 .unwrap(),
620 SchedulerCommandResult::Applied
621 );
622 assert_eq!(
623 core.apply_command(SchedulerCommand::CancelRequest {
624 request_id: waiting_id,
625 })
626 .unwrap(),
627 SchedulerCommandResult::Noop
628 );
629 assert_eq!(core.num_requests(), 0);
630
631 let running_id = Uuid::from_u128(20_100 + case as u128);
632 let mut running_request = request(running_id, (100..108).collect());
633 running_request.max_output_tokens = 32;
634 core.receive(running_request);
635 core.execute_hidden_pass(0.0);
636 assert_eq!(request_metrics(&core).running_requests, 1);
637 let active_blocks_before_cancel = request_metrics(&core).active_decode_blocks;
638 assert!(
639 active_blocks_before_cancel > 0,
640 "{engine_type:?} running request should own KV blocks"
641 );
642 assert_eq!(
643 core.apply_command(SchedulerCommand::CancelRequest {
644 request_id: running_id,
645 })
646 .unwrap(),
647 SchedulerCommandResult::Applied
648 );
649 assert_eq!(core.num_requests(), 0);
650 let active_blocks_after_cancel = request_metrics(&core).active_decode_blocks;
651 assert!(
652 active_blocks_after_cancel < active_blocks_before_cancel,
653 "{engine_type:?} cancellation should release request-owned KV blocks"
654 );
655 if engine_type == EngineType::Vllm {
656 assert_eq!(active_blocks_after_cancel, 0);
657 }
658 }
659 }
660 #[test]
661 fn welford_acc_empty() {
662 let acc = WelfordAcc::default();
663 assert_eq!(acc.count, 0);
664 assert_eq!(acc.sum, 0.0);
665 assert_eq!(acc.variance(), 0.0);
666 }
667
668 #[test]
669 fn accept_length_ignores_terminal_signals_without_tokens() {
670 let token_uuid = Uuid::from_u128(1);
671 let signals = [
672 OutputSignal {
673 uuid: Uuid::from_u128(2),
674 token_id: None,
675 completed: true,
676 rejected: false,
677 handoff_delay_ms: None,
678 },
679 OutputSignal {
680 uuid: token_uuid,
681 token_id: Some(7),
682 completed: false,
683 rejected: false,
684 handoff_delay_ms: None,
685 },
686 OutputSignal {
687 uuid: token_uuid,
688 token_id: Some(8),
689 completed: true,
690 rejected: false,
691 handoff_delay_ms: None,
692 },
693 OutputSignal {
694 uuid: Uuid::from_u128(3),
695 token_id: Some(9),
696 completed: true,
697 rejected: true,
698 handoff_delay_ms: None,
699 },
700 ];
701
702 assert_eq!(accept_length_sample(&signals), (2, 1));
703 }
704
705 #[test]
706 fn welford_acc_single_value() {
707 let mut acc = WelfordAcc::default();
708 acc.add(42.0);
709 assert_eq!(acc.count, 1);
710 assert_eq!(acc.sum, 42.0);
711 assert_eq!(acc.variance(), 0.0);
712 }
713
714 #[test]
715 fn welford_acc_population_variance() {
716 let mut acc = WelfordAcc::default();
717 for v in [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
720 acc.add(v);
721 }
722 assert_eq!(acc.count, 8);
723 assert_eq!(acc.sum, 40.0);
724 assert!((acc.variance() - 4.0).abs() < 1e-10);
725 }
726
727 #[test]
728 fn welford_acc_matches_python() {
729 let mut acc = WelfordAcc::default();
734 acc.add(100.0);
735 acc.add(200.0);
736 acc.add(300.0);
737 assert_eq!(acc.count, 3);
738 assert_eq!(acc.sum, 600.0);
739 let expected = 20000.0 / 3.0;
740 assert!(
741 (acc.variance() - expected).abs() < 1e-10,
742 "expected {expected}, got {}",
743 acc.variance()
744 );
745 }
746
747 #[test]
748 fn unavailable_destination_keeps_source_held_until_both_owners_are_cancelled() {
749 for (case, engine_type) in [EngineType::Vllm, EngineType::Sglang]
750 .into_iter()
751 .enumerate()
752 {
753 let mut source = core(engine_type, WorkerType::Prefill, 8);
754 let mut destination = core(engine_type, WorkerType::Decode, 2);
755 let held_handoff = HandoffId::from(Uuid::from_u128(30_000 + case as u128));
756 let capacity_handoff = HandoffId::from(Uuid::from_u128(30_100 + case as u128));
757 let request_id = Uuid::from_u128(30_200 + case as u128);
758
759 assert!(matches!(
760 destination
761 .apply_command(SchedulerCommand::ReserveDestination {
762 handoff_id: capacity_handoff,
763 request: request(
764 Uuid::from_u128(30_300 + case as u128),
765 (100..108).collect(),
766 ),
767 })
768 .unwrap(),
769 SchedulerCommandResult::DestinationAccepted { .. }
770 ));
771 source
772 .apply_command(SchedulerCommand::SubmitHandoffPrefill {
773 handoff_id: held_handoff,
774 request: request(request_id, (0..8).collect()),
775 })
776 .unwrap();
777 let mut now_ms = 0.0;
778 for _ in 0..8 {
779 let pass = source.execute_hidden_pass(now_ms);
780 now_ms = pass.end_ms;
781 if source.is_empty() {
782 break;
783 }
784 }
785 assert!(source.is_empty());
786 assert!(!source.is_drained());
787
788 assert_eq!(
789 destination
790 .apply_command(SchedulerCommand::ReserveDestination {
791 handoff_id: held_handoff,
792 request: request(request_id, (0..4).collect()),
793 })
794 .unwrap(),
795 SchedulerCommandResult::DestinationAccepted { request_id }
796 );
797 assert_eq!(
798 destination
799 .apply_command(SchedulerCommand::CancelDestination {
800 handoff_id: held_handoff,
801 })
802 .unwrap(),
803 SchedulerCommandResult::Applied
804 );
805 assert_eq!(
806 destination
807 .apply_command(SchedulerCommand::CancelDestination {
808 handoff_id: capacity_handoff,
809 })
810 .unwrap(),
811 SchedulerCommandResult::Applied
812 );
813 assert_eq!(
814 source
815 .apply_command(SchedulerCommand::CancelSource {
816 handoff_id: held_handoff,
817 })
818 .unwrap(),
819 SchedulerCommandResult::Applied
820 );
821 assert!(source.is_empty());
822 assert!(source.is_drained());
823 assert!(destination.is_empty());
824 assert!(destination.is_drained());
825 }
826 }
827
828 #[test]
829 fn destination_cancellation_retries_the_blocked_fifo_head() {
830 for (case, engine_type) in [EngineType::Vllm, EngineType::Sglang]
831 .into_iter()
832 .enumerate()
833 {
834 let mut destination = core(engine_type, WorkerType::Decode, 2);
835 let first_handoff = HandoffId::from(Uuid::from_u128(35_000 + case as u128));
836 let second_handoff = HandoffId::from(Uuid::from_u128(35_100 + case as u128));
837 let second_request = Uuid::from_u128(35_200 + case as u128);
838
839 let first = destination
840 .apply_command_effects(
841 SchedulerCommand::ReserveDestination {
842 handoff_id: first_handoff,
843 request: request(
844 Uuid::from_u128(35_300 + case as u128),
845 (100..108).collect(),
846 ),
847 },
848 true,
849 )
850 .unwrap();
851 assert!(matches!(
852 first.lifecycle_events.as_slice(),
853 [SchedulerLifecycleEvent::DestinationReserved {
854 handoff_id,
855 ..
856 }] if *handoff_id == first_handoff
857 ));
858
859 let second = destination
860 .apply_command_effects(
861 SchedulerCommand::ReserveDestination {
862 handoff_id: second_handoff,
863 request: request(second_request, (200..204).collect()),
864 },
865 true,
866 )
867 .unwrap();
868 assert!(second.lifecycle_events.is_empty());
869
870 let canceled = destination
871 .apply_command_effects(
872 SchedulerCommand::CancelDestination {
873 handoff_id: first_handoff,
874 },
875 true,
876 )
877 .unwrap();
878 assert_eq!(canceled.result, SchedulerCommandResult::Applied);
879 assert!(matches!(
880 canceled.lifecycle_events.as_slice(),
881 [SchedulerLifecycleEvent::DestinationReserved {
882 handoff_id,
883 request_id,
884 ..
885 }] if *handoff_id == second_handoff && *request_id == second_request
886 ));
887 }
888 }
889
890 #[test]
891 fn blocked_destination_head_prevents_fresh_kv_admission_without_spinning() {
892 for (case, engine_type) in [EngineType::Vllm, EngineType::Sglang]
893 .into_iter()
894 .enumerate()
895 {
896 let mut destination = core(engine_type, WorkerType::Decode, 4);
897 let owner_handoff = HandoffId::from(Uuid::from_u128(36_000 + case as u128));
898 let blocked_handoff = HandoffId::from(Uuid::from_u128(36_100 + case as u128));
899 let owner_request = Uuid::from_u128(36_200 + case as u128);
900 let fresh_request = Uuid::from_u128(36_400 + case as u128);
901
902 let owner = destination
903 .apply_command_effects(
904 SchedulerCommand::ReserveDestination {
905 handoff_id: owner_handoff,
906 request: request(owner_request, (100..108).collect()),
907 },
908 true,
909 )
910 .unwrap();
911 assert_eq!(owner.lifecycle_events.len(), 1);
912 let occupied_before = match &destination {
913 EngineCore::Vllm(core) => core.mocker_metrics().active_decode_blocks,
914 EngineCore::Sglang(core) => core.mocker_metrics().active_decode_blocks,
915 };
916 assert!(occupied_before > 0);
917
918 let blocked = destination
919 .apply_command_effects(
920 SchedulerCommand::ReserveDestination {
921 handoff_id: blocked_handoff,
922 request: request(
923 Uuid::from_u128(36_300 + case as u128),
924 (200..212).collect(),
925 ),
926 },
927 true,
928 )
929 .unwrap();
930 assert!(blocked.lifecycle_events.is_empty());
931 assert!(destination.is_empty());
932 assert!(!destination.is_drained());
933 let pending_only = destination.execute_hidden_pass(0.0);
934 assert_eq!(pending_only.end_ms, 0.0);
935 assert!(pending_only.admissions.is_empty());
936 assert!(pending_only.output_signals.is_empty());
937
938 destination.receive(request(fresh_request, (300..304).collect()));
939 let pass = destination.execute_hidden_pass(0.0);
940 assert!(pass.admissions.is_empty());
941 assert!(pass.output_signals.is_empty());
942 assert_eq!(pass.end_ms, 0.0);
943 assert_eq!(destination.num_requests(), 1);
944 let occupied_after = match &destination {
945 EngineCore::Vllm(core) => core.mocker_metrics().active_decode_blocks,
946 EngineCore::Sglang(core) => core.mocker_metrics().active_decode_blocks,
947 };
948 assert_eq!(occupied_after, occupied_before);
949
950 assert_eq!(
951 destination
952 .apply_command(SchedulerCommand::ActivateDestination {
953 handoff_id: owner_handoff,
954 })
955 .unwrap(),
956 SchedulerCommandResult::Applied
957 );
958 let materialized = destination.execute_hidden_pass(0.0);
959 assert!(
960 materialized
961 .admissions
962 .iter()
963 .any(|admission| admission.uuid == owner_request)
964 );
965 assert!(
966 materialized
967 .admissions
968 .iter()
969 .all(|admission| admission.uuid != fresh_request)
970 );
971
972 assert_eq!(
973 destination
974 .apply_command(SchedulerCommand::CancelDestination {
975 handoff_id: blocked_handoff,
976 })
977 .unwrap(),
978 SchedulerCommandResult::Applied
979 );
980 assert_eq!(
981 destination
982 .apply_command(SchedulerCommand::CancelDestination {
983 handoff_id: owner_handoff,
984 })
985 .unwrap(),
986 SchedulerCommandResult::Applied
987 );
988 let fresh = destination.execute_hidden_pass(1.0);
989 assert!(
990 fresh
991 .admissions
992 .iter()
993 .any(|admission| admission.uuid == fresh_request)
994 );
995 }
996 }
997
998 #[test]
999 fn unchanged_capacity_generation_does_not_reprobe_pending_destination() {
1000 for (case, engine_type) in [EngineType::Vllm, EngineType::Sglang]
1001 .into_iter()
1002 .enumerate()
1003 {
1004 let mut destination = core(engine_type, WorkerType::Decode, 2);
1005 let owner_handoff = HandoffId::from(Uuid::from_u128(36_500 + case as u128));
1006 let pending_handoff = HandoffId::from(Uuid::from_u128(36_600 + case as u128));
1007 let pending_request = Uuid::from_u128(36_700 + case as u128);
1008
1009 let owner = destination
1010 .apply_command_effects(
1011 SchedulerCommand::ReserveDestination {
1012 handoff_id: owner_handoff,
1013 request: request(Uuid::from_u128(36_800 + case as u128), (0..8).collect()),
1014 },
1015 true,
1016 )
1017 .unwrap();
1018 assert_eq!(owner.lifecycle_events.len(), 1);
1019 let pending = destination
1020 .apply_command_effects(
1021 SchedulerCommand::ReserveDestination {
1022 handoff_id: pending_handoff,
1023 request: request(pending_request, (100..104).collect()),
1024 },
1025 true,
1026 )
1027 .unwrap();
1028 assert!(pending.lifecycle_events.is_empty());
1029
1030 let attempts_after_initial_failure = destination_reservation_attempts(&destination);
1031 for _ in 0..3 {
1032 assert!(destination.retry_pending_destinations().is_empty());
1033 }
1034 assert_eq!(
1035 destination_reservation_attempts(&destination),
1036 attempts_after_initial_failure
1037 );
1038
1039 let cancellation = destination
1040 .apply_command_effects(
1041 SchedulerCommand::CancelDestination {
1042 handoff_id: owner_handoff,
1043 },
1044 true,
1045 )
1046 .unwrap();
1047 assert!(matches!(
1048 cancellation.lifecycle_events.as_slice(),
1049 [SchedulerLifecycleEvent::DestinationReserved {
1050 handoff_id,
1051 request_id,
1052 ..
1053 }] if *handoff_id == pending_handoff && *request_id == pending_request
1054 ));
1055 assert_eq!(
1056 destination_reservation_attempts(&destination),
1057 attempts_after_initial_failure + 1
1058 );
1059 assert_eq!(
1060 destination
1061 .apply_command(SchedulerCommand::CancelDestination {
1062 handoff_id: pending_handoff,
1063 })
1064 .unwrap(),
1065 SchedulerCommandResult::Applied
1066 );
1067 }
1068 }
1069
1070 #[test]
1071 fn vllm_prebuilt_waiting_request_runs_before_blocked_pending_destination() {
1072 let mut destination = core(EngineType::Vllm, WorkerType::Decode, 4);
1073 let ready_handoff = HandoffId::from(Uuid::from_u128(36_900));
1074 let pending_handoff = HandoffId::from(Uuid::from_u128(36_901));
1075 let ready_request = Uuid::from_u128(36_902);
1076 let pending_request = Uuid::from_u128(36_903);
1077 let fresh_request = Uuid::from_u128(36_904);
1078
1079 destination.receive(request(fresh_request, (200..204).collect()));
1080
1081 assert_eq!(
1082 destination
1083 .apply_command(SchedulerCommand::ReserveDestination {
1084 handoff_id: ready_handoff,
1085 request: request(ready_request, (0..8).collect()),
1086 })
1087 .unwrap(),
1088 SchedulerCommandResult::DestinationAccepted {
1089 request_id: ready_request
1090 }
1091 );
1092 assert_eq!(
1093 destination
1094 .apply_command(SchedulerCommand::ActivateDestination {
1095 handoff_id: ready_handoff,
1096 })
1097 .unwrap(),
1098 SchedulerCommandResult::Applied
1099 );
1100 let pending = destination
1101 .apply_command_effects(
1102 SchedulerCommand::ReserveDestination {
1103 handoff_id: pending_handoff,
1104 request: request(pending_request, (100..112).collect()),
1105 },
1106 true,
1107 )
1108 .unwrap();
1109 assert!(pending.lifecycle_events.is_empty());
1110
1111 let first_pass = destination.execute_hidden_pass(0.0);
1112 assert!(
1113 first_pass
1114 .admissions
1115 .iter()
1116 .any(|admission| admission.uuid == ready_request)
1117 );
1118 assert!(
1119 first_pass
1120 .admissions
1121 .iter()
1122 .all(|admission| admission.uuid != fresh_request)
1123 );
1124
1125 let mut reservation_events = Vec::new();
1126 for now_ms in 1..=4 {
1127 destination.execute_hidden_pass(f64::from(now_ms));
1128 reservation_events.extend(destination.retry_pending_destinations());
1129 if !reservation_events.is_empty() {
1130 break;
1131 }
1132 }
1133 assert!(matches!(
1134 reservation_events.as_slice(),
1135 [SchedulerLifecycleEvent::DestinationReserved {
1136 handoff_id,
1137 request_id,
1138 ..
1139 }] if *handoff_id == pending_handoff && *request_id == pending_request
1140 ));
1141 assert_eq!(
1142 destination
1143 .apply_command(SchedulerCommand::CancelDestination {
1144 handoff_id: pending_handoff,
1145 })
1146 .unwrap(),
1147 SchedulerCommandResult::Applied
1148 );
1149 }
1150
1151 #[test]
1152 fn activated_waiting_destination_cancels_without_consuming_the_running_slot() {
1153 for (case, engine_type) in [EngineType::Vllm, EngineType::Sglang]
1154 .into_iter()
1155 .enumerate()
1156 {
1157 let mut destination = core(engine_type, WorkerType::Decode, 16);
1158 let handoff_id = HandoffId::from(Uuid::from_u128(38_000 + case as u128));
1159 let request_id = Uuid::from_u128(38_100 + case as u128);
1160 let reserved = destination
1161 .apply_command_effects(
1162 SchedulerCommand::ReserveDestination {
1163 handoff_id,
1164 request: request(request_id, (0..8).collect()),
1165 },
1166 true,
1167 )
1168 .unwrap();
1169 assert_eq!(reserved.lifecycle_events.len(), 1);
1170 let reserved_occupancy = match &destination {
1171 EngineCore::Vllm(core) => core.mocker_metrics().active_decode_blocks,
1172 EngineCore::Sglang(core) => core.mocker_metrics().active_decode_blocks,
1173 };
1174 assert!(reserved_occupancy > 0);
1175
1176 destination.receive(DirectRequest {
1177 tokens: (100..108).collect(),
1178 max_output_tokens: 8,
1179 uuid: Some(Uuid::from_u128(38_200 + case as u128)),
1180 ..Default::default()
1181 });
1182 let pass = destination.execute_hidden_pass(0.0);
1183 assert_eq!(pass.admissions.len(), 1);
1184 let before_activation = match &destination {
1185 EngineCore::Vllm(core) => core.mocker_metrics().active_decode_blocks,
1186 EngineCore::Sglang(core) => core.mocker_metrics().active_decode_blocks,
1187 };
1188 assert_eq!(
1189 destination
1190 .apply_command(SchedulerCommand::ActivateDestination { handoff_id })
1191 .unwrap(),
1192 SchedulerCommandResult::Applied
1193 );
1194 let activated_occupancy = match &destination {
1195 EngineCore::Vllm(core) => core.mocker_metrics().active_decode_blocks,
1196 EngineCore::Sglang(core) => core.mocker_metrics().active_decode_blocks,
1197 };
1198 assert_eq!(activated_occupancy, before_activation);
1199 assert_eq!(
1200 destination
1201 .apply_command(SchedulerCommand::CancelDestination { handoff_id })
1202 .unwrap(),
1203 SchedulerCommandResult::Applied
1204 );
1205 assert_eq!(
1206 destination
1207 .apply_command(SchedulerCommand::CancelDestination { handoff_id })
1208 .unwrap(),
1209 SchedulerCommandResult::Noop
1210 );
1211 assert_eq!(destination.num_requests(), 1);
1212 }
1213 }
1214
1215 #[test]
1216 fn preterminal_source_cancel_removes_scheduled_request_once() {
1217 for (case, engine_type) in [EngineType::Vllm, EngineType::Sglang]
1218 .into_iter()
1219 .enumerate()
1220 {
1221 let mut source = core(engine_type, WorkerType::Prefill, 8);
1222 let handoff_id = HandoffId::from(Uuid::from_u128(40_000 + case as u128));
1223 let request_id = Uuid::from_u128(40_100 + case as u128);
1224 source
1225 .apply_command(SchedulerCommand::SubmitHandoffPrefill {
1226 handoff_id,
1227 request: request(request_id, (0..8).collect()),
1228 })
1229 .unwrap();
1230 assert_eq!(source.num_requests(), 1);
1231
1232 assert_eq!(
1233 source
1234 .apply_command(SchedulerCommand::CancelSource { handoff_id })
1235 .unwrap(),
1236 SchedulerCommandResult::Applied
1237 );
1238 assert!(source.is_empty());
1239 assert!(source.is_drained());
1240 assert_eq!(
1241 source
1242 .apply_command(SchedulerCommand::CancelSource { handoff_id })
1243 .unwrap(),
1244 SchedulerCommandResult::Noop
1245 );
1246 }
1247 }
1248}
1249
1250#[cfg(all(test, feature = "kvbm-offload"))]
1251mod offload_init_tests {
1252 use super::{init_kvbm_live, init_kvbm_offline};
1253 use crate::common::protocols::{KvEventPublishers, MockEngineArgs};
1254 use crate::kv_manager::G1Manager;
1255
1256 fn make_kv_manager() -> G1Manager {
1257 G1Manager::new_with_event_sink(8, 4, KvEventPublishers::default(), 0)
1258 }
1259
1260 fn args_with_g2_and_bpt(bpt: usize) -> MockEngineArgs {
1261 MockEngineArgs::builder()
1262 .num_gpu_blocks(8)
1263 .num_g2_blocks(Some(8))
1264 .block_size(4)
1265 .kv_bytes_per_token(Some(bpt))
1266 .build()
1267 .unwrap()
1268 .normalized()
1269 .unwrap()
1270 }
1271
1272 #[tokio::test]
1273 async fn init_kvbm_live_attaches_engine_when_g2_and_bpt_set() {
1274 let args = args_with_g2_and_bpt(131_072);
1275 let mut kv = make_kv_manager();
1276 assert!(!kv.has_offload_engine());
1277 let engine = init_kvbm_live(&args, &mut kv)
1278 .await
1279 .expect("init must succeed")
1280 .expect("engine built with G2 and bpt present");
1281 assert!(kv.has_offload_engine());
1282 assert!(engine.lock().unwrap().earliest_pending_deadline().is_none());
1285 assert!(kv.earliest_offload_deadline().is_none());
1286 }
1287
1288 #[tokio::test]
1289 async fn init_kvbm_live_returns_none_without_g2_blocks() {
1290 let args = MockEngineArgs::builder()
1291 .num_gpu_blocks(8)
1292 .block_size(4)
1293 .kv_bytes_per_token(Some(131_072))
1294 .build()
1295 .unwrap()
1296 .normalized()
1297 .unwrap();
1298 assert!(args.num_g2_blocks.is_none());
1299 let mut kv = make_kv_manager();
1300 let result = init_kvbm_live(&args, &mut kv)
1301 .await
1302 .expect("init must succeed");
1303 assert!(result.is_none());
1304 assert!(!kv.has_offload_engine());
1305 }
1306
1307 #[tokio::test]
1308 async fn init_kvbm_live_returns_none_without_bpt() {
1309 let args = MockEngineArgs::default();
1310 assert!(args.kv_bytes_per_token.is_none());
1311 let mut kv = make_kv_manager();
1312 let result = init_kvbm_live(&args, &mut kv)
1313 .await
1314 .expect("init must succeed");
1315 assert!(result.is_none());
1316 assert!(!kv.has_offload_engine());
1317 }
1318
1319 #[test]
1320 fn init_kvbm_offline_attaches_engine_and_keeps_runtime_alive() {
1321 let args = args_with_g2_and_bpt(131_072);
1328 let mut kv = make_kv_manager();
1329 let engine = init_kvbm_offline(&args, &mut kv)
1330 .expect("offline init must succeed")
1331 .expect("engine built with G2 and bpt present");
1332 assert!(kv.has_offload_engine());
1333 engine.lock().unwrap().tick(100.0);
1335 assert!(kv.earliest_offload_deadline().is_none());
1336 }
1337
1338 #[test]
1339 fn init_kvbm_offline_returns_none_without_g2_blocks() {
1340 let args = MockEngineArgs::builder()
1341 .num_gpu_blocks(8)
1342 .block_size(4)
1343 .kv_bytes_per_token(Some(131_072))
1344 .build()
1345 .unwrap()
1346 .normalized()
1347 .unwrap();
1348 assert!(args.num_g2_blocks.is_none());
1349 let mut kv = make_kv_manager();
1350 let result = init_kvbm_offline(&args, &mut kv).expect("init must succeed");
1351 assert!(result.is_none());
1352 assert!(!kv.has_offload_engine());
1353 }
1354
1355 #[test]
1356 fn init_kvbm_offline_returns_none_without_bpt() {
1357 let args = MockEngineArgs::default();
1358 assert!(args.kv_bytes_per_token.is_none());
1359 let mut kv = make_kv_manager();
1360 let result = init_kvbm_offline(&args, &mut kv).expect("init must succeed");
1361 assert!(result.is_none());
1362 assert!(!kv.has_offload_engine());
1363 }
1364}