1use serde::{Deserialize, Serialize};
34
35use crate::BackendKind;
36use crate::perfmodel::EngineConfig;
37
38#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
41pub struct KvCacheEstimateRequest {
42 pub engine: EngineConfig,
43 pub max_num_tokens: u32,
44 pub max_batch_size: u32,
45 pub kv_cache_memory_fraction: KvCacheMemoryFraction,
46 pub gpu_memory_capacity_bytes_override: Option<u64>,
49 pub tolerance_fraction: Option<f64>,
51 pub options: KvCacheEstimateOptions,
52}
53
54#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
60pub enum KvCacheMemoryFraction {
61 OfTotal(f64),
64 OfFree(f64),
67}
68
69impl KvCacheMemoryFraction {
70 fn to_wire(self) -> (&'static str, f64) {
73 match self {
74 Self::OfTotal(f) => ("of_total", f),
75 Self::OfFree(f) => ("of_free", f),
76 }
77 }
78}
79
80fn default_naive_kv_reservation() -> f64 {
84 0.80
85}
86
87#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
88pub struct KvCacheEstimateOptions {
89 pub allow_naive_fallback: bool,
90 pub allow_hf_config_download: bool,
91 #[serde(default = "default_naive_kv_reservation")]
95 pub naive_kv_reservation: f64,
96}
97
98#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
100pub struct KvCacheEstimate {
101 pub total_gpu_capacity_bytes: u64,
102 pub total_kv_size_bytes: u64,
103 pub kv_size_per_token_bytes: u64,
104 pub total_kv_size_tokens: u64,
105 pub source: EstimateSource,
106 pub memory_breakdown: Option<MemoryBreakdown>,
108 pub tolerance_adjusted: Option<KvCacheEstimateAdjusted>,
110}
111
112#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
113pub enum EstimateSource {
114 Native,
116 NaiveFallback,
118}
119
120#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
124pub struct MemoryBreakdown {
125 pub weights_bytes: u64,
126 pub activations_bytes: u64,
127 pub runtime_overhead_bytes: u64,
128 pub comm_overhead_bytes: u64,
129}
130
131#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
132pub struct KvCacheEstimateAdjusted {
133 pub tolerance_fraction: f64,
134 pub total_kv_size_bytes: u64,
135 pub total_kv_size_tokens: u64,
136}
137
138#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
139pub enum KvCacheEstimateError {
140 Unsupported {
141 model: String,
142 backend: BackendKind,
143 gpu_sku: String,
144 reason: String,
145 },
146 InsufficientModelMetadata {
147 missing_fields: Vec<String>,
148 },
149 NoKvBudget {
150 total_gpu_capacity_bytes: u64,
151 non_kv_bytes: u64,
152 },
153 IncompatibleMemoryFraction {
154 backend: BackendKind,
155 variant_kind: &'static str,
156 },
157 BadConfig {
158 field: String,
159 reason: String,
160 },
161 HfConfigFetchFailed {
162 hf_id: String,
163 source: String,
164 },
165}
166
167impl std::fmt::Display for KvCacheEstimateError {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 match self {
170 Self::Unsupported {
171 model,
172 backend,
173 gpu_sku,
174 reason,
175 } => write!(
176 f,
177 "unsupported model/backend/GPU for KV-cache estimation: model={model}, \
178 backend={backend:?}, gpu_sku={gpu_sku}: {reason}"
179 ),
180 Self::InsufficientModelMetadata { missing_fields } => {
181 write!(
182 f,
183 "insufficient model metadata; missing: {missing_fields:?}"
184 )
185 }
186 Self::NoKvBudget {
187 total_gpu_capacity_bytes,
188 non_kv_bytes,
189 } => write!(
190 f,
191 "no KV budget: non-KV memory ({non_kv_bytes} bytes) meets/exceeds the \
192 KV-cache memory limit (capacity={total_gpu_capacity_bytes} bytes)"
193 ),
194 Self::IncompatibleMemoryFraction {
195 backend,
196 variant_kind,
197 } => write!(
198 f,
199 "incompatible memory fraction: backend {backend:?} does not accept \
200 KvCacheMemoryFraction::{variant_kind}"
201 ),
202 Self::BadConfig { field, reason } => {
203 write!(f, "bad memory config field {field:?}: {reason}")
204 }
205 Self::HfConfigFetchFailed { hf_id, source } => {
206 write!(f, "HF config fetch failed for {hf_id:?}: {source}")
207 }
208 }
209 }
210}
211
212impl std::error::Error for KvCacheEstimateError {}
213
214#[cfg(feature = "python")]
229pub fn estimate_kv_cache(
230 req: KvCacheEstimateRequest,
231) -> Result<KvCacheEstimate, KvCacheEstimateError> {
232 fetch_python_estimate(&req)
233}
234
235#[cfg(feature = "python")]
242fn fetch_python_estimate(
243 req: &KvCacheEstimateRequest,
244) -> Result<KvCacheEstimate, KvCacheEstimateError> {
245 use pyo3::prelude::*;
246 use pyo3::types::PyDict;
247
248 let engine = &req.engine;
249 let parallel = &engine.parallel;
250 let quant = &engine.quantization;
251 let nextn = engine
252 .speculative
253 .as_ref()
254 .and_then(|s| s.nextn)
255 .unwrap_or(0);
256 let (fraction_kind, fraction_value) = req.kv_cache_memory_fraction.to_wire();
257
258 Python::with_gil(|py| -> PyResult<KvCacheEstimate> {
259 let engine_mod = py.import("aiconfigurator.sdk.memory")?;
260 let kwargs = PyDict::new(py);
261 kwargs.set_item("backend_version", engine.backend_version.as_deref())?;
262 kwargs.set_item("max_num_tokens", req.max_num_tokens)?;
263 kwargs.set_item("max_batch_size", req.max_batch_size)?;
264 kwargs.set_item("memory_fraction_kind", fraction_kind)?;
265 kwargs.set_item("memory_fraction_value", fraction_value)?;
266 kwargs.set_item("tp_size", parallel.tp_size)?;
267 kwargs.set_item("pp_size", parallel.pp_size)?;
268 kwargs.set_item("attention_dp_size", parallel.attention_dp_size.unwrap_or(1))?;
269 kwargs.set_item("moe_tp_size", parallel.moe_tp_size)?;
270 kwargs.set_item("moe_ep_size", parallel.moe_ep_size)?;
271 kwargs.set_item(
272 "gemm_quant_mode",
273 quant.weight_dtype.as_ref().map(dtype_str),
274 )?;
275 kwargs.set_item("moe_quant_mode", quant.moe_dtype.as_ref().map(dtype_str))?;
276 kwargs.set_item(
277 "kvcache_quant_mode",
278 quant.kv_cache_dtype.as_ref().map(dtype_str),
279 )?;
280 kwargs.set_item(
281 "fmha_quant_mode",
282 quant.activation_dtype.as_ref().map(dtype_str),
283 )?;
284 kwargs.set_item("nextn", nextn)?;
288 kwargs.set_item(
289 "systems_path",
290 engine.systems_path.as_deref().and_then(|p| p.to_str()),
291 )?;
292 kwargs.set_item(
293 "gpu_memory_capacity_bytes_override",
294 req.gpu_memory_capacity_bytes_override,
295 )?;
296 kwargs.set_item("tolerance_fraction", req.tolerance_fraction)?;
297 kwargs.set_item("naive_kv_reservation", req.options.naive_kv_reservation)?;
298 kwargs.set_item("allow_naive_fallback", req.options.allow_naive_fallback)?;
299 kwargs.set_item(
300 "allow_hf_config_download",
301 req.options.allow_hf_config_download,
302 )?;
303
304 let out = engine_mod.call_method(
305 "estimate_kv_cache",
306 (
307 engine.model_name.as_str(),
308 engine.system_name.as_str(),
309 engine.backend.as_str(),
310 ),
311 Some(&kwargs),
312 )?;
313
314 estimate_from_dict(&out)
315 })
316 .map_err(|e| KvCacheEstimateError::Unsupported {
321 model: engine.model_name.clone(),
322 backend: engine.backend.clone(),
323 gpu_sku: engine.system_name.clone(),
324 reason: format!("estimate_kv_cache: {e}"),
325 })
326}
327
328#[cfg(feature = "python")]
334fn estimate_from_dict(
335 out: &pyo3::Bound<'_, pyo3::types::PyAny>,
336) -> pyo3::PyResult<KvCacheEstimate> {
337 use pyo3::exceptions::PyValueError;
338 use pyo3::types::PyAnyMethods;
339
340 let u64_at = |k: &str| -> pyo3::PyResult<u64> { out.get_item(k)?.extract::<u64>() };
341
342 let source = match out.get_item("source")?.extract::<String>()?.as_str() {
343 "native" => EstimateSource::Native,
344 "naive_fallback" => EstimateSource::NaiveFallback,
345 other => {
346 return Err(PyValueError::new_err(format!(
347 "estimate_kv_cache returned unknown source {other:?}"
348 )));
349 }
350 };
351
352 let breakdown_item = out.get_item("memory_breakdown")?;
353 let memory_breakdown = if breakdown_item.is_none() {
354 None
355 } else {
356 let get = |k: &str| -> pyo3::PyResult<u64> { breakdown_item.get_item(k)?.extract::<u64>() };
357 Some(MemoryBreakdown {
358 weights_bytes: get("weights_bytes")?,
359 activations_bytes: get("activations_bytes")?,
360 runtime_overhead_bytes: get("runtime_overhead_bytes")?,
361 comm_overhead_bytes: get("comm_overhead_bytes")?,
362 })
363 };
364
365 let adjusted_item = out.get_item("tolerance_adjusted")?;
369 let tolerance_adjusted = if adjusted_item.is_none() {
370 None
371 } else {
372 Some(KvCacheEstimateAdjusted {
373 tolerance_fraction: adjusted_item
374 .get_item("tolerance_fraction")?
375 .extract::<f64>()?,
376 total_kv_size_bytes: adjusted_item
377 .get_item("total_kv_size_bytes")?
378 .extract::<u64>()?,
379 total_kv_size_tokens: adjusted_item
380 .get_item("total_kv_size_tokens")?
381 .extract::<u64>()?,
382 })
383 };
384
385 Ok(KvCacheEstimate {
386 total_gpu_capacity_bytes: u64_at("total_gpu_capacity_bytes")?,
387 total_kv_size_bytes: u64_at("total_kv_size_bytes")?,
388 kv_size_per_token_bytes: u64_at("kv_size_per_token_bytes")?,
389 total_kv_size_tokens: u64_at("total_kv_size_tokens")?,
390 source,
391 memory_breakdown,
392 tolerance_adjusted,
393 })
394}
395
396#[cfg(feature = "python")]
399fn dtype_str(dt: &crate::DataType) -> &'static str {
400 use crate::DataType::*;
401 match dt {
402 Bfloat16 => "bfloat16",
403 Float16 => "float16",
404 Fp8 => "fp8",
405 Fp8Static => "fp8_static",
406 Fp8Block => "fp8_block",
407 Nvfp4 => "nvfp4",
408 Int8 => "int8",
409 Int4 => "int4",
410 W4afp8 => "w4afp8",
411 W4a16Mxfp4 => "w4a16_mxfp4",
412 W4a8Mxfp4Mxfp8 => "w4a8_mxfp4_mxfp8",
413 W4a8Mxfp4Mxfp8Trtllm => "w4a8_mxfp4_mxfp8_trtllm",
414 W4a16Mxfp4Cutlass => "w4a16_mxfp4_cutlass",
415 W4a16Nvfp4 => "w4a16_nvfp4",
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
434 fn memory_fraction_to_wire() {
435 assert_eq!(
436 KvCacheMemoryFraction::OfFree(0.9).to_wire(),
437 ("of_free", 0.9)
438 );
439 assert_eq!(
440 KvCacheMemoryFraction::OfTotal(0.85).to_wire(),
441 ("of_total", 0.85)
442 );
443 }
444}