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`).
62// - 14 (PR #1533): `GdnOp` gained `mamba_ssm_dtype` — a positional bincode
63//   op-layout change (the serde default covers JSON only).
64// - 15 (AIC-1715/1716): `Context/GenerationAttentionOp` gained `lane_order`
65//   (appended at the struct tail; always serialized — bincode decodes
66//   positionally). Concurrently claimed v8, v9, v10, and v12 on its own
67//   branch (v8 alongside #1503's v7/v8, v9 alongside #1461's
68//   `Op::FpmForward` v9, v10 alongside issue #1498's Mhc `seq_split` v10,
69//   v12 alongside PR-6's `DsaModuleOp` `attn_projection_quant_modes` v12,
70//   and v14 alongside #1533's `GdnOp::mamba_ssm_dtype` v14); each landed
71//   first, so this renumbers to 15 at merge (same v3/v4, v5/v6 precedent).
72pub const ENGINE_SPEC_SCHEMA_VERSION: u32 = 15;
73
74/// Static engine identity and setup information carried by an
75/// [`crate::perfmodel::engine::spec::EngineSpec`].
76///
77/// Cohesive multi-field groupings (`parallel`, `quantization`,
78/// `speculative`) are extracted into sub-structs but `#[serde(flatten)]`-ed
79/// so the wire JSON stays flat. Python (`sdk/engine.py`) emits a flat object
80/// with keys like `tp_size`, `weight_dtype`, `nextn`, which deserialize into
81/// the regrouped struct unchanged.
82#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
83pub struct EngineConfig {
84    pub schema_version: u32,
85
86    // Model
87    pub model_name: String,
88
89    // System
90    pub system_name: String,
91    /// Optional override for the bundled `systems/` directory. `None` (the
92    /// default) uses the resolution path baked into the build/env.
93    #[serde(default)]
94    pub systems_path: Option<PathBuf>,
95
96    // Backend
97    pub backend: BackendKind,
98    pub backend_version: Option<String>,
99
100    /// Forward-pass modeling mode (`"op_level"` | `"fpm"`); `None` keeps
101    /// Python's default (op_level). Threaded to `compile_engine` so the FPM
102    /// arena can select the whole-model engine through the supported
103    /// predictor API (additive-optional: absent in older payloads).
104    #[serde(default)]
105    pub forward_model: Option<String>,
106
107    // KV
108    pub kv_block_size: Option<u32>,
109
110    // Cohesive groupings (multi-field, semantically coupled).
111    #[serde(flatten)]
112    pub parallel: ParallelMapping,
113    #[serde(flatten)]
114    pub quantization: QuantizationConfig,
115    #[serde(flatten)]
116    pub speculative: Option<SpeculativeConfig>,
117
118    /// Shared-layer (sibling/cross-version) source inheritance on/off. The
119    /// engine resolves per-op sources ITSELF (`perf_database/source_resolution.rs`
120    /// — schema v13; the resolved `perf_db_sources` map left the wire with the
121    /// Python resolver). `None` derives the flag from `database_mode`
122    /// (SILICON/HYBRID = on), mirroring Python `_shared_layer_enabled`;
123    /// `Some` carries an explicit override (Python's `shared_layer=` kwarg,
124    /// used by regression harnesses to pin per-version behavior).
125    #[serde(default)]
126    pub enable_shared_layer: Option<bool>,
127
128    /// Fail-closed provenance mode (Python's `strict_provenance` /
129    /// `AIC_STRICT_PROVENANCE`): malformed sidecar metadata errors the load
130    /// instead of warn-and-continue. Absent on old specs -> false.
131    #[serde(default)]
132    pub strict_provenance: bool,
133
134    /// Perf-database lookup mode (Python's `database._default_database_mode`).
135    /// SILICON queries collected tables only; HYBRID falls back to the
136    /// util-space empirical layer on a typed silicon miss; EMPIRICAL always
137    /// answers `SOL/util`. Absent on old specs -> Silicon (back-compat).
138    #[serde(default)]
139    pub database_mode: crate::common::enums::DatabaseMode,
140
141    /// Directory-less `next` load (design §14). Set by the Python spec
142    /// builder ONLY after it resolved the requested version to the
143    /// fleet-advertised `next` slot and loaded it without a local version
144    /// directory (every op rides channel-1 backward fill). Tells the engine
145    /// reload to skip the missing-directory gate for THIS spec; raw-version
146    /// and provenance gates are untouched, and native builders never set it
147    /// (additive-optional: absent in older payloads -> false).
148    #[serde(default)]
149    pub tolerate_dirless_version: bool,
150
151    /// Enabled empirical transfer kinds as explicit tokens (`xshape` /
152    /// `xquant` / `xprofile` / `xop`). Python resolves preset names before
153    /// serialising, so no preset vocabulary exists on the wire. `None` =
154    /// the default ALL-transfers policy (mirrors `common.ALL_TRANSFERS`).
155    #[serde(default)]
156    pub transfer_policy: Option<Vec<String>>,
157
158    #[serde(default)]
159    pub extra: BTreeMap<String, String>,
160}
161
162/// Per-op-file ordered source list, keyed by op-file basename. See
163/// [`EngineConfig::perf_db_sources`].
164pub type PerfDbSources = BTreeMap<String, Vec<PerfSource>>;
165
166/// One perf-data source: an absolute file path plus an optional
167/// `kernel_source` allowlist. `None` admits every row (the primary source);
168/// `Some(set)` keeps only rows whose `kernel_source` is in the set (sibling
169/// inheritance). Wire form is a 2-element JSON array `[path, [ks...] | null]`.
170#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
171pub struct PerfSource(pub PathBuf, pub Option<Vec<String>>);
172
173impl PerfSource {
174    pub fn path(&self) -> &std::path::Path {
175        &self.0
176    }
177    pub fn kernel_sources(&self) -> Option<&[String]> {
178        self.1.as_deref()
179    }
180}
181
182/// Parallelism layout. Flattened into [`EngineConfig`] so the flat wire keys
183/// (`tp_size`, `pp_size`, ...) parse unchanged.
184#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
185pub struct ParallelMapping {
186    pub tp_size: u32,
187    pub pp_size: u32,
188    #[serde(default)]
189    pub attention_dp_size: Option<u32>,
190    #[serde(default)]
191    pub moe_tp_size: Option<u32>,
192    #[serde(default)]
193    pub moe_ep_size: Option<u32>,
194    /// Context-parallel size. Part of the engine identity so cp variants get
195    /// distinct compiled handles. `None`/1 means no CP. The per-op CP math is
196    /// carried on the ops themselves (seq_split / cp_size / attn_cp_size), not
197    /// re-derived from this field.
198    #[serde(default)]
199    pub cp_size: Option<u32>,
200}
201
202/// Precision/quantization dtypes. Flattened into [`EngineConfig`]. Field
203/// names and types are unchanged from the former flat struct so the flat
204/// wire keys (`weight_dtype`, `moe_dtype`, ...) parse unchanged.
205#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
206pub struct QuantizationConfig {
207    pub weight_dtype: Option<DataType>,
208    #[serde(default)]
209    pub moe_dtype: Option<DataType>,
210    pub activation_dtype: Option<DataType>,
211    pub kv_cache_dtype: Option<DataType>,
212}
213
214/// Multi-Token Prediction speculative-decoding parameters. Wrapped in
215/// `Option<>` on [`EngineConfig`] so models without MTP don't carry the
216/// noise, and `#[serde(flatten)]`-ed so the flat wire key (`nextn`) parses
217/// unchanged. Accepted-token progress is modeled above `aic-core`.
218#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
219pub struct SpeculativeConfig {
220    /// Multi-Token Prediction speculative decoding depth / draft length
221    /// (Python's `task_config.nextn`). `None`/0 disables MTP scaling. MTP is
222    /// never auto-enabled; the user opts in explicitly.
223    #[serde(default)]
224    pub nextn: Option<u32>,
225}
226
227/// Backend performance database family.
228#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
229#[serde(rename_all = "lowercase")]
230pub enum BackendKind {
231    Trtllm,
232    Sglang,
233    Vllm,
234}
235
236impl BackendKind {
237    pub(crate) fn as_str(&self) -> &'static str {
238        match self {
239            Self::Trtllm => "trtllm",
240            Self::Sglang => "sglang",
241            Self::Vllm => "vllm",
242        }
243    }
244}
245
246/// Precision/quantization dtypes carried on the engine-config wire.
247#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
248#[serde(rename_all = "snake_case")]
249pub enum DataType {
250    #[serde(rename = "bfloat16")]
251    Bfloat16,
252    #[serde(rename = "float16")]
253    Float16,
254    #[serde(rename = "fp8")]
255    Fp8,
256    #[serde(rename = "fp8_static")]
257    Fp8Static,
258    #[serde(rename = "fp8_block")]
259    Fp8Block,
260    #[serde(rename = "nvfp4")]
261    Nvfp4,
262    #[serde(rename = "int8")]
263    Int8,
264    #[serde(rename = "int4")]
265    Int4,
266    #[serde(rename = "w4afp8")]
267    W4afp8,
268    #[serde(rename = "w4a16_mxfp4")]
269    W4a16Mxfp4,
270    #[serde(rename = "w4a8_mxfp4_mxfp8")]
271    W4a8Mxfp4Mxfp8,
272    #[serde(rename = "w4a8_mxfp4_mxfp8_trtllm")]
273    W4a8Mxfp4Mxfp8Trtllm,
274    #[serde(rename = "w4a16_mxfp4_cutlass")]
275    W4a16Mxfp4Cutlass,
276    // Append-only wire extension: keep existing bincode discriminants stable.
277    #[serde(rename = "w4a16_nvfp4")]
278    W4a16Nvfp4,
279}
280
281#[cfg(test)]
282mod engine_config_wire_tests {
283    use super::*;
284
285    /// Python's `compile_engine` (`sdk/engine.py`) emits a flat JSON object.
286    /// The regrouped `EngineConfig` uses `#[serde(flatten)]` to keep that wire
287    /// contract. This guards that the flat shape - including explicit nulls and
288    /// the now-dropped `model_arch` key - still deserializes into the nested
289    /// struct.
290    #[test]
291    fn flat_python_payload_deserializes_into_regrouped_config() {
292        let json = r#"{
293            "schema_version": 1,
294            "model_name": "Qwen/Qwen3-32B",
295            "model_arch": "Qwen3ForCausalLM",
296            "system_name": "h200_sxm",
297            "backend": "trtllm",
298            "backend_version": "1.0.0",
299            "tp_size": 2,
300            "pp_size": 1,
301            "moe_tp_size": null,
302            "moe_ep_size": null,
303            "attention_dp_size": null,
304            "weight_dtype": "bfloat16",
305            "moe_dtype": null,
306            "activation_dtype": "bfloat16",
307            "kv_cache_dtype": "bfloat16",
308            "kv_block_size": null,
309            "nextn": null,
310            "extra": {}
311        }"#;
312
313        let config: EngineConfig = serde_json::from_str(json).expect("flat payload must parse");
314
315        // Parallelism regrouping.
316        assert_eq!(config.parallel.tp_size, 2);
317        assert_eq!(config.parallel.pp_size, 1);
318        assert_eq!(config.parallel.attention_dp_size, None);
319        assert_eq!(config.parallel.moe_tp_size, None);
320        assert_eq!(config.parallel.moe_ep_size, None);
321
322        // Quantization regrouping.
323        assert_eq!(config.quantization.weight_dtype, Some(DataType::Bfloat16));
324        assert_eq!(config.quantization.moe_dtype, None);
325
326        // Speculative: Python always emits the `nextn` key, so the flattened
327        // option is `Some` with inner `None` (MTP disabled), not `None`.
328        let nextn = config.speculative.as_ref().and_then(|s| s.nextn);
329        assert_eq!(nextn, None);
330
331        // `model_arch` was dropped; the stray key must be ignored, not rejected.
332        assert!(!config.extra.contains_key("model_arch"));
333        assert_eq!(config.model_name, "Qwen/Qwen3-32B");
334
335        // `systems_path` is new and defaults to None when absent.
336        assert_eq!(config.systems_path, None);
337    }
338}