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}