1use anyhow::{Context, Result};
11use ndarray::{Array1, Array2};
12use ndarray_interp::InterpolateError;
13use ndarray_interp::interp1d::{Interp1DBuilder, Linear};
14use ndarray_interp::interp2d::{Bilinear, Interp2DBuilder};
15use std::path::Path;
16use std::sync::Arc;
17
18pub trait PrefillInterpolator: Send + Sync {
20 fn interp(&self, x: f64) -> Result<f64, InterpolateError>;
21}
22
23pub trait DecodeInterpolator: Send + Sync {
25 fn interp(&self, x: f64, y: f64) -> Result<f64, InterpolateError>;
26}
27
28pub trait AicCallback: Send + Sync {
31 fn predict_prefill(
34 &self,
35 batch_size: usize,
36 effective_isl: usize,
37 prefix: usize,
38 ) -> Result<f64>;
39
40 fn predict_decode(&self, batch_size: usize, isl: usize, osl: usize) -> Result<f64>;
43}
44
45struct PrefillInterp1D {
47 inner: ndarray_interp::interp1d::Interp1D<
48 ndarray::OwnedRepr<f64>,
49 ndarray::OwnedRepr<f64>,
50 ndarray::Ix1,
51 Linear,
52 >,
53}
54
55impl PrefillInterpolator for PrefillInterp1D {
56 fn interp(&self, x: f64) -> Result<f64, InterpolateError> {
57 self.inner.interp_scalar(x)
58 }
59}
60
61struct DecodeInterp2D {
63 inner: ndarray_interp::interp2d::Interp2D<
64 ndarray::OwnedRepr<f64>,
65 ndarray::OwnedRepr<f64>,
66 ndarray::OwnedRepr<f64>,
67 ndarray::Ix2,
68 Bilinear,
69 >,
70}
71
72impl DecodeInterpolator for DecodeInterp2D {
73 fn interp(&self, x: f64, y: f64) -> Result<f64, InterpolateError> {
74 self.inner.interp_scalar(x, y)
75 }
76}
77
78#[derive(Default)]
80pub enum PerfModel {
81 #[default]
83 Polynomial,
84 Fixed { prefill_ms: f64, decode_ms: f64 },
86 Interpolated {
89 prefill_interp: Arc<dyn PrefillInterpolator>,
90 decode_interp: Arc<dyn DecodeInterpolator>,
91 },
92 Aiconfigurator { callback: Arc<dyn AicCallback> },
95}
96
97impl Clone for PerfModel {
98 fn clone(&self) -> Self {
99 match self {
100 PerfModel::Polynomial => PerfModel::Polynomial,
101 PerfModel::Fixed {
102 prefill_ms,
103 decode_ms,
104 } => PerfModel::Fixed {
105 prefill_ms: *prefill_ms,
106 decode_ms: *decode_ms,
107 },
108 PerfModel::Interpolated {
109 prefill_interp,
110 decode_interp,
111 } => PerfModel::Interpolated {
112 prefill_interp: Arc::clone(prefill_interp),
113 decode_interp: Arc::clone(decode_interp),
114 },
115 PerfModel::Aiconfigurator { callback } => PerfModel::Aiconfigurator {
116 callback: Arc::clone(callback),
117 },
118 }
119 }
120}
121
122impl std::fmt::Debug for PerfModel {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 match self {
125 PerfModel::Polynomial => write!(f, "PerfModel::Polynomial"),
126 PerfModel::Fixed {
127 prefill_ms,
128 decode_ms,
129 } => write!(
130 f,
131 "PerfModel::Fixed {{ prefill_ms: {prefill_ms}, decode_ms: {decode_ms} }}"
132 ),
133 PerfModel::Interpolated { .. } => write!(f, "PerfModel::Interpolated {{ .. }}"),
134 PerfModel::Aiconfigurator { .. } => write!(f, "PerfModel::Aiconfigurator"),
135 }
136 }
137}
138
139impl PerfModel {
140 pub fn from_npz(path: &Path) -> Result<Self> {
149 use ndarray_npy::NpzReader;
150 use std::fs::File;
151
152 tracing::info!("Loading performance model from NPZ file: {:?}", path);
153
154 let file =
155 File::open(path).with_context(|| format!("Failed to open NPZ file: {:?}", path))?;
156
157 let mut npz = NpzReader::new(file)
158 .with_context(|| format!("Failed to create NPZ reader for: {:?}", path))?;
159
160 let prefill_isl: Array1<f64> = npz
162 .by_name("prefill_isl")
163 .with_context(|| "Failed to load prefill_isl from NPZ")?;
164 let prefill_ttft_ms: Array1<f64> = npz
165 .by_name("prefill_ttft_ms")
166 .with_context(|| "Failed to load prefill_ttft_ms from NPZ")?;
167
168 let decode_active_kv_tokens: Array1<f64> = npz
170 .by_name("decode_active_kv_tokens")
171 .with_context(|| "Failed to load decode_active_kv_tokens from NPZ")?;
172 let decode_context_length: Array1<f64> = npz
173 .by_name("decode_context_length")
174 .with_context(|| "Failed to load decode_context_length from NPZ")?;
175 let decode_itl: Array2<f64> = npz
176 .by_name("decode_itl")
177 .with_context(|| "Failed to load decode_itl from NPZ")?;
178
179 if prefill_isl.len() != prefill_ttft_ms.len() {
181 anyhow::bail!(
182 "Prefill array length mismatch: isl={}, ttft={}",
183 prefill_isl.len(),
184 prefill_ttft_ms.len()
185 );
186 }
187
188 if decode_itl.nrows() != decode_active_kv_tokens.len()
189 || decode_itl.ncols() != decode_context_length.len()
190 {
191 anyhow::bail!(
192 "Decode array dimension mismatch: itl shape=({}, {}), active_kv={}, context={}",
193 decode_itl.nrows(),
194 decode_itl.ncols(),
195 decode_active_kv_tokens.len(),
196 decode_context_length.len()
197 );
198 }
199
200 tracing::info!(
201 "Loaded performance model: prefill_points={}, decode_grid={}x{}",
202 prefill_isl.len(),
203 decode_itl.nrows(),
204 decode_itl.ncols()
205 );
206
207 let prefill_interp = Interp1DBuilder::new(prefill_ttft_ms)
209 .x(prefill_isl)
210 .strategy(Linear::new().extrapolate(true))
211 .build()
212 .with_context(|| "Failed to build prefill interpolator")?;
213
214 let decode_interp = Interp2DBuilder::new(decode_itl)
215 .x(decode_active_kv_tokens)
216 .y(decode_context_length)
217 .strategy(Bilinear::new().extrapolate(true))
218 .build()
219 .with_context(|| "Failed to build decode interpolator")?;
220
221 Ok(PerfModel::Interpolated {
222 prefill_interp: Arc::new(PrefillInterp1D {
223 inner: prefill_interp,
224 }),
225 decode_interp: Arc::new(DecodeInterp2D {
226 inner: decode_interp,
227 }),
228 })
229 }
230
231 pub fn from_aic_callback(callback: Arc<dyn AicCallback>) -> Self {
233 PerfModel::Aiconfigurator { callback }
234 }
235
236 pub fn predict_prefill_time(
243 &self,
244 batch_size: usize,
245 isl: usize,
246 prefix: usize,
247 ) -> Result<f64> {
248 if matches!(self, Self::Polynomial | Self::Fixed { .. }) {
249 anyhow::bail!("built-in timing is implemented by the AISimulate engine");
250 }
251 let new_tokens_per_req = isl.saturating_sub(prefix);
252 if batch_size == 0 || new_tokens_per_req == 0 {
253 return Ok(0.0);
254 }
255 let time = match self {
256 PerfModel::Polynomial => unreachable!("polynomial handled above"),
257 PerfModel::Fixed { .. } => unreachable!("fixed timing handled above"),
258 PerfModel::Interpolated { prefill_interp, .. } => {
259 let tokens = (batch_size * new_tokens_per_req) as f64;
260 prefill_interp.interp(tokens).unwrap_or(0.0)
261 }
262 PerfModel::Aiconfigurator { callback } => callback
263 .predict_prefill(batch_size, new_tokens_per_req, prefix)
264 .context("AIC prefill prediction failed")?,
265 };
266 Ok(time.max(0.0))
267 }
268
269 pub fn predict_decode_time(
278 &self,
279 batch_size: usize,
280 active_kv_tokens: usize,
281 context_length: usize,
282 _total_kv_tokens: usize,
283 ) -> Result<f64> {
284 if matches!(self, Self::Polynomial | Self::Fixed { .. }) {
285 anyhow::bail!("built-in timing is implemented by the AISimulate engine");
286 }
287 if batch_size == 0 {
288 return Ok(0.0);
289 }
290 let time = match self {
291 PerfModel::Polynomial => unreachable!("polynomial handled above"),
292 PerfModel::Fixed { .. } => unreachable!("fixed timing handled above"),
293 PerfModel::Interpolated { decode_interp, .. } => decode_interp
294 .interp(active_kv_tokens as f64, context_length as f64)
295 .unwrap_or(0.0),
296 PerfModel::Aiconfigurator { callback } => callback
297 .predict_decode(batch_size, context_length, 2)
298 .context("AIC decode prediction failed")?,
299 };
300 let result = time.max(1.0);
302 tracing::trace!(
303 "Decode time prediction: batch_size={batch_size}, active_kv_tokens={active_kv_tokens}, context_length={context_length}, time={result:.2}ms"
304 );
305 Ok(result)
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::{AicCallback, PerfModel};
312 use std::sync::Arc;
313
314 struct EchoBatchCallback;
315
316 impl AicCallback for EchoBatchCallback {
317 fn predict_prefill(
318 &self,
319 batch_size: usize,
320 _effective_isl: usize,
321 _prefix: usize,
322 ) -> anyhow::Result<f64> {
323 Ok(batch_size as f64)
324 }
325
326 fn predict_decode(
327 &self,
328 batch_size: usize,
329 _isl: usize,
330 _osl: usize,
331 ) -> anyhow::Result<f64> {
332 Ok(batch_size as f64)
333 }
334 }
335
336 struct FailingCallback;
337
338 impl AicCallback for FailingCallback {
339 fn predict_prefill(
340 &self,
341 _batch_size: usize,
342 _effective_isl: usize,
343 _prefix: usize,
344 ) -> anyhow::Result<f64> {
345 anyhow::bail!("missing AIC prefill point")
346 }
347
348 fn predict_decode(
349 &self,
350 _batch_size: usize,
351 _isl: usize,
352 _osl: usize,
353 ) -> anyhow::Result<f64> {
354 anyhow::bail!("missing AIC decode point")
355 }
356 }
357
358 #[test]
359 fn polynomial_is_only_a_builtin_engine_marker() {
360 let model = PerfModel::default();
361 assert!(matches!(model, PerfModel::Polynomial));
362 assert!(model.predict_prefill_time(1, 128, 0).is_err());
363 assert!(model.predict_decode_time(1, 128, 128, 1024).is_err());
364 }
365
366 #[test]
367 fn aic_forwards_scheduler_local_batch() {
368 let model = PerfModel::from_aic_callback(Arc::new(EchoBatchCallback));
369
370 assert_eq!(model.predict_prefill_time(7, 128, 0).unwrap(), 7.0);
371 assert_eq!(model.predict_decode_time(9, 0, 128, 0).unwrap(), 9.0);
372 }
373
374 #[test]
375 fn aic_prefill_prediction_errors_propagate() {
376 let error = PerfModel::from_aic_callback(Arc::new(FailingCallback))
377 .predict_prefill_time(2, 128, 32)
378 .unwrap_err();
379
380 assert_eq!(error.to_string(), "AIC prefill prediction failed");
381 assert_eq!(error.root_cause().to_string(), "missing AIC prefill point");
382 }
383
384 #[test]
385 fn aic_decode_prediction_errors_propagate() {
386 let error = PerfModel::from_aic_callback(Arc::new(FailingCallback))
387 .predict_decode_time(2, 64, 128, 1024)
388 .unwrap_err();
389
390 assert_eq!(error.to_string(), "AIC decode prediction failed");
391 assert_eq!(error.root_cause().to_string(), "missing AIC decode point");
392 }
393}