use crate::policy::pool_budget::{PoolSizes, RebuildRequest};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct EngineActivity {
pub pending_prefill: usize,
pub running_decode: usize,
pub undrained_replies: usize,
pub finished: usize,
}
impl EngineActivity {
pub fn is_idle(&self) -> bool {
self.pending_prefill == 0 && self.running_decode == 0 && self.undrained_replies == 0
}
pub fn busy_with(&self) -> Vec<&'static str> {
let mut busy = Vec::new();
if self.pending_prefill > 0 {
busy.push("prefill");
}
if self.running_decode > 0 {
busy.push("decode");
}
if self.undrained_replies > 0 {
busy.push("undrained replies");
}
busy
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RebuildOutcome {
Applied(PoolSizes),
RejectedIntact { reason: String },
RolledBack { reason: String, restored: PoolSizes },
Latched {
reason: String,
rollback_error: String,
},
}
impl RebuildOutcome {
pub fn engine_preserved(&self) -> bool {
!matches!(self, RebuildOutcome::Latched { .. })
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TouchedPools {
pub moe_cache: bool,
pub kv: bool,
}
impl TouchedPools {
fn of(request: &RebuildRequest) -> Self {
TouchedPools {
moe_cache: request.moe_cache_slots.is_some(),
kv: request.kv_pages.is_some()
|| request.mamba_slots.is_some()
|| request.swa_pages.is_some(),
}
}
pub fn any(&self) -> bool {
self.moe_cache || self.kv
}
}
#[derive(Debug)]
pub struct RebuildTxn {
rollback_to: PoolSizes,
touched: TouchedPools,
teardown_started: bool,
}
impl RebuildTxn {
pub fn open(request: &RebuildRequest, current: &PoolSizes) -> Self {
RebuildTxn {
rollback_to: *current,
touched: TouchedPools::of(request),
teardown_started: false,
}
}
pub fn touched(&self) -> TouchedPools {
self.touched
}
pub fn teardown_started(&self) -> bool {
self.teardown_started
}
pub fn rollback_target(&self) -> &PoolSizes {
&self.rollback_to
}
pub fn run(
mut self,
target: PoolSizes,
free_then_allocate: impl FnOnce(&mut dyn FnMut(), PoolSizes) -> Result<(), String>,
restore: impl FnOnce(PoolSizes, TouchedPools) -> Result<(), String>,
) -> RebuildOutcome {
let started = std::cell::Cell::new(false);
let mut mark = || started.set(true);
match free_then_allocate(&mut mark, target) {
Ok(()) => {
self.teardown_started = started.get();
RebuildOutcome::Applied(target)
}
Err(reason) => {
self.teardown_started = started.get();
if !self.teardown_started {
return RebuildOutcome::RejectedIntact { reason };
}
match restore(self.rollback_to, self.touched) {
Ok(()) => RebuildOutcome::RolledBack {
reason,
restored: self.rollback_to,
},
Err(rollback_error) => RebuildOutcome::Latched {
reason,
rollback_error,
},
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sizes(kv: u64, moe: u64) -> PoolSizes {
PoolSizes {
moe_cache_slots: moe,
kv_pages: kv,
prefill_overlap: true,
}
}
fn kv_request(pages: u64) -> RebuildRequest {
RebuildRequest {
moe_cache_slots: None,
kv_pages: Some(pages),
mamba_slots: None,
swa_pages: None,
}
}
#[test]
fn a_finished_request_does_not_hold_a_resize_but_every_live_one_does() {
let idle = EngineActivity {
finished: 12,
..EngineActivity::default()
};
assert!(idle.is_idle(), "finished rows hold nothing");
assert!(idle.busy_with().is_empty());
for busy in [
EngineActivity {
pending_prefill: 1,
..EngineActivity::default()
},
EngineActivity {
running_decode: 1,
..EngineActivity::default()
},
EngineActivity {
undrained_replies: 1,
..EngineActivity::default()
},
] {
assert!(!busy.is_idle(), "{busy:?} still holds resources");
assert_eq!(busy.busy_with().len(), 1, "and says which: {busy:?}");
}
}
#[test]
fn a_failure_before_the_free_leaves_the_engine_untouched() {
let txn = RebuildTxn::open(&kv_request(128), &sizes(64, 8));
let mut restored = false;
let outcome = txn.run(
sizes(128, 8),
|_mark, _target| Err("device refused the reservation".to_string()),
|_, _| {
restored = true;
Ok(())
},
);
assert_eq!(
outcome,
RebuildOutcome::RejectedIntact {
reason: "device refused the reservation".to_string()
}
);
assert!(
!restored,
"there is nothing to restore, so restore must not run"
);
assert!(outcome.engine_preserved());
}
#[test]
fn a_failure_after_the_free_puts_the_old_sizes_back() {
let current = sizes(64, 8);
let txn = RebuildTxn::open(&kv_request(4096), ¤t);
let mut asked_to_restore = None;
let outcome = txn.run(
sizes(4096, 8),
|mark, _target| {
mark();
Err("out of memory allocating the new pool".to_string())
},
|old, touched| {
asked_to_restore = Some((old, touched));
Ok(())
},
);
let (old, touched) = asked_to_restore.expect("the rollback ran");
assert_eq!(old, current, "the rollback target is what was there before");
assert!(touched.kv, "the KV side is what this request named");
assert!(
!touched.moe_cache,
"and the expert cache is left alone, so its prefixes survive"
);
match outcome {
RebuildOutcome::RolledBack { restored, .. } => assert_eq!(restored, current),
other => panic!("expected RolledBack, got {other:?}"),
}
assert!(
outcome.engine_preserved(),
"a resize that did not happen must not cost the engine its \
ability to serve"
);
}
#[test]
fn only_a_failed_rollback_latches_the_engine() {
let txn = RebuildTxn::open(&kv_request(4096), &sizes(64, 8));
let outcome = txn.run(
sizes(4096, 8),
|mark, _| {
mark();
Err("oom".to_string())
},
|_, _| Err("could not re-allocate the old pool either".to_string()),
);
match &outcome {
RebuildOutcome::Latched { rollback_error, .. } => {
assert!(rollback_error.contains("old pool"))
}
other => panic!("expected Latched, got {other:?}"),
}
assert!(
!outcome.engine_preserved(),
"no pools and no way back is the one state that must stop \
pretending requests can be served"
);
}
#[test]
fn a_resize_that_succeeds_reports_the_sizes_that_are_now_live() {
let txn = RebuildTxn::open(&kv_request(256), &sizes(64, 8));
let outcome = txn.run(
sizes(256, 8),
|mark, target| {
mark();
assert_eq!(target.kv_pages, 256);
Ok(())
},
|_, _| panic!("a successful resize must not restore anything"),
);
assert_eq!(outcome, RebuildOutcome::Applied(sizes(256, 8)));
assert!(outcome.engine_preserved());
}
#[test]
fn a_rollback_leaves_the_pools_the_request_never_named_alone() {
let current = sizes(64, 8);
let kv_only = RebuildTxn::open(&kv_request(256), ¤t);
assert_eq!(
kv_only.touched(),
TouchedPools {
moe_cache: false,
kv: true
}
);
assert_eq!(
kv_only.rollback_target().kv_pages,
64,
"the touched pool's old size"
);
assert!(
!kv_only.teardown_started(),
"a fresh transaction has freed nothing"
);
let moe_only = RebuildTxn::open(
&RebuildRequest {
moe_cache_slots: Some(16),
kv_pages: None,
mamba_slots: None,
swa_pages: None,
},
¤t,
);
assert_eq!(
moe_only.touched(),
TouchedPools {
moe_cache: true,
kv: false
},
"an expert-cache resize must not drag the KV pool through a \
teardown"
);
for request in [
RebuildRequest {
moe_cache_slots: None,
kv_pages: None,
mamba_slots: Some(4),
swa_pages: None,
},
RebuildRequest {
moe_cache_slots: None,
kv_pages: None,
mamba_slots: None,
swa_pages: Some(4),
},
] {
assert!(
RebuildTxn::open(&request, ¤t).touched().kv,
"{request:?}"
);
}
assert!(!RebuildTxn::open(
&RebuildRequest {
moe_cache_slots: None,
kv_pages: None,
mamba_slots: None,
swa_pages: None
},
¤t
)
.touched()
.any());
}
}