use crate::serving::admission::AdmittedChunk;
pub fn usage_ratio(used: usize, total: usize) -> f64 {
if total == 0 {
return 0.0;
}
used as f64 / total as f64
}
pub fn throughput(tokens: usize, seconds: f64) -> f64 {
if seconds <= 0.0 {
return 0.0;
}
tokens as f64 / seconds
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PoolUsage {
pub used: usize,
pub total: usize,
}
impl PoolUsage {
pub fn from_available(total: usize, available: usize) -> Self {
PoolUsage {
used: total.saturating_sub(available),
total,
}
}
pub fn ratio(&self) -> f64 {
usage_ratio(self.used, self.total)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BatchStatus {
pub running_reqs: usize,
pub queue_reqs: usize,
pub kv_pages: PoolUsage,
pub page_size: usize,
pub window: Option<PoolUsage>,
pub recurrent: Option<PoolUsage>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PrefillSnapshot {
pub new_seqs: usize,
pub new_tokens: usize,
pub cached_tokens: usize,
}
impl PrefillSnapshot {
pub fn from_chunks(chunks: &[AdmittedChunk]) -> Self {
PrefillSnapshot {
new_seqs: chunks.len(),
new_tokens: chunks.iter().map(|chunk| chunk.chunk_len).sum(),
cached_tokens: chunks
.iter()
.filter_map(|chunk| chunk.admission)
.map(|admission| admission.cached_tokens)
.sum(),
}
}
}
pub const DEFAULT_DECODE_LOG_INTERVAL: usize = 40;
#[derive(Debug)]
pub struct StatusReporter {
decode_log_interval: usize,
last_prefill_time: f64,
last_decode_time: f64,
decode_forwards: usize,
decode_tokens: usize,
}
impl StatusReporter {
pub fn new(decode_log_interval: usize, now: f64) -> Self {
StatusReporter {
decode_log_interval: decode_log_interval.max(1),
last_prefill_time: now,
last_decode_time: now,
decode_forwards: 0,
decode_tokens: 0,
}
}
pub fn decode_log_interval(&self) -> usize {
self.decode_log_interval
}
pub fn report_prefill(
&mut self,
now: f64,
snapshot: &PrefillSnapshot,
status: &BatchStatus,
) -> String {
let gap = now - self.last_prefill_time;
self.last_prefill_time = now;
let input_throughput = throughput(snapshot.new_tokens, gap);
format!(
"Prefill batch, \
#new-seq: {}, \
#new-token: {}, \
#cached-token: {}, \
token usage: {:.2}, \
{}{}#running-req: {}, \
#queue-req: {}, \
input throughput (token/s): {:.2}",
snapshot.new_seqs,
snapshot.new_tokens,
snapshot.cached_tokens,
status.kv_pages.ratio(),
window_field(status.window),
recurrent_field(status.recurrent),
status.running_reqs,
status.queue_reqs,
input_throughput,
)
}
pub fn report_decode(
&mut self,
now: f64,
batch_reqs: usize,
status: &BatchStatus,
) -> Option<String> {
self.decode_forwards += 1;
self.decode_tokens += batch_reqs;
if !self
.decode_forwards
.is_multiple_of(self.decode_log_interval)
{
return None;
}
let gap = now - self.last_decode_time;
self.last_decode_time = now;
let gen_throughput = throughput(self.decode_tokens, gap);
self.decode_tokens = 0;
Some(format!(
"Decode batch, \
#running-req: {}, \
#token: {}, \
token usage: {:.2}, \
{}{}gen throughput (token/s): {:.2}, \
#queue-req: {}",
status.running_reqs,
status.kv_pages.used * status.page_size,
status.kv_pages.ratio(),
window_field(status.window),
recurrent_field(status.recurrent),
gen_throughput,
status.queue_reqs,
))
}
}
fn pool_field(name: &str, unit: &str, usage: Option<PoolUsage>) -> String {
match usage {
Some(usage) => format!(
"#{name}-{unit}: {}/{}, {name} usage: {:.2}, ",
usage.used,
usage.total,
usage.ratio()
),
None => String::new(),
}
}
fn window_field(usage: Option<PoolUsage>) -> String {
pool_field("swa", "token", usage)
}
fn recurrent_field(usage: Option<PoolUsage>) -> String {
pool_field("mamba", "slot", usage)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serving::admission::tests::{geometry, roomy};
use crate::serving::admission::{ChunkState, PendingRequest, PrefillPass};
#[test]
fn a_status_line_never_divides_by_zero() {
assert_eq!(usage_ratio(3, 4), 0.75);
assert_eq!(usage_ratio(3, 0), 0.0);
assert_eq!(throughput(120, 2.0), 60.0);
assert_eq!(throughput(120, 0.0), 0.0);
assert_eq!(throughput(120, -1.0), 0.0);
}
fn batch_status() -> BatchStatus {
BatchStatus {
running_reqs: 2,
queue_reqs: 1,
kv_pages: PoolUsage {
used: 50,
total: 200,
},
page_size: 16,
window: None,
recurrent: None,
}
}
#[test]
fn evictable_entries_are_memory_and_not_occupancy() {
let usage = PoolUsage::from_available(7, 1 + 5);
assert_eq!(usage.used, 1, "5 evictable + 1 free are all available");
assert_eq!(usage.total, 7);
assert!(
(usage.ratio() - 1.0 / 7.0).abs() < 1e-9,
"{}",
usage.ratio()
);
let naive = PoolUsage::from_available(7, 1);
assert_eq!(
naive.used, 6,
"counting evictable as used is what this test rejects"
);
}
#[test]
fn every_pool_gauge_counts_availability_the_same_way() {
let kv = PoolUsage::from_available(200, 50 + 30);
assert_eq!((kv.used, kv.total), (120, 200));
}
#[test]
fn a_pool_the_model_does_not_have_is_omitted_from_the_line() {
let mut reporter = StatusReporter::new(1, 0.0);
let dense = batch_status();
let line = reporter.report_prefill(1.0, &PrefillSnapshot::default(), &dense);
assert!(!line.contains("swa"), "{line}");
assert!(!line.contains("mamba"), "{line}");
assert!(!line.contains("0/0"), "{line}");
let hybrid = BatchStatus {
window: Some(PoolUsage {
used: 8448,
total: 76800,
}),
recurrent: Some(PoolUsage {
used: 37,
total: 256,
}),
..dense
};
let line = reporter.report_prefill(2.0, &PrefillSnapshot::default(), &hybrid);
assert!(
line.contains("#swa-token: 8448/76800, swa usage: 0.11, "),
"{line}"
);
assert!(
line.contains("#mamba-slot: 37/256, mamba usage: 0.14, "),
"{line}"
);
let line = reporter.report_decode(3.0, 1, &hybrid).unwrap();
assert!(line.contains("#swa-token: 8448/76800"), "{line}");
assert!(line.contains("#mamba-slot: 37/256"), "{line}");
let line = reporter.report_decode(4.0, 1, &dense).unwrap();
assert!(!line.contains("swa"), "{line}");
assert!(!line.contains("mamba"), "{line}");
}
#[test]
fn decode_lines_are_emitted_only_every_nth_forward() {
let mut reporter = StatusReporter::new(3, 0.0);
let status = BatchStatus {
kv_pages: PoolUsage {
used: 60,
total: 200,
},
queue_reqs: 0,
..batch_status()
};
assert_eq!(reporter.report_decode(1.0, 2, &status), None);
assert_eq!(reporter.report_decode(1.5, 2, &status), None);
let status = BatchStatus {
queue_reqs: 4,
kv_pages: PoolUsage {
used: 62,
total: 200,
},
..status
};
let line = reporter
.report_decode(2.0, 2, &status)
.expect("the third forward logs");
assert!(line.starts_with("Decode batch, "), "{line}");
assert!(line.contains("#running-req: 2, "), "{line}");
assert!(line.contains("#token: 992, "), "62 pages of 16: {line}");
assert!(line.contains("token usage: 0.31, "), "{line}");
assert!(line.contains("#queue-req: 4"), "{line}");
assert!(
line.contains("gen throughput (token/s): 3.00"),
"6 tokens over 2.0s: {line}"
);
}
#[test]
fn decode_throughput_covers_the_interval_and_not_the_whole_run() {
let mut reporter = StatusReporter::new(2, 0.0);
let status = BatchStatus {
kv_pages: PoolUsage { used: 1, total: 10 },
page_size: 1,
..batch_status()
};
assert_eq!(reporter.report_decode(1.0, 5, &status), None);
let line = reporter.report_decode(2.0, 5, &status).unwrap();
assert!(
line.contains("gen throughput (token/s): 5.00"),
"10 tokens over 2.0s: {line}"
);
assert_eq!(reporter.report_decode(3.0, 3, &status), None);
let line = reporter.report_decode(4.0, 3, &status).unwrap();
assert!(
line.contains("gen throughput (token/s): 3.00"),
"6 tokens over the 2.0s since the last line: {line}"
);
assert!(
!line.contains("gen throughput (token/s): 4.00"),
"a lifetime average would read 16 tokens over 4.0s: {line}"
);
}
#[test]
fn a_zero_gap_and_an_unallocated_pool_still_render() {
let mut reporter = StatusReporter::new(1, 0.0);
let status = BatchStatus {
kv_pages: PoolUsage::default(),
page_size: 1,
..batch_status()
};
let line = reporter.report_decode(0.0, 4, &status).unwrap();
assert!(line.contains("gen throughput (token/s): 0.00"), "{line}");
assert!(line.contains("token usage: 0.00"), "{line}");
assert!(line.contains("#token: 0, "), "{line}");
let line = reporter.report_prefill(0.0, &PrefillSnapshot::default(), &status);
assert!(line.contains("input throughput (token/s): 0.00"), "{line}");
}
#[test]
fn the_decode_interval_is_clamped_to_at_least_one_forward() {
let mut reporter = StatusReporter::new(0, 0.0);
assert_eq!(reporter.decode_log_interval(), 1);
assert!(reporter.report_decode(1.0, 1, &batch_status()).is_some());
}
#[test]
fn a_prefill_line_reports_the_schedule_time_snapshot() {
let mut pass = PrefillPass::new(512, 0, geometry());
let request = PendingRequest::new(1, 1000, 100);
let chunk = pass.take_chunk(&request, 300, 0, &roomy()).unwrap();
let snapshot = PrefillSnapshot::from_chunks(&[chunk]);
assert_eq!(snapshot.new_seqs, 1);
assert_eq!(snapshot.new_tokens, 512);
assert_eq!(snapshot.cached_tokens, 300);
let post_forward_new_tokens = 1;
let post_forward_cached_tokens = chunk.computed_len + chunk.chunk_len;
assert_eq!(post_forward_cached_tokens, 812);
let mut reporter = StatusReporter::new(DEFAULT_DECODE_LOG_INTERVAL, 0.0);
let line = reporter.report_prefill(0.5, &snapshot, &batch_status());
assert!(line.starts_with("Prefill batch, "), "{line}");
assert!(line.contains("#new-seq: 1, "), "{line}");
assert!(line.contains("#new-token: 512, "), "{line}");
assert!(line.contains("#cached-token: 300, "), "{line}");
assert!(
!line.contains(&format!("#new-token: {post_forward_new_tokens},")),
"{line}"
);
assert!(
!line.contains(&format!("#cached-token: {post_forward_cached_tokens},")),
"{line}"
);
assert!(line.contains("token usage: 0.25, "), "{line}");
assert!(line.contains("#running-req: 2, "), "{line}");
assert!(line.contains("#queue-req: 1, "), "{line}");
assert!(
line.contains("input throughput (token/s): 1024.00"),
"512 tokens over 0.5s: {line}"
);
}
#[test]
fn a_prefix_hit_is_reported_once_across_a_prompts_chunks() {
let mut pass = PrefillPass::new(1024, 0, geometry());
let fresh = PendingRequest::new(1, 600, 100);
let first = pass.take_chunk(&fresh, 200, 0, &roomy()).unwrap();
assert_eq!(first.chunk_len, 400);
let continued = PendingRequest {
chunk: Some(ChunkState {
computed_len: 512,
slot: 1,
swa_evicted_len: 0,
locked_prefix_len: 0,
}),
..PendingRequest::new(2, 2000, 100)
};
let second = pass.take_chunk(&continued, 0, 1, &roomy()).unwrap();
assert_eq!(second.chunk_len, 624);
assert_eq!(second.admission, None);
let snapshot = PrefillSnapshot::from_chunks(&[first, second]);
assert_eq!(snapshot.new_seqs, 2, "continuations are batch entries");
assert_eq!(snapshot.new_tokens, 1024, "and their tokens are computed");
assert_eq!(
snapshot.cached_tokens, 200,
"only the first chunk's prompt reports a hit"
);
}
}