use std::path::{Path, PathBuf};
use std::sync::Arc;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::sync::GILOnceCell;
use pyo3::types::PyType;
use crate::common::error::AicError;
use crate::perfmodel::EngineConfig;
use crate::perfmodel::engine::runtime::{
DEFAULT_STATIC_STRIDE, Engine, PerOpSolValue, PerOpValue, RuntimeConfig, StaticMode,
StaticResult,
};
use crate::{BackendKind, DataType, ENGINE_CONFIG_SCHEMA_VERSION};
#[pyfunction]
fn _build_smoke() -> u32 {
ENGINE_CONFIG_SCHEMA_VERSION
}
static PERF_DATA_NOT_AVAILABLE_ERROR: GILOnceCell<Py<PyType>> = GILOnceCell::new();
static EMPIRICAL_NOT_IMPLEMENTED_ERROR: GILOnceCell<Py<PyType>> = GILOnceCell::new();
static MISSING_SYSTEM_FLOPS_ERROR: GILOnceCell<Py<PyType>> = GILOnceCell::new();
fn sdk_error_type(
py: Python<'_>,
cell: &'static GILOnceCell<Py<PyType>>,
name: &str,
) -> Option<Py<PyType>> {
cell.get_or_try_init(py, || -> PyResult<Py<PyType>> {
Ok(py
.import("aiconfigurator_core.sdk.errors")?
.getattr(name)?
.downcast_into::<PyType>()?
.unbind())
})
.ok()
.map(|ty| ty.clone_ref(py))
}
fn aic_to_py(e: AicError) -> PyErr {
let sdk_class: Option<(&'static GILOnceCell<Py<PyType>>, &str)> = if e.is_missing_perf_data() {
Some((&PERF_DATA_NOT_AVAILABLE_ERROR, "PerfDataNotAvailableError"))
} else if matches!(e, AicError::EmpiricalNotImplemented(_)) {
Some((
&EMPIRICAL_NOT_IMPLEMENTED_ERROR,
"EmpiricalNotImplementedError",
))
} else if matches!(e, AicError::MissingSystemFlops(_)) {
Some((&MISSING_SYSTEM_FLOPS_ERROR, "MissingSystemFlopsError"))
} else {
None
};
let message = e.to_string();
if let Some((cell, name)) = sdk_class {
let typed = Python::with_gil(|py| {
sdk_error_type(py, cell, name)
.map(|ty| PyErr::from_type(ty.into_bound(py), message.clone()))
});
if let Some(err) = typed {
return err;
}
}
PyValueError::new_err(message)
}
fn parse_mode(mode: &str) -> PyResult<StaticMode> {
match mode {
"static" => Ok(StaticMode::Both),
"static_ctx" => Ok(StaticMode::Context),
"static_gen" => Ok(StaticMode::Generation),
other => Err(PyValueError::new_err(format!(
"invalid mode {other:?}; expected one of \"static\", \"static_ctx\", \"static_gen\""
))),
}
}
fn resolve_systems_root(systems_path: Option<&str>) -> PyResult<PathBuf> {
if let Some(p) = systems_path {
return Ok(PathBuf::from(p));
}
if let Some(p) = std::env::var_os("AICONFIGURATOR_SYSTEMS_PATH") {
return Ok(PathBuf::from(p));
}
let installed_root = Python::with_gil(|py| -> PyResult<Option<PathBuf>> {
let Ok(perf_database) = py.import("aiconfigurator_core.sdk.perf_database") else {
return Ok(None);
};
let paths: Vec<String> = perf_database.call_method0("get_systems_paths")?.extract()?;
Ok(paths.into_iter().next().map(PathBuf::from))
})?;
if let Some(p) = installed_root {
return Ok(p);
}
crate::repo_relative("python/aisimulate/src/aiconfigurator_core/systems").ok_or_else(|| {
PyValueError::new_err(
"could not resolve systems path: pass systems_path, set \
AICONFIGURATOR_SYSTEMS_PATH, install aisimulate, or run \
from an AIC checkout",
)
})
}
#[pyclass(name = "AicEngine")]
pub struct AicEngine {
inner: Arc<Engine>,
}
impl AicEngine {
fn new(engine: Engine) -> Self {
AicEngine {
inner: Arc::new(engine),
}
}
pub fn prefill_latency_ms(&self, bs: u32, isl: u32, prefix: u32) -> Result<f64, AicError> {
self.inner.predict_prefill_latency(bs, isl, prefix)
}
pub fn decode_latency_ms(&self, bs: u32, isl: u32, osl: u32) -> Result<f64, AicError> {
self.inner.predict_decode_latency(bs, isl, osl)
}
}
#[pymethods]
impl AicEngine {
#[staticmethod]
#[pyo3(signature = (bytes, systems_path=None))]
fn from_spec(bytes: &[u8], systems_path: Option<&str>) -> PyResult<AicEngine> {
let systems_root = resolve_systems_root(systems_path)?;
let engine = Python::with_gil(|py| {
py.allow_threads(|| Engine::from_spec_bytes(bytes, &systems_root))
})
.map_err(aic_to_py)?;
Ok(AicEngine::new(engine))
}
#[pyo3(signature = (
batch_size,
beam_width,
isl,
osl,
prefix,
seq_imbalance_correction_scale,
gen_seq_imbalance_correction_scale,
mode="static",
stride=DEFAULT_STATIC_STRIDE,
))]
#[allow(clippy::too_many_arguments)]
fn run_static(
&self,
py: Python<'_>,
batch_size: u32,
beam_width: u32,
isl: u32,
osl: u32,
prefix: u32,
seq_imbalance_correction_scale: f64,
gen_seq_imbalance_correction_scale: f64,
mode: &str,
stride: u32,
) -> PyResult<(f64, f64, f64)> {
let rt = RuntimeConfig {
batch_size,
beam_width,
isl,
osl,
prefix,
seq_imbalance_correction_scale,
gen_seq_imbalance_correction_scale,
};
let mode = parse_mode(mode)?;
self.inner.reset_provenance();
let result: StaticResult = py
.allow_threads(|| self.inner.run_static(&rt, mode, stride))
.map_err(aic_to_py)?;
Ok((result.context_ms, result.generation_ms, result.total_ms))
}
fn last_provenance(&self) -> Option<&'static str> {
self.inner.last_provenance()
}
#[pyo3(signature = (bs, isl, prefix=0))]
fn predict_prefill_latency(
&self,
py: Python<'_>,
bs: u32,
isl: u32,
prefix: u32,
) -> PyResult<f64> {
self.inner.reset_provenance();
py.allow_threads(|| self.inner.predict_prefill_latency(bs, isl, prefix))
.map_err(aic_to_py)
}
#[pyo3(signature = (bs, isl, osl=2))]
fn predict_decode_latency(&self, py: Python<'_>, bs: u32, isl: u32, osl: u32) -> PyResult<f64> {
self.inner.reset_provenance();
py.allow_threads(|| self.inner.predict_decode_latency(bs, isl, osl))
.map_err(aic_to_py)
}
#[pyo3(signature = (ctx_tokens, gen_tokens, isl, osl, prefix=0,
seq_imbalance_correction_scale=1.0,
gen_seq_imbalance_correction_scale=1.0))]
#[allow(clippy::too_many_arguments)]
fn mixed_step_latency(
&self,
py: Python<'_>,
ctx_tokens: u32,
gen_tokens: u32,
isl: u32,
osl: u32,
prefix: u32,
seq_imbalance_correction_scale: f64,
gen_seq_imbalance_correction_scale: f64,
) -> PyResult<f64> {
self.inner.reset_provenance();
py.allow_threads(|| {
self.inner.mixed_step_latency(
ctx_tokens,
gen_tokens,
isl,
osl,
prefix,
seq_imbalance_correction_scale,
gen_seq_imbalance_correction_scale,
)
})
.map_err(aic_to_py)
}
#[pyo3(signature = (ctx_tokens, gen_tokens, isl, osl, prefix=0, seq_imbalance_correction_scale=1.0, gen_seq_imbalance_correction_scale=1.0))]
#[allow(clippy::too_many_arguments)]
fn mixed_step_breakdown(
&self,
py: Python<'_>,
ctx_tokens: u32,
gen_tokens: u32,
isl: u32,
osl: u32,
prefix: u32,
seq_imbalance_correction_scale: f64,
gen_seq_imbalance_correction_scale: f64,
) -> PyResult<(f64, f64, f64, f64)> {
self.inner.reset_provenance();
py.allow_threads(|| {
self.inner.mixed_step_breakdown(
ctx_tokens,
gen_tokens,
isl,
osl,
prefix,
seq_imbalance_correction_scale,
gen_seq_imbalance_correction_scale,
)
})
.map(|parts| (parts[0], parts[1], parts[2], parts[3]))
.map_err(aic_to_py)
}
#[pyo3(signature = (gen_tokens, isl, osl, gen_seq_imbalance_correction_scale=1.0))]
fn decode_step_latency(
&self,
py: Python<'_>,
gen_tokens: u32,
isl: u32,
osl: u32,
gen_seq_imbalance_correction_scale: f64,
) -> PyResult<f64> {
self.inner.reset_provenance();
py.allow_threads(|| {
self.inner
.decode_step_latency(gen_tokens, isl, osl, gen_seq_imbalance_correction_scale)
})
.map_err(aic_to_py)
}
#[pyo3(signature = (
batch_size,
beam_width,
isl,
osl,
prefix,
seq_imbalance_correction_scale,
gen_seq_imbalance_correction_scale,
mode="static",
stride=DEFAULT_STATIC_STRIDE,
))]
#[allow(clippy::too_many_arguments)]
fn run_static_per_op(
&self,
py: Python<'_>,
batch_size: u32,
beam_width: u32,
isl: u32,
osl: u32,
prefix: u32,
seq_imbalance_correction_scale: f64,
gen_seq_imbalance_correction_scale: f64,
mode: &str,
stride: u32,
) -> PyResult<(Vec<PerOpValue>, Vec<PerOpValue>)> {
let rt = RuntimeConfig {
batch_size,
beam_width,
isl,
osl,
prefix,
seq_imbalance_correction_scale,
gen_seq_imbalance_correction_scale,
};
let mode = parse_mode(mode)?;
self.inner.reset_provenance();
py.allow_threads(|| self.inner.run_static_per_op(&rt, mode, stride))
.map_err(aic_to_py)
}
#[pyo3(signature = (ctx_tokens, gen_tokens, isl, osl, prefix=0,
seq_imbalance_correction_scale=1.0,
gen_seq_imbalance_correction_scale=1.0))]
#[allow(clippy::too_many_arguments)]
fn mixed_step_breakdown_per_op(
&self,
py: Python<'_>,
ctx_tokens: u32,
gen_tokens: u32,
isl: u32,
osl: u32,
prefix: u32,
seq_imbalance_correction_scale: f64,
gen_seq_imbalance_correction_scale: f64,
) -> PyResult<(Vec<PerOpValue>, Vec<PerOpValue>, Vec<PerOpValue>)> {
self.inner.reset_provenance();
py.allow_threads(|| {
self.inner.mixed_step_breakdown_per_op(
ctx_tokens,
gen_tokens,
isl,
osl,
prefix,
seq_imbalance_correction_scale,
gen_seq_imbalance_correction_scale,
)
})
.map_err(aic_to_py)
}
#[pyo3(signature = (gen_tokens, isl, osl, gen_seq_imbalance_correction_scale=1.0))]
fn decode_step_per_op(
&self,
py: Python<'_>,
gen_tokens: u32,
isl: u32,
osl: u32,
gen_seq_imbalance_correction_scale: f64,
) -> PyResult<Vec<PerOpValue>> {
self.inner.reset_provenance();
py.allow_threads(|| {
self.inner
.decode_step_per_op(gen_tokens, isl, osl, gen_seq_imbalance_correction_scale)
})
.map_err(aic_to_py)
}
#[pyo3(signature = (indices, batch_size, s, prefix=0, seq_imbalance_correction_scale=1.0, x=None))]
#[allow(clippy::too_many_arguments)]
fn evaluate_context_ops(
&self,
py: Python<'_>,
indices: Vec<usize>,
batch_size: u32,
s: u32,
prefix: u32,
seq_imbalance_correction_scale: f64,
x: Option<u32>,
) -> PyResult<Vec<PerOpValue>> {
self.inner.reset_provenance();
py.allow_threads(|| {
self.inner.evaluate_context_ops(
&indices,
batch_size,
s,
prefix,
seq_imbalance_correction_scale,
x,
)
})
.map_err(aic_to_py)
}
#[pyo3(signature = (indices, batch_size, s, gen_seq_imbalance_correction_scale=1.0, prefix=0, x=None))]
#[allow(clippy::too_many_arguments)]
fn evaluate_generation_ops(
&self,
py: Python<'_>,
indices: Vec<usize>,
batch_size: u32,
s: u32,
gen_seq_imbalance_correction_scale: f64,
prefix: u32,
x: Option<u32>,
) -> PyResult<Vec<PerOpValue>> {
self.inner.reset_provenance();
py.allow_threads(|| {
self.inner.evaluate_generation_ops(
&indices,
batch_size,
s,
gen_seq_imbalance_correction_scale,
prefix,
x,
)
})
.map_err(aic_to_py)
}
#[pyo3(signature = (ops_json, is_context, batch_size, s, prefix=0, imbalance_correction_scale=1.0, x=None))]
#[allow(clippy::too_many_arguments)]
fn evaluate_ops_json(
&self,
py: Python<'_>,
ops_json: &str,
is_context: bool,
batch_size: u32,
s: u32,
prefix: u32,
imbalance_correction_scale: f64,
x: Option<u32>,
) -> PyResult<Vec<PerOpValue>> {
self.inner.reset_provenance();
py.allow_threads(|| {
self.inner.evaluate_ops_json(
ops_json,
is_context,
batch_size,
s,
prefix,
imbalance_correction_scale,
x,
)
})
.map_err(aic_to_py)
}
#[pyo3(signature = (ops_json, is_context, batch_size, s, prefix=0, imbalance_correction_scale=1.0, x=None))]
#[allow(clippy::too_many_arguments)]
fn evaluate_ops_sol_json(
&self,
py: Python<'_>,
ops_json: &str,
is_context: bool,
batch_size: u32,
s: u32,
prefix: u32,
imbalance_correction_scale: f64,
x: Option<u32>,
) -> PyResult<Vec<PerOpSolValue>> {
self.inner.reset_provenance();
py.allow_threads(|| {
self.inner.evaluate_ops_sol_json(
ops_json,
is_context,
batch_size,
s,
prefix,
imbalance_correction_scale,
x,
)
})
.map_err(aic_to_py)
}
fn table_view_json(&self, py: Python<'_>, attribute: &str) -> PyResult<Option<String>> {
py.allow_threads(|| {
crate::perf_database::table_view::table_view_json(self.inner.database(), attribute)
})
.map_err(aic_to_py)
}
}
#[pyfunction]
fn engine_spec_bincode_from_json(spec_json: &str) -> PyResult<Vec<u8>> {
let spec: crate::perfmodel::engine::spec::EngineSpec = serde_json::from_str(spec_json)
.map_err(|e| PyValueError::new_err(format!("engine spec JSON decode: {e}")))?;
spec.to_bincode().map_err(aic_to_py)
}
#[pyfunction]
fn weights_ops_json(ops_json: &str) -> PyResult<Vec<f64>> {
let ops: Vec<crate::operators::Op> = serde_json::from_str(ops_json)
.map_err(|e| PyValueError::new_err(format!("ops JSON decode: {e}")))?;
Ok(ops.iter().map(crate::operators::Op::weight_bytes).collect())
}
#[pyfunction]
fn gemm_quant_util_levels() -> Vec<(f64, f64, f64)> {
crate::operators::gemm::GEMM_QUANT_UTIL_LEVEL.to_vec()
}
#[pyfunction]
fn moe_quant_util_levels() -> Vec<(f64, f64, f64)> {
crate::operators::moe::MOE_QUANT_UTIL_LEVEL.to_vec()
}
#[pyfunction]
fn table_view_attributes() -> Vec<(String, Vec<String>)> {
crate::perf_database::table_view::TABLE_VIEW_ATTRIBUTES
.iter()
.map(|(attribute, basenames)| {
(
attribute.to_string(),
basenames.iter().map(|b| b.to_string()).collect(),
)
})
.collect()
}
#[pyfunction]
#[pyo3(signature = (systems_root, system_data_root, backend, version, op_file_basename, primary_path=None, enable_shared_layer=true, strict=false))]
#[allow(clippy::too_many_arguments)]
fn resolve_op_sources_report_json(
py: Python<'_>,
systems_root: &str,
system_data_root: &str,
backend: &str,
version: &str,
op_file_basename: &str,
primary_path: Option<&str>,
enable_shared_layer: bool,
strict: bool,
) -> PyResult<String> {
let ctx = crate::perf_database::ResolveCtx {
systems_root: PathBuf::from(systems_root),
system_data_root: PathBuf::from(system_data_root),
backend: backend.to_string(),
version: version.to_string(),
enable_shared_layer,
strict,
};
let report = py
.allow_threads(|| {
crate::perf_database::resolve_one(
&ctx,
op_file_basename,
primary_path.map(std::path::Path::new),
)
})
.map_err(|e| PyValueError::new_err(e.to_string()))?;
serde_json::to_string(&report).map_err(|e| PyValueError::new_err(e.to_string()))
}
#[pyfunction]
fn engine_spec_schema_version() -> u32 {
crate::ENGINE_SPEC_SCHEMA_VERSION
}
#[pyfunction]
fn ops_json_from_ops(ops: &Bound<'_, PyAny>) -> PyResult<String> {
let ops = crate::py_ops::ops_from_sequence(ops)?;
crate::py_ops::reject_retired_ops(&ops).map_err(PyValueError::new_err)?;
serde_json::to_string(&ops).map_err(|e| PyValueError::new_err(e.to_string()))
}
#[pyfunction]
fn op_from_spec_json(py: Python<'_>, spec_json: &str) -> PyResult<Py<PyAny>> {
crate::py_ops::op_from_spec_json(py, spec_json)
}
#[derive(Clone, Debug)]
struct EngineBuildRequest {
model_path: String,
system: String,
backend: String,
backend_version: Option<String>,
tp_size: u32,
pp_size: u32,
attention_dp_size: u32,
moe_tp_size: Option<u32>,
moe_ep_size: Option<u32>,
gemm_quant_mode: Option<String>,
moe_quant_mode: Option<String>,
kvcache_quant_mode: Option<String>,
fmha_quant_mode: Option<String>,
comm_quant_mode: Option<String>,
nextn: u32,
kv_block_size: Option<u32>,
systems_path: Option<String>,
forward_model: Option<String>,
}
#[derive(Clone, Debug)]
pub struct AicEngineBuilder {
request: EngineBuildRequest,
}
impl AicEngineBuilder {
pub fn new(
model_path: impl Into<String>,
system: impl Into<String>,
backend: BackendKind,
) -> Self {
Self {
request: EngineBuildRequest {
model_path: model_path.into(),
system: system.into(),
backend: backend.as_str().to_owned(),
backend_version: None,
tp_size: 1,
pp_size: 1,
attention_dp_size: 1,
moe_tp_size: None,
moe_ep_size: None,
gemm_quant_mode: None,
moe_quant_mode: None,
kvcache_quant_mode: None,
fmha_quant_mode: None,
comm_quant_mode: None,
nextn: 0,
kv_block_size: None,
systems_path: None,
forward_model: None,
},
}
}
pub fn forward_model(mut self, forward_model: &str) -> Self {
self.request.forward_model = Some(forward_model.to_owned());
self
}
pub fn backend_version(mut self, value: impl Into<String>) -> Self {
self.request.backend_version = Some(value.into());
self
}
pub fn tp_size(mut self, value: u32) -> Self {
self.request.tp_size = value;
self
}
pub fn pp_size(mut self, value: u32) -> Self {
self.request.pp_size = value;
self
}
pub fn attention_dp_size(mut self, value: u32) -> Self {
self.request.attention_dp_size = value;
self
}
pub fn moe_parallelism(mut self, tp_size: Option<u32>, ep_size: Option<u32>) -> Self {
self.request.moe_tp_size = tp_size;
self.request.moe_ep_size = ep_size;
self
}
pub fn gemm_quant_mode(mut self, value: impl Into<String>) -> Self {
self.request.gemm_quant_mode = Some(value.into());
self
}
pub fn moe_quant_mode(mut self, value: impl Into<String>) -> Self {
self.request.moe_quant_mode = Some(value.into());
self
}
pub fn kvcache_quant_mode(mut self, value: impl Into<String>) -> Self {
self.request.kvcache_quant_mode = Some(value.into());
self
}
pub fn fmha_quant_mode(mut self, value: impl Into<String>) -> Self {
self.request.fmha_quant_mode = Some(value.into());
self
}
pub fn comm_quant_mode(mut self, value: impl Into<String>) -> Self {
self.request.comm_quant_mode = Some(value.into());
self
}
pub fn speculative_decoding(mut self, nextn: u32) -> Self {
self.request.nextn = nextn;
self
}
pub fn kv_block_size(mut self, value: u32) -> Self {
self.request.kv_block_size = Some(value);
self
}
pub fn systems_path(mut self, value: impl Into<String>) -> Self {
self.request.systems_path = Some(value.into());
self
}
pub fn build(self) -> Result<AicEngine, AicError> {
build_engine_from_request(self.request)
}
}
#[cfg(test)]
mod builder_tests {
use super::*;
#[test]
fn builder_defaults_match_compile_engine_defaults() {
let builder = AicEngineBuilder::new("model", "system", BackendKind::Vllm);
assert_eq!(builder.request.tp_size, 1);
assert_eq!(builder.request.pp_size, 1);
assert_eq!(builder.request.attention_dp_size, 1);
assert_eq!(builder.request.nextn, 0);
assert!(builder.request.backend_version.is_none());
assert!(builder.request.moe_tp_size.is_none());
assert!(builder.request.moe_ep_size.is_none());
assert!(builder.request.kv_block_size.is_none());
}
#[test]
fn builder_retains_explicit_options() {
let builder = AicEngineBuilder::new("model", "system", BackendKind::Sglang)
.backend_version("0.5.9")
.tp_size(8)
.pp_size(2)
.attention_dp_size(4)
.moe_parallelism(Some(1), Some(8))
.speculative_decoding(2)
.kv_block_size(16)
.systems_path("/tmp/systems");
assert_eq!(builder.request.backend, "sglang");
assert_eq!(builder.request.backend_version.as_deref(), Some("0.5.9"));
assert_eq!((builder.request.tp_size, builder.request.pp_size), (8, 2));
assert_eq!(builder.request.attention_dp_size, 4);
assert_eq!(
(builder.request.moe_tp_size, builder.request.moe_ep_size),
(Some(1), Some(8))
);
assert_eq!(builder.request.nextn, 2);
assert_eq!(builder.request.kv_block_size, Some(16));
assert_eq!(
builder.request.systems_path.as_deref(),
Some("/tmp/systems")
);
}
}
fn build_engine_from_request(request: EngineBuildRequest) -> Result<AicEngine, AicError> {
let engine = compile_engine_from_request(request)?;
Ok(AicEngine::new(engine))
}
fn compile_engine_from_request(request: EngineBuildRequest) -> Result<Engine, AicError> {
let systems_root = resolve_systems_root(request.systems_path.as_deref())
.map_err(|e| AicError::DataRoot(format!("resolve systems path: {e}")))?;
let systems_root_str = systems_root.to_str().ok_or_else(|| {
AicError::DataRoot(format!(
"systems path is not valid UTF-8: {}",
systems_root.display()
))
})?;
let spec_bytes: Vec<u8> = Python::with_gil(|py| -> PyResult<Vec<u8>> {
let engine_mod = py.import("aiconfigurator_core.sdk.engine")?;
let kwargs = pyo3::types::PyDict::new(py);
kwargs.set_item("backend_version", request.backend_version.as_deref())?;
kwargs.set_item("tp_size", request.tp_size)?;
kwargs.set_item("pp_size", request.pp_size)?;
kwargs.set_item("attention_dp_size", request.attention_dp_size)?;
kwargs.set_item("moe_tp_size", request.moe_tp_size)?;
kwargs.set_item("moe_ep_size", request.moe_ep_size)?;
kwargs.set_item("gemm_quant_mode", request.gemm_quant_mode.as_deref())?;
kwargs.set_item("moe_quant_mode", request.moe_quant_mode.as_deref())?;
kwargs.set_item("kvcache_quant_mode", request.kvcache_quant_mode.as_deref())?;
kwargs.set_item("fmha_quant_mode", request.fmha_quant_mode.as_deref())?;
kwargs.set_item("comm_quant_mode", request.comm_quant_mode.as_deref())?;
kwargs.set_item("forward_model", request.forward_model.as_deref())?;
kwargs.set_item("nextn", request.nextn)?;
kwargs.set_item("kv_block_size", request.kv_block_size)?;
kwargs.set_item("systems_path", systems_root_str)?;
engine_mod
.call_method(
"compile_engine",
(
request.model_path.as_str(),
request.system.as_str(),
request.backend.as_str(),
),
Some(&kwargs),
)?
.extract::<Vec<u8>>()
})
.map_err(|e| AicError::UnsupportedModel(format!("compile_engine: {e}")))?;
Engine::from_spec_bytes(&spec_bytes, systems_root.as_path() as &Path)
}
pub(crate) fn compile_engine_to_engine(
config: &EngineConfig,
systems_path: Option<&str>,
) -> Result<Engine, AicError> {
let nextn = config
.speculative
.as_ref()
.and_then(|s| s.nextn)
.unwrap_or(0);
compile_engine_from_request(EngineBuildRequest {
model_path: config.model_name.clone(),
system: config.system_name.clone(),
backend: config.backend.as_str().to_owned(),
backend_version: config.backend_version.clone(),
tp_size: config.parallel.tp_size,
pp_size: config.parallel.pp_size,
attention_dp_size: config.parallel.attention_dp_size.unwrap_or(1),
moe_tp_size: config.parallel.moe_tp_size,
moe_ep_size: config.parallel.moe_ep_size,
gemm_quant_mode: gemm_quant_name(config.quantization.weight_dtype.as_ref())
.map(str::to_owned),
moe_quant_mode: moe_quant_name(config.quantization.moe_dtype.as_ref()).map(str::to_owned),
kvcache_quant_mode: kvcache_quant_name(config.quantization.kv_cache_dtype.as_ref())
.map(str::to_owned),
fmha_quant_mode: fmha_quant_name(config.quantization.activation_dtype.as_ref())
.map(str::to_owned),
comm_quant_mode: None,
nextn,
kv_block_size: config.kv_block_size,
systems_path: systems_path.map(str::to_owned),
forward_model: config.forward_model.clone(),
})
}
fn gemm_quant_name(dtype: Option<&DataType>) -> Option<&'static str> {
match dtype? {
DataType::Bfloat16 => Some("bfloat16"),
DataType::Fp8 => Some("fp8"),
DataType::Fp8Static => Some("fp8_static"),
DataType::Fp8Block => Some("fp8_block"),
DataType::Nvfp4 => Some("nvfp4"),
DataType::Int8 => Some("int8_wo"),
DataType::Int4 => Some("int4_wo"),
DataType::W4a16Nvfp4 => Some("w4a16_nvfp4"),
_ => None,
}
}
fn moe_quant_name(dtype: Option<&DataType>) -> Option<&'static str> {
match dtype? {
DataType::Bfloat16 => Some("bfloat16"),
DataType::Fp8 => Some("fp8"),
DataType::Fp8Block => Some("fp8_block"),
DataType::Nvfp4 => Some("nvfp4"),
DataType::Int4 => Some("int4_wo"),
DataType::W4afp8 => Some("w4afp8"),
DataType::W4a16Mxfp4 => Some("w4a16_mxfp4"),
DataType::W4a8Mxfp4Mxfp8 => Some("w4a8_mxfp4_mxfp8"),
DataType::W4a16Nvfp4 => Some("w4a16_nvfp4"),
_ => None,
}
}
fn fmha_quant_name(dtype: Option<&DataType>) -> Option<&'static str> {
match dtype? {
DataType::Bfloat16 => Some("bfloat16"),
DataType::Fp8 => Some("fp8"),
DataType::Fp8Block => Some("fp8_block"),
_ => None,
}
}
fn kvcache_quant_name(dtype: Option<&DataType>) -> Option<&'static str> {
match dtype? {
DataType::Bfloat16 => Some("bfloat16"),
DataType::Int8 => Some("int8"),
DataType::Fp8 => Some("fp8"),
_ => None,
}
}
#[pyclass(name = "RustForwardPassPerfModel")]
pub struct PyForwardPassPerfModel {
inner: crate::ForwardPassPerfModel,
}
fn parse_fpm_options(options_json: Option<&str>) -> PyResult<crate::ForwardPassPerfOptions> {
match options_json {
None => Ok(crate::ForwardPassPerfOptions::default()),
Some(s) if s.trim().is_empty() => Ok(crate::ForwardPassPerfOptions::default()),
Some(s) => serde_json::from_str(s)
.map_err(|e| PyValueError::new_err(format!("invalid options JSON: {e}"))),
}
}
fn parse_fpm_iteration(fpm_json: &str) -> PyResult<Vec<crate::ForwardPassMetrics>> {
let value: serde_json::Value = serde_json::from_str(fpm_json)
.map_err(|e| PyValueError::new_err(format!("invalid FPM JSON: {e}")))?;
let metrics: Vec<crate::ForwardPassMetrics> = if value.is_array() {
serde_json::from_value(value)
} else {
serde_json::from_value(value).map(|m| vec![m])
}
.map_err(|e| PyValueError::new_err(format!("invalid FPM payload: {e}")))?;
Ok(metrics)
}
#[pymethods]
impl PyForwardPassPerfModel {
#[staticmethod]
#[pyo3(signature = (config_json, options_json=None))]
fn from_native(config_json: &str, options_json: Option<&str>) -> PyResult<Self> {
let config: EngineConfig = serde_json::from_str(config_json)
.map_err(|e| PyValueError::new_err(format!("invalid engine config JSON: {e}")))?;
let options = parse_fpm_options(options_json)?;
let inner = crate::ForwardPassPerfModel::from_native(config, options).map_err(aic_to_py)?;
Ok(Self { inner })
}
#[staticmethod]
#[pyo3(signature = (config_json, options_json=None))]
fn best_available(config_json: &str, options_json: Option<&str>) -> PyResult<Self> {
let config: EngineConfig = serde_json::from_str(config_json)
.map_err(|e| PyValueError::new_err(format!("invalid engine config JSON: {e}")))?;
let options = parse_fpm_options(options_json)?;
let inner =
crate::ForwardPassPerfModel::best_available(config, options).map_err(aic_to_py)?;
Ok(Self { inner })
}
#[staticmethod]
#[pyo3(signature = (options_json=None))]
fn from_regression(options_json: Option<&str>) -> PyResult<Self> {
let options = parse_fpm_options(options_json)?;
let inner = crate::ForwardPassPerfModel::from_regression(options).map_err(aic_to_py)?;
Ok(Self { inner })
}
fn estimate_forward_pass_time_ms(
&self,
py: Python<'_>,
fpm_json: &str,
) -> PyResult<Option<f64>> {
let metrics = parse_fpm_iteration(fpm_json)?;
py.allow_threads(|| self.inner.estimate_forward_pass_time_ms(&metrics))
.map_err(aic_to_py)
}
fn tune_with_fpms(&mut self, py: Python<'_>, iterations_json: &str) -> PyResult<()> {
let iterations: Vec<Vec<crate::ForwardPassMetrics>> = serde_json::from_str(iterations_json)
.map_err(|e| PyValueError::new_err(format!("invalid tuning iterations JSON: {e}")))?;
py.allow_threads(|| self.inner.tune_with_fpms(&iterations))
.map_err(aic_to_py)
}
fn diagnostics(&self) -> PyResult<String> {
serde_json::to_string(&self.inner.diagnostics())
.map_err(|e| PyValueError::new_err(format!("diagnostics serialize: {e}")))
}
fn min_correction_factor(&self) -> Option<f64> {
self.inner.min_correction_factor()
}
fn max_correction_factor(&self) -> Option<f64> {
self.inner.max_correction_factor()
}
fn avg_correction_factor(&self) -> Option<f64> {
self.inner.avg_correction_factor()
}
}
pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(_build_smoke, m)?)?;
m.add_function(wrap_pyfunction!(engine_spec_bincode_from_json, m)?)?;
m.add_function(wrap_pyfunction!(weights_ops_json, m)?)?;
m.add_function(wrap_pyfunction!(gemm_quant_util_levels, m)?)?;
m.add_function(wrap_pyfunction!(moe_quant_util_levels, m)?)?;
m.add_function(wrap_pyfunction!(table_view_attributes, m)?)?;
m.add_function(wrap_pyfunction!(resolve_op_sources_report_json, m)?)?;
m.add_function(wrap_pyfunction!(ops_json_from_ops, m)?)?;
m.add_function(wrap_pyfunction!(engine_spec_schema_version, m)?)?;
m.add_function(wrap_pyfunction!(op_from_spec_json, m)?)?;
m.add_class::<AicEngine>()?;
m.add_class::<PyForwardPassPerfModel>()?;
crate::perfmodel::py_ops::register(m)?;
Ok(())
}
#[cfg(all(test, feature = "embed-python"))]
mod tests {
use super::*;
use std::collections::BTreeMap;
use crate::common::enums::{FmhaQuantMode, GemmQuantMode, KvCacheQuantMode};
use crate::operators::op::Op;
use crate::operators::{ContextAttentionOp, ElementwiseOp, GemmOp, GenerationAttentionOp};
use crate::perfmodel::EngineConfig;
use crate::perfmodel::engine::spec::EngineSpec;
use crate::{BackendKind, ParallelMapping, QuantizationConfig};
fn systems_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../python/aisimulate/src/aiconfigurator_core/systems")
}
fn py_init() {
pyo3::prepare_freethreaded_python();
}
#[test]
fn w4a16_nvfp4_wire_dtype_maps_to_distinct_gemm_mode() {
assert_eq!(
gemm_quant_name(Some(&DataType::W4a16Nvfp4)),
Some("w4a16_nvfp4")
);
assert_ne!(
gemm_quant_name(Some(&DataType::W4a16Nvfp4)),
gemm_quant_name(Some(&DataType::Int4))
);
assert_eq!(
moe_quant_name(Some(&DataType::W4a16Nvfp4)),
Some("w4a16_nvfp4")
);
assert_ne!(
moe_quant_name(Some(&DataType::W4a16Nvfp4)),
moe_quant_name(Some(&DataType::Int4))
);
}
const TEST_MODEL: &str = "MiniMaxAI/MiniMax-M2.5";
fn context_ops() -> Vec<Op> {
vec![
Op::Elementwise(ElementwiseOp {
name: "rmsnorm".into(),
scale_factor: 1.0,
bytes_per_token: 8192.0,
scale_num_tokens: 1,
seq_split: 1,
}),
Op::Gemm(GemmOp {
name: "qkv_gemm".into(),
scale_factor: 1.0,
n: 4096,
k: 4096,
quant_mode: GemmQuantMode::Fp8Block,
scale_num_tokens: 0,
low_precision_input: false,
seq_split: 1,
below_grid_sol: false,
}),
Op::ContextAttention(ContextAttentionOp {
name: "context_attention".into(),
scale_factor: 1.0,
n: 32,
n_kv: 8,
head_size: 128,
window_size: 0,
kv_cache_dtype: KvCacheQuantMode::Fp8,
fmha_quant_mode: FmhaQuantMode::Bfloat16,
use_qk_norm: false,
cp_size: 1,
}),
]
}
fn generation_ops() -> Vec<Op> {
vec![
Op::Elementwise(ElementwiseOp {
name: "rmsnorm".into(),
scale_factor: 1.0,
bytes_per_token: 8192.0,
scale_num_tokens: 1,
seq_split: 1,
}),
Op::GenerationAttention(GenerationAttentionOp {
name: "generation_attention".into(),
scale_factor: 1.0,
n: 32,
n_kv: 8,
head_size: 128,
window_size: 0,
kv_cache_dtype: KvCacheQuantMode::Fp8,
}),
]
}
fn fixture_engine_config() -> EngineConfig {
EngineConfig {
schema_version: crate::ENGINE_CONFIG_SCHEMA_VERSION,
model_name: TEST_MODEL.to_string(),
system_name: "b200_sxm".to_string(),
systems_path: None,
backend: BackendKind::Vllm,
backend_version: Some("0.19.0".to_string()),
forward_model: None,
kv_block_size: None,
parallel: ParallelMapping {
tp_size: 8,
pp_size: 1,
attention_dp_size: Some(1),
moe_tp_size: Some(1),
moe_ep_size: Some(8),
cp_size: None,
},
quantization: QuantizationConfig {
weight_dtype: None,
moe_dtype: None,
activation_dtype: None,
kv_cache_dtype: None,
},
speculative: None,
enable_shared_layer: None,
strict_provenance: false,
database_mode: Default::default(),
transfer_policy: None,
extra: BTreeMap::new(),
}
}
fn fixture_spec_bytes() -> Vec<u8> {
let spec = EngineSpec::new(fixture_engine_config(), context_ops(), generation_ops());
spec.to_bincode().unwrap()
}
#[test]
fn aic_engine_matches_raw_engine() {
py_init();
let bytes = fixture_spec_bytes();
let root = systems_root();
let raw = Engine::from_spec_bytes(&bytes, &root).unwrap();
let aic = AicEngine::from_spec(&bytes, root.to_str()).unwrap();
let rt = RuntimeConfig {
batch_size: 1,
isl: 1024,
osl: 8,
..Default::default()
};
let raw_static = raw
.run_static(&rt, StaticMode::Both, DEFAULT_STATIC_STRIDE)
.unwrap();
let (ctx, generation, total) = Python::with_gil(|py| {
aic.run_static(
py,
1,
1,
1024,
8,
0,
1.0,
1.0,
"static",
DEFAULT_STATIC_STRIDE,
)
})
.unwrap();
assert!((ctx - raw_static.context_ms).abs() < 1e-12);
assert!((generation - raw_static.generation_ms).abs() < 1e-12);
assert!((total - raw_static.total_ms).abs() < 1e-12);
let raw_prefill = raw
.run_static(
&RuntimeConfig {
batch_size: 2,
isl: 1024,
osl: 1,
prefix: 0,
..Default::default()
},
StaticMode::Context,
DEFAULT_STATIC_STRIDE,
)
.unwrap()
.total_ms;
let prefill = Python::with_gil(|py| aic.predict_prefill_latency(py, 2, 1024, 0)).unwrap();
assert!((prefill - raw_prefill).abs() < 1e-12);
let raw_decode = raw
.run_static(
&RuntimeConfig {
batch_size: 4,
isl: 1024,
osl: 2,
..Default::default()
},
StaticMode::Generation,
DEFAULT_STATIC_STRIDE,
)
.unwrap()
.total_ms;
let decode = Python::with_gil(|py| aic.predict_decode_latency(py, 4, 1024, 2)).unwrap();
assert!((decode - raw_decode).abs() < 1e-12);
}
#[test]
fn mode_strings_map_correctly() {
py_init();
let bytes = fixture_spec_bytes();
let root = systems_root();
let aic = AicEngine::from_spec(&bytes, root.to_str()).unwrap();
Python::with_gil(|py| {
let ctx_only = aic
.run_static(py, 1, 1, 1024, 8, 0, 1.0, 1.0, "static_ctx", 32)
.unwrap();
assert!(ctx_only.0 > 0.0 && ctx_only.1 == 0.0);
let gen_only = aic
.run_static(py, 1, 1, 1024, 8, 0, 1.0, 1.0, "static_gen", 32)
.unwrap();
assert!(gen_only.0 == 0.0 && gen_only.1 > 0.0);
assert!(
aic.run_static(py, 1, 1, 1024, 8, 0, 1.0, 1.0, "bogus", 32)
.is_err()
);
});
}
#[test]
fn per_step_bindings_match_raw_engine() {
py_init();
let bytes = fixture_spec_bytes();
let root = systems_root();
let raw = Engine::from_spec_bytes(&bytes, &root).unwrap();
let aic = AicEngine::from_spec(&bytes, root.to_str()).unwrap();
let raw_mixed = raw
.mixed_step_latency(1024, 2, 1024, 8, 0, 1.0, 1.0)
.unwrap();
let mixed =
Python::with_gil(|py| aic.mixed_step_latency(py, 1024, 2, 1024, 8, 0, 1.0, 1.0))
.unwrap();
assert!((mixed - raw_mixed).abs() < 1e-12);
let raw_breakdown = raw
.mixed_step_breakdown(1024, 2, 1024, 8, 0, 1.0, 1.0)
.unwrap();
let breakdown =
Python::with_gil(|py| aic.mixed_step_breakdown(py, 1024, 2, 1024, 8, 0, 1.0, 1.0))
.unwrap();
assert_eq!(
breakdown,
(
raw_breakdown[0],
raw_breakdown[1],
raw_breakdown[2],
raw_breakdown[3],
)
);
let raw_decode = raw.decode_step_latency(4, 1024, 8, 1.0).unwrap();
let decode = Python::with_gil(|py| aic.decode_step_latency(py, 4, 1024, 8, 1.0)).unwrap();
assert!((decode - raw_decode).abs() < 1e-12);
}
#[test]
fn engine_spec_json_to_bincode_round_trips() {
let bytes = fixture_spec_bytes();
let original = EngineSpec::from_bincode(&bytes).unwrap();
let json = serde_json::to_string(&original).unwrap();
let out = engine_spec_bincode_from_json(&json).unwrap();
let decoded = EngineSpec::from_bincode(&out).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn inherent_predict_matches_raw_engine() {
py_init();
let bytes = fixture_spec_bytes();
let root = systems_root();
let raw = Engine::from_spec_bytes(&bytes, &root).unwrap();
let aic = AicEngine::from_spec(&bytes, root.to_str()).unwrap();
let raw_prefill = raw.predict_prefill_latency(2, 1024, 0).unwrap();
let aic_prefill = aic.prefill_latency_ms(2, 1024, 0).unwrap();
assert!((aic_prefill - raw_prefill).abs() < 1e-12);
assert!(aic_prefill > 0.0 && aic_prefill.is_finite());
let raw_decode = raw.predict_decode_latency(4, 1024, 2).unwrap();
let aic_decode = aic.decode_latency_ms(4, 1024, 2).unwrap();
assert!((aic_decode - raw_decode).abs() < 1e-12);
assert!(aic_decode > 0.0 && aic_decode.is_finite());
}
#[test]
fn aic_to_py_maps_typed_errors_to_sdk_classes() {
py_init();
Python::with_gil(|py| {
let sdk_available = py.import("aiconfigurator_core.sdk.errors").is_ok();
let check = |err: AicError, sdk_name: &str| {
let pyerr = aic_to_py(err);
let type_name = pyerr.get_type(py).name().unwrap().to_string();
if sdk_available {
assert_eq!(type_name, sdk_name);
} else {
assert_eq!(type_name, "ValueError");
}
};
check(
AicError::PerfDatabase("missing table".to_string()),
"PerfDataNotAvailableError",
);
check(
AicError::Io {
path: PathBuf::from("/nope"),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "nope"),
},
"PerfDataNotAvailableError",
);
check(
AicError::EmpiricalNotImplemented("no basis".to_string()),
"EmpiricalNotImplementedError",
);
let other = aic_to_py(AicError::InvalidEngineConfig("bad".to_string()));
assert_eq!(other.get_type(py).name().unwrap().to_string(), "ValueError");
let msg_err = aic_to_py(AicError::PerfDatabase("missing table xyz".to_string()));
assert!(msg_err.value(py).to_string().contains("missing table xyz"));
});
}
#[test]
fn compute_bindings_reset_provenance_per_call() {
use crate::operators::util_empirical::ProvenanceTier;
py_init();
let bytes = fixture_spec_bytes();
let root = systems_root();
let aic = AicEngine::from_spec(&bytes, root.to_str()).unwrap();
assert_eq!(aic.last_provenance(), None);
aic.inner.database().note_provenance(ProvenanceTier::XOp);
assert_eq!(aic.last_provenance(), Some("xop"));
Python::with_gil(|py| {
aic.run_static(py, 1, 1, 1024, 8, 0, 1.0, 1.0, "static", 32)
.unwrap();
});
assert_eq!(aic.last_provenance(), None);
aic.inner
.database()
.note_provenance(ProvenanceTier::Empirical);
Python::with_gil(|py| {
aic.mixed_step_latency(py, 1024, 2, 1024, 8, 0, 1.0, 1.0)
.unwrap();
});
assert_eq!(aic.last_provenance(), None);
aic.inner.database().note_provenance(ProvenanceTier::XShape);
Python::with_gil(|py| {
aic.decode_step_latency(py, 4, 1024, 8, 1.0).unwrap();
});
assert_eq!(aic.last_provenance(), None);
}
}