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}