use serde::{Deserialize, Serialize};
use crate::BackendKind;
use crate::perfmodel::EngineConfig;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct KvCacheEstimateRequest {
pub engine: EngineConfig,
pub max_num_tokens: u32,
pub max_batch_size: u32,
pub kv_cache_memory_fraction: KvCacheMemoryFraction,
pub gpu_memory_capacity_bytes_override: Option<u64>,
pub tolerance_fraction: Option<f64>,
pub options: KvCacheEstimateOptions,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub enum KvCacheMemoryFraction {
OfTotal(f64),
OfFree(f64),
}
impl KvCacheMemoryFraction {
fn to_wire(self) -> (&'static str, f64) {
match self {
Self::OfTotal(f) => ("of_total", f),
Self::OfFree(f) => ("of_free", f),
}
}
}
fn default_naive_kv_reservation() -> f64 {
0.80
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct KvCacheEstimateOptions {
pub allow_naive_fallback: bool,
pub allow_hf_config_download: bool,
#[serde(default = "default_naive_kv_reservation")]
pub naive_kv_reservation: f64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct KvCacheEstimate {
pub total_gpu_capacity_bytes: u64,
pub total_kv_size_bytes: u64,
pub kv_size_per_token_bytes: u64,
pub total_kv_size_tokens: u64,
pub source: EstimateSource,
pub memory_breakdown: Option<MemoryBreakdown>,
pub tolerance_adjusted: Option<KvCacheEstimateAdjusted>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum EstimateSource {
Native,
NaiveFallback,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct MemoryBreakdown {
pub weights_bytes: u64,
pub activations_bytes: u64,
pub runtime_overhead_bytes: u64,
pub comm_overhead_bytes: u64,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct KvCacheEstimateAdjusted {
pub tolerance_fraction: f64,
pub total_kv_size_bytes: u64,
pub total_kv_size_tokens: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum KvCacheEstimateError {
Unsupported {
model: String,
backend: BackendKind,
gpu_sku: String,
reason: String,
},
InsufficientModelMetadata {
missing_fields: Vec<String>,
},
NoKvBudget {
total_gpu_capacity_bytes: u64,
non_kv_bytes: u64,
},
IncompatibleMemoryFraction {
backend: BackendKind,
variant_kind: &'static str,
},
BadConfig {
field: String,
reason: String,
},
HfConfigFetchFailed {
hf_id: String,
source: String,
},
}
impl std::fmt::Display for KvCacheEstimateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unsupported {
model,
backend,
gpu_sku,
reason,
} => write!(
f,
"unsupported model/backend/GPU for KV-cache estimation: model={model}, \
backend={backend:?}, gpu_sku={gpu_sku}: {reason}"
),
Self::InsufficientModelMetadata { missing_fields } => {
write!(
f,
"insufficient model metadata; missing: {missing_fields:?}"
)
}
Self::NoKvBudget {
total_gpu_capacity_bytes,
non_kv_bytes,
} => write!(
f,
"no KV budget: non-KV memory ({non_kv_bytes} bytes) meets/exceeds the \
KV-cache memory limit (capacity={total_gpu_capacity_bytes} bytes)"
),
Self::IncompatibleMemoryFraction {
backend,
variant_kind,
} => write!(
f,
"incompatible memory fraction: backend {backend:?} does not accept \
KvCacheMemoryFraction::{variant_kind}"
),
Self::BadConfig { field, reason } => {
write!(f, "bad memory config field {field:?}: {reason}")
}
Self::HfConfigFetchFailed { hf_id, source } => {
write!(f, "HF config fetch failed for {hf_id:?}: {source}")
}
}
}
}
impl std::error::Error for KvCacheEstimateError {}
#[cfg(feature = "python")]
pub fn estimate_kv_cache(
req: KvCacheEstimateRequest,
) -> Result<KvCacheEstimate, KvCacheEstimateError> {
fetch_python_estimate(&req)
}
#[cfg(feature = "python")]
fn fetch_python_estimate(
req: &KvCacheEstimateRequest,
) -> Result<KvCacheEstimate, KvCacheEstimateError> {
use pyo3::prelude::*;
use pyo3::types::PyDict;
let engine = &req.engine;
let parallel = &engine.parallel;
let quant = &engine.quantization;
let nextn = engine
.speculative
.as_ref()
.and_then(|s| s.nextn)
.unwrap_or(0);
let (fraction_kind, fraction_value) = req.kv_cache_memory_fraction.to_wire();
Python::with_gil(|py| -> PyResult<KvCacheEstimate> {
let engine_mod = py.import("aiconfigurator.sdk.memory")?;
let kwargs = PyDict::new(py);
kwargs.set_item("backend_version", engine.backend_version.as_deref())?;
kwargs.set_item("max_num_tokens", req.max_num_tokens)?;
kwargs.set_item("max_batch_size", req.max_batch_size)?;
kwargs.set_item("memory_fraction_kind", fraction_kind)?;
kwargs.set_item("memory_fraction_value", fraction_value)?;
kwargs.set_item("tp_size", parallel.tp_size)?;
kwargs.set_item("pp_size", parallel.pp_size)?;
kwargs.set_item("attention_dp_size", parallel.attention_dp_size.unwrap_or(1))?;
kwargs.set_item("moe_tp_size", parallel.moe_tp_size)?;
kwargs.set_item("moe_ep_size", parallel.moe_ep_size)?;
kwargs.set_item(
"gemm_quant_mode",
quant.weight_dtype.as_ref().map(dtype_str),
)?;
kwargs.set_item("moe_quant_mode", quant.moe_dtype.as_ref().map(dtype_str))?;
kwargs.set_item(
"kvcache_quant_mode",
quant.kv_cache_dtype.as_ref().map(dtype_str),
)?;
kwargs.set_item(
"fmha_quant_mode",
quant.activation_dtype.as_ref().map(dtype_str),
)?;
kwargs.set_item("nextn", nextn)?;
kwargs.set_item(
"systems_path",
engine.systems_path.as_deref().and_then(|p| p.to_str()),
)?;
kwargs.set_item(
"gpu_memory_capacity_bytes_override",
req.gpu_memory_capacity_bytes_override,
)?;
kwargs.set_item("tolerance_fraction", req.tolerance_fraction)?;
kwargs.set_item("naive_kv_reservation", req.options.naive_kv_reservation)?;
kwargs.set_item("allow_naive_fallback", req.options.allow_naive_fallback)?;
kwargs.set_item(
"allow_hf_config_download",
req.options.allow_hf_config_download,
)?;
let out = engine_mod.call_method(
"estimate_kv_cache",
(
engine.model_name.as_str(),
engine.system_name.as_str(),
engine.backend.as_str(),
),
Some(&kwargs),
)?;
estimate_from_dict(&out)
})
.map_err(|e| KvCacheEstimateError::Unsupported {
model: engine.model_name.clone(),
backend: engine.backend.clone(),
gpu_sku: engine.system_name.clone(),
reason: format!("estimate_kv_cache: {e}"),
})
}
#[cfg(feature = "python")]
fn estimate_from_dict(
out: &pyo3::Bound<'_, pyo3::types::PyAny>,
) -> pyo3::PyResult<KvCacheEstimate> {
use pyo3::exceptions::PyValueError;
use pyo3::types::PyAnyMethods;
let u64_at = |k: &str| -> pyo3::PyResult<u64> { out.get_item(k)?.extract::<u64>() };
let source = match out.get_item("source")?.extract::<String>()?.as_str() {
"native" => EstimateSource::Native,
"naive_fallback" => EstimateSource::NaiveFallback,
other => {
return Err(PyValueError::new_err(format!(
"estimate_kv_cache returned unknown source {other:?}"
)));
}
};
let breakdown_item = out.get_item("memory_breakdown")?;
let memory_breakdown = if breakdown_item.is_none() {
None
} else {
let get = |k: &str| -> pyo3::PyResult<u64> { breakdown_item.get_item(k)?.extract::<u64>() };
Some(MemoryBreakdown {
weights_bytes: get("weights_bytes")?,
activations_bytes: get("activations_bytes")?,
runtime_overhead_bytes: get("runtime_overhead_bytes")?,
comm_overhead_bytes: get("comm_overhead_bytes")?,
})
};
let adjusted_item = out.get_item("tolerance_adjusted")?;
let tolerance_adjusted = if adjusted_item.is_none() {
None
} else {
Some(KvCacheEstimateAdjusted {
tolerance_fraction: adjusted_item
.get_item("tolerance_fraction")?
.extract::<f64>()?,
total_kv_size_bytes: adjusted_item
.get_item("total_kv_size_bytes")?
.extract::<u64>()?,
total_kv_size_tokens: adjusted_item
.get_item("total_kv_size_tokens")?
.extract::<u64>()?,
})
};
Ok(KvCacheEstimate {
total_gpu_capacity_bytes: u64_at("total_gpu_capacity_bytes")?,
total_kv_size_bytes: u64_at("total_kv_size_bytes")?,
kv_size_per_token_bytes: u64_at("kv_size_per_token_bytes")?,
total_kv_size_tokens: u64_at("total_kv_size_tokens")?,
source,
memory_breakdown,
tolerance_adjusted,
})
}
#[cfg(feature = "python")]
fn dtype_str(dt: &crate::DataType) -> &'static str {
use crate::DataType::*;
match dt {
Bfloat16 => "bfloat16",
Float16 => "float16",
Fp8 => "fp8",
Fp8Static => "fp8_static",
Fp8Block => "fp8_block",
Nvfp4 => "nvfp4",
Int8 => "int8",
Int4 => "int4",
W4afp8 => "w4afp8",
W4a16Mxfp4 => "w4a16_mxfp4",
W4a8Mxfp4Mxfp8 => "w4a8_mxfp4_mxfp8",
W4a8Mxfp4Mxfp8Trtllm => "w4a8_mxfp4_mxfp8_trtllm",
W4a16Mxfp4Cutlass => "w4a16_mxfp4_cutlass",
W4a16Nvfp4 => "w4a16_nvfp4",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn memory_fraction_to_wire() {
assert_eq!(
KvCacheMemoryFraction::OfFree(0.9).to_wire(),
("of_free", 0.9)
);
assert_eq!(
KvCacheMemoryFraction::OfTotal(0.85).to_wire(),
("of_total", 0.85)
);
}
}