aiconfigurator_core/fpm/model.rs
1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Forward-pass perf model: native AIC estimate with optional online correction
5//! and regression fallback, plus readiness/diagnostics.
6//!
7//! The `Native` variant holds an `Arc<Engine>` and the native estimate routes
8//! through [`crate::engine::Engine::forward_pass_time_ms`]. The online
9//! correction / regression / diagnostics / readiness logic is engine-agnostic.
10
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13
14use serde::{Deserialize, Serialize};
15
16use crate::engine::Engine;
17use crate::{AicError, EngineConfig, ForwardPassMetrics};
18
19use super::correction::CorrectionBuckets;
20use super::metrics::validate_forward_pass_metrics;
21use super::options::{validate_options, ForwardPassPerfOptions};
22use super::regression::BucketedRegression;
23use super::samples::{AxisRange, StoreStats, WithOptions};
24
25/// Current readiness and tuning state for a `ForwardPassPerfModel`.
26#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
27pub struct ForwardPassPerfDiagnostics {
28 /// Active prediction source. Native models become `aic_with_correction`
29 /// after at least one inferred workload kind has enough correction samples.
30 pub source: ForwardPassPerfSource,
31 /// Whether the model can currently produce learned estimates for at least
32 /// one workload kind, or why it cannot.
33 pub readiness: ForwardPassPerfReadiness,
34 /// Number of retained tuning observations across all inferred workload kinds.
35 pub retained_observations: usize,
36 /// Number of populated native-correction regions whose workload kind has at least
37 /// `min_observations` total retained samples.
38 pub correction_ready_buckets: usize,
39 /// Fallback reason when `best_available` had to use regression instead of native AIC.
40 pub last_warning: Option<String>,
41}
42
43/// Prediction backend currently used by `ForwardPassPerfModel`.
44#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
45#[serde(rename_all = "snake_case")]
46pub enum ForwardPassPerfSource {
47 /// Strict native AIC estimator with no correction workload kind ready yet.
48 Aic,
49 /// Workload-specific regression fallback, used without native AIC support.
50 FallbackRegression,
51 /// Native AIC estimator with at least one learned correction workload kind.
52 AicWithCorrection,
53}
54
55/// Readiness state reported by `ForwardPassPerfDiagnostics`.
56#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
57#[serde(rename_all = "snake_case")]
58pub enum ForwardPassPerfReadiness {
59 /// The model has either native AIC support or enough learned data.
60 Ready,
61 /// Regression fallback exists, but does not yet have enough observations.
62 InsufficientData,
63 /// Native AIC was unavailable and `best_available` fell back to regression.
64 UnsupportedConfig,
65 /// Reserved for callers that surface rejected FPM input as diagnostics.
66 InvalidInput,
67}
68
69/// Forward-pass-level performance model with optional online tuning.
70///
71/// This API intentionally stays at AIC's forward-pass abstraction. It does not
72/// model TTFT, ITL, SLA, engine capacity, queueing policy, or Dynamo engine
73/// limits. Callers pass FPMs for one engine iteration and receive one
74/// forward-pass latency estimate in milliseconds.
75///
76/// The prefill/decode/mixed workload kind is inferred from each iteration's
77/// `scheduled_requests` fields; it is not chosen at construction:
78///
79/// - prefill: scheduled prefill tokens and no scheduled decode work, using
80/// `[sum_prefill_tokens]`
81/// - decode: scheduled decode work and no scheduled prefill tokens, using
82/// `[num_decode_requests, sum_decode_kv_tokens]`
83/// - mixed/agg: both scheduled prefill and decode work, using
84/// `[sum_prefill_tokens, sum_decode_kv_tokens]`
85/// - empty: no scheduled prefill or decode work, estimates `0.0` and is not
86/// used for tuning
87///
88/// Native correction grids use fixed constructor-time ranges from
89/// `ForwardPassPerfOptions`: `max_num_tokens` bounds `sum_prefill_tokens`,
90/// `max_batch_size` bounds `num_decode_requests`, and `max_kv_tokens` bounds
91/// `sum_decode_kv_tokens`. `min_faster_correction_factor` and
92/// `max_slower_correction_factor` place independent absolute bounds on learned
93/// native correction factors in each direction, defaulting to `0.5` and `2.0`.
94/// Callers may explicitly disable either bound.
95///
96/// Queued request fields are accepted for FPM schema parity but ignored by this
97/// forward-pass-level model. `estimate_forward_pass_time_ms` treats FPM as a
98/// workload descriptor: it uses scheduled workload fields and ignores
99/// `wall_time`. `tune_with_fpms` treats FPM as observed telemetry: it uses the
100/// same scheduled workload fields as features and uses positive `wall_time` as
101/// the observation target. For attention-DP configurations, the input for one
102/// iteration is one FPM per attention-DP rank; tuning merges that list into one
103/// observation by taking max-rank load features and max nonzero `wall_time`.
104#[derive(Clone, Debug)]
105pub struct ForwardPassPerfModel {
106 mode: ForwardPassPerfMode,
107 options: ForwardPassPerfOptions,
108 last_warning: Option<String>,
109}
110
111#[derive(Clone, Debug)]
112enum ForwardPassPerfMode {
113 Native {
114 /// Compiled engine. `Arc` so `ForwardPassPerfModel` stays `Clone`
115 /// (the `Engine` itself is not `Clone`); cheap clones share the loaded
116 /// op lists + perf-database tree.
117 engine: Arc<Engine>,
118 corrections: WorkloadStores<CorrectionBuckets>,
119 },
120 Regression {
121 regressions: WorkloadStores<BucketedRegression>,
122 },
123}
124
125impl ForwardPassPerfModel {
126 /// API:
127 /// `ForwardPassPerfModel::from_native(config, options) -> Result<Self, AicError>`
128 ///
129 /// Description: create a strict native AIC forward-pass model.
130 ///
131 /// Compiles `config` into an [`Engine`] by crossing into Python once
132 /// (mirroring [`crate::AicEngineBuilder`]): `compile_engine` walks the model
133 /// and returns bincoded spec bytes, then [`Engine::from_spec_bytes`] loads
134 /// the matching perf database. This constructor fails if `config` cannot be
135 /// compiled. Use `best_available` when unsupported native configs should
136 /// fall back to the learned regression model.
137 pub fn from_native(
138 config: EngineConfig,
139 options: ForwardPassPerfOptions,
140 ) -> Result<Self, AicError> {
141 validate_options(&options)?;
142 let engine = build_engine_via_python(&config, None)?;
143 Ok(Self::from_engine(Arc::new(engine), options))
144 }
145
146 /// API:
147 /// `ForwardPassPerfModel::from_native_with_roots(config, options, systems_root) -> Result<Self, AicError>`
148 ///
149 /// Description: create a strict native AIC forward-pass model with an
150 /// explicit `systems/` data root (forwarded to `compile_engine` and used to
151 /// load the perf database). Same tuning and failure behavior as
152 /// `from_native`.
153 pub fn from_native_with_roots(
154 config: EngineConfig,
155 options: ForwardPassPerfOptions,
156 systems_root: impl AsRef<Path>,
157 ) -> Result<Self, AicError> {
158 validate_options(&options)?;
159 let engine = build_engine_via_python(&config, Some(systems_root.as_ref()))?;
160 Ok(Self::from_engine(Arc::new(engine), options))
161 }
162
163 /// Internal: build a native model directly from an already-compiled
164 /// [`Engine`]. Holds the actual native-mode logic; the public `from_native`
165 /// constructors compile the `Engine` (crossing into Python) and call this.
166 /// Used by the `#[cfg(test)]` suite to construct a native model from a
167 /// hand-built fixture `Engine` without Python.
168 pub(crate) fn from_engine(engine: Arc<Engine>, options: ForwardPassPerfOptions) -> Self {
169 Self {
170 mode: ForwardPassPerfMode::Native {
171 engine,
172 corrections: WorkloadStores::with_options(&options),
173 },
174 options,
175 last_warning: None,
176 }
177 }
178
179 /// API:
180 /// `ForwardPassPerfModel::from_regression(options) -> Result<Self, AicError>`
181 ///
182 /// Description: create a regression-only forward-pass model.
183 ///
184 /// This mode is for native-AIC-unsupported models. It returns `None` from
185 /// `estimate_forward_pass_time_ms` for non-empty iterations until the
186 /// inferred workload kind has at least `options.min_observations` tuning samples.
187 /// Correction factor getters always return `None` in this mode.
188 pub fn from_regression(options: ForwardPassPerfOptions) -> Result<Self, AicError> {
189 validate_options(&options)?;
190 Ok(Self {
191 mode: ForwardPassPerfMode::Regression {
192 regressions: WorkloadStores::with_options(&options),
193 },
194 options,
195 last_warning: None,
196 })
197 }
198
199 /// API:
200 /// `ForwardPassPerfModel::best_available(config, options) -> Result<Self, AicError>`
201 ///
202 /// Description: create a native model when possible, otherwise fall back to
203 /// regression.
204 ///
205 /// Fallback reason is preserved in `diagnostics().last_warning`. The
206 /// resulting model still uses the same FPM workload-kind inference and
207 /// tuning input contract as `from_native` and `from_regression`.
208 pub fn best_available(
209 config: EngineConfig,
210 options: ForwardPassPerfOptions,
211 ) -> Result<Self, AicError> {
212 match Self::from_native(config, options.clone()) {
213 Ok(model) => Ok(model),
214 Err(err) if can_fallback_to_regression(&err) => {
215 Self::regression_with_warning(options, err)
216 }
217 Err(err) => Err(err),
218 }
219 }
220
221 /// API:
222 /// `ForwardPassPerfModel::best_available_with_roots(config, options, systems_root) -> Result<Self, AicError>`
223 ///
224 /// Description: create a `best_available` model with an explicit `systems/`
225 /// data root.
226 pub fn best_available_with_roots(
227 config: EngineConfig,
228 options: ForwardPassPerfOptions,
229 systems_root: impl AsRef<Path>,
230 ) -> Result<Self, AicError> {
231 match Self::from_native_with_roots(config, options.clone(), systems_root) {
232 Ok(model) => Ok(model),
233 Err(err) if can_fallback_to_regression(&err) => {
234 Self::regression_with_warning(options, err)
235 }
236 Err(err) => Err(err),
237 }
238 }
239
240 fn regression_with_warning(
241 options: ForwardPassPerfOptions,
242 err: AicError,
243 ) -> Result<Self, AicError> {
244 let mut model = Self::from_regression(options)?;
245 model.last_warning = Some(format!(
246 "native forward-pass estimator unavailable; using fallback regression: {err}"
247 ));
248 Ok(model)
249 }
250
251 /// API:
252 /// `model.estimate_forward_pass_time_ms(metrics_by_rank) -> Result<Option<f64>, AicError>`
253 ///
254 /// Description: estimate one forward-pass iteration in milliseconds.
255 ///
256 /// `metrics_by_rank` must contain the FPMs for a single engine iteration,
257 /// one entry per attention-DP rank. Single-rank callers pass a one-element
258 /// slice. The inferred workload kind uses only `scheduled_requests` as described on
259 /// `ForwardPassPerfModel`; queued fields and `wall_time` are ignored for
260 /// estimation.
261 ///
262 /// Native models return an AIC estimate immediately, multiplied by the
263 /// correction factor for the matching workload region. Correction factors
264 /// default to `1.0` for inferred workload kinds with fewer than
265 /// `min_observations` total samples, empty regions, and queries outside the
266 /// configured correction-grid workload ranges in
267 /// `ForwardPassPerfOptions`. Regression models return `Ok(None)` until the
268 /// matching inferred workload kind has enough tuning samples. Empty
269 /// scheduled work returns `Ok(Some(0.0))`.
270 ///
271 /// Pure Rust over the `Engine` — no Python re-entry.
272 pub fn estimate_forward_pass_time_ms(
273 &self,
274 metrics_by_rank: &[ForwardPassMetrics],
275 ) -> Result<Option<f64>, AicError> {
276 let feature = IterationFeatures::from_metrics(metrics_by_rank)?;
277 let Some(feature) = feature else {
278 return Ok(Some(0.0));
279 };
280
281 match &self.mode {
282 ForwardPassPerfMode::Native {
283 engine,
284 corrections,
285 } => {
286 let native = engine.forward_pass_time_ms(metrics_by_rank)?;
287 let corrected = native
288 * corrections
289 .store(feature.workload_kind)
290 .correction_factor_for(&feature.x);
291 Ok(Some(corrected))
292 }
293 ForwardPassPerfMode::Regression { regressions } => {
294 Ok(regressions.store(feature.workload_kind).predict(&feature.x))
295 }
296 }
297 }
298
299 /// API:
300 /// `model.tune_with_fpms(iterations) -> Result<(), AicError>`
301 ///
302 /// Description: tune the model from observed FPM iterations.
303 ///
304 /// The outer slice is a list of observed iterations. Each inner slice is
305 /// the per-attention-DP-rank FPM list for one iteration:
306 /// `[[iter0_rank0, iter0_rank1], [iter1_rank0, iter1_rank1]]`.
307 /// Single-rank callers still use one FPM per inner slice.
308 ///
309 /// For each non-empty iteration, this method infers the workload kind from
310 /// scheduled request fields, takes max-rank load features, and uses the max
311 /// finite positive `wall_time` across ranks as the observed latency target
312 /// in milliseconds. Iterations with no scheduled work or no positive
313 /// `wall_time` are ignored. Native models update the matching region's
314 /// median `observed_ms / native_ms` correction factor, with each ratio
315 /// bounded by `min_faster_correction_factor` and
316 /// `max_slower_correction_factor` when configured. Regions are used only
317 /// after their inferred workload kind has `min_observations` total samples;
318 /// empty regions keep the default factor `1.0`. Observations outside the
319 /// configured correction-grid workload ranges are ignored by native
320 /// correction models. Regression models learn a workload-specific linear
321 /// fit.
322 ///
323 /// Pure Rust over the `Engine` — no Python re-entry.
324 pub fn tune_with_fpms(
325 &mut self,
326 iterations: &[Vec<ForwardPassMetrics>],
327 ) -> Result<(), AicError> {
328 for metrics_by_rank in iterations {
329 let observation = IterationObservation::from_metrics(metrics_by_rank)?;
330 let Some(observation) = observation else {
331 continue;
332 };
333
334 match &mut self.mode {
335 ForwardPassPerfMode::Native {
336 engine,
337 corrections,
338 } => {
339 let native = engine.forward_pass_time_ms(metrics_by_rank)?;
340 corrections
341 .store_mut(observation.feature.workload_kind)
342 .add_observation(observation.feature.x, observation.wall_time_ms, native);
343 }
344 ForwardPassPerfMode::Regression { regressions } => {
345 regressions
346 .store_mut(observation.feature.workload_kind)
347 .add_observation(observation.feature.x, observation.wall_time_ms);
348 }
349 }
350 }
351 Ok(())
352 }
353
354 /// API:
355 /// `model.diagnostics() -> ForwardPassPerfDiagnostics`
356 ///
357 /// Description: return the current backend, readiness, retained sample
358 /// count, and fallback warning.
359 pub fn diagnostics(&self) -> ForwardPassPerfDiagnostics {
360 match &self.mode {
361 ForwardPassPerfMode::Native { corrections, .. } => {
362 let ready_buckets = corrections.ready_bucket_count();
363 ForwardPassPerfDiagnostics {
364 source: if ready_buckets > 0 {
365 ForwardPassPerfSource::AicWithCorrection
366 } else {
367 ForwardPassPerfSource::Aic
368 },
369 readiness: ForwardPassPerfReadiness::Ready,
370 retained_observations: corrections.observation_count(),
371 correction_ready_buckets: ready_buckets,
372 last_warning: self.last_warning.clone(),
373 }
374 }
375 ForwardPassPerfMode::Regression { regressions } => {
376 let ready = regressions.any_ready();
377 ForwardPassPerfDiagnostics {
378 source: ForwardPassPerfSource::FallbackRegression,
379 readiness: if ready {
380 ForwardPassPerfReadiness::Ready
381 } else if self.last_warning.is_some() {
382 ForwardPassPerfReadiness::UnsupportedConfig
383 } else {
384 ForwardPassPerfReadiness::InsufficientData
385 },
386 retained_observations: regressions.observation_count(),
387 correction_ready_buckets: 0,
388 last_warning: self.last_warning.clone(),
389 }
390 }
391 }
392 }
393
394 /// API:
395 /// `model.min_correction_factor() -> Option<f64>`
396 ///
397 /// Description: return the smallest ready native correction factor across
398 /// all workload kinds.
399 ///
400 /// Returns `None` before any native correction workload kind has enough samples.
401 /// Regression-only models also return `None`.
402 pub fn min_correction_factor(&self) -> Option<f64> {
403 self.correction_factors()
404 .into_iter()
405 .reduce(|a, b| a.min(b))
406 }
407
408 /// API:
409 /// `model.max_correction_factor() -> Option<f64>`
410 ///
411 /// Description: return the largest ready native correction factor across
412 /// all workload kinds.
413 ///
414 /// Returns `None` before any native correction workload kind has enough samples.
415 /// Regression-only models also return `None`.
416 pub fn max_correction_factor(&self) -> Option<f64> {
417 self.correction_factors()
418 .into_iter()
419 .reduce(|a, b| a.max(b))
420 }
421
422 /// API:
423 /// `model.avg_correction_factor() -> Option<f64>`
424 ///
425 /// Description: return the arithmetic mean of ready native correction
426 /// factors across all workload kinds.
427 ///
428 /// Returns `None` before any native correction workload kind has enough samples.
429 /// Regression-only models also return `None`.
430 pub fn avg_correction_factor(&self) -> Option<f64> {
431 let factors = self.correction_factors();
432 if factors.is_empty() {
433 None
434 } else {
435 Some(factors.iter().sum::<f64>() / factors.len() as f64)
436 }
437 }
438
439 /// API:
440 /// `model.options() -> &ForwardPassPerfOptions`
441 ///
442 /// Description: return the immutable tuning options used by this model.
443 pub fn options(&self) -> &ForwardPassPerfOptions {
444 &self.options
445 }
446
447 fn correction_factors(&self) -> Vec<f64> {
448 match &self.mode {
449 ForwardPassPerfMode::Native { corrections, .. } => corrections.correction_factors(),
450 ForwardPassPerfMode::Regression { .. } => Vec::new(),
451 }
452 }
453}
454
455/// Build a compiled [`Engine`] from an [`EngineConfig`] by crossing into Python
456/// once to run `aiconfigurator.sdk.engine.compile_engine`, then loading the
457/// matching perf database via [`Engine::from_spec_bytes`]. This is the internal
458/// `EngineConfig` counterpart to [`crate::AicEngineBuilder`] and maps its
459/// modular fields onto the flat `compile_engine` kwargs.
460///
461/// `systems_root` overrides the bundled `systems/` dir for BOTH the
462/// `compile_engine` call (`systems_path` kwarg) and the Rust-side perf-DB load.
463fn build_engine_via_python(
464 config: &EngineConfig,
465 systems_root: Option<&Path>,
466) -> Result<Engine, AicError> {
467 // `compile_engine`'s `systems_path` kwarg: explicit override -> config's
468 // own `systems_path` -> None (Python resolves it).
469 let systems_path: Option<PathBuf> = systems_root
470 .map(PathBuf::from)
471 .or_else(|| config.systems_path.clone());
472 // A non-UTF-8 override path cannot be passed through the Python kwarg; fail
473 // loudly rather than silently dropping the override.
474 let systems_path_str = match systems_path.as_ref() {
475 Some(p) => Some(p.to_str().ok_or_else(|| {
476 AicError::InvalidEngineConfig(format!(
477 "systems_path is not valid UTF-8: {}",
478 p.display()
479 ))
480 })?),
481 None => None,
482 };
483
484 crate::py::compile_engine_to_engine(config, systems_path_str)
485}
486
487#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
488pub(crate) enum WorkloadKind {
489 Prefill,
490 Decode,
491 Mixed,
492}
493
494#[derive(Clone, Debug)]
495pub(crate) struct IterationFeatures {
496 pub(crate) workload_kind: WorkloadKind,
497 pub(crate) x: Vec<f64>,
498}
499
500impl IterationFeatures {
501 pub(crate) fn from_metrics(
502 metrics_by_rank: &[ForwardPassMetrics],
503 ) -> Result<Option<Self>, AicError> {
504 if metrics_by_rank.is_empty() {
505 return Err(AicError::InvalidForwardPassMetrics(
506 "at least one attention-DP rank metric is required".to_string(),
507 ));
508 }
509 for metrics in metrics_by_rank {
510 validate_forward_pass_metrics(metrics)?;
511 }
512
513 Ok(metrics_by_rank
514 .iter()
515 .filter_map(Self::from_single_rank)
516 .max_by(|left, right| {
517 left.load_score()
518 .partial_cmp(&right.load_score())
519 .unwrap_or(std::cmp::Ordering::Equal)
520 }))
521 }
522
523 fn from_single_rank(metrics: &ForwardPassMetrics) -> Option<Self> {
524 let scheduled = &metrics.scheduled_requests;
525 let has_prefill = scheduled.sum_prefill_tokens > 0;
526 let has_decode = scheduled.num_decode_requests > 0 || scheduled.sum_decode_kv_tokens > 0;
527 let feature = match (has_prefill, has_decode) {
528 (false, false) => return None,
529 (true, false) => Self {
530 workload_kind: WorkloadKind::Prefill,
531 x: vec![f64::from(scheduled.sum_prefill_tokens)],
532 },
533 (false, true) => Self {
534 workload_kind: WorkloadKind::Decode,
535 x: vec![
536 f64::from(scheduled.num_decode_requests),
537 f64::from(scheduled.sum_decode_kv_tokens),
538 ],
539 },
540 (true, true) => Self {
541 workload_kind: WorkloadKind::Mixed,
542 x: vec![
543 f64::from(scheduled.sum_prefill_tokens),
544 f64::from(scheduled.sum_decode_kv_tokens),
545 ],
546 },
547 };
548 Some(feature)
549 }
550
551 fn load_score(&self) -> f64 {
552 self.x.iter().sum()
553 }
554}
555
556#[derive(Clone, Debug)]
557pub(crate) struct IterationObservation {
558 pub(crate) feature: IterationFeatures,
559 pub(crate) wall_time_ms: f64,
560}
561
562impl IterationObservation {
563 pub(crate) fn from_metrics(
564 metrics_by_rank: &[ForwardPassMetrics],
565 ) -> Result<Option<Self>, AicError> {
566 let Some(feature) = IterationFeatures::from_metrics(metrics_by_rank)? else {
567 return Ok(None);
568 };
569 let wall_time = metrics_by_rank
570 .iter()
571 .map(|metrics| metrics.wall_time)
572 .filter(|wall_time| wall_time.is_finite() && *wall_time > 0.0)
573 .fold(0.0_f64, f64::max);
574 if wall_time <= 0.0 {
575 return Ok(None);
576 }
577 Ok(Some(Self {
578 feature,
579 wall_time_ms: wall_time * 1000.0,
580 }))
581 }
582}
583
584#[derive(Clone, Debug)]
585pub(crate) struct WorkloadStores<T> {
586 prefill: T,
587 decode: T,
588 mixed: T,
589}
590
591impl<T: WithOptions> WorkloadStores<T> {
592 fn with_options(options: &ForwardPassPerfOptions) -> Self {
593 Self {
594 prefill: T::with_options(options, &[AxisRange::from_zero_to(options.max_num_tokens)]),
595 decode: T::with_options(
596 options,
597 &[
598 AxisRange::from_zero_to(options.max_batch_size),
599 AxisRange::from_zero_to(options.max_kv_tokens),
600 ],
601 ),
602 mixed: T::with_options(
603 options,
604 &[
605 AxisRange::from_zero_to(options.max_num_tokens),
606 AxisRange::from_zero_to(options.max_kv_tokens),
607 ],
608 ),
609 }
610 }
611}
612
613impl<T: StoreStats> WorkloadStores<T> {
614 fn observation_count(&self) -> usize {
615 self.prefill.observation_count()
616 + self.decode.observation_count()
617 + self.mixed.observation_count()
618 }
619
620 fn any_ready(&self) -> bool {
621 self.prefill.is_ready() || self.decode.is_ready() || self.mixed.is_ready()
622 }
623}
624
625impl WorkloadStores<CorrectionBuckets> {
626 fn ready_bucket_count(&self) -> usize {
627 self.prefill.ready_bucket_count()
628 + self.decode.ready_bucket_count()
629 + self.mixed.ready_bucket_count()
630 }
631
632 fn correction_factors(&self) -> Vec<f64> {
633 let mut factors = self.prefill.correction_factors();
634 factors.extend(self.decode.correction_factors());
635 factors.extend(self.mixed.correction_factors());
636 factors
637 }
638}
639
640impl<T> WorkloadStores<T> {
641 fn store(&self, workload_kind: WorkloadKind) -> &T {
642 match workload_kind {
643 WorkloadKind::Prefill => &self.prefill,
644 WorkloadKind::Decode => &self.decode,
645 WorkloadKind::Mixed => &self.mixed,
646 }
647 }
648
649 fn store_mut(&mut self, workload_kind: WorkloadKind) -> &mut T {
650 match workload_kind {
651 WorkloadKind::Prefill => &mut self.prefill,
652 WorkloadKind::Decode => &mut self.decode,
653 WorkloadKind::Mixed => &mut self.mixed,
654 }
655 }
656}
657
658/// Decide whether `best_available` should fall back to regression instead of
659/// propagating `err`. Covers the unsupported-model / data-availability errors
660/// that mean "this model can't be served natively". A failed native build via
661/// Python `compile_engine` surfaces as [`AicError::UnsupportedModel`] (see
662/// `py::compile_engine_from_flat`), which is covered here.
663///
664/// [`AicError::InvalidEngineConfig`] is deliberately NOT fallback-safe: it is
665/// used for hard caller/config errors (e.g. a non-UTF-8 `systems_path`, invalid
666/// FPM options, a malformed spec). Those must surface rather than silently
667/// degrade `best_available` to regression mode.
668fn can_fallback_to_regression(err: &AicError) -> bool {
669 matches!(
670 err,
671 AicError::UnsupportedModel(_)
672 | AicError::DataRoot(_)
673 | AicError::ModelConfig(_)
674 | AicError::PerfDatabase(_)
675 | AicError::Io { .. }
676 | AicError::Parquet { .. }
677 )
678}