Skip to main content

aisimulate_core/engine/scheduler/
rank.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Thin neutral contract adapter over the mechanically moved scheduler cores.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use anyhow::{Result, anyhow};
10use uuid::Uuid;
11
12use crate::engine::common::perf_model::PerfModel;
13use crate::engine::common::protocols::{
14    DirectRequest, EngineType, KvTransferTimingMode, MockEngineArgs,
15    PreemptionMode as CorePreemptionMode, SglangArgs, WorkerType as CoreWorkerType,
16};
17use crate::engine::generalized::{CommandContext, RankEngine, RankIdentity, RankPass};
18use crate::engine::{
19    Admission, Backend, Command, CommandEffects, CommandResult, EngineConfig, ForwardPassMetrics,
20    HandoffId, LifecycleEvent, Metrics, Output, PassCompletionEffects, PassStartEffects,
21    PendingPass, PreemptionMode, Request, TimingModel, TransferTimingMode, WorkerType,
22};
23
24use super::{
25    EngineCore, EnginePassResult, KvEventVisibility, MockerMetrics,
26    SchedulerCommand as CoreCommand, SchedulerCommandEffects as CoreCommandEffects,
27    SchedulerCommandResult as CoreCommandResult, SchedulerLifecycleEvent as CoreLifecycle,
28    SglangCore, VllmCore,
29};
30
31pub fn engine_seed_offset(identity: RankIdentity) -> Result<u64> {
32    identity
33        .worker_id
34        .checked_mul(u64::from(identity.dp_size.get()))
35        .and_then(|base| base.checked_add(u64::from(identity.dp_rank)))
36        .ok_or_else(|| anyhow!("native mock-engine seed offset overflow"))
37}
38
39/// One preserved vLLM/SGLang scheduler rank behind the neutral contract.
40pub struct SchedulerRank {
41    core: EngineCore,
42    handoff_requests: HashMap<HandoffId, Uuid>,
43}
44
45impl SchedulerRank {
46    pub fn new_with_timing_model(
47        identity: RankIdentity,
48        config: &EngineConfig,
49        timing: Arc<dyn TimingModel>,
50        seed_offset: u64,
51    ) -> Result<Self> {
52        config.validate()?;
53        let args = core_args(config, timing);
54        let capture_kv_events = config.emit_kv_events;
55        let core = match config.backend {
56            Backend::Vllm | Backend::Trtllm => EngineCore::Vllm(VllmCore::new_with_worker_rank(
57                args,
58                identity.worker_id,
59                identity.dp_rank,
60                seed_offset,
61                capture_kv_events,
62            )),
63            Backend::Sglang => EngineCore::Sglang(SglangCore::new_with_worker_rank(
64                args,
65                identity.worker_id,
66                identity.dp_rank,
67                seed_offset,
68                capture_kv_events,
69            )),
70        };
71        Ok(Self {
72            core,
73            handoff_requests: HashMap::new(),
74        })
75    }
76
77    fn core_command(command: Command) -> CoreCommand {
78        match command {
79            Command::Submit(request) => CoreCommand::Submit(core_request(request)),
80            Command::CancelRequest { request_id, .. } => CoreCommand::CancelRequest { request_id },
81            Command::SubmitHandoffPrefill {
82                handoff_id,
83                request,
84            } => CoreCommand::SubmitHandoffPrefill {
85                handoff_id,
86                request: core_request(request),
87            },
88            Command::ReserveDestination {
89                handoff_id,
90                request,
91            } => CoreCommand::ReserveDestination {
92                handoff_id,
93                request: core_request(request),
94            },
95            Command::ActivateDestination { handoff_id } => {
96                CoreCommand::ActivateDestination { handoff_id }
97            }
98            Command::ReleaseSource { handoff_id } => CoreCommand::ReleaseSource { handoff_id },
99            Command::CancelSource { handoff_id } => CoreCommand::CancelSource { handoff_id },
100            Command::CancelDestination { handoff_id } => {
101                CoreCommand::CancelDestination { handoff_id }
102            }
103        }
104    }
105
106    fn metrics(&self) -> Metrics {
107        let metrics = match &self.core {
108            EngineCore::Vllm(core) => core.mocker_metrics(),
109            EngineCore::Sglang(core) => core.mocker_metrics(),
110        };
111        map_metrics(metrics)
112    }
113}
114
115impl RankEngine for SchedulerRank {
116    type Config = EngineConfig;
117    type Command = Command;
118    type CommandEffects = CommandEffects;
119    type PassStartEffects = PassStartEffects;
120    type PendingPass = PendingPass;
121    type PassCompletionEffects = PassCompletionEffects;
122    type InternalEffects = ();
123
124    fn new(identity: RankIdentity, config: &Self::Config) -> Result<Self> {
125        let timing = config.built_in_timing_model()?;
126        let seed_offset = engine_seed_offset(identity)?;
127        Self::new_with_timing_model(identity, config, timing, seed_offset)
128    }
129
130    fn apply_command_effects(
131        &mut self,
132        command: Self::Command,
133        context: CommandContext,
134        pending_pass: Option<&mut Self::PendingPass>,
135    ) -> Result<Self::CommandEffects> {
136        let pending_suppression = pending_output_suppression(&command, &self.handoff_requests);
137        let handoff_update = handoff_tracking_update(&command);
138        let core_command = Self::core_command(command);
139        let mut effects = self
140            .core
141            .apply_command_effects(core_command, context.allow_immediate_admission())?;
142        // Preserve the scheduler's command boundary: native G1 publishes KV
143        // mutations into the rank-local capture sink, while command effects
144        // are the only observation returned to the driver at this point. If
145        // these events remain buffered, a later pass may drain and discard an
146        // incomplete prefix of the stream before the Router observes it.
147        effects.kv_events.extend(self.core.drain_kv_events());
148        let suppressed_pending_output = if let (Some((request_id, discard_on_noop)), Some(pending)) =
149            (pending_suppression, pending_pass)
150            && (effects.result != CoreCommandResult::Noop || discard_on_noop)
151        {
152            let before = pending.effects.outputs.len();
153            pending
154                .effects
155                .outputs
156                .retain(|output| output.request_id != request_id);
157            pending
158                .effects
159                .lifecycle_events
160                .retain(|event| match *event {
161                    LifecycleEvent::SourceHeld { request_id: id, .. }
162                    | LifecycleEvent::DestinationReserved { request_id: id, .. } => {
163                        id != request_id
164                    }
165                });
166            before != pending.effects.outputs.len()
167        } else {
168            false
169        };
170        if effects.result != CoreCommandResult::Noop || suppressed_pending_output {
171            self.apply_handoff_tracking_update(handoff_update);
172        }
173        for request_id in &effects.retired_requests {
174            self.handoff_requests
175                .retain(|_, tracked_request| tracked_request != request_id);
176        }
177        map_command_effects(effects, self.metrics(), suppressed_pending_output)
178    }
179
180    fn is_ready(&self) -> bool {
181        !self.core.is_drained()
182    }
183
184    fn waiting_for_external_command(&self) -> bool {
185        self.core.waiting_for_external_command()
186    }
187
188    fn execute_pass(
189        &mut self,
190        now_ms: f64,
191    ) -> Result<RankPass<Self::PassStartEffects, Self::PendingPass>> {
192        let pass = self.core.try_execute_hidden_pass(now_ms)?;
193        let end_ms = pass.end_ms;
194        let (same_timestamp_retry, start_effects, completion_effects) = split_pass(pass)?;
195        Ok(RankPass {
196            end_ms,
197            same_timestamp_retry,
198            start_effects,
199            pending: PendingPass {
200                started_at_ms: now_ms,
201                effects: completion_effects,
202            },
203        })
204    }
205
206    fn complete_pass(
207        &mut self,
208        mut pending: Self::PendingPass,
209        end_ms: f64,
210    ) -> Result<Self::PassCompletionEffects> {
211        // The preserved scheduler retries deferred destination reservations
212        // when a forward pass releases capacity. Keep that wakeup at the
213        // pass-completion boundary: command-time retry is suppressed while a
214        // pass is in flight, and waiting until an unrelated later pass can
215        // leave disaggregated replay permanently asleep.
216        pending.effects.lifecycle_events.extend(
217            self.core
218                .retry_pending_destinations()
219                .into_iter()
220                .map(map_lifecycle),
221        );
222        let completion_kv_events = self.core.drain_kv_events();
223        pending.effects.kv_events.extend(completion_kv_events);
224        // Occupancy is authoritative at the shared completion boundary, but
225        // SGLang cache hit/total are transient observations from this pass.
226        // Preserve those fields while refreshing the rest of the snapshot;
227        // a later live adapter latches the last non-empty observation.
228        let sglang_cache_hit_tokens = pending.effects.metrics.sglang_cache_hit_tokens;
229        let sglang_cache_total_tokens = pending.effects.metrics.sglang_cache_total_tokens;
230        pending.effects.metrics = self.metrics();
231        pending.effects.metrics.sglang_cache_hit_tokens = sglang_cache_hit_tokens;
232        pending.effects.metrics.sglang_cache_total_tokens = sglang_cache_total_tokens;
233        pending.effects.forward_pass_metrics.duration_ms =
234            (end_ms - pending.started_at_ms).max(0.0);
235        for output in &pending.effects.outputs {
236            if output.completed {
237                self.handoff_requests
238                    .retain(|_, request_id| *request_id != output.request_id);
239            }
240        }
241        Ok(pending.effects)
242    }
243
244    fn complete_idle_group_pass(
245        &mut self,
246        started_at_ms: f64,
247        end_ms: f64,
248    ) -> Result<Option<Self::PassCompletionEffects>> {
249        let lifecycle_events = self
250            .core
251            .retry_pending_destinations()
252            .into_iter()
253            .map(map_lifecycle)
254            .collect::<Vec<_>>();
255        let kv_events = self.core.drain_kv_events();
256        Ok(Some(PassCompletionEffects {
257            lifecycle_events,
258            kv_events,
259            metrics: self.metrics(),
260            forward_pass_metrics: ForwardPassMetrics {
261                duration_ms: (end_ms - started_at_ms).max(0.0),
262                ..Default::default()
263            },
264            ..PassCompletionEffects::default()
265        }))
266    }
267
268    fn next_internal_deadline_ms(&self) -> Option<f64> {
269        None
270    }
271
272    fn process_internal_work(
273        &mut self,
274        _now_ms: f64,
275        _pass_in_flight: bool,
276    ) -> Result<Self::InternalEffects> {
277        Ok(())
278    }
279
280    fn is_drained(&self) -> bool {
281        self.core.is_drained()
282    }
283}
284
285#[derive(Clone, Copy)]
286enum HandoffTrackingUpdate {
287    None,
288    Insert(HandoffId, Uuid),
289    RemoveHandoff(HandoffId),
290    RemoveRequest(Uuid),
291}
292
293impl SchedulerRank {
294    fn apply_handoff_tracking_update(&mut self, update: HandoffTrackingUpdate) {
295        match update {
296            HandoffTrackingUpdate::None => {}
297            HandoffTrackingUpdate::Insert(handoff_id, request_id) => {
298                self.handoff_requests.insert(handoff_id, request_id);
299            }
300            HandoffTrackingUpdate::RemoveHandoff(handoff_id) => {
301                self.handoff_requests.remove(&handoff_id);
302            }
303            HandoffTrackingUpdate::RemoveRequest(request_id) => self
304                .handoff_requests
305                .retain(|_, tracked_request| *tracked_request != request_id),
306        }
307    }
308}
309
310fn handoff_tracking_update(command: &Command) -> HandoffTrackingUpdate {
311    match command {
312        Command::SubmitHandoffPrefill {
313            handoff_id,
314            request,
315        }
316        | Command::ReserveDestination {
317            handoff_id,
318            request,
319        } => HandoffTrackingUpdate::Insert(*handoff_id, request.request_id),
320        Command::ReleaseSource { handoff_id }
321        | Command::CancelSource { handoff_id }
322        | Command::CancelDestination { handoff_id } => {
323            HandoffTrackingUpdate::RemoveHandoff(*handoff_id)
324        }
325        Command::CancelRequest { request_id, .. } => {
326            HandoffTrackingUpdate::RemoveRequest(*request_id)
327        }
328        Command::Submit(_) | Command::ActivateDestination { .. } => HandoffTrackingUpdate::None,
329    }
330}
331
332fn core_args(config: &EngineConfig, timing: Arc<dyn TimingModel>) -> MockEngineArgs {
333    MockEngineArgs {
334        engine_type: match config.backend {
335            Backend::Vllm => EngineType::Vllm,
336            Backend::Sglang => EngineType::Sglang,
337            Backend::Trtllm => EngineType::Trtllm,
338        },
339        num_gpu_blocks: config.num_gpu_blocks,
340        block_size: config.block_size,
341        max_model_len: config.max_model_len,
342        max_num_seqs: Some(config.max_num_seqs),
343        max_num_batched_tokens: Some(config.max_num_batched_tokens),
344        enable_prefix_caching: config.enable_prefix_caching,
345        enable_chunked_prefill: config.enable_chunked_prefill,
346        speedup_ratio: config.speedup_ratio,
347        decode_speedup_ratio: config.decode_speedup_ratio,
348        worker_type: match config.worker_type {
349            WorkerType::Aggregated => CoreWorkerType::Aggregated,
350            WorkerType::Prefill => CoreWorkerType::Prefill,
351            WorkerType::Decode => CoreWorkerType::Decode,
352        },
353        perf_model: Arc::new(PerfModel::External { timing }),
354        aic_nextn: config.aic_nextn,
355        aic_nextn_accept_rates: config.aic_nextn_accept_rates.clone(),
356        aic_mtp_seed: config.aic_mtp_seed,
357        kv_bytes_per_token: config.kv_bytes_per_token,
358        kv_transfer_bandwidth: config.kv_transfer_bandwidth,
359        kv_transfer_timing_mode: match config.kv_transfer_timing_mode {
360            TransferTimingMode::FullPrompt => KvTransferTimingMode::FullPrompt,
361            TransferTimingMode::DestinationMissing => KvTransferTimingMode::DestinationMissing,
362        },
363        preemption_mode: match config.preemption_mode {
364            PreemptionMode::Lifo => CorePreemptionMode::Lifo,
365            PreemptionMode::Fifo => CorePreemptionMode::Fifo,
366        },
367        sglang: Some(SglangArgs {
368            schedule_policy: Some(
369                match config.sglang.schedule_policy {
370                    crate::engine::SglangSchedulePolicy::Fifo => "fifo",
371                    crate::engine::SglangSchedulePolicy::Lpm => "lpm",
372                }
373                .to_string(),
374            ),
375            page_size: Some(config.block_size),
376            max_prefill_tokens: Some(config.sglang.max_prefill_tokens),
377            chunked_prefill_size: Some(config.sglang.chunked_prefill_size),
378            clip_max_new_tokens: Some(config.sglang.clip_max_new_tokens),
379            schedule_conservativeness: Some(config.sglang.schedule_conservativeness),
380        }),
381        emit_kv_events: config.emit_kv_events,
382        emit_kv_token_ids: config.emit_kv_token_ids,
383    }
384}
385
386fn core_request(request: Request) -> DirectRequest {
387    DirectRequest {
388        tokens: request.tokens,
389        max_output_tokens: request.max_output_tokens,
390        output_token_ids: request.output_token_ids,
391        uuid: Some(request.request_id),
392        arrival_timestamp_ms: None,
393    }
394}
395
396fn pending_output_suppression(
397    command: &Command,
398    handoffs: &HashMap<HandoffId, Uuid>,
399) -> Option<(Uuid, bool)> {
400    match *command {
401        Command::CancelRequest {
402            request_id,
403            discard_pending_output,
404        } => Some((request_id, discard_pending_output)),
405        Command::CancelSource { handoff_id } | Command::CancelDestination { handoff_id } => {
406            handoffs
407                .get(&handoff_id)
408                .copied()
409                .map(|request_id| (request_id, false))
410        }
411        _ => None,
412    }
413}
414
415fn map_command_effects(
416    effects: CoreCommandEffects,
417    metrics: Metrics,
418    suppressed_pending_output: bool,
419) -> Result<CommandEffects> {
420    let result = match effects.result {
421        CoreCommandResult::Submitted(id) => CommandResult::Submitted(id),
422        CoreCommandResult::DestinationAccepted { request_id } => {
423            CommandResult::DestinationAccepted { request_id }
424        }
425        CoreCommandResult::Applied => CommandResult::Applied,
426        CoreCommandResult::Noop => CommandResult::Noop,
427    };
428    Ok(CommandEffects {
429        result,
430        lifecycle_events: effects
431            .lifecycle_events
432            .into_iter()
433            .map(map_lifecycle)
434            .collect(),
435        kv_events: effects.kv_events,
436        retired_requests: effects.retired_requests,
437        metrics,
438        suppressed_pending_output,
439    })
440}
441
442fn map_lifecycle(event: CoreLifecycle) -> LifecycleEvent {
443    match event {
444        CoreLifecycle::SourceHeld {
445            handoff_id,
446            request_id,
447            transfer_timing,
448        } => LifecycleEvent::SourceHeld {
449            handoff_id,
450            request_id,
451            transfer_timing,
452        },
453        CoreLifecycle::DestinationReserved {
454            handoff_id,
455            request_id,
456            transferable_prompt_tokens,
457        } => LifecycleEvent::DestinationReserved {
458            handoff_id,
459            request_id,
460            transferable_prompt_tokens,
461        },
462    }
463}
464
465fn map_metrics(metrics: MockerMetrics) -> Metrics {
466    Metrics {
467        dp_rank: metrics.dp_rank,
468        active_blocks: metrics.active_decode_blocks,
469        total_blocks: metrics.total_blocks,
470        cache_usage: metrics.gpu_cache_usage_perc,
471        running_requests: metrics.running_requests,
472        waiting_requests: metrics.waiting_requests,
473        preemptions_total: metrics.vllm_preemptions_total,
474        sglang_cache_hit_tokens: metrics.sglang_cache_hit_tokens,
475        sglang_cache_total_tokens: metrics.sglang_cache_total_tokens,
476    }
477}
478
479fn split_pass(
480    pass: EnginePassResult,
481) -> Result<(
482    crate::engine::generalized::SameTimestampRetry,
483    PassStartEffects,
484    PassCompletionEffects,
485)> {
486    let EnginePassResult {
487        same_timestamp_retry,
488        output_signals,
489        admissions,
490        pressure_events,
491        lifecycle_events,
492        mocker_metrics,
493        kv_event_visibility,
494        kv_events,
495        fpm,
496        ..
497    } = pass;
498    let (start_kv, completion_kv) = match kv_event_visibility {
499        KvEventVisibility::PassEnd => (Vec::new(), kv_events),
500    };
501    let start = PassStartEffects {
502        admissions: admissions
503            .into_iter()
504            .map(|admission| Admission {
505                request_id: admission.uuid,
506                reused_input_tokens: admission.reused_input_tokens,
507            })
508            .collect(),
509        pressure_events,
510        kv_events: start_kv,
511    };
512    let completion = PassCompletionEffects {
513        outputs: output_signals
514            .into_iter()
515            .map(|output| Output {
516                request_id: output.uuid,
517                token_id: output.token_id,
518                completed: output.completed,
519                rejected: output.rejected,
520                cached_tokens: output.cached_tokens,
521            })
522            .collect(),
523        lifecycle_events: lifecycle_events.into_iter().map(map_lifecycle).collect(),
524        kv_events: completion_kv,
525        metrics: map_metrics(mocker_metrics),
526        forward_pass_metrics: fpm.map(map_fpm).unwrap_or_default(),
527    };
528    Ok((same_timestamp_retry, start, completion))
529}
530
531fn map_fpm(fpm: crate::engine::common::protocols::ForwardPassSnapshot) -> ForwardPassMetrics {
532    ForwardPassMetrics {
533        num_prefill_requests: fpm.num_prefill_requests,
534        sum_prefill_tokens: fpm.sum_prefill_tokens,
535        var_prefill_length: fpm.var_prefill_length,
536        sum_prefill_kv_tokens: fpm.sum_prefill_kv_tokens,
537        num_decode_requests: fpm.num_decode_requests,
538        sum_decode_kv_tokens: fpm.sum_decode_kv_tokens,
539        var_decode_kv_tokens: fpm.var_decode_kv_tokens,
540        num_queued_prefill: fpm.num_queued_prefill,
541        sum_queued_prefill_tokens: fpm.sum_queued_prefill_tokens,
542        var_queued_prefill_length: fpm.var_queued_prefill_length,
543        num_queued_decode: fpm.num_queued_decode,
544        sum_queued_decode_kv_tokens: fpm.sum_queued_decode_kv_tokens,
545        var_queued_decode_kv_tokens: fpm.var_queued_decode_kv_tokens,
546        duration_ms: fpm.wall_time_secs * 1_000.0,
547    }
548}
549
550#[cfg(test)]
551mod tests {
552    use std::num::NonZeroU32;
553
554    use super::*;
555    use crate::engine::{PressureKind, TimingModelConfig};
556
557    fn rank() -> SchedulerRank {
558        rank_for_worker(WorkerType::Aggregated)
559    }
560
561    fn rank_for_worker(worker_type: WorkerType) -> SchedulerRank {
562        let config = EngineConfig {
563            worker_type,
564            num_gpu_blocks: 8,
565            block_size: 4,
566            max_num_seqs: 2,
567            max_num_batched_tokens: 16,
568            speedup_ratio: 0.0,
569            timing_model: TimingModelConfig::Fixed {
570                prefill_ms: 10.0,
571                decode_ms: 10.0,
572            },
573            ..EngineConfig::default()
574        };
575        SchedulerRank::new(
576            RankIdentity {
577                worker_id: 1,
578                dp_rank: 0,
579                dp_size: NonZeroU32::MIN,
580            },
581            &config,
582        )
583        .unwrap()
584    }
585
586    fn start_request_pass(
587        rank: &mut SchedulerRank,
588        request_id: Uuid,
589        output_token_ids: Vec<u32>,
590    ) -> PendingPass {
591        let effects = rank
592            .apply_command_effects(
593                Command::Submit(Request {
594                    request_id,
595                    tokens: vec![1, 2, 3, 4],
596                    max_output_tokens: output_token_ids.len(),
597                    output_token_ids: Some(output_token_ids),
598                }),
599                CommandContext {
600                    now_ms: 0.0,
601                    pass_in_flight: false,
602                },
603                None,
604            )
605            .unwrap();
606        assert_eq!(effects.result, CommandResult::Submitted(request_id));
607        let pass = rank.execute_pass(0.0).unwrap();
608        assert!(
609            pass.pending
610                .effects
611                .outputs
612                .iter()
613                .any(|output| output.request_id == request_id)
614        );
615        pass.pending
616    }
617
618    #[test]
619    fn ordinary_cancel_suppresses_pending_output_when_scheduler_state_is_removed() {
620        let request_id = Uuid::from_u128(90_001);
621        let mut rank = rank();
622        let mut pending = start_request_pass(&mut rank, request_id, vec![5, 6]);
623
624        let effects = rank
625            .apply_command_effects(
626                Command::CancelRequest {
627                    request_id,
628                    discard_pending_output: false,
629                },
630                CommandContext {
631                    now_ms: 1.0,
632                    pass_in_flight: true,
633                },
634                Some(&mut pending),
635            )
636            .unwrap();
637
638        assert_eq!(effects.result, CommandResult::Applied);
639        assert!(effects.suppressed_pending_output);
640        assert!(pending.effects.outputs.is_empty());
641    }
642
643    #[test]
644    fn ordinary_noop_cancel_preserves_pending_output() {
645        let request_id = Uuid::from_u128(90_002);
646        let mut rank = rank();
647        let mut pending = start_request_pass(&mut rank, request_id, vec![5]);
648
649        let effects = rank
650            .apply_command_effects(
651                Command::CancelRequest {
652                    request_id,
653                    discard_pending_output: false,
654                },
655                CommandContext {
656                    now_ms: 1.0,
657                    pass_in_flight: true,
658                },
659                Some(&mut pending),
660            )
661            .unwrap();
662
663        assert_eq!(effects.result, CommandResult::Noop);
664        assert!(!effects.suppressed_pending_output);
665        assert_eq!(pending.effects.outputs.len(), 1);
666    }
667
668    #[test]
669    fn explicit_discard_suppresses_pending_output_after_noop_cancellation() {
670        let request_id = Uuid::from_u128(90_003);
671        let mut rank = rank();
672        let mut pending = start_request_pass(&mut rank, request_id, vec![5]);
673
674        let effects = rank
675            .apply_command_effects(
676                Command::CancelRequest {
677                    request_id,
678                    discard_pending_output: true,
679                },
680                CommandContext {
681                    now_ms: 1.0,
682                    pass_in_flight: true,
683                },
684                Some(&mut pending),
685            )
686            .unwrap();
687
688        assert_eq!(effects.result, CommandResult::Noop);
689        assert!(effects.suppressed_pending_output);
690        assert!(pending.effects.outputs.is_empty());
691    }
692
693    #[test]
694    fn handoff_tracking_is_inserted_on_success_and_cleared_by_cancel() {
695        let mut rank = rank_for_worker(WorkerType::Decode);
696        let handoff_id = HandoffId::from(Uuid::from_u128(91_001));
697        let request_id = Uuid::from_u128(91_002);
698        let reservation = rank
699            .apply_command_effects(
700                Command::ReserveDestination {
701                    handoff_id,
702                    request: Request {
703                        request_id,
704                        tokens: vec![1, 2, 3, 4],
705                        max_output_tokens: 1,
706                        output_token_ids: Some(vec![5]),
707                    },
708                },
709                CommandContext {
710                    now_ms: 0.0,
711                    pass_in_flight: false,
712                },
713                None,
714            )
715            .unwrap();
716        assert!(matches!(
717            reservation.result,
718            CommandResult::DestinationAccepted { .. }
719        ));
720        assert_eq!(rank.handoff_requests.get(&handoff_id), Some(&request_id));
721
722        let cancellation = rank
723            .apply_command_effects(
724                Command::CancelDestination { handoff_id },
725                CommandContext {
726                    now_ms: 0.0,
727                    pass_in_flight: false,
728                },
729                None,
730            )
731            .unwrap();
732        assert_eq!(cancellation.result, CommandResult::Applied);
733        assert!(!rank.handoff_requests.contains_key(&handoff_id));
734    }
735
736    #[test]
737    fn pass_start_exposes_vllm_preemption_pressure_event() {
738        let config = EngineConfig {
739            num_gpu_blocks: 6,
740            block_size: 4,
741            max_num_seqs: 2,
742            max_num_batched_tokens: 16,
743            enable_prefix_caching: false,
744            enable_chunked_prefill: true,
745            speedup_ratio: 0.0,
746            preemption_mode: PreemptionMode::Lifo,
747            timing_model: TimingModelConfig::Fixed {
748                prefill_ms: 10.0,
749                decode_ms: 10.0,
750            },
751            ..EngineConfig::default()
752        };
753        let mut rank = SchedulerRank::new(
754            RankIdentity {
755                worker_id: 7,
756                dp_rank: 0,
757                dp_size: NonZeroU32::MIN,
758            },
759            &config,
760        )
761        .unwrap();
762        let first = Uuid::from_u128(92_001);
763        let second = Uuid::from_u128(92_002);
764        for (request_id, tokens) in [
765            (first, (0..8).collect::<Vec<_>>()),
766            (second, (100..108).collect::<Vec<_>>()),
767        ] {
768            let effects = rank
769                .apply_command_effects(
770                    Command::Submit(Request {
771                        request_id,
772                        tokens,
773                        max_output_tokens: 8,
774                        output_token_ids: None,
775                    }),
776                    CommandContext {
777                        now_ms: 0.0,
778                        pass_in_flight: false,
779                    },
780                    None,
781                )
782                .unwrap();
783            assert_eq!(effects.result, CommandResult::Submitted(request_id));
784        }
785
786        let mut now_ms = 0.0;
787        let mut observed = None;
788        for _ in 0..16 {
789            let pass = rank.execute_pass(now_ms).unwrap();
790            if let Some(event) = pass.start_effects.pressure_events.first() {
791                assert_eq!(pass.start_effects.pressure_events.len(), 1);
792                observed = Some(event.clone());
793            }
794            let end_ms = pass.end_ms;
795            rank.complete_pass(pass.pending, end_ms).unwrap();
796            if observed.is_some() {
797                break;
798            }
799            now_ms = end_ms.max(now_ms + 1.0);
800        }
801
802        let event = observed.expect("tight native G1 capacity should preempt one vLLM request");
803        assert_eq!(event.at_ms, now_ms);
804        assert_eq!(event.kind, PressureKind::VllmPreemption);
805        assert_eq!(event.request_id, second);
806        assert_eq!(event.state_before.running_requests, 2);
807        assert_eq!(event.state_before.waiting_requests, Some(0));
808        assert_eq!(event.state_after.running_requests, 1);
809        assert_eq!(event.state_after.waiting_requests, Some(1));
810        assert!(event.request_active_blocks_before > 0);
811        assert!(event.state_after.active_blocks < event.state_before.active_blocks);
812        assert_eq!(event.logical_available_blocks_before, None);
813        assert_eq!(event.required_blocks_before, None);
814    }
815
816    #[test]
817    fn terminal_handoff_output_clears_tracking() {
818        let mut rank = rank_for_worker(WorkerType::Prefill);
819        let handoff_id = HandoffId::from(Uuid::from_u128(92_001));
820        let request_id = Uuid::from_u128(92_002);
821        let submission = rank
822            .apply_command_effects(
823                Command::SubmitHandoffPrefill {
824                    handoff_id,
825                    request: Request {
826                        request_id,
827                        tokens: vec![1, 2, 3, 4],
828                        max_output_tokens: 1,
829                        output_token_ids: Some(vec![5]),
830                    },
831                },
832                CommandContext {
833                    now_ms: 0.0,
834                    pass_in_flight: false,
835                },
836                None,
837            )
838            .unwrap();
839        assert_eq!(submission.result, CommandResult::Submitted(request_id));
840        assert_eq!(rank.handoff_requests.get(&handoff_id), Some(&request_id));
841
842        let pass = rank.execute_pass(0.0).unwrap();
843        let completion = rank.complete_pass(pass.pending, pass.end_ms).unwrap();
844        assert!(
845            completion
846                .outputs
847                .iter()
848                .any(|output| output.request_id == request_id && output.completed)
849        );
850        assert!(!rank.handoff_requests.contains_key(&handoff_id));
851    }
852}