1use 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
39pub 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 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 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 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}