use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use ferrox_models::Ceiling;
use crate::budget::ContextCeiling;
use crate::generate::DecodeError;
pub(super) struct BlockBudget {
pub(super) block_size: usize,
pub(super) ceiling: Arc<ContextCeiling>,
pub(super) total: Option<usize>,
pub(super) free: AtomicUsize,
pub(super) rejected_too_large: AtomicU64,
}
impl BlockBudget {
pub(super) fn new(
block_size: usize,
total: Option<usize>,
ceiling: Arc<ContextCeiling>,
) -> Self {
assert!(block_size > 0, "kv block size must be positive");
BlockBudget {
block_size,
ceiling,
total,
free: AtomicUsize::new(total.unwrap_or(0)),
rejected_too_large: AtomicU64::new(0),
}
}
pub(super) fn bytes_for(&self, positions: usize) -> u64 {
self.ceiling.bytes_for(positions)
}
pub(super) fn immovable_refusal(&self, requested: usize, held: usize) -> Option<DecodeError> {
if let Some(err) = self.ceiling.refusal(requested) {
return Some(err);
}
let total = self.total?;
let positions = held;
let blocks = self.blocks_for(positions);
if blocks <= total {
return None;
}
self.rejected_too_large.fetch_add(1, Ordering::Relaxed);
let limit_positions = total * self.block_size;
Some(DecodeError::KvBudgetExceeded {
binding: Ceiling::DeviceMemory.code(),
estimated_bytes: self.bytes_for(positions),
limit_bytes: self.bytes_for(limit_positions),
positions,
positions_limit: limit_positions,
detail: format!(
"request needs {blocks} KV blocks ({positions} token positions at {} per \
block) but this server's whole KV budget is {total} blocks; an idle server \
would refuse it identically",
self.block_size
),
})
}
pub(super) fn blocks_for(&self, positions: usize) -> usize {
positions.div_ceil(self.block_size).max(1)
}
pub(super) fn try_reserve(&self, blocks: usize) -> bool {
if self.total.is_none() {
return true;
}
let free = self.free.load(Ordering::Relaxed);
if blocks > free {
return false;
}
self.free.store(free - blocks, Ordering::Relaxed);
true
}
pub(super) fn release(&self, blocks: usize) {
let Some(total) = self.total else {
return;
};
let free = self.free.load(Ordering::Relaxed);
debug_assert!(
free + blocks <= total,
"released more blocks than were ever reserved"
);
self.free
.store((free + blocks).min(total), Ordering::Relaxed);
}
pub(super) fn free(&self) -> usize {
self.total
.map(|_| self.free.load(Ordering::Relaxed))
.unwrap_or(0)
}
}