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//! Engine-specific scheduling implementations.
5
6mod 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/// Welford's online algorithm for count / sum / population-variance.
37///
38/// Mirrors the Python `WelfordAccumulator` in `forward_pass_metrics.py`.
39#[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
65/// Build a [`ForwardPassSnapshot`] from engine-agnostic iterators.
66///
67/// Each engine (vLLM, SGLang) calls this with its own iterators, avoiding
68/// duplicated variance/accumulation logic.
69///
70/// - `scheduled_prefills`: `(prompt_len, prefix_tokens, tokens_computed)` per request
71/// - `scheduled_decodes`: `sequence_len` per request
72/// - `queued_prefills`: `prompt_len` per waiting prefill request
73/// - `queued_decodes`: `kv_tokens` per preempted decode request
74pub(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
126/// Return (visible output tokens, request-forwards) for accept-length
127/// accounting. A signal with a token corresponds to one visible token; multiple
128/// token signals with the same UUID in a pass are an MTP/spec-decode burst.
129pub(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    /// Controls when replay/live schedulers should expose this pass's buffered
167    /// KV events to the real router or publisher sink.
168    pub(crate) router_event_visibility: RouterEventVisibility,
169    /// Router-visible KV events emitted during this pass.
170    pub(crate) kv_events: Vec<RouterEvent>,
171    /// Forward pass metrics snapshot for this iteration.
172    pub(crate) fpm: Option<ForwardPassSnapshot>,
173    /// Visible output tokens emitted by this pass for accept-length accounting.
174    pub(crate) accept_length_output_tokens: usize,
175    /// Number of request decode forwards that emitted those visible tokens.
176    pub(crate) accept_length_decode_forwards: usize,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub(crate) enum RouterEventVisibility {
181    /// Expose buffered KV events when the pass starts, before the modeled sleep.
182    PassStart,
183    /// Expose buffered KV events when the pass finishes, before output flush.
184    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/// Output channel used by a live scheduler.
353///
354#[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                // Legacy output-only consumers do not have an admission event sink.
401                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
450/// Engine-agnostic scheduler interface.
451///
452/// Both vLLM and SGLang schedulers implement this trait so that the engine
453/// wrapper (`MockEngine`) can work with either backend through the same API.
454pub trait SchedulerHandle: Send + Sync {
455    /// Send a request to the scheduler's waiting queue.
456    fn receive(&self, request: DirectRequest);
457
458    /// Get a clone of the request sender channel for direct sending.
459    fn request_sender(&self) -> mpsc::UnboundedSender<DirectRequest>;
460
461    /// Get a watch receiver for scheduler metrics (active decode blocks, etc.).
462    fn metrics_receiver(&self) -> tokio::sync::watch::Receiver<MockerMetrics>;
463
464    /// Bounded ordered channel for request and disaggregated lifecycle commands.
465    fn command_sender(&self) -> mpsc::Sender<SchedulerCommandEnvelope>;
466
467    /// Bounded cancellation channel observed even while a modeled pass is running.
468    ///
469    /// Cancellation removes scheduler state and can suppress pending output immediately. During a
470    /// modeled pass, published running/waiting metrics refresh at the next pass boundary; exact
471    /// mid-pass metrics would require incremental per-request residency accounting.
472    fn cancellation_sender(&self) -> mpsc::Sender<SchedulerCancellationEnvelope>;
473
474    /// Take the single lifecycle-event stream owned by this DP-rank scheduler.
475    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/// Attach a [`crate::kvbm_offload::MockOffloadEngine`] driven by
485/// wall-clock `now_ms` supplied by live replay. Returns `Ok(None)` unless
486/// `num_g2_blocks` explicitly opts into G2 and `kv_bytes_per_token` supplies
487/// the simulated block size.
488#[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/// Attach a [`crate::kvbm_offload::MockOffloadEngine`] driven by
505/// virtual `now_ms` supplied by offline replay. The same engine hot path is
506/// used for live and offline; only the caller's clock source differs.
507#[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/// Build an offload engine with its private runtime attached.
535///
536/// kvbm-engine uses background pipeline/session tasks even though the mocker
537/// scheduler is synchronous. Keeping the runtime inside the engine lets each
538/// scheduler pass explicitly pump those tasks after transfer completions.
539#[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/// Shared test utilities for scheduler stress tests.
553#[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        // Values: 2, 4, 4, 4, 5, 5, 7, 9
718        // Mean = 5, Population variance = 4.0
719        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        // Reproduce the Python WelfordAccumulator behavior:
730        // values = [100, 200, 300], mean = 200,
731        // population variance = ((100-200)^2 + (200-200)^2 + (300-200)^2) / 3
732        //                     = (10000 + 0 + 10000) / 3 = 6666.666...
733        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        // Returned Arc shares the same engine as the one on kv_manager;
1283        // earliest_offload_deadline reflects an idle engine.
1284        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        // Sync entry: no ambient tokio runtime. init_kvbm_offline owns
1322        // its own runtime and moves it onto the engine via
1323        // attach_runtime. After init returns, the engine (and its
1324        // runtime) must still be usable — `tick` is a sync call that
1325        // internally depends on the worker thread continuing to drain
1326        // kvbm-engine's background tasks.
1327        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 is still callable post-init — no runtime-dropped hang.
1334        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}