Skip to main content

aisimulate_core/perfmodel/
config.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Public wire/identity configuration types carried by an
5//! [`crate::perfmodel::engine::spec::EngineSpec`]. [`EngineConfig`] and its cohesive
6//! sub-structs ([`ParallelMapping`], [`QuantizationConfig`],
7//! [`SpeculativeConfig`]) mirror the flat JSON object emitted by Python's
8//! `compile_engine` (`sdk/engine.py`); [`BackendKind`] and [`DataType`] are
9//! the wire enums those structs reference. These are re-exported at the crate
10//! root, so `crate::EngineConfig`, `crate::BackendKind`, ... resolve unchanged.
11
12use std::collections::BTreeMap;
13use std::path::PathBuf;
14
15use serde::{Deserialize, Serialize};
16
17pub const ENGINE_CONFIG_SCHEMA_VERSION: u32 = 1;
18// bincode op payloads are positional, so a producer/consumer skew is only
19// distinguishable by this version — `EngineSpec::from_bincode` reads and
20// checks it before decoding the op lists. Bump whenever an `OpSpec` field
21// changes; keep in lockstep with `sdk/engine.py::ENGINE_SPEC_SCHEMA_VERSION`.
22// History:
23// - 2 (v0.10.0): op-payload layout change — the context-parallelism +
24//   perf-DB refactor added serialized fields such as `seq_split` /
25//   `cp_size` to `OpSpec`.
26// - 3 (PR #1405): MTP acceptance moved above aic-core —
27//   `nextn_accept_rates` removed from the spec payload.
28// - 4 (PR #1355): `Msa{Context,Generation}` variants inserted (bincode enum
29//   indices after `DsaGeneration` shifted). The MSA insertion and #1405
30//   each claimed version 3 on their own branch, so their merge needed a
31//   fresh number.
32// - 5 (PR #1460): MlaModuleOp gained `native_num_heads: Option<u32>`
33//   (#1458) — a bincode op-layout change.
34// - 6: `Kda` op variant appended (Kimi-K3). Claimed version 5 on its own
35//   branch concurrently with #1460, so the merge renumbered it (same
36//   precedent as the v3/v4 collision above).
37// - 7: `MoEDispatchOp` gained `attn_ar_modeled` — a bincode op-layout
38//   change (same class as v5).
39// - 8: `GemmOp` gained `below_grid_sol` — a bincode op-layout change (same
40//   class as v5).
41// - 9: `Op::FpmForward` whole-model variant added (forward_model="fpm").
42//   Claimed 5, 7 and 8 concurrently with other landings; renumbered at
43//   each merge (same precedent as the v3/v4 collision above).
44// - 10 (issue #1498): `MhcModuleOp` gained `seq_split` (CP per-rank token
45//   division) — a bincode op-layout change. Claimed 7 concurrently with
46//   `attn_ar_modeled`; renumbered at the rebase.
47// - 11 (AIC-1601): the wideEP MoE op variants (`WideEpMoe` /
48//   `WideEpMoeDispatch`) were removed mid-enum, shifting every later
49//   bincode enum index; large-EP is now modeled natively by the
50//   `MoeAllToAll` / `MoeExpertCompute` variants appended after
51//   `FpmForward`, and `MoeExpertComputeOp` carries the `enable_eplb`
52//   legacy-fidelity field.
53// - 12 (PR-6): `DsaModuleOp` gained `attn_projection_quant_modes` — a
54//   bincode op-layout change (same class as v5/v7/v8/v10; the
55//   `#[serde(default)]` only covers the JSON wire, bincode is positional).
56// - 13 (deprecation-cleanup PR): the engine owns shared-layer source
57//   resolution. `EngineConfig` dropped the Python-resolved
58//   `perf_db_sources` map (a bincode config-layout change) and gained
59//   `enable_shared_layer` / `strict_provenance` policy flags; the engine
60//   re-derives every table's source list from the perf-data tree
61//   (`perf_database/source_resolution.rs`).
62pub const ENGINE_SPEC_SCHEMA_VERSION: u32 = 13;
63
64/// Static engine identity and setup information carried by an
65/// [`crate::perfmodel::engine::spec::EngineSpec`].
66///
67/// Cohesive multi-field groupings (`parallel`, `quantization`,
68/// `speculative`) are extracted into sub-structs but `#[serde(flatten)]`-ed
69/// so the wire JSON stays flat. Python (`sdk/engine.py`) emits a flat object
70/// with keys like `tp_size`, `weight_dtype`, `nextn`, which deserialize into
71/// the regrouped struct unchanged.
72#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
73pub struct EngineConfig {
74    pub schema_version: u32,
75
76    // Model
77    pub model_name: String,
78
79    // System
80    pub system_name: String,
81    /// Optional override for the bundled `systems/` directory. `None` (the
82    /// default) uses the resolution path baked into the build/env.
83    #[serde(default)]
84    pub systems_path: Option<PathBuf>,
85
86    // Backend
87    pub backend: BackendKind,
88    pub backend_version: Option<String>,
89
90    /// Forward-pass modeling mode (`"op_level"` | `"fpm"`); `None` keeps
91    /// Python's default (op_level). Threaded to `compile_engine` so the FPM
92    /// arena can select the whole-model engine through the supported
93    /// predictor API (additive-optional: absent in older payloads).
94    #[serde(default)]
95    pub forward_model: Option<String>,
96
97    // KV
98    pub kv_block_size: Option<u32>,
99
100    // Cohesive groupings (multi-field, semantically coupled).
101    #[serde(flatten)]
102    pub parallel: ParallelMapping,
103    #[serde(flatten)]
104    pub quantization: QuantizationConfig,
105    #[serde(flatten)]
106    pub speculative: Option<SpeculativeConfig>,
107
108    /// Shared-layer (sibling/cross-version) source inheritance on/off. The
109    /// engine resolves per-op sources ITSELF (`perf_database/source_resolution.rs`
110    /// — schema v13; the resolved `perf_db_sources` map left the wire with the
111    /// Python resolver). `None` derives the flag from `database_mode`
112    /// (SILICON/HYBRID = on), mirroring Python `_shared_layer_enabled`;
113    /// `Some` carries an explicit override (Python's `shared_layer=` kwarg,
114    /// used by regression harnesses to pin per-version behavior).
115    #[serde(default)]
116    pub enable_shared_layer: Option<bool>,
117
118    /// Fail-closed provenance mode (Python's `strict_provenance` /
119    /// `AIC_STRICT_PROVENANCE`): malformed sidecar metadata errors the load
120    /// instead of warn-and-continue. Absent on old specs -> false.
121    #[serde(default)]
122    pub strict_provenance: bool,
123
124    /// Perf-database lookup mode (Python's `database._default_database_mode`).
125    /// SILICON queries collected tables only; HYBRID falls back to the
126    /// util-space empirical layer on a typed silicon miss; EMPIRICAL always
127    /// answers `SOL/util`. Absent on old specs -> Silicon (back-compat).
128    #[serde(default)]
129    pub database_mode: crate::common::enums::DatabaseMode,
130
131    /// Enabled empirical transfer kinds as explicit tokens (`xshape` /
132    /// `xquant` / `xprofile` / `xop`). Python resolves preset names before
133    /// serialising, so no preset vocabulary exists on the wire. `None` =
134    /// the default ALL-transfers policy (mirrors `common.ALL_TRANSFERS`).
135    #[serde(default)]
136    pub transfer_policy: Option<Vec<String>>,
137
138    #[serde(default)]
139    pub extra: BTreeMap<String, String>,
140}
141
142/// Per-op-file ordered source list, keyed by op-file basename. See
143/// [`EngineConfig::perf_db_sources`].
144pub type PerfDbSources = BTreeMap<String, Vec<PerfSource>>;
145
146/// One perf-data source: an absolute file path plus an optional
147/// `kernel_source` allowlist. `None` admits every row (the primary source);
148/// `Some(set)` keeps only rows whose `kernel_source` is in the set (sibling
149/// inheritance). Wire form is a 2-element JSON array `[path, [ks...] | null]`.
150#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
151pub struct PerfSource(pub PathBuf, pub Option<Vec<String>>);
152
153impl PerfSource {
154    pub fn path(&self) -> &std::path::Path {
155        &self.0
156    }
157    pub fn kernel_sources(&self) -> Option<&[String]> {
158        self.1.as_deref()
159    }
160}
161
162/// Parallelism layout. Flattened into [`EngineConfig`] so the flat wire keys
163/// (`tp_size`, `pp_size`, ...) parse unchanged.
164#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
165pub struct ParallelMapping {
166    pub tp_size: u32,
167    pub pp_size: u32,
168    #[serde(default)]
169    pub attention_dp_size: Option<u32>,
170    #[serde(default)]
171    pub moe_tp_size: Option<u32>,
172    #[serde(default)]
173    pub moe_ep_size: Option<u32>,
174    /// Context-parallel size. Part of the engine identity so cp variants get
175    /// distinct compiled handles. `None`/1 means no CP. The per-op CP math is
176    /// carried on the ops themselves (seq_split / cp_size / attn_cp_size), not
177    /// re-derived from this field.
178    #[serde(default)]
179    pub cp_size: Option<u32>,
180}
181
182/// Precision/quantization dtypes. Flattened into [`EngineConfig`]. Field
183/// names and types are unchanged from the former flat struct so the flat
184/// wire keys (`weight_dtype`, `moe_dtype`, ...) parse unchanged.
185#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
186pub struct QuantizationConfig {
187    pub weight_dtype: Option<DataType>,
188    #[serde(default)]
189    pub moe_dtype: Option<DataType>,
190    pub activation_dtype: Option<DataType>,
191    pub kv_cache_dtype: Option<DataType>,
192}
193
194/// Multi-Token Prediction speculative-decoding parameters. Wrapped in
195/// `Option<>` on [`EngineConfig`] so models without MTP don't carry the
196/// noise, and `#[serde(flatten)]`-ed so the flat wire key (`nextn`) parses
197/// unchanged. Accepted-token progress is modeled above `aic-core`.
198#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
199pub struct SpeculativeConfig {
200    /// Multi-Token Prediction speculative decoding depth / draft length
201    /// (Python's `task_config.nextn`). `None`/0 disables MTP scaling. MTP is
202    /// never auto-enabled; the user opts in explicitly.
203    #[serde(default)]
204    pub nextn: Option<u32>,
205}
206
207/// Backend performance database family.
208#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
209#[serde(rename_all = "lowercase")]
210pub enum BackendKind {
211    Trtllm,
212    Sglang,
213    Vllm,
214}
215
216impl BackendKind {
217    pub(crate) fn as_str(&self) -> &'static str {
218        match self {
219            Self::Trtllm => "trtllm",
220            Self::Sglang => "sglang",
221            Self::Vllm => "vllm",
222        }
223    }
224}
225
226/// Precision/quantization dtypes carried on the engine-config wire.
227#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
228#[serde(rename_all = "snake_case")]
229pub enum DataType {
230    #[serde(rename = "bfloat16")]
231    Bfloat16,
232    #[serde(rename = "float16")]
233    Float16,
234    #[serde(rename = "fp8")]
235    Fp8,
236    #[serde(rename = "fp8_static")]
237    Fp8Static,
238    #[serde(rename = "fp8_block")]
239    Fp8Block,
240    #[serde(rename = "nvfp4")]
241    Nvfp4,
242    #[serde(rename = "int8")]
243    Int8,
244    #[serde(rename = "int4")]
245    Int4,
246    #[serde(rename = "w4afp8")]
247    W4afp8,
248    #[serde(rename = "w4a16_mxfp4")]
249    W4a16Mxfp4,
250    #[serde(rename = "w4a8_mxfp4_mxfp8")]
251    W4a8Mxfp4Mxfp8,
252    #[serde(rename = "w4a8_mxfp4_mxfp8_trtllm")]
253    W4a8Mxfp4Mxfp8Trtllm,
254    #[serde(rename = "w4a16_mxfp4_cutlass")]
255    W4a16Mxfp4Cutlass,
256    // Append-only wire extension: keep existing bincode discriminants stable.
257    #[serde(rename = "w4a16_nvfp4")]
258    W4a16Nvfp4,
259}
260
261#[cfg(test)]
262mod engine_config_wire_tests {
263    use super::*;
264
265    /// Python's `compile_engine` (`sdk/engine.py`) emits a flat JSON object.
266    /// The regrouped `EngineConfig` uses `#[serde(flatten)]` to keep that wire
267    /// contract. This guards that the flat shape - including explicit nulls and
268    /// the now-dropped `model_arch` key - still deserializes into the nested
269    /// struct.
270    #[test]
271    fn flat_python_payload_deserializes_into_regrouped_config() {
272        let json = r#"{
273            "schema_version": 1,
274            "model_name": "Qwen/Qwen3-32B",
275            "model_arch": "Qwen3ForCausalLM",
276            "system_name": "h200_sxm",
277            "backend": "trtllm",
278            "backend_version": "1.0.0",
279            "tp_size": 2,
280            "pp_size": 1,
281            "moe_tp_size": null,
282            "moe_ep_size": null,
283            "attention_dp_size": null,
284            "weight_dtype": "bfloat16",
285            "moe_dtype": null,
286            "activation_dtype": "bfloat16",
287            "kv_cache_dtype": "bfloat16",
288            "kv_block_size": null,
289            "nextn": null,
290            "extra": {}
291        }"#;
292
293        let config: EngineConfig = serde_json::from_str(json).expect("flat payload must parse");
294
295        // Parallelism regrouping.
296        assert_eq!(config.parallel.tp_size, 2);
297        assert_eq!(config.parallel.pp_size, 1);
298        assert_eq!(config.parallel.attention_dp_size, None);
299        assert_eq!(config.parallel.moe_tp_size, None);
300        assert_eq!(config.parallel.moe_ep_size, None);
301
302        // Quantization regrouping.
303        assert_eq!(config.quantization.weight_dtype, Some(DataType::Bfloat16));
304        assert_eq!(config.quantization.moe_dtype, None);
305
306        // Speculative: Python always emits the `nextn` key, so the flattened
307        // option is `Some` with inner `None` (MTP disabled), not `None`.
308        let nextn = config.speculative.as_ref().and_then(|s| s.nextn);
309        assert_eq!(nextn, None);
310
311        // `model_arch` was dropped; the stray key must be ignored, not rejected.
312        assert!(!config.extra.contains_key("model_arch"));
313        assert_eq!(config.model_name, "Qwen/Qwen3-32B");
314
315        // `systems_path` is new and defaults to None when absent.
316        assert_eq!(config.systems_path, None);
317    }
318}