pub(crate) use ferrox_core::expert_budget::{BudgetTooSmall, PoolSizes};
use ferrox_core::expert_budget::required_bytes;
pub const SWA_RETAIN_GAP: usize = 16;
pub const DEFAULT_SWA_EVICTION_INTERVAL: usize = 128;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RebuildRequest {
pub moe_cache_slots: Option<u64>,
pub kv_pages: Option<u64>,
pub mamba_slots: Option<u64>,
pub swa_pages: Option<u64>,
}
impl RebuildRequest {
pub fn is_empty(&self) -> bool {
self.moe_cache_slots.is_none()
&& self.kv_pages.is_none()
&& self.mamba_slots.is_none()
&& self.swa_pages.is_none()
}
pub fn invalidates_prefix_cache(&self) -> bool {
self.kv_pages.is_some() || self.mamba_slots.is_some() || self.swa_pages.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RebuildRejected {
Busy,
NoSuchPool(&'static str),
BelowFloor {
pool: &'static str,
requested: u64,
floor: u64,
},
TooLarge(BudgetTooSmall),
}
impl std::fmt::Display for RebuildRejected {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RebuildRejected::Busy => {
write!(f, "the engine is not idle; retry when it drains")
}
RebuildRejected::NoSuchPool(pool) => {
write!(f, "this model has no {pool} pool")
}
RebuildRejected::BelowFloor {
pool,
requested,
floor,
} => write!(
f,
"{pool}={requested} is below the working-set floor of {floor}"
),
RebuildRejected::TooLarge(inner) => write!(f, "{inner}"),
}
}
}
impl std::error::Error for RebuildRejected {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PoolFloors {
pub moe_slots: u64,
pub kv_pages: u64,
pub mamba_slots: Option<u64>,
pub swa_pages: Option<u64>,
}
#[allow(clippy::too_many_arguments)]
pub fn validate_rebuild(
request: &RebuildRequest,
current: &PoolSizes,
floors: &PoolFloors,
idle: bool,
budget_bytes: i64,
bytes_per_expert: u64,
bytes_per_page: u64,
fixed_bytes: u64,
) -> Result<PoolSizes, RebuildRejected> {
if !idle {
return Err(RebuildRejected::Busy);
}
let below = |pool, requested: u64, floor: u64| RebuildRejected::BelowFloor {
pool,
requested,
floor,
};
if let Some(slots) = request.moe_cache_slots {
if slots < floors.moe_slots {
return Err(below("moe", slots, floors.moe_slots));
}
}
if let Some(pages) = request.kv_pages {
if pages < floors.kv_pages.max(2) {
return Err(below("kv", pages, floors.kv_pages.max(2)));
}
}
if let Some(slots) = request.mamba_slots {
match floors.mamba_slots {
None => return Err(RebuildRejected::NoSuchPool("recurrent-state")),
Some(floor) if slots < floor => return Err(below("mamba", slots, floor)),
Some(_) => {}
}
}
if let Some(pages) = request.swa_pages {
match floors.swa_pages {
None => return Err(RebuildRejected::NoSuchPool("window")),
Some(floor) if pages < floor => return Err(below("swa", pages, floor)),
Some(_) => {}
}
}
let target = PoolSizes {
moe_cache_slots: request.moe_cache_slots.unwrap_or(current.moe_cache_slots),
kv_pages: request.kv_pages.unwrap_or(current.kv_pages),
prefill_overlap: current.prefill_overlap,
};
let needed = required_bytes(
target.moe_cache_slots,
target.kv_pages,
bytes_per_expert,
bytes_per_page,
);
let budget_bytes = budget_bytes.saturating_sub(fixed_bytes as i64);
if needed > budget_bytes {
return Err(RebuildRejected::TooLarge(BudgetTooSmall {
needed_bytes: needed,
budget_bytes,
sizes: target,
}));
}
Ok(target)
}
#[cfg(test)]
mod tests {
use super::*;
const GIB: u64 = 1 << 30;
const MIB: u64 = 1 << 20;
#[test]
fn a_rebuild_is_refused_while_the_engine_is_busy() {
let current = PoolSizes {
moe_cache_slots: 64,
kv_pages: 1024,
prefill_overlap: true,
};
let floors = PoolFloors {
moe_slots: 8,
kv_pages: 16,
..PoolFloors::default()
};
let request = RebuildRequest {
moe_cache_slots: Some(128),
..RebuildRequest::default()
};
assert_eq!(
validate_rebuild(&request, ¤t, &floors, false, i64::MAX, MIB, MIB, 0),
Err(RebuildRejected::Busy)
);
}
#[test]
fn a_rebuild_below_a_floor_or_over_the_budget_changes_nothing() {
let current = PoolSizes {
moe_cache_slots: 64,
kv_pages: 1024,
prefill_overlap: true,
};
let floors = PoolFloors {
moe_slots: 8,
kv_pages: 16,
mamba_slots: None,
swa_pages: Some(32),
};
assert!(matches!(
validate_rebuild(
&RebuildRequest {
moe_cache_slots: Some(4),
..RebuildRequest::default()
},
¤t,
&floors,
true,
i64::MAX,
MIB,
MIB,
0
),
Err(RebuildRejected::BelowFloor { pool: "moe", .. })
));
assert_eq!(
validate_rebuild(
&RebuildRequest {
mamba_slots: Some(64),
..RebuildRequest::default()
},
¤t,
&floors,
true,
i64::MAX,
MIB,
MIB,
0
),
Err(RebuildRejected::NoSuchPool("recurrent-state"))
);
assert!(matches!(
validate_rebuild(
&RebuildRequest {
kv_pages: Some(1 << 40),
..RebuildRequest::default()
},
¤t,
&floors,
true,
(2 * GIB) as i64,
MIB,
MIB,
0
),
Err(RebuildRejected::TooLarge(_))
));
}
#[test]
fn a_rebuild_leaves_untouched_pools_at_their_current_size() {
let current = PoolSizes {
moe_cache_slots: 64,
kv_pages: 1024,
prefill_overlap: true,
};
let floors = PoolFloors {
moe_slots: 8,
kv_pages: 16,
..PoolFloors::default()
};
let target = validate_rebuild(
&RebuildRequest {
moe_cache_slots: Some(128),
..RebuildRequest::default()
},
¤t,
&floors,
true,
(4 * GIB) as i64,
MIB,
MIB,
0,
)
.expect("fits");
assert_eq!(target.moe_cache_slots, 128);
assert_eq!(target.kv_pages, 1024);
}
#[test]
fn only_a_kv_side_resize_invalidates_the_prefix_cache() {
assert!(!RebuildRequest {
moe_cache_slots: Some(128),
..RebuildRequest::default()
}
.invalidates_prefix_cache());
for request in [
RebuildRequest {
kv_pages: Some(1),
..RebuildRequest::default()
},
RebuildRequest {
mamba_slots: Some(1),
..RebuildRequest::default()
},
RebuildRequest {
swa_pages: Some(1),
..RebuildRequest::default()
},
] {
assert!(request.invalidates_prefix_cache());
}
assert!(RebuildRequest::default().is_empty());
}
}