use std::collections::VecDeque;
use std::fmt;
use std::time::Instant;
pub use crate::serve::multi_seq_kv::SlotId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RequestId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SlotHandle {
pub slot_id: SlotId,
pub generation: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchedulerPolicy {
FifoSerial,
InflightBatched,
}
#[derive(Debug, Clone)]
pub struct RequestSlot {
pub request_id: RequestId,
pub handle: Option<SlotHandle>,
pub admitted_at: Instant,
pub prompt_tokens: u32,
pub max_tokens: u32,
}
#[derive(Debug, Clone)]
pub struct AdmitRequest {
pub prompt_tokens: u32,
pub max_tokens: u32,
pub kv_bytes_needed: u64,
}
impl Default for AdmitRequest {
fn default() -> Self {
Self {
prompt_tokens: 0,
max_tokens: 0,
kv_bytes_needed: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchedulerStep {
Idle,
Prefill { handle: SlotHandle, n_tokens: u32 },
Decode { handles: Vec<SlotHandle> },
Mixed {
prefill: SlotHandle,
n_prefill_tokens: u32,
decode_handles: Vec<SlotHandle>,
},
}
pub const DEFAULT_PREFILL_CHUNK_TOKENS: u32 = 512;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AdmitError {
QueueFull {
queue_capacity: u32,
total_admissible: u32,
in_flight: u32,
},
SchedulerStopped,
SlotBudgetExceeded {
needed_bytes: u64,
budget_bytes: u64,
},
}
impl fmt::Display for AdmitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AdmitError::QueueFull {
queue_capacity,
total_admissible,
in_flight,
} => write!(
f,
"queue full (queue_capacity={}, total_admissible={}, in_flight={})",
queue_capacity, total_admissible, in_flight,
),
AdmitError::SchedulerStopped => write!(f, "scheduler stopped"),
AdmitError::SlotBudgetExceeded {
needed_bytes,
budget_bytes,
} => write!(
f,
"per-slot KV budget exceeded (needed_bytes={}, budget_bytes={}) \
— ADR-040 §3.5: reduce max_tokens or request a smaller prompt",
needed_bytes, budget_bytes,
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StepError {
EngineFailed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerStats {
pub policy: SchedulerPolicy,
pub in_flight_slots: u32,
pub queue_capacity: u32,
pub admitted_total: u64,
pub rejected_429_total: u64,
pub completed_total: u64,
}
pub trait Scheduler: Send {
fn policy(&self) -> SchedulerPolicy;
fn admit(&mut self, req: AdmitRequest) -> Result<RequestSlot, AdmitError>;
fn step(&mut self) -> Result<SchedulerStep, StepError>;
fn release(&mut self, handle: SlotHandle);
fn stats(&self) -> SchedulerStats;
}
pub struct FifoSchedulerAdapter {
queue_capacity: u32,
queue: VecDeque<QueuedFifoRequest>,
in_flight: Option<InFlightFifoSlot>,
slot_generation: u64,
next_request_id: u64,
admitted_total: u64,
rejected_429_total: u64,
completed_total: u64,
per_slot_kv_budget_bytes: u64,
}
#[derive(Debug, Clone)]
struct QueuedFifoRequest {
request_id: RequestId,
admitted_at: Instant,
prompt_tokens: u32,
max_tokens: u32,
}
#[derive(Debug, Clone)]
struct InFlightFifoSlot {
request_id: RequestId,
handle: SlotHandle,
admitted_at: Instant,
prompt_tokens: u32,
max_tokens: u32,
prefill_remaining: Option<u32>,
tokens_produced: u32,
}
impl FifoSchedulerAdapter {
pub fn new(queue_capacity: u32) -> Self {
Self::new_with_kv_budget(queue_capacity, 0)
}
pub fn new_with_kv_budget(queue_capacity: u32, per_slot_kv_budget_bytes: u64) -> Self {
let queue_capacity = queue_capacity.max(1);
Self {
queue_capacity,
queue: VecDeque::new(),
in_flight: None,
slot_generation: 0,
next_request_id: 0,
admitted_total: 0,
rejected_429_total: 0,
completed_total: 0,
per_slot_kv_budget_bytes,
}
}
pub fn per_slot_kv_budget_bytes(&self) -> u64 {
self.per_slot_kv_budget_bytes
}
fn in_flight_count(&self) -> u32 {
if self.in_flight.is_some() {
1
} else {
0
}
}
fn total_admissible(&self) -> u32 {
self.queue_capacity.saturating_add(1)
}
fn next_request_id(&mut self) -> RequestId {
let id = RequestId(self.next_request_id);
self.next_request_id = self.next_request_id.wrapping_add(1);
id
}
fn build_in_flight(
&self,
request_id: RequestId,
admitted_at: Instant,
prompt_tokens: u32,
max_tokens: u32,
) -> InFlightFifoSlot {
InFlightFifoSlot {
request_id,
handle: SlotHandle {
slot_id: SlotId(0),
generation: self.slot_generation,
},
admitted_at,
prompt_tokens,
max_tokens,
prefill_remaining: if prompt_tokens == 0 {
None
} else {
Some(prompt_tokens)
},
tokens_produced: 0,
}
}
pub fn cancel_queued(&mut self, request_id: RequestId) -> bool {
let before = self.queue.len();
self.queue.retain(|q| q.request_id != request_id);
let removed = before > self.queue.len();
if removed {
self.completed_total = self.completed_total.saturating_add(1);
}
removed
}
pub fn slot_generation(&self, _slot_id: SlotId) -> u64 {
self.slot_generation
}
fn promote_one(&mut self) {
debug_assert!(
self.in_flight.is_none(),
"promote_one called with in_flight occupied"
);
while let Some(q) = self.queue.pop_front() {
if classify_admit(q.prompt_tokens, q.max_tokens)
== InitialAdmitOutcome::CompletedAtAdmit
{
self.completed_total = self.completed_total.saturating_add(1);
continue;
}
self.in_flight = Some(self.build_in_flight(
q.request_id,
q.admitted_at,
q.prompt_tokens,
q.max_tokens,
));
return;
}
}
pub fn advance_after_prefill(&mut self, handle: SlotHandle, n_consumed: u32) {
let Some(slot) = self.in_flight.as_mut() else {
return;
};
if slot.handle != handle {
return;
}
let Some(remaining) = slot.prefill_remaining else {
return;
};
let new_remaining = remaining.saturating_sub(n_consumed);
slot.prefill_remaining = if new_remaining == 0 {
None
} else {
Some(new_remaining)
};
}
pub fn advance_after_decode(&mut self, handle: SlotHandle) {
let should_release = {
let Some(slot) = self.in_flight.as_mut() else {
return;
};
if slot.handle != handle {
return;
}
if slot.prefill_remaining.is_some() {
return;
}
slot.tokens_produced = slot.tokens_produced.saturating_add(1);
slot.tokens_produced >= slot.max_tokens
};
if should_release {
self.release(handle);
}
}
}
impl Scheduler for FifoSchedulerAdapter {
fn policy(&self) -> SchedulerPolicy {
SchedulerPolicy::FifoSerial
}
fn admit(&mut self, req: AdmitRequest) -> Result<RequestSlot, AdmitError> {
if self.per_slot_kv_budget_bytes > 0 && req.kv_bytes_needed > self.per_slot_kv_budget_bytes
{
self.rejected_429_total = self.rejected_429_total.saturating_add(1);
return Err(AdmitError::SlotBudgetExceeded {
needed_bytes: req.kv_bytes_needed,
budget_bytes: self.per_slot_kv_budget_bytes,
});
}
if self.queue.len() as u32 >= self.queue_capacity && self.in_flight.is_some() {
self.rejected_429_total = self.rejected_429_total.saturating_add(1);
return Err(AdmitError::QueueFull {
queue_capacity: self.queue_capacity,
total_admissible: self.total_admissible(),
in_flight: self.in_flight_count(),
});
}
let request_id = self.next_request_id();
let admitted_at = Instant::now();
self.admitted_total = self.admitted_total.saturating_add(1);
if classify_admit(req.prompt_tokens, req.max_tokens)
== InitialAdmitOutcome::CompletedAtAdmit
{
self.completed_total = self.completed_total.saturating_add(1);
return Ok(RequestSlot {
request_id,
handle: None,
admitted_at,
prompt_tokens: req.prompt_tokens,
max_tokens: req.max_tokens,
});
}
let public = if self.in_flight.is_none() {
let slot =
self.build_in_flight(request_id, admitted_at, req.prompt_tokens, req.max_tokens);
let public = RequestSlot {
request_id,
handle: Some(slot.handle),
admitted_at,
prompt_tokens: req.prompt_tokens,
max_tokens: req.max_tokens,
};
self.in_flight = Some(slot);
public
} else {
self.queue.push_back(QueuedFifoRequest {
request_id,
admitted_at,
prompt_tokens: req.prompt_tokens,
max_tokens: req.max_tokens,
});
RequestSlot {
request_id,
handle: None,
admitted_at,
prompt_tokens: req.prompt_tokens,
max_tokens: req.max_tokens,
}
};
Ok(public)
}
fn step(&mut self) -> Result<SchedulerStep, StepError> {
let Some(slot) = self.in_flight.as_ref() else {
return Ok(SchedulerStep::Idle);
};
match slot.prefill_remaining {
Some(remaining) => Ok(SchedulerStep::Prefill {
handle: slot.handle,
n_tokens: remaining,
}),
None => Ok(SchedulerStep::Decode {
handles: vec![slot.handle],
}),
}
}
fn release(&mut self, handle: SlotHandle) {
let Some(slot) = self.in_flight.as_ref() else {
return;
};
if slot.handle != handle {
return;
}
self.in_flight = None;
self.slot_generation = self.slot_generation.saturating_add(1);
self.completed_total = self.completed_total.saturating_add(1);
self.promote_one();
}
fn stats(&self) -> SchedulerStats {
SchedulerStats {
policy: SchedulerPolicy::FifoSerial,
in_flight_slots: self.in_flight_count(),
queue_capacity: self.queue_capacity,
admitted_total: self.admitted_total,
rejected_429_total: self.rejected_429_total,
completed_total: self.completed_total,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SlotPhase {
Prefilling {
tokens_remaining: u32,
},
Decoding {
tokens_produced: u32,
max_tokens: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InitialAdmitOutcome {
PhaseToPrefilling,
PhaseToDecoding,
CompletedAtAdmit,
}
fn classify_admit(prompt_tokens: u32, max_tokens: u32) -> InitialAdmitOutcome {
if max_tokens == 0 {
return InitialAdmitOutcome::CompletedAtAdmit;
}
if prompt_tokens == 0 {
return InitialAdmitOutcome::PhaseToDecoding;
}
InitialAdmitOutcome::PhaseToPrefilling
}
#[derive(Debug, Clone)]
struct InflightSlot {
request_id: RequestId,
handle: SlotHandle,
admitted_at: Instant,
prompt_tokens: u32,
max_tokens: u32,
phase: SlotPhase,
}
#[derive(Debug, Clone)]
struct QueuedInflightRequest {
request_id: RequestId,
admitted_at: Instant,
prompt_tokens: u32,
max_tokens: u32,
}
pub struct InflightBatchedScheduler {
queue_capacity: u32,
max_slots: u32,
in_flight: Vec<InflightSlot>,
queue: VecDeque<QueuedInflightRequest>,
slot_id_free_list: Vec<SlotId>,
next_fresh_slot_id: u32,
slot_generations: Vec<u64>,
next_request_id: u64,
admitted_total: u64,
rejected_429_total: u64,
completed_total: u64,
per_slot_kv_budget_bytes: u64,
}
impl InflightBatchedScheduler {
pub fn new(queue_capacity: u32, max_slots: u32) -> Self {
Self::new_with_kv_budget(queue_capacity, max_slots, 0)
}
pub fn new_with_kv_budget(
queue_capacity: u32,
max_slots: u32,
per_slot_kv_budget_bytes: u64,
) -> Self {
let queue_capacity = queue_capacity.max(1);
let max_slots = max_slots.max(1);
Self {
queue_capacity,
max_slots,
in_flight: Vec::with_capacity(max_slots as usize),
queue: VecDeque::new(),
slot_id_free_list: Vec::new(),
next_fresh_slot_id: 0,
slot_generations: vec![0; max_slots as usize],
next_request_id: 0,
admitted_total: 0,
rejected_429_total: 0,
completed_total: 0,
per_slot_kv_budget_bytes,
}
}
pub fn per_slot_kv_budget_bytes(&self) -> u64 {
self.per_slot_kv_budget_bytes
}
fn alloc_slot_id(&mut self) -> SlotId {
if let Some(id) = self.slot_id_free_list.pop() {
return id;
}
debug_assert!(
self.next_fresh_slot_id < self.max_slots,
"alloc_slot_id called past max_slots — caller violated in_flight cap invariant"
);
let id = SlotId(self.next_fresh_slot_id);
self.next_fresh_slot_id += 1;
id
}
fn total_admissible(&self) -> u32 {
self.queue_capacity.saturating_add(self.max_slots)
}
fn next_request_id(&mut self) -> RequestId {
let id = RequestId(self.next_request_id);
self.next_request_id = self.next_request_id.wrapping_add(1);
id
}
pub fn slot_generation(&self, slot_id: SlotId) -> u64 {
let idx = slot_id.0 as usize;
debug_assert!(
idx < self.slot_generations.len(),
"slot_generation called with out-of-bounds slot_id"
);
self.slot_generations[idx]
}
fn initial_admit_outcome(prompt_tokens: u32, max_tokens: u32) -> InitialAdmitOutcome {
classify_admit(prompt_tokens, max_tokens)
}
fn try_promote_one_queued(&mut self) -> Option<SlotHandle> {
if (self.in_flight.len() as u32) >= self.max_slots {
return None;
}
while let Some(q) = self.queue.pop_front() {
match classify_admit(q.prompt_tokens, q.max_tokens) {
InitialAdmitOutcome::CompletedAtAdmit => {
self.completed_total = self.completed_total.saturating_add(1);
continue;
}
outcome @ (InitialAdmitOutcome::PhaseToPrefilling
| InitialAdmitOutcome::PhaseToDecoding) => {
let slot_id = self.alloc_slot_id();
let generation = self.slot_generations[slot_id.0 as usize];
let handle = SlotHandle {
slot_id,
generation,
};
let phase = match outcome {
InitialAdmitOutcome::PhaseToPrefilling => SlotPhase::Prefilling {
tokens_remaining: q.prompt_tokens,
},
InitialAdmitOutcome::PhaseToDecoding => SlotPhase::Decoding {
tokens_produced: 0,
max_tokens: q.max_tokens,
},
InitialAdmitOutcome::CompletedAtAdmit => {
unreachable!("inner match already discriminated CompletedAtAdmit")
}
};
self.in_flight.push(InflightSlot {
request_id: q.request_id,
handle,
admitted_at: q.admitted_at,
prompt_tokens: q.prompt_tokens,
max_tokens: q.max_tokens,
phase,
});
return Some(handle);
}
}
}
None
}
fn first_prefilling_idx(&self) -> Option<usize> {
self.in_flight
.iter()
.position(|s| matches!(s.phase, SlotPhase::Prefilling { .. }))
}
fn collect_decoding_handles(&self) -> Vec<SlotHandle> {
self.in_flight
.iter()
.filter(|s| matches!(s.phase, SlotPhase::Decoding { .. }))
.map(|s| s.handle)
.collect()
}
pub fn advance_after_prefill(&mut self, handle: SlotHandle, n_consumed: u32) {
let Some(idx) = self.in_flight.iter().position(|s| s.handle == handle) else {
return;
};
let slot = &mut self.in_flight[idx];
let SlotPhase::Prefilling { tokens_remaining } = slot.phase else {
return;
};
let new_remaining = tokens_remaining.saturating_sub(n_consumed);
slot.phase = if new_remaining == 0 {
SlotPhase::Decoding {
tokens_produced: 0,
max_tokens: slot.max_tokens,
}
} else {
SlotPhase::Prefilling {
tokens_remaining: new_remaining,
}
};
}
pub fn advance_after_decode(&mut self, handle: SlotHandle) {
let Some(idx) = self.in_flight.iter().position(|s| s.handle == handle) else {
return;
};
let SlotPhase::Decoding {
tokens_produced,
max_tokens,
} = self.in_flight[idx].phase
else {
return;
};
let new_produced = tokens_produced.saturating_add(1);
if new_produced >= max_tokens {
let slot_id = handle.slot_id;
self.in_flight.remove(idx);
let gen_idx = slot_id.0 as usize;
self.slot_generations[gen_idx] = self.slot_generations[gen_idx].saturating_add(1);
self.slot_id_free_list.push(slot_id);
self.completed_total = self.completed_total.saturating_add(1);
} else {
self.in_flight[idx].phase = SlotPhase::Decoding {
tokens_produced: new_produced,
max_tokens,
};
}
}
pub fn cancel_queued(&mut self, request_id: RequestId) -> bool {
let before = self.queue.len();
self.queue.retain(|q| q.request_id != request_id);
let removed = before > self.queue.len();
if removed {
self.completed_total = self.completed_total.saturating_add(1);
}
removed
}
}
impl Scheduler for InflightBatchedScheduler {
fn policy(&self) -> SchedulerPolicy {
SchedulerPolicy::InflightBatched
}
fn admit(&mut self, req: AdmitRequest) -> Result<RequestSlot, AdmitError> {
if self.per_slot_kv_budget_bytes > 0 && req.kv_bytes_needed > self.per_slot_kv_budget_bytes
{
self.rejected_429_total = self.rejected_429_total.saturating_add(1);
return Err(AdmitError::SlotBudgetExceeded {
needed_bytes: req.kv_bytes_needed,
budget_bytes: self.per_slot_kv_budget_bytes,
});
}
let in_flight = self.in_flight.len() as u32;
let queued = self.queue.len() as u32;
if in_flight >= self.max_slots && queued >= self.queue_capacity {
self.rejected_429_total = self.rejected_429_total.saturating_add(1);
return Err(AdmitError::QueueFull {
queue_capacity: self.queue_capacity,
total_admissible: self.total_admissible(),
in_flight,
});
}
let request_id = self.next_request_id();
let admitted_at = Instant::now();
self.admitted_total = self.admitted_total.saturating_add(1);
let outcome = Self::initial_admit_outcome(req.prompt_tokens, req.max_tokens);
if matches!(outcome, InitialAdmitOutcome::CompletedAtAdmit) {
self.completed_total = self.completed_total.saturating_add(1);
return Ok(RequestSlot {
request_id,
handle: None,
admitted_at,
prompt_tokens: req.prompt_tokens,
max_tokens: req.max_tokens,
});
}
let public = if in_flight < self.max_slots {
let slot_id = self.alloc_slot_id();
let generation = self.slot_generations[slot_id.0 as usize];
let handle = SlotHandle {
slot_id,
generation,
};
let phase = match outcome {
InitialAdmitOutcome::PhaseToPrefilling => SlotPhase::Prefilling {
tokens_remaining: req.prompt_tokens,
},
InitialAdmitOutcome::PhaseToDecoding => SlotPhase::Decoding {
tokens_produced: 0,
max_tokens: req.max_tokens,
},
InitialAdmitOutcome::CompletedAtAdmit => {
unreachable!("CompletedAtAdmit already handled by the early-return above")
}
};
self.in_flight.push(InflightSlot {
request_id,
handle,
admitted_at,
prompt_tokens: req.prompt_tokens,
max_tokens: req.max_tokens,
phase,
});
RequestSlot {
request_id,
handle: Some(handle),
admitted_at,
prompt_tokens: req.prompt_tokens,
max_tokens: req.max_tokens,
}
} else {
self.queue.push_back(QueuedInflightRequest {
request_id,
admitted_at,
prompt_tokens: req.prompt_tokens,
max_tokens: req.max_tokens,
});
RequestSlot {
request_id,
handle: None,
admitted_at,
prompt_tokens: req.prompt_tokens,
max_tokens: req.max_tokens,
}
};
Ok(public)
}
fn step(&mut self) -> Result<SchedulerStep, StepError> {
let _ = self.try_promote_one_queued();
let prefill_idx = self.first_prefilling_idx();
let decode_handles = self.collect_decoding_handles();
match (prefill_idx, decode_handles.is_empty()) {
(Some(idx), false) => {
let SlotPhase::Prefilling { tokens_remaining } = self.in_flight[idx].phase else {
return Err(StepError::EngineFailed(
"first_prefilling_idx returned non-Prefilling index — invariant violated"
.to_string(),
));
};
Ok(SchedulerStep::Mixed {
prefill: self.in_flight[idx].handle,
n_prefill_tokens: tokens_remaining.min(DEFAULT_PREFILL_CHUNK_TOKENS),
decode_handles,
})
}
(Some(idx), true) => {
let SlotPhase::Prefilling { tokens_remaining } = self.in_flight[idx].phase else {
return Err(StepError::EngineFailed(
"first_prefilling_idx returned non-Prefilling index — invariant violated"
.to_string(),
));
};
Ok(SchedulerStep::Prefill {
handle: self.in_flight[idx].handle,
n_tokens: tokens_remaining.min(DEFAULT_PREFILL_CHUNK_TOKENS),
})
}
(None, false) => Ok(SchedulerStep::Decode {
handles: decode_handles,
}),
(None, true) => Ok(SchedulerStep::Idle),
}
}
fn release(&mut self, handle: SlotHandle) {
let Some(idx) = self.in_flight.iter().position(|s| s.handle == handle) else {
return;
};
self.in_flight.remove(idx);
let gen_idx = handle.slot_id.0 as usize;
self.slot_generations[gen_idx] = self.slot_generations[gen_idx].saturating_add(1);
self.slot_id_free_list.push(handle.slot_id);
self.completed_total = self.completed_total.saturating_add(1);
}
fn stats(&self) -> SchedulerStats {
SchedulerStats {
policy: SchedulerPolicy::InflightBatched,
in_flight_slots: self.in_flight.len() as u32,
queue_capacity: self.queue_capacity,
admitted_total: self.admitted_total,
rejected_429_total: self.rejected_429_total,
completed_total: self.completed_total,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn req(prompt_tokens: u32, max_tokens: u32) -> AdmitRequest {
AdmitRequest {
prompt_tokens,
max_tokens,
kv_bytes_needed: 0,
}
}
fn req_with_kv(prompt_tokens: u32, max_tokens: u32, kv_bytes_needed: u64) -> AdmitRequest {
AdmitRequest {
prompt_tokens,
max_tokens,
kv_bytes_needed,
}
}
fn handle_of(slot: &RequestSlot) -> SlotHandle {
slot.handle
.expect("expected admitted-in-flight slot (handle == Some)")
}
#[test]
fn fifo_admit_then_step_returns_prefill_for_the_admitted_slot() {
let mut s = FifoSchedulerAdapter::new(4);
let slot = s.admit(req(11, 32)).expect("admit ok");
match s.step().expect("step ok") {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, handle_of(&slot));
assert_eq!(n_tokens, 11);
}
other => panic!("expected Prefill, got {:?}", other),
}
}
#[test]
fn fifo_admit_twice_queues_second_until_first_releases() {
let mut s = FifoSchedulerAdapter::new(4);
let a = s.admit(req(10, 8)).expect("admit a");
let b = s.admit(req(20, 16)).expect("admit b");
assert!(
a.handle.is_some(),
"a admitted to in_flight has Some(handle)"
);
assert!(
b.handle.is_none(),
"b queued has None handle until promoted"
);
assert_ne!(
a.request_id, b.request_id,
"request ids are unique per admit"
);
assert_eq!(s.stats().in_flight_slots, 1);
let a_handle = handle_of(&a);
assert_eq!(a_handle.slot_id, SlotId(0));
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_handle);
assert_eq!(n_tokens, 10);
}
other => panic!("expected Prefill for a, got {:?}", other),
}
s.release(a_handle);
assert_eq!(s.stats().in_flight_slots, 1);
assert_eq!(s.stats().completed_total, 1);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle.slot_id, SlotId(0));
assert_eq!(
handle.generation,
a_handle.generation + 1,
"post-promote generation bumped by release"
);
assert_eq!(n_tokens, 20);
}
other => panic!("expected Prefill for b, got {:?}", other),
}
}
#[test]
fn fifo_admit_at_capacity_returns_queue_full_with_all_three_fields() {
let mut s = FifoSchedulerAdapter::new(2);
let _a = s.admit(req(1, 1)).expect("a in-flight");
let _b = s.admit(req(1, 1)).expect("b queued");
let _c = s.admit(req(1, 1)).expect("c queued");
match s.admit(req(1, 1)) {
Err(AdmitError::QueueFull {
queue_capacity,
total_admissible,
in_flight,
}) => {
assert_eq!(queue_capacity, 2);
assert_eq!(
total_admissible, 3,
"FIFO total = queue_capacity (2) + 1 in-flight"
);
assert_eq!(in_flight, 1, "FIFO max in_flight is 1");
}
other => panic!("expected QueueFull, got {:?}", other),
}
assert_eq!(s.stats().rejected_429_total, 1);
}
#[test]
fn fifo_release_unknown_slot_is_noop() {
let mut s = FifoSchedulerAdapter::new(4);
let _a = s.admit(req(1, 1)).expect("admit a");
s.release(SlotHandle {
slot_id: SlotId(9_999),
generation: 0,
});
assert_eq!(s.stats().completed_total, 0);
assert_eq!(s.stats().in_flight_slots, 1);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, .. } => assert_eq!(handle.slot_id, SlotId(0)),
other => panic!("expected Prefill, got {:?}", other),
}
}
#[test]
fn fifo_stats_admitted_rejected_completed_counters_advance() {
let mut s = FifoSchedulerAdapter::new(1);
let a = s.admit(req(1, 1)).expect("a");
let _b = s.admit(req(1, 1)).expect("b queued");
assert!(s.admit(req(1, 1)).is_err(), "c must be rejected");
let stats = s.stats();
assert_eq!(stats.admitted_total, 2);
assert_eq!(stats.rejected_429_total, 1);
assert_eq!(stats.completed_total, 0);
s.release(handle_of(&a));
let stats = s.stats();
assert_eq!(stats.admitted_total, 2);
assert_eq!(stats.rejected_429_total, 1);
assert_eq!(stats.completed_total, 1);
}
#[test]
fn fifo_step_returns_idle_when_no_work() {
let mut s = FifoSchedulerAdapter::new(4);
assert_eq!(s.step().unwrap(), SchedulerStep::Idle);
}
#[test]
fn fifo_policy_returns_fifoserial() {
let s = FifoSchedulerAdapter::new(4);
assert_eq!(s.policy(), SchedulerPolicy::FifoSerial);
assert_eq!(s.stats().policy, SchedulerPolicy::FifoSerial);
}
#[test]
fn fifo_decode_phase_returns_single_slot_in_decode_variant() {
let mut s = FifoSchedulerAdapter::new(4);
let a = s.admit(req(8, 4)).expect("admit a");
let a_handle = handle_of(&a);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, .. } => assert_eq!(handle, a_handle),
other => panic!("expected Prefill, got {:?}", other),
}
s.advance_after_prefill(a_handle, 8);
match s.step().unwrap() {
SchedulerStep::Decode { handles } => {
assert_eq!(handles.len(), 1);
assert_eq!(handles[0], a_handle);
}
other => panic!("expected Decode, got {:?}", other),
}
}
#[test]
fn fifo_queue_capacity_zero_normalizes_to_one() {
let s = FifoSchedulerAdapter::new(0);
assert_eq!(s.stats().queue_capacity, 1);
}
#[test]
fn fifo_serial_always_assigns_slot_id_0() {
let mut s = FifoSchedulerAdapter::new(4);
let a = s.admit(req(1, 1)).expect("admit a");
let a_handle = handle_of(&a);
assert_eq!(a_handle.slot_id, SlotId(0));
s.release(a_handle);
let b = s.admit(req(1, 1)).expect("admit b");
let b_handle = handle_of(&b);
assert_eq!(b_handle.slot_id, SlotId(0), "second admit reuses slot 0");
assert!(
b_handle.generation > a_handle.generation,
"generation bumped on release"
);
}
#[test]
fn fifo_concurrent_admits_under_mutex_match_429_boundary() {
use std::sync::{Arc, Mutex};
use std::thread;
let sched = Arc::new(Mutex::new(FifoSchedulerAdapter::new(2)));
let mut handles = vec![];
for i in 0..4 {
let s = Arc::clone(&sched);
handles.push(thread::spawn(move || {
let mut g = s.lock().unwrap();
g.admit(AdmitRequest {
prompt_tokens: 1,
max_tokens: 1,
kv_bytes_needed: 0,
})
.map(|slot| (i, slot.request_id))
}));
}
let mut admitted = 0;
let mut rejected = 0;
for h in handles {
match h.join().unwrap() {
Ok(_) => admitted += 1,
Err(AdmitError::QueueFull { .. }) => rejected += 1,
Err(e) => panic!("unexpected error: {:?}", e),
}
}
assert_eq!(admitted, 3);
assert_eq!(rejected, 1);
}
#[test]
fn fifo_admit_request_id_is_unique_and_monotonic() {
let mut s = FifoSchedulerAdapter::new(200);
let first = s.admit(req(1, 1)).expect("first admit");
let mut last_id = first.request_id;
s.release(handle_of(&first));
let mut ids = vec![last_id];
for _ in 0..99 {
let r = s.admit(req(1, 1)).expect("admit ok");
assert!(r.request_id.0 > last_id.0, "request id monotonic");
last_id = r.request_id;
ids.push(last_id);
s.release(handle_of(&r));
}
let mut sorted = ids.clone();
sorted.sort_by_key(|id| id.0);
sorted.dedup_by_key(|id| id.0);
assert_eq!(sorted.len(), ids.len(), "all request ids distinct");
}
#[test]
fn fifo_cancel_queued_removes_exactly_one_not_all() {
let mut s = FifoSchedulerAdapter::new(4);
let _a = s.admit(req(1, 1)).expect("a in_flight");
let b = s.admit(req(1, 1)).expect("b queued");
let _c = s.admit(req(1, 1)).expect("c queued");
let _d = s.admit(req(1, 1)).expect("d queued");
assert_eq!(s.stats().in_flight_slots, 1);
assert_eq!(s.queue.len(), 3, "3 queued");
let removed = s.cancel_queued(b.request_id);
assert!(removed, "cancel returns true for known request");
assert_eq!(
s.queue.len(),
2,
"exactly one queued request removed (was 3)"
);
assert_eq!(s.stats().in_flight_slots, 1, "in_flight unaffected");
assert_eq!(s.stats().completed_total, 1, "cancellation bumps completed");
}
#[test]
fn fifo_cancel_queued_unknown_request_id_returns_false() {
let mut s = FifoSchedulerAdapter::new(4);
let _a = s.admit(req(1, 1)).expect("a in_flight");
assert!(!s.cancel_queued(RequestId(99_999)));
assert_eq!(s.stats().completed_total, 0);
}
#[test]
fn fifo_slot_generation_bumps_on_release() {
let mut s = FifoSchedulerAdapter::new(4);
assert_eq!(s.slot_generation(SlotId(0)), 0);
let a = s.admit(req(1, 1)).expect("a");
let a_handle = handle_of(&a);
assert_eq!(a_handle.generation, 0);
s.release(a_handle);
assert_eq!(
s.slot_generation(SlotId(0)),
1,
"generation bumped on release"
);
}
#[test]
fn fifo_stale_handle_after_recycle_is_noop() {
let mut s = FifoSchedulerAdapter::new(4);
let a = s.admit(req(1, 1)).expect("a");
let a_handle = handle_of(&a);
s.advance_after_prefill(a_handle, 1);
s.advance_after_decode(a_handle); assert_eq!(s.stats().in_flight_slots, 0);
let b = s.admit(req(5, 3)).expect("b");
let b_handle = handle_of(&b);
assert_eq!(b_handle.slot_id, a_handle.slot_id);
assert_eq!(b_handle.generation, a_handle.generation + 1);
s.advance_after_prefill(a_handle, 5);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, b_handle);
assert_eq!(n_tokens, 5, "B's prompt_tokens untouched by stale callback");
}
other => panic!("expected Prefill for B, got {:?}", other),
}
s.advance_after_decode(a_handle);
assert_eq!(
s.stats().completed_total,
1,
"stale advance did not bump completed"
);
}
#[test]
fn inflight_admit_succeeds_below_max_slots() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(1, 1)).expect("a");
let b = s.admit(req(1, 1)).expect("b");
let a_h = handle_of(&a);
let b_h = handle_of(&b);
assert_ne!(a_h.slot_id, b_h.slot_id, "distinct physical slots");
assert_eq!(s.stats().in_flight_slots, 2);
assert_eq!(s.stats().admitted_total, 2);
}
#[test]
fn inflight_admit_returns_queue_full_at_capacity_plus_max_slots() {
let mut s = InflightBatchedScheduler::new(2, 2);
let _ = s.admit(req(1, 1)).expect("in-flight 0");
let _ = s.admit(req(1, 1)).expect("in-flight 1");
let _ = s.admit(req(1, 1)).expect("queued 0");
let _ = s.admit(req(1, 1)).expect("queued 1");
match s.admit(req(1, 1)) {
Err(AdmitError::QueueFull {
queue_capacity,
total_admissible,
in_flight,
}) => {
assert_eq!(queue_capacity, 2);
assert_eq!(total_admissible, 4);
assert_eq!(in_flight, 2);
}
other => panic!("expected QueueFull, got {:?}", other),
}
assert_eq!(s.stats().rejected_429_total, 1);
}
#[test]
fn inflight_policy_returns_inflightbatched() {
let s = InflightBatchedScheduler::new(4, 2);
assert_eq!(s.policy(), SchedulerPolicy::InflightBatched);
assert_eq!(s.stats().policy, SchedulerPolicy::InflightBatched);
}
#[test]
fn inflight_release_drops_slot_from_in_flight() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(1, 1)).expect("a");
let b = s.admit(req(1, 1)).expect("b");
assert_eq!(s.stats().in_flight_slots, 2);
s.release(handle_of(&a));
assert_eq!(s.stats().in_flight_slots, 1);
assert_eq!(s.stats().completed_total, 1);
s.release(handle_of(&b));
assert_eq!(s.stats().in_flight_slots, 0);
assert_eq!(s.stats().completed_total, 2);
s.release(SlotHandle {
slot_id: SlotId(9_999),
generation: 0,
});
assert_eq!(s.stats().completed_total, 2);
}
#[test]
fn inflight_stats_counters_advance() {
let mut s = InflightBatchedScheduler::new(1, 1);
let a = s.admit(req(1, 1)).expect("a in-flight");
let _b = s.admit(req(1, 1)).expect("b queued");
assert!(s.admit(req(1, 1)).is_err(), "c must be rejected");
let stats = s.stats();
assert_eq!(stats.admitted_total, 2);
assert_eq!(stats.rejected_429_total, 1);
assert_eq!(stats.completed_total, 0);
assert_eq!(stats.in_flight_slots, 1);
s.release(handle_of(&a));
assert_eq!(s.stats().completed_total, 1);
assert_eq!(s.stats().in_flight_slots, 0);
match s.step().unwrap() {
SchedulerStep::Prefill { .. } => {}
other => panic!("expected Prefill for promoted b, got {:?}", other),
}
let stats = s.stats();
assert_eq!(stats.completed_total, 1);
assert_eq!(stats.in_flight_slots, 1);
}
#[test]
fn inflight_step_empty_returns_idle() {
let mut s = InflightBatchedScheduler::new(4, 2);
assert_eq!(s.step().unwrap(), SchedulerStep::Idle);
}
#[test]
fn inflight_step_admit_then_step_returns_prefill_for_admitted_slot() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(13, 32)).expect("admit a");
let a_h = handle_of(&a);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_h);
assert_eq!(n_tokens, 13);
}
other => panic!("expected Prefill, got {:?}", other),
}
}
#[test]
fn inflight_step_after_prefill_completes_returns_decode() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(3, 8)).expect("admit a");
let a_h = handle_of(&a);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_h);
assert_eq!(n_tokens, 3);
}
other => panic!("expected Prefill, got {:?}", other),
}
s.advance_after_prefill(a_h, 3);
match s.step().unwrap() {
SchedulerStep::Decode { handles } => {
assert_eq!(handles.len(), 1);
assert_eq!(handles[0], a_h);
}
other => panic!("expected Decode, got {:?}", other),
}
}
#[test]
fn inflight_step_decode_advances_per_token() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(2, 4)).expect("admit a");
let a_h = handle_of(&a);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_h);
assert_eq!(n_tokens, 2);
}
other => panic!("expected Prefill, got {:?}", other),
}
s.advance_after_prefill(a_h, 2);
for i in 1..=4u32 {
match s.step().unwrap() {
SchedulerStep::Decode { handles } => {
assert_eq!(handles, vec![a_h], "decode iter {}", i);
}
other => panic!("expected Decode at iter {}, got {:?}", i, other),
}
s.advance_after_decode(a_h);
}
assert_eq!(s.stats().in_flight_slots, 0);
assert_eq!(s.stats().completed_total, 1);
assert_eq!(s.step().unwrap(), SchedulerStep::Idle);
}
#[test]
fn inflight_step_promotes_queued_when_slot_frees() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(5, 4)).expect("a in-flight");
let _b = s.admit(req(7, 4)).expect("b in-flight");
let _c = s.admit(req(11, 4)).expect("c queued");
assert_eq!(s.stats().in_flight_slots, 2);
s.release(handle_of(&a));
assert_eq!(s.stats().in_flight_slots, 1);
assert_eq!(s.stats().completed_total, 1);
match s.step().unwrap() {
SchedulerStep::Prefill {
handle: _,
n_tokens,
} => {
assert_eq!(n_tokens, 7, "first Prefilling slot in FIFO order is b");
}
other => panic!("expected Prefill after promotion, got {:?}", other),
}
assert_eq!(
s.stats().in_flight_slots,
2,
"c was promoted into the freed slot"
);
}
#[test]
fn inflight_step_returns_mixed_when_prefill_and_decode_coexist() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(4, 8)).expect("admit a");
let _b = s.admit(req(5, 8)).expect("admit b");
let a_h = handle_of(&a);
s.step().unwrap();
s.advance_after_prefill(a_h, 4);
let b_h = s
.in_flight
.iter()
.find(|x| x.handle != a_h)
.map(|x| x.handle)
.expect("b in_flight");
s.step().unwrap();
s.advance_after_prefill(b_h, 5);
match s.step().unwrap() {
SchedulerStep::Decode { handles } => assert_eq!(handles.len(), 2),
other => panic!("expected Decode of 2 slots, got {:?}", other),
}
let c = s.admit(req(9, 8)).expect("admit c queued");
assert!(
c.handle.is_none(),
"queued slot has None handle (no sentinel)"
);
for _ in 0..8 {
s.advance_after_decode(a_h);
}
assert_eq!(s.stats().in_flight_slots, 1, "A auto-released");
match s.step().unwrap() {
SchedulerStep::Mixed {
prefill,
n_prefill_tokens,
decode_handles,
} => {
assert_eq!(n_prefill_tokens, 9);
assert_eq!(decode_handles.len(), 1);
assert_eq!(decode_handles[0], b_h);
assert_eq!(prefill.slot_id, SlotId(0), "c got a's recycled slot id");
assert!(
prefill.generation > a_h.generation,
"c's generation is bumped past a's"
);
}
other => panic!("expected Mixed, got {:?}", other),
}
}
#[test]
fn inflight_step_chunks_prefill_at_default_size() {
assert_eq!(DEFAULT_PREFILL_CHUNK_TOKENS, 512);
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(1500, 4)).expect("admit a");
let a_h = handle_of(&a);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_h);
assert_eq!(n_tokens, 512);
}
other => panic!("expected Prefill(512), got {:?}", other),
}
s.advance_after_prefill(a_h, 512);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_h);
assert_eq!(n_tokens, 512);
}
other => panic!("expected Prefill(512), got {:?}", other),
}
s.advance_after_prefill(a_h, 512);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_h);
assert_eq!(n_tokens, 476);
}
other => panic!("expected Prefill(476), got {:?}", other),
}
s.advance_after_prefill(a_h, 476);
match s.step().unwrap() {
SchedulerStep::Decode { handles } => {
assert_eq!(handles, vec![a_h]);
}
other => panic!("expected Decode after chunked prefill, got {:?}", other),
}
}
#[test]
fn inflight_step_auto_releases_on_max_tokens() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(1, 2)).expect("admit a");
let a_h = handle_of(&a);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_h);
assert_eq!(n_tokens, 1);
}
other => panic!("expected Prefill, got {:?}", other),
}
s.advance_after_prefill(a_h, 1);
match s.step().unwrap() {
SchedulerStep::Decode { handles } => assert_eq!(handles, vec![a_h]),
other => panic!("expected Decode iter 1, got {:?}", other),
}
s.advance_after_decode(a_h);
assert_eq!(s.stats().in_flight_slots, 1);
assert_eq!(s.stats().completed_total, 0);
match s.step().unwrap() {
SchedulerStep::Decode { handles } => assert_eq!(handles, vec![a_h]),
other => panic!("expected Decode iter 2, got {:?}", other),
}
s.advance_after_decode(a_h);
assert_eq!(s.stats().in_flight_slots, 0);
assert_eq!(s.stats().completed_total, 1);
assert_eq!(s.step().unwrap(), SchedulerStep::Idle);
}
#[test]
fn inflight_advance_after_prefill_unknown_slot_is_noop() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(5, 8)).expect("admit a");
let a_h = handle_of(&a);
s.advance_after_prefill(
SlotHandle {
slot_id: SlotId(9_999),
generation: 0,
},
100,
);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_h);
assert_eq!(n_tokens, 5);
}
other => panic!("expected Prefill, got {:?}", other),
}
s.advance_after_prefill(a_h, 5);
s.advance_after_prefill(a_h, 1); match s.step().unwrap() {
SchedulerStep::Decode { handles } => {
assert_eq!(handles, vec![a_h]);
}
other => panic!("expected Decode, got {:?}", other),
}
}
#[test]
fn inflight_advance_after_decode_overflow_is_clamped_at_max_tokens() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(1, 3)).expect("admit a, max_tokens=3");
let a_h = handle_of(&a);
s.advance_after_prefill(a_h, 1);
s.advance_after_decode(a_h);
assert_eq!(s.stats().in_flight_slots, 1);
s.advance_after_decode(a_h);
assert_eq!(s.stats().in_flight_slots, 1);
s.advance_after_decode(a_h);
assert_eq!(s.stats().in_flight_slots, 0);
assert_eq!(s.stats().completed_total, 1);
s.advance_after_decode(a_h);
s.advance_after_decode(a_h);
assert_eq!(s.stats().completed_total, 1);
}
#[test]
fn inflight_max_slots_zero_normalizes_to_one() {
let mut s = InflightBatchedScheduler::new(4, 0);
let a = s.admit(req(3, 1)).expect("admit must succeed");
let a_h = handle_of(&a);
assert_eq!(s.stats().in_flight_slots, 1);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_h);
assert_eq!(n_tokens, 3);
}
other => panic!("expected Prefill, got {:?}", other),
}
}
#[test]
fn inflight_slot_generation_bumps_on_release() {
let mut s = InflightBatchedScheduler::new(4, 2);
assert_eq!(s.slot_generation(SlotId(0)), 0);
let a = s.admit(req(1, 1)).expect("a");
let a_h = handle_of(&a);
assert_eq!(a_h.generation, 0);
s.release(a_h);
assert_eq!(s.slot_generation(SlotId(0)), 1, "release bumps generation");
}
#[test]
fn inflight_slot_generation_bumps_on_auto_release() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(1, 1)).expect("a");
let a_h = handle_of(&a);
s.advance_after_prefill(a_h, 1);
s.advance_after_decode(a_h); assert_eq!(
s.slot_generation(a_h.slot_id),
1,
"auto-release bumps generation just like explicit release"
);
}
#[test]
fn inflight_stale_callback_after_recycle_is_noop_not_corrupt() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(1, 1)).expect("a"); let a_h = handle_of(&a);
s.step().unwrap();
s.advance_after_prefill(a_h, 1);
s.step().unwrap();
s.advance_after_decode(a_h); assert_eq!(s.stats().in_flight_slots, 0);
let b = s.admit(req(8, 4)).expect("b");
let b_h = handle_of(&b);
assert_eq!(b_h.slot_id, a_h.slot_id, "B got A's recycled slot id");
assert_eq!(
b_h.generation,
a_h.generation + 1,
"B's generation is exactly one past A's"
);
s.advance_after_prefill(a_h, 5);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, b_h);
assert_eq!(n_tokens, 8, "B's prompt untouched by stale callback");
}
other => panic!("expected Prefill for B, got {:?}", other),
}
s.advance_after_decode(a_h);
assert_eq!(
s.stats().completed_total,
1,
"stale decode did not double-complete"
);
}
#[test]
fn inflight_handle_carries_correct_generation_at_promote() {
let mut s = InflightBatchedScheduler::new(4, 1);
let a = s.admit(req(1, 1)).expect("a in_flight");
let a_h = handle_of(&a);
let b = s.admit(req(7, 1)).expect("b queued");
assert!(b.handle.is_none(), "b queued has None handle");
s.advance_after_prefill(a_h, 1);
s.advance_after_decode(a_h);
assert_eq!(s.stats().in_flight_slots, 0);
match s.step().unwrap() {
SchedulerStep::Prefill {
handle: b_h,
n_tokens,
} => {
assert_eq!(n_tokens, 7);
assert_eq!(b_h.slot_id, a_h.slot_id, "B recycled A's slot id");
assert_eq!(
b_h.generation,
a_h.generation + 1,
"B's promoted handle generation == prior slot generation + 1"
);
assert_eq!(s.slot_generation(b_h.slot_id), b_h.generation);
}
other => panic!("expected Prefill for promoted B, got {:?}", other),
}
}
#[test]
fn inflight_concurrent_advance_under_mutex_with_handle_safe() {
use std::sync::{Arc, Mutex};
use std::thread;
let sched = Arc::new(Mutex::new(InflightBatchedScheduler::new(6, 3)));
let mut handles = vec![];
for thread_idx in 0..3u32 {
let s = Arc::clone(&sched);
handles.push(thread::spawn(move || {
let _ = {
let mut g = s.lock().unwrap();
g.admit(AdmitRequest {
prompt_tokens: 1,
max_tokens: 4,
kv_bytes_needed: 0,
})
.expect("admit ok")
};
for _ in 0..5 {
let action: SchedulerStep = {
let mut g = s.lock().unwrap();
g.step().unwrap()
};
match action {
SchedulerStep::Prefill { handle, n_tokens } => {
let mut g = s.lock().unwrap();
g.advance_after_prefill(handle, n_tokens);
}
SchedulerStep::Decode { handles } => {
let mut g = s.lock().unwrap();
for h in handles {
g.advance_after_decode(h);
}
}
SchedulerStep::Mixed {
prefill,
n_prefill_tokens,
decode_handles,
} => {
let mut g = s.lock().unwrap();
g.advance_after_prefill(prefill, n_prefill_tokens);
for h in decode_handles {
g.advance_after_decode(h);
}
}
SchedulerStep::Idle => {}
}
}
let _ = thread_idx;
}));
}
for h in handles {
h.join().unwrap();
}
let stats = sched.lock().unwrap().stats();
assert_eq!(stats.admitted_total, 3);
assert_eq!(stats.completed_total, 3);
assert_eq!(stats.in_flight_slots, 0);
assert_eq!(stats.rejected_429_total, 0);
}
#[test]
fn inflight_admit_returns_request_id_and_no_handle_when_queued() {
let mut s = InflightBatchedScheduler::new(2, 1);
let a = s.admit(req(1, 1)).expect("a");
let b = s.admit(req(1, 1)).expect("b queued");
assert!(a.handle.is_some(), "a in_flight has Some handle");
assert!(b.handle.is_none(), "b queued has None handle");
assert_ne!(a.request_id, b.request_id);
}
#[test]
fn inflight_cancel_queued_removes_exactly_one_not_all() {
let mut s = InflightBatchedScheduler::new(4, 1);
let _a = s.admit(req(1, 1)).expect("a in_flight");
let q1 = s.admit(req(1, 1)).expect("q1 queued");
let q2 = s.admit(req(1, 1)).expect("q2 queued");
let _q3 = s.admit(req(1, 1)).expect("q3 queued");
assert_eq!(s.queue.len(), 3);
let removed = s.cancel_queued(q2.request_id);
assert!(removed);
assert_eq!(
s.queue.len(),
2,
"exactly one queued request removed (regression: previously ALL queued removed)"
);
assert_eq!(
s.stats().in_flight_slots,
1,
"in_flight unaffected by cancel_queued"
);
let remaining_ids: Vec<_> = s.queue.iter().map(|q| q.request_id).collect();
assert!(remaining_ids.contains(&q1.request_id), "q1 still queued");
assert!(!remaining_ids.contains(&q2.request_id), "q2 removed");
}
#[test]
fn inflight_cancel_queued_unknown_request_id_returns_false() {
let mut s = InflightBatchedScheduler::new(4, 1);
let _a = s.admit(req(1, 1)).expect("a in_flight");
assert!(!s.cancel_queued(RequestId(99_999)));
assert_eq!(s.stats().completed_total, 0);
}
#[test]
fn inflight_admit_request_id_is_unique_and_monotonic() {
let mut s = InflightBatchedScheduler::new(200, 1);
let mut last = None;
let mut ids = vec![];
for _ in 0..100 {
let r = s.admit(req(1, 1)).expect("admit ok");
if let Some(l) = last {
let prev: RequestId = l;
assert!(r.request_id.0 > prev.0, "request_id monotonic");
}
last = Some(r.request_id);
ids.push(r.request_id);
}
assert_eq!(ids.len(), 100);
let mut sorted = ids.clone();
sorted.sort_by_key(|id| id.0);
sorted.dedup_by_key(|id| id.0);
assert_eq!(sorted.len(), 100, "all 100 request ids distinct");
}
#[test]
fn inflight_step_priority_older_prefilling_wins_with_promotion() {
let mut s = InflightBatchedScheduler::new(4, 3);
let a = s.admit(req(1000, 4)).expect("a");
let a_h = handle_of(&a);
match s.step().unwrap() {
SchedulerStep::Prefill { handle, n_tokens } => {
assert_eq!(handle, a_h);
assert_eq!(n_tokens, 512);
}
other => panic!("expected Prefill(A, 512), got {:?}", other),
}
s.advance_after_prefill(a_h, 512);
let b = s.admit(req(10, 4)).expect("b");
let b_h = handle_of(&b);
s.advance_after_prefill(b_h, 10);
let c = s.admit(req(5, 4)).expect("c");
let c_h = handle_of(&c);
match s.step().unwrap() {
SchedulerStep::Mixed {
prefill,
n_prefill_tokens,
decode_handles,
} => {
assert_eq!(
prefill, a_h,
"older Prefilling slot A wins over newer C (cfa-iter2.5 C3)"
);
assert_eq!(
n_prefill_tokens, 488,
"A's mid-chunk continuation must not be starved"
);
assert_eq!(decode_handles, vec![b_h]);
let _ = c_h; }
other => panic!("expected Mixed(A, 488, [B]), got {:?}", other),
}
}
#[test]
fn inflight_admit_with_prompt_tokens_0_skips_to_decoding() {
let mut s = InflightBatchedScheduler::new(4, 2);
let a = s.admit(req(0, 3)).expect("admit a with empty prompt");
let a_h = handle_of(&a);
match s.step().unwrap() {
SchedulerStep::Decode { handles } => {
assert_eq!(
handles,
vec![a_h],
"zero-prompt admit transitions directly to Decoding"
);
}
other => panic!("expected Decode (M2), got {:?}", other),
}
s.advance_after_decode(a_h);
s.advance_after_decode(a_h);
s.advance_after_decode(a_h);
assert_eq!(s.stats().in_flight_slots, 0);
assert_eq!(s.stats().completed_total, 1);
}
#[test]
fn inflight_promote_queued_with_prompt_tokens_0_skips_to_decoding() {
let mut s = InflightBatchedScheduler::new(4, 1);
let a = s.admit(req(5, 1)).expect("a in_flight");
let a_h = handle_of(&a);
let _b = s.admit(req(0, 2)).expect("b queued with zero prompt");
s.advance_after_prefill(a_h, 5);
s.advance_after_decode(a_h);
match s.step().unwrap() {
SchedulerStep::Decode { handles } => {
assert_eq!(
handles.len(),
1,
"zero-prompt promoted slot skips Prefilling, emits Decode"
);
}
other => panic!(
"expected Decode for promoted zero-prompt B, got {:?}",
other
),
}
}
#[test]
fn fifo_admit_with_prompt_tokens_0_skips_to_decoding() {
let mut s = FifoSchedulerAdapter::new(4);
let a = s.admit(req(0, 2)).expect("admit a");
let a_h = handle_of(&a);
match s.step().unwrap() {
SchedulerStep::Decode { handles } => {
assert_eq!(
handles,
vec![a_h],
"FIFO: zero-prompt admit skips Prefilling"
);
}
other => panic!("expected Decode (M2 FIFO), got {:?}", other),
}
s.advance_after_decode(a_h);
s.advance_after_decode(a_h); assert_eq!(s.stats().in_flight_slots, 0);
}
#[test]
fn fifo_admit_with_max_tokens_0_returns_handle_none() {
let mut s = FifoSchedulerAdapter::new(4);
let r = s.admit(req(8, 0)).expect("zero-budget admit must succeed");
assert!(
r.handle.is_none(),
"max_tokens=0 admit must return handle: None (no slot allocated)"
);
assert_eq!(
r.prompt_tokens, 8,
"RequestSlot still echoes input prompt_tokens"
);
assert_eq!(r.max_tokens, 0, "RequestSlot still echoes input max_tokens");
let stats = s.stats();
assert_eq!(stats.admitted_total, 1, "admitted_total bumped");
assert_eq!(
stats.completed_total, 1,
"completed_total bumped at admit time (zero-budget short-circuit)"
);
assert_eq!(
stats.in_flight_slots, 0,
"no physical slot allocated for zero-budget admit"
);
assert_eq!(
s.step().unwrap(),
SchedulerStep::Idle,
"no in-flight slot, no queued slot → Idle"
);
let b = s.admit(req(3, 5)).expect("normal admit after zero-budget");
let b_h = handle_of(&b);
assert_eq!(
b_h.slot_id,
SlotId(0),
"next admit gets the never-allocated slot 0"
);
assert_eq!(
b_h.generation, 0,
"no generation bump (no release happened — slot was never allocated)"
);
}
#[test]
fn inflight_admit_with_max_tokens_0_does_not_leak_slot() {
let mut s = InflightBatchedScheduler::new(8, 4);
for i in 0..16 {
let r = s.admit(req(3, 0)).expect("zero-budget admit must succeed");
assert!(
r.handle.is_none(),
"iter {}: max_tokens=0 admit must return handle: None",
i
);
}
let stats = s.stats();
assert_eq!(
stats.in_flight_slots, 0,
"16 zero-budget admits must leak ZERO in-flight slots (regression: \
prior iter-2.5 pushed Decoding{{0,0}} into in_flight)"
);
assert_eq!(stats.admitted_total, 16, "all 16 counted as admitted");
assert_eq!(
stats.completed_total, 16,
"all 16 counted as completed-at-admit (no slot lifecycle)"
);
assert_eq!(
stats.rejected_429_total, 0,
"no QueueFull — short-circuit happens after capacity check"
);
assert_eq!(s.step().unwrap(), SchedulerStep::Idle);
}
#[test]
fn fifo_admit_prompt_tokens_0_max_tokens_0_no_leak() {
let mut s = FifoSchedulerAdapter::new(4);
let r = s.admit(req(0, 0)).expect("both-zeros admit must succeed");
assert!(
r.handle.is_none(),
"prompt_tokens=0 AND max_tokens=0 → handle: None (max_tokens wins)"
);
let stats = s.stats();
assert_eq!(stats.admitted_total, 1);
assert_eq!(stats.completed_total, 1);
assert_eq!(stats.in_flight_slots, 0);
let mut s2 = InflightBatchedScheduler::new(4, 2);
let r2 = s2.admit(req(0, 0)).expect("both-zeros inflight admit ok");
assert!(
r2.handle.is_none(),
"inflight prompt_tokens=0 AND max_tokens=0 → handle: None"
);
let stats2 = s2.stats();
assert_eq!(stats2.in_flight_slots, 0);
assert_eq!(stats2.completed_total, 1);
}
#[test]
fn inflight_promote_queued_with_max_tokens_0_does_not_leak() {
let mut s = InflightBatchedScheduler::new(4, 2);
let _a = s
.admit(req(5, 4))
.expect("a normal admit, in_flight slot 0");
let _b = s
.admit(req(7, 4))
.expect("b normal admit, in_flight slot 1");
assert_eq!(s.stats().in_flight_slots, 2);
assert_eq!(s.queue.len(), 0);
let c = s.admit(req(11, 0)).expect("c zero-budget admit ok");
assert!(
c.handle.is_none(),
"zero-budget admit at in_flight-cap still short-circuits — not queued"
);
assert_eq!(
s.queue.len(),
0,
"zero-budget request MUST NOT enter the queue (cfa-iter-C2.5 M1)"
);
assert_eq!(s.stats().in_flight_slots, 2, "in_flight unchanged");
assert_eq!(s.stats().completed_total, 1, "c counted as completed");
s.queue.push_back(QueuedInflightRequest {
request_id: RequestId(99_999),
admitted_at: Instant::now(),
prompt_tokens: 5,
max_tokens: 0,
});
s.queue.push_back(QueuedInflightRequest {
request_id: RequestId(99_998),
admitted_at: Instant::now(),
prompt_tokens: 7,
max_tokens: 4, });
let completed_before = s.stats().completed_total;
let in_flight_before = s.stats().in_flight_slots;
s.release(handle_of(&_a));
let _ = s.step().unwrap();
let stats = s.stats();
assert!(
stats.completed_total >= completed_before + 2,
"release + zero-budget-queued-skip both bump completed_total \
(was {}, now {})",
completed_before,
stats.completed_total
);
assert_eq!(
stats.in_flight_slots, in_flight_before,
"in_flight unchanged: released a → promoted normal request \
past skipped zero-budget queued entry"
);
}
#[test]
fn request_slot_admitted_at_is_monotonic() {
let mut s = FifoSchedulerAdapter::new(4);
let a = s.admit(req(1, 1)).expect("a");
let b = s.admit(req(1, 1)).expect("b");
assert!(b.admitted_at >= a.admitted_at);
}
#[test]
fn admit_error_queue_full_names_queue_capacity_and_total_admissible_and_in_flight() {
let err = AdmitError::QueueFull {
queue_capacity: 7,
total_admissible: 8,
in_flight: 3,
};
let dbg = format!("{:?}", err);
assert!(dbg.contains("queue_capacity"));
assert!(dbg.contains("total_admissible"));
assert!(dbg.contains("in_flight"));
assert!(dbg.contains('7'));
assert!(dbg.contains('8'));
assert!(dbg.contains('3'));
let disp = format!("{}", err);
assert!(disp.contains("queue_capacity=7"));
assert!(disp.contains("total_admissible=8"));
assert!(disp.contains("in_flight=3"));
}
#[test]
fn fifo_admit_below_per_slot_budget_succeeds() {
let mut s = FifoSchedulerAdapter::new_with_kv_budget(4, 1024 * 1024);
let r = s
.admit(req_with_kv(8, 16, 512 * 1024))
.expect("admit below budget must succeed");
assert!(
r.handle.is_some(),
"below-budget admit lands in_flight with Some(handle)"
);
assert_eq!(s.stats().admitted_total, 1);
assert_eq!(s.stats().rejected_429_total, 0);
assert_eq!(s.per_slot_kv_budget_bytes(), 1024 * 1024);
}
#[test]
fn fifo_admit_above_per_slot_budget_errors_with_named_fields() {
let mut s = FifoSchedulerAdapter::new_with_kv_budget(4, 1024 * 1024);
let needed = 2 * 1024 * 1024;
match s.admit(req_with_kv(1024, 64, needed)) {
Err(AdmitError::SlotBudgetExceeded {
needed_bytes,
budget_bytes,
}) => {
assert_eq!(
needed_bytes, needed,
"error names the request's needed bytes"
);
assert_eq!(
budget_bytes,
1024 * 1024,
"error names the per-slot budget bytes"
);
}
other => panic!("expected SlotBudgetExceeded, got {:?}", other),
}
let stats = s.stats();
assert_eq!(
stats.rejected_429_total, 1,
"SlotBudgetExceeded bumps rejected_429_total (maps to 429 upstream)"
);
assert_eq!(stats.admitted_total, 0);
assert_eq!(
stats.in_flight_slots, 0,
"no physical slot allocated for over-budget admit"
);
}
#[test]
fn fifo_per_slot_budget_zero_means_unbounded() {
let mut s = FifoSchedulerAdapter::new(4);
assert_eq!(
s.per_slot_kv_budget_bytes(),
0,
"new() defaults to per_slot_kv_budget_bytes = 0 (unbounded)"
);
let r = s
.admit(req_with_kv(1, 1, u64::MAX))
.expect("zero-budget scheduler must accept any kv_bytes_needed");
assert!(r.handle.is_some());
assert_eq!(s.stats().rejected_429_total, 0);
let mut s2 = FifoSchedulerAdapter::new_with_kv_budget(4, 0);
assert_eq!(s2.per_slot_kv_budget_bytes(), 0);
s2.admit(req_with_kv(1, 1, u64::MAX))
.expect("explicit 0-budget also unbounded");
}
#[test]
fn inflight_admit_above_per_slot_budget_errors() {
let mut s = InflightBatchedScheduler::new_with_kv_budget(8, 4, 4 * 1024 * 1024);
let needed = 5 * 1024 * 1024;
match s.admit(req_with_kv(2048, 128, needed)) {
Err(AdmitError::SlotBudgetExceeded {
needed_bytes,
budget_bytes,
}) => {
assert_eq!(needed_bytes, needed);
assert_eq!(budget_bytes, 4 * 1024 * 1024);
}
other => panic!("expected SlotBudgetExceeded, got {:?}", other),
}
assert_eq!(s.stats().rejected_429_total, 1);
assert_eq!(s.stats().admitted_total, 0);
assert_eq!(
s.stats().in_flight_slots,
0,
"over-budget admit does not allocate a physical slot"
);
}
#[test]
fn admit_error_slot_budget_exceeded_display_names_needed_and_budget() {
let err = AdmitError::SlotBudgetExceeded {
needed_bytes: 12_345_678,
budget_bytes: 4_096_000,
};
let dbg = format!("{:?}", err);
assert!(dbg.contains("SlotBudgetExceeded"));
assert!(dbg.contains("needed_bytes"));
assert!(dbg.contains("budget_bytes"));
assert!(dbg.contains("12345678"));
assert!(dbg.contains("4096000"));
let disp = format!("{}", err);
assert!(
disp.contains("needed_bytes=12345678"),
"Display names needed_bytes verbatim: {}",
disp
);
assert!(
disp.contains("budget_bytes=4096000"),
"Display names budget_bytes verbatim: {}",
disp
);
assert!(
disp.contains("ADR-040"),
"Display cites ADR-040 §3.5: {}",
disp
);
assert!(
disp.contains("max_tokens") || disp.contains("prompt"),
"Display names the actionable remediation: {}",
disp
);
}
#[test]
fn inflight_per_slot_budget_independent_per_slot() {
let per_slot = 1024 * 1024;
let mut s = InflightBatchedScheduler::new_with_kv_budget(8, 4, per_slot);
let mut admitted = Vec::new();
for i in 0..4 {
let r = s
.admit(req_with_kv(64, 16, per_slot))
.unwrap_or_else(|e| panic!("slot {} at-budget admit must succeed; got {:?}", i, e));
assert!(
r.handle.is_some(),
"slot {}: at-budget admit lands in_flight",
i
);
admitted.push(r);
}
let stats = s.stats();
assert_eq!(
stats.admitted_total, 4,
"all 4 at-budget admits counted; per-slot budget does not sum"
);
assert_eq!(
stats.in_flight_slots, 4,
"all 4 physical slots occupied (max_slots=4)"
);
assert_eq!(
stats.rejected_429_total, 0,
"ZERO 429s — each request fits its own per-slot budget"
);
let r5 = s
.admit(req_with_kv(64, 16, per_slot))
.expect("5th at-budget admit queues (not a budget violation)");
assert!(
r5.handle.is_none(),
"5th admit is queued (in_flight at max_slots=4)"
);
assert_eq!(
s.stats().rejected_429_total,
0,
"queueing is not a budget rejection"
);
match s.admit(req_with_kv(64, 16, per_slot + 1)) {
Err(AdmitError::SlotBudgetExceeded {
needed_bytes,
budget_bytes,
}) => {
assert_eq!(needed_bytes, per_slot + 1);
assert_eq!(budget_bytes, per_slot);
}
other => panic!("expected SlotBudgetExceeded, got {:?}", other),
}
}
#[test]
fn admit_request_default_kv_bytes_needed_is_zero() {
let r = AdmitRequest::default();
assert_eq!(r.prompt_tokens, 0);
assert_eq!(r.max_tokens, 0);
assert_eq!(r.kv_bytes_needed, 0);
}
}