use metrale_config::ModelConfig;
use super::headroom::Yardstick;
use crate::cli;
const MIB: f64 = 1024.0 * 1024.0;
const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
pub(super) struct RingFit {
pub(super) slots: usize,
pub(super) decision: String,
pub(super) warning: Option<String>,
}
pub(super) fn requested_slots(args: &cli::ServeArgs, config: &ModelConfig) -> usize {
if std::env::var("METRALE_SSM_RESERVE_RING_FULL").is_ok() {
return if config.num_ssm_layers() > 0 {
metrale_kernels::DECODE_ROLLBACK_RING_SLOTS
} else {
0
};
}
metrale_model_layers::ssm_reserve::decode_rollback_ring_slots(
config.num_ssm_layers(),
args.speculative || args.dflash,
)
.slots
}
pub(super) fn slot_bytes(args: &cli::ServeArgs, per_seq_blob: usize) -> usize {
args.max_batch_size.ceiling() * per_seq_blob
}
pub(super) fn formula(slots: usize, max_batch: usize, per_seq_blob: usize) -> String {
format!(
"ring: {slots} slots x {max_batch} seqs x {:.1} MB/seq = {:.2} GB",
per_seq_blob as f64 / MIB,
(slots * max_batch * per_seq_blob) as f64 / GIB,
)
}
pub(super) fn autofit(
args: &cli::ServeArgs,
requested: usize,
slot_bytes: usize,
per_seq_blob: usize,
reserve_without_ring: usize,
free_mem: usize,
yardstick: &Yardstick,
) -> RingFit {
let explicit = metrale_model_layers::ssm_reserve::published_decode_ring_slots().is_some();
let fit = fit_ring(
args,
requested,
slot_bytes,
per_seq_blob,
reserve_without_ring,
free_mem,
yardstick,
explicit,
);
if fit.slots != requested {
metrale_model_layers::ssm_reserve::set_decode_ring_slots(fit.slots);
}
fit
}
#[allow(clippy::too_many_arguments)]
pub(super) fn fit_ring(
args: &cli::ServeArgs,
requested: usize,
slot_bytes: usize,
per_seq_blob: usize,
reserve_without_ring: usize,
free_mem: usize,
yardstick: &Yardstick,
explicit: bool,
) -> RingFit {
let (beside, limit) = yardstick.ladder_basis(reserve_without_ring, free_mem);
let asked = beside.saturating_add(requested.saturating_mul(slot_bytes));
let decide = |slots: usize| describe(args, yardstick, slots, slot_bytes, beside, limit);
if requested == 0 || slot_bytes == 0 || explicit || asked <= limit {
return RingFit {
slots: requested,
decision: decide(requested),
warning: None,
};
}
let fitted = metrale_model_layers::ssm_reserve::fit_decode_ring_slots(
requested, beside, slot_bytes, limit,
);
let fitted_total = beside.saturating_add(fitted.saturating_mul(slot_bytes));
if fitted_total > limit && matches!(yardstick, Yardstick::PreLoadFree(_)) {
return RingFit {
slots: requested,
decision: decide(requested),
warning: None,
};
}
RingFit {
slots: fitted,
decision: decide(fitted),
warning: Some(format!(
"{} (was {:.2} GB); {}. Sized from {}, not from --max-batch-size {} (#915): \
rollback depth yields, concurrency does not. Pass --ssm-decode-ring-slots N to \
pin a depth (and be refused rather than shrunk).",
formula(fitted, args.max_batch_size.ceiling(), per_seq_blob),
(requested * slot_bytes) as f64 / GIB,
if fitted_total > limit {
format!(
"even 0 slots does not clear the {:.2} GB KV floor inside {:.2} GB of \
predicted headroom — the KV budget stage will decide, on measured bytes",
beside as f64 / GIB,
limit as f64 / GIB,
)
} else {
format!(
"{} {:.2} of {:.2} GB",
yardstick.total_label(),
fitted_total as f64 / GIB,
limit as f64 / GIB,
)
},
yardstick.name(),
args.max_batch_size.ceiling(),
)),
}
}
fn describe(
args: &cli::ServeArgs,
yardstick: &Yardstick,
slots: usize,
slot_bytes: usize,
beside: usize,
limit: usize,
) -> String {
let ring_bytes = slots.saturating_mul(slot_bytes);
match yardstick {
Yardstick::PostLoad(h) => format!(
"SSM decode-ring fit against the predicted post-load KV headroom: {}",
h.describe(args.gpu_memory_utilization, ring_bytes, slots),
),
Yardstick::PreLoadFree(why) => format!(
"SSM decode-ring fit against pre-load free memory ({why}): ring({slots}) {:.2} + \
rest of reserve {:.2} = {:.2} of {:.2} GB free",
ring_bytes as f64 / GIB,
beside as f64 / GIB,
(beside + ring_bytes) as f64 / GIB,
limit as f64 / GIB,
),
}
}
#[cfg(test)]
#[path = "decode_ring_tests.rs"]
mod tests;